How to Set Up HTTPS on EC2 with Let's Encrypt and Nginx

Running a public website over plain HTTP means every browser flags it as 'Not Secure' — and beyond the UX damage, unencrypted traffic exposes session tokens, form data, and API keys to any network observer between your EC2 instance and the user. Setting up HTTPS on EC2 using Let's Encrypt and Nginx eliminates that risk at zero certificate cost, and the entire process takes under 30 minutes on a properly configured instance.

TL;DR: HTTPS on EC2 with Let's Encrypt

PhaseActionTool
1. PrereqsOpen ports 80 & 443, point DNS A record to EC2 public IPAWS Console / Route 53
2. InstallInstall Nginx and Certbotapt / yum
3. CertifyRun certbot --nginx to obtain and install certificateCertbot
4. VerifyConfirm auto-renewal timer is activesystemctl / cron
5. HardenEnforce HTTPS redirect, review cipher configNginx config

How Let's Encrypt Certificate Issuance Works on EC2

Let's Encrypt uses the ACME protocol to prove you control a domain before issuing a certificate. The most common challenge type — HTTP-01 — requires that Let's Encrypt's validation servers can reach a specific URL on port 80 of your domain. Certbot temporarily serves a token file at /.well-known/acme-challenge/ via Nginx, Let's Encrypt fetches it, and upon success issues a 90-day certificate. Certbot then rewrites your Nginx config to enable TLS on port 443 and optionally adds an HTTP→HTTPS redirect. The certificate lives at /etc/letsencrypt/live/<domain>/ and must be renewed before expiry — Certbot installs a systemd timer or cron job to handle this automatically.

sequenceDiagram participant B as Browser participant N as Nginx (EC2) participant C as Certbot participant L as Let's Encrypt ACME B->>N: HTTP request (port 80) C->>L: Request certificate for domain L->>C: Issue HTTP-01 challenge token C->>N: Write token to /.well-known/acme-challenge/ L->>N: Fetch challenge token (port 80) N-->>L: Return token file L-->>C: Domain validated — certificate issued C->>N: Update nginx config with cert/key paths B->>N: HTTPS request (port 443) N-->>B: TLS-encrypted response
  1. Browser → EC2 (port 80): Initial HTTP request hits Nginx.
  2. Certbot → Let's Encrypt: Certbot requests a certificate and receives a challenge token.
  3. Let's Encrypt → EC2 (port 80): ACME servers validate domain ownership by fetching the challenge file.
  4. Certificate issued: Certbot writes the cert/key to /etc/letsencrypt/live/ and updates Nginx config.
  5. Browser → EC2 (port 443): All subsequent traffic is TLS-encrypted.

Prerequisites Before Running Certbot

Two infrastructure requirements must be satisfied before Certbot can succeed — skipping either produces a cryptic timeout error that looks like a Certbot bug but is actually a network or DNS problem.

1. Security Group: Open Ports 80 and 443

Let's Encrypt's validation servers originate from public IPs outside your VPC. Your EC2 Security Group must allow inbound TCP on both port 80 (for the ACME challenge) and port 443 (for HTTPS traffic). Port 80 cannot be blocked even if you intend to redirect all traffic to HTTPS — the challenge happens over HTTP.

# Verify current inbound rules on your Security Group
aws ec2 describe-security-groups \
  --group-ids sg-0123456789abcdef0 \
  --query 'SecurityGroups[*].IpPermissions' \
  --region us-east-1

# Add port 80 if missing
aws ec2 authorize-security-group-ingress \
  --group-id sg-0123456789abcdef0 \
  --protocol tcp \
  --port 80 \
  --cidr 0.0.0.0/0 \
  --region us-east-1

# Add port 443 if missing
aws ec2 authorize-security-group-ingress \
  --group-id sg-0123456789abcdef0 \
  --protocol tcp \
  --port 443 \
  --cidr 0.0.0.0/0 \
  --region us-east-1

2. DNS: Point Your Domain to the EC2 Public IP

The ACME HTTP-01 challenge resolves your domain via public DNS — it does not accept an IP address directly. Your domain's A record must resolve to your EC2 instance's public IP before you run Certbot. If you're using an Elastic IP, assign it to the instance first so the address doesn't change on stop/start cycles.

# Confirm your domain resolves to the correct IP
nslookup yourdomain.com

# Or with dig
dig +short yourdomain.com A
Think of the ACME challenge like a courier requiring a signature at the address on the package. If the address (DNS) doesn't point to your door (EC2), the delivery fails — regardless of how valid your identity is.

Step 1: Install Nginx on Your EC2 Instance

Connect to your instance via SSH, then install Nginx. The package name and command differ by AMI family — Ubuntu/Debian use apt, Amazon Linux 2 / RHEL use yum or dnf.

Ubuntu / Debian:

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

Amazon Linux 2:

sudo amazon-linux-extras install nginx1 -y
sudo systemctl enable nginx
sudo systemctl start nginx

Verify Nginx is serving traffic before proceeding — Certbot's Nginx plugin requires a running Nginx process to modify:

curl -I http://yourdomain.com

You should see HTTP/1.1 200 OK (or a redirect). A connection refused here means either Nginx isn't running or port 80 is still blocked at the Security Group level.

Step 2: Install Certbot with the Nginx Plugin

Certbot's installation method varies by OS. The Snap-based installation is the approach recommended by the Certbot project for most Linux distributions, as it provides the most up-to-date version independent of OS package repositories.

Ubuntu / Debian (Snap — recommended):

sudo snap install --classic certbot
sudo ln -s /snap/bin/certbot /usr/bin/certbot

Amazon Linux 2 (via EPEL):

sudo amazon-linux-extras install epel -y
sudo yum install certbot python3-certbot-nginx -y

Confirm Certbot is accessible:

certbot --version

Step 3: Obtain and Install the Certificate — HTTPS on EC2 in One Command

The --nginx plugin does the heavy lifting: it obtains the certificate, modifies your Nginx server block to reference the cert and key files, and optionally configures the HTTP-to-HTTPS redirect. Run it as root:

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

Certbot will prompt you for:

  1. An email address for expiry notifications and account recovery.
  2. Agreement to the Let's Encrypt Terms of Service.
  3. Whether to redirect HTTP traffic to HTTPS (choose 2: Redirect unless you have a specific reason not to).

On success, Certbot prints the certificate path and expiry date. The certificate and private key are stored at:

/etc/letsencrypt/live/yourdomain.com/fullchain.pem
/etc/letsencrypt/live/yourdomain.com/privkey.pem

Step 4: Verify the Nginx Configuration Certbot Generated

Certbot modifies your Nginx server block automatically, but it's worth reviewing what it wrote — especially if you have a custom upstream proxy configuration that Certbot may have partially overwritten.

sudo nginx -t
sudo cat /etc/nginx/sites-enabled/default

A correctly modified server block will contain ssl_certificate and ssl_certificate_key directives pointing to the Let's Encrypt paths, and a separate server block on port 80 that issues a 301 redirect to HTTPS. If nginx -t reports a syntax error, do not reload — fix the config first.

🔽 Click to expand: Example Certbot-generated Nginx server block
server {
    listen 80;
    server_name yourdomain.com www.yourdomain.com;
    return 301 https://$host$request_uri;
}

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

    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 / {
        # Your application proxy or root config
        proxy_pass http://localhost:3000;
        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;
    }
}

After confirming the config is valid, reload Nginx to apply it:

sudo systemctl reload nginx

Step 5: Confirm Auto-Renewal Is Active

Let's Encrypt certificates expire after 90 days. Certbot installs a renewal mechanism automatically, but you should verify it's actually scheduled — a silent failure here means your site goes back to HTTP in 90 days with no warning until the expiry notification email arrives.

Check the systemd timer (Snap installs):

sudo systemctl status snap.certbot.renew.timer

Check the cron job (package manager installs):

sudo cat /etc/cron.d/certbot

Perform a dry-run renewal to confirm the full renewal path works:

sudo certbot renew --dry-run

A successful dry-run prints Congratulations, all simulated renewals succeeded. If it fails, the error output will identify whether the problem is DNS, port 80 access, or a Certbot configuration issue — all of which are easier to fix now than at 3am when the certificate actually expires.

Experience Signal: The Misdiagnosed Renewal Failure

A common production failure pattern: the initial certificate installs cleanly, the site shows the padlock, and everything looks fine. Then 89 days later, monitoring alerts fire because the certificate expired. The engineer checks Certbot logs and sees renewal failures going back weeks — but no one noticed because the failure emails went to a shared inbox that nobody reads.

The actual cause is almost never Certbot itself. The renewal fails because a deployment pipeline change added a new Security Group rule that blocks all inbound traffic except from a specific CIDR — which inadvertently blocked Let's Encrypt's validation servers on port 80. The symptom is an expired certificate. The misdiagnosis is "Certbot stopped working." The actual cause is a Security Group change that broke the ACME challenge path.

The fix: always run certbot renew --dry-run after any Security Group or firewall change, and treat renewal failures as a P1 alert — not a background task.

graph TD A["systemd timer fires certbot renew"] --> B["Certbot requests ACME challenge"] B --> C["Let's Encrypt fetches port 80 on domain"] C --> D{"Security Group allows port 80?"} D -- Yes --> E["Challenge succeeds Certificate renewed"] D -- No --> F["Connection timeout Renewal fails silently"] F --> G["Certbot logs error No alert triggered"] G --> H["Certificate expires HTTPS breaks"] E --> I["Nginx reloaded with new cert"]
  1. Certbot renew triggered: systemd timer fires the renewal attempt.
  2. ACME challenge sent: Let's Encrypt attempts to reach port 80 on your domain.
  3. Security Group blocks: A restrictive inbound rule silently drops the validation request.
  4. Renewal fails silently: Certbot logs the error, but no alert fires until expiry.
  5. Certificate expires: HTTPS breaks; browsers show security warnings.

Depth Signal: The Elastic IP and DNS Propagation Ordering Problem

There's a non-obvious ordering dependency that breaks first-time setups: engineers often allocate an Elastic IP, update the DNS A record, and immediately run Certbot — before DNS propagation completes. The ACME validation servers resolve the domain, get the old IP (or NXDOMAIN), and the challenge fails with a connection timeout. Certbot's error message says "Failed to connect to host for DVSNI challenge" which sounds like a TLS problem, not a DNS problem.

The correct sequence is: allocate Elastic IP → associate with instance → update DNS → wait for propagation (verify with dig +short yourdomain.com from an external resolver) → then run Certbot. DNS TTL values on the old record determine how long you wait — if the previous TTL was 3600 seconds, you may need to wait up to an hour. Lowering the TTL to 60 seconds before making the change is a standard operational practice that most guides omit.

Wrap-Up and Next Steps for HTTPS on EC2

At this point your EC2 instance is serving HTTPS with a valid Let's Encrypt certificate, Nginx is redirecting all HTTP traffic to HTTPS, and auto-renewal is scheduled. The padlock is live. A few hardening steps worth considering next:

  • Test your TLS configuration at SSL Labs Server Test — Certbot's default options-ssl-nginx.conf is reasonably secure, but this confirms your cipher suite and protocol versions.
  • Add HSTS — once you're confident HTTPS is stable, add add_header Strict-Transport-Security "max-age=31536000" always; to your Nginx server block to instruct browsers to never attempt HTTP.
  • Consider AWS Certificate Manager (ACM) — if you later move to an Application Load Balancer in front of EC2, ACM provides managed certificates that renew automatically without any Certbot configuration. ACM certificates cannot be installed directly on EC2 instances; they require an AWS load balancer or CloudFront distribution.
  • Monitor certificate expiry — set up a CloudWatch alarm or external monitor on certificate expiry date as a safety net independent of Certbot's renewal mechanism.

Official references: Certbot Instructions (EFF) | Let's Encrypt Documentation | EC2 Security Groups (AWS Docs)

Glossary

TermDefinition
ACME ProtocolAutomated Certificate Management Environment — the protocol Let's Encrypt uses to verify domain ownership and issue certificates.
HTTP-01 ChallengeA domain validation method where Let's Encrypt fetches a token file from port 80 of your domain to confirm you control it.
CertbotAn open-source ACME client maintained by the EFF that automates certificate issuance and Nginx/Apache configuration.
Elastic IPA static public IPv4 address in AWS that persists across EC2 instance stop/start cycles.
HSTSHTTP Strict Transport Security — a response header that instructs browsers to only connect via HTTPS for a specified duration.

Related Posts

Comments

Popular posts from this blog

EC2 No Internet Access in Custom VPC: Fix Internet Gateway and Route Table

EC2 SSH Connection Timeout: Which Security Group Rules to Check

Difference Between IAM User and IAM Role: Which One Should Your EC2 Use?