spring-boot-admin-docs/src/site/docs/05-security/20-actuator-security.md
Secure your client application's actuator endpoints and configure Spring Boot Admin Server to access them.
When client applications expose actuator endpoints, they should be secured. The Admin Server needs credentials to access these secured endpoints.
graph TD
A["Spring Boot Admin Server
Needs credentials to access
secured actuator endpoints"] -->|HTTP Basic Auth
user.name + user.password| B["Client Application
Secured actuator endpoints:
/actuator/health
/actuator/metrics
/actuator/env"]
Add Spring Security to your client application:
Maven:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
Gradle:
implementation 'org.springframework.boot:spring-boot-starter-security'
application.yml:
spring:
security:
user:
name: actuator
password: ${ACTUATOR_PASSWORD}
management:
endpoints:
web:
exposure:
include: "*"
Pass credentials via metadata:
spring:
boot:
admin:
client:
url: http://admin-server:8080
instance:
metadata:
user.name: actuator
user.password: ${ACTUATOR_PASSWORD}
application.yml:
spring:
boot:
admin:
instance-auth:
enabled: true
The Admin Server will automatically use credentials from instance metadata to access actuator endpoints.
Simplest approach using default Spring Security user:
spring:
application:
name: my-service
security:
user:
name: actuator
password: ${ACTUATOR_PASSWORD}
boot:
admin:
client:
url: http://admin-server:8080
instance:
metadata:
user.name: ${spring.security.user.name}
user.password: ${spring.security.user.password}
management:
endpoints:
web:
exposure:
include: health,info,metrics,env,loggers
For more control, create a custom SecurityFilterChain:
package com.example.myservice;
import org.springframework.boot.actuate.autoconfigure.security.servlet.EndpointRequest;
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;
@Configuration
public class ActuatorSecurityConfig {
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests(auth -> auth
// Permit health endpoint for load balancers
.requestMatchers(EndpointRequest.to("health")).permitAll()
// Secure all other actuator endpoints
.requestMatchers(EndpointRequest.toAnyEndpoint()).authenticated()
// Allow application endpoints
.anyRequest().permitAll()
)
// Use HTTP Basic for actuator
.httpBasic(Customizer.withDefaults())
// Disable CSRF for stateless API
.csrf(csrf -> csrf.disable());
return http.build();
}
@Bean
public InMemoryUserDetailsManager userDetailsService(PasswordEncoder encoder) {
UserDetails actuator = User.builder()
.username("actuator")
.password(encoder.encode(System.getenv("ACTUATOR_PASSWORD")))
.roles("ACTUATOR")
.build();
return new InMemoryUserDetailsManager(actuator);
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
}
Separate security for actuator and application:
@Configuration
@Order(1) // Higher precedence
public class ActuatorSecurityConfig {
@Bean
@Order(1)
public SecurityFilterChain actuatorFilterChain(HttpSecurity http) throws Exception {
http
.securityMatcher(EndpointRequest.toAnyEndpoint())
.authorizeHttpRequests(auth -> auth
.requestMatchers(EndpointRequest.to("health")).permitAll()
.anyRequest().hasRole("ACTUATOR")
)
.httpBasic(Customizer.withDefaults())
.csrf(csrf -> csrf.disable());
return http.build();
}
@Bean
public InMemoryUserDetailsManager actuatorUserDetailsService(PasswordEncoder encoder) {
UserDetails actuator = User.builder()
.username("actuator")
.password(encoder.encode(System.getenv("ACTUATOR_PASSWORD")))
.roles("ACTUATOR")
.build();
return new InMemoryUserDetailsManager(actuator);
}
}
@Configuration
@Order(2) // Lower precedence
public class ApplicationSecurityConfig {
@Bean
@Order(2)
public SecurityFilterChain applicationFilterChain(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests(auth -> auth
.requestMatchers("/api/public/**").permitAll()
.anyRequest().authenticated()
)
.oauth2Login(Customizer.withDefaults());
return http.build();
}
}
Run actuator on a different port for isolation:
management:
server:
port: 8081 # Separate management port
endpoints:
web:
exposure:
include: "*"
spring:
security:
user:
name: actuator
password: ${ACTUATOR_PASSWORD}
boot:
admin:
client:
instance:
# Admin Server will auto-detect management port
# Or specify explicitly:
management-base-url: http://localhost:8081
metadata:
user.name: actuator
user.password: ${ACTUATOR_PASSWORD}
Security configuration:
@Configuration
public class SecurityConfig {
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests(auth -> auth
// No actuator endpoints on main port
.anyRequest().permitAll()
)
.csrf(csrf -> csrf.disable());
return http.build();
}
}
On port 8081, actuator endpoints are secured with Spring Security's default security.
spring:
boot:
admin:
instance-auth:
enabled: true
Admin Server will:
user.name and user.passwordSet default credentials for all instances:
spring:
boot:
admin:
instance-auth:
enabled: true
default-user-name: actuator
default-password: ${DEFAULT_ACTUATOR_PASSWORD}
Instances can override via metadata.
Configure different credentials for each service:
spring:
boot:
admin:
instance-auth:
enabled: true
service-map:
# Service name from spring.application.name
my-service:
user-name: my-service-actuator
user-password: ${MY_SERVICE_PASSWORD}
another-service:
user-name: another-actuator
user-password: ${ANOTHER_SERVICE_PASSWORD}
# Fallback for services not in service-map
default-user-name: default-actuator
default-password: ${DEFAULT_PASSWORD}
Client (my-service):
spring:
application:
name: my-service
security:
user:
name: my-service-actuator
password: ${MY_SERVICE_PASSWORD}
Client passes credentials in metadata:
spring:
boot:
admin:
client:
instance:
metadata:
user.name: actuator
user.password: ${ACTUATOR_PASSWORD}
Server uses metadata automatically:
spring:
boot:
admin:
instance-auth:
enabled: true
Pros:
Cons:
Server has all credentials:
spring:
boot:
admin:
instance-auth:
enabled: true
service-map:
service-a:
user-name: service-a-user
user-password: ${SERVICE_A_PASSWORD}
Client just secures actuator:
spring:
security:
user:
name: service-a-user
password: ${SERVICE_A_PASSWORD}
Pros:
Cons:
All clients use same credentials:
Server:
spring:
boot:
admin:
instance-auth:
enabled: true
default-user-name: actuator
default-password: ${ACTUATOR_PASSWORD}
All Clients:
spring:
security:
user:
name: actuator
password: ${ACTUATOR_PASSWORD}
Pros:
Cons:
Only expose necessary endpoints:
management:
endpoints:
web:
exposure:
include: health,info,metrics,env,loggers
Or exclude specific endpoints:
management:
endpoints:
web:
exposure:
include: "*"
exclude: heapdump,threaddump
Health endpoint details:
management:
endpoint:
health:
show-details: when-authorized
roles: ACTUATOR
By default, credentials in metadata are sanitized:
spring:
boot:
admin:
metadata-keys-to-sanitize:
- ".*password$"
- ".*secret$"
- ".*key$"
- ".*token$"
- ".*credentials.*"
Metadata user.password will appear as ****** in responses, but the server still uses it internally.
When using service discovery (Eureka, Consul, etc.), credentials can be set via metadata:
Eureka:
eureka:
instance:
metadata-map:
user.name: actuator
user.password: ${ACTUATOR_PASSWORD}
Consul:
spring:
cloud:
consul:
discovery:
metadata:
user.name: actuator
user.password: ${ACTUATOR_PASSWORD}
Kubernetes ConfigMap:
apiVersion: v1
kind: ConfigMap
metadata:
name: my-service-config
data:
application.yml: |
spring:
boot:
admin:
client:
instance:
metadata:
user.name: actuator
user.password: ${ACTUATOR_PASSWORD}
Use HTTPS for actuator endpoints:
management:
server:
port: 8443
ssl:
enabled: true
key-store: classpath:actuator-keystore.p12
key-store-password: ${KEYSTORE_PASSWORD}
key-store-type: PKCS12
spring:
boot:
admin:
client:
instance:
management-base-url: https://localhost:8443
metadata:
user.name: actuator
user.password: ${ACTUATOR_PASSWORD}
Generate keystore:
keytool -genkeypair -alias actuator \
-keyalg RSA -keysize 2048 \
-storetype PKCS12 \
-keystore actuator-keystore.p12 \
-validity 3650 \
-storepass changeit
Client:
management:
endpoints:
web:
exposure:
include: "*"
spring:
boot:
admin:
client:
url: http://localhost:8080
No Spring Security dependency, all endpoints open.
Client:
spring:
application:
name: payment-service
security:
user:
name: ${ACTUATOR_USER}
password: ${ACTUATOR_PASSWORD}
boot:
admin:
client:
url: https://admin.company.com
username: ${ADMIN_CLIENT_USER}
password: ${ADMIN_CLIENT_PASSWORD}
instance:
service-base-url: https://payment.company.com
management-base-url: https://payment.company.com:8443
metadata:
user.name: ${ACTUATOR_USER}
user.password: ${ACTUATOR_PASSWORD}
tags:
environment: production
management:
server:
port: 8443
ssl:
enabled: true
key-store: classpath:keystore.p12
key-store-password: ${KEYSTORE_PASSWORD}
endpoints:
web:
exposure:
include: health,info,metrics,env,loggers
endpoint:
health:
show-details: when-authorized
Server:
spring:
boot:
admin:
instance-auth:
enabled: true
# Uses credentials from instance metadata
Secret:
apiVersion: v1
kind: Secret
metadata:
name: actuator-credentials
type: Opaque
stringData:
username: actuator
password: secure-password-123
Deployment:
apiVersion: apps/v1
kind: Deployment
metadata:
name: my-service
spec:
template:
spec:
containers:
- name: my-service
image: my-service:latest
env:
- name: ACTUATOR_USER
valueFrom:
secretKeyRef:
name: actuator-credentials
key: username
- name: ACTUATOR_PASSWORD
valueFrom:
secretKeyRef:
name: actuator-credentials
key: password
ports:
- containerPort: 8080
- containerPort: 8081 # Actuator
application.yml (in ConfigMap):
spring:
security:
user:
name: ${ACTUATOR_USER}
password: ${ACTUATOR_PASSWORD}
boot:
admin:
client:
instance:
metadata:
user.name: ${ACTUATOR_USER}
user.password: ${ACTUATOR_PASSWORD}
management:
server:
port: 8081
application.yml (common):
spring:
boot:
admin:
client:
instance:
metadata:
user.name: ${ACTUATOR_USER:actuator}
user.password: ${ACTUATOR_PASSWORD}
management:
endpoints:
web:
exposure:
include: "*"
application-dev.yml:
spring:
security:
user:
name: actuator
password: dev-password
boot:
admin:
client:
url: http://localhost:8080
application-prod.yml:
spring:
security:
user:
name: ${ACTUATOR_USER}
password: ${ACTUATOR_PASSWORD}
boot:
admin:
client:
url: https://admin.company.com
username: ${ADMIN_CLIENT_USER}
password: ${ADMIN_CLIENT_PASSWORD}
management:
endpoints:
web:
exposure:
include: health,info,metrics,env,loggers
endpoint:
health:
show-details: when-authorized
Cause: Admin Server doesn't have valid credentials.
Check:
Instance metadata contains credentials:
curl http://admin-server:8080/instances/{id} | jq '.metadata'
Credentials match actuator configuration:
curl -u actuator:password http://client:8080/actuator/health
Solution: Add credentials to metadata:
spring:
boot:
admin:
client:
instance:
metadata:
user.name: actuator
user.password: ${ACTUATOR_PASSWORD}
Cause: Cannot access health endpoint.
Check:
curl -u actuator:password http://client:8080/actuator/health
Solution: Verify:
Cause: Different security rules for different endpoints.
Solution: Ensure all actuator endpoints have same security:
http.authorizeHttpRequests(auth -> auth
.requestMatchers(EndpointRequest.toAnyEndpoint()).authenticated()
)
Expected: Credentials should be sanitized as ******.
Check sanitization patterns:
spring:
boot:
admin:
metadata-keys-to-sanitize:
- ".*password$"
This is working correctly if API responses show ****** for user.password, even though the server uses the real value
internally.