Skip to main content

Command Palette

Search for a command to run...

Put a Login on Swagger and Actuator (Before Someone Else Does)

Both ship wide open by default. The layered way to lock them down in Spring Boot — expose less, authenticate, role-gate, isolate.

Updated
6 min read
Put a Login on Swagger and Actuator (Before Someone Else Does)
K
I am a Lead Full Stack Engineer with 6.5+ years of experience building scalable cloud-native platforms, distributed systems, and production-grade applications across telecom, fintech, govtech, and edtech domains. My core strength is backend engineering with Java, Spring Boot, microservices, and AWS, but I work across the entire delivery pipeline — from schema design and APIs to frontend interfaces and deployment systems. I describe my engineering style with one line: “I ship end-to-end. Schema to surface. No handoffs.” I believe strong engineering comes from ownership, not isolated specialization. The same engineer who designs the service should understand the UI consuming it, the deployment pipeline running it, and the metrics validating it in production. That mindset has shaped how I build systems, mentor teams, and deliver software. Over the years, I have worked on carrier-scale enterprise platforms, CRM modernization systems, loan-processing applications, real-time tutoring infrastructure, and department-scale governance portals. Across every domain, the engineering discipline remains the same: understand the problem deeply, design clear system boundaries, instrument what matters, and deliver measurable outcomes. My backend stack primarily revolves around Java, Spring Boot, Spring Cloud, distributed microservices, REST APIs, authentication systems, caching, resiliency patterns, and performance optimization. I have also built extensively using Node.js and NestJS for modern service architectures. On the frontend side, I work with React, Angular, TypeScript, and React Native to deliver responsive and scalable user experiences. I have hands-on experience with cloud-native infrastructure and DevOps workflows using AWS services like EC2, Lambda, S3, ECR, RDS, CloudWatch, CodeBuild, and CodePipeline, along with Docker, Jenkins, SonarQube, Grafana, ELK Stack, and CI/CD automation. I care deeply about observability, operational visibility, and systems that remain maintainable under scale. One thing that defines my approach is that every system should move a metric. I focus on engineering outcomes — improving performance, reducing operational friction, increasing delivery speed, simplifying developer workflows, or creating better user experiences. If a feature does not create measurable lift, it is incomplete. I am also deeply interested in modern AI-assisted engineering workflows. I actively use tools like GitHub Copilot, Claude, Gemini, Cursor, and agentic development systems to accelerate development, improve productivity, and rethink how software teams build products at scale. Beyond coding, I enjoy mentoring engineers, improving engineering standards, reviewing architectures, and building systems that other developers can scale confidently. I value clarity over complexity, practical execution over theoretical perfection, and shipping over endless planning. Today, my focus areas include distributed systems, platform engineering, cloud-native architecture, AI-powered developer tooling, scalable backend infrastructure, and modern full-stack application design. Backend-deep. Full-stack by delivery. Schema to surface. Service to screen. No handoff costs.

Two endpoints ship with your Spring Boot app that you never wrote and probably stopped thinking about months ago. One hands any visitor a complete, machine-readable map of your entire API. The other will, on request, send them a copy of your application's memory — tokens, passwords, connection strings and all. They're called Swagger and Actuator, you almost certainly turned them on for a good reason in development, and the unpleasant part is that "development" and "production" share the same config more often than anyone admits.

Neither is a bug. Both are features you opted into. The mistake is leaving them standing open to the internet because they were open on your laptop and nothing ever yelled at you to close them.

Let's look at what's actually behind each door, then lock them properly — not with one flag, but in layers.

What you're actually exposing

Swagger UI / api-docs hands out your full API surface and schemas; Actuator exposes env, beans, heapdump, loggers and shutdown — config, secrets, and control.

Swagger / OpenAPI. springdoc serves /swagger-ui/index.html for humans and /v3/api-docs as raw JSON for machines. That JSON is the whole thing: every route, every method, every parameter, every request and response schema. For a legitimate consumer it's documentation. For an attacker it's reconnaissance they didn't have to do — your undocumented internal endpoints, your admin routes, your "we'll secure that later" controller, all neatly listed.

Actuator. This one's worse, because some of its endpoints don't just describe the app, they operate it. A quick tour of the sharp ones:

  • /actuator/env and /configprops — your configuration, including a lot of things that were never meant to leave the server.
  • /actuator/beans and /mappings — your whole bean graph and URL map, i.e. the internal architecture.
  • /actuator/heapdump — downloads a full heap dump. Whatever secrets were sitting in memory are now a file on the attacker's disk.
  • /actuator/loggers — lets you change log levels at runtime via POST. Crank a package to DEBUG and watch the secrets scroll.
  • /actuator/shutdown — exactly what it says. Disabled by default, but people enable it and forget.

Good news first: by default Boot only exposes health over HTTP, and env sanitizes obvious keys. The danger is the line that's in half the tutorials on the internet:

management:
  endpoints:
    web:
      exposure:
        include: "*"   # <- exposes every actuator endpoint. great in a demo, a gift in prod.

Ship that, leave it unauthenticated, and every endpoint above is one curl away.

Lock it in layers, not with one switch

The instinct is to find the single "secure it" setting. There isn't one, and that's fine — defense in depth means each layer assumes the one above it failed.

Four layers: expose only what you need, authenticate, authorize with ROLE_ADMIN, and isolate onto a separate management port.

Layer 1 — expose less

The cheapest fix is the one you skip: don't publish what you don't need. Pin the actuator exposure list to the endpoints you actually use, and never * in production.

management:
  endpoints:
    web:
      exposure:
        include: health,info     # not "*"
  endpoint:
    health:
      show-details: when-authorized   # full health only after login; anonymous sees just UP/DOWN

And Swagger genuinely does not need to exist in production for most apps. Turn it off per profile:

# application-prod.yml
springdoc:
  api-docs:
    enabled: false
  swagger-ui:
    enabled: false

If you do keep it in prod — internal tools, a partner API — then it has to get the same auth as everything else below. An endpoint that doesn't exist can't be attacked; that's always the strongest version of "secured."

Layers 2 and 3 — authenticate, then authorize

Add spring-boot-starter-security, and the framework gives you a login out of the box. But "logged in" is not the bar for these endpoints — admin is. The distinction matters: a regular user account that gets phished shouldn't come with a heap dump button.

Spring Security's EndpointRequest matchers know about actuator, so you don't hand-write the paths:

@Bean
SecurityFilterChain security(HttpSecurity http) throws Exception {
    http
        .authorizeHttpRequests(auth -> auth
            // health + info stay open for load balancers and uptime checks
            .requestMatchers(EndpointRequest.to(HealthEndpoint.class, InfoEndpoint.class)).permitAll()
            // every OTHER actuator endpoint: admins only
            .requestMatchers(EndpointRequest.toAnyEndpoint()).hasRole("ADMIN")
            // Swagger UI + the raw OpenAPI doc: admins only
            .requestMatchers("/swagger-ui/**", "/swagger-ui.html", "/v3/api-docs/**").hasRole("ADMIN")
            // your real application rules
            .anyRequest().authenticated()
        )
        .httpBasic(Customizer.withDefaults())   // or formLogin for a browser UI
        // actuator POSTs (e.g. /loggers) are not browser forms — exempt them from CSRF
        .csrf(csrf -> csrf.ignoringRequestMatchers(EndpointRequest.toAnyEndpoint()));
    return http.build();
}
The filter chain: health permitAll, toAnyEndpoint hasRole ADMIN, swagger paths hasRole ADMIN, anyRequest your app rules — most-specific matcher first.

One thing the diagram is quietly insisting on: order matters. Spring Security takes the first matcher that matches, so the narrow health rule has to come before the broad toAnyEndpoint() — flip them and toAnyEndpoint() swallows health and your load balancer starts getting 401s. Most-specific first, every time.

And the credentials behind ROLE_ADMIN have to be real. Not the random password Boot prints to the console on startup, not user / password in a properties file. A proper user — from your database or directory — with a BCrypt-hashed secret:

@Bean
PasswordEncoder passwordEncoder() {
    return new BCryptPasswordEncoder();
}

Layer 4 — isolate the port

The last layer is network, not code. Move actuator off your public application port entirely:

management:
  server:
    port: 9001          # actuator lives here...
    address: 127.0.0.1  # ...and only answers on loopback / your internal net

Now even a misconfigured rule upstream doesn't help an outside attacker, because the port they can reach doesn't serve actuator at all. Your monitoring, which lives on the same box or inside the same network, still gets in. This pairs naturally with a firewall or security group that simply never routes 9001 to the outside world.

The short version

Swagger and Actuator aren't dangerous because they're insecure. They're dangerous because they're useful, which is exactly why you turned them on and then stopped seeing them. Treat them like any other privileged surface:

  • Expose less — pin the actuator list, and turn Swagger off in prod unless you have a reason not to.
  • Authenticate — put Spring Security in front of both.
  • AuthorizehasRole("ADMIN"), not merely "logged in," with real BCrypt-backed credentials.
  • Isolate — separate management port, bound to the internal network.

No single one of these is the answer. Stacked, they mean that the day one layer is misconfigured — and someday one will be — the other three are still standing between a stranger and a copy of your app's memory.