Skip to content

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

MethodCostAuto-RenewalBest For
Let's EncryptFreeYes (90-day certs)Most deployments
Cloudflare ProxyFreeYes (Cloudflare manages)DDoS protection, CDN
Custom CertificateVariesManualEnterprise, 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:

  1. Your domain's DNS A record points to your server's IP address
  2. Port 80 is accessible from the internet (for HTTP-01 challenge validation)
  3. 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

bash
# 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=120s

Step 2: Generate manifests with Let's Encrypt

bash
forge deploy:k8s --ssl=letsencrypt --email=admin@myapp.com

Step 3: Apply the ClusterIssuer

The generated cluster-issuer.yaml configures Let's Encrypt:

yaml
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: nginx
bash
kubectl apply -f k8s/cert-manager/cluster-issuer.yaml

Step 4: Apply Ingress with TLS

The generated Ingress references the ClusterIssuer via annotation:

yaml
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 rules
bash
kubectl apply -f k8s/ingress.yaml

Step 5: Verify certificate issuance

bash
# 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 myapp

Use staging first

For testing, use the Let's Encrypt staging server to avoid hitting rate limits:

yaml
# In cluster-issuer.yaml, change:
server: https://acme-staging-v02.api.letsencrypt.org/directory

Switch 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

bash
forge deploy:compose --env=production --ssl=letsencrypt

Step 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

bash
docker compose -f docker-compose.prod.yaml up -d

Caddy will automatically:

  1. Obtain certificates from Let's Encrypt
  2. Configure HTTPS on port 443
  3. Redirect HTTP to HTTPS
  4. Renew certificates before they expire
  5. 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

bash
forge deploy:ssl --method=certbot --domain=myapp.com --email=admin@myapp.com

Step 2: Run the generated script on your server

bash
ssh deploy@myapp.com 'bash -s' < deploy/ssl/setup-certbot.sh

The script performs:

bash
#!/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.pem

Step 3: Configure auto-renewal

certbot sets up a systemd timer for automatic renewal. Verify it is active:

bash
# Check renewal timer
sudo systemctl status certbot.timer

# Test renewal (dry run)
sudo certbot renew --dry-run

Step 4: Configure Nginx to use the certificates

nginx
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

  1. Create a free Cloudflare account at cloudflare.com
  2. Add your domain and follow the DNS setup instructions
  3. Update your domain's nameservers to point to Cloudflare

Step 2: Configure SSL mode

In Cloudflare Dashboard > SSL/TLS:

ModeDescriptionRecommendation
OffNo encryptionNever use
FlexibleEncrypts browser to Cloudflare onlyNot recommended
FullEncrypts end-to-end, accepts self-signed on serverAcceptable
Full (Strict)Encrypts end-to-end, requires valid cert on serverRecommended

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:

  1. Go to Cloudflare Dashboard > SSL/TLS > Origin Server
  2. Click "Create Certificate"
  3. Select hostnames (e.g., *.myapp.com, myapp.com)
  4. Choose validity period (up to 15 years)
  5. Download the certificate and private key

Install on your server:

bash
# 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.pem

Configure Nginx to use the Origin Certificate:

nginx
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):

bash
# 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

bash
# 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.key

Step 3: Configure your web server

nginx
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;
}
yaml
# Create TLS secret from certificate files
kubectl create secret tls myapp-tls \
  --cert=myapp.com-fullchain.crt \
  --key=myapp.com.key \
  -n myapp
caddy
myapp.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

FeatureLet's EncryptCloudflare ProxyCustom Certificate
CostFreeFree (with CF)$10 - $500+/year
Auto-RenewalYes (90-day cycle)Yes (CF manages)No (manual)
Setup EffortLowMediumHigh
DDoS ProtectionNoYes (included)No
CDNNoYes (included)No
Certificate TypesDV onlyDV (proxy), OV/EV (origin)DV, OV, EV
Wildcard SupportYes (DNS-01)YesYes
Browser TrustAll browsersAll browsersAll browsers
Kubernetescert-managerProxy modeManual secret
Docker ComposeCaddy (auto)Proxy modeManual volume
SSH / Bare MetalcertbotProxy modeManual install

FORGE SSL Commands

FORGE provides CLI commands for each deployment method:

bash
# 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:

bash
# 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
done

Online tools for comprehensive testing:

Troubleshooting

Certificate not issuing

bash
# 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

bash
# 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.yaml

Mixed content warnings

Ensure all resources are loaded over HTTPS. Check for hardcoded http:// URLs in your application configuration and templates.

Next Steps

Released under the MIT License.