Back to Spring Boot Admin

Server Authentication

spring-boot-admin-docs/src/site/docs/05-security/10-server-authentication.md

4.0.424.9 KB
Original Source

Server Authentication

Secure your Spring Boot Admin Server using Spring Security to protect the UI and API endpoints.

Overview

A secured Admin Server requires:

  1. Spring Security dependency
  2. SecurityFilterChain configuration
  3. User credentials (in-memory, database, LDAP, OAuth2, etc.)
  4. CSRF protection with exemptions for client registration

Quick Start

1. Add Spring Security Dependency

Maven:

xml
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-security</artifactId>
</dependency>

Gradle:

gradle
implementation 'org.springframework.boot:spring-boot-starter-security'

2. Basic Configuration

Minimal security with default Spring Boot user:

yaml
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:

java
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:

java
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:

yaml
spring:
  boot:
    admin:
      context-path: /admin

Adjust security matchers:

java
.requestMatchers(PathPatternRequestMatcher.withDefaults()
    .matcher(adminServer.path("/assets/**")))
.permitAll()

The adminServer.path() method handles context path automatically.

Remember-Me

Enable persistent sessions:

java
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:

java
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:

java
@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:

java
@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:

sql
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:

java
@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:

yaml
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:

xml
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-oauth2-client</artifactId>
</dependency>

Configuration:

yaml
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:

java
@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:

java
@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:

java
@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:

yaml
server:
  port: 8080

HTTPS Configuration

Enable HTTPS for secure communication:

yaml
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:

bash
keytool -genkeypair -alias spring-boot-admin \
  -keyalg RSA -keysize 2048 \
  -storetype PKCS12 \
  -keystore keystore.p12 \
  -validity 3650 \
  -storepass changeit

Update Admin Client configuration:

yaml
spring:
  boot:
    admin:
      client:
        url: https://admin-server:8443

Reverse Proxy Setup

Behind Nginx

Nginx Configuration:

nginx
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:

yaml
server:
  forward-headers-strategy: native

spring:
  boot:
    admin:
      ui:
        public-url: https://admin.company.com

Behind Apache

Apache Configuration:

apache
<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:

java
@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:

java
@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/**:

java
.requestMatchers(PathPatternRequestMatcher.withDefaults()
    .matcher(adminServer.path("/assets/**")))
.permitAll()

Issue: Infinite redirect loop

Cause: Login page requires authentication.

Solution: Permit /login:

java
.requestMatchers(PathPatternRequestMatcher.withDefaults()
    .matcher(adminServer.path("/login")))
.permitAll()

Issue: Clients cannot register

Cause: CSRF protection blocking /instances endpoint.

Solution: Exempt client registration endpoints:

java
.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:

java
@Bean
public InMemoryUserDetailsManager userDetailsService(PasswordEncoder encoder) {
    // ...
}

Issue: 401 on API requests

Cause: API client not providing credentials.

Solution: Use HTTP Basic authentication:

bash
curl -u admin:password http://localhost:8080/instances

Complete Example

java
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:

yaml
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