internal API 인증은 전용 필터 체인으로 기본 차단한다
- 상태: 채택됨
- 작성일: 2026-07-23
- 관련 문서: ADR-0008, APP-73, APP-121, APP-122
맥락
APP-121이 internal 조회 API에 X-Internal-Token 검증을 컨트롤러 메서드마다 심어 놨다. 기본값이
"열림"이라 새 엔드포인트를 추가하며 잊으면 조용히 무인증이 된다. 실제로 분석 콜백
(POST /internal/v1/callbacks/analyses/{id})이 무인증이었고 분석 결과 위조가 가능했다.
결정
securityMatcher("/internal/**")인 Security 필터 체인을 하나 더 둔다(@Order(1)). 그 안에서InternalTokenAuthenticationFilter가X-Internal-Token을MessageDigest.isEqual로 상수 시간 비교한다. 기본값이 닫힘이라 새 경로는 자동으로 보호된다.- 토큰이 빈 문자열이면 기동을 실패시킨다.
- 계약에는
apiKeysecurity scheme으로 표현한다. operation 파라미터로 내리지 않는다.
구현 중 세 가지가 반대로 확정됐다.
- 필터를
@Component빈으로 두지 않는다. 빈이면 Boot가 서블릿 필터로도 자동 등록해/internal밖에서도 돈다. 거기에 유효 토큰이 함께 오면 쿠키 인증 주체를 덮어쓴다. - 계약 정규화는 경로 목록이 아니라
/internal/접두사로 판정한다. 목록을 손으로 관리하면 "등록을 잊으면 어긋나는" 구조를 계약 쪽에 다시 만든다. - 인증을
SecurityContextHolder뿐 아니라 요청 저장소에도 저장한다.OncePerRequestFilter는 ASYNC 재dispatch에서 안 도는데AuthorizationFilter는 돈다. 그 틈에/internal로 SSE가 하나 생기면 정상 호출이 거부된다.
대안
대안 1 — 메서드별 검증 유지(APP-121 방식). 새 체인을 안 만들어도 되고 Security 설정이 하나로 남는다. 기각한 이유: 이미 한 번 실패했다. 기본값이 열림인 구조는 잊는 순간 뚫린다. 실제로 콜백이 뚫려 있었다.
대안 2 — 서블릿 필터 직접 등록. Security 스택을 안 건드린다. 기각한 이유: 401 본문을 손으로 써야 하고 다른 401과 형식이 갈라진다.
대안 3 — HandlerInterceptor. 컨트롤러 진입 직전이라 다루기 쉽다.
기각한 이유: 보안 이후에 돌아 늦다.
결과
- 기본값이 닫힘이라 새 internal 경로는 만들자마자 보호된다. 분석 결과 위조 경로가 막혔다.
InternalApiAccessHandler와 전용 예외, 해당 DocsTest가 통째로 삭제됐다.- Security 설정이 두 체인으로 갈렸다. 필터를 빈으로 두면 안 된다는 함정도 함께 안고 간다. 무심코
@Component를 붙이면/internal밖에서도 돌아 쿠키 인증 주체를 덮어쓴다. - 인증을
SecurityContextHolder와 요청 저장소에 이중으로 저장한다. - 계약이 바뀌어(콜백도 헤더 요구) 배포 순서 제약이 생겼다. server가 먼저 나가면 AI 콜백이 401을 받아 분석이 안 나온다. heymoa-ai 반영 이슈를 신설하고 배포 순서 경고를 남겨야 했다.