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; TLS 1.3 suites and TLS 1.2 ciphers may be mixed in one string

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 ciphers. Colons, commas and spaces all work as separators.

The two groups are configured through different OpenSSL APIs, so the server splits the string and applies each half separately. Omitting a group leaves OpenSSL's defaults for that protocol version in place — listing only TLS 1.2 ciphers does not disable TLS 1.3, and listing only TLS 1.3 suites does not disable TLS 1.2. To actually restrict a version, name the suites you want for it. TLS 1.2 control syntax (HIGH, !aNULL, @STRENGTH) keeps working as before.

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 cwfr)

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.