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:
-
Disable Default Security Configuration: If you’re providing your own
SecurityFilterChainbean, Spring Security will automatically disable the default configuration. However, if you’re extendingWebSecurityConfigurerAdapterand 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/healthmight return a 200 OK without authentication. - Fix: Ensure you have a
SecurityFilterChainbean defined, or if usingWebSecurityConfigurerAdapter, make sure you’ve overriddenconfigure(HttpSecurity)to explicitly define your security rules. If you have multipleWebSecurityConfigurerAdapterclasses, ensure their order of precedence is correct using@Order. - Why it works: By defining your own
SecurityFilterChainor carefully configuringWebSecurityConfigurerAdapter, you explicitly control which security mechanisms are applied and to which paths, overriding any less secure defaults.
- Diagnosis: Run your application and check for enabled endpoints you didn’t intend. For example,
-
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
UserDetailsServiceimplementation. If it doesn’t explicitly configure a password encoder or usesNoOpPasswordEncoder, your passwords are not hashed. -
Fix: Configure
BCryptPasswordEncoder(orSCryptPasswordEncoder,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:
BCryptPasswordEncoderuses a strong, one-way hashing algorithm with a salt, making it computationally infeasible to reverse-engineer passwords even if the database is compromised.
-
-
CSRF Protection: Cross-Site Request Forgery (CSRF) protection is enabled by default but can be misconfigured or disabled.
- Diagnosis: Check your
HttpSecurityconfiguration. 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 theCookieCsrfTokenRepositoryto be accessible by JavaScript (thoughhttpOnlyFalse()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.
- Diagnosis: Check your
-
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()(ornewSession()) 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, andIF_REQUIREDorALWAYScontrols when sessions are created.
-
-
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.
-
-
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).
-
-
Limit Endpoint Exposure: Not all endpoints need to be accessible.
-
Diagnosis: Review your
.authorizeRequests()or.requestMatchers()configurations. Are security-sensitive endpoints like/actuatoror 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.
-
-
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).