479 lines
10 KiB
Markdown

# 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=<your-newt-id>
NEWT_SECRET=<your-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 <<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**:
```bash
# 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**:
```javascript
// 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**:
```bash
# 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**:
```bash
# 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 ...
```
---
## Related Documentation
- **[Docker Compose](docker-compose.md)** Nginx container setup
- **[Nginx Configuration](nginx.md)** Reverse proxy config
- **[Tunneling](tunneling.md)** Pangolin tunnel SSL
- **[Environment Variables](environment-variables.md)** SSL-related env vars