425 lines
41 KiB
Markdown
425 lines
41 KiB
Markdown
---
|
|
title: branch / feature-management-actuator-security-contract
|
|
source_type: branch-note
|
|
status: raw
|
|
id: BR-CA-SKELETON-OPERATIONAL-CONTRACT-021
|
|
kind: project-work-item
|
|
project: ca-skeleton-operational-contract
|
|
work_item: WI-CA-SKELETON-OPERATIONAL-CONTRACT-021
|
|
inherits: [DEC-CA-SKELETON-OPERATIONAL-CONTRACT-STACK-FRAMEWORK-001@1]
|
|
refines: []
|
|
overrides: []
|
|
depends_on: []
|
|
contract_packet: 1
|
|
branch: feature-management-actuator-security-contract
|
|
parent_branch:
|
|
governing_docs: [wiki/projects/ca-tmpl/security-baseline-jwt-actuator-secrets]
|
|
related_projects: [ca-skeleton]
|
|
tags: [branch, ca-skeleton, actuator, management, security]
|
|
created: 2026-05-22
|
|
target_merge:
|
|
status_label: in-progress
|
|
contract_packet_sha256: 8efcccc3f1adafbda15730eb0506dc851a4177a83e02825fb0928f61536bf939
|
|
---
|
|
|
|
# branch: feature-management-actuator-security-contract
|
|
|
|
> Layer: `raw/branch-notes/` — actuator/management endpoint 노출 보안 기준을 정의합니다.
|
|
|
|
<!-- section-id: branch-parent -->
|
|
## 부모 (필수)
|
|
|
|
[[raw/project-notes/ca-skeleton-operational-contract]]
|
|
|
|
<!-- GENERATED: branch-contract:start -->
|
|
<!-- section-id: branch-contract-packet -->
|
|
## 브랜치 계약 패킷
|
|
|
|
- **생성 시 프로젝트 개정**: `1`
|
|
- **패킷 스키마**: `contract_packet: 1`
|
|
- **완료 조건**: management endpoint exposure·authorization test가 통과한다
|
|
|
|
<!-- section-id: inherited-project-decisions -->
|
|
### 상속한 프로젝트 결정
|
|
|
|
| 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]] |
|
|
|
|
<!-- section-id: branch-local-decisions -->
|
|
### 브랜치 지역 결정
|
|
|
|
> 기존 branch-local 결정은 아래 `## Decision Evidence Map / 결정-근거 매핑`의 D-row가 소유하며 이 packet에서 복제하지 않는다.
|
|
|
|
| Decision ID | Decision | Relation | Supporting Claims | Status |
|
|
|---|---|---|---|---|
|
|
|
|
<!-- section-id: declared-overrides -->
|
|
### 선언한 예외
|
|
|
|
| Override ID | Overrides | Reason | Approval | Status |
|
|
|---|---|---|---|---|
|
|
|
|
없음.
|
|
<!-- GENERATED: branch-contract:end -->
|
|
|
|
<!-- section-id: branch-goal -->
|
|
## 목표
|
|
|
|
actuator는 운영에 필수지만 잘못 노출되면 env, config, metric, health detail이 공격 표면이 됩니다. skeleton은 management endpoint allowlist와 profile별 노출 정책을 가져야 합니다.
|
|
|
|
- 이슈:
|
|
- PR:
|
|
|
|
<!-- section-id: branch-scope -->
|
|
## 범위
|
|
|
|
### 포함 범위
|
|
|
|
- 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.
|
|
|
|
- **채택 결정 (separate management port + prod allowlist)**:
|
|
- [[raw/official-docs/actuator-endpoint-exposure-spring-official]] — Spring 공식 default + exposure 정책
|
|
- [[raw/official-docs/actuator-management-port-spring-official]] — Spring 공식 separate port 권고
|
|
- **검토한 대안**:
|
|
- **대안 1: Single port + path ACL** — cloud ingress 환경에서 합리적; ca-tmpl이 "platform ingress 보호 문서화 시" 허용으로 포섭
|
|
- **대안 2: mTLS for management endpoints** — [[raw/official-docs/security-mtls-rfc-8705]] (zero-trust 권장이나 cert 운영 부담)
|
|
- **대안 3: Network ACL only** — ca-tmpl baseline 선택 (단순 + 충분)
|
|
- **대안 4: Service mesh sidecar auth (Istio)** — [[raw/official-docs/actuator-istio-sidecar-management-alt]] (mesh 가정이 강함, skeleton 중립성 손실)
|
|
- **비교 핵심**: separate port 9001은 cloud-native + skeleton 중립성 우선. mTLS는 cert 부담, Istio는 mesh 종속. Single port는 platform ingress 보호 시 명시적으로 허용 — escape hatch 보유.
|
|
|
|
**후속 보강 (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.yml` 에 `management:` 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). 이를 위해 `ManagementSecurityConfig` 에 `HttpStatusEntryPoint(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 검증.
|
|
- **다른 계약 의존**:
|
|
- [[raw/branch-notes/feature-runtime-health-lifecycle-contract]] `D2` 에 의존 — health endpoint *shape* owner. 본 branch 는 exposure/auth 만. shape(`/actuator/health/{liveness,readiness,startup}` sub-path)가 바뀌면 allowlist 의 health 항목 영향.
|
|
- [[raw/branch-notes/feature-secrets-config-source-contract]] 에 의존 — actuator 출력 내 secret masking(`db-password-no-leak-in-actuator`, `datasource-username/url-masked-in-actuator`). env/configprops 부분 노출 시 masking 은 이 owner.
|
|
- [[raw/branch-notes/feature-env-driven-runtime-configuration]] 에 의존 — `APP_SERVER_PORT`(8080) owner. 본 branch 의 `MANAGEMENT_SERVER_PORT`(9001) 와의 분리 전제. consume only.
|
|
- [[raw/branch-notes/feature-rate-limit-idempotency-contract]] 에 의존(미해소 gap) — prometheus scrape rate-limit 예외. 현재 그 branch 가 정의 안 함(D7).
|
|
|
|
## 테스트 계약
|
|
|
|
- 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-pattern]] 의 `AUCP-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-web` 이 `spring-boot-starter-security` 를 `implementation` (not `api`) 으로 선언 → `app-bootstrap` 의 compile classpath 에 security 타입 없음.
|
|
- 시도: `ManagementSecurityConfig` 가 `HttpSecurity`, `SecurityFilterChain` 을 import → compileJava 실패 (6 errors).
|
|
- 해결: `app-bootstrap/build.gradle` 에 `implementation '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.yml` 에 `management.server.port=0` 오버라이드 추가 (random port). 계약 테스트는 `ApplicationContextRunner` (no live server) 로 properties 검증 — 포트 충돌 없음.
|
|
|
|
## 묶음
|
|
|
|
<!-- GENERATED: sources:start -->
|
|
- [[raw/company-tech-blogs/security-toss-actuator-healthcheck]]
|
|
- [[raw/company-tech-blogs/security-woowahan-actuator-safe-usage]]
|
|
- [[raw/official-docs/actuator-endpoint-exposure-spring-official]]
|
|
- [[raw/official-docs/actuator-istio-sidecar-management-alt]]
|
|
- [[raw/official-docs/actuator-management-port-spring-official]]
|
|
- [[raw/official-docs/runtime-health-spring-actuator-groups]]
|
|
- [[raw/official-docs/security-authorization-cheatsheet-owasp]]
|
|
- [[raw/official-docs/security-jwt-rfc-7519-validation]]
|
|
- [[raw/official-docs/security-mtls-rfc-8705]]
|
|
<!-- GENERATED: sources:end -->
|
|
|
|
> Phase C2 실 코드 작성 완료 (2026-06-15). 아래 항목 실제 구현됨.
|
|
|
|
### Implemented (2026-06-15) — actually-implemented + locally-verified
|
|
|
|
- `src/shared-contract/.../error/OperationalError.java` — `ACTUATOR_FORBIDDEN(Category.AUTHZ, 403, false)` 상수 추가 (D2 registry 정합)
|
|
- `src/app-bootstrap/build.gradle` — `spring-boot-starter-actuator`, `micrometer-registry-prometheus`, `spring-boot-starter-security` 추가
|
|
- `src/app-bootstrap/src/main/resources/application.yml` — `management:` 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.yml` — `management.server.port: 0` 오버라이드 (test dual-port 방지)
|
|
- `src/.env` — `MANAGEMENT_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.java` — `management_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.java` — `DELETE /actuator/loggers/**` denyAll() 추가 (POST 와 나란히). logger-level reset 도 write mutation — POST 단독 차단은 불완전했음.
|
|
- `ActuatorSecurityHttpTest.java` — `loggers_reset_via_delete_is_denied` 테스트 추가 (DELETE /actuator/loggers/dev.caskeleton → 403). `delete` MockMvcRequestBuilders import 추가.
|
|
- `ManagementActuatorSecurityContractTest.java` — `health_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-implemented` — `management_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-verified` — `ActuatorSecurityHttpTest` HTTP 구동: health/info/prometheus 200, loggers 비인증 401, env 404 |
|
|
| application.yml 의 실제 include/exclude/port/show-details/shutdown 값 (주입값이 아닌 *artifact* 고정) | `locally-verified` — `application_yml_*` 4개 테스트가 main `application.yml` 파싱·단언(teeth-check 로 regression 감지 확인) |
|
|
| loggers prod read-only (D2) — 인증된 caller 도 log level 변경 불가 | `actually-implemented` + `locally-verified` — `POST /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-verified` — `application_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 위임)
|