본문으로 건너뛰기

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)). 그 안에서 InternalTokenAuthenticationFilterX-Internal-TokenMessageDigest.isEqual로 상수 시간 비교한다. 기본값이 닫힘이라 새 경로는 자동으로 보호된다.
  • 토큰이 빈 문자열이면 기동을 실패시킨다.
  • 계약에는 apiKey security 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 반영 이슈를 신설하고 배포 순서 경고를 남겨야 했다.