Skip to content

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

# Automatic configuration (modifies Nginx config)
sudo certbot --nginx -d your-domain.com -d api.your-domain.com

# Follow prompts:
# 1. Enter email address
# 2. Agree to terms
# 3. Choose whether to redirect HTTP to HTTPS (recommended: Yes)
# Only obtain certificates (manual Nginx configuration)
sudo certbot certonly --nginx -d your-domain.com -d api.your-domain.com

# Certificates saved to:
# /etc/letsencrypt/live/your-domain.com/fullchain.pem
# /etc/letsencrypt/live/your-domain.com/privkey.pem
# Automatic configuration
sudo certbot --apache -d your-domain.com -d api.your-domain.com
# Stop your web server temporarily
sudo systemctl stop nginx  # or apache2

# Obtain certificate
sudo certbot certonly --standalone -d your-domain.com -d api.your-domain.com

# Restart web server
sudo systemctl start nginx  # or apache2

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:

sudo chmod +x /etc/letsencrypt/renewal-hooks/deploy/reload-webserver.sh

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

  1. Add Domain to Cloudflare
  2. Sign up at cloudflare.com
  3. Add your domain
  4. Update nameservers at your registrar

  5. Enable SSL/TLS

  6. Go to SSL/TLS tab
  7. 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
  8. Origin Certificate (Recommended)

  9. Go to SSL/TLS > Origin Server
  10. Create Certificate
  11. Save certificate and private key
  12. 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

  1. 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
  1. 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
  1. 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

ssl_certificate /etc/ssl/certs/selfsigned.crt;
ssl_certificate_key /etc/ssl/private/selfsigned.key;

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

  1. Visit https://your-domain.com
  2. Click padlock icon in address bar
  3. Verify:
  4. ✅ Certificate is valid
  5. ✅ Issued to your domain
  6. ✅ Not expired
  7. ✅ 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:

# Run daily at 9 AM
0 9 * * * /usr/local/bin/check-ssl-expiry.sh

Next Steps