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:
- Verify domain ownership
- Obtain the certificate
- Automatically configure Nginx
- 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
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;
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