Securing web traffic is a foundational responsibility for web developers and DevOps engineers. HTTPS (TLS) protects data in transit while security headers such as Content Security Policy (CSP) and HTTP Strict Transport Security (HSTS) harden browser behavior and reduce attack surface. This guide explains the protocols, shows practical header configurations, and gives deployment advice you can apply today.
Overview
- SSL vs TLS: TLS is the modern protocol; “SSL” is legacy naming
- TLS handshake basics: key exchange, authentication, and symmetric encryption
- CSP: reduce XSS risk by restricting sources for scripts, styles, frames
- HSTS: force browsers to use HTTPS only
- Practical deployment: certificate issuance/renewal, secure server configs, testing
SSL/TLS Essentials
What TLS Does
Transport Layer Security (TLS) encrypts HTTP traffic, preventing eavesdropping and tampering. A TLS connection provides:
- Confidentiality (encryption)
- Integrity (detect tampering)
- Authentication (server identity via certificate)
Note: People still say “SSL/TLS” but TLS 1.2 and TLS 1.3 are the recommended protocols—disable SSLv3/TLS < 1.2.
How the TLS Handshake Works (high level)
- Client Hello: the client sends supported protocol versions, cipher suites, and a random value.
- Server Hello: server picks protocol version and cipher suite and sends its certificate chain.
- Key Exchange: using ECDHE or similar, both parties derive a shared secret.
- Verification and Finished messages: both sides verify the handshake and begin encrypted communication.
TLS 1.3 simplifies and speeds up this process (fewer roundtrips, mandatory forward secrecy).
Certificates and Trust
A TLS certificate includes a public key and identity info (subject). Browsers trust certificates issued by recognized Certificate Authorities (CAs). Common management steps:
- Generate a private key and CSR (certificate signing request)
- Have a CA issue the certificate (Let’s Encrypt, commercial CAs)
- Install cert and key on your server
- Renew before expiry (Let’s Encrypt: 90 days)
Inspect a certificate with openssl:
openssl s_client -connect example.com:443 -servername example.com \
| openssl x509 -noout -text
Key Concepts and Best Practices
- Use TLS 1.3 where possible; fall back to TLS 1.2 only if necessary
- Prefer ECDHE key exchange for Perfect Forward Secrecy (PFS)
- Use 2048+ RSA keys or ECDSA P-256 keys
- Enable OCSP stapling to speed up revocation checks
- Disable old ciphers (RC4, DES, 3DES, export ciphers)
- Regularly test configs with SSL Labs: https://www.ssllabs.com/ssltest/
Example nginx TLS snippet (minimal, secure):
ssl_protocols TLSv1.2 TLSv1.3;
ssl_prefer_server_ciphers off; # TLS1.3 chooses secure ciphers
ssl_ciphers 'ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384';
ssl_session_cache shared:SSL:10m;
ssl_session_timeout 1d;
ssl_stapling on;
ssl_stapling_verify on;
resolver 8.8.8.8 8.8.4.4 valid=300s;
resolver_timeout 5s;
Content Security Policy (CSP)
CSP is a browser mechanism that restricts what resources a page may load and execute, greatly reducing the impact of cross-site scripting (XSS) vulnerabilities.
CSP Basics
Set CSP with the Content-Security-Policy header. The policy lists directives that control resource types, e.g. default-src, script-src, style-src, img-src, frame-ancestors.
A conservative example:
Content-Security-Policy: default-src 'self'; script-src 'self' https://cdn.example.com; object-src 'none'; base-uri 'self'; frame-ancestors 'none';
default-src 'self'allows resources only from the same originscript-srcallows scripts from the site’s origin and a trusted CDNobject-src 'none'disables pluginsframe-ancestors 'none'prevents framing (clickjacking)
Nonces and Hashes (recommended over ‘unsafe-inline’)
Inline scripts/styles are frequent causes of weakness. Instead of unsafe-inline, use nonces or hashes.
Server-side: generate a random nonce per response and include it in script tags and the CSP header.
Content-Security-Policy: script-src 'self' 'nonce-<RANDOM_NONCE>' https://cdn.example.com;
HTML:
<script nonce="<RANDOM_NONCE>">console.log('allowed inline script');</script>
Hashes are useful for static inline scripts (sha256-abc…). Nonces are better for dynamic pages.
Reporting and Iteration
Use Content-Security-Policy-Report-Only during rollout to collect violations without blocking. Provide a report-uri or report-to endpoint to gather reports.
Content-Security-Policy-Report-Only: default-src 'self'; report-uri /csp-report-endpoint
Common Pitfalls
unsafe-inlineandunsafe-evalnegate much of CSP’s protection—avoid them- Overly broad host wildcards (e.g.,
*.example.com) may include attacker-controlled subdomains in misconfigured DNS - Forgetting to include
frame-ancestorsallows clickjacking
HTTP Strict Transport Security (HSTS)
HSTS tells browsers to only use HTTPS when contacting your site for a specified period.
Header and Options
Strict-Transport-Security: max-age=31536000; includeSubDomains; preload
max-ageis seconds—31536000equals one yearincludeSubDomainsapplies HSTS to all subdomainspreloadindicates intent to submit to the browser preload list (see https://hstspreload.org)
Deployment Notes
- Start with
max-age=3600(1 hour) and noincludeSubDomainsduring testing - Once stable, increase
max-ageto one year and addincludeSubDomainsif you control all subdomains - Only add
preloadafter careful testing—being preloaded means removal requires coordination with browsers
Nginx example
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains; preload" always;
Secure Deployment Practices
Certificate Management
- Use Let’s Encrypt for automation or a commercial CA for extended validation
- Automate renewals (certbot, acme.sh, or platform-managed certs)
- Monitor expiry and set alerts
- Use separate keys for dev/staging and production
- For high-value keys, consider storing private keys in an HSM or cloud KMS
Example: issue and auto-renew with Certbot (nginx):
sudo apt install certbot python3-certbot-nginx
sudo certbot --nginx -d example.com -d www.example.com
# Certbot installs renewal cron; test with:
sudo certbot renew --dry-run
Server Configuration Checklist
- Redirect all HTTP to HTTPS (301)
- Add HSTS after testing
- Enable strong TLS protocols and ciphers
- Enable OCSP stapling
- Serve cookies with
Secure; HttpOnly; SameSite=Strictwhere appropriate - Disable weak TLS versions and ciphers
- Keep server software and OpenSSL updated
HTTP redirect example (nginx):
server {
listen 80;
server_name example.com www.example.com;
return 301 https://$host$request_uri;
}
Extra Security Headers
Add these headers in addition to CSP and HSTS:
add_header X-Content-Type-Options "nosniff" always;
add_header X-Frame-Options "DENY" always;
add_header Referrer-Policy "no-referrer-when-downgrade" always;
add_header Permissions-Policy "geolocation=(), microphone=()" always; # formerly Feature-Policy
add_header X-XSS-Protection "0" always; # deprecated but some still set it
Testing & Validation
- SSL Labs test: https://www.ssllabs.com/ssltest/
- Mozilla Observatory: https://observatory.mozilla.org/
- CSP reporting to see blocked resources
- Automated scans in CI to check for weak ciphers and expiring certs
Common Pitfalls
- Applying HSTS before HTTPS redirects are stable (locks users out if misconfigured)
- Using wide CSPs (
default-src *) that defeat the purpose - Forgetting to renew certificates or relying on manual renewals
- Exposing private keys in version control—never commit private keys
Quick Reference: Example Express.js hardening
const express = require('express');
const helmet = require('helmet');
const app = express();
// Helmet sets many headers (CSP must be configured carefully)
app.use(helmet());
// HSTS (enable after verifying HTTPS)
app.use(helmet.hsts({
maxAge: 31536000,
includeSubDomains: true,
preload: true
}));
// CSP with nonce (example middleware sets res.locals.cspNonce)
app.use((req, res, next) => {
const nonce = crypto.randomBytes(16).toString('base64');
res.locals.cspNonce = nonce;
res.setHeader("Content-Security-Policy",
`default-src 'self'; script-src 'self' 'nonce-${nonce}' https://cdn.example.com;`);
next();
});
app.get('/', (req, res) => {
res.send(`<script nonce="${res.locals.cspNonce}">console.log('hello')</script>`);
});
TLS 1.3 Handshake in Detail
TLS 1.3, standardized in RFC 8446, redesigned the handshake to complete in one round trip (1-RTT) for new connections and zero round trips (0-RTT) for resumed connections.
Full TLS 1.3 Handshake (1-RTT)
Client Server
| |
|--- ClientHello ---------------------------------------->|
| Protocol: TLS 1.3 |
| Key share: X25519 public key |
| Ciphers: TLS_AES_256_GCM_SHA384, ... |
| Extensions: SNI, ALPN, supported_versions |
| |
| ServerHello --|
| Cipher suite -|
| Key share ----|
| Certificate --|
| CertVerify ---|
| Finished -----|
|<---------------------------------------------------------|
| |
|--- Finished ------------------------------------------->|
|--- Application Data ----------------------------------->|
Step-by-step breakdown:
-
ClientHello: Client sends supported protocol versions, a Diffie-Hellman key share (typically X25519 or P-256), cipher suites, and extensions (SNI for hostname, ALPN for protocol negotiation). Unlike TLS 1.2, the client predicts the key agreement algorithm and sends its share immediately.
-
ServerHello: Server selects the protocol version (TLS 1.3), cipher suite, and sends its own key share. Both parties now compute the shared secret using ECDHE.
-
Encrypted Extensions: Server sends extensions (ALPN negotiation, server name) encrypted with the handshake traffic key.
-
Certificate: Server sends its certificate chain, encrypted. TLS 1.3 encrypts the certificate, hiding the server identity from passive observers.
-
CertificateVerify: Server signs the handshake transcript to prove possession of the private key.
-
Finished: Server sends an HMAC of the handshake, authenticated.
-
Client Finished: Client authenticates the handshake similarly.
-
Application Data: Encrypted communication begins.
TLS 1.2 vs TLS 1.3 Comparison
| Feature | TLS 1.2 | TLS 1.3 |
|---|---|---|
| Round trips | 2 (full handshake) | 1 (full handshake) |
| Handshake latency | 2 RTT | 1 RTT (0 with 0-RTT) |
| Cipher suites | 40+ combinations | 5 AEAD ciphers |
| Key exchange | Separate negotiation | Embedded in ClientHello |
| Forward secrecy | Optional | Mandatory (ECDHE) |
| Certificate encryption | No | Yes |
| 0-RTT resumption | No | Yes (with anti-replay) |
| Supported signature algs | RSA, ECDSA, DSA | ECDSA, EdDSA, RSA-PSS |
| Compression | Supported (disabled) | Removed |
| Renegotiation | Supported | Removed |
| Static RSA key exchange | Supported (insecure) | Removed |
| Insecure feature removal | Manual | Built-in |
0-RTT (Zero Round Trip Resumption)
TLS 1.3 supports sending application data with the ClientHello for resumed connections:
Client (with cached PSK) Server
| |
|--- ClientHello + 0-RTT Data -------------------------->|
| PSK key ID |
| Early application data |
| |
| ServerHello --|
| Finished -----|
|<---------------------------------------------------------|
|--- End of EarlyData ---------------------------------->|
Considerations for 0-RTT:
- Data is replayable — an attacker can forward the same 0-RTT data multiple times
- Applications must implement idempotency guarantees for 0-RTT requests
- Suitable for GET requests, unsafe for non-idempotent mutations
- Maximum early data size is configurable (typically 16KB)
- Not all clients support 0-RTT
HSTS Preload List Submission
The HSTS preload list is a hardcoded list of domains in browser source code that receive automatic HTTPS enforcement, even before the first HSTS header is received.
Submission Process
-
Prerequisites: Your site must:
- Have a valid TLS certificate on the root domain
- Redirect all HTTP traffic to HTTPS (301)
- Serve all subdomains over HTTPS
- Serve the HSTS header with
max-ageof at least 31536000 (1 year) - Include
includeSubDomainsdirective - Include
preloaddirective in the HSTS header
-
Test your configuration:
# Check HSTS header curl -sI https://example.com | grep -i strict-transport # Expected: strict-transport-security: max-age=31536000; includeSubDomains; preload # Test redirect curl -sI http://example.com | grep -i location # Expected: location: https://example.com/ -
Submit to hstspreload.org: Visit https://hstspreload.org and enter your domain. The site validates your configuration against preload requirements.
-
Wait for inclusion: After successful submission, your domain appears in the preload list within weeks to months. Browser vendors (Chrome, Firefox, Safari, Edge) update their preload lists independently.
-
Removal difficulty: Once preloaded, removing a domain requires submitting a removal request to the Chromium HSTS preload list repository and waiting for a browser release cycle (3-6 months for Chromium). Preload removal is not guaranteed — some browsers may never remove your domain.
Content Security Policy Directive Reference
Core Directives
| Directive | Controls | Example |
|---|---|---|
default-src |
Fallback for all resource types | default-src 'self' |
script-src |
JavaScript sources | script-src 'self' https://cdn.example.com |
style-src |
CSS sources | style-src 'self' 'unsafe-inline' |
img-src |
Image sources | img-src 'self' https://images.example.com data: |
connect-src |
XHR, fetch, WebSocket | connect-src 'self' https://api.example.com wss://ws.example.com |
font-src |
Web font sources | font-src 'self' https://fonts.gstatic.com |
frame-src |
Frame/iframe sources | frame-src 'self' https://player.vimeo.com |
frame-ancestors |
Parent frame embedding | frame-ancestors 'none' |
media-src |
Audio/video sources | media-src 'self' https://cdn.example.com |
object-src |
Plugin sources (Flash, Java) | object-src 'none' |
base-uri |
<base> tag URLs |
base-uri 'self' |
form-action |
Form submission targets | form-action 'self' https://api.example.com |
report-uri |
Violation reporting endpoint | report-uri /csp-violations |
report-to |
Reporting API group | report-to csp-endpoint |
manifest-src |
Web app manifest | manifest-src 'self' |
worker-src |
Web Worker, Service Worker | worker-src 'self' blob: |
Source Expressions
| Expression | Meaning |
|---|---|
'none' |
No sources allowed |
'self' |
Same origin |
'unsafe-inline' |
Allow inline code (weakens protection) |
'unsafe-eval' |
Allow eval() and similar (weakens protection) |
'strict-dynamic' |
Trust scripts loaded by trusted scripts |
'nonce-<value>' |
Allow script with matching nonce attribute |
'sha256-<hash>' |
Allow script with matching hash |
https: |
All HTTPS sources |
http: |
All HTTP sources (weakens protection) |
data: |
Data URIs |
blob: |
Blob URIs |
https://*.example.com |
Specific domain pattern |
CSP Bypass Techniques and Prevention
| Technique | How It Works | Prevention |
|---|---|---|
| JSONP endpoints | Old APIs with ?callback= parameter execute arbitrary JavaScript |
Use strict-dynamic instead of CDN whitelists |
| CDN file uploads | Upload malicious script to allowlisted CDN | Pin specific script hashes, avoid https://* |
| Base tag injection | Override relative URLs via injected <base> tag |
Set base-uri 'none' or base-uri 'self' |
| Form action override | Redirect form submissions to attacker endpoint | Set form-action 'self' |
| Dangling markup injection | Capture page content via injected <img src=attacker? |
Set img-src 'self' and strict markup encoding |
| Angular sandbox escape | Exploit AngularJS expression evaluation | Use 'strict-dynamic' and avoid Angular 1.x |
| Policy misconfiguration | default-src 'self' 'unsafe-inline' bypasses most protection |
Audit with automated CSP evaluator |
Example of strict-dynamic usage:
Content-Security-Policy: script-src 'self' 'strict-dynamic' 'nonce-RAND123'
This policy tells the browser to trust scripts loaded by scripts that already have the correct nonce. Third-party CDN whitelists are ignored when strict-dynamic is present in Chromium browsers.
Certificate Transparency (CT)
Certificate Transparency logs provide an audit mechanism for TLS certificate issuance. Every publicly trusted CA must submit certificates to CT logs before issuance.
How CT Works
- When a CA issues a certificate, it submits the certificate to multiple CT logs
- Each log returns a Signed Certificate Timestamp (SCT) proving submission
- The SCT is delivered to clients via:
- X.509 extension: Embedded in the certificate (most reliable)
- TLS extension: Sent during handshake as
signed_certificate_timestamp - OCSP stapling: Included in OCSP response
- Browsers check SCTs against known logs and reject certificates without valid SCTs (Chrome requires at least 2 SCTs)
Checking CT Status
# Check SCTs in certificate
openssl s_client -connect example.com:443 -servername example.com \
| openssl x509 -noout -text | grep -A 20 "CT Precertificate"
# Using ct-client tool
ct-client verify example.com
CT Monitoring
Operators should monitor CT logs for unauthorized certificate issuance:
# Monitor for new certificates issued for your domain
go run github.com/google/certificate-transparency-go/cmd/ct_monitor \
--domains example.com --log_list ct_log_list.json
Security Scoring and Automation
Automated Security Assessment Tools
| Tool | Purpose | URL | Usage |
|---|---|---|---|
| SSL Labs Test | TLS configuration grading | https://www.ssllabs.com/ssltest/ | Web-based, API available |
| Mozilla Observatory | Security headers + TLS | https://observatory.mozilla.org/ | Web-based, CLI available |
| Security Headers | Header configuration grading | https://securityheaders.com/ | Web-based, API available |
| CSP Evaluator | CSP policy analysis | https://csp-evaluator.withgoogle.com/ | Web-based |
| HSTS Preload Check | HSTS readiness check | https://hstspreload.org/ | Web-based |
| testssl.sh | Comprehensive TLS testing | https://testssl.sh/ | CLI tool |
CI/CD Header Validation
Integrate security header checks into your deployment pipeline:
# GitHub Actions: Validate security headers on deploy
security-headers-check:
runs-on: ubuntu-latest
steps:
- name: Check security headers
run: |
URL="https://${{ vars.SITE_DOMAIN }}"
echo "Testing: $URL"
# Check HSTS
HSTS=$(curl -sI "$URL" | grep -i strict-transport-security)
if [ -z "$HSTS" ]; then
echo "FAIL: Missing HSTS header"
exit 1
fi
echo "PASS: HSTS header found"
# Check CSP
CSP=$(curl -sI "$URL" | grep -i content-security-policy)
if [ -z "$CSP" ]; then
echo "FAIL: Missing CSP header"
exit 1
fi
echo "PASS: CSP header found"
# Check TLS version
TLS=$(echo | openssl s_client -connect "${{ vars.SITE_DOMAIN }}:443" 2>/dev/null \
| openssl x509 -noout -text | grep -i "tls")
echo "TLS version: $TLS"
Automated Certificate Monitoring
#!/bin/bash
# cert-monitor.sh — Check certificate expiry and send alerts
DOMAINS=("example.com" "api.example.com")
THRESHOLD_DAYS=30
for domain in "${DOMAINS[@]}"; do
expiry_date=$(echo | openssl s_client -connect "$domain":443 \
-servername "$domain" 2>/dev/null \
| openssl x509 -noout -enddate \
| cut -d= -f2)
expiry_epoch=$(date -d "$expiry_date" +%s)
now_epoch=$(date +%s)
days_left=$(( (expiry_epoch - now_epoch) / 86400 ))
if [ "$days_left" -lt "$THRESHOLD_DAYS" ]; then
echo "ALERT: $domain certificate expires in $days_left days"
# Send alert (email, Slack, PagerDuty)
else
echo "OK: $domain expires in $days_left days"
fi
done
Real-World Configuration Examples
Apache
<VirtualHost *:443>
ServerName example.com
SSLEngine on
SSLCertificateFile /etc/ssl/certs/example.com.pem
SSLCertificateKeyFile /etc/ssl/private/example.com.key
# TLS 1.3 and 1.2 only
SSLProtocol -all +TLSv1.3 +TLSv1.2
# Secure ciphers (TLS 1.3 ciphers are built-in)
SSLCipherSuite ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384
SSLHonorCipherOrder on
# OCSP Stapling
SSLUseStapling on
SSLStaplingResponderTimeout 5
SSLStaplingReturnResponderErrors off
# Security headers
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 "strict-origin-when-cross-origin"
# CSP
Header always set Content-Security-Policy "default-src 'self'; script-src 'self' 'nonce-${NONCE}'; object-src 'none'; base-uri 'self'; frame-ancestors 'none';"
</VirtualHost>
<VirtualHost *:80>
ServerName example.com
Redirect permanent / https://example.com/
</VirtualHost>
Cloudflare
When using Cloudflare as a reverse proxy, TLS termination happens at the edge:
Cloudflare Settings:
SSL/TLS: Full (strict)
Minimum TLS Version: 1.2
Opportunistic Encryption: On
Always Use HTTPS: On
Automatic HTTPS Rewrites: On
Certificate Transparency Monitoring: On
HTTP/2 to Origin: On
HTTP/3: On
Security Headers (via Cloudflare Transform Rules):
- Strict-Transport-Security: max-age=31536000; includeSubDomains; preload
- X-Content-Type-Options: nosniff
- X-Frame-Options: DENY
- Referrer-Policy: strict-origin-when-cross-origin
- Permissions-Policy: geolocation=(), microphone=()
WAF Custom Rules:
- Block missing CSP header
- Block weak TLS versions
- Rate limit by IP
CDN (Generic)
# When behind a CDN (e.g., CloudFront, Fastly, Akamai)
# TLS is terminated at the CDN edge — configure there.
# Origin server should still serve security headers:
server {
listen 443 ssl http2;
server_name origin.example.com;
# Origin certificate (wildcard or Let's Encrypt)
ssl_certificate /etc/ssl/certs/origin.pem;
ssl_certificate_key /etc/ssl/private/origin.key;
ssl_protocols TLSv1.3;
ssl_ciphers HIGH:!aNULL:!MD5;
# Forward client certificate info from CDN
ssl_verify_client optional;
# 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 Content-Security-Policy "default-src 'self'; script-src 'self'; object-src 'none';" always;
# ... application routing ...
}
Conclusion
HTTPS and security headers are essential, practical measures with immediate impact:
- Use TLS 1.3 and prefer ECDHE for forward secrecy
- Automate certificate issuance and renewal (Let’s Encrypt + certbot)
- Use CSP (with nonces or hashes) to mitigate XSS
- Gradually deploy HSTS and consider preload only when ready
- Harden servers, monitor certificates, and test with SSL Labs and Mozilla Observatory
These controls significantly reduce common attack vectors and are well worth the engineering effort.
Further Reading
- Mozilla TLS recommendations: https://ssl-config.mozilla.org/
- HSTS Preload: https://hstspreload.org/
- Content Security Policy (MDN): https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP
- OWASP TLS Cheat Sheet: https://cheatsheetseries.owasp.org/cheatsheets/Transport_Layer_Protection_Cheat_Sheet.html
- SSL Labs: https://www.ssllabs.com/
Comments