헤더를 가방처럼 다루기
HTTP 로깅 프레임워크는 요청 헤더를 단순한 키-값 쌍의 가방으로 취급합니다. 로깅 API는 전체 가방을 그대로 노출합니다. 운영자는 디버깅을 위해 헤더 로깅을 활성화합니다. 요청이 실패하면 헤더가 문제를 알려주기 때문입니다. 내장된 거부 목록도 없고, 문서에도 자격 증명 필터링에 대한 내용이 없습니다. 전체 헤더가 디스크에 기록됩니다.
일반적인 요청에 포함된 자격 증명 헤더:
- Authorization: Bearer eyJhbGciOiJIUzI1NiJ9... (JWT 또는 OAuth 토큰)
- Cookie: session=abc123; auth=xyz789
- X-API-Key: sk-live-abc123...
- X-Auth-Token: ghp_abc123... (GitHub personal access token pattern)
이 값들은 요청을 인증합니다. 로그 파일에 기록되면, 누구나 해당 요청을 인증할 수 있게 됩니다.
자격 증명 파이프라인
로그 파일에 기록된 자격 증명은 한 곳에 머무르지 않습니다. 다음과 같이 이동합니다:
1. 웹 서버가 /var/log/nginx/access.log에 기록합니다.
2. 로그 로테이션 에이전트(logrotate)가 /var/log/nginx/access.log.1로 복사합니다.
3. 로그 수집기(Fluentd, Filebeat, Logstash)가 읽어 집계기로 전송합니다.
4. 로그 수집기(Elasticsearch, Splunk, Datadog)가 인덱싱 및 저장
5. 기본 정책에 따라 30~90일간 보관
자격 증명은 다섯 곳 모두에 동시에 존재합니다. 세션 토큰을 취소해도 로그 수집기에서 자격 증명이 제거되지 않습니다. 보관 기간 동안 로그 접근 권한이 있는 누구나 검색·내보내기·접근할 수 있습니다.
노출 기간(The Exposure Window)
메모리 내 자격 증명의 노출 기간: max(세션 지속 시간, 프로세스 수명). 세션: 수시간~수일. 프로세스: 수시간~수주.
로그 내 자격 증명의 노출 기간: max(세션 지속 시간, 로그 보관 기간). 세션: 수시간~수일. 보관 기간: 30~90일.
메모리에서 탈취된 자격 증명은 공격자가 세션 기간 중에 있어야 했습니다. 로그에서 탈취된 자격 증명은 로그 수집기에 접근할 수만 있으면 보관 기간 전체에 걸쳐 소급하여 탈취할 수 있습니다.
MOAD-0003 vs MOAD-0004
MOAD-0003 (메모리 누출): 자격 증명이 메모리에서 잘못된 요청 핸들러로 누출됩니다. 스레드 풀을 통해 프로세스 실행 중에만 접근할 수 있으며, 일시적입니다.
MOAD-0004 (로그에 기록된 비밀): 자격 증명이 디스크에 저장되어 로그 순환, 로그 전송 및 로그 집계를 통해 유지됩니다. 로그 접근 권한이 있는 누구나 30~90일 동안 소급하여 접근할 수 있으며, 지속적입니다.
구조적 차이: 일시적(ephemeral) vs 지속적(persistent). 수정은 다른 계층에서 이루어집니다.
일시적 vs 지속적
일시적/지속적 구분은 위험 표면, 수정 계층 및 사고 대응 요구사항을 결정합니다.
직렬화 계층에서의 Credential Denylist
해결 방법: 직렬화 계층에 credential denylist를 적용하는 것입니다. 헤더 값이 로그 출력에 도달하기 전에 헤더 이름을 denylist와 비교합니다. 값은 [REDACTED]로 대체합니다.
CREDENTIAL_HEADERS = {
'authorization',
'cookie',
'x-api-key',
'x-auth-token',
'x-csrf-token',
'proxy-authorization',
}
def sanitize_headers(headers: dict) -> dict:
return {
k: '[REDACTED]' if k.lower() in CREDENTIAL_HEADERS else v
for k, v in headers.items()
}
Denylist는 로그 쿼리 계층이 아닌 직렬화 계층에 있어야 합니다. 로그 쿼리 난독화: 자격 증명이 디스크에 도달한 후에 적용되며, 원본 값은 그대로 존재하고 표시만 숨겨집니다. 직렬화 계층 난독화: 자격 증명이 디스크에 도달하지 않습니다. 원본 값은 로그 파일, 로그 전송기, 로그 수집기에 전혀 기록되지 않습니다.
Denylist 테스트
세 가지 테스트 패턴:
- Positive: Authorization: Bearer token123 헤더가 포함된 요청은 Authorization: [REDACTED]가 포함된 로그 항목을 생성합니다.
- Negative: Content-Type: application/json 요청은 값이 그대로 포함된 로그 항목을 생성합니다
- 대소문자 구분 없음: AUTHORIZATION: Bearer token123도 [REDACTED]를 생성합니다 (HTTP 헤더 이름은 대소문자를 구분하지 않습니다)
Denylist는 유지보수가 필요합니다: 새로운 자격 증명 헤더 패턴(예: 사용자 정의 X-Service-Auth 헤더)은 명시적으로 추가해야 합니다. 이 수정은 구조적이지만 자동으로 유지되지 않습니다.
Denylist 적용
한 팀이 프로덕션 장애를 디버깅하기 위해 Nginx 접근 로그 형식에 모든 요청 헤더를 포함하도록 구성했습니다. 구성:
log_format debug_format '$remote_addr - $request - $http_authorization - $http_cookie';
access_log /var/log/nginx/debug.log debug_format;
사고를 해결하고 디버그 설정을 제거하려고 했으나, 다음 배포 주기(7일 후) 전에 변경 사항이 프로덕션에 반영되지 않았습니다.