노출 검사와 비밀 교체
85분 안팎
학습 목표
비밀을 출력하지 않고 교체 결과를 확인합니다.
개념
미검출과 미노출은 다른 결론입니다
토큰을 로그에서 찾지 못했다고 모든 경로에서 노출이 없다고 결론내리지 않습니다. 검사한 파일 범위, 값의 표현, 압축 여부를 함께 적어야 합니다. 이번 레슨은 알려진 교육용 토큰의 정확한 바이트가 저장소·CI 산출물·이미지 계층·실행 로그·상태 파일에 있는지 검사합니다. 통과 결과는 그 범위에서 그 값이 발견되지 않았다는 증거입니다. 인코딩되거나 여러 조각으로 나뉜 값, 검사하지 않은 외부 캐시까지 안전하다는 증명은 아닙니다. 이 한계를 인계 문서에 남기는 것이 업무 품질입니다.
진단 자체가 노출을 만들 수 있습니다
grep으로 토큰을 검색하며 일치한 줄을 출력하면 검사 결과 파일에 비밀이 다시 남습니다. 명령 인자로 값을 쓰는 방식도 피합니다. 제공 scan.py는 비밀 파일 경로를 받고 내부에서 바이트를 읽은 뒤 미검출·노출·입력 오류 상태만 출력합니다. 일치한 원문은 출력하지 않습니다. 테스트는 검사기의 stdout와 stderr에 토큰이 포함되지 않는지 함께 확인합니다. 노출 위치 조사가 필요하면 보호된 환경에서 파일 경로와 단계로 범위를 좁히며 값을 채팅이나 CI 로그에 복사하지 않습니다.
일치 검색은 바이트로 수행합니다
토큰은 텍스트처럼 보여도 이미지 archive와 바이너리 산출물 안에 들어갈 수 있습니다. 모든 파일을 UTF-8 문자열로만 읽으면 디코딩 오류 때문에 일부 범위를 빠뜨립니다. scan.py는 read_bytes로 읽고 secret in data로 정확한 바이트 포함을 확인합니다. 줄 단위 텍스트 처리와 달리 숨김 파일도 Path.rglob으로 순회합니다. 오류 파일을 조용히 건너뛰지 않습니다. 이 교육 도구는 파일 전체를 메모리에 읽으므로 큰 운영 산출물에는 스트리밍 검사나 전용 도구가 필요하다고 범위를 표시합니다.
원본 비밀 파일은 검사 범위 밖에 둡니다
현재 토큰 원본을 검사하면 값이 있는 것이 당연하므로 외부 파일 자체는 검색 범위와 분리합니다. 원본이 검사 디렉터리 안에 있으면 scanner는 입력 오류를 냅니다. 예외를 위해 저장소의 token 파일만 몰래 제외하면 소스에 비밀이 들어간 실수를 숨길 수 있습니다. 경로가 없거나 심볼릭 링크가 있으면 검사 범위를 확정하기 어려워 오류로 닫습니다. 비밀 파일이 비었거나 너무 짧아도 검사 오류입니다. 빈 바이트열은 모든 파일에 포함되므로 정상 검색처럼 다룰 수 없습니다.
종료 상태를 세 가지로 읽습니다
CLI 종료 0은 정확한 토큰 미검출, 1은 노출 검출, 2는 입력이나 읽기 오류입니다. 검사 실행 실패를 노출 없음으로 바꾸면 CI가 손상된 증거로 다음 단계를 진행합니다. check.sh는 set -e로 예상하지 않은 실패를 차단하지만 테스트에서는 의도한 노출 사례의 exit 1을 직접 비교합니다. 정상 파일과 노출 파일 모두 검사해야 항상 PASS를 출력하는 구현을 잡을 수 있습니다. starter는 일치 판정을 비워 두었으므로 로그 노출 fixture에서 테스트가 실패하도록 설계했습니다.
일곱 사례로 검사기의 계약을 확인합니다
clean.log의 상태 코드와 [REDACTED]는 통과해야 합니다. 실제 토큰을 포함한 로그·바이너리·숨김 파일은 각각 검출해야 합니다. 없는 입력 디렉터리, 범위 안에 있는 비밀 원본, 빈 비밀은 검사 오류가 되어야 합니다. 이 일곱 사례는 정상 동작과 경계 실패를 함께 다룹니다. test_scan.py는 임시 디렉터리에서 공개 fixture를 만들고 끝나면 지웁니다. 실제 비밀을 fixture 코드 안에 넣는 테스트가 아니라 검사기를 검증하기 위한 공개 교육 자료라고 구분합니다.
이미지의 마지막 모습만 보지 않습니다
Dockerfile에서 COPY한 비밀을 다음 RUN에서 삭제해도 이전 계층에 값이 남을 수 있습니다. 실행 컨테이너의 최종 파일만 검색하면 그 계층을 놓칩니다. 미션 검사는 docker save로 자기 이미지 archive를 만들고 각 tar 계층의 파일 내용을 펼쳐 검색합니다. classic layer.tar와 tar 형식 OCI blob 후보를 다룹니다. 압축된 산출물은 먼저 풀어야 하고 최종 파일만 검사한 결과를 이미지 전체 검사라고 부르지 않습니다. 검사할 이미지 ID가 현재 배포된 ID와 같은지도 inspect로 확인합니다.
기록은 허용 목록으로 줄입니다
요청 로그는 승인한 경로 레이블과 상태 코드·처리 시간·요청 식별자만 기록합니다. 임의 URI는 other로 표시하고 쿼리와 인증 헤더를 덤프하지 않습니다. 토큰을 경로에 넣는 요청까지 그대로 기록하면 헤더를 숨긴 것만으로 충분하지 않습니다. 오류 메시지에도 HTTP 요청 객체나 환경 변수 전체를 넣지 않습니다. 요청 식별자는 비밀 값을 넣는 필드가 아니며 민감 자료를 식별자로 전달하지 않는 계약을 둡니다. 마스킹된 값은 원래 값이 로그 어딘가에 같이 남지 않았는지도 검사해야 합니다.
교체 검증은 새 성공과 옛 실패를 묶습니다
새 토큰이 200으로 성공하는 것만 확인하면 이전 토큰이 계속 유효한지 알 수 없습니다. 미션은 교체 전 옛 토큰 200, 교체 후 새 토큰 200, 옛 토큰 401을 확인합니다. 토큰 없는 요청과 임의의 잘못된 토큰도 401이어야 합니다. 비밀 파일을 잠시 치웠을 때 503이 나오는지 확인하여 파일 장애 때 공개 모드로 돌아가지 않는지도 검사합니다. evidence에는 값 대신 상태 코드만 남깁니다. 교체 성공은 기능 변경을 확인한 결과이고 바이트 검사는 자료 노출을 확인한 별도의 결과입니다.
원자적 교체와 읽기 주기를 연결합니다
새 값을 임시 파일에 완전히 쓰고 접근 모드를 맞춘 뒤 같은 디렉터리의 token 이름으로 rename합니다. 쓰는 도중 Java가 반쯤 된 파일을 읽을 가능성을 줄이는 방식입니다. 디렉터리 마운트를 유지하고 요청마다 파일을 읽는 교육 구현에서는 다음 요청에서 새 값을 읽습니다. 실제 서비스가 시작할 때 한 번만 읽으면 파일만 바꿔도 반영되지 않아 재로딩이나 재시작이 필요합니다. 적용 방식에 맞춰 검사 시점과 이전 값 폐기 시점을 정하며 일반적인 교체 명령이 모든 서비스에서 즉시 적용된다고 말하지 않습니다.
노출을 발견하면 삭제와 폐기를 구분합니다
실제 자격증명이 저장소나 산출물에 들어갔다면 파일을 삭제한 것만으로 이미 복사된 값을 회수할 수 없습니다. 노출된 값의 권한을 폐기하고 새 값으로 바꾸며 유출 경로와 영향을 조사해야 합니다. 교육용 값은 연습 범위에서만 사용하고 외부 시스템 권한을 갖지 않습니다. 이전 이미지 archive, CI 로그, 로컬 백업처럼 보관된 경로도 조사 대상으로 적습니다. 이번 scanner 통과를 실제 사고 대응 완료로 표현하지 않습니다. 누가 어떤 범위를 확인했고 어떤 값이 폐기됐는지 기록합니다.
후속 모듈에 값 대신 절차를 넘깁니다
다음 배포·롤백 모듈은 검증한 이미지 ID와 현재 비밀 파일 경로, 권한 표, 교체 절차를 이어받습니다. README나 evidence에 토큰 값을 넣지 않습니다. 미션은 현재 토큰과 프로젝트 자원을 남겨 이어 쓰되 임시 옛 토큰 사본과 펼친 이미지 계층은 종료 시 제거합니다. 서비스 데이터는 정리 전에 보존합니다. 최종 설명에는 로컬 scanner 일곱 건의 결과와 실제 Docker HTTP 검사의 외부 대기를 분리하여 남깁니다. 재현 가능한 검증 범위가 인계자의 판단을 돕습니다.
따라하기
출력 없는 노출 판정
공개 fixture를 바이트로 검사하고 값 대신 상태만 출력합니다.
token=b'lab-only-fixture-012345'
for data in [b'status=200 token=[REDACTED]',b'prefix '+token+b' suffix']:
print('EXPOSED' if token in data else 'CLEAN')실행 결과
CLEAN EXPOSED
로컬 검사기 완성
solution에서 직접 실행한 출력입니다. starter에서는 실패합니다. 노출 검사 zip의 scan.py에서 비워 둔 바이트 포함 판정을 고칩니다. check.sh는 7가지 경우와 진단 출력의 비밀 미포함을 확인합니다.
bash check.sh실행 결과
PASS 7 scanner cases; no secret in diagnostics
검사 범위 입력
token은 검색 디렉터리 밖에 있어야 합니다. 종료 0·1·2를 구분하고 값을 명령 인자로 쓰지 않습니다.
python3 scan.py /외부전용경로/token /검사할/디렉터리실제 교체 증거 확인
미션 check.sh를 완료하고 evidence/security.json에서 before 200·after 200·revoked 401·missing_file 503을 확인합니다. Docker 실행을 못 했다면 이 항목은 외부 대기로 남깁니다.
확인 문제
실습
scan.py의 정확한 바이트 일치 검사를 구현합니다. 로그·바이너리·숨김 파일 노출과 입력 오류를 구분하고 비밀 원문을 출력하지 않습니다. bash check.sh로 7건을 확인합니다. 실제 HTTP 토큰 교체는 이 로컬 검사와 별개로 미션 zip에서 확인합니다.
실행 명령
bash check.sh
기대 결과
PASS 7 scanner cases; no secret in diagnostics
모범 답안
모범 답안 내려받기더 읽기
면접 질문
- 비밀 교체가 완료되었다는 증거를 어떤 성공·실패 요청으로 남기나요?
- 노출 검사 통과가 모든 자료의 안전을 의미하지 않는 이유는 무엇인가요?