DevOps teams have never been more central to protecting customer trust. In 2025, expectations for secure-by-default web experiences include enforced HTTPS, modern TLS, and automated certificate lifecycle management. This step-by-step tutorial shows you exactly how to implement SSL/TLS from planning through production hardening, with practical examples across NGINX/Apache, Kubernetes, and cloud load balancers. You’ll leave with a blueprint you can use today—plus the operational practices to keep your certificates from expiring and your site from going down.
What SSL/TLS Provides (and Why DevOps Should Care)
SSL/TLS creates an encrypted tunnel between clients and your application, delivering:
- Confidentiality: Prevents eavesdropping on requests and responses.
- Integrity: Detects tampering in transit.
- Authentication: Confirms the server (and optionally the client) is who it claims to be.
Modern deployments should:
- Prefer TLS 1.3, and support TLS 1.2 for compatibility.
- Use strong, forward-secret cipher suites.
- Implement HSTS and OCSP stapling.
- Automate certificate issuance and renewal (ACME).
- Monitor and alert on certificate health and expiry.
Industry trends continue toward shorter certificate lifetimes and automated management. Plan for renewal cycles of 90 days or less even if your CA supports longer validity.
Key Concepts in 60 Seconds
- Public vs. private certificates:
- Public: Issued by trusted CAs for Internet-facing domains.
- Private: Your own internal CA for service-to-service/mTLS or non-public domains.
- RSA vs ECDSA:
- ECDSA (P-256/P-384) offers better performance and smaller certs.
- Many teams deploy both ECDSA and RSA to maximize client compatibility.
- Wildcard vs SAN:
- Wildcard (*.example.com) simplifies subdomain coverage (DNS-01 challenge required).
- SAN certs list explicit hostnames. Good for multi-domain apps.
- OCSP Stapling:
- Server provides revocation status to clients, reducing latency and privacy leaks.
- HSTS:
- Instructs browsers to only use HTTPS. Consider preload for maximal protection.
- mTLS:
- Verifies clients with certificates. Great for internal services and zero-trust networks.
Pre-Implementation Checklist
Before you start:
- Inventory domains and environments (dev/stage/prod).
- Confirm you control DNS and can add records (for ACME DNS-01).
- Ensure HTTP (80) and HTTPS (443) are accessible as needed for validation.
- Decide issuance method:
- Let’s Encrypt (or another ACME CA) for automation.
- Commercial CA for EV/OV needs or enterprise policies.
- Choose key type and size:
- ECDSA P-256 or P-384; RSA 2048 or 3072 when needed.
- Threat model and compliance:
- PCI DSS mandates TLS 1.2+.
- Review internal policies on cryptography and key custody.
- Map out automation:
- Where does ACME run? Web server, ingress, or CI agent?
- How do you store and rotate keys? Consider HSM/KMS for high assurance.
Step 1: Obtain Certificates via ACME (Let’s Encrypt)
For public websites, using ACME automates issuance and renewal.
Option A: HTTP-01 Validation with Certbot (NGINX/Apache)
- Ensure port 80 routes to the host.
- Install certbot and the relevant plugin.
Ubuntu/Debian:
sudo apt update
sudo apt install -y certbot python3-certbot-nginx
# For Apache: python3-certbot-apache
Issue a certificate:
sudo certbot --nginx -d example.com -d www.example.com \
--redirect --hsts --staple-ocsp --email [email protected] --agree-tos
- The plugin edits server configs, adds HTTPS, redirects HTTP->HTTPS, and enables OCSP stapling/HSTS where supported.
- Renewal is automatic via systemd timer (check with
systemctl list-timers
).
Option B: DNS-01 Validation (Wildcards and Complex Topologies)
For wildcard certs or when HTTP-01 isn’t feasible:
# Using acme.sh with Route53 as an example
curl https://get.acme.sh | sh -s [email protected]
export AWS_ACCESS_KEY_ID=...
export AWS_SECRET_ACCESS_KEY=...
~/.acme.sh/acme.sh --issue -d example.com -d '*.example.com' --dns dns_aws
~/.acme.sh/acme.sh --install-cert -d example.com \
--key-file /etc/ssl/private/example.com.key \
--fullchain-file /etc/ssl/certs/example.com.fullchain.pem \
--reloadcmd "systemctl reload nginx"
- Prefer scoped credentials (IAM/policy-limited) for DNS APIs.
- Use the CA’s staging environment for testing to avoid rate limits.
Step 2: Server-Side TLS Configuration
NGINX: Modern, Fast, and Flexible
Minimal secure config (ECDSA+RSA, TLS 1.2/1.3, OCSP stapling, HSTS, HTTP/3):
# /etc/nginx/conf.d/example.conf
server {
listen 80;
server_name example.com www.example.com;
return 301 https://$host$request_uri;
}
server {
# HTTP/2
listen 443 ssl http2;
# HTTP/3 (QUIC) if NGINX built with quic support
# listen 443 quic reuseport;
server_name example.com www.example.com;
# Dual certs: ECDSA first, then RSA
ssl_certificate /etc/ssl/certs/example-ecdsa.fullchain.pem;
ssl_certificate_key /etc/ssl/private/example-ecdsa.key;
ssl_certificate /etc/ssl/certs/example-rsa.fullchain.pem;
ssl_certificate_key /etc/ssl/private/example-rsa.key;
ssl_protocols TLSv1.2 TLSv1.3;
ssl_prefer_server_ciphers off;
# TLS 1.2 cipher suites; TLS 1.3 suites are fixed by spec
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';
# OCSP stapling
ssl_stapling on;
ssl_stapling_verify on;
ssl_trusted_certificate /etc/ssl/certs/chain.pem;
resolver 1.1.1.1 8.8.8.8 valid=300s;
resolver_timeout 5s;
# Session settings
ssl_session_cache shared:SSL:50m;
ssl_session_timeout 10m;
ssl_session_tickets off; # manage keys carefully if enabling
# Security headers
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains; preload" always;
add_header X-Content-Type-Options "nosniff" always;
add_header X-Frame-Options "DENY" always;
add_header Referrer-Policy "no-referrer-when-downgrade" always;
# CSP requires tailoring to your app; example only:
add_header Content-Security-Policy "default-src 'self'; frame-ancestors 'none'; upgrade-insecure-requests" always;
# Root/location as usual
root /var/www/html;
index index.html;
}
Tips:
- Place private keys in
/etc/ssl/private
with permissions 600 root:root. - For HTTP/3, add
alt-svc
header and ensure QUIC build; test client support. - Monitor for mixed content if you force HTTPS.
Apache HTTPD (mod_ssl and HTTP/2)
<VirtualHost *:80>
ServerName example.com
ServerAlias www.example.com
Redirect permanent / https://example.com/
</VirtualHost>
<VirtualHost *:443>
ServerName example.com
ServerAlias www.example.com
SSLEngine on
SSLProtocol all -SSLv3 -TLSv1 -TLSv1.1
SSLCipherSuite ECDHE+AESGCM:ECDHE+CHACHA20
SSLHonorCipherOrder off
SSLCertificateFile /etc/ssl/certs/example-ecdsa.crt
SSLCertificateKeyFile /etc/ssl/private/example-ecdsa.key
SSLCertificateChainFile /etc/ssl/certs/chain.pem
# Apache doesn't natively serve dual certs in one vhost; consider RSA-only or use multiple vhosts/SNI as needed.
SSLUseStapling On
SSLStaplingResponderTimeout 5
SSLStaplingReturnResponderErrors Off
SSLCACertificateFile /etc/ssl/certs/chain.pem
Header always set Strict-Transport-Security "max-age=31536000; includeSubDomains; preload"
Header always set X-Content-Type-Options "nosniff"
Header always set X-Frame-Options "DENY"
Header always set Referrer-Policy "no-referrer-when-downgrade"
Protocols h2 http/1.1
DocumentRoot "/var/www/html"
</VirtualHost>
HAProxy (Edge Proxy TLS Termination)
global
tune.ssl.default-dh-param 2048
defaults
mode http
timeout connect 5s
timeout client 30s
timeout server 30s
frontend https-in
bind :443 ssl crt /etc/haproxy/certs/example.pem alpn h2,http/1.1
http-response set-header Strict-Transport-Security "max-age=31536000; includeSubDomains; preload"
default_backend app
backend app
server s1 127.0.0.1:8080 check
Note: Concatenate fullchain and key into a PEM for HAProxy, or use crt-list for dual certs (RSA/ECDSA).
Caddy (Automatic HTTPS with Minimal Config)
example.com, www.example.com {
root * /var/www/html
file_server
encode gzip
header {
Strict-Transport-Security "max-age=31536000; includeSubDomains; preload"
}
}
Caddy handles ACME, OCSP stapling, HTTP->HTTPS, and renewals automatically.
Step 3: Kubernetes with cert-manager
For cluster-native automation:
- Install cert-manager via Helm:
helm repo add jetstack https://charts.jetstack.io
helm repo update
helm install cert-manager jetstack/cert-manager --namespace cert-manager --create-namespace --set installCRDs=true
- Create a ClusterIssuer (Let’s Encrypt HTTP-01 with NGINX Ingress):
apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
name: letsencrypt-prod
spec:
acme:
email: [email protected]
server: https://acme-v02.api.letsencrypt.org/directory
privateKeySecretRef:
name: letsencrypt-prod
solvers:
- http01:
ingress:
class: nginx
- Ingress with TLS:
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: web
annotations:
cert-manager.io/cluster-issuer: letsencrypt-prod
nginx.ingress.kubernetes.io/force-ssl-redirect: "true"
spec:
ingressClassName: nginx
tls:
- hosts:
- example.com
- www.example.com
secretName: web-tls
rules:
- host: example.com
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: web-svc
port:
number: 80
For wildcards, switch to DNS-01 with a DNS provider (e.g., Route53) and a DNS solver instead of HTTP-01.
Step 4: Cloud Load Balancers and CDNs
AWS: ACM + ALB (Terraform example)
ACM certificate (DNS-validated):
resource "aws_acm_certificate" "site" {
domain_name = "example.com"
validation_method = "DNS"
subject_alternative_names = ["www.example.com"]
}
resource "aws_route53_record" "site_validation" {
for_each = {
for dvo in aws_acm_certificate.site.domain_validation_options : dvo.domain_name => {
name = dvo.resource_record_name
type = dvo.resource_record_type
record = dvo.resource_record_value
}
}
zone_id = data.aws_route53_zone.primary.zone_id
name = each.value.name
type = each.value.type
ttl = 60
records = [each.value.record]
}
resource "aws_acm_certificate_validation" "site" {
certificate_arn = aws_acm_certificate.site.arn
validation_record_fqdns = [for r in aws_route53_record.site_validation : r.fqdn]
}
Attach to ALB listener:
resource "aws_lb_listener" "https" {
load_balancer_arn = aws_lb.app.arn
port = "443"
protocol = "HTTPS"
ssl_policy = "ELBSecurityPolicy-TLS13-1-2-2021-06"
certificate_arn = aws_acm_certificate_validation.site.certificate_arn
default_action {
type = "forward"
target_group_arn = aws_lb_target_group.app.arn
}
}
CDN: For CloudFront, issue the ACM cert in us-east-1 and attach it to your distribution.
Azure: Front Door or Application Gateway
- Azure Front Door Standard/Premium can issue and auto-renew managed certs for your custom domains.
- App Gateway supports TLS termination with certificates from Key Vault; use managed identity for rotation.
GCP: HTTPS Load Balancer (Managed Cert)
resource "google_compute_managed_ssl_certificate" "site" {
name = "site-cert"
managed {
domains = ["example.com", "www.example.com"]
}
}
Attach to target HTTPS proxy; Google manages issuance/renewal.
Step 5: Enforce Security Headers and HSTS
- HSTS: Start with a shorter max-age (e.g., 1 day) to validate, then move to 1 year with includeSubDomains. Preloading requires:
- HTTPS on root domain and all subdomains.
- max-age >= 31536000 and includeSubDomains and preload directives.
- Cookies: Set Secure and SameSite attributes for session cookies.
- Avoid HPKP (deprecated). Use Certificate Transparency (CT) which is handled by public CAs.
Step 6: Implement mTLS for Internal Services
For zero-trust or sensitive paths, require clients to present certificates.
NGINX mTLS example
server {
listen 443 ssl http2;
server_name internal.example.com;
ssl_certificate /etc/ssl/certs/svc.fullchain.pem;
ssl_certificate_key /etc/ssl/private/svc.key;
ssl_client_certificate /etc/ssl/certs/ca-internal.pem; # trusted client CA
ssl_verify_client on; # or optional
location / {
proxy_pass http://upstream;
}
}
Issue internal certs using an internal CA (e.g., HashiCorp Vault PKI, Smallstep, or cloud CA). Prefer short-lived certificates and automated issuance with workload identity.
Vault quickstart (simplified):
vault secrets enable pki
vault secrets tune -max-lease-ttl=87600h pki
vault write pki/root/generate/internal common_name="Example Internal CA" ttl=87600h
vault write pki/roles/service allow_any_name=true max_ttl=720h
vault write pki/issue/service common_name="api.internal" ttl=24h > cert.json
Distribute certs securely with an agent/sidecar and rotate frequently.
Step 7: CI/CD Integration and Automation
Build TLS checks into your pipelines to catch misconfigurations before production.
- Lint server configs:
- NGINX:
nginx -t
- Apache:
apachectl configtest
- NGINX:
- Automated tests:
testssl.sh
orsslyze
against preview environments.- Validate no mixed content and correct redirects.
- Example GitHub Action: alert on soon-to-expire certs
name: TLS Health
on:
schedule:
- cron: "0 3 * * *"
jobs:
check-cert:
runs-on: ubuntu-latest
steps:
- name: Check expiry
run: |
END_DATE=$(echo | openssl s_client -servername example.com -connect example.com:443 2>/dev/null \
| openssl x509 -noout -enddate | cut -d= -f2)
END_TS=$(date -d "$END_DATE" +%s)
NOW_TS=$(date +%s)
DAYS=$(( (END_TS - NOW_TS) / 86400 ))
echo "Days until expiry: $DAYS"
if [ $DAYS -lt 21 ]; then
echo "Cert expires soon!"
exit 1
fi
- Gate deployments on SSL Labs / Mozilla Observatory scores in non-prod and enforce policy checks in prod.
Step 8: Monitoring and Alerting
Make certificate health observable:
- Blackbox Exporter (Prometheus) for TLS expiry and validity.
- Grafana dashboards for cert expiry, OCSP status, error rates.
- Synthetic checks from multiple regions to capture CDN/LB edges.
- Track cert issuance/audit logs (e.g., Vault audit, CA logs).
- Watch Certificate Transparency for unexpected certs for your domains (e.g., crt.sh, automated CT monitors).
- Log and alert on TLS handshake failures; they often signal client incompatibility or misconfigurations.
Step 9: Hardening Playbook (2025-Ready)
- Protocols:
- Enable TLS 1.3.
- Keep TLS 1.2 for compatibility; disable 1.0 and 1.1.
- Cipher suites:
- Prefer ECDHE with AES-GCM or ChaCha20-Poly1305.
- Avoid CBC and static RSA key exchange suites.
- Keys and algorithms:
- Prefer ECDSA P-256/P-384; offer RSA for legacy.
- Use strong randomness and lock down private key permissions.
- OCSP Stapling:
- Enable and verify; consider Must-Staple on internal policies after testing.
- HSTS:
- Enforce site-wide with appropriate staging before preload submission.
- Session tickets:
- Disable or rotate keys frequently; be mindful of 0-RTT implications in TLS 1.3 (replay risks).
- HTTP/2 and HTTP/3:
- Enable ALPN and prioritize HPACK/QPACK-tested stacks.
- Mixed content:
- Block and fix at source; use CSP’s upgrade-insecure-requests as a bridge.
- Don’t use deprecated features:
- HPKP is deprecated; avoid.
- Secrets management:
- Store keys in secure vaults; consider HSM/KMS-backed keys for critical endpoints.
- Rate limits:
- Avoid ACME rate limit issues by testing with staging and batching SANs appropriately.
Step 10: Troubleshooting Common Pitfalls
- Incomplete certificate chain:
- Ensure you serve the full chain (server + intermediates). Many clients break without it.
- ACME validation fails:
- HTTP-01: Port 80 blocked, wrong vhost, CDN caching validation path, or IPv6 AAAA record points to a host not serving the challenge.
- DNS-01: Propagation delays, wrong TXT record, or insufficient API permissions.
- Mixed content warnings:
- Update asset URLs to HTTPS; set CSP upgrade-insecure-requests temporarily.
- SNI issues:
- Multiple sites on one IP require proper server_name/vhost config and SNI-enabled clients.
- Legacy clients:
- Some old systems don’t support ECDSA or TLS 1.2. Decide to support via RSA fallback or explicitly drop support.
- Time sync:
- TLS depends on accurate system time. Run NTP/chrony.
- SELinux/AppArmor:
- Ensure daemons can read key files; adjust contexts or confinement as needed.
- Too many redirects:
- Ensure single redirect chain HTTP->HTTPS->canonical host; avoid loops with proxies/CDNs.
Operational Lifecycle: Rotation, Revocation, and Incidents
- Renewal cadence:
- Automate renewals and reloads. Dry-run weekly and alert if renewals fail.
- Blue/green for cert updates:
- Stash the new cert and reload gracefully or rotate behind a load balancer.
- Revocation:
- If a key is compromised, revoke immediately and deploy a replacement. Validate OCSP stapling updates.
- Backups:
- Carefully consider whether to back up private keys. If you do, encrypt and store securely with strict access controls.
- Runbooks:
- Document steps for issuance, rollback, revocation, and hotfixes. Practice with game days.
- Change management:
- Treat TLS changes as code: Git, reviews, and CI checks.
Practical Decision Matrix
- Public site with many subdomains:
- Use DNS-01 + wildcard certs via ACME; automate with DNS API.
- App behind AWS ALB/CloudFront:
- Use ACM; attach to ALB and distribution; let AWS handle renewals.
- Kubernetes microservices:
- Use cert-manager for ingress; consider mTLS internally with SPIFFE/SPIRE or Vault PKI.
- High-performance API:
- Prefer ECDSA certs; keep RSA available if client base requires.
- Strict enterprise controls:
- Consider HSM-backed keys and commercial CAs, but keep ACME automation patterns internally.
Verification: Test Like an Attacker and a Customer
- SSL Labs server test: Aim for A or A+ (watch for HSTS requirements).
- Mozilla Observatory: Improve headers incrementally.
- testssl.sh/sslyze: Run in CI and on every environment.
- Browser/devtools: Check for mixed content and protocol negotiation (ALPN).
- Mobile devices: Test with a mix of Android/iOS versions and captive portals.
Example: End-to-End with NGINX and Let’s Encrypt
- Install NGINX and Certbot.
- Open ports 80/443; ensure DNS resolves to your server.
- Run:
sudo certbot --nginx -d example.com -d www.example.com --redirect --hsts --staple-ocsp
- Verify:
sudo nginx -t && sudo systemctl reload nginx
curl -I https://example.com
# Check for 200, HSTS header, and valid certificate
- Monitor:
- Enable alerts when
certbot renew --dry-run
fails. - Add Prometheus blackbox checks for TLS expiry.
Example: cert-manager with DNS-01 (Wildcard)
- Install cert-manager (as above).
- Configure a ClusterIssuer using your DNS provider (e.g., Route53):
apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
name: letsencrypt-dns
spec:
acme:
email: [email protected]
server: https://acme-v02.api.letsencrypt.org/directory
privateKeySecretRef:
name: letsencrypt-dns
solvers:
- dns01:
route53:
region: us-east-1
hostedZoneID: Z1234567890
accessKeyID: YOUR_KEY
secretAccessKeySecretRef:
name: route53-credentials-secret
key: secret-access-key
- Request a wildcard cert:
apiVersion: cert-manager.io/v1
kind: Certificate
metadata:
name: wildcard-example
namespace: default
spec:
secretName: wildcard-example-tls
dnsNames:
- example.com
- '*.example.com'
issuerRef:
name: letsencrypt-dns
kind: ClusterIssuer
- Reference
wildcard-example-tls
in your Ingress. cert-manager will handle issuance and renewal.
Security-by-Default Checklist
- TLS 1.3 enabled; TLS 1.2 retained; older disabled.
- Strong cipher suites; PFS ensured.
- ECDSA certs preferred; RSA fallback if needed.
- HSTS with includeSubDomains; plan for preload.
- OCSP stapling on and verified.
- HTTP->HTTPS redirect without loops.
- Automated issuance and renewal with alerts.
- Certificate monitoring (expiry, CT logs).
- Secrets managed securely; keys locked down.
- mTLS where appropriate for internal traffic.
- CI/CD tests for TLS and headers.
- Clear runbooks and incident playbooks.
Final Thoughts and Next Steps
In 2025, robust TLS isn’t just about turning on HTTPS—it’s about treating certificates as living assets, codifying your configuration, and observing the system end-to-end. Start with ACME automation, enable modern protocols and headers, then evolve into internal mTLS and HSM-backed keys as your maturity grows.
Action plan for this week:
- Stand up ACME automation on a non-critical domain.
- Harden your edge proxy with TLS 1.3, OCSP stapling, and HSTS.
- Add certificate expiry checks to CI and Ops alerting.
- Run SSL Labs and Observatory; create tickets for the gaps.
- Draft an incident runbook for cert expiry and revocation.
With these steps, your DevOps team will deliver a fast, modern, and resilient TLS posture that protects users and keeps outages at bay.