Skip to main content

Overview

SSL/TLS certificates encrypt traffic between clients and your Documenso instance. This guide covers:
  • Let’s Encrypt - Free automated certificates
  • cert-manager - Kubernetes certificate management
  • Custom certificates - Using your own certificates
  • Wildcard certificates - For multiple subdomains
Always use HTTPS in production. Documenso handles sensitive documents and user authentication that must be encrypted.

Let’s Encrypt with Certbot

Prerequisites

  • A domain name pointing to your server
  • Port 80 and 443 accessible from the internet
  • Nginx or Apache installed

Install Certbot

sudo apt update
sudo apt install certbot python3-certbot-nginx

Obtain Certificate (Nginx)

Automatic configuration:
sudo certbot --nginx -d sign.example.com
Certbot will:
  1. Verify domain ownership
  2. Obtain the certificate
  3. Automatically configure Nginx
  4. Set up auto-renewal

Obtain Certificate (Standalone)

If you’re not using Nginx or want more control:
# Stop your web server temporarily
sudo systemctl stop nginx

# Obtain certificate
sudo certbot certonly --standalone -d sign.example.com

# Start web server
sudo systemctl start nginx
Certificates are stored in:
/etc/letsencrypt/live/sign.example.com/
├── fullchain.pem  # Certificate + intermediate certificates
├── privkey.pem    # Private key
├── cert.pem       # Certificate only
└── chain.pem      # Intermediate certificates

Manual Nginx Configuration

If using standalone mode, configure Nginx manually:
server {
    listen 443 ssl http2;
    server_name sign.example.com;

    ssl_certificate /etc/letsencrypt/live/sign.example.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/sign.example.com/privkey.pem;
    ssl_trusted_certificate /etc/letsencrypt/live/sign.example.com/chain.pem;

    # SSL protocols and ciphers
    ssl_protocols TLSv1.2 TLSv1.3;
    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 off;

    # SSL session cache
    ssl_session_cache shared:SSL:10m;
    ssl_session_timeout 10m;

    # OCSP stapling
    ssl_stapling on;
    ssl_stapling_verify on;

    location / {
        proxy_pass http://localhost:3000;
        # ... other proxy settings
    }
}
Test configuration:
sudo nginx -t
sudo systemctl reload nginx

Auto-Renewal

Certbot automatically sets up renewal. Test it:
# Dry run (test without actually renewing)
sudo certbot renew --dry-run

# Force renewal
sudo certbot renew --force-renewal
Renewal timer status:
sudo systemctl status certbot.timer

Wildcard Certificates

For multiple subdomains (requires DNS challenge):
sudo certbot certonly --manual --preferred-challenges dns \
  -d example.com -d *.example.com
You’ll need to add a TXT record to your DNS:
_acme-challenge.example.com. 300 IN TXT "random-token-here"

Let’s Encrypt with Caddy

Caddy automatically obtains and renews certificates - no manual setup required!

Basic Configuration

# /etc/caddy/Caddyfile
sign.example.com {
    reverse_proxy localhost:3000
}
That’s it! Caddy will:
  • Automatically obtain a Let’s Encrypt certificate
  • Renew it before expiration
  • Handle HTTPS redirects

Custom Email for Notifications

{
    email admin@example.com
}

sign.example.com {
    reverse_proxy localhost:3000
}

Use Staging Environment (Testing)

{
    acme_ca https://acme-staging-v02.api.letsencrypt.org/directory
}

sign.example.com {
    reverse_proxy localhost:3000
}
Use the staging environment during testing to avoid hitting Let’s Encrypt rate limits (50 certificates per domain per week).

cert-manager (Kubernetes)

Install cert-manager

Install cert-manager in your Kubernetes cluster:
# Install cert-manager CRDs
kubectl apply -f https://github.com/cert-manager/cert-manager/releases/download/v1.13.0/cert-manager.crds.yaml

# Add cert-manager Helm repository
helm repo add jetstack https://charts.jetstack.io
helm repo update

# Install cert-manager
helm install cert-manager jetstack/cert-manager \
  --namespace cert-manager \
  --create-namespace \
  --version v1.13.0
Verify installation:
kubectl get pods -n cert-manager

Create ClusterIssuer

Let’s Encrypt production issuer:
# letsencrypt-prod.yaml
apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
  name: letsencrypt-prod
spec:
  acme:
    # Let's Encrypt production server
    server: https://acme-v02.api.letsencrypt.org/directory
    
    # Email for important notifications
    email: admin@example.com
    
    # Secret to store the ACME account private key
    privateKeySecretRef:
      name: letsencrypt-prod
    
    # HTTP-01 challenge provider
    solvers:
      - http01:
          ingress:
            class: nginx
Apply:
kubectl apply -f letsencrypt-prod.yaml

Staging Issuer (Testing)

# letsencrypt-staging.yaml
apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
  name: letsencrypt-staging
spec:
  acme:
    server: https://acme-staging-v02.api.letsencrypt.org/directory
    email: admin@example.com
    privateKeySecretRef:
      name: letsencrypt-staging
    solvers:
      - http01:
          ingress:
            class: nginx

Update Ingress

Add cert-manager annotation to your Ingress:
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: documenso
  namespace: documenso
  annotations:
    cert-manager.io/cluster-issuer: "letsencrypt-prod"  # Use cert-manager
    nginx.ingress.kubernetes.io/proxy-body-size: "50m"
spec:
  ingressClassName: nginx
  tls:
    - hosts:
        - sign.example.com
      secretName: documenso-tls  # cert-manager will create this secret
  rules:
    - host: sign.example.com
      http:
        paths:
          - path: /
            pathType: Prefix
            backend:
              service:
                name: documenso
                port:
                  name: http
Apply:
kubectl apply -f ingress.yaml

Verify Certificate

Check certificate status:
# Get certificates
kubectl get certificate -n documenso

# Describe certificate
kubectl describe certificate documenso-tls -n documenso

# Check certificate secret
kubectl get secret documenso-tls -n documenso
Expected output:
NAME            READY   SECRET          AGE
documenso-tls   True    documenso-tls   5m

DNS-01 Challenge (Wildcard)

For wildcard certificates, use DNS-01 challenge:
apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
  name: letsencrypt-dns
spec:
  acme:
    server: https://acme-v02.api.letsencrypt.org/directory
    email: admin@example.com
    privateKeySecretRef:
      name: letsencrypt-dns
    solvers:
      - dns01:
          cloudflare:
            email: cloudflare@example.com
            apiTokenSecretRef:
              name: cloudflare-api-token
              key: api-token
        selector:
          dnsZones:
            - "example.com"
Create Cloudflare API token secret:
kubectl create secret generic cloudflare-api-token \
  --namespace cert-manager \
  --from-literal=api-token=your-cloudflare-api-token

Custom Certificates

Using Your Own Certificates

If you have certificates from a commercial CA or internal CA:

Nginx Configuration

server {
    listen 443 ssl http2;
    server_name sign.example.com;

    ssl_certificate /etc/ssl/certs/documenso.crt;
    ssl_certificate_key /etc/ssl/private/documenso.key;
    
    # Include intermediate certificates if needed
    ssl_trusted_certificate /etc/ssl/certs/ca-bundle.crt;

    # ... rest of configuration
}

Caddy Configuration

sign.example.com {
    tls /path/to/cert.pem /path/to/key.pem
    
    reverse_proxy localhost:3000
}

Kubernetes Secret

Create a TLS secret:
kubectl create secret tls documenso-tls \
  --cert=path/to/cert.crt \
  --key=path/to/key.key \
  --namespace documenso
Reference in Ingress:
spec:
  tls:
    - hosts:
        - sign.example.com
      secretName: documenso-tls

Certificate Formats

Convert between certificate formats:
# PEM to DER
openssl x509 -outform der -in cert.pem -out cert.der

# DER to PEM
openssl x509 -inform der -in cert.der -out cert.pem

# PFX/P12 to PEM
openssl pkcs12 -in cert.pfx -out cert.pem -nodes

# Combine certificate and key
cat cert.crt intermediate.crt > fullchain.pem

Self-Signed Certificates (Development Only)

Never use self-signed certificates in production. They provide no trust and browsers will show security warnings.

Generate Self-Signed Certificate

# Generate private key and certificate
openssl req -x509 -nodes -days 365 -newkey rsa:2048 \
  -keyout selfsigned.key \
  -out selfsigned.crt \
  -subj "/C=US/ST=State/L=City/O=Organization/CN=sign.example.com"

# Or with SAN (Subject Alternative Names)
openssl req -x509 -nodes -days 365 -newkey rsa:2048 \
  -keyout selfsigned.key \
  -out selfsigned.crt \
  -config <(cat <<EOF
[req]
default_bits = 2048
prompt = no
default_md = sha256
distinguished_name = dn
x509_extensions = v3_req

[dn]
C=US
ST=State
L=City
O=Organization
CN=sign.example.com

[v3_req]
subjectAltName = @alt_names

[alt_names]
DNS.1 = sign.example.com
DNS.2 = localhost
IP.1 = 127.0.0.1
EOF
)

Use with Nginx

server {
    listen 443 ssl;
    server_name sign.example.com;

    ssl_certificate /path/to/selfsigned.crt;
    ssl_certificate_key /path/to/selfsigned.key;

    # ... rest of configuration
}

Security Best Practices

Modern SSL Configuration

Use modern SSL protocols and ciphers:
# Nginx
ssl_protocols TLSv1.2 TLSv1.3;
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 off;

HSTS Header

Force HTTPS for all future connections:
# Nginx
add_header Strict-Transport-Security "max-age=63072000; includeSubDomains; preload" always;
# Caddy
header Strict-Transport-Security "max-age=63072000; includeSubDomains; preload"

OCSP Stapling

Improve SSL performance and privacy:
ssl_stapling on;
ssl_stapling_verify on;
ssl_trusted_certificate /etc/letsencrypt/live/sign.example.com/chain.pem;
resolver 8.8.8.8 8.8.4.4 valid=300s;
resolver_timeout 5s;

Test SSL Configuration

Use SSL Labs to verify your configuration:
https://www.ssllabs.com/ssltest/analyze.html?d=sign.example.com
Target grade: A+

Troubleshooting

Certificate Not Trusted

Causes:
  • Self-signed certificate
  • Missing intermediate certificates
  • Expired certificate
Solution:
# Check certificate validity
openssl s_client -connect sign.example.com:443 -servername sign.example.com

# Check certificate expiration
openssl x509 -in /etc/letsencrypt/live/sign.example.com/cert.pem -noout -dates

# Verify certificate chain
openssl verify -CAfile /etc/letsencrypt/live/sign.example.com/chain.pem \
  /etc/letsencrypt/live/sign.example.com/cert.pem

Let’s Encrypt Rate Limits

Error: “too many certificates already issued” Solution:
  • Use staging environment for testing
  • Wait for rate limit to reset (1 week)
  • Use wildcard certificate to cover multiple subdomains
Check rate limits:
https://crt.sh/?q=example.com

cert-manager Certificate Pending

Check cert-manager logs:
# Get cert-manager pods
kubectl get pods -n cert-manager

# Check logs
kubectl logs -n cert-manager cert-manager-xxxxxxxx-xxxxx

# Describe certificate
kubectl describe certificate documenso-tls -n documenso

# Check certificate request
kubectl get certificaterequest -n documenso
kubectl describe certificaterequest <name> -n documenso
Common issues:
  • DNS not pointing to cluster
  • Ingress controller not working
  • HTTP-01 challenge path blocked

Mixed Content Warnings

Ensure NEXT_PUBLIC_WEBAPP_URL uses HTTPS:
NEXT_PUBLIC_WEBAPP_URL=https://sign.example.com
Not:
NEXT_PUBLIC_WEBAPP_URL=http://sign.example.com  # Wrong!

Certificate Monitoring

Check Expiration

Monitor certificate expiration:
# Check expiration date
echo | openssl s_client -servername sign.example.com -connect sign.example.com:443 2>/dev/null | openssl x509 -noout -dates

# Days until expiration
echo | openssl s_client -servername sign.example.com -connect sign.example.com:443 2>/dev/null | openssl x509 -noout -checkend 2592000 && echo "Valid for 30+ days" || echo "Expiring soon!"

Automated Monitoring

Use monitoring tools:

See Also