본문 바로가기

Actuator 보안

Actuator 접근을 네트워크·관리 포트·인증·권한·응답 마스킹으로 제한하고 메트릭 수집 경로의 위협을 검증합니다.

Actuator는 애플리케이션의 내부 구조를 많이 알고 있고, 일부 엔드포인트는 애플리케이션의 상태를 바꿉니다. 공개 API와 같은 인그레스에 모두 노출하면 빈 그래프, 구성 키, 힙, 요청 경로, 로그 수준 제어가 그대로 공격 표면이 됩니다.

그래서 Actuator 보안은 기반 경로를 바꾸는 설정 한 줄이 아니라, 요청이 닿을 수 있는 경로, 인증, 권한, 응답 데이터의 최소화, 변경 감사를 겹겹이 쌓은 구조여야 합니다. 이 글에서는 엔드포인트별 위험을 분류하고, 네트워크와 관리 포트, 보안 필터 체인, 응답 정제, 메트릭 수집 경로를 차례로 다룹니다.

SECURITY · CHAIN SELECTION AND DATA

관리 요청은 선택된 보안 체인과 데이터 정책을 따른다

비공개 관리 경로, 요청 인증/인가, 최소화된 출력은 서로 다른 책임입니다. 관리 체인과 일반 앱 체인은 우선순위와 매칭 범위를 함께 검증합니다.

요청의 매칭 범위에 따른 체인 선택높은 우선순위의 관리 체인이 EndpointRequest와 일치한 요청을 처리하며, 불일치 앱 요청에는 별도 일반 체인이 필요하다. 두 체인을 모든 요청이 연속 통과하지 않는다. 원 코드에는 앱 체인과 세분 역할·감사가 아직 없다. 같은 4노드3관계를 모바일에도 적용한다. 네트워크 도달과 출력 최소화는 보조 패널에 유지한다.도달한 요청관리 매처 일치?관리 보안 체인예제 Order 1일반 앱 체인별도 구성 필요예아니요
요청의 매칭 범위에 따른 체인 선택높은 우선순위의 관리 체인이 EndpointRequest와 일치한 요청을 처리하며, 불일치 앱 요청에는 별도 일반 체인이 필요하다. 두 체인을 모든 요청이 연속 통과하지 않는다. 원 코드에는 앱 체인과 세분 역할·감사가 아직 없다. 같은 4노드3관계를 모바일에도 적용한다. 네트워크 도달과 출력 최소화는 보조 패널에 유지한다.도달한 요청관리 매처일치?관리 보안 체인예제 Order 1일반 앱 체인별도 구성 필요예아니요

요청 · 어떤 체인이 처리하는가

  1. 실제 도달 경로

    비공개 포트·TLS·프록시·NetworkPolicy를 확인합니다. 같은 파드의 SSRF와 침해된 사이드카도 고려합니다.

  2. 관리 매처와 높은 우선순위

    EndpointRequest에 맞는 요청을 관리 SecurityFilterChain이 처리합니다. 작업별 읽기·쓰기·비상 권한을 설계합니다.

  3. 일반 앱 fallback

    관리 체인에 맞지 않는 앱 요청은 별도 체인이 처리해야 합니다. 두 체인을 모든 요청이 연속 통과하는 구조가 아닙니다.

출력 · 무엇을 전달하는가

  1. 상태와 상세를 분리

    health 공개 허용과 상세 공개 설정을 함께 시험합니다. env/configprops의 키·구조도 노출 정보입니다.

  2. 수집 전에 최소화

    허용 목록 DTO와 제한된 태그를 사용합니다. 민감 레이블은 대시보드에서 숨기기 전에 제거합니다.

  3. 주체와 큰 응답을 제한

    수집기 인증 정보를 대시보드 브라우저에 넘기지 않습니다. dump 크기·동시성·시간과 감사 정책을 둡니다.

사용자 체인 등록 시 Boot 기본 보안은 물러납니다. 본문 OPS 단일 역할 코드는 세분 권한·감사를 아직 구현하지 않습니다. CSRF는 브라우저/기계 호출 위협 모델에 따라 좁게 결정합니다.

그림의 위쪽은 도달한 요청이 어느 보안 체인에서 처리되는지를 나눈 것입니다. 관리 엔드포인트 매처와 일치한 요청은 우선순위가 높은 관리 보안 체인이 처리하고, 일치하지 않는 애플리케이션 요청은 따로 구성한 일반 체인이 처리합니다. 모든 요청이 두 체인을 차례로 통과하는 구조가 아니며, 예제 코드에는 아직 일반 체인과 세분된 역할, 감사가 없습니다. 아래 칸들은 요청이 실제로 닿는 경로와, 상태와 상세의 분리, 수집 전 데이터 최소화 같은 출력 쪽 정책을 정리합니다.


엔드포인트의 위험 목록

상태는 로드 밸런서가 익명으로 읽어야 할 수 있지만, 상태의 상세는 내부 운영자만 봐야 합니다. 메트릭 이름 목록은 위험이 비교적 낮아도, 태그 값이 테넌트나 경로를 드러낼 수 있습니다. env, configprops, beans, mappings는 구조 정보를, heapdump와 threaddump는 크고 민감한 메모리 내용을 담고, loggers와 shutdown은 상태를 바꾸는 권한을 갖습니다.

허용 목록에는 엔드포인트 ID뿐 아니라 작업 메서드와 응답 필드까지 포함합니다. 버전을 올렸을 때 새 엔드포인트가 생겨도 와일드카드 노출로 자동 공개되지 않게 하고, 탐색 결과의 차이를 릴리스 통과 기준으로 삼습니다.

src/main/java/board/security/EndpointRisk.java
package board.security;

public record EndpointRisk(
        String id,
        boolean mutating,
        boolean containsConfiguration,
        long maximumResponseBytes,
        String requiredRole
) {
    public EndpointRisk {
        if (id.isBlank() || maximumResponseBytes < 1 || requiredRole.isBlank()) {
            throw new IllegalArgumentException("invalid endpoint risk entry");
        }
        if (mutating && requiredRole.equals("PROBE")) {
            throw new IllegalArgumentException("probe role cannot mutate");
        }
    }
}

이 레코드는 엔드포인트마다 상태 변경 여부, 구성 정보 포함 여부, 최대 응답 크기, 필요한 역할을 기록하며, 익명 점검용 PROBE 역할에는 상태를 바꾸는 엔드포인트를 맡기지 못하게 막습니다. 허용 목록을 관리할 담당자와 검토 주기도 정해 둡니다. heapdump처럼 비상시에만 필요한 엔드포인트는 평소에 접근을 막아 두고, 장애 대응 승인을 받은 뒤 잠시 여는 절차가 더 안전합니다.


네트워크와 관리 포트

별도의 management.server.port를 비공개 서비스나 localhost에 바인딩하고, 공개 인그레스 경로에서 뺍니다. 보안 그룹, Kubernetes의 NetworkPolicy, 서비스 메시 정책으로 수집기와 운영자 네트워크만 허용합니다. 다만 같은 파드 안에서 일어난 SSRF나 침해된 사이드카는 이런 네트워크 차단을 넘을 수 있으므로, 네트워크만으로는 충분하지 않습니다.

관리용 TLS와 클라이언트 인증서를 별도의 신뢰 도메인으로 운영할 수도 있습니다. 리버스 프록시가 인증을 처리한다면, 위조할 수 있는 전달 헤더를 애플리케이션이 그대로 믿지 않도록 신뢰할 프록시의 범위를 제한합니다. 인증 없이 상태 점검이 필요하다면 상태 전용 경로 하나만 예외로 둡니다.


Actuator 보안 체인

애플리케이션 API의 보안과 관리 엔드포인트의 정책이 다르다면, 별도의 SecurityFilterChain을 만들고 Actuator 체인에 더 높은 우선순위를 줍니다. 경로 문자열을 손으로 조합하기보다 EndpointRequest 매처를 쓰면, 기반 경로가 바뀌거나 엔드포인트 매핑이 달라져도 따라갑니다. 이 구성은 Spring Security와 Boot의 EndpointRequest가 들어 있는 spring-boot-starter-security가 필요하므로, Actuator 엔드포인트의 build.gradle에 implementation 'org.springframework.boot:spring-boot-starter-security'를 더합니다.

src/main/java/board/security/ManagementSecurityConfiguration.java
package board.security;

import org.springframework.boot.health.actuate.endpoint.HealthEndpoint;
import org.springframework.boot.security.autoconfigure.actuate.web.servlet.EndpointRequest;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.annotation.Order;
import org.springframework.security.config.Customizer;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.web.SecurityFilterChain;

@Configuration(proxyBeanMethods = false)
public class ManagementSecurityConfiguration {
    @Bean
    @Order(1)
    SecurityFilterChain managementSecurity(HttpSecurity http) throws Exception {
        http.securityMatcher(EndpointRequest.toAnyEndpoint())
                .authorizeHttpRequests(authorize -> authorize
                        .requestMatchers(EndpointRequest.to(HealthEndpoint.class))
                        .permitAll()
                        .anyRequest().hasRole("OPS"))
                .httpBasic(Customizer.withDefaults());
        return http.build();
    }
}

이 체인은 모든 관리 엔드포인트를 대상으로 하며, 상태 엔드포인트만 모두에게 허용하고 나머지는 OPS 역할을 요구합니다. 실제 인증에는 오래 쓰는 기본 인증 정보보다 워크로드 신원, mTLS, 주기적으로 교체하는 비밀 정보를 선호합니다. 상태를 바꾸는 엔드포인트에서 CSRF 보호를 무조건 끄지 말고, 호출하는 쪽이 브라우저인지 기계인지에 따라 위협 모델로 정합니다.

상태 경로를 모두에게 허용했다면, 상세 공개 설정과 함께 테스트합니다. 위 코드는 관리 엔드포인트만 다룹니다. 사용자가 SecurityFilterChain을 등록하면 Boot의 기본 보안 구성이 물러나므로, 일반 애플리케이션 요청을 처리할 별도의 체인을 반드시 구성해야 합니다. 작업별 역할과 감사 기록은 아래에서 설계하고 검증할 대상이며, 이 OPS 단일 역할 예제에는 아직 구현되지 않았습니다.


응답 정제와 크기 제한

env와 configprops가 값을 가려도, 키 이름과 객체 구조만으로 아키텍처가 드러납니다. 토큰, 비밀번호, 인증 정보, 개인 키뿐 아니라 조직 고유의 비밀 정보 키도 정제(sanitizing) 목록에 추가합니다. 사용자 정의 엔드포인트는 허용 목록에 맞춘 DTO만 반환하고, 엔티티나 예외, 환경 맵을 그대로 직렬화하지 않습니다.

heapdump, logfile, threaddump처럼 응답이 큰 엔드포인트에는 타임아웃, 동시 다운로드 수, 최대 크기, 감사 기록을 둡니다. 운영자가 필요하다는 이유로 브라우저에서 바로 내려받게 열지 말고, 비상 승인 절차로 제한합니다. 배포한 뒤에는 역할별로 다음과 같은 결과가 나오는지 확인합니다.

보안 smoke failure
anonymous health status -> 200, details hidden
anonymous metrics -> 401
viewer logger write -> 403
operator logger write -> 204, audit event recorded
public ingress env -> 404 at proxy
response contains apiToken raw value -> SANITIZATION_REGRESSION

더 깊이: Boot가 처음부터 가려 두는 것과 열어 두는 것

Actuator 문서에 따르면 /env, /configprops, /quartz가 돌려주는 값은 기본으로 모두 별표 문자열로 바뀌어 가려집니다. 원래 값을 보려면 그 엔드포인트의 show-values를 never가 아닌 값으로 바꿔야 하고, 적용되는 사용자 정의 SanitizingFunction 빈도 없어야 합니다. when-authorized로 두면 인가된 사용자에게만 보이는데, HTTP에서는 인증을 마치고 그 엔드포인트의 roles에 적은 역할을 가진 사용자가 인가된 사용자이며, roles를 적지 않으면 인증된 사용자는 모두 인가된 것으로 봅니다. JMX에서는 모든 사용자를 언제나 인가된 것으로 보므로, JMX 노출을 켰다면 이 설정만으로 값을 숨길 수 없습니다.

접근 설정의 기본값도 엔드포인트마다 다릅니다. 같은 문서는 shutdown과 heapdump를 뺀 모든 엔드포인트의 접근이 기본으로 제한되지 않는다고 설명합니다. 즉 env나 threaddump는 웹 노출 목록에 이름을 적는 순간 HTTP로 열리고, 이 글의 위험 목록처럼 막아 둬야 할 엔드포인트라면 노출 목록에서 빼는 것과 별도로 management.endpoint.<id>.access를 none이나 read-only로 정해야 합니다. 열어 둘 엔드포인트가 적다면 management.endpoints.access.default=none으로 기본값을 닫고 필요한 것만 다시 여는 방식이 허용 목록의 원칙과 맞습니다.


메트릭 수집 경로와 테넌트 정보

Micrometer 레지스트리가 애플리케이션 안에서 미터를 만들고, Actuator의 Prometheus 엔드포인트가 그것을 수집용 텍스트로 바꾸며, Prometheus가 가져가 시계열 데이터베이스에 저장하고, Grafana가 쿼리합니다. 이 단계마다 별도의 인증 주체와 최소 권한을 둡니다. 수집기의 토큰을 대시보드를 보는 브라우저에 넘기지 말고, Grafana의 데이터 소스가 서버 쪽에서 조회하게 합니다.

메트릭 레이블에 사용자 ID, 이메일, 전체 URL, 예외 메시지를 넣으면 시계열의 카디널리티와 개인정보 노출 문제가 함께 생깁니다. method, 템플릿화한 경로, 상태 코드의 범주, 제한된 결과 값처럼 값의 집합이 정해진 차원만 허용합니다. 수집 엔드포인트를 아무리 잘 보호해도 대시보드 조회자가 민감한 레이블을 볼 수 있으므로, 수집하기 전에 제거합니다.


수집 단계의 실패

애플리케이션이 UP이어도 수집 인증 실패, 서비스 탐색 대상 누락, 타임아웃, 중복 시계열, 원격 저장소의 지연 때문에 그래프가 비어 있을 수 있습니다. 대시보드의 데이터 없음을 트래픽 0으로 해석하지 않습니다. up 시계열, 수집 소요 시간, 대상의 마지막 오류, 규칙 평가 상태를 함께 보여 줍니다.

src/main/java/board/security/ScrapeDecision.java
package board.security;

import java.time.Duration;

public record ScrapeDecision(
        boolean applicationReady,
        boolean targetUp,
        Duration sampleAge,
        String display
) {
    public static ScrapeDecision decide(
            boolean applicationReady,
            boolean targetUp,
            Duration sampleAge,
            Duration staleAfter
    ) {
        if (!targetUp || sampleAge.compareTo(staleAfter) > 0) {
            return new ScrapeDecision(
                    applicationReady, targetUp, sampleAge, "OBSERVABILITY_STALE");
        }
        return new ScrapeDecision(applicationReady, true, sampleAge, "CURRENT");
    }
}

이 결정 모델은 수집 대상이 내려갔거나 표본이 기준보다 오래됐으면 OBSERVABILITY_STALE을, 그렇지 않으면 CURRENT를 표시합니다. applicationReady는 결과에 그대로 보관할 뿐 이 판단에는 쓰지 않으므로, CURRENT는 수집 표본이 신선하다는 뜻이지 애플리케이션이 새 트래픽을 받을 준비가 됐다는 뜻이 아닙니다. 수집 간격보다 짧은 순간의 변화는 놓칠 수 있고, 카운터의 초기화와 오래된 시계열도 쿼리에서 처리해야 합니다.


보안 변경 검증

익명 점검, 인증된 수집기, 읽기 전용 조회자, 운영자, 비상 승인 관리자 역할마다 엔드포인트와 메서드의 기대 상태를 표로 만듭니다. 보안 필터 체인의 단위 테스트만으로는 프록시와 관리 포트의 라우팅을 확인할 수 없으므로, 배포 환경에서의 통합 검증을 추가합니다.

인증 실패를 상태 DOWN으로 연결하지 않습니다. 수집기의 인증 정보를 교체하는 도중 401이 나면 모니터링 경보로 처리하고, 애플리케이션의 트래픽을 빼지 않습니다. 감사 로그 수집기가 실패했을 때 상태 변경 요청을 거부할지도 별도의 정책으로 둡니다.


정리

  • 엔드포인트마다 상태 변경 여부, 구성 정보, 응답 크기, 필요한 역할을 기록하고, 비상용 엔드포인트는 평소에 막아 둡니다.
  • 관리 포트와 네트워크 정책으로 도달 경로를 좁히되, 같은 파드 안의 위협까지 막지는 못하므로 인증과 권한을 함께 둡니다.
  • 관리용 SecurityFilterChain은 EndpointRequest 매처로 범위를 정하고, 사용자 체인을 등록하면 일반 요청용 체인도 따로 구성합니다.
  • 응답과 메트릭 레이블은 수집하기 전에 정제하고, 수집 실패와 애플리케이션 준비 상태를 같은 복구 행동으로 묶지 않습니다.

연습 문제

1. 역할별 접근 행렬

상태는 익명, Prometheus 수집은 수집기, 메트릭과 정보는 조회자, loggers 쓰기는 운영자, heapdump는 비상 승인 사용자에게만 허용하는 행렬을 작성하세요. 별도의 관리 포트와 공개 프록시 차단을 구성하고, 원시 토큰, 이메일 레이블, 크기 제한 없는 페이로드가 발견되면 릴리스를 실패시키세요.

해설 보기

엔드포인트 이름보다 작업의 위험을 먼저 분류합니다. 같은 엔드포인트라도 읽기와 쓰기의 권한이 다를 수 있고, 상태와 상세도 나눌 수 있습니다. 역할과 그 역할이 할 수 있는 일을 다음처럼 코드로 고정할 수 있습니다.

package board.security;

public enum OperationsRole {
    PROBE,
    SCRAPER,
    VIEWER,
    OPERATOR,
    BREAK_GLASS;

    public boolean canMutateLoggers() {
        return this == OPERATOR || this == BREAK_GLASS;
    }

    public boolean canDownloadHeap() {
        return this == BREAK_GLASS;
    }
}

역할 이름만 정해 두지 말고, 신원을 발급하고 회수하는 절차와 인증 정보 교체의 담당자까지 운영 문서에 연결합니다.

2. 수집 표본의 신선도

ScrapeDecision.decide에 applicationReady=false, targetUp=true, sampleAge=30초, staleAfter=30초를 넣으면 display는 무엇인가요? 같은 조건에서 sampleAge만 31초로 바꾸거나, targetUp만 false로 바꾸면 어떻게 달라지나요?

해설 보기

첫 입력은 CURRENT입니다. 비교가 sampleAge.compareTo(staleAfter) > 0이므로 30초와 30초는 오래된 것으로 보지 않고, applicationReady=false는 이 판단에 쓰이지 않습니다.

두 변형은 모두 OBSERVABILITY_STALE입니다. 31초는 기준을 넘고, targetUp=false도 같은 분기로 갑니다. 세 경우 모두 applicationReady=false는 결과에 그대로 남습니다. 수집 인증 실패나 표본 공백과 업무 준비 상태의 실패를 같은 복구 행동으로 묶지 않습니다.

3. 역할과 위험 항목의 검사

OperationsRole의 다섯 역할 가운데 canMutateLoggers()와 canDownloadHeap()이 참인 역할을 각각 고르세요. 또 new EndpointRisk("loggers", true, false, 1024, "PROBE")를 만들면 어떻게 되나요?

해설 보기

canMutateLoggers()는 OPERATOR와 BREAK_GLASS에서 참이고, canDownloadHeap()은 BREAK_GLASS에서만 참입니다. PROBE, SCRAPER, VIEWER는 둘 다 거짓입니다.

EndpointRisk는 상태를 바꾸는 엔드포인트에 PROBE 역할을 붙이면 생성자에서 IllegalArgumentException("probe role cannot mutate")를 던집니다. 이런 검사는 정책 표 자체의 실수를 잡아 줄 뿐이며, 실제 요청에서 그 역할이 강제되는지는 보안 체인과 배포 환경의 검증으로 확인합니다.

다음 글 Micrometer 레지스트리에서는 이렇게 보호한 메트릭이 애플리케이션 안에서 어떻게 만들어지는지, 미터와 레지스트리, 태그의 관계를 살펴봅니다.