Skip to main content
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:
1

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
2

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
3

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:
1

Ensure pyopenssl is Installed

pip install pyopenssl
Or verify installation:
python3 -c "import OpenSSL; print(OpenSSL.__version__)"
2

Start the Server

python servidor.py
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
3

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:
1

Obtain SSL Certificates

# 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
2

Configure Certificate Paths

Point the server to your certificates using environment variables:
export CERT_FILE="/etc/letsencrypt/live/biometric.example.com/fullchain.pem"
export KEY_FILE="/etc/letsencrypt/live/biometric.example.com/privkey.pem"
3

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
4

Start and Verify

python server.py
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:
1

Uninstall pyopenssl (Optional)

pip uninstall pyopenssl
2

Start Server

python server.py
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

VariableDefaultDescription
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

Pattern 2: Direct SSL with Certificate Renewal

Run the server with direct SSL and automate certificate renewal:
1

Setup Let's Encrypt

sudo certbot certonly --standalone -d biometric.example.com
2

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
3

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:
1

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"
2

Submit CSR to Corporate CA

Submit biometric.csr to your IT security team for signing.
3

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
4

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

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
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:
openssl s_client -connect biometric.example.com:5000 -showcerts

Comparison: SSL Modes

ModeUse CaseSecuritySetup DifficultyCost
Auto-generatedDevelopment, testingLow (self-signed)Very EasyFree
Let’s EncryptProduction, internet-facingHighEasyFree
Commercial CAEnterprise productionHighMediumPaid
Corporate PKIInternal enterpriseHighMediumInternal
HTTP (no SSL)Behind reverse proxy, internal onlyDepends on proxyVery EasyFree

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

Build docs developers (and LLMs) love