SSL Certificates
FORGE supports multiple SSL/TLS certificate strategies depending on your deployment method and requirements. Every deployment method -- Docker Compose, Kubernetes, or SSH -- includes built-in SSL configuration to ensure your application is served over HTTPS in production.
SSL Options at a Glance
| Method | Cost | Auto-Renewal | Best For |
|---|---|---|---|
| Let's Encrypt | Free | Yes (90-day certs) | Most deployments |
| Cloudflare Proxy | Free | Yes (Cloudflare manages) | DDoS protection, CDN |
| Custom Certificate | Varies | Manual | Enterprise, EV certs, wildcards |
Recommendation
For most applications, Let's Encrypt is the best option. It is free, fully automated, and trusted by all browsers. Use Cloudflare Proxy if you also need DDoS protection and CDN capabilities.
Let's Encrypt
Let's Encrypt provides free, automated TLS certificates trusted by all major browsers. Certificates are valid for 90 days and are automatically renewed before expiry.
Requirements
Before obtaining a Let's Encrypt certificate, ensure:
- Your domain's DNS A record points to your server's IP address
- Port 80 is accessible from the internet (for HTTP-01 challenge validation)
- You have a valid email address for expiry notifications
With Kubernetes (cert-manager)
cert-manager is a Kubernetes-native certificate management controller that automates issuance and renewal.
Step 1: Install cert-manager
# Install cert-manager into your cluster
kubectl apply -f https://github.com/cert-manager/cert-manager/releases/download/v1.14.0/cert-manager.yaml
# Verify installation
kubectl wait --for=condition=ready pod \
-l app.kubernetes.io/instance=cert-manager \
-n cert-manager --timeout=120sStep 2: Generate manifests with Let's Encrypt
forge deploy:k8s --ssl=letsencrypt --email=admin@myapp.comStep 3: Apply the ClusterIssuer
The generated cluster-issuer.yaml configures Let's Encrypt:
apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
name: letsencrypt-prod
spec:
acme:
email: admin@myapp.com
server: https://acme-v02.api.letsencrypt.org/directory
privateKeySecretRef:
name: letsencrypt-prod-account-key
solvers:
- http01:
ingress:
class: nginxkubectl apply -f k8s/cert-manager/cluster-issuer.yamlStep 4: Apply Ingress with TLS
The generated Ingress references the ClusterIssuer via annotation:
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: myapp-ingress
annotations:
cert-manager.io/cluster-issuer: letsencrypt-prod
spec:
tls:
- hosts:
- myapp.com
- admin.myapp.com
- api.myapp.com
secretName: myapp-tls
rules:
# ... routing ruleskubectl apply -f k8s/ingress.yamlStep 5: Verify certificate issuance
# Check certificate status
kubectl get certificate -n myapp
# View certificate details
kubectl describe certificate myapp-tls -n myapp
# Check the certificate order
kubectl get certificaterequest -n myapp
kubectl get order -n myappUse staging first
For testing, use the Let's Encrypt staging server to avoid hitting rate limits:
# In cluster-issuer.yaml, change:
server: https://acme-staging-v02.api.letsencrypt.org/directorySwitch to the production server once you have confirmed everything works.
With Docker Compose (Caddy)
Caddy handles Let's Encrypt automatically with zero configuration. When Caddy sees a real domain name in its configuration, it obtains and renews TLS certificates without any manual setup.
Step 1: Generate the production Compose file
forge deploy:compose --env=production --ssl=letsencryptStep 2: Configure Caddy
The generated Caddyfile:
{
email admin@myapp.com
}
myapp.com {
reverse_proxy web:3000
encode gzip
}
admin.myapp.com {
reverse_proxy admin:3001
encode gzip
}
api.myapp.com {
reverse_proxy api:8080
encode gzip
}Step 3: Start the stack
docker compose -f docker-compose.prod.yaml up -dCaddy will automatically:
- Obtain certificates from Let's Encrypt
- Configure HTTPS on port 443
- Redirect HTTP to HTTPS
- Renew certificates before they expire
- Enable OCSP stapling
Caddy certificate storage
Caddy stores certificates in the caddy_data Docker volume. This volume persists across container restarts, so certificates are not re-requested unnecessarily.
With SSH (certbot)
For traditional server deployments without containers, use certbot to obtain and manage Let's Encrypt certificates.
Step 1: Generate the SSL setup script
forge deploy:ssl --method=certbot --domain=myapp.com --email=admin@myapp.comStep 2: Run the generated script on your server
ssh deploy@myapp.com 'bash -s' < deploy/ssl/setup-certbot.shThe script performs:
#!/bin/bash
set -euo pipefail
# Install certbot
sudo apt update
sudo apt install -y certbot python3-certbot-nginx
# Obtain certificates for all domains
sudo certbot certonly --nginx \
-d myapp.com \
-d admin.myapp.com \
-d api.myapp.com \
--email admin@myapp.com \
--agree-tos \
--non-interactive
# Certificates are stored at:
# /etc/letsencrypt/live/myapp.com/fullchain.pem
# /etc/letsencrypt/live/myapp.com/privkey.pemStep 3: Configure auto-renewal
certbot sets up a systemd timer for automatic renewal. Verify it is active:
# Check renewal timer
sudo systemctl status certbot.timer
# Test renewal (dry run)
sudo certbot renew --dry-runStep 4: Configure Nginx to use the certificates
server {
listen 443 ssl http2;
server_name myapp.com;
ssl_certificate /etc/letsencrypt/live/myapp.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/myapp.com/privkey.pem;
# Recommended SSL settings
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256;
ssl_prefer_server_ciphers off;
# HSTS
add_header Strict-Transport-Security "max-age=63072000" always;
location / {
proxy_pass http://localhost:3000;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}
server {
listen 80;
server_name myapp.com admin.myapp.com api.myapp.com;
return 301 https://$host$request_uri;
}Cloudflare Proxy
Cloudflare provides free SSL/TLS when you proxy traffic through their network. This also adds DDoS protection, a CDN, and Web Application Firewall (WAF) capabilities.
Setup
Step 1: Add your domain to Cloudflare
- Create a free Cloudflare account at cloudflare.com
- Add your domain and follow the DNS setup instructions
- Update your domain's nameservers to point to Cloudflare
Step 2: Configure SSL mode
In Cloudflare Dashboard > SSL/TLS:
| Mode | Description | Recommendation |
|---|---|---|
| Off | No encryption | Never use |
| Flexible | Encrypts browser to Cloudflare only | Not recommended |
| Full | Encrypts end-to-end, accepts self-signed on server | Acceptable |
| Full (Strict) | Encrypts end-to-end, requires valid cert on server | Recommended |
Use Full (Strict) mode
Always use Full (Strict) mode. This requires a valid certificate on your origin server (use a Cloudflare Origin Certificate or Let's Encrypt). Flexible mode leaves traffic between Cloudflare and your server unencrypted.
Step 3: Install a Cloudflare Origin Certificate
Origin Certificates are free certificates issued by Cloudflare for encrypting traffic between Cloudflare and your server:
- Go to Cloudflare Dashboard > SSL/TLS > Origin Server
- Click "Create Certificate"
- Select hostnames (e.g.,
*.myapp.com,myapp.com) - Choose validity period (up to 15 years)
- Download the certificate and private key
Install on your server:
# Save certificate files
sudo mkdir -p /etc/ssl/cloudflare
sudo nano /etc/ssl/cloudflare/myapp.com.pem # Paste certificate
sudo nano /etc/ssl/cloudflare/myapp.com-key.pem # Paste private key
# Set permissions
sudo chmod 600 /etc/ssl/cloudflare/myapp.com-key.pemConfigure Nginx to use the Origin Certificate:
server {
listen 443 ssl http2;
server_name myapp.com;
ssl_certificate /etc/ssl/cloudflare/myapp.com.pem;
ssl_certificate_key /etc/ssl/cloudflare/myapp.com-key.pem;
location / {
proxy_pass http://localhost:3000;
}
}Step 4: Enable additional security features
In the Cloudflare Dashboard, enable:
- Always Use HTTPS -- Redirects all HTTP traffic to HTTPS
- Automatic HTTPS Rewrites -- Fixes mixed content issues
- Minimum TLS Version -- Set to TLS 1.2
- HSTS -- Enable HTTP Strict Transport Security
Custom Certificates
For enterprise requirements such as Extended Validation (EV) certificates, wildcard certificates from specific CAs, or organization-validated certificates.
Setup
Step 1: Obtain a certificate from your CA
Generate a Certificate Signing Request (CSR):
# Generate private key and CSR
openssl req -new -newkey rsa:2048 -nodes \
-keyout myapp.com.key \
-out myapp.com.csr \
-subj "/C=US/ST=State/L=City/O=MyOrg/CN=myapp.com"Submit the CSR to your Certificate Authority (DigiCert, Sectigo, etc.) and download the issued certificate.
Step 2: Install on your server
# Create certificate directory
sudo mkdir -p /etc/ssl/custom
# Copy certificate files
sudo cp myapp.com.crt /etc/ssl/custom/
sudo cp myapp.com.key /etc/ssl/custom/
sudo cp ca-bundle.crt /etc/ssl/custom/
# Create full chain
cat /etc/ssl/custom/myapp.com.crt /etc/ssl/custom/ca-bundle.crt \
> /etc/ssl/custom/myapp.com-fullchain.crt
# Set permissions
sudo chmod 600 /etc/ssl/custom/myapp.com.keyStep 3: Configure your web server
server {
listen 443 ssl http2;
server_name myapp.com;
ssl_certificate /etc/ssl/custom/myapp.com-fullchain.crt;
ssl_certificate_key /etc/ssl/custom/myapp.com.key;
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256;
}# Create TLS secret from certificate files
kubectl create secret tls myapp-tls \
--cert=myapp.com-fullchain.crt \
--key=myapp.com.key \
-n myappmyapp.com {
tls /etc/ssl/custom/myapp.com-fullchain.crt /etc/ssl/custom/myapp.com.key
reverse_proxy web:3000
}Certificate renewal
Custom certificates do not auto-renew. Set a calendar reminder to renew before expiry. Most certificates are valid for 1 year.
SSL Options Comparison
| Feature | Let's Encrypt | Cloudflare Proxy | Custom Certificate |
|---|---|---|---|
| Cost | Free | Free (with CF) | $10 - $500+/year |
| Auto-Renewal | Yes (90-day cycle) | Yes (CF manages) | No (manual) |
| Setup Effort | Low | Medium | High |
| DDoS Protection | No | Yes (included) | No |
| CDN | No | Yes (included) | No |
| Certificate Types | DV only | DV (proxy), OV/EV (origin) | DV, OV, EV |
| Wildcard Support | Yes (DNS-01) | Yes | Yes |
| Browser Trust | All browsers | All browsers | All browsers |
| Kubernetes | cert-manager | Proxy mode | Manual secret |
| Docker Compose | Caddy (auto) | Proxy mode | Manual volume |
| SSH / Bare Metal | certbot | Proxy mode | Manual install |
FORGE SSL Commands
FORGE provides CLI commands for each deployment method:
# Kubernetes with Let's Encrypt
forge deploy:k8s --ssl=letsencrypt --email=admin@myapp.com
# Docker Compose with Caddy (auto Let's Encrypt)
forge deploy:compose --ssl=letsencrypt --email=admin@myapp.com
# SSH server with certbot
forge deploy:ssl --method=certbot --domain=myapp.com --email=admin@myapp.com
# Generate SSL setup scripts without executing
forge deploy:ssl --method=certbot --domain=myapp.com --output=./deploy/ssl/Verifying SSL Configuration
After deploying, verify your SSL setup:
# Check certificate details
openssl s_client -connect myapp.com:443 -servername myapp.com 2>/dev/null | \
openssl x509 -noout -subject -dates -issuer
# Test with curl
curl -vI https://myapp.com 2>&1 | grep -E "SSL|issuer|expire"
# Check all subdomains
for domain in myapp.com admin.myapp.com api.myapp.com; do
echo "--- $domain ---"
curl -sI "https://$domain" | head -1
doneOnline tools for comprehensive testing:
- SSL Labs Server Test -- Detailed grade and configuration analysis
- Security Headers -- HTTP security header verification
Troubleshooting
Certificate not issuing
# cert-manager: Check certificate request status
kubectl describe certificaterequest -n myapp
kubectl describe order -n myapp
kubectl describe challenge -n myapp
# Common causes:
# - DNS not pointing to server
# - Port 80 blocked by firewall
# - Rate limit exceeded (use staging server)Certificate expired
# certbot: Force renewal
sudo certbot renew --force-renewal
# cert-manager: Delete and recreate
kubectl delete certificate myapp-tls -n myapp
kubectl apply -f k8s/cert-manager/certificate.yamlMixed content warnings
Ensure all resources are loaded over HTTPS. Check for hardcoded http:// URLs in your application configuration and templates.
Next Steps
- Docker Compose Deployment -- Deploy with automatic HTTPS
- Kubernetes Deployment -- Scale with cert-manager
- SSH Deployment -- Traditional server setup