Security Headers & TLS Hardening for Production APIs: HSTS, CSP, CORS & TLS 1.3 in 2026
Security headers and TLS configuration are the most commonly skipped production security controls — they don't affect functionality, so they're easy to ignore. But OWASP A05:2021 Security Misconfiguration includes missing security headers, and weak TLS enables MITM attacks that steal session tokens and API credentials. This guide gives you a complete, copy-paste Spring Boot configuration for headers and TLS that passes SSL Labs A+ and securityheaders.com A grade.
TL;DR — Test Before You Ship
"Misconfigured security headers and weak TLS are silent vulnerabilities — your API works fine, but attackers can exploit clickjacking, MIME sniffing, cookie theft, or downgrade attacks. Implement HSTS with preload, strict CSP, CORS allowlist, X-Frame-Options, and TLS 1.3-only. Test with securityheaders.com and SSL Labs before every production release."
Table of Contents
- Why Security Headers & TLS Are Skipped and Shouldn't Be
- HTTP Security Headers: The Complete List
- HSTS: HTTP Strict Transport Security
- Content Security Policy (CSP) for APIs
- CORS: The Most Misunderstood Security Control
- Spring Boot Security Headers Configuration
- TLS 1.3: Configuration & Cipher Suite Hardening
- Certificate Lifecycle: Let's Encrypt, ACM & OCSP
- Testing: SSL Labs, securityheaders.com & HSTS Preload
- Security Headers Checklist for Production
- Conclusion
1. Why Security Headers & TLS Are Skipped and Shouldn't Be
OWASP A05:2021 — Security Misconfiguration is the fifth most critical web application security risk, affecting 90% of applications tested. Missing security headers are explicitly called out as a misconfiguration. They're skipped because: they have no functional impact in normal usage, they're not tested by default in integration or unit tests, and developers rarely see the consequences until an actual attack.
The attacks they prevent are real: clickjacking (iframe embedding to trick users into unintended clicks), MIME-type sniffing attacks, protocol downgrade attacks from HTTPS to HTTP (enabling MITM), cookie theft via insecure cookie attributes, and cross-site scripting enabled by missing CSP. Spring Boot's default security headers are better than none but insufficient for production.
2. HTTP Security Headers: The Complete List
| Header | Attack Prevented | Recommended Value |
|---|---|---|
| Strict-Transport-Security | HTTP downgrade / MITM | max-age=31536000; includeSubDomains; preload |
| Content-Security-Policy | XSS, data injection | default-src 'none'; frame-ancestors 'none' |
| X-Frame-Options | Clickjacking | DENY |
| X-Content-Type-Options | MIME sniffing attacks | nosniff |
| Referrer-Policy | Referrer information leak | strict-origin-when-cross-origin |
| Permissions-Policy | Camera/mic/geolocation abuse | camera=(), microphone=(), geolocation=() |
| X-XSS-Protection | Legacy XSS filter | 0 (deprecated — use CSP instead) |
3. HSTS: HTTP Strict Transport Security
HSTS instructs browsers to only communicate with your domain over HTTPS — no HTTP fallback, ever. The preload directive goes further: browsers ship with a preloaded HSTS list, so they never send even the first request over HTTP, even to a domain they've never visited before.
Strict-Transport-Security: max-age=31536000; includeSubDomains; preload
// Spring Security Java config
http.headers(headers -> headers
.httpStrictTransportSecurity(hsts -> hsts
.maxAgeInSeconds(31536000)
.includeSubDomains(true)
.preload(true)
)
);
HSTS Preload requirements: serve only HTTPS (no HTTP), have HSTS header with max-age ≥ 31536000, includeSubDomains, and preload directive. Submit at hstspreload.org. Warning: once on the preload list, removing yourself takes months.
4. Content Security Policy (CSP) for APIs
CSP for REST APIs is simpler than for web apps: since browsers don't render your API responses as HTML, you only need to prevent your API from being embedded in frames and restrict what can be loaded if content is ever rendered.
# For REST APIs (no HTML rendering):
Content-Security-Policy: default-src 'none'; frame-ancestors 'none'
# For web apps with inline scripts (use nonces):
Content-Security-Policy: default-src 'self';
script-src 'self' 'nonce-{RANDOM_NONCE}';
style-src 'self' 'nonce-{RANDOM_NONCE}';
img-src 'self' data: https:;
font-src 'self';
connect-src 'self' https://api.myservice.com;
frame-ancestors 'none';
base-uri 'self';
form-action 'self';
report-uri https://mdsanwarhossain.me/csp-report
# Spring Security: add via headers filter
http.headers(h -> h.contentSecurityPolicy(csp ->
csp.policyDirectives("default-src 'none'; frame-ancestors 'none'")
));
5. CORS: The Most Misunderstood Security Control
Critical Misconception: CORS is enforced by browsers, not by servers. A server with Access-Control-Allow-Origin: * will still respond to requests from curl, Postman, or malicious server-side code. CORS only protects against browser-based cross-origin requests — it is NOT a substitute for authentication and authorization.
// WRONG — allows any origin with credentials
@CrossOrigin(origins = "*", allowCredentials = "true")
// This causes a browser error but signals misconfiguration
// CORRECT — explicit origin allowlist
@Bean
public CorsConfigurationSource corsConfigurationSource() {
CorsConfiguration config = new CorsConfiguration();
config.setAllowedOrigins(List.of(
"https://app.myservice.com",
"https://admin.myservice.com"
));
config.setAllowedMethods(List.of("GET", "POST", "PUT", "DELETE", "OPTIONS"));
config.setAllowedHeaders(List.of("Authorization", "Content-Type", "X-Request-ID"));
config.setAllowCredentials(true);
config.setMaxAge(3600L);
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/api/**", config);
return source;
}
6. Spring Boot Security Headers Configuration
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.cors(cors -> cors.configurationSource(corsConfigurationSource()))
.csrf(AbstractHttpConfigurer::disable) // REST APIs use stateless JWT
.headers(headers -> headers
.httpStrictTransportSecurity(hsts -> hsts
.maxAgeInSeconds(31536000)
.includeSubDomains(true)
.preload(true))
.frameOptions(frame -> frame.deny())
.contentTypeOptions(Customizer.withDefaults())
.referrerPolicy(referrer -> referrer
.policy(ReferrerPolicyHeaderWriter.ReferrerPolicy.STRICT_ORIGIN_WHEN_CROSS_ORIGIN))
.permissionsPolicy(permissions -> permissions
.policy("camera=(), microphone=(), geolocation=(), interest-cohort=()"))
.contentSecurityPolicy(csp -> csp
.policyDirectives("default-src 'none'; frame-ancestors 'none'"))
)
.sessionManagement(session -> session
.sessionCreationPolicy(SessionCreationPolicy.STATELESS));
return http.build();
}
}
7. TLS 1.3: Configuration & Cipher Suite Hardening
TLS 1.0 and 1.1 are officially deprecated (RFC 8996). PCI-DSS 3.2+ requires disabling TLS 1.0. TLS 1.2 is acceptable but TLS 1.3 is preferred: it eliminates weak cipher suites, mandates Perfect Forward Secrecy (PFS), and reduces handshake latency by one round-trip. Java 11+ (JDK 11) supports TLS 1.3 natively.
# application.yml — Spring Boot embedded Tomcat TLS config
server:
port: 8443
ssl:
enabled: true
enabled-protocols: TLSv1.3,TLSv1.2
protocol: TLS
key-store: classpath:keystore.p12
key-store-password: ${SSL_KEYSTORE_PASSWORD}
key-store-type: PKCS12
# TLS 1.3 cipher suites (Java 11+)
ciphers: >
TLS_AES_256_GCM_SHA384,
TLS_AES_128_GCM_SHA256,
TLS_CHACHA20_POLY1305_SHA256,
TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384,
TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256
Key cipher suite properties: All TLS 1.3 cipher suites provide AEAD (authenticated encryption) and mandatory PFS via ECDHE key exchange. Avoid: RSA key exchange (no PFS), RC4, 3DES, MD5, SHA-1 based suites, anonymous DH suites.
8. Certificate Lifecycle: Let's Encrypt, ACM & OCSP
Certificate expiry is one of the most common self-inflicted production outages. Automate certificate lifecycle end-to-end.
- Let's Encrypt + Certbot: Free, automated 90-day certs. Auto-renewal via cron:
certbot renew --quiet. Deploy via cert-manager in Kubernetes withClusterIssuerusing ACME/ECDSA keys. - AWS Certificate Manager (ACM): Free managed certs for AWS load balancers. Auto-renews 60 days before expiry. Zero operational overhead for AWS workloads.
- OCSP Stapling: Reduces latency by having the server staple the OCSP response (certificate validity proof) directly in the TLS handshake, eliminating the client's need to contact the CA's OCSP responder.
- Certificate Transparency: All public certificates must be logged to CT logs. Monitor your CT logs at crt.sh for unauthorized certificate issuance for your domain.
- Expiry monitoring: Alert at 30 days and 7 days before expiry. Use Prometheus blackbox exporter
probe_ssl_earliest_cert_expirymetric or Datadog SSL integration.
9. Testing: SSL Labs, securityheaders.com & HSTS Preload
- SSL Labs (ssllabs.com/ssltest): Comprehensive TLS analysis. Tests protocol support, cipher suites, certificate chain, HSTS, key strength. Target: A+ grade. Common reasons for missing A+: TLS 1.2 still enabled without TLS 1.3 preference, or HSTS max-age below 180 days.
- securityheaders.com: Tests all HTTP security headers. Shows current values, missing headers, and correct configurations. Target: A grade. Missing headers show as red — each is a specific attack vector.
- Mozilla Observatory (observatory.mozilla.org): Combined security header + TLS testing with scoring. Good for executive-level security reporting.
- Automate in CI/CD: Use curl to validate headers in integration tests:
curl -I https://api.myservice.com/ | grep -i "strict-transport\|x-frame\|content-security"
10. Security Headers Checklist for Production
- ✅ HSTS with max-age=31536000, includeSubDomains, preload — submitted to HSTS preload list
- ✅ No allowedOrigins("*") with allowCredentials — use explicit origin allowlist in CORS config
- ✅ Content-Security-Policy header set — default-src 'none' for REST APIs, nonce-based for web apps
- ✅ X-Frame-Options: DENY — prevents clickjacking (use frame-ancestors 'none' in CSP as well)
- ✅ X-Content-Type-Options: nosniff — prevents MIME type sniffing attacks
- ✅ Referrer-Policy: strict-origin-when-cross-origin — prevents referrer information leakage
- ✅ TLS 1.2 minimum, TLS 1.3 preferred — no SSLv3, no TLS 1.0, no TLS 1.1
- ✅ Only AEAD cipher suites — no RC4, 3DES, MD5, SHA-1, no anonymous DH
- ✅ Certificate expiry alert at 30 and 7 days — auto-renewal configured (certbot/cert-manager/ACM)
- ✅ SSL Labs A+ grade verified — run before each production release
- ✅ securityheaders.com A grade verified — all required headers present and correctly configured
- ✅ CT log monitoring enabled — crt.sh alerts for unexpected certificate issuance on your domains
11. Conclusion
Security headers and TLS hardening are high-value, low-effort production security controls. The Spring Security configuration in Section 6, combined with the TLS configuration in Section 7, takes under an hour to implement and gives you protection against clickjacking, MIME sniffing, protocol downgrade attacks, and weak cipher exploitation.
Run SSL Labs and securityheaders.com after every significant infrastructure change. These free tools give you an objective, external view of your security posture that internal testing often misses. A+ on SSL Labs and A on securityheaders.com should be a build gate for every production release, not an afterthought.