The ZKTeco Biometric Server includes built-in SSL/TLS support with automatic certificate generation, allowing you to run your API server over HTTPS with minimal configuration.
Overview
Both servidor.py (single-device) and server.py (multi-device) support:
Automatic SSL certificate generation using self-signed certificates
Custom SSL certificates for production environments
HTTP fallback when SSL is unavailable or disabled
SSL configuration is identical for both single-device and multi-device servers.
How SSL Auto-Generation Works
On server startup, the following logic executes:
Check for Existing Certificates
The server looks for cert.pem and key.pem in the working directory:
If both files exist → Use existing certificates
If either is missing → Proceed to generation
Generate Self-Signed Certificate
If pyopenssl is installed, the server generates:
RSA 2048-bit private key
X.509 self-signed certificate
Valid for 365 days
Common Name (CN): "ZKTeco Server" or SERVER_HOST (multi-device)
Files created:
cert.pem - Certificate file
key.pem - Private key file
Start in HTTPS or HTTP Mode
Certificate generation successful → Start with HTTPS on configured port
Generation failed or pyopenssl not installed → Fall back to HTTP
Configuration Methods
Method 1: Auto-Generated Certificates (Development)
The simplest method for development and testing:
Ensure pyopenssl is Installed
Or verify installation: python3 -c "import OpenSSL; print(OpenSSL.__version__)"
Start the Server
Single Device
Multi Device
Expected logs: 2026-03-06 10:30:00 [INFO] Certificados SSL generados.
2026-03-06 10:30:00 [INFO] Servidor ZKTeco en https://0.0.0.0:5000
Test HTTPS Connection
Self-signed certificates require the -k flag to skip verification: curl -k https://localhost:5000/
Or in browser, accept the security warning to proceed.
Self-signed certificates are NOT suitable for production . Browsers and clients will show security warnings. Use this method only for development and testing.
Method 2: Custom Certificates (Production)
Use proper SSL certificates from a Certificate Authority (CA) for production:
Obtain SSL Certificates
Let's Encrypt (Free)
Commercial CA
Corporate PKI
# Install certbot
sudo apt install certbot
# Generate certificate
sudo certbot certonly --standalone \
-d biometric.example.com \
--email [email protected] \
--agree-tos
# Certificates created at:
# /etc/letsencrypt/live/biometric.example.com/fullchain.pem
# /etc/letsencrypt/live/biometric.example.com/privkey.pem
Purchase from providers like:
DigiCert
Sectigo
GlobalSign
Follow their process to generate CSR and receive certificates. Request certificate from your organization’s IT security team using your internal Certificate Authority.
Configure Certificate Paths
Point the server to your certificates using environment variables: Linux/macOS
systemd Service
Docker Compose
export CERT_FILE = "/etc/letsencrypt/live/biometric.example.com/fullchain.pem"
export KEY_FILE = "/etc/letsencrypt/live/biometric.example.com/privkey.pem"
Set Proper Permissions
# Certificate files must be readable by the server user
sudo chmod 644 /etc/letsencrypt/live/biometric.example.com/fullchain.pem
sudo chmod 600 /etc/letsencrypt/live/biometric.example.com/privkey.pem
# For systemd service running as www-data
sudo chown www-data:www-data /etc/letsencrypt/live/biometric.example.com/privkey.pem
Start and Verify
Expected logs: 2026-03-06 10:30:00 [INFO] Certificados SSL encontrados.
2026-03-06 10:30:00 [INFO] Servidor ZKTeco en https://0.0.0.0:5000
Test without -k flag: curl https://biometric.example.com:5000/
Method 3: HTTP Mode (No SSL)
Run without SSL for internal networks or when using a reverse proxy:
Uninstall pyopenssl (Optional)
Start Server
Expected logs: 2026-03-06 10:30:00 [WARNING] pyopenssl no instalado. Corriendo en HTTP.
2026-03-06 10:30:00 [INFO] Servidor ZKTeco en http://0.0.0.0:5000
HTTP mode is suitable when using a reverse proxy (Nginx, Apache) that handles SSL termination.
Environment Variables Reference
Variable Default Description CERT_FILEcert.pemPath to SSL certificate file KEY_FILEkey.pemPath to SSL private key file
These variables are used by both servidor.py and server.py.
Certificate Generation Source Code
The auto-generation logic from the source:
def generar_certificado ():
if os.path.exists( CERT_FILE ) and os.path.exists( KEY_FILE ):
log.info( "Certificados SSL encontrados." )
return True
try :
from OpenSSL import crypto
k = crypto.PKey()
k.generate_key(crypto. TYPE_RSA , 2048 )
cert = crypto.X509()
cert.get_subject(). CN = "ZKTeco Server"
cert.set_serial_number( 1 )
cert.gmtime_adj_notBefore( 0 )
cert.gmtime_adj_notAfter( 365 * 24 * 60 * 60 ) # 1 year
cert.set_issuer(cert.get_subject())
cert.set_pubkey(k)
cert.sign(k, "sha256" )
with open ( CERT_FILE , "wb" ) as f:
f.write(crypto.dump_certificate(crypto. FILETYPE_PEM , cert))
with open ( KEY_FILE , "wb" ) as f:
f.write(crypto.dump_privatekey(crypto. FILETYPE_PEM , k))
log.info( "Certificados SSL generados." )
return True
except ImportError :
log.warning( "pyopenssl no instalado. Corriendo en HTTP." )
return False
except Exception as e:
log.error( f "Error SSL: { e } " )
return False
Production Deployment Patterns
Pattern 1: Reverse Proxy with SSL Termination
Recommended for production - handle SSL at the proxy level:
server {
listen 443 ssl http2;
server_name biometric.example.com;
ssl_certificate /etc/letsencrypt/live/biometric.example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/biometric.example.com/privkey.pem;
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers HIGH:!aNULL:!MD5;
location / {
proxy_pass http://127.0.0.1:5000;
proxy_set_header Host $ host ;
proxy_set_header X-Real-IP $ remote_addr ;
proxy_set_header X-Forwarded-For $ proxy_add_x_forwarded_for ;
proxy_set_header X-Forwarded-Proto $ scheme ;
}
}
# Redirect HTTP to HTTPS
server {
listen 80 ;
server_name biometric.example.com;
return 301 https://$ server_name $ request_uri ;
}
Run ZKTeco server in HTTP mode: # Don't install pyopenssl, let Nginx handle SSL
python server.py
< VirtualHost *:443 >
ServerName biometric.example.com
SSLEngine on
SSLCertificateFile /etc/letsencrypt/live/biometric.example.com/fullchain.pem
SSLCertificateKeyFile /etc/letsencrypt/live/biometric.example.com/privkey.pem
ProxyPreserveHost On
ProxyPass / http:// 127 . 0 . 0 . 1 : 5000 /
ProxyPassReverse / http:// 127 . 0 . 0 . 1 : 5000 /
</ VirtualHost >
< VirtualHost *:80 >
ServerName biometric.example.com
Redirect permanent / https://biometric.example.com/
</ VirtualHost >
Enable required modules: sudo a2enmod ssl proxy proxy_http
sudo systemctl restart apache2
biometric.example.com {
reverse_proxy localhost:5000
}
Caddy automatically obtains and renews Let’s Encrypt certificates!
Pattern 2: Direct SSL with Certificate Renewal
Run the server with direct SSL and automate certificate renewal:
Setup Let's Encrypt
sudo certbot certonly --standalone -d biometric.example.com
Configure Server to Use Certificates
Create environment file /etc/zkteco/ssl.env: CERT_FILE = /etc/letsencrypt/live/biometric.example.com/fullchain.pem
KEY_FILE = /etc/letsencrypt/live/biometric.example.com/privkey.pem
Update systemd service: [Service]
EnvironmentFile =/etc/zkteco/ssl.env
ExecStart =/usr/bin/python3 server.py
Setup Auto-Renewal Hook
Create /etc/letsencrypt/renewal-hooks/post/restart-zkteco.sh: #!/bin/bash
systemctl restart zkteco-multi
Make executable: sudo chmod +x /etc/letsencrypt/renewal-hooks/post/restart-zkteco.sh
Test renewal (dry run): sudo certbot renew --dry-run
Pattern 3: Internal PKI with Corporate Certificates
For enterprise environments with internal Certificate Authority:
Generate CSR
openssl req -new -newkey rsa:2048 -nodes \
-keyout biometric.key \
-out biometric.csr \
-subj "/C=US/ST=State/L=City/O=Company/CN=biometric.internal.company.com"
Submit CSR to Corporate CA
Submit biometric.csr to your IT security team for signing.
Install Signed Certificate
Receive signed certificate and CA chain, then: # Concatenate certificate with CA chain
cat biometric.crt intermediate.crt root.crt > fullchain.pem
# Configure server
export CERT_FILE = "/etc/pki/tls/certs/fullchain.pem"
export KEY_FILE = "/etc/pki/tls/private/biometric.key"
python server.py
Distribute CA Certificate
Client applications need to trust your corporate CA: # Add to system trust store
sudo cp root.crt /usr/local/share/ca-certificates/company-ca.crt
sudo update-ca-certificates
Common SSL Issues and Solutions
SSL Certificate Verification Failed
Error: SSL certificate problem: self signed certificateFor Development: # Skip verification (not recommended for production)
curl -k https://localhost:5000/
For Production:
Use certificates from a trusted CA (Let’s Encrypt, DigiCert, etc.)
Ensure certificate chain is complete
Verify certificate Common Name matches hostname
Permission Denied Reading Key File
Error: PermissionError: [Errno 13] Permission denied: '/etc/letsencrypt/live/...privkey.pem'Solution: # Option 1: Change file permissions
sudo chmod 644 /etc/letsencrypt/live/biometric.example.com/privkey.pem
# Option 2: Run as root (not recommended)
sudo python3 server.py
# Option 3: Add user to certificate group
sudo chown :www-data /etc/letsencrypt/live/biometric.example.com/privkey.pem
sudo chmod 640 /etc/letsencrypt/live/biometric.example.com/privkey.pem
sudo usermod -a -G www-data zkteco-user
Error: Browser console shows mixed content warningsSolution:
Ensure all API calls use https:// when page is loaded over HTTPS:// Don't hardcode protocol
// fetch('http://biometric.example.com/devices') ❌
// Use relative URL or match page protocol
fetch ( '/devices' ) ✅
fetch ( ` ${ window . location . protocol } //biometric.example.com/devices` ) ✅
Error: SSL certificate problem: certificate has expiredSolution: # Check certificate expiration
openssl x509 -in cert.pem -noout -dates
# For Let's Encrypt, renew
sudo certbot renew
sudo systemctl restart zkteco-multi
# For auto-generated, delete and regenerate
rm cert.pem key.pem
python server.py
Error: ModuleNotFoundError: No module named 'OpenSSL'Solution: # Install pyopenssl
pip install pyopenssl
# Or if server should run HTTP only, this is expected
# Server will fall back to HTTP mode automatically
Error: SSL: certificate subject name 'ZKTeco Server' does not match target host name 'biometric.example.com'Solution:
Auto-generated certificates use CN=“ZKTeco Server”
For production, use proper certificates with matching hostname
Or access via IP if using self-signed: https://192.168.1.100:5000/
Security Best Practices
Use Strong Protocols When using reverse proxy, disable older protocols: ssl_protocols TLSv1.2 TLSv1.3;
ssl_prefer_server_ciphers on ;
Regular Certificate Renewal Automate Let’s Encrypt renewal: # Runs twice daily
sudo systemctl enable certbot.timer
sudo systemctl start certbot.timer
Restrict Key File Access Private keys should be readable only by server: chmod 600 key.pem
chown www-data:www-data key.pem
Use HSTS Headers Force HTTPS with HTTP Strict Transport Security: add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
Certificate Validation
Verify your SSL configuration:
Test SSL Connection
Check Certificate Details
Verify Certificate Chain
Test with curl
openssl s_client -connect biometric.example.com:5000 -showcerts
Comparison: SSL Modes
Mode Use Case Security Setup Difficulty Cost Auto-generated Development, testing Low (self-signed) Very Easy Free Let’s Encrypt Production, internet-facing High Easy Free Commercial CA Enterprise production High Medium Paid Corporate PKI Internal enterprise High Medium Internal HTTP (no SSL) Behind reverse proxy, internal only Depends on proxy Very Easy Free
Next Steps
Environment Variables Complete configuration reference including SSL variables
Single Device Deployment Deploy with SSL enabled
Multi-Device Deployment Scale with secure HTTPS
API Reference Use the API over HTTPS