Spring Boot Security Hardening Checklist

Spring Boot Security, by default, is not secure enough for production environments.

Let’s see what a typical Spring Boot application looks like when it’s first wired up with Spring Security. We’ll use a basic setup with just a few common configurations to illustrate.

// SecurityConfig.java
@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
            .authorizeRequests()
                .antMatchers("/public").permitAll()
                .anyRequest().authenticated()
                .and()
            .formLogin() // Default form login enabled
                .and()
            .httpBasic(); // Default basic auth enabled
    }

    @Bean
    public UserDetailsService userDetailsService() {
        InMemoryUserDetailsManager manager = new InMemoryUserDetailsManager();
        manager.createUser(User.withUsername("user").password("password").roles("USER").build());
        return manager;
    }
}

This configuration, while functional, leaves several doors wide open. The real power comes from understanding how to turn those open doors into fortified gateways.

Here’s a checklist to harden your Spring Boot Security:

  1. Disable Default Security Configuration: If you’re providing your own SecurityFilterChain bean, Spring Security will automatically disable the default configuration. However, if you’re extending WebSecurityConfigurerAdapter and not explicitly disabling older configurations, you might inherit unwanted defaults.

    • Diagnosis: Run your application and check for enabled endpoints you didn’t intend. For example, curl http://localhost:8080/actuator/health might return a 200 OK without authentication.
    • Fix: Ensure you have a SecurityFilterChain bean defined, or if using WebSecurityConfigurerAdapter, make sure you’ve overridden configure(HttpSecurity) to explicitly define your security rules. If you have multiple WebSecurityConfigurerAdapter classes, ensure their order of precedence is correct using @Order.
    • Why it works: By defining your own SecurityFilterChain or carefully configuring WebSecurityConfigurerAdapter, you explicitly control which security mechanisms are applied and to which paths, overriding any less secure defaults.
  2. Stronger Password Encoding: The default NoOpPasswordEncoder (which stores passwords in plain text) is a critical vulnerability. You must use a strong, modern encoding mechanism.

    • Diagnosis: Inspect your UserDetailsService implementation. If it doesn’t explicitly configure a password encoder or uses NoOpPasswordEncoder, your passwords are not hashed.

    • Fix: Configure BCryptPasswordEncoder (or SCryptPasswordEncoder, Argon2PasswordEncoder):

      @Bean
      public PasswordEncoder passwordEncoder() {
          return new BCryptPasswordEncoder();
      }
      

      When creating users, ensure their passwords are encoded: manager.createUser(User.withUsername("user").password(passwordEncoder().encode("password")).roles("USER").build());

    • Why it works: BCryptPasswordEncoder uses a strong, one-way hashing algorithm with a salt, making it computationally infeasible to reverse-engineer passwords even if the database is compromised.

  3. CSRF Protection: Cross-Site Request Forgery (CSRF) protection is enabled by default but can be misconfigured or disabled.

    • Diagnosis: Check your HttpSecurity configuration. If you see .csrf().disable(), it’s a vulnerability. Ensure CSRF tokens are being generated and submitted for state-changing requests (POST, PUT, DELETE).
    • Fix: Keep .csrf().csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse()) (or a similar robust implementation) enabled. For single-page applications (SPAs) that rely on cookies, you might need to configure the CookieCsrfTokenRepository to be accessible by JavaScript (though httpOnlyFalse() is a common compromise; a more secure approach involves custom token management).
    • Why it works: CSRF protection ensures that requests to your application originate from your own website, not from a malicious third-party site, by requiring a unique, unpredictable token with each request.
  4. Session Management and Fixation: Default session management can be vulnerable to fixation attacks.

    • Diagnosis: Review your .sessionManagement() configuration. Look for disabled session creation or insecure session ID handling.

    • Fix: Configure session fixation protection and set appropriate session timeouts:

      http
          .sessionManagement()
              .sessionCreationPolicy(SessionCreationPolicy.IF_REQUIRED) // Or ALWAYS, NEVER
              .sessionFixation().migrateSession() // Or newSession()
              .maximumSessions(1) // Or a reasonable limit
              .expiredSessionStrategy(new ExpiredSessionStrategy()) // Custom handler
              .invalidSessionUrl("/login?invalid=true");
      
    • Why it works: migrateSession() (or newSession()) invalidates the old session ID upon login, preventing an attacker from pre-setting a session ID. maximumSessions(1) ensures only one session per user is active, and IF_REQUIRED or ALWAYS controls when sessions are created.

  5. HTTP Strict Transport Security (HSTS): Ensure all communication is over HTTPS.

    • Diagnosis: Check if your application is accessible via HTTP after being configured for HTTPS.

    • Fix: Enable HSTS using a filter. You can configure it within Spring Security or as a separate servlet filter.

      // If using Spring Security's built-in support (Spring Boot 2.x+)
      http
          .requiresChannel()
              .anyRequest().requiresSecure(); // Or requiresInsecure() for specific paths
      
      // Or as a separate filter for more control (example for Tomcat/Jetty)
      @Bean
      public FilterRegistrationBean securityHeadersFilter() {
          FilterRegistrationBean filterRegBean = new FilterRegistrationBean();
          filterRegBean.setFilter(new HstsHeaderFilter()); // Custom or library filter
          filterRegBean.addUrlPatterns("/*");
          filterRegBean.setOrder(Ordered.HIGHEST_PRECEDENCE);
          return filterRegBean;
      }
      // HstsHeaderFilter would add 'Strict-Transport-Security' header
      
    • Why it works: HSTS instructs browsers to only communicate with your domain over HTTPS for a specified period, preventing downgrade attacks and man-in-the-middle attacks over insecure HTTP.

  6. Security Headers: Implement essential security headers to protect against common web vulnerabilities.

    • Diagnosis: Use browser developer tools or online security scanners (like SecurityHeaders.com) to check for missing headers like Content-Security-Policy, X-Content-Type-Options, X-Frame-Options, Referrer-Policy.

    • Fix: Add these headers, often via a custom filter or Spring Security’s HeadersSecurityFilter.

      http
          .headers()
              .contentTypeOptions() // X-Content-Type-Options: nosniff
              .xssProtection() // X-XSS-Protection: 1; mode=block
              .cacheControl() // Cache-Control, Pragma, Expires headers
              .httpStrictTransportSecurity() // HSTS header
              .frameOptions() // X-Frame-Options: DENY (or SAMEORIGIN)
              .contentSecurityPolicy("default-src 'self'; script-src 'self'"); // CSP
      
    • Why it works: These headers instruct the browser to enforce security policies, such as preventing clickjacking (X-Frame-Options), blocking XSS by disallowing certain content types (X-Content-Type-Options), and controlling where content can be loaded from (Content-Security-Policy).

  7. Limit Endpoint Exposure: Not all endpoints need to be accessible.

    • Diagnosis: Review your .authorizeRequests() or .requestMatchers() configurations. Are security-sensitive endpoints like /actuator or management APIs exposed more broadly than necessary?

    • Fix: Explicitly restrict access to sensitive endpoints and grant access only to authenticated users with appropriate roles.

      http
          .authorizeRequests()
              .antMatchers("/public/**").permitAll()
              .antMatchers("/api/admin/**").hasRole("ADMIN")
              .antMatchers("/actuator/**").hasRole("ACTUATOR_USER") // Example
              .anyRequest().authenticated();
      
    • Why it works: This principle of least privilege ensures that users and systems can only access the resources they absolutely need, reducing the attack surface.

  8. Rate Limiting: Protect against brute-force attacks on login endpoints or resource-intensive APIs.

    • Diagnosis: No built-in Spring Security feature directly handles rate limiting. You’d typically observe excessive login attempts or API abuse in logs.

    • Fix: Integrate a rate-limiting library (e.g., Bucket4j, Resilience4j) or a dedicated API gateway.

      // Example using Bucket4j (simplified)
      // Add Bucket4j filter to your SecurityFilterChain
      // Configure rules for login endpoints, e.g., max 10 attempts per minute per IP.
      
    • Why it works: Rate limiting caps the number of requests a client can make within a given time frame, making brute-force attacks and denial-of-service attempts significantly harder and more expensive for an attacker.

After implementing these, the next common hurdle you’ll face is correctly configuring authorization for complex role hierarchies and method-level security (@PreAuthorize, @PostAuthorize).

Want structured learning?

Take the full Spring-boot course →