SSL Certificates Setup¶
Configure HTTPS/TLS certificates for secure access to the New Hires Reporting System.
Overview¶
HTTPS is required for production deployments to:
- ✅ Encrypt data in transit
- ✅ Protect AWS credentials and sensitive employer data
- ✅ Enable modern browser security features
- ✅ Build user trust
- ✅ Improve SEO rankings
Certificate Options¶
| Option | Cost | Renewal | Best For |
|---|---|---|---|
| Let's Encrypt | Free | Auto (90 days) | Recommended - Production |
| Cloudflare | Free | Auto | Sites using Cloudflare |
| Commercial | $50-500/year | Manual | Enterprise requirements |
| Self-Signed | Free | Manual | Testing only |
Let's Encrypt with Certbot¶
Recommended for most deployments
Prerequisites¶
# Update system
sudo apt update && sudo apt upgrade -y
# Install Certbot
sudo apt install certbot -y
# Install plugin for your web server
sudo apt install python3-certbot-nginx -y # For Nginx
# OR
sudo apt install python3-certbot-apache -y # For Apache
Obtain Certificates¶
Certificate Locations¶
After successful issuance:
/etc/letsencrypt/live/your-domain.com/
├── fullchain.pem # Certificate + intermediate chain
├── privkey.pem # Private key
├── cert.pem # Certificate only
└── chain.pem # Intermediate chain only
Use fullchain.pem
Always use fullchain.pem in your reverse proxy configuration, not cert.pem.
Auto-Renewal¶
Let's Encrypt certificates expire after 90 days. Set up automatic renewal:
# Test renewal process (dry run)
sudo certbot renew --dry-run
# Setup automatic renewal (Certbot installs cron/systemd timer automatically)
sudo systemctl status certbot.timer
# Check renewal timer
sudo systemctl list-timers | grep certbot
# Manual renewal (if needed)
sudo certbot renew
Post-Renewal Hook¶
Reload web server after renewal:
Create /etc/letsencrypt/renewal-hooks/deploy/reload-webserver.sh:
#!/bin/bash
# Reload web server after certificate renewal
if systemctl is-active --quiet nginx; then
systemctl reload nginx
fi
if systemctl is-active --quiet apache2; then
systemctl reload apache2
fi
if systemctl is-active --quiet caddy; then
systemctl reload caddy
fi
Make executable:
Caddy (Automatic HTTPS)¶
Caddy automatically obtains and renews Let's Encrypt certificates.
Configuration¶
Simply specify your domain in the Caddyfile:
# Frontend (React + Nginx container)
your-domain.com {
reverse_proxy 127.0.0.1:8080
# HTTPS configured automatically!
}
# Backend (FastAPI)
api.your-domain.com {
reverse_proxy 127.0.0.1:8000
}
First Run¶
# Start Caddy
sudo systemctl start caddy
# Caddy will automatically:
# 1. Obtain Let's Encrypt certificates
# 2. Configure HTTPS
# 3. Redirect HTTP to HTTPS
# 4. Handle renewals
# Check logs
sudo journalctl -u caddy -f
No additional configuration needed!
Cloudflare SSL¶
If using Cloudflare for DNS:
Steps¶
- Add Domain to Cloudflare
- Sign up at cloudflare.com
- Add your domain
-
Update nameservers at your registrar
-
Enable SSL/TLS
- Go to SSL/TLS tab
-
Choose mode:
- Full (Strict): Recommended - Use with Let's Encrypt on origin
- Full: Origin cert can be self-signed
- Flexible: Not recommended - Origin uses HTTP
-
Origin Certificate (Recommended)
- Go to SSL/TLS > Origin Server
- Create Certificate
- Save certificate and private key
- Install on your server
Nginx Configuration with Cloudflare Origin Cert¶
server {
listen 443 ssl http2;
server_name your-domain.com;
# Cloudflare Origin Certificate
ssl_certificate /etc/ssl/cloudflare/your-domain.com.pem;
ssl_certificate_key /etc/ssl/cloudflare/your-domain.com.key;
# Cloudflare SSL settings
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers 'ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256';
ssl_prefer_server_ciphers on;
# ... rest of config
}
Commercial SSL Certificate¶
For enterprise requirements or specific compliance needs.
Purchase Certificate¶
Popular providers: - DigiCert - Industry standard, expensive - Sectigo - Good value - GlobalSign - Enterprise focus - GoDaddy - Budget option
Installation¶
- Generate CSR (Certificate Signing Request):
# Generate private key
sudo openssl genrsa -out /etc/ssl/private/your-domain.com.key 2048
# Generate CSR
sudo openssl req -new -key /etc/ssl/private/your-domain.com.key \
-out /etc/ssl/certs/your-domain.com.csr
# Submit CSR to certificate provider
cat /etc/ssl/certs/your-domain.com.csr
- Install Certificate (after provider issues it):
# Save certificate from provider to:
/etc/ssl/certs/your-domain.com.crt
# Save intermediate certificate to:
/etc/ssl/certs/your-domain.com-intermediate.crt
# Create full chain:
cat /etc/ssl/certs/your-domain.com.crt \
/etc/ssl/certs/your-domain.com-intermediate.crt > \
/etc/ssl/certs/your-domain.com-fullchain.crt
- Configure Nginx:
ssl_certificate /etc/ssl/certs/your-domain.com-fullchain.crt;
ssl_certificate_key /etc/ssl/private/your-domain.com.key;
Self-Signed Certificates (Testing Only)¶
Not for Production
Self-signed certificates should NEVER be used in production. Browsers will show security warnings.
Generate Self-Signed Certificate¶
# Generate private key and certificate (valid 365 days)
sudo openssl req -x509 -nodes -days 365 -newkey rsa:2048 \
-keyout /etc/ssl/private/selfsigned.key \
-out /etc/ssl/certs/selfsigned.crt \
-subj "/C=US/ST=State/L=City/O=Organization/CN=your-domain.com"
# Or generate with multiple domains (SAN)
sudo openssl req -x509 -nodes -days 365 -newkey rsa:2048 \
-keyout /etc/ssl/private/selfsigned.key \
-out /etc/ssl/certs/selfsigned.crt \
-config <(cat <<EOF
[req]
default_bits = 2048
prompt = no
default_md = sha256
distinguished_name = dn
req_extensions = v3_req
[dn]
C=US
ST=State
L=City
O=Organization
CN=your-domain.com
[v3_req]
subjectAltName = @alt_names
[alt_names]
DNS.1 = your-domain.com
DNS.2 = api.your-domain.com
DNS.3 = localhost
IP.1 = 127.0.0.1
EOF
)
Use in Nginx¶
Verification¶
Test SSL Configuration¶
# Test certificate with OpenSSL
openssl s_client -connect your-domain.com:443 -servername your-domain.com
# Check certificate expiration
echo | openssl s_client -connect your-domain.com:443 -servername your-domain.com 2>/dev/null | \
openssl x509 -noout -dates
# Online testing tools:
# https://www.ssllabs.com/ssltest/
# https://www.digicert.com/help/
Browser Testing¶
- Visit
https://your-domain.com - Click padlock icon in address bar
- Verify:
- ✅ Certificate is valid
- ✅ Issued to your domain
- ✅ Not expired
- ✅ Secure connection (TLS 1.2+)
Troubleshooting¶
"Certificate Not Trusted"¶
Cause: Missing intermediate certificate or self-signed certificate
Solutions:
- Ensure using fullchain.pem not cert.pem
- Verify intermediate certificates installed
- Check certificate chain: openssl s_client -connect your-domain.com:443 -showcerts
- Don't use self-signed certificates in production
"Too Many Failed Authorizations"¶
Cause: Let's Encrypt rate limit hit
Solutions:
- Wait 1 hour for rate limit reset
- Use --dry-run for testing
- Ensure DNS records are correct before running Certbot
- See Let's Encrypt rate limits
"Connection Not Secure" Warning¶
Cause: Browser doesn't trust certificate
Solutions: - Verify certificate is from trusted CA (not self-signed) - Check certificate hasn't expired - Ensure hostname matches certificate CN/SAN - Clear browser cache
Certificate Renewal Fails¶
Cause: Port 80 blocked or web server misconfigured
Solutions:
# Check if port 80 is accessible
sudo netstat -tlnp | grep :80
# Test renewal with verbose output
sudo certbot renew --dry-run --verbose
# Check Certbot logs
sudo cat /var/log/letsencrypt/letsencrypt.log
Mixed Content Warnings¶
Cause: Page loads over HTTPS but resources use HTTP
Solutions: - Ensure all resources (images, scripts, stylesheets) use HTTPS - Check browser console for mixed content errors - Update hardcoded HTTP URLs to use HTTPS - Enable automatic HTTPS rewrites in reverse proxy
Security Best Practices¶
- ✅ Use Let's Encrypt for free, trusted certificates
- ✅ Enable HSTS (HTTP Strict Transport Security)
- ✅ Use TLSv1.2 or TLSv1.3 only (disable older versions)
- ✅ Disable weak ciphers
- ✅ Enable OCSP stapling
- ✅ Monitor certificate expiration
- ✅ Set up renewal alerts
- ✅ Use strong private keys (2048-bit minimum, 4096-bit recommended)
- ✅ Protect private keys (600 permissions)
- ✅ Never commit private keys to Git
Enhanced Nginx SSL Configuration¶
# Modern SSL configuration
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 (HTTP Strict Transport Security)
add_header Strict-Transport-Security "max-age=63072000; includeSubDomains; preload" always;
# OCSP Stapling
ssl_stapling on;
ssl_stapling_verify on;
ssl_trusted_certificate /etc/letsencrypt/live/your-domain.com/chain.pem;
resolver 8.8.8.8 8.8.4.4 valid=300s;
resolver_timeout 5s;
# Session settings
ssl_session_timeout 1d;
ssl_session_cache shared:SSL:50m;
ssl_session_tickets off;
Monitoring¶
Certificate Expiration Alerts¶
Create monitoring script /usr/local/bin/check-ssl-expiry.sh:
#!/bin/bash
# Check SSL certificate expiration
DOMAIN="your-domain.com"
ALERT_DAYS=30
# Get expiry date
EXPIRY_DATE=$(echo | openssl s_client -connect $DOMAIN:443 -servername $DOMAIN 2>/dev/null | \
openssl x509 -noout -enddate | cut -d= -f2)
# Convert to seconds
EXPIRY_EPOCH=$(date -d "$EXPIRY_DATE" +%s)
NOW_EPOCH=$(date +%s)
DAYS_LEFT=$(( ($EXPIRY_EPOCH - $NOW_EPOCH) / 86400 ))
if [ $DAYS_LEFT -lt $ALERT_DAYS ]; then
echo "WARNING: SSL certificate for $DOMAIN expires in $DAYS_LEFT days!"
# Send alert (email, Slack, etc.)
fi
Add to crontab:
Next Steps¶
- Configure Health Monitoring
- Review Security Hardening
- Set up Reverse Proxy (if not done yet)