LabHub
배우기 러닝패스 코스

FDE 캡스톤: 창고가 같은 주문을 세 번 받았다 · 밖으로 못 나가는 로그와 지원 번들 · 이론

밖으로 못 나가는 로그 — 고객이 직접 돌리는 지원 번들

LabHub 에서 이어서 보기

한 줄 요약

로그를 밖으로 보낼 수 없는 고객에게 필요한 것은 "로그 좀 보내 주세요" 가 아니라, 고객이 자기 서버에서 돌리고 자기 눈으로 검토한 뒤 승인할 수 있는 수집기다. 허용 목록으로 모으고, 가린 건수를 적고, 같은 입력이면 같은 바이트가 나오게 묶어야 보안 담당자가 승인할 수 있다.

왜 이게 필요했나

장애 사흘째, 고객 운영팀이 말했다. "로그는 사내 규정상 밖으로 못 나갑니다. 필요한 걸 정리해 주시면 저희가 뽑아 드릴게요." FDE 가 파일 이름 몇 개를 적어 보냈고, 돌아온 압축 파일에는 설정 파일 원본이 들어 있었다. DB 비밀번호와 결제 API 키가 그대로였다. 고객 보안팀은 그 파일이 나간 사실을 알고 나서 다음 번들부터 전부 사전 검토하겠다고 했고, 검토에 이틀씩 걸리기 시작했다.

문제는 사람이 아니라 절차였다. 무엇을 넣을지를 매번 사람이 고르면 매번 다르게 고르고, 무엇을 가렸는지 적어 두지 않으면 검토자는 파일을 처음부터 끝까지 읽을 수밖에 없다. 검토자가 빨리 승인하려면 세 가지가 보여야 한다. 무엇이 들어갔는가(허용 목록), 무엇을 몇 건 가렸는가(manifest), 지금 받은 파일이 검토한 그 파일인가(해시). 이 셋을 사람 대신 스크립트가 매번 똑같이 만들어 주는 것이 지원 번들 수집기다.

어떻게 동작하나

1. 허용 목록으로 모은다. "비밀이 든 파일을 빼고 전부" 는 차단 목록이고, 차단 목록은 모르는 파일을 통과시킨다. 판(VERSION), 설정(etc 아래), 현재 로그(logs 바로 아래 *.log), 서비스의 환경처럼 넣을 것만 이름으로 정한다. 회전된 app.log.1, 고객 데이터 디렉터리, 심볼릭 링크는 규칙에 없으니 자연히 빠진다. 링크를 따라가면 서버 밖의 파일(예를 들어 호스트의 인증 파일)이 딸려 들어올 수 있다.

2. 환경은 셸이 아니라 서비스의 것을 읽는다. 수집기를 돌린 셸의 환경 변수는 장애와 무관하다. 리눅스에서는 [proc_pid_environ(5)](https://man7.org/linux/man-pages/man5/proc_pid_environ.5.html) 이 설명하는 /proc/<pid>/environ 에 프로세스가 시작될 때의 환경이 NUL 바이트로 구분돼 들어 있다. 같은 문서가 짚듯 시작 뒤에 프로세스가 스스로 바꾼 값은 반영되지 않는다. 순서가 실행마다 달라질 수 있으니 줄로 바꾼 뒤 이름순으로 정렬해 둔다.

3. 가림은 좁고 정확하게. 규칙이 넓으면 password_min_length: 12token_ttl_sec: 3600 같은 설정까지 사라져 번들이 쓸모없어지고, 좁으면 비밀이 샌다. 그래서 "키 이름이 password·secret·token·api_key 로 끝나는 값", "Bearer 뒤의 값", "도메인이 있는 이메일", "BEGIN 부터 END 까지의 개인키 블록" 처럼 모양으로 정한다. 순서도 중요하다. 개인키는 여러 줄이라 줄 단위 규칙보다 먼저 블록 통째로 바꿔야 한다. 가린 자리에는 [REDACTED:token] 처럼 종류를 남긴다. 검토자는 표식을 세어 manifest 의 숫자와 맞춰 볼 수 있고, FDE 는 "여기 토큰이 있었다" 는 사실만으로도 원인을 좁힐 수 있다.

4. 가린 뒤에 자른다. 로그는 크니 파일마다 상한을 두고 최근 줄만 남긴다. 먼저 자르고 나중에 가리면, 잘린 경계에 걸친 개인키 블록은 BEGIN 줄이 사라져 규칙에 걸리지 않고 본문만 남는다. 줄 중간에서 자르지 않는 것도 같은 이유다.

5. manifest 에는 결정적인 값만. 파일마다 경로, 크기, sha256, 가림 건수, 잘림 여부를 적는다. 생성 시각을 넣고 싶어지지만, 넣는 순간 같은 입력으로 만든 번들이 매번 달라진다.

6. 같은 입력이면 같은 바이트. [reproducible-builds.org 의 아카이브 문서](https://reproducible-builds.org/docs/archives/)는 tar 가 수정 시각, 파일 순서, 소유자 이름과 번호, 권한을 기록해 결과가 흔들린다고 정리하고, GNU tar 에 --sort=name(1.28 부터), --mtime, --owner=0 --group=0 --numeric-owner 를 권한다. 파이썬으로 묶는다면 [tarfile 문서](https://docs.python.org/3/library/tarfile.html)의 TarInfo 에서 mtime·uid·gid·uname·gname·mode 를 직접 정할 수 있고, 한 겹 더 있다. [gzip 문서](https://docs.python.org/3/library/gzip.html)는 GzipFile 의 mtime 을 주지 않으면 현재 시각이 머리에 들어가고, 시간에 의존하지 않는 결과가 필요하면 mtime=0 을 주라고 적는다.

아래는 이 실습 이미지에서 1초 기다리며 원본의 mtime 만 바꾼 뒤 다시 묶어 sha256 을 비교한 결과다(실측: GNU tar 1.35, gzip 1.12, Python 3.12.3).

| 묶는 방법 | 두 번 묶은 sha256 |
| --- | --- |
| tar -czf 옵션 없이 | 달라짐 |
| tar --sort=name --mtime=@0 --owner=0 --group=0 --numeric-owner -czf | 같음 |
| Python tarfile.open(..., "w:gz") 로 디렉터리 add | 달라짐 |
| TarInfo 의 시각은 고정했지만 GzipFile 에 mtime 을 주지 않음 | 달라짐 |
| TarInfo 의 시각·소유자·권한 고정 + GzipFile(mtime=0) | 같음 |

GNU tar 의 -z 는 실측에서 gzip 머리 시각이 0 으로 들어갔다. 반면 Python 의 w:gz 는 머리에 현재 시각이 들어가 파일 내용이 같아도 해시가 달라졌다.

support-bundle/  VERSION  env.txt            ← run/environ 을 줄로 바꿔 정렬  etc/...            ← 가린 설정  logs/app.log       ← 가린 뒤 최근 줄만  manifest.json      ← path·size·sha256·redactions·truncated

현장에서 만나는 모습

고객 보안 담당자와의 대화가 달라진다. 예전에는 "이 파일에 민감 정보가 없다는 걸 어떻게 보장하나요?" 에 답할 말이 없었다. 수집기가 있으면 "허용 목록은 이 여덟 줄이고, 가림 규칙은 이 네 가지이며, 이번 번들에서는 토큰 33건과 이메일 45건을 가렸습니다. 받은 파일의 sha256 은 이 값입니다" 라고 말할 수 있다. 검토자는 표식 수를 세어 보고, 해시를 비교하고, 승인한다.

고객마다 추가로 가려야 할 것도 나온다. 사번, 사내 호스트 이름, 내부 티켓 번호 같은 것이다. 이것을 수집기 코드에 박으면 고객마다 다른 판을 관리하게 되고, 고객이 규칙을 직접 고칠 수도 없다. 규칙 파일로 받아 적용하고, 그 종류도 manifest 의 건수에 합쳐 보여 주는 편이 오래간다.

실무에서 진짜 중요한 것

다음 실습에서 할 것

재료로 주는 고객 서버 사본에서 수집 범위를 정하고, 가림 필터와 수집기를 차례로 키운다. manifest, 로그 크기 상한, 결정적 tar.gz, 고객 규칙 파일을 한 단계씩 더하고, 마지막에 재료로 전달 번들과 해시·가림 보고서를 만든다. 채점기는 매번 비밀을 새로 심은 서버 트리로 여러분의 수집기를 직접 돌려 본다.