Files
llm-wiki/raw/branch-notes/feature-management-actuator-security-contract.md
T

41 KiB

title, source_type, status, id, kind, project, work_item, inherits, refines, overrides, depends_on, contract_packet, branch, parent_branch, governing_docs, related_projects, tags, created, target_merge, status_label, contract_packet_sha256
title source_type status id kind project work_item inherits refines overrides depends_on contract_packet branch parent_branch governing_docs related_projects tags created target_merge status_label contract_packet_sha256
branch / feature-management-actuator-security-contract branch-note raw BR-CA-SKELETON-OPERATIONAL-CONTRACT-021 project-work-item ca-skeleton-operational-contract WI-CA-SKELETON-OPERATIONAL-CONTRACT-021
DEC-CA-SKELETON-OPERATIONAL-CONTRACT-STACK-FRAMEWORK-001@1
1 feature-management-actuator-security-contract
wiki/projects/ca-tmpl/security-baseline-jwt-actuator-secrets
ca-skeleton
branch
ca-skeleton
actuator
management
security
2026-05-22 in-progress 8efcccc3f1adafbda15730eb0506dc851a4177a83e02825fb0928f61536bf939

branch: feature-management-actuator-security-contract

Layer: raw/branch-notes/ — actuator/management endpoint 노출 보안 기준을 정의합니다.

부모 (필수)

raw/project-notes/ca-skeleton-operational-contract

브랜치 계약 패킷

  • 생성 시 프로젝트 개정: 1
  • 패킷 스키마: contract_packet: 1
  • 완료 조건: management endpoint exposure·authorization test가 통과한다

상속한 프로젝트 결정

Decision Ref Project Summary Branch Application Source
DEC-CA-SKELETON-OPERATIONAL-CONTRACT-STACK-FRAMEWORK-001@1 framework는 Spring Boot 3.5.14다 Spring Boot Actuator endpoint exposure와 authorization contract에 적용한다 raw/project-notes/ca-skeleton-operational-contract

브랜치 지역 결정

기존 branch-local 결정은 아래 ## Decision Evidence Map / 결정-근거 매핑의 D-row가 소유하며 이 packet에서 복제하지 않는다.

Decision ID Decision Relation Supporting Claims Status

선언한 예외

Override ID Overrides Reason Approval Status

없음.

목표

actuator는 운영에 필수지만 잘못 노출되면 env, config, metric, health detail이 공격 표면이 됩니다. skeleton은 management endpoint allowlist와 profile별 노출 정책을 가져야 합니다.

  • 이슈:
  • PR:

범위

포함 범위

  • actuator endpoint allowlist.
  • health detail exposure 기준.
  • metrics endpoint 인증 기준.
  • management port 분리 여부.
  • prod env/configprops 노출 금지.
  • management endpoint security log 기준.

제외 범위

  • Kubernetes ingress rule.
  • cloud load balancer health check 설정.
  • enterprise admin portal 구현.

근거 (필수, 최소 1개+)

본 branch의 결정 근거. 상세 비교는 §외부 근거 / 대안 조사 (있다면) 참조.

Source 정당화하는 결정
raw/official-docs/actuator-endpoint-exposure-spring-official Spring 공식 default + exposure 정책
raw/official-docs/actuator-management-port-spring-official Spring 공식 separate port 권고
raw/official-docs/security-mtls-rfc-8705 zero-trust 권장이나 cert 운영 부담
raw/official-docs/actuator-istio-sidecar-management-alt mesh 가정이 강함, skeleton 중립성 손실
raw/company-tech-blogs/security-woowahan-actuator-safe-usage 우아한형제들 SOC팀 — 별도 포트 + endpoint allowlist + shutdown/heapdump forbidden 권고
raw/company-tech-blogs/security-toss-actuator-healthcheck 토스 — health endpoint 자체도 민감 정보 포함 가능, public 접근 금지
raw/official-docs/archunit-conditional-on-property-3-layer-pattern ArchUnit custom rule — actuator-security 코드의 shape-ownership 정적 경계 강제 (D6 부분 근거)

외부 근거 / 대안 조사 (2026-05-22 — Group G-B: Management / Actuator Security)

본 branch의 management port 9001 분리 + prod allowlist (health/prometheus/info) + heapdump/threaddump prod forbidden + loggers prod read-only 결정에 대한 외부 source.

후속 보강 (2026-05-22): 한국 보안 사례 source 추가. raw/company-tech-blogs/security-woowahan-actuator-safe-usage (우아한형제들 SOC팀 — 별도 포트 + endpoint allowlist + shutdown/heapdump forbidden 권고) 및 raw/company-tech-blogs/security-toss-actuator-healthcheck (토스 — health endpoint 자체도 민감 정보 포함 가능, public 접근 금지) 참조.

후속 보강 (2026-06-14 — /branch-spec): D6 ownership 강제 메커니즘 근거로 raw/official-docs/archunit-conditional-on-property-3-layer-pattern (AUCP-C1~C4) 편입. ArchUnit 은 package/type/annotation 의 정적 경계만 강제 가능 (AUCP-C1) — "PR diff 의 shape 변경 감지" 는 ArchUnit 범위 밖이므로 그 부분은 CODEOWNERS/CI gate 로 위임 (§구현 가이드 §5). D7(Prometheus rate-limit 면제)은 자동조사 후에도 외부 normative 근거 없음 + rate-limit owner 미정의 → UNSUPPORTED_DECISION 유지 (cross-branch gap, §엣지·실패·의존).

TODO

TODO drained 2026-05-22 — 결정은 아래 "결정 사항" / "Exposure Policy" 참조. actuator allowlist / health detail exposure / metrics auth / management port / prod env·configprops forbidden / security log 기준 모두 결정 라인 또는 표로 반영됨. 잔존 TODO 없음.

진행 중 메모

  • readiness/liveness와 management endpoint 보안은 연결되지만 별도 기준입니다.

결정 사항 (decisions)

  • 2026-05-22: actuator exposure security를 runtime lifecycle에서 분리.
  • 2026-05-22: actuator/health endpoint shape owner는 feature-runtime-health-lifecycle-contract, 이 branch는 endpoint exposure/auth/security log만 소유.
  • 2026-05-22: prod/staging management port는 분리 권장, 단일 port는 platform ingress 보호가 문서화될 때만 허용.
  • 2026-05-22: management port default = 9001 (separate from app 8080). single-port는 platform ingress 보호 + 문서화 시만 허용.
  • 2026-05-22: metrics endpoint 인증 = network ACL (cluster-internal scrape only) default. basic auth는 cluster 외부 노출 시 의무. mTLS는 zero-trust 환경에서 권장.
  • 2026-05-22: heapdump/threaddump endpoint = prod forbidden, non-prod에서만 admin role.
  • 2026-05-22: prod allowlist endpoint final = health/liveness, health/readiness, health/startup, prometheus, info (build info only, no secret). env/configprops/heapdump/threaddump는 prod forbidden. loggers는 prod read-only.
  • 2026-05-22: Prometheus scrape는 rate-limit 면제 (network ACL로 보호).

Exposure Policy

endpoint prod default
liveness/readiness exposed with minimal detail
metrics/prometheus authenticated or management network only
env/configprops forbidden
heapdump/threaddump forbidden unless break-glass runbook
shutdown forbidden

Work Item Contract

각 TODO는 아래 판정 단위로 재작성되어야 canonical 승급 가능합니다. TODO가 단순히 기준 작성으로 남아 있으면 이 branch는 완료로 보지 않습니다.

field required rule
Decision yes 구현자가 선택해야 하는 기본값
Allowed yes 허용되는 예외와 조건
Forbidden yes 절대 금지되는 구현/문서 상태
Required registry update conditional error/env/header/log/metric/capability 변경 시 필수
Required contract test yes 계약 위반 시 실패해야 하는 테스트
Failure condition yes review/build에서 실패로 판정할 상태
Canonical extraction target yes wiki/projects 승급 위치

결정-근거 매핑

각 결정이 어떤 raw source claim 으로 뒷받침되는지 명시. company-tech-blog 증거는 company-case-study 로 표기하며 공식 best practice 로 승격하지 않음.

Decision ID Decision Supporting Claims Evidence Strength Open Risk
D1 management port = 9001 (separate from app 8080), single-port 는 platform ingress 보호 + 문서화 시만 허용 raw/official-docs/actuator-management-port-spring-official.md#SB-ACT-PORT-C1, raw/official-docs/actuator-management-port-spring-official.md#SB-ACT-PORT-C2, raw/official-docs/actuator-management-port-spring-official.md#SB-ACT-PORT-C3, raw/company-tech-blogs/security-woowahan-actuator-safe-usage.md#WW-ACT-C4 · registry FACT: ca-tmpl/docs/registries/env-keys.yaml#MANAGEMENT_SERVER_PORT (default 9001, owner_branch 본 branch, required_test actuator-contract:management-port-separated) official-vendor-doc + company-case-study (Spring 이 두 옵션 모두 sensible 로 명시 — 어느 쪽이 absolute 최선 아님) port 번호 9001 자체는 Spring 권장 default 아님 (사용자 선택 — registry 에 고정됨). LoadBalancer/NodePort 실수 노출 방지 위한 network policy 검증 필요
D2 prod allowlist = health/liveness,health/readiness,health/startup,prometheus,info (build info only), env/configprops/heapdump/threaddump 는 prod forbidden, loggers 는 prod read-only raw/official-docs/actuator-endpoint-exposure-spring-official.md#SB-ACT-EXP-C1, raw/official-docs/actuator-endpoint-exposure-spring-official.md#SB-ACT-EXP-C2, raw/official-docs/actuator-endpoint-exposure-spring-official.md#SB-ACT-EXP-C4, raw/company-tech-blogs/security-woowahan-actuator-safe-usage.md#WW-ACT-C1, raw/company-tech-blogs/security-woowahan-actuator-safe-usage.md#WW-ACT-C2, raw/company-tech-blogs/security-woowahan-actuator-safe-usage.md#WW-ACT-C3 · registry FACT: ca-tmpl/docs/registries/error-codes.yaml#ACTUATOR_FORBIDDEN (AUTHZ/403, owner_branch 본 branch, required_test contract-verification:management-actuator) official-vendor-doc + company-case-study (Spring default sanitize env/configprops → ca-tmpl 은 한 단계 더 strict 한 자체 결정. heapdump/threaddump prod 금지는 Spring 공식 의무 아님) info 의 contributor 가 추가 정보로 secret 노출 가능 — review 통제 필요. env/configprops 부분 노출 시 secret masking 은 feature-secrets-config-source-contract 위임 (§엣지·실패·의존)
D3 metrics endpoint 인증 = network ACL (cluster-internal scrape only) default, external 노출 시 basic auth 의무, zero-trust 에서 mTLS 권장 raw/official-docs/actuator-endpoint-exposure-spring-official.md#SB-ACT-EXP-C2, raw/official-docs/actuator-endpoint-exposure-spring-official.md#SB-ACT-EXP-C3, raw/company-tech-blogs/security-woowahan-actuator-safe-usage.md#WW-ACT-C8 official-vendor-doc + company-case-study (Spring 권고 옵션 3개 중 firewall/Spring Security 선택 — 어느 쪽이 absolute 최선 아님) custom SecurityFilterChain 정의 시 Spring auto-secured 가 비활성 (SB-ACT-EXP-C3 의 흔한 함정) — actuator path 보호 룰을 명시적으로 검증 필요
D4 shutdown endpoint forbidden (모든 환경) raw/company-tech-blogs/security-woowahan-actuator-safe-usage.md#WW-ACT-C6 company-case-study (Spring 공식 default 는 disabled 지만 절대 금지는 우아한형제들 운영 권고 — 공식 표준 아님) dev/staging 에서도 항상 금지인지 결정 — WW-ACT-C6 는 prod 강조로 해석. local 단축키 필요 시 별도 escape hatch 필요
D5 heapdump/threaddump endpoint = prod forbidden, non-prod 에서만 admin role 로 허용 raw/company-tech-blogs/security-woowahan-actuator-safe-usage.md#WW-ACT-C3 company-case-study (Spring 공식 의무 아님 — 회사 운영 권고) non-prod 에서 admin role 발급/회수 절차 미정의 — IAM branch 와 cross-link 필요
D6 shape-ownership 경계 강제 — actuator-security 코드는 HealthEndpoint/HealthIndicator/HealthComponent 를 선언·구현하지 않음 (shape owner 는 feature-runtime-health-lifecycle-contract, 본 branch 는 exposure/auth 만 소유) raw/official-docs/archunit-conditional-on-property-3-layer-pattern.md#AUCP-C1 (ArchUnit custom rule = classes that ${PREDICATE} should ${CONDITION} — package/type/annotation 정적 boundary 강제 가능), raw/official-docs/archunit-conditional-on-property-3-layer-pattern.md#AUCP-C2 (classpath 有 시 type/annotation 접근) · 정합: raw/branch-notes/feature-runtime-health-lifecycle-contract D2 (shape owner 분할 SSOT) official-vendor-doc (partial — ArchUnit static package/type boundary 한정) PR diff 기반 shape-change 감지는 ArchUnit 범위 밖 (bytecode static ≠ git diff — AUCP Usage Boundary §"증명하지 않는 것"). 그 부분은 CODEOWNERS / CI diff gate 로 위임 = UNSUPPORTED_IMPL_DECISION (§구현 가이드 §5). HealthEndpoint 등 Spring type 의존 rule 이므로 classpath 필요
D7 Prometheus scrape 는 rate-limit 면제 (network ACL 로 보호) UNSUPPORTED_DECISION (rate-limit 면제는 cited official-doc 직접 인용 없음 + rate-limit owner raw/branch-notes/feature-rate-limit-idempotency-contract 도 metrics/scrape 예외를 결정하지 않음 — 그 branch D4 rate-limit-key 자체가 UNSUPPORTED) n/a cross-branch gap: scrape 예외 메커니즘을 rate-limit owner 가 SSOT 로 정의해야 함. 미정의 시 prometheus scrape 가 rate-limit 에 걸려 metrics gap (§엣지·실패·의존). raw/branch-notes/feature-rate-limit-idempotency-contract 와 동시 결정 필요
D8 (보조 정합) 토스 — health endpoint 자체도 보안 민감 정보 포함 가능, public 접근 금지 raw/company-tech-blogs/security-toss-actuator-healthcheck.md#TOSS-HEALTH-C1, raw/company-tech-blogs/security-toss-actuator-healthcheck.md#TOSS-HEALTH-C5, raw/company-tech-blogs/security-toss-actuator-healthcheck.md#TOSS-HEALTH-C6 company-case-study (best practice 승격 금지 — 토스 한국 사례) show-details: always 가 prod 에서 우회로 활성되지 않도록 ArchUnit 또는 config 검증 필요

구현 가이드

결정 (Decisions) 이 "무엇 을 할 것인가" 라면, 본 §는 "어디에 어떻게 구현될 것인가" 의 사전 명세 — 다음 구현자가 되묻지 않아도 코드를 작성할 수 있는 수준.

구현 상태 (ca-tmpl ground truth, updated 2026-06-15): Phase C2 구현 완료. application.ymlmanagement: block 추가됨, ManagementActuatorSecurityContractTest 4개 계약 테스트 통과, ManagementSecurityConfig @Order(0) SecurityFilterChain 구현, management_security_does_not_depend_on_health_internals ArchUnit rule 통과. ACTUATOR_FORBIDDEN enum 추가됨. 상태: actually-implemented (D1/D2/D3/D4/D6/D8) + locally-verified. D5(non-prod admin role) + D7(rate-limit carve-out)는 여전히 planned.

Post-review 경화 (2026-06-15, /branch-spec 검증 follow-up): 검토에서 드러난 3개 테스트/동작 gap 보강 — (F1) ManagementActuatorSecurityContractTest실제 application.yml 의 include/exclude/port/show-details/shutdown 을 파싱·고정(주입값 검증의 tautology 제거, 9개 테스트로 확장), (F2) 신규 ActuatorSecurityHttpTest (7개) 가 MockMvc 로 SecurityFilterChain 을 HTTP 레벨로 구동 — health/info/prometheus 200, loggers 비인증 401, env 404(excluded). 이를 위해 ManagementSecurityConfigHttpStatusEntryPoint(401) 명시(프레임워크 default 403 → 의미상 올바른 401), (F3) loggers prod read-only 를 실제 강제POST /actuator/loggers/** denyAll() 추가(이전엔 authenticated 면 log level 변경 가능했음 = 계약 위반). app-bootstrap 전체 410 테스트 green, 회귀 없음.

1. Management port separation (D1)

Trace: D1 + SB-ACT-PORT-C1~C3 + WW-ACT-C4. registry FACT: ca-tmpl/docs/registries/env-keys.yaml#MANAGEMENT_SERVER_PORT (default 9001, owner 본 branch, required_test actuator-contract:management-port-separated).

  • UNSUPPORTED_IMPL_DECISION: port 번호 9001 은 Spring 권장 default 아님 (사용자 선택) — registry 에 고정. trade-off: 8080(app)과 충돌만 피하면 임의값 가능, 9001 은 관례적 선택.
항목 명세 (planned) anchor
config key management.server.port${MANAGEMENT_SERVER_PORT:9001} (application.yml 에 management.server block 신규 추가) env-keys.yaml#MANAGEMENT_SERVER_PORT
app port (consume only) server.port${APP_SERVER_PORT:8080} — owner feature-env-driven-runtime-configuration, 본 branch 는 분리 대상으로만 참조 env-keys.yaml#APP_SERVER_PORT
contract test actuator-contract:management-port-separated — app port 와 management port 가 다른 listener 인지 검증 (planned) required_test
single-port escape hatch management.server.port 미설정 = app port 공유 허용, platform ingress path ACL 보호가 문서화될 때만 (D1 조건)

2. Prod exposure allowlist (D2)

Trace: D2 + SB-ACT-EXP-C1/C2/C4 + WW-ACT-C1~C3. registry FACT: forbidden endpoint 접근 → error-codes.yaml#ACTUATOR_FORBIDDEN (AUTHZ/403, client_safe "Permission denied", log WARN, required_test contract-verification:management-actuator).

  • UNSUPPORTED_IMPL_DECISION: non-prod 의 정확한 노출 집합은 cited doc 이 권고하지 않음 — prod 만 strict allowlist, non-prod 는 더 넓게(운영 편의) = 사용자 trade-off. exclude 명시 vs include-only 의 선택도 운영 trade-off (여기선 defense-in-depth 위해 forbidden 을 explicit exclude).
profile management.endpoints.web.exposure.include ...exposure.exclude 비고
prod health,prometheus,info,loggers env,configprops,heapdump,threaddump,shutdown loggers 는 read-only(write 차단은 SecurityFilterChain §3). info = build info only, no secret
non-prod 더 넓게 허용 (UNSUPPORTED_IMPL — 정확 집합 미정) shutdown (항상, D4) heapdump/threaddump 는 admin role 게이트(§4)
  • forbidden endpoint 접근 시 ACTUATOR_FORBIDDEN (403, WARN log) — 보안 이벤트 로그 필수(§테스트 계약). runbook runbook://management/actuator-forbidden 는 planned(아직 docs/runbooks/ 부재).
  • DELEGATED (R3): env/configprops 가 부분 노출되는 경로의 secret masking 은 본 branch 범위 밖 → feature-secrets-config-source-contract (secrets-contract:db-password-no-leak-in-actuator, datasource-username/url-masked-in-actuator).

3. Metrics / actuator auth (D3)

Trace: D3 + SB-ACT-EXP-C2/C3 + WW-ACT-C8.

  • UNSUPPORTED_IMPL_DECISION: custom SecurityFilterChain 정의 시 Spring 의 actuator auto-secure 가 비활성(SB-ACT-EXP-C3 함정) → actuator path 보호를 명시적 rule 로 작성해야 함. matcher 표현(EndpointRequest.toAnyEndpoint() 등)은 Spring Security 관용이나 정확한 bean 모양은 본 branch 결정 아닌 구현 detail.
노출 위치 기본 (planned) 강화 옵션
cluster-internal scrape network ACL only (app-level auth 없음) — baseline
cluster 외부 노출 basic auth 의무 (D3) zero-trust 환경 mTLS — security-mtls-rfc-8705, cert 운영 부담으로 baseline 아님
  • SecurityFilterChain bean (ManagementSecurityConfig, actually-implemented): securityMatcher(EndpointRequest.toAnyEndpoint()) 로 actuator path 만 가로채고, health/info/prometheus permitAll(), 나머지 authenticated(). 비인증 접근은 HttpStatusEntryPoint(HttpStatus.UNAUTHORIZED)401 응답(default 403 아님 — ActuatorSecurityHttpTest.loggers_endpoint_challenges_unauthenticated_caller_with_401 검증). loggers write 차단은 아래 §4 가 아닌 본 체인의 POST /actuator/loggers/** denyAll() + DELETE /actuator/loggers/** denyAll() 두 라인으로 구현(D2 read-only). DELETE 는 logger-level reset mutation 으로 POST 와 동일한 write 위험 — 함께 막아야 일관성 보장.

4. Dangerous endpoints (D4, D5)

Trace: D4(shutdown forbidden 전 환경, WW-ACT-C6) + D5(heapdump/threaddump prod forbidden·non-prod admin role, WW-ACT-C3).

  • UNSUPPORTED_IMPL_DECISION: non-prod admin role 의 발급/회수 절차 는 본 branch 결정 근거 없음 → IAM/security branch 위임(D5 Open Risk). "절대 금지(전 환경)" vs "non-prod escape hatch" 는 D4 의 운영 trade-off(local 단축키 필요 시 별도 hatch).
  • shutdown: 모든 profile exclude (Spring default disabled 와 정합, D4 는 한 단계 더 — explicit 금지).
  • heapdump/threaddump: prod exclude; non-prod 는 admin role gate(절차 미정 = planned).

5. Shape-ownership boundary enforcement (D6)

Trace: D6 + AUCP-C1(ArchUnit custom rule PREDICATE/CONDITION) + AUCP-C2(classpath type 접근) + 정합 raw/branch-notes/feature-runtime-health-lifecycle-contract#D2`(shape owner 분할).

  • SUPPORTED (정적 경계): ArchUnit rule — actuator-security package 의 class 가 HealthEndpoint/HealthIndicator/HealthComponent 를 선언·구현·의존하지 않는다. noClasses().that().resideInAPackage("..management.security..").should().dependOnClassesThat().areAssignableTo(HealthIndicator.class) 형태 (AUCP-C1 표준 형식, classpath 필요 → AUCP-C2).
  • UNSUPPORTED_IMPL_DECISION: "PR diff 에서 health response field 변경 감지"(현 §테스트 계약 표현)는 ArchUnit 범위 밖(bytecode static ≠ git diff — AUCP Usage Boundary). → CODEOWNERS / CI diff gate 로 위임. trade-off: ArchUnit 은 구조 경계만, 변경 출처는 CI 책임.

6. Prometheus rate-limit exemption (D7)

Trace: D7 — UNSUPPORTED_DECISION(자동조사 후에도 외부 normative 근거 없음).

  • UNSUPPORTED_IMPL_DECISION (cross-branch gap): prometheus scrape path 의 rate-limit carve-out 메커니즘은 rate-limit owner(raw/branch-notes/feature-rate-limit-idempotency-contract)가 정의해야 SSOT 정합. 현재 그 branch 는 metrics 예외를 결정하지 않음(D4 rate-limit-key 자체 UNSUPPORTED). 본 branch 는 network ACL 보호를 가정만 함 — filter 예외 코드는 rate-limit branch 와 동시 결정 전까지 planned.
  • 임시 운영선 (interim, rate-limit owner 결정 전까지): prometheus scrape 는 network ACL (cluster-internal scrape only) 단독 의존으로 운영 — rate-limit filter 를 적용하지 않는 별도 management network 에 둠(D1 의 management port 9001 분리가 이 격리를 제공). 즉 carve-out 코드를 짜지 않고도 "scrape 가 rate-limit 에 걸려 metrics 가 비는" 실패 경로가 발생하지 않음(scrape 트래픽이 rate-limited app port 를 통과하지 않으므로). 본격 filter carve-out 은 management endpoint 가 app port 와 단일 포트로 합쳐지는(single-port escape hatch, D1) 경우에만 필요해지며, 그 때 rate-limit owner 와 동시 PR.

엣지·실패·의존

R4(깊이 게이트) 캡처용. 정상 경로 외에 구현 중 부딪힐 실패/엣지/다른 계약 의존을 미리 열거.

  • 실패·엣지 경로:
    • forbidden endpoint 접근 → 403 ACTUATOR_FORBIDDEN + WARN 보안 로그. 접근 실패가 security event log 에 안 남으면 테스트 fail(§테스트 계약).
    • custom SecurityFilterChain 가 actuator auto-secure 를 비활성화 → actuator path 가 permitAll 로 누수(SB-ACT-EXP-C3). 기대: 통합 테스트로 /actuator/env 비인증 접근 시 401/403.
    • single-port mode 에서 ingress path ACL 누락 → management endpoint 가 public LB 로 노출. 기대: network policy 검증(D1 Open Risk).
    • prometheus scrape 가 rate-limit 에 걸림 → metrics gap. 기대: scrape carve-out(D7) — 현재 미구현.
    • info/show-details: always 가 prod profile 에 실수로 override → 민감정보 노출(D8). 기대: prod profile config 검증.
  • 다른 계약 의존:

테스트 계약

  • prod에서 env/configprops endpoint가 노출되면 실패.
  • health detail이 prod에서 과노출되면 실패.
  • metrics endpoint가 인증 없이 열리면 실패.
  • management endpoint 접근 실패가 security event log에 남지 않으면 실패.
  • shape ownership 위반 검사: 본 branch의 PR diff에서 org.springframework.boot.actuate.health.HealthEndpoint, HealthIndicator, /actuator/health/* endpoint response field 변경 시 fail. 측정 방법: PR diff filter — actuator security branch가 owner인 영역(exposure, port, auth)이 아닌 response shape 영역(HealthEndpoint, HealthIndicator, HealthComponent) 변경이 포함되면 review reject. ArchUnit으로 이 branch가 자칭 owner인 file 외 변경 금지. (⚠️ ArchUnit 은 정적 구조 경계만 — PR diff 변경 감지는 CODEOWNERS/CI gate 책임. §구현 가이드 §5 의 SUPPORTED/UNSUPPORTED 분리 참조.)

검증해야 할 주장

공식 문서나 사례는 근거지만, 내 프로젝트에서의 동작을 자동으로 보장하지 않는다.

Claim Why uncertain How to verify Status
prod 에서 management.endpoints.web.exposure.include=health,prometheus,info 설정 시 실제 노출되는 sub-endpoint 집합 (특히 /actuator/health/liveness group sub-path 포함 여부) SB-ACT-EXP-C1/C2 는 default 만 다룸 — group sub-path 노출 동작은 별도 페이지 local 통합 테스트로 /actuator/health/liveness curl + status 200 확인 + /actuator/env 403/404 확인 planned
custom SecurityFilterChain 정의된 ca-tmpl 환경에서 actuator path 가 permitAll() vs authenticated() 어디로 떨어지는지 SB-ACT-EXP-C3 가 명시한 함정 — auto-config 비활성 시 명시적 설정 필요 SecurityFilterChain bean 정의 검증 + 통합 테스트로 /actuator/env 비인증 접근 시 401 확인 needs-confirmation
prometheus endpoint 의 prod 노출 시 scrape 인증 (network ACL 만으로 충분한지) D3 의 network ACL 가정은 클러스터 외부 노출 차단 의존 — 별도 검증 k8s NetworkPolicy 적용 + 외부 IP 에서 /actuator/prometheus curl 시 차단 확인 planned
ArchUnit 기반 shape ownership 검사 (D6) 의 실 구현 가능 여부 D6 — ArchUnit 은 정적 boundary(AUCP-C1)만, diff 감지는 범위 밖. fitness function 도입 결정 코드 단계 보류 raw/official-docs/archunit-conditional-on-property-3-layer-patternAUCP-C1~C4 검토 후 noClasses().should().dependOnClassesThat().areAssignableTo(HealthIndicator) rule 작성 가능성 평가 + CODEOWNERS gate 분리 needs-confirmation
heapdump/threaddump non-prod admin role 발급/회수 절차 (D5) non-prod IAM 정책이 정의되지 않음 IAM branch 와 cross-link, admin role 발급 runbook 작성 planned
show-details: always 가 prod profile 에서 차단되는지 (D8 관련) Spring profile 별 config override 가 실수로 prod 에 적용 가능 ArchUnit 또는 @Value("${management.endpoint.health.show-details}") 확인 + prod profile 통합 테스트 planned
prometheus scrape 의 rate-limit 예외 (D7) 가 어느 owner 의 어느 메커니즘으로 구현되는지 rate-limit owner(feature-rate-limit-idempotency-contract)가 metrics 예외를 미정의 — cross-branch gap rate-limit branch 와 동시 결정: scrape path carve-out 을 rate-limit filter SSOT 에 추가할지 vs network ACL 단독 의존할지 needs-confirmation

관심사 커버리지 (coverage-auditor 자동 생성 — 2026-06-14)

/coverage (coverage-auditor) 생성물 — governing doc wiki/projects/ca-tmpl/security-baseline-jwt-actuator-secrets.md 의 Actuator axis(§D2)가 요구하는 관심사를 이 branch 가 빠짐없이 덮는지의 결과. 판정: Covered (missing 0 / Blocking 0). 기준: rules/coverage-gate.md.

관심사 상태 owner 심각도 근거
Management port 9001 분리 covered-here D1 · env-keys.yaml#MANAGEMENT_SERVER_PORT (owner_branch 본 branch)
Prod exposure allowlist (health/prometheus/info) covered-here D2 · §구현 가이드 §2 · governing doc §D2
env/configprops prod forbidden covered-here D2 · error-codes.yaml#ACTUATOR_FORBIDDEN (owner_branch 본 branch)
heapdump/threaddump prod forbidden covered-here D2 + D5 · §구현 가이드 §4
shutdown endpoint forbidden (전 환경) covered-here D4 · §구현 가이드 §4
loggers prod read-only covered-here D2 · §구현 가이드 §2 표
Metrics network ACL default (metrics auth) covered-here D3 · §구현 가이드 §3
Health detail exposure 기준 (show-details policy) covered-here D8 · Claims To Verify (show-details prod 차단 검증)
Health endpoint shape delegated raw/branch-notes/feature-runtime-health-lifecycle-contract OK 위임: raw/branch-notes/feature-runtime-health-lifecycle-contract D2 (양방향 owner 합의) · §엣지·실패·의존
Secret masking inside actuator output (env/configprops 부분 노출) delegated raw/branch-notes/feature-secrets-config-source-contract OK 위임: raw/branch-notes/feature-secrets-config-source-contract (secrets-contract:datasource-username/url-masked-in-actuator registry test) · 역방향 위임 링크 존재
Prometheus rate-limit 면제 (scrape carve-out) delegated raw/branch-notes/feature-rate-limit-idempotency-contract Should-fix 위임: raw/branch-notes/feature-rate-limit-idempotency-contract (D7, cross-branch gap — rate-limit owner 미정의). 임시 운영선 = network ACL 단독(§구현 가이드 §6). owner 가 carve-out 을 결정하면 동시 PR
Security event logging (forbidden 접근 시 WARN) covered-here §테스트 계약 · §구현 가이드 §2 · error-codes.yaml#ACTUATOR_FORBIDDEN (log_level: WARN)
Ownership boundary (shape vs exposure 분리) covered-here D6 · §구현 가이드 §5 · runtime-health D2 양방향 포인터

마주친 문제

  • 2026-06-15: app-bootstrap compile classpath에 Spring Security 없음

    • 원인: adapter-webspring-boot-starter-securityimplementation (not api) 으로 선언 → app-bootstrap 의 compile classpath 에 security 타입 없음.
    • 시도: ManagementSecurityConfigHttpSecurity, SecurityFilterChain 을 import → compileJava 실패 (6 errors).
    • 해결: app-bootstrap/build.gradleimplementation 'org.springframework.boot:spring-boot-starter-security' 추가. composition root 가 cross-cutting security wiring 을 소유하는 것은 정상 (AGENTS.md §app-bootstrap).
    • 별도 에러 노트로 분리됨: 불필요 (원인·해결이 1-liner, 재발 가능성 낮음)
  • 2026-06-15: @SpringBootTest 에서 dual-port 충돌 방지

    • 원인: management.server.port=9001 설정 시 @SpringBootTest full-context 가 두 번째 포트를 바인드하려 해 기존 smoke 테스트와 충돌 가능.
    • 해결: application-test.ymlmanagement.server.port=0 오버라이드 추가 (random port). 계약 테스트는 ApplicationContextRunner (no live server) 로 properties 검증 — 포트 충돌 없음.

묶음

Phase C2 실 코드 작성 완료 (2026-06-15). 아래 항목 실제 구현됨.

Implemented (2026-06-15) — actually-implemented + locally-verified

  • src/shared-contract/.../error/OperationalError.javaACTUATOR_FORBIDDEN(Category.AUTHZ, 403, false) 상수 추가 (D2 registry 정합)
  • src/app-bootstrap/build.gradlespring-boot-starter-actuator, micrometer-registry-prometheus, spring-boot-starter-security 추가
  • src/app-bootstrap/src/main/resources/application.ymlmanagement: block 신규 추가: port 9001, exposure allowlist, exclude list, show-details: when-authorized, shutdown.enabled: false, info.build.enabled: true
  • src/app-bootstrap/src/test/resources/application-test.ymlmanagement.server.port: 0 오버라이드 (test dual-port 방지)
  • src/.envMANAGEMENT_SERVER_PORT=9001 추가
  • src/app-bootstrap/.../management/security/ManagementSecurityConfig.java@Order(0) actuator SecurityFilterChain: health/info/prometheus permitAll, 나머지 authenticated
  • src/app-bootstrap/src/test/.../architecture/CleanArchitectureTest.javamanagement_security_does_not_depend_on_health_internals ArchUnit rule 추가 (D6 정적 경계)
  • src/app-bootstrap/src/test/.../contract/ManagementActuatorSecurityContractTest.java — 4개 계약 테스트 신규 작성 (error-code/management-port-separated/exposure-policy/show-details-when-authorized)

Post-review hardening (2026-06-15, ca-quality-reviewer fixes) — actually-implemented + locally-verified

  • ManagementSecurityConfig.javaDELETE /actuator/loggers/** denyAll() 추가 (POST 와 나란히). logger-level reset 도 write mutation — POST 단독 차단은 불완전했음.
  • ActuatorSecurityHttpTest.javaloggers_reset_via_delete_is_denied 테스트 추가 (DELETE /actuator/loggers/dev.caskeleton → 403). delete MockMvcRequestBuilders import 추가.
  • ManagementActuatorSecurityContractTest.javahealth_show_details_is_when_authorized_not_always 메서드 삭제. 이 메서드는 .withPropertyValues("management.endpoint.health.show-details=when-authorized") 로 값을 직접 주입하고 같은 값을 assert 하는 tautology — 실제 application.yml regression 을 감지할 수 없었음. 진짜 regression guard 는 application_yml_pins_show_details_when_authorized_and_shutdown_disabled (main application.yml artifact 파싱) 이며, 이 테스트는 그대로 유지됨.

Verification (locally-verified)

Command Result
./gradlew :shared-contract:test BUILD SUCCESSFUL
./gradlew :app-bootstrap:test --tests '*ManagementActuatorSecurityContractTest' BUILD SUCCESSFUL
./gradlew :app-bootstrap:test --tests '*CleanArchitectureTest' BUILD SUCCESSFUL
./gradlew :app-bootstrap:test BUILD SUCCESSFUL (full suite)
./gradlew verifyCleanArchitectureDependencies BUILD SUCCESSFUL
./gradlew verifyEnvKeys BUILD SUCCESSFUL — 98 env keys, 74 required placeholders
./gradlew verifyPublicPathSnapshot BUILD SUCCESSFUL — 1 public path unchanged

Post-review hardening verification (2026-06-15)

Command Result
./gradlew :app-bootstrap:compileTestJava BUILD SUCCESSFUL
./gradlew :app-bootstrap:test --tests '*ManagementActuatorSecurityContractTest' --tests '*ActuatorSecurityHttpTest' BUILD SUCCESSFUL
./gradlew :app-bootstrap:test BUILD SUCCESSFUL (full module, no regression)

Claims To Verify — 상태 업데이트 (2026-06-15)

Claim Status 변경
ArchUnit shape-ownership rule (D6) 실 구현 가능 여부 actually-implementedmanagement_security_does_not_depend_on_health_internals rule 작성됨, CleanArchitectureTest 통과 확인
show-details: always prod 차단 (D8) locally-verified — contract test health_show_details_is_when_authorized_not_always 통과
management port 분리 (D1) locally-verified — contract test management_server_port_defaults_to_9001_and_differs_from_app_port 통과
exposure allowlist (D2) locally-verified — contract test forbidden_endpoints_are_not_in_exposure_include_allowlist 통과
custom SecurityFilterChain 에서 actuator path 가 permitAll vs authenticated 어디로 떨어지는지 (SB-ACT-EXP-C3 함정) locally-verifiedActuatorSecurityHttpTest HTTP 구동: health/info/prometheus 200, loggers 비인증 401, env 404
application.yml 의 실제 include/exclude/port/show-details/shutdown 값 (주입값이 아닌 artifact 고정) locally-verifiedapplication_yml_* 4개 테스트가 main application.yml 파싱·단언(teeth-check 로 regression 감지 확인)
loggers prod read-only (D2) — 인증된 caller 도 log level 변경 불가 actually-implemented + locally-verifiedPOST /actuator/loggers/** denyAll() + DELETE /actuator/loggers/** denyAll(), loggers_write_is_denied_even_for_authenticated_caller (POST 403) + loggers_reset_via_delete_is_denied (DELETE 403) + loggers_read_is_allowed_for_authenticated_caller (200)
show-details: always prod 차단 (D8) — tautology 제거, real pin test 만 유지 locally-verifiedapplication_yml_pins_show_details_when_authorized_and_shutdown_disabled 가 main application.yml 파싱·단언(진짜 regression guard). tautological health_show_details_is_when_authorized_not_always 삭제됨 (2026-06-15 post-review).

Non-goals (this task) — OUT_OF_BRANCH_SCOPE

  • Health endpoint GROUPS (management.endpoint.health.group.*) — runtime-health branch owns
  • Runbook stub bodies (docs/runbooks/management-actuator-forbidden.md) — operational-runbook branch owns
  • adapter-web 변경 없음 (existing SecurityConfig untouched — actuator chain is additive)

오류 기록 (본 feature 작업 중 발생)

  • Spring Security compile classpath 문제 (해결됨 — §마주친 문제 참조)
  • test dual-port 방지 (해결됨 — §마주친 문제 참조)

면접 준비 (이 작업에서 나올 수 있는 면접 질문)

  • [[raw/interviews/actuator-security-management-port-interview]] — "Spring Boot actuator를 별도 포트로 분리하는 이유와 SecurityFilterChain 순서 제어(Order) 방법"

블로그·채용공고 연계 글감

  • [[raw/blog-topics/spring-actuator-security-separate-port-archunit]] — "Spring Boot actuator 별도 포트 + ArchUnit으로 health shape-ownership 경계 강제하기"

관련 일일 노트

  • [[raw/daily-notes/2026-06-15]]

완료 후 정리

  • PR 링크:
  • 리뷰 메모:
  • 머지 결과 / 배포 환경: locally-verified (worktree — rebase 후 통합 예정)
  • wiki 추출 대상 (verified만, wiki/projects/로만 추출):
    • actually-implemented 항목: ACTUATOR_FORBIDDEN enum, ManagementSecurityConfig (@Order(0) + 401 entry point + loggers POST denyAll + DELETE denyAll), ArchUnit rule, contract test (8, tautology 1개 삭제 후) + HTTP 통합 테스트 (ActuatorSecurityHttpTest, 8, loggers_reset_via_delete_is_denied 추가)
    • locally-verified 항목: management port separation, exposure policy(application.yml artifact 고정), show-details (real pin test only — tautology removed), env-key gate, HTTP 보안 posture(probe 200 / 비인증 401 / excluded 404), loggers read-only(POST 403 + DELETE 403)
    • prod-verified 항목: (없음 — local worktree only)
  • 추출하지 않을 항목 (planned / documented-only / abandoned):
    • heapdump/threaddump non-prod admin role 절차 (D5 — planned, IAM branch 위임)
    • prometheus rate-limit carve-out (D7 — cross-branch gap, rate-limit branch 위임)