479 lines
10 KiB
Markdown
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
|