HTTPS on EC2: Free SSL with Let's Encrypt, Certbot & Nginx

Your EC2-hosted site is live, but browsers are stamping it with a red 'Not Secure' warning — a trust-killer for users and a ranking penalty from search engines. This guide walks you through obtaining a free, auto-renewing SSL/TLS certificate from Let's Encrypt using Certbot and wiring it into Nginx on an Amazon EC2 instance running Ubuntu.

TL;DR — What You'll Do

StepActionOutcome
1Open ports 80 & 443 in Security GroupEC2 accepts HTTP/HTTPS traffic
2Point your domain's A record to EC2 Elastic IPDomain resolves to your instance
3Install NginxWeb server ready to serve requests
4Install Certbot + Nginx pluginCertificate toolchain in place
5Run certbot --nginxCertificate issued & Nginx auto-configured
6Verify auto-renewalCertificate renews every 60–90 days automatically

Architecture Overview

Before touching the terminal, understand the full request path from browser to your application:

graph LR Browser["🌐 Browser"] -->|"TCP 443"| SG["AWS Security Group
(Inbound: 80, 443)"] SG --> Nginx["Nginx
(TLS Termination)"] Nginx -->|"HTTP proxy"| App["Your App
(e.g., Node.js :3000)"] Nginx -.->|"Certificate files"| Cert["/etc/letsencrypt/live/
fullchain.pem + privkey.pem"] LE["Let's Encrypt CA"] -.->|"Issues cert via Certbot"| Cert Browser -->|"TCP 80 (redirect)"| SG
  1. Browser initiates a TLS handshake on port 443 to your EC2 Elastic IP.
  2. AWS Security Group acts as the first gate — it must allow inbound TCP 443 (and 80 for HTTP→HTTPS redirect).
  3. Nginx terminates TLS using the Let's Encrypt certificate stored on disk, then proxies the decrypted request to your app (e.g., Node.js on port 3000).
  4. Let's Encrypt CA is only involved during certificate issuance and renewal — not on every request.
  5. Certbot's ACME challenge temporarily serves a token over HTTP (port 80) to prove domain ownership during issuance.

Prerequisites

  • EC2 instance running Ubuntu 22.04 LTS (steps are similar for 20.04)
  • An Elastic IP associated with the instance
  • A registered domain name with an A record pointing to that Elastic IP
  • SSH access with sudo privileges
Analogy: Think of Let's Encrypt as a free notary service. Certbot is your paralegal — it prepares the paperwork, proves you own the property (domain), gets the notary's stamp (certificate), and files it with Nginx. The notary never sits in your office; it only shows up when the stamp needs renewing.

Step 1 — Configure EC2 Security Group

Your Security Group is a stateful firewall at the AWS network layer. Without opening ports 80 and 443, no external traffic reaches Nginx — and Certbot's ACME HTTP-01 challenge will fail silently.

Navigate to EC2 → Security Groups → Inbound Rules → Edit and add:

TypeProtocolPortSourcePurpose
HTTPTCP800.0.0.0/0, ::/0ACME challenge + HTTP→HTTPS redirect
HTTPSTCP4430.0.0.0/0, ::/0Encrypted traffic
SSHTCP22Your IP onlyAdmin access (least privilege)

Or via AWS CLI:

# Replace sg-xxxxxxxxxxxxxxxxx with your Security Group ID
aws ec2 authorize-security-group-ingress \
  --group-id sg-xxxxxxxxxxxxxxxxx \
  --protocol tcp --port 80 --cidr 0.0.0.0/0

aws ec2 authorize-security-group-ingress \
  --group-id sg-xxxxxxxxxxxxxxxxx \
  --protocol tcp --port 443 --cidr 0.0.0.0/0

Step 2 — Assign an Elastic IP (If Not Done)

A dynamic public IP changes on every instance stop/start, breaking your DNS A record. An Elastic IP is a static IPv4 address you own until you release it.

# Allocate a new Elastic IP
aws ec2 allocate-address --domain vpc

# Associate it with your instance (replace with real IDs)
aws ec2 associate-address \
  --instance-id i-0123456789abcdef0 \
  --allocation-id eipalloc-0123456789abcdef0

Then update your domain registrar's DNS: add an A record pointing yourdomain.com (and www.yourdomain.com) to this Elastic IP. DNS propagation can take up to 48 hours, but typically resolves within minutes.

Step 3 — Install Nginx

sudo apt update && sudo apt upgrade -y
sudo apt install nginx -y
sudo systemctl enable nginx
sudo systemctl start nginx

Verify Nginx is running:

sudo systemctl status nginx
# Expected: Active: active (running)

Visiting http://<your-elastic-ip> in a browser should show the default Nginx welcome page.

Step 4 — Create an Nginx Server Block for Your Domain

Certbot needs a server block that references your domain name before it can configure HTTPS. Create a minimal one:

🔽 [Click to expand] /etc/nginx/sites-available/yourdomain.com
server {
    listen 80;
    listen [::]:80;

    server_name yourdomain.com www.yourdomain.com;

    root /var/www/yourdomain.com/html;
    index index.html index.htm;

    location / {
        try_files $uri $uri/ =404;
    }
}
# Create the web root directory
sudo mkdir -p /var/www/yourdomain.com/html
sudo chown -R $USER:$USER /var/www/yourdomain.com/html

# Enable the site by symlinking to sites-enabled
sudo ln -s /etc/nginx/sites-available/yourdomain.com \
           /etc/nginx/sites-enabled/

# Test config syntax
sudo nginx -t

# Reload Nginx
sudo systemctl reload nginx

Step 5 — Install Certbot with the Nginx Plugin

The recommended installation method on Ubuntu 22.04 is via snap, as maintained by the Electronic Frontier Foundation (EFF):

# Remove any OS-packaged certbot to avoid conflicts
sudo apt remove certbot -y

# Install via snap (ensures latest version)
sudo snap install --classic certbot

# Create a symlink so certbot is in PATH
sudo ln -s /snap/bin/certbot /usr/bin/certbot

Step 6 — Obtain and Install the Certificate

This single command handles everything: domain validation via HTTP-01 ACME challenge, certificate issuance, and Nginx configuration update.

sudo certbot --nginx -d yourdomain.com -d www.yourdomain.com

Certbot will prompt you for:

  1. An email address for expiry notifications
  2. Agreement to the Let's Encrypt Terms of Service
  3. Whether to share your email with EFF (optional)
  4. Whether to redirect HTTP to HTTPS (choose 2: Redirect — always recommended)

On success, Certbot modifies your Nginx server block to add SSL directives and creates a redirect from port 80 to 443.

What Certbot Writes to Your Nginx Config

After running Certbot, your server block will look similar to this:

🔽 [Click to expand] Modified /etc/nginx/sites-available/yourdomain.com
server {
    listen 80;
    server_name yourdomain.com www.yourdomain.com;
    # Certbot adds this redirect block:
    return 301 https://$host$request_uri;
}

server {
    listen 443 ssl;
    server_name yourdomain.com www.yourdomain.com;

    root /var/www/yourdomain.com/html;
    index index.html;

    # Managed by Certbot
    ssl_certificate /etc/letsencrypt/live/yourdomain.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/yourdomain.com/privkey.pem;
    include /etc/letsencrypt/options-ssl-nginx.conf;
    ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem;

    location / {
        try_files $uri $uri/ =404;
    }
}

Step 7 — Verify Auto-Renewal

Let's Encrypt certificates are valid for 90 days. Certbot installs a systemd timer (or cron job) that attempts renewal twice daily when the certificate is within 30 days of expiry.

# Check the renewal timer status
sudo systemctl status snap.certbot.renew.timer

# Perform a dry-run to confirm renewal works without issuing a cert
sudo certbot renew --dry-run

Expected output includes: Congratulations, all simulated renewals succeeded.

Step 8 — (Optional) Reverse Proxy to a Backend App

If Nginx is fronting a Node.js, Python, or Java app running on a local port, update the location / block:

🔽 [Click to expand] Nginx reverse proxy config snippet
location / {
    proxy_pass http://127.0.0.1:3000;
    proxy_http_version 1.1;
    proxy_set_header Upgrade $http_upgrade;
    proxy_set_header Connection 'upgrade';
    proxy_set_header Host $host;
    proxy_set_header X-Real-IP $remote_addr;
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    proxy_set_header X-Forwarded-Proto $scheme;
    proxy_cache_bypass $http_upgrade;
}
sudo nginx -t && sudo systemctl reload nginx

Certificate Issuance Flow (Sequence Diagram)

sequenceDiagram participant You as You (EC2 Terminal) participant Certbot participant Nginx as Nginx (port 80) participant LE as Lets Encrypt ACME API You->>Certbot: certbot --nginx -d yourdomain.com Certbot->>LE: Request certificate + challenge token LE-->>Certbot: HTTP-01 challenge token Certbot->>Nginx: Serve token at /.well-known/acme-challenge/ LE->>Nginx: GET /.well-known/acme-challenge/token Nginx-->>LE: Token response LE-->>Certbot: Domain validated - certificate issued Certbot->>Nginx: Write cert paths to config Certbot->>You: HTTPS activated
  1. You run certbot --nginx on the EC2 instance.
  2. Certbot contacts the Let's Encrypt ACME API and requests a challenge token for your domain.
  3. Let's Encrypt makes an HTTP request to http://yourdomain.com/.well-known/acme-challenge/<token> to verify you control the domain.
  4. Certbot temporarily serves the token response via Nginx on port 80.
  5. Let's Encrypt validates the response and issues the signed certificate.
  6. Certbot writes the certificate files to /etc/letsencrypt/live/yourdomain.com/ and updates the Nginx config.

Common Errors & Fixes

ErrorRoot CauseFix
Connection refused on port 80Security Group blocks port 80Add inbound rule for TCP 80
DNS problem: NXDOMAINA record not propagated yetWait for DNS propagation; verify with dig yourdomain.com
Too many certificates already issuedLet's Encrypt rate limit hit (5 certs/domain/week)Use --staging flag for testing; wait for rate limit reset
nginx: [emerg] unknown directiveCertbot wrote config with syntax errorRun sudo nginx -t to identify line; check Certbot version
Certificate not renewingPort 80 blocked or Nginx down at renewal timeEnsure port 80 stays open; check certbot renew --dry-run

Security Hardening Checklist

  • Keep port 80 open — required for HTTP-01 renewal challenges (Certbot handles the redirect to HTTPS)
  • Restrict SSH (port 22) to your specific IP in the Security Group
  • Enable UFW on the OS level as a secondary firewall layer: sudo ufw allow 'Nginx Full'
  • Use HSTS — add add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always; to your HTTPS server block
  • Disable weak TLS versions — Certbot's options-ssl-nginx.conf already enforces TLS 1.2+ by default
  • Never expose private keys/etc/letsencrypt/live/ is root-only by default; do not change permissions

Glossary

TermDefinition
Let's EncryptA free, automated, open Certificate Authority (CA) run by the Internet Security Research Group (ISRG)
CertbotEFF's open-source ACME client that automates certificate issuance and renewal from Let's Encrypt
ACMEAutomated Certificate Management Environment — the protocol Certbot uses to communicate with Let's Encrypt
HTTP-01 ChallengeDomain ownership proof method where Let's Encrypt fetches a token file served over HTTP on port 80
Elastic IPA static public IPv4 address in AWS that persists independently of the EC2 instance lifecycle

Next Steps

Related Posts

Comments

Popular posts from this blog

EC2 No Internet Access in Custom VPC: Attaching an Internet Gateway and Fixing Route Tables

IAM User vs. IAM Role: Why Your EC2 Instance Should Never Use a User

EC2 SSH Connection Timeout: The Exact Security Group Rules You Need to Fix It