Skip to content

SSL certificates

SSL/TLS certificates provide an encrypted HTTPS connection between the client and server, protecting transmitted data from interception.

Configuration

To enable HTTPS, add a tls section to the server configuration:

json
{
    "servers": {
        "s1": {
            "domains": ["example.com", "www.example.com"],
            "ip": "0.0.0.0",
            "port": 443,
            "root": "/var/www/example.com/web",
            "tls": {
                "fullchain": "/etc/ssl/certs/fullchain.pem",
                "private": "/etc/ssl/private/privkey.pem",
                "ciphers": "TLS_AES_256_GCM_SHA384 TLS_CHACHA20_POLY1305_SHA256 TLS_AES_128_GCM_SHA256 ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256"
            },
            "http": {
                "routes": { ... }
            }
        }
    }
}

Parameters

All three fields are required — the server will not start if any of them is missing or empty.

ParameterDescription
fullchainPath to the certificate file in PEM format (including intermediate certificates)
privatePath to the private key file in PEM format
ciphersList of supported ciphers, passed to OpenSSL as is

Protocol and ciphers

The server uses TLS_server_method(): SSLv2 and SSLv3 are disabled and renegotiation is forbidden. The ciphers string is a single string where names prefixed with TLS_ (TLS_AES_256_GCM_SHA384, …) are TLS 1.3 suites and the rest (ECDHE-RSA-AES256-GCM-SHA384, …) are TLS 1.2 suites. The separator may be a space or a colon.

Obtaining a certificate

Let's Encrypt (free)

Let's Encrypt provides free SSL certificates valid for 90 days with automatic renewal.

Installing Certbot

bash
# Ubuntu/Debian
sudo apt update
sudo apt install certbot

# CentOS/RHEL
sudo yum install certbot

Obtaining a certificate

bash
# Standalone mode (server must be stopped)
sudo certbot certonly --standalone -d example.com -d www.example.com

# Webroot mode (server is running)
sudo certbot certonly --webroot -w /var/www/example.com/web -d example.com -d www.example.com

After successful issuance, certificates will be located at:

  • /etc/letsencrypt/live/example.com/fullchain.pem — certificate
  • /etc/letsencrypt/live/example.com/privkey.pem — private key

Configuration with Let's Encrypt

json
{
    "tls": {
        "fullchain": "/etc/letsencrypt/live/example.com/fullchain.pem",
        "private": "/etc/letsencrypt/live/example.com/privkey.pem",
        "ciphers": "TLS_AES_256_GCM_SHA384 TLS_CHACHA20_POLY1305_SHA256 TLS_AES_128_GCM_SHA256 ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384"
    }
}

Automatic renewal

bash
# Test renewal
sudo certbot renew --dry-run

# Add to cron (runs twice daily)
echo "0 0,12 * * * root certbot renew --quiet" | sudo tee /etc/cron.d/certbot-renew

Self-signed certificate

For development and testing, you can use a self-signed certificate:

bash
# Create directory
sudo mkdir -p /etc/ssl/private

# Generate private key and certificate
sudo openssl req -x509 -nodes -days 365 -newkey rsa:2048 \
    -keyout /etc/ssl/private/selfsigned.key \
    -out /etc/ssl/certs/selfsigned.crt \
    -subj "/C=US/ST=California/L=San Francisco/O=Development/CN=localhost"

# Set permissions
sudo chmod 600 /etc/ssl/private/selfsigned.key

Configuration

json
{
    "tls": {
        "fullchain": "/etc/ssl/certs/selfsigned.crt",
        "private": "/etc/ssl/private/selfsigned.key",
        "ciphers": "TLS_AES_256_GCM_SHA384 TLS_AES_128_GCM_SHA256"
    }
}

Warning

Self-signed certificates trigger browser warnings. Use them only for development.

Commercial certificate

When purchasing a certificate from a Certificate Authority (CA), you will receive:

  • Domain certificate (.crt)
  • Intermediate certificates (intermediate/chain)
  • Private key (.key)

Combining certificates

bash
# Create fullchain from certificate and intermediates
cat domain.crt intermediate.crt > fullchain.pem

Modern configuration (TLS 1.3 + TLS 1.2)

TLS_AES_256_GCM_SHA384 TLS_CHACHA20_POLY1305_SHA256 TLS_AES_128_GCM_SHA256 ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256

Maximum compatibility

TLS_AES_256_GCM_SHA384 TLS_CHACHA20_POLY1305_SHA256 TLS_AES_128_GCM_SHA256 TLS_AES_128_CCM_8_SHA256 TLS_AES_128_CCM_SHA256 ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-SHA384:ECDHE-RSA-AES256-SHA384:ECDHE-ECDSA-AES128-SHA256:ECDHE-RSA-AES128-SHA256

Multiple certificates on one IP (SNI)

The framework supports SNI (Server Name Indication). If several servers listen on the same ip and port, the certificate of the server whose domain pattern matches the host name sent by the client in SNI is selected during the TLS handshake. This lets you serve different certificates for different domains on the same address.

json
{
    "servers": {
        "site_a": {
            "domains": ["a.example.com"],
            "ip": "0.0.0.0",
            "port": 443,
            "tls": {
                "fullchain": "/etc/ssl/a/fullchain.pem",
                "private": "/etc/ssl/a/privkey.pem",
                "ciphers": "..."
            },
            "http": { "routes": { ... } }
        },
        "site_b": {
            "domains": ["b.example.com"],
            "ip": "0.0.0.0",
            "port": 443,
            "tls": {
                "fullchain": "/etc/ssl/b/fullchain.pem",
                "private": "/etc/ssl/b/privkey.pem",
                "ciphers": "..."
            },
            "http": { "routes": { ... } }
        }
    }
}

Domain matching

SNI matching uses the same PCRE regular expressions as the server domains. IDN domains are automatically converted to Punycode, and IP addresses used as SNI are ignored per RFC 6066. If nothing matches, the default server's certificate (the one that accepted the connection) is used.

HTTP and HTTPS on the same server

To support both protocols, create two servers:

json
{
    "servers": {
        "http": {
            "domains": ["example.com"],
            "ip": "0.0.0.0",
            "port": 80,
            "http": {
                "redirects": {
                    "/(.*)": "https://example.com/{1}"
                }
            }
        },
        "https": {
            "domains": ["example.com"],
            "ip": "0.0.0.0",
            "port": 443,
            "tls": {
                "fullchain": "/etc/ssl/certs/fullchain.pem",
                "private": "/etc/ssl/private/privkey.pem",
                "ciphers": "..."
            },
            "http": {
                "routes": { ... }
            }
        }
    }
}

Verifying the setup

Checking the certificate

bash
# View certificate information
openssl x509 -in /etc/ssl/certs/fullchain.pem -text -noout

# Check expiration date
openssl x509 -in /etc/ssl/certs/fullchain.pem -enddate -noout

# Verify key and certificate match
openssl x509 -noout -modulus -in fullchain.pem | openssl md5
openssl rsa -noout -modulus -in privkey.pem | openssl md5
# Hashes should match

Checking the connection

bash
# Check SSL connection
openssl s_client -connect example.com:443 -servername example.com

# Check supported protocols
openssl s_client -connect example.com:443 -tls1_3
openssl s_client -connect example.com:443 -tls1_2

Online verification

Security

File permissions

bash
# Certificate (can be public)
sudo chmod 644 /etc/ssl/certs/fullchain.pem

# Private key (root only)
sudo chmod 600 /etc/ssl/private/privkey.pem
sudo chown root:root /etc/ssl/private/privkey.pem

Security headers

Add security headers via middleware:

c
int middleware_security_headers(httpctx_t* ctx) {
    httpresponse_t* res = ctx->response;

    // Force HTTPS usage
    res->add_header(res, "Strict-Transport-Security", "max-age=31536000; includeSubDomains");

    // Clickjacking protection
    res->add_header(res, "X-Frame-Options", "SAMEORIGIN");

    // XSS protection
    res->add_header(res, "X-Content-Type-Options", "nosniff");

    return 1;
}

Troubleshooting

Error "certificate verify failed"

Make sure the fullchain.pem file contains the complete certificate chain (domain certificate + intermediates).

Error "key values mismatch"

The private key does not match the certificate. Verify the match using the command above.

Error "permission denied"

Check the permissions on the certificate and key files. The server process must have read access.

Certificate not updating

After replacing the certificate files, perform a hot reload — it re-reads config.json and builds new SSL contexts without restarting the process:

bash
kill -SIGUSR1 $(pidof cpdy)

The reload behavior is controlled by the main.reload setting in config.json:

  • "soft" — new connections use the updated configuration, existing connections keep running;
  • "hard" — additionally forces the current sockets to close and clients to reconnect.

Released under the MIT License.