SSL Certificates¶
This guide covers how HTTPS is set up for the AWS deployment, how auto-renewal works, and what to do if something goes wrong.
Overview¶
Both 2sigma.io and docs.2sigma.io are served over HTTPS using a free SSL certificate from Let's Encrypt, managed by certbot. A single certificate covers both domains. HTTP requests are automatically redirected to HTTPS via a 301 in nginx.
The certificate auto-renews via a systemd timer that runs twice daily. No manual intervention is needed under normal circumstances.
How It Works¶
Let's Encrypt is a free, automated certificate authority. It issues certificates that browsers trust, just like paid CAs, but at no cost.
Certbot is the tool that talks to Let's Encrypt. It requests the certificate, proves you control the domain (by temporarily serving a challenge file on port 80), and saves the cert files to /etc/letsencrypt/.
Certificates from Let's Encrypt expire every 90 days. This is intentional on their part: short lifetimes encourage automation and limit the damage if a cert is ever compromised. Certbot handles renewal automatically so you never need to think about the 90-day window.
The renewal flow:
- A systemd timer fires twice daily (3 AM and 3 PM UTC, with a random delay up to 1 hour)
- It runs
certbot renew --quiet - Certbot checks if any cert is within 30 days of expiry. If not, it does nothing
- If renewal is needed, certbot requests a new cert from Let's Encrypt
- After a successful renewal, the deploy hook at
/etc/letsencrypt/renewal-hooks/deploy/reload-nginx.shruns automatically and reloads nginx so it picks up the new cert files
HTTP-to-HTTPS redirection is handled by a dedicated nginx server block that listens on port 80 and returns a 301 to the HTTPS equivalent URL.
Current Setup¶
| Item | Value |
|---|---|
| Certificate path | /etc/letsencrypt/live/2sigma.io/ |
| Domains covered | 2sigma.io, docs.2sigma.io |
| Issued | Feb 18, 2026 |
| Expires | May 19, 2026 (auto-renews before then) |
| Renewal timer | certbot-renew.timer (systemd, twice daily) |
| Timer schedule | 03:00 and 15:00 UTC, RandomizedDelaySec=3600 |
| Deploy hook | /etc/letsencrypt/renewal-hooks/deploy/reload-nginx.sh |
Infrastructure Files¶
| File | What changed |
|---|---|
deploy/nginx/nginx.conf |
HTTP-to-HTTPS redirect server block; two HTTPS server blocks (app + docs) with ssl_certificate directives; HSTS header |
deploy/docker-compose.yml |
Port 443 exposed; /etc/letsencrypt mounted read-only into the nginx container |
deploy/.env.production |
CORS_ORIGINS updated to include https://2sigma.io |
terraform/main.tf |
Port 443 was already open in the security group (no change needed) |
Initial Setup (from scratch)¶
Use this if you ever need to set up SSL on a new EC2 instance.
Step 1: Verify DNS
Both domains must have A records pointing to the EC2 Elastic IP before certbot can issue a certificate. Let's Encrypt will try to reach your server over the public internet to verify domain ownership.
| Record | Type | Value |
|---|---|---|
2sigma.io |
A | 3.151.25.120 |
docs.2sigma.io |
A | 3.151.25.120 |
Step 2: Install certbot
Step 3: Stop nginx to free port 80
Certbot's standalone mode starts its own temporary web server on port 80 to complete the domain challenge. Nginx must not be holding that port.
Step 4: Request the certificate
sudo certbot certonly --standalone --non-interactive --agree-tos \
--email admin@2sigma.io \
-d 2sigma.io \
-d docs.2sigma.io
Certbot saves the cert files to /etc/letsencrypt/live/2sigma.io/.
Step 5: Create the deploy hook
This script reloads nginx after each successful renewal so it picks up the new cert without a full restart.
sudo tee /etc/letsencrypt/renewal-hooks/deploy/reload-nginx.sh > /dev/null << 'EOF'
#!/bin/bash
cd /home/ec2-user/ai-tutor-backend/deploy
docker compose --env-file .env.production exec -T nginx nginx -s reload
EOF
sudo chmod +x /etc/letsencrypt/renewal-hooks/deploy/reload-nginx.sh
Step 6: Create the systemd timer and service
Create the service unit:
sudo tee /etc/systemd/system/certbot-renew.service > /dev/null << 'EOF'
[Unit]
Description=Certbot renewal
[Service]
Type=oneshot
ExecStart=/usr/bin/certbot renew --quiet
EOF
Create the timer unit:
sudo tee /etc/systemd/system/certbot-renew.timer > /dev/null << 'EOF'
[Unit]
Description=Run certbot renewal twice daily
[Timer]
OnCalendar=*-*-* 03,15:00:00
RandomizedDelaySec=3600
Persistent=true
[Install]
WantedBy=timers.target
EOF
Step 7: Enable the timer
Step 8: Start nginx
Step 9: Verify
Both should return a 200 response. If you see a certificate error, check that the cert paths in nginx.conf match /etc/letsencrypt/live/2sigma.io/.
Adding a New Domain¶
When adding another subdomain, you must reissue the certificate listing all domains together. Certbot replaces the existing cert rather than adding to it.
Step 1: Stop nginx
Step 2: Reissue with all domains
List every domain
You must include all existing domains plus the new one. Omitting a domain removes it from the certificate.
Step 3: Add the nginx server block
Add a new server block in deploy/nginx/nginx.conf for the new subdomain, pointing at the same cert files.
Step 4: Start nginx
Checking Certificate Status¶
Check what certs are installed and when they expire:
Check that the renewal timer is active:
Test renewal without actually renewing (dry run):
cd ~/ai-tutor-backend/deploy && docker compose --env-file .env.production stop nginx
sudo certbot renew --dry-run
docker compose --env-file .env.production up -d nginx
Dry run requires stopping nginx
The dry run uses the same standalone challenge as the real renewal, so port 80 must be free.
Inspect the cert from outside the server:
echo | openssl s_client -servername 2sigma.io -connect 2sigma.io:443 2>/dev/null \
| openssl x509 -noout -subject -dates
This shows the subject (domains covered) and the notAfter expiry date without needing SSH access.
Troubleshooting¶
"Challenge failed" during certbot¶
Two likely causes:
- DNS A records don't point to this EC2 instance yet. Verify with
dig 2sigma.ioornslookup 2sigma.io. - Port 80 is still in use. Nginx must be stopped before running certbot in standalone mode. Check with
sudo ss -tlnp | grep :80.
Nginx won't start after cert setup¶
Check that the ssl_certificate and ssl_certificate_key paths in nginx.conf match the actual cert location:
Also confirm the /etc/letsencrypt directory is mounted into the nginx container in docker-compose.yml.
Certificate expired / browser shows security warning¶
First check whether the timer is running:
If the timer is inactive or failed, re-enable it:
Check the renewal log for errors:
If the cert is already expired, you'll need to stop nginx, run sudo certbot renew --force-renewal, then start nginx again.
Mixed content warnings in the browser¶
The browser is loading some resources over HTTP while the page itself is HTTPS. This usually means the frontend is making API calls to http://... instead of using a relative path.
Check that NEXT_PUBLIC_API_BASE_URL is set to /api/v1 (relative) rather than http://3.151.25.120/api/v1 or any other absolute HTTP URL. Relative URLs inherit the protocol of the page, so they work correctly under HTTPS without any changes.