Security

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.

Md Sanwar Hossain April 6, 2026 17 min read TLS & Headers
Security headers and TLS hardening for production APIs HSTS CSP CORS TLS 1.3 Spring Boot

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

  1. Why Security Headers & TLS Are Skipped and Shouldn't Be
  2. HTTP Security Headers: The Complete List
  3. HSTS: HTTP Strict Transport Security
  4. Content Security Policy (CSP) for APIs
  5. CORS: The Most Misunderstood Security Control
  6. Spring Boot Security Headers Configuration
  7. TLS 1.3: Configuration & Cipher Suite Hardening
  8. Certificate Lifecycle: Let's Encrypt, ACM & OCSP
  9. Testing: SSL Labs, securityheaders.com & HSTS Preload
  10. Security Headers Checklist for Production
  11. 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;
}
Security headers and TLS hardening stack: HSTS, CORS, CSP, X-Frame-Options, certificate transparency
Security Headers & TLS Hardening Stack — layered from transport to content controls. Source: mdsanwarhossain.me

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.

TLS certificate lifecycle and auto-renewal flow: certificate request to ACME Let's Encrypt to deployment to auto-renewal
TLS Certificate Lifecycle & Auto-Renewal Flow — from request to production deployment. Source: mdsanwarhossain.me

9. Testing: SSL Labs, securityheaders.com & HSTS Preload

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.

12. mTLS for Service-to-Service Communication

TLS secures browser-to-server traffic, but in microservice architectures the most critical attack surface is lateral movement between services. Mutual TLS (mTLS) requires both client and server to present certificates, eliminating the risk of a compromised service impersonating another. In Spring Boot microservices, configure mTLS at the Tomcat/Netty level or delegate to your service mesh (Istio/Linkerd):

# application.yml — Spring Boot mTLS server config
server:
  ssl:
    enabled: true
    key-store: classpath:keystore/server.p12
    key-store-password: ${KEYSTORE_PASSWORD}
    key-store-type: PKCS12
    trust-store: classpath:keystore/truststore.p12
    trust-store-password: ${TRUSTSTORE_PASSWORD}
    client-auth: need           # REQUIRE client certificate
    protocol: TLSv1.3
    enabled-protocols: TLSv1.3
// Spring Security: extract identity from client certificate
@Configuration
public class MtlsSecurityConfig {

    @Bean
    SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        return http
            .x509(x509 -> x509
                .subjectPrincipalRegex("CN=(.*?),")      // extract service name from CN
                .userDetailsService(mtlsUserDetailsService())
            )
            .authorizeHttpRequests(auth -> auth
                .requestMatchers("/internal/**").hasRole("SERVICE")
                .anyRequest().authenticated()
            )
            .build();
    }
}

For Istio-managed mTLS (the production choice for large deployments), enable strict mode at the namespace level and rely on Istio's automatic certificate rotation via SPIFFE/SPIRE — your Spring Boot app needs zero certificate management code:

# Istio PeerAuthentication — strict mTLS for namespace
apiVersion: security.istio.io/v1beta1
kind: PeerAuthentication
metadata:
  name: default
  namespace: production
spec:
  mtls:
    mode: STRICT     # Reject plaintext traffic — no exceptions

13. WAF Integration and Rate Limiting Patterns

Even with A+ TLS and airtight security headers, your API is exposed to Layer 7 attacks — SQLi via query parameters, XSS in JSON bodies, and HTTP flood DDoS. A Web Application Firewall (WAF) inspects HTTP request content and blocks malicious patterns before they reach your Spring Boot application. AWS WAF, Cloudflare WAF, and Nginx with ModSecurity all integrate cleanly with Spring Boot deployments:

# AWS WAF association via CloudFormation
WebACLAssociation:
  Type: AWS::WAFv2::WebACLAssociation
  Properties:
    WebACLArn: !Ref WAFWebACL
    ResourceArn: !Ref ALBArn

# WAF rules — OWASP Core Rule Set managed by AWS
ManagedRuleGroups:
  - Name: AWSManagedRulesCommonRuleSet
    Priority: 10
    OverrideAction: None   # Block matching requests
  - Name: AWSManagedRulesKnownBadInputsRuleSet
    Priority: 20
    OverrideAction: None

Complement WAF with application-level rate limiting using Spring's Bucket4j integration. Rate limits protect against credential stuffing, scraping, and brute-force attacks that bypass WAF because they use valid HTTP syntax:

// Bucket4j rate limiter — 100 requests/min per IP
@Component
public class RateLimitFilter extends OncePerRequestFilter {

    private final Map<String, Bucket> buckets = new ConcurrentHashMap<>();

    private Bucket bucketFor(String ip) {
        return buckets.computeIfAbsent(ip, k ->
            Bucket.builder()
                .addLimit(Bandwidth.classic(100,
                    Refill.greedy(100, Duration.ofMinutes(1))))
                .build()
        );
    }

    @Override
    protected void doFilterInternal(HttpServletRequest req,
            HttpServletResponse res, FilterChain chain)
            throws ServletException, IOException {

        String ip = req.getRemoteAddr();
        Bucket bucket = bucketFor(ip);

        if (bucket.tryConsume(1)) {
            res.addHeader("X-RateLimit-Remaining",
                String.valueOf(bucket.getAvailableTokens()));
            chain.doFilter(req, res);
        } else {
            res.setStatus(429);
            res.addHeader("Retry-After", "60");
            res.getWriter().write("{\"error\":\"Rate limit exceeded\"}");
        }
    }
}

The security layering model for a production Spring Boot API: DNS → Cloudflare WAF (Layer 7 attack blocking) → AWS ALB with WAF (rate limiting and geo-blocking) → Nginx (TLS termination, security headers) → Spring Boot (application-level rate limiting, Spring Security). Each layer catches different attack vectors, and no single misconfiguration can expose you end-to-end.

14. Automating Security Header Validation in CI/CD

Manually testing security headers after each release is error-prone. Automate the validation in your CI pipeline with a combination of OWASP ZAP passive scan and a dedicated security headers check. Here's how to integrate both into a GitHub Actions pipeline:

# .github/workflows/security-headers-check.yml
name: Security Headers Validation
on:
  push:
    branches: [main, release/*]

jobs:
  security-scan:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - name: Start application
        run: docker compose up -d app
      - name: Wait for health check
        run: curl --retry 10 --retry-delay 2 http://localhost:8080/actuator/health
      - name: Run OWASP ZAP baseline scan
        uses: zaproxy/action-baseline@v0.12.0
        with:
          target: http://localhost:8080
          rules_file_name: .zap/rules.tsv
          fail_action: true
      - name: Check critical headers
        run: |
          headers=$(curl -sI http://localhost:8080/api/health)
          echo "$headers" | grep -i "Strict-Transport-Security" || exit 1
          echo "$headers" | grep -i "X-Content-Type-Options: nosniff" || exit 1
          echo "$headers" | grep -i "X-Frame-Options" || exit 1
          echo "$headers" | grep -i "Content-Security-Policy" || exit 1
          echo "✅ All critical security headers present"

Additionally, write JUnit integration tests using MockMvc to assert on security header values. This catches regressions when Spring Security configuration changes:

@SpringBootTest
@AutoConfigureMockMvc
class SecurityHeadersTest {

    @Autowired
    private MockMvc mockMvc;

    @Test
    void everyResponse_hasRequiredSecurityHeaders() throws Exception {
        mockMvc.perform(get("/api/health"))
            .andExpect(header().string(
                "X-Content-Type-Options", "nosniff"))
            .andExpect(header().string(
                "X-Frame-Options", "DENY"))
            .andExpect(header().exists(
                "Strict-Transport-Security"))
            .andExpect(header().exists(
                "Content-Security-Policy"));
    }

    @Test
    void hstsHeader_includesMaxAgeAndSubdomains() throws Exception {
        mockMvc.perform(get("/api/health"))
            .andExpect(header().string(
                "Strict-Transport-Security",
                containsString("max-age=31536000")))
            .andExpect(header().string(
                "Strict-Transport-Security",
                containsString("includeSubDomains")));
    }
}

Make security header tests mandatory in your PR checks — a failed security header regression should block deployment just like a failing unit test. The combination of automated CI validation and manual SSL Labs checks gives you defense-in-depth against accidental security header regressions.

15. Subresource Integrity and Supply Chain Security for Frontend Assets

Even with a hardened server, a compromised CDN delivering your JavaScript is a critical attack vector. Subresource Integrity (SRI) allows browsers to verify that files delivered from CDNs haven't been tampered with. Any CDN-hosted script or stylesheet that deviates from its expected cryptographic hash is blocked before execution — protecting your users from CDN-level supply chain attacks:

<!-- SRI-protected CDN scripts -->
<script
  src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/js/bootstrap.bundle.min.js"
  integrity="sha384-C6RzsynM9kWDrMNeT87bh95OGNyZPhcTNXj1NW7RuBCsyN/o0jlpcV8Qyq46cDfL"
  crossorigin="anonymous"></script>

<!-- SRI-protected stylesheet -->
<link
  rel="stylesheet"
  href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css"
  integrity="sha384-T3c6CoIi6uLrA9TneNEoa7RxnatzjcDSCmG1MXxSR1GAsXEV/Dwwykc2MPK8M2HN"
  crossorigin="anonymous">

Generate SRI hashes with the openssl tool or the online SRI Hash Generator. Automate hash generation in your build pipeline so that every CDN library upgrade automatically generates the new hash — preventing the common mistake of forgetting to update the hash after a version bump:

# Generate SRI hash from a CDN URL
curl -s https://cdn.example.com/lib.js | \
  openssl dgst -sha384 -binary | \
  openssl base64 -A | \
  awk '{print "sha384-"$0}'

# Expected output: sha384-C6RzsynM9kWDrMNeT87bh95OGNyZPhcTNXj1NW7RuBCsyN/...

Combine SRI with a strict Content Security Policy that disallows inline scripts and only permits scripts from your own origin and approved CDNs. This two-layer defence blocks both CDN compromise (SRI) and injected inline JavaScript (CSP). For Spring Boot Thymeleaf applications, configure SRI in your layout templates and use the th:integrity attribute for dynamic hash binding during the build. Self-hosted assets (your application's own JS/CSS) don't require SRI since they're served from your origin and protected by TLS, but all third-party CDN dependencies should have SRI hashes.

16. Measuring and Tracking Your Security Posture Over Time

Security hardening without measurement is incomplete. Establish a baseline security posture score for your production API on day one, then track it over time with automated weekly scans. The combination of SSL Labs, securityheaders.com, and OWASP ZAP gives you three independent data points that cover different attack surfaces — TLS configuration, HTTP response headers, and application-level vulnerabilities respectively.

Build a simple security scorecard dashboard using your CI/CD pipeline that runs all three checks weekly and stores the results. Define minimum acceptable scores (SSL Labs: A+, securityheaders.com: A, ZAP: zero high findings) as quality gates. Any regression below these thresholds automatically creates a security ticket and notifies the engineering team. This transforms security from a one-time hardening exercise into a continuous engineering practice.

Track these metrics on a rolling 90-day chart: number of security headers correctly configured, TLS protocol version support (TLS 1.3 adoption), certificate expiry days remaining (alert at 30 days), and number of open ZAP medium/high findings. When these metrics are visible on your engineering dashboard alongside deployment frequency and error rates, security becomes a first-class engineering concern rather than a compliance checkbox that gets attention only before audits.

security headers production HSTS preload Content Security Policy CORS misconfiguration TLS 1.3 Spring Boot cipher suite hardening X-Frame-Options Spring Security headers certificate lifecycle SSL Labs A+

Leave a Comment

Related Posts

Md Sanwar Hossain - Software Engineer
Md Sanwar Hossain

Software Engineer · Java · Spring Boot · Microservices · AI/LLM Systems

All Posts
Last updated: April 6, 2026