10 KiB

SSL/TLS Certificate Management

Overview

Changemaker Lite V2 supports multiple SSL/TLS certificate sources for HTTPS deployment:

  • Let's Encrypt: Free automated certificates (recommended for self-hosted)
  • Cloudflare Origin Certificates: Static 15-year certificates (if using Cloudflare)
  • Pangolin Tunnel SSL: Tunnel provider handles SSL termination

Recommendation: Use Pangolin tunnel for simplest setup (SSL handled by tunnel provider).


Certificate Sources

Let's Encrypt with Certbot

Best for: Self-hosted deployments with public DNS

Process:

  1. Install Certbot
  2. Generate certificate (DNS or HTTP challenge)
  3. Configure Nginx
  4. Auto-renewal via cron

Installation (Ubuntu/Debian):

sudo apt update
sudo apt install certbot python3-certbot-nginx

Generate Certificate (HTTP-01 challenge):

# Stop Nginx temporarily
docker compose stop nginx

# Generate cert
sudo certbot certonly --standalone \
  -d cmlite.org \
  -d "*.cmlite.org" \
  --email admin@cmlite.org \
  --agree-tos \
  --non-interactive

# Start Nginx
docker compose start nginx

Certificate Location:

/etc/letsencrypt/live/cmlite.org/
├─ fullchain.pem  (certificate + intermediate)
├─ privkey.pem    (private key)
└─ chain.pem      (intermediate only)

Nginx Configuration:

server {
    listen 443 ssl http2;
    server_name api.cmlite.org;

    ssl_certificate /etc/letsencrypt/live/cmlite.org/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/cmlite.org/privkey.pem;

    ssl_protocols TLSv1.2 TLSv1.3;
    ssl_ciphers 'ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256';
    ssl_prefer_server_ciphers on;

    # ... locations
}

HTTP Redirect:

server {
    listen 80;
    server_name api.cmlite.org;
    return 301 https://$host$request_uri;
}

Cloudflare Origin Certificates

Best for: Sites using Cloudflare DNS + proxy

Process:

  1. Generate certificate in Cloudflare dashboard
  2. Download certificate + private key
  3. Install in Nginx
  4. Set SSL mode to "Full (strict)"

Generate Certificate:

  1. Cloudflare dashboard → SSL/TLS → Origin Server
  2. Click "Create Certificate"
  3. Hostnames: cmlite.org, *.cmlite.org
  4. Validity: 15 years
  5. Download certificate + private key

Install Certificate:

# Create directory
sudo mkdir -p /etc/ssl/cloudflare

# Save files
sudo nano /etc/ssl/cloudflare/cmlite.org.pem      # Certificate
sudo nano /etc/ssl/cloudflare/cmlite.org.key      # Private key

# Set permissions
sudo chmod 600 /etc/ssl/cloudflare/cmlite.org.key

Nginx Configuration:

server {
    listen 443 ssl http2;
    server_name api.cmlite.org;

    ssl_certificate /etc/ssl/cloudflare/cmlite.org.pem;
    ssl_certificate_key /etc/ssl/cloudflare/cmlite.org.key;

    # ... TLS config
}

Cloudflare SSL Mode: Set to "Full (strict)" (not "Flexible").


Pangolin Tunnel SSL

Best for: Quick deployment without SSL management

How it works:

  1. Pangolin tunnel terminates SSL at tunnel endpoint
  2. Traffic forwarded to your Nginx as HTTP
  3. No certificate management needed

Setup:

# Configure tunnel (see Tunneling guide)
PANGOLIN_ENDPOINT=https://pangolin.bnkserve.org
NEWT_ID=<your-newt-id>
NEWT_SECRET=<your-newt-secret>

# Start Newt container
docker compose up -d newt

Nginx Configuration: Keep HTTP-only (tunnel handles HTTPS):

server {
    listen 80;
    server_name api.cmlite.org;

    # No SSL config needed — tunnel terminates HTTPS
    location / {
        proxy_pass http://changemaker-v2-api:4000;
    }
}

DNS Setup: Point domain to tunnel endpoint (provided by Pangolin).

See Tunneling for complete guide.


Nginx SSL Configuration

Strong TLS Settings

server {
    listen 443 ssl http2;
    server_name api.cmlite.org;

    # Certificates
    ssl_certificate /etc/letsencrypt/live/cmlite.org/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/cmlite.org/privkey.pem;

    # Protocols (TLS 1.2+ only)
    ssl_protocols TLSv1.2 TLSv1.3;

    # Ciphers (secure + fast)
    ssl_ciphers 'ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384';
    ssl_prefer_server_ciphers on;

    # Session caching (performance)
    ssl_session_cache shared:SSL:10m;
    ssl_session_timeout 10m;

    # OCSP stapling (performance + privacy)
    ssl_stapling on;
    ssl_stapling_verify on;
    ssl_trusted_certificate /etc/letsencrypt/live/cmlite.org/chain.pem;

    # HSTS (already set globally in nginx.conf)
    # add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;

    # ... locations
}

Explanation:

  • TLS 1.2+: Disables insecure SSLv3, TLS 1.0/1.1
  • Ciphers: ECDHE for forward secrecy, AES-GCM for speed
  • Session cache: Reduces TLS handshake overhead
  • OCSP stapling: Faster certificate validation

HTTP/2

Already enabled (:443 ssl http2):

  • Multiplexes requests over single connection
  • Server push support (optional)
  • Faster page loads

No additional config needed — Nginx handles HTTP/2 automatically.


HSTS (HTTP Strict Transport Security)

Already set globally (in nginx/nginx.conf):

add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;

Effect:

  • Browsers cache HTTPS requirement for 1 year
  • Prevents downgrade attacks
  • Applies to all subdomains

Warning: Only enable after verifying HTTPS works (can't easily undo).

Test before enabling:

# Test HTTPS works
curl -I https://api.cmlite.org

# Check for redirects
curl -L https://api.cmlite.org

Certificate Renewal

Automated Renewal (Certbot)

Setup cron job:

# Edit crontab
sudo crontab -e

# Add renewal job (checks twice daily)
0 0,12 * * * certbot renew --quiet --post-hook "docker compose -f /path/to/changemaker.lite/docker-compose.yml exec nginx nginx -s reload"

Manual renewal:

# Dry run (test)
sudo certbot renew --dry-run

# Real renewal
sudo certbot renew

# Reload Nginx
docker compose exec nginx nginx -s reload

Renewal conditions:

  • Certificates expire in <30 days
  • HTTP-01 challenge succeeds (port 80 must be open)

Manual Renewal (Cloudflare)

Cloudflare origin certificates valid for 15 years — no renewal needed.

If replacing certificate:

  1. Generate new cert in Cloudflare dashboard
  2. Download files
  3. Replace files in /etc/ssl/cloudflare/
  4. Reload Nginx: docker compose exec nginx nginx -s reload

Monitoring Expiry

Check expiry date:

# Via OpenSSL
echo | openssl s_client -connect api.cmlite.org:443 -servername api.cmlite.org 2>/dev/null | openssl x509 -noout -dates

# Output:
# notBefore=Jan  1 00:00:00 2024 GMT
# notAfter=Apr  1 23:59:59 2024 GMT

Automated monitoring (via Prometheus + Alertmanager):

# In alerts.yml
- alert: SSLCertificateExpiringSoon
  expr: probe_ssl_earliest_cert_expiry - time() < 86400 * 30  # 30 days
  for: 1h
  labels:
    severity: warning
  annotations:
    summary: "SSL certificate expiring in <30 days"

Testing SSL

SSL Labs

Online test: https://www.ssllabs.com/ssltest/

Target grade: A or A+

Common issues:

  • Missing intermediate certificate (use fullchain.pem not cert.pem)
  • Weak ciphers (update ssl_ciphers list)
  • Missing HSTS header (already set globally)

Command Line

Test TLS handshake:

openssl s_client -connect api.cmlite.org:443 -servername api.cmlite.org

Check certificate chain:

openssl s_client -connect api.cmlite.org:443 -servername api.cmlite.org -showcerts

Test specific protocol:

# TLS 1.2
openssl s_client -connect api.cmlite.org:443 -tls1_2

# TLS 1.3
openssl s_client -connect api.cmlite.org:443 -tls1_3

# SSLv3 (should fail)
openssl s_client -connect api.cmlite.org:443 -ssl3

Wildcard Certificates

For *.cmlite.org (covers all subdomains):

Let's Encrypt (DNS-01 Challenge)

Required: API access to DNS provider (Cloudflare, Route53, etc.)

Example (Cloudflare):

# Install Cloudflare plugin
sudo apt install python3-certbot-dns-cloudflare

# Create credentials file
cat > ~/.secrets/cloudflare.ini <<EOF
dns_cloudflare_api_token = YOUR_API_TOKEN
EOF
chmod 600 ~/.secrets/cloudflare.ini

# Generate wildcard cert
sudo certbot certonly \
  --dns-cloudflare \
  --dns-cloudflare-credentials ~/.secrets/cloudflare.ini \
  -d cmlite.org \
  -d "*.cmlite.org" \
  --email admin@cmlite.org \
  --agree-tos

Advantage: Single certificate covers all subdomains (api, app, db, etc.).


Troubleshooting

Certificate Not Trusted

Symptoms: Browser shows "Not Secure" warning

Causes:

  1. Missing intermediate certificate
  2. Wrong certificate file
  3. Certificate expired

Solution:

# Use fullchain.pem (includes intermediate)
ssl_certificate /etc/letsencrypt/live/cmlite.org/fullchain.pem;

# NOT cert.pem (missing intermediate)
# ssl_certificate /etc/letsencrypt/live/cmlite.org/cert.pem;  # ❌ WRONG

# Reload Nginx
docker compose exec nginx nginx -s reload

Mixed Content Warnings

Symptoms: Some assets load via HTTP on HTTPS page

Cause: Hard-coded http:// URLs in HTML/JS

Solution:

// Change absolute URLs to protocol-relative
// ❌ WRONG
const apiUrl = 'http://api.cmlite.org';

// ✅ CORRECT
const apiUrl = 'https://api.cmlite.org';

// ✅ BEST (protocol-relative)
const apiUrl = location.protocol + '//api.cmlite.org';

Renewal Failures

Symptoms: Certbot renewal fails

Diagnosis:

# Test renewal
sudo certbot renew --dry-run

# Check logs
sudo tail -f /var/log/letsencrypt/letsencrypt.log

Common causes:

  • Port 80 blocked (HTTP-01 challenge fails)
  • DNS not pointing to server (domain validation fails)
  • Rate limit hit (5 certs/week per domain)

Solution:

# Check port 80 open
sudo netstat -tulpn | grep :80

# Test HTTP challenge
curl http://cmlite.org/.well-known/acme-challenge/test

# Use DNS-01 challenge instead (no port 80 needed)
sudo certbot certonly --dns-cloudflare ...