Field Notes: Hardening Nginx TLS on a Cloud VPS for a Public API
Field Notes: Hardening Nginx TLS on a Cloud VPS for a Public API
When a small SaaS startup exposed its JSON API to the internet, the first wave of customers reported intermittent SSL handshake failures and, more worryingly, a few security‑scan alerts flagging weak ciphers. The engineering team needed a quick, reproducible hardening process that would keep the API reachable, comply with industry best‑practice TLS settings, and stay within the modest resources of a single‑CPU Cloud VPS.
Problem Statement
The API runs on a Ubuntu 22.04 LTS droplet with nginx acting as a reverse proxy to a Node.js service listening on localhost:3000. Initial Nginx configuration used the default ssl_protocols and ssl_ciphers directives, which left the server vulnerable to:
- TLS 1.0/1.1 support (deprecated).
- Cipher suites susceptible to MITM attacks (e.g.,
RC4,3DES). - Missing HTTP Strict Transport Security (HSTS) header.
- No OCSP stapling, increasing latency for certificate validation.
Complicating factors:
- The VPS provides only 1 vCPU and 1 GB RAM – heavy‑weight solutions must stay lightweight.
- Zero‑downtime is mandatory; the API cannot be taken offline for a prolonged period.
- Compliance requirements demand at least TLS 1.2 with forward‑secrecy.
Constraints and Assumptions
- We assume a valid Let’s Encrypt certificate is already issued.
- All traffic must be forced over HTTPS; HTTP must redirect to HTTPS.
- Only standard Ubuntu repositories may be used – no third‑party binaries that could increase attack surface.
- Monitoring and alerting are already in place via
systemdandnetdata, so we only need to add health checks for TLS.
Technical Decisions
After evaluating common hardening patterns, the team settled on the following:
- Use
nginxbuilt‑in TLS features – no external reverse‑proxy or load balancer. - Enable only TLS 1.2 and TLS 1.3, disabling older protocols.
- Adopt the Mozilla “Intermediate” cipher suite recommendation, which balances security and compatibility.
- Add HSTS with a 6‑month max‑age and include subdomains.
- Enable OCSP stapling to reduce client latency.
- Leverage
systemdsocket activation for graceful reloads, ensuring zero‑downtime.
To host the API reliably, the team provisioned a Cloud VPS with 1 vCPU, 1 GB RAM, and 25 GB SSD. This configuration offers enough headroom for Nginx, the Node.js process, and the occasional burst of traffic without incurring unnecessary cost.
Implementation Steps
1. Install and Verify Nginx
sudo apt update
sudo apt install -y nginx
nginx -v # should report 1.18.0+ (or newer) with TLS support
2. Obtain a Let’s Encrypt Certificate (if not already present)
sudo apt install -y certbot python3-certbot-nginx
sudo certbot --nginx -d api.example.com
Certbot automatically configures a basic HTTPS server block. We will replace it with a hardened version.
3. Harden the Nginx Server Block
Create a dedicated file /etc/nginx/sites-available/api.conf and link it:
sudo nano /etc/nginx/sites-available/api.conf
sudo ln -s /etc/nginx/sites-available/api.conf /etc/nginx/sites-enabled/
Insert the following configuration (comments explain each directive):
server {
listen 80;
server_name api.example.com;
return 301 https://$host$request_uri; # HTTP → HTTPS redirect
}
server {
listen 443 ssl http2;
server_name api.example.com;
# TLS protocol selection
ssl_protocols TLSv1.2 TLSv1.3;
ssl_prefer_server_ciphers on;
# Mozilla Intermediate cipher suite
ssl_ciphers 'ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:
ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:
ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305';
# Forward secrecy & perfect forward secrecy
ssl_ecdh_curve X25519:secp384r1;
# OCSP stapling
ssl_stapling on;
ssl_stapling_verify on;
resolver 8.8.8.8 8.8.4.4 valid=300s;
resolver_timeout 5s;
# HSTS (6 months, include subdomains)
add_header Strict-Transport-Security "max-age=15768000; includeSubDomains" always;
# Additional security headers
add_header X-Content-Type-Options nosniff;
add_header X-Frame-Options SAMEORIGIN;
add_header Referrer-Policy no-referrer-when-downgrade;
# TLS session settings
ssl_session_timeout 1d;
ssl_session_cache shared:SSL:10m;
ssl_session_tickets off;
# Root and proxy settings
location / {
proxy_pass http://127.0.0.1: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;
}
}
4. Test Configuration and Reload Gracefully
sudo nginx -t # syntax check
sudo systemctl reload nginx # reload without dropping connections
5. Verify TLS Settings
Use openssl and sslscan to confirm the server only offers TLS 1.2/1.3 and the expected cipher list.
openssl s_client -connect api.example.com:443 -tls1_2 -servername api.example.com
sslscan --no-fallback --no-renegotiation api.example.com
6. Automate Certificate Renewal
Certbot installs a systemd timer by default, but we add a sanity check to reload Nginx after renewal.
sudo systemctl edit certbot-renew.service
# Add the following override:
[Service]
ExecPost=/usr/sbin/nginx -s reload
Risks & Mitigation Strategies
- Client Compatibility: Some legacy clients may only support TLS 1.0/1.1. Mitigation – keep a separate sub‑domain for internal tools, or log failed handshakes and inform users.
- OCSP Stapling Failures: If the resolver cannot reach the OCSP responder, connections may stall. Mitigation – configure multiple public DNS resolvers and monitor
nginxerror logs forssl_staplingerrors. - Configuration Drift: Manual edits could revert hardening. Mitigation – store
/etc/nginxin Git, enforce pull‑request reviews, and useansibleorcheffor idempotent deployment. - Resource Exhaustion: TLS handshakes are CPU‑intensive. Mitigation – enable
ssl_session_cacheand monitor CPU usage; if needed, upgrade to a higher‑tier VPS.
Operational Checks
After deployment, the team added the following recurring checks to their monitoring suite:
- Certificate Expiry Alert: Use
certbot certificatesoutput parsed bycronto trigger an email 30 days before expiry. - Handshake Success Rate: Netdata’s
nginx.ssl_handshakemetric should stay above 99.9 %. - OCSP Stapling Health: Log pattern
"ssl_stapling error"and create a Grafana alert if occurrences exceed a threshold. - HSTS Header Verification: Periodically curl the endpoint and grep for
Strict-Transport-Security.
Outcome
Within a single maintenance window, the API achieved:
- Full TLS 1.2/1.3 compliance, passing the SSL Labs “A” rating.
- Zero‑downtime reloads thanks to
systemd‑driven Nginx reloads. - Reduced latency for TLS handshakes (average 45 ms) after enabling OCSP stapling.
- No further security‑scan warnings in the next quarterly audit.
The lightweight Cloud VPS proved sufficient for the hardened configuration, and the team now uses the same hardening template for all new micro‑services.
Conclusion
Hardening Nginx TLS on a modest Cloud VPS can be accomplished with a handful of well‑documented directives, careful testing, and automated renewal hooks. By treating the TLS configuration as code, the team minimized drift, ensured compliance, and kept the public API secure without sacrificing performance or availability.