# 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): ```bash sudo apt update sudo apt install certbot python3-certbot-nginx ``` **Generate Certificate** (HTTP-01 challenge): ```bash # 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**: ```nginx 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**: ```nginx 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**: ```bash # 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**: ```nginx 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**: ```bash # Configure tunnel (see Tunneling guide) PANGOLIN_ENDPOINT=https://pangolin.bnkserve.org NEWT_ID= NEWT_SECRET= # Start Newt container docker compose up -d newt ``` **Nginx Configuration**: Keep HTTP-only (tunnel handles HTTPS): ```nginx 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](tunneling.md) for complete guide. --- ## Nginx SSL Configuration ### Strong TLS Settings ```nginx 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`): ```nginx 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**: ```bash # 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**: ```bash # 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**: ```bash # 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**: ```bash # 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): ```yaml # 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**: ```bash openssl s_client -connect api.cmlite.org:443 -servername api.cmlite.org ``` **Check certificate chain**: ```bash openssl s_client -connect api.cmlite.org:443 -servername api.cmlite.org -showcerts ``` **Test specific protocol**: ```bash # 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): ```bash # Install Cloudflare plugin sudo apt install python3-certbot-dns-cloudflare # Create credentials file cat > ~/.secrets/cloudflare.ini <