Server Authentication
Secure your Spring Boot Admin Server using Spring Security to protect the UI and API endpoints.
Overview
A secured Admin Server requires:
- Spring Security dependency
- SecurityFilterChain configuration
- User credentials (in-memory, database, LDAP, OAuth2, etc.)
- CSRF protection with exemptions for client registration
Quick Start
1. Add Spring Security Dependency
Maven:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
Gradle:
implementation 'org.springframework.boot:spring-boot-starter-security'
2. Basic Configuration
Minimal security with default Spring Boot user:
spring:
security:
user:
name: admin
password: ${ADMIN_PASSWORD}
This provides:
- Form login at
/login - HTTP Basic authentication for API
- Single user with username
admin
3. Access the UI
Navigate to http://localhost:8080, and you'll be redirected to the login page.
Complete Security Configuration
For more control, use a custom SecurityFilterChain:
package com.example.admin;
import java.util.UUID;
import org.springframework.boot.autoconfigure.security.SecurityProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.Customizer;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.provisioning.InMemoryUserDetailsManager;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.SavedRequestAwareAuthenticationSuccessHandler;
import org.springframework.security.web.authentication.www.BasicAuthenticationFilter;
import org.springframework.security.web.csrf.CookieCsrfTokenRepository;
import org.springframework.security.web.csrf.CsrfTokenRequestAttributeHandler;
import org.springframework.security.web.servlet.util.matcher.PathPatternRequestMatcher;
import de.codecentric.boot.admin.server.config.AdminServerProperties;
import static org.springframework.http.HttpMethod.DELETE;
import static org.springframework.http.HttpMethod.POST;
@Configuration
public class SecurityConfig {
private final AdminServerProperties adminServer;
public SecurityConfig(AdminServerProperties adminServer) {
this.adminServer = adminServer;
}
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
// Redirect to login after successful authentication
SavedRequestAwareAuthenticationSuccessHandler successHandler =
new SavedRequestAwareAuthenticationSuccessHandler();
successHandler.setTargetUrlParameter("redirectTo");
successHandler.setDefaultTargetUrl(adminServer.path("/"));
http
.authorizeHttpRequests(auth -> auth
// Permit access to static resources
.requestMatchers(PathPatternRequestMatcher.withDefaults()
.matcher(adminServer.path("/assets/**")))
.permitAll()
// Permit access to login page
.requestMatchers(PathPatternRequestMatcher.withDefaults()
.matcher(adminServer.path("/login")))
.permitAll()
// Permit Admin Server's own actuator endpoints
.requestMatchers(PathPatternRequestMatcher.withDefaults()
.matcher(adminServer.path("/actuator/info")))
.permitAll()
.requestMatchers(PathPatternRequestMatcher.withDefaults()
.matcher(adminServer.path("/actuator/health")))
.permitAll()
// Require authentication for all other requests
.anyRequest().authenticated()
)
// Form login for UI
.formLogin(formLogin -> formLogin
.loginPage(adminServer.path("/login"))
.successHandler(successHandler)
)
// Logout configuration
.logout(logout -> logout
.logoutUrl(adminServer.path("/logout"))
)
// HTTP Basic for API clients
.httpBasic(Customizer.withDefaults());
// CSRF configuration (see CSRF Protection section)
http.addFilterAfter(new CustomCsrfFilter(), BasicAuthenticationFilter.class)
.csrf(csrf -> csrf
.csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse())
.csrfTokenRequestHandler(new CsrfTokenRequestAttributeHandler())
.ignoringRequestMatchers(
// Exempt client registration endpoints
PathPatternRequestMatcher.withDefaults()
.matcher(POST, adminServer.path("/instances")),
PathPatternRequestMatcher.withDefaults()
.matcher(DELETE, adminServer.path("/instances/*")),
// Exempt Admin Server's own actuator
PathPatternRequestMatcher.withDefaults()
.matcher(adminServer.path("/actuator/**"))
)
);
// Remember-me functionality
http.rememberMe(rememberMe -> rememberMe
.key(UUID.randomUUID().toString())
.tokenValiditySeconds(1209600) // 2 weeks
);
return http.build();
}
@Bean
public InMemoryUserDetailsManager userDetailsService(PasswordEncoder passwordEncoder) {
UserDetails user = User.builder()
.username("admin")
.password(passwordEncoder.encode(System.getenv("ADMIN_PASSWORD")))
.roles("ADMIN")
.build();
return new InMemoryUserDetailsManager(user);
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
}
Custom CSRF Filter
Required to make CSRF token available to JavaScript:
package com.example.admin;
import java.io.IOException;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.Cookie;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.security.web.csrf.CsrfToken;
import org.springframework.web.filter.OncePerRequestFilter;
import org.springframework.web.util.WebUtils;
public class CustomCsrfFilter extends OncePerRequestFilter {
public static final String CSRF_COOKIE_NAME = "XSRF-TOKEN";
@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response,
FilterChain filterChain)
throws ServletException, IOException {
CsrfToken csrf = (CsrfToken) request.getAttribute(CsrfToken.class.getName());
if (csrf != null) {
Cookie cookie = WebUtils.getCookie(request, CSRF_COOKIE_NAME);
String token = csrf.getToken();
if (cookie == null || token != null && !token.equals(cookie.getValue())) {
cookie = new Cookie(CSRF_COOKIE_NAME, token);
cookie.setPath("/");
response.addCookie(cookie);
}
}
filterChain.doFilter(request, response);
}
}
Configuration Options
Context Path
If your Admin Server uses a custom context path:
spring:
boot:
admin:
context-path: /admin
Adjust security matchers:
.requestMatchers(PathPatternRequestMatcher.withDefaults()
.matcher(adminServer.path("/assets/**")))
.permitAll()
The adminServer.path() method handles context path automatically.
Remember-Me
Enable persistent sessions:
http.rememberMe(rememberMe -> rememberMe
.key(UUID.randomUUID().toString()) // Unique key
.tokenValiditySeconds(1209600) // 2 weeks
.rememberMeParameter("remember-me") // Form parameter name
)
Note: Remember-me requires a UserDetailsService bean.
Session Management
Configure session behavior:
http.sessionManagement(session -> session
.sessionCreationPolicy(SessionCreationPolicy.IF_REQUIRED)
.maximumSessions(1) // Max 1 session per user
.maxSessionsPreventsLogin(false) // Invalidate old session
)
User Management
In-Memory Users
Simple for development or small deployments:
@Bean
public InMemoryUserDetailsManager userDetailsService(PasswordEncoder encoder) {
UserDetails admin = User.builder()
.username("admin")
.password(encoder.encode(System.getenv("ADMIN_PASSWORD")))
.roles("ADMIN")
.build();
UserDetails viewer = User.builder()
.username("viewer")
.password(encoder.encode(System.getenv("VIEWER_PASSWORD")))
.roles("USER")
.build();
return new InMemoryUserDetailsManager(admin, viewer);
}
Database Users
Use JdbcUserDetailsManager for database-backed users:
@Bean
public JdbcUserDetailsManager userDetailsService(DataSource dataSource,
PasswordEncoder encoder) {
JdbcUserDetailsManager manager = new JdbcUserDetailsManager(dataSource);
// Create default admin if not exists
if (!manager.userExists("admin")) {
UserDetails admin = User.builder()
.username("admin")
.password(encoder.encode(System.getenv("ADMIN_PASSWORD")))
.roles("ADMIN")
.build();
manager.createUser(admin);
}
return manager;
}
Database Schema:
CREATE TABLE users (
username VARCHAR(50) NOT NULL PRIMARY KEY,
password VARCHAR(100) NOT NULL,
enabled BOOLEAN NOT NULL
);
CREATE TABLE authorities (
username VARCHAR(50) NOT NULL,
authority VARCHAR(50) NOT NULL,
FOREIGN KEY (username) REFERENCES users(username)
);
CREATE UNIQUE INDEX ix_auth_username ON authorities (username, authority);
LDAP Authentication
Authenticate against an LDAP server:
@Bean
public SecurityFilterChain filterChain(HttpSecurity http,
AdminServerProperties adminServer) throws Exception {
http
.authorizeHttpRequests(/* ... */)
.formLogin(/* ... */)
.logout(/* ... */)
.httpBasic(Customizer.withDefaults());
return http.build();
}
@Configuration
public static class LdapConfig {
@Bean
public EmbeddedLdapServerContextSourceFactoryBean contextSourceFactoryBean() {
EmbeddedLdapServerContextSourceFactoryBean factory =
new EmbeddedLdapServerContextSourceFactoryBean();
factory.setPort(8389);
return factory;
}
@Bean
public AuthenticationManager authenticationManager(BaseLdapPathContextSource contextSource) {
LdapBindAuthenticationManagerFactory factory =
new LdapBindAuthenticationManagerFactory(contextSource);
factory.setUserDnPatterns("uid={0},ou=people");
factory.setUserDetailsContextMapper(new PersonContextMapper());
return factory.createAuthenticationManager();
}
}
Configuration:
spring:
ldap:
urls: ldap://ldap.company.com:389
base: dc=company,dc=com
username: cn=admin,dc=company,dc=com
password: ${LDAP_PASSWORD}
OAuth2 / OIDC
Use OAuth2 for Single Sign-On (SSO):
Dependencies:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-oauth2-client</artifactId>
</dependency>
Configuration:
spring:
security:
oauth2:
client:
registration:
keycloak:
client-id: spring-boot-admin
client-secret: ${OAUTH2_CLIENT_SECRET}
scope: openid,profile,email
authorization-grant-type: authorization_code
redirect-uri: "{baseUrl}/login/oauth2/code/{registrationId}"
provider:
keycloak:
issuer-uri: https://keycloak.company.com/realms/main
Security Configuration:
@Bean
public SecurityFilterChain filterChain(HttpSecurity http,
AdminServerProperties adminServer) throws Exception {
http
.authorizeHttpRequests(auth -> auth
.requestMatchers(PathPatternRequestMatcher.withDefaults()
.matcher(adminServer.path("/assets/**")))
.permitAll()
.requestMatchers(PathPatternRequestMatcher.withDefaults()
.matcher(adminServer.path("/login")))
.permitAll()
.anyRequest().authenticated()
)
.oauth2Login(oauth2 -> oauth2
.loginPage(adminServer.path("/login"))
)
.logout(logout -> logout
.logoutUrl(adminServer.path("/logout"))
.logoutSuccessUrl(adminServer.path("/"))
);
// CSRF and other configurations...
return http.build();
}
Role-Based Access Control
Restrict access by roles:
@Bean
public SecurityFilterChain filterChain(HttpSecurity http,
AdminServerProperties adminServer) throws Exception {
http.authorizeHttpRequests(auth -> auth
.requestMatchers(PathPatternRequestMatcher.withDefaults()
.matcher(adminServer.path("/assets/**")))
.permitAll()
.requestMatchers(PathPatternRequestMatcher.withDefaults()
.matcher(adminServer.path("/login")))
.permitAll()
// Only ADMIN can delete instances
.requestMatchers(PathPatternRequestMatcher.withDefaults()
.matcher(DELETE, adminServer.path("/instances/*")))
.hasRole("ADMIN")
// Only ADMIN can access logfile endpoint
.requestMatchers(PathPatternRequestMatcher.withDefaults()
.matcher(adminServer.path("/instances/*/actuator/logfile")))
.hasRole("ADMIN")
// USER and ADMIN can view everything else
.anyRequest().hasAnyRole("USER", "ADMIN")
)
.formLogin(formLogin -> formLogin.loginPage(adminServer.path("/login")))
.httpBasic(Customizer.withDefaults());
return http.build();
}
Create users with different roles:
@Bean
public InMemoryUserDetailsManager userDetailsService(PasswordEncoder encoder) {
UserDetails admin = User.builder()
.username("admin")
.password(encoder.encode(System.getenv("ADMIN_PASSWORD")))
.roles("ADMIN")
.build();
UserDetails viewer = User.builder()
.username("viewer")
.password(encoder.encode(System.getenv("VIEWER_PASSWORD")))
.roles("USER")
.build();
return new InMemoryUserDetailsManager(admin, viewer);
}
HTTP vs HTTPS
Local Development (HTTP)
For local development, HTTP is acceptable:
server:
port: 8080
HTTPS Configuration
Enable HTTPS for secure communication:
server:
port: 8443
ssl:
enabled: true
key-store: classpath:keystore.p12
key-store-password: ${KEYSTORE_PASSWORD}
key-store-type: PKCS12
key-alias: spring-boot-admin
Generate keystore:
keytool -genkeypair -alias spring-boot-admin \
-keyalg RSA -keysize 2048 \
-storetype PKCS12 \
-keystore keystore.p12 \
-validity 3650 \
-storepass changeit
Update Admin Client configuration:
spring:
boot:
admin:
client:
url: https://admin-server:8443
Reverse Proxy Setup
Behind Nginx
Nginx Configuration:
server {
listen 80;
server_name admin.company.com;
location / {
proxy_pass http://localhost:8080;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}
Admin Server Configuration:
server:
forward-headers-strategy: native
spring:
boot:
admin:
ui:
public-url: https://admin.company.com
Behind Apache
Apache Configuration:
<VirtualHost *:80>
ServerName admin.company.com
ProxyPreserveHost On
ProxyPass / http://localhost:8080/
ProxyPassReverse / http://localhost:8080/
RequestHeader set X-Forwarded-Proto "https"
RequestHeader set X-Forwarded-Port "443"
</VirtualHost>
Security Headers
Add security headers to protect against common attacks:
@Bean
public SecurityFilterChain filterChain(HttpSecurity http,
AdminServerProperties adminServer) throws Exception {
http
.headers(headers -> headers
// Content Security Policy
.contentSecurityPolicy(csp -> csp
.policyDirectives("default-src 'self'; " +
"script-src 'self' 'unsafe-inline'; " +
"style-src 'self' 'unsafe-inline'; " +
"img-src 'self' data:; " +
"font-src 'self' data:")
)
// Frame options
.frameOptions(frame -> frame.sameOrigin())
// XSS protection
.xssProtection(xss -> xss.headerValue(XXssProtectionHeaderWriter.HeaderValue.ENABLED_MODE_BLOCK))
// HSTS
.httpStrictTransportSecurity(hsts -> hsts
.includeSubDomains(true)
.maxAgeInSeconds(31536000)
)
);
// Other configurations...
return http.build();
}
Multiple Authentication Methods
Support both form login and HTTP Basic:
@Bean
public SecurityFilterChain filterChain(HttpSecurity http,
AdminServerProperties adminServer) throws Exception {
http
.authorizeHttpRequests(/* ... */)
.formLogin(formLogin -> formLogin
.loginPage(adminServer.path("/login"))
)
.httpBasic(Customizer.withDefaults())
.logout(logout -> logout
.logoutUrl(adminServer.path("/logout"))
);
return http.build();
}
- Form login: For browser-based UI access
- HTTP Basic: For API clients, scripts, monitoring tools
Troubleshooting
Issue: Login page not loading
Cause: Assets blocked by security configuration.
Solution: Permit /assets/**:
.requestMatchers(PathPatternRequestMatcher.withDefaults()
.matcher(adminServer.path("/assets/**")))
.permitAll()
Issue: Infinite redirect loop
Cause: Login page requires authentication.
Solution: Permit /login:
.requestMatchers(PathPatternRequestMatcher.withDefaults()
.matcher(adminServer.path("/login")))
.permitAll()
Issue: Clients cannot register
Cause: CSRF protection blocking /instances endpoint.
Solution: Exempt client registration endpoints:
.csrf(csrf -> csrf
.ignoringRequestMatchers(
PathPatternRequestMatcher.withDefaults()
.matcher(POST, adminServer.path("/instances")),
PathPatternRequestMatcher.withDefaults()
.matcher(DELETE, adminServer.path("/instances/*"))
)
)
Issue: Remember-me not working
Cause: No UserDetailsService configured.
Solution: Add UserDetailsService bean:
@Bean
public InMemoryUserDetailsManager userDetailsService(PasswordEncoder encoder) {
// ...
}
Issue: 401 on API requests
Cause: API client not providing credentials.
Solution: Use HTTP Basic authentication:
curl -u admin:password http://localhost:8080/instances
Complete Example
package com.example.admin;
import java.util.UUID;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.Customizer;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.provisioning.InMemoryUserDetailsManager;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.SavedRequestAwareAuthenticationSuccessHandler;
import org.springframework.security.web.authentication.www.BasicAuthenticationFilter;
import org.springframework.security.web.csrf.CookieCsrfTokenRepository;
import org.springframework.security.web.csrf.CsrfTokenRequestAttributeHandler;
import org.springframework.security.web.servlet.util.matcher.PathPatternRequestMatcher;
import de.codecentric.boot.admin.server.config.AdminServerProperties;
import de.codecentric.boot.admin.server.config.EnableAdminServer;
import static org.springframework.http.HttpMethod.DELETE;
import static org.springframework.http.HttpMethod.POST;
@EnableAdminServer
@Configuration
public class AdminServerConfig {
private final AdminServerProperties adminServer;
public AdminServerConfig(AdminServerProperties adminServer) {
this.adminServer = adminServer;
}
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
SavedRequestAwareAuthenticationSuccessHandler successHandler =
new SavedRequestAwareAuthenticationSuccessHandler();
successHandler.setTargetUrlParameter("redirectTo");
successHandler.setDefaultTargetUrl(adminServer.path("/"));
http
.authorizeHttpRequests(auth -> auth
.requestMatchers(PathPatternRequestMatcher.withDefaults()
.matcher(adminServer.path("/assets/**")))
.permitAll()
.requestMatchers(PathPatternRequestMatcher.withDefaults()
.matcher(adminServer.path("/login")))
.permitAll()
.requestMatchers(PathPatternRequestMatcher.withDefaults()
.matcher(adminServer.path("/actuator/info")))
.permitAll()
.requestMatchers(PathPatternRequestMatcher.withDefaults()
.matcher(adminServer.path("/actuator/health")))
.permitAll()
.anyRequest().authenticated()
)
.formLogin(formLogin -> formLogin
.loginPage(adminServer.path("/login"))
.successHandler(successHandler)
)
.logout(logout -> logout
.logoutUrl(adminServer.path("/logout"))
)
.httpBasic(Customizer.withDefaults());
http.addFilterAfter(new CustomCsrfFilter(), BasicAuthenticationFilter.class)
.csrf(csrf -> csrf
.csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse())
.csrfTokenRequestHandler(new CsrfTokenRequestAttributeHandler())
.ignoringRequestMatchers(
PathPatternRequestMatcher.withDefaults()
.matcher(POST, adminServer.path("/instances")),
PathPatternRequestMatcher.withDefaults()
.matcher(DELETE, adminServer.path("/instances/*")),
PathPatternRequestMatcher.withDefaults()
.matcher(adminServer.path("/actuator/**"))
)
);
http.rememberMe(rememberMe -> rememberMe
.key(UUID.randomUUID().toString())
.tokenValiditySeconds(1209600)
);
return http.build();
}
@Bean
public InMemoryUserDetailsManager userDetailsService(PasswordEncoder passwordEncoder) {
UserDetails admin = User.builder()
.username("admin")
.password(passwordEncoder.encode(System.getenv("ADMIN_PASSWORD")))
.roles("ADMIN")
.build();
UserDetails viewer = User.builder()
.username("viewer")
.password(passwordEncoder.encode(System.getenv("VIEWER_PASSWORD")))
.roles("USER")
.build();
return new InMemoryUserDetailsManager(admin, viewer);
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
}
application.yml:
spring:
application:
name: spring-boot-admin-server
boot:
admin:
context-path: /admin
ui:
title: "Production Monitor"
server:
port: 8443
ssl:
enabled: true
key-store: classpath:keystore.p12
key-store-password: ${KEYSTORE_PASSWORD}
key-store-type: PKCS12
See Also
- Actuator Security - Secure client actuator endpoints
- CSRF Protection - Detailed CSRF configuration