Configuration, Profiles & Actuator

Securing & Customizing Actuator

18 min Lesson 9 of 13

Securing & Customizing Actuator

Spring Boot Actuator is tremendously useful in production — but by default it exposes a lot of internal detail about your application: environment variables, configuration properties, bean graph, thread dumps, and more. Leaving these endpoints wide open is a serious security risk. In this lesson you will learn exactly which endpoints to expose, how to protect them behind Spring Security, and how to tailor the output so it reveals only what each consumer legitimately needs.

The Default Exposure Surface

Actuator endpoints are available in two transports: HTTP and JMX. Their default exposure policies differ deliberately:

  • HTTP: only health and info are exposed by default.
  • JMX: all endpoints are exposed by default (JMX is usually firewall-restricted anyway).

The first rule of Actuator security is: expose the minimum you actively need over HTTP. Configure this in application.properties:

# Expose only what you need — start restrictive, open up deliberately management.endpoints.web.exposure.include=health,info,metrics,loggers management.endpoints.web.exposure.exclude=env,beans,heapdump,threaddump,mappings
Never expose env, heapdump, or shutdown to the public internet. The env endpoint lists every resolved property — including secrets that leak through environment variables. A heap dump contains the full JVM heap, which can be parsed offline to extract passwords, tokens, and session data. The shutdown endpoint terminates the process.

Moving Actuator to a Separate Port

A clean architectural decision is to serve Actuator on a different port reachable only inside your private network or VPN. Your load balancer never forwards external traffic to that port:

management.server.port=8081 management.server.address=127.0.0.1 # bind only to localhost

With this configuration all Actuator traffic is served on port 8081 bound to loopback. External clients cannot reach it regardless of Spring Security rules. This is your network-layer defence — it works even if your security configuration has a bug.

Layer your defences. Use a private management port AND Spring Security together. The port restriction handles infrastructure-level isolation; Spring Security handles authentication for internal consumers such as monitoring agents that run on the same machine.

Protecting Actuator with Spring Security

When Actuator shares the same port as the main application you must protect its endpoints with Spring Security. Add the starter if it is not already present:

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

Then write a SecurityFilterChain bean that applies different rules to Actuator paths versus regular application paths. In Spring Boot 3 / Spring Security 6 the lambda DSL is the idiomatic approach:

import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; import org.springframework.security.core.userdetails.User; import org.springframework.security.core.userdetails.UserDetailsService; import org.springframework.security.provisioning.InMemoryUserDetailsManager; import org.springframework.security.web.SecurityFilterChain; import static org.springframework.security.config.Customizer.withDefaults; @Configuration @EnableWebSecurity public class SecurityConfig { @Bean public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { http .authorizeHttpRequests(auth -> auth // Public health check — used by load-balancer probes .requestMatchers("/actuator/health").permitAll() // Everything else under /actuator requires the ACTUATOR_ADMIN role .requestMatchers("/actuator/**").hasRole("ACTUATOR_ADMIN") // All other application endpoints require authentication .anyRequest().authenticated() ) .httpBasic(withDefaults()); return http.build(); } // In production: load users from a database or LDAP, never in-memory @Bean public UserDetailsService userDetailsService() { var admin = User.withDefaultPasswordEncoder() .username("ops") .password("changeme") .roles("ACTUATOR_ADMIN") .build(); return new InMemoryUserDetailsManager(admin); } }

Key points in this configuration:

  • /actuator/health is permitAll() so load-balancer health probes work without credentials.
  • All other /actuator/** paths require the ACTUATOR_ADMIN role — a dedicated role keeps Actuator access separate from application-level admin privileges.
  • Order matters in requestMatchers: the more specific /actuator/health rule must come before the broader /actuator/** wildcard.

Customizing the Health Endpoint

The health endpoint can show different levels of detail depending on whether the caller is authenticated. Configure this in application.properties:

# NEVER — always show only UP/DOWN (no details at all) # WHEN_AUTHORIZED — show full details only to authenticated users # ALWAYS — show full details to everyone (do not use in production) management.endpoint.health.show-details=when_authorized management.endpoint.health.show-components=when_authorized

An unauthenticated load-balancer probe therefore sees only:

{ "status": "UP" }

While an authenticated monitoring agent sees the full breakdown including database, disk space, and custom health indicators — without leaking that detail to the public.

Filtering Sensitive Data from the env Endpoint

If you must expose env on an internal network, Spring Boot automatically sanitizes property values whose keys match a built-in list of patterns such as password, secret, key, token, and credentials. The value is replaced with ****** in the response. You can extend this list with your own patterns:

management.endpoint.env.additional-keys-to-sanitize=api-key,db-pass,stripe.*
Sanitization is not a substitute for access control. A determined attacker who can reach the env endpoint can try to reconstruct a masked value via trial and error if they already know the format. Treat sanitization as a last line of defence — your first line is not exposing env at all on a public network.

Writing a Custom Actuator Endpoint

Sometimes the built-in endpoints do not cover what you need. You can write your own endpoint using @Endpoint (both HTTP and JMX) or @WebEndpoint (HTTP only). Annotate operations with @ReadOperation, @WriteOperation, or @DeleteOperation:

import org.springframework.boot.actuate.endpoint.annotation.Endpoint; import org.springframework.boot.actuate.endpoint.annotation.ReadOperation; import org.springframework.boot.actuate.endpoint.annotation.WriteOperation; import org.springframework.stereotype.Component; import java.time.Instant; import java.util.Map; import java.util.concurrent.atomic.AtomicReference; @Component @Endpoint(id = "releaseinfo") // exposed at /actuator/releaseinfo public class ReleaseInfoEndpoint { private final AtomicReference<String> notes = new AtomicReference<>("none"); @ReadOperation public Map<String, Object> releaseInfo() { return Map.of( "version", System.getenv().getOrDefault("APP_VERSION", "unknown"), "deployedAt", Instant.now().toString(), "notes", notes.get() ); } @WriteOperation public void updateNotes(String text) { notes.set(text); } }

The endpoint ID (releaseinfo) becomes the URL segment. Spring Boot automatically registers it and applies the same security rules you defined for /actuator/**. No extra wiring is needed.

Renaming the Actuator Base Path

Changing the Actuator base path from /actuator to something less predictable adds a small but real layer of obscurity — attackers running automated scans for /actuator/env will not find it:

management.endpoints.web.base-path=/internal/ops

The health endpoint is now at /internal/ops/health. Update any monitoring tool or load-balancer probe configuration to match.

Summary

Securing Actuator is not a single setting — it is a deliberate, layered strategy:

  1. Expose minimally: use exposure.include to whitelist only endpoints you need.
  2. Isolate at the network level: bind management.server.port to a private address when possible.
  3. Authenticate with Spring Security: keep /actuator/health public for probes; protect everything else behind a dedicated role.
  4. Control detail visibility: set show-details=when_authorized so unauthenticated callers see only a status summary.
  5. Sanitize sensitive keys: extend the sanitization list if you expose env internally.
  6. Rename the base path: adds obscurity on top of authentication.

With these controls in place, Actuator becomes a powerful, safe operational window into your running service.