# task-2980 Phase 3 — 시크릿 노출 실측 감사 (아르고스 직접 재검증)

- 검증자: 아르고스 (테스터/보안 검증, 코드 수정 없음)
- 검증일: 2026-08-20
- 대상: worktree `/home/jay/.worktrees/task-2980-dev1` (branch `task/task-2980-dev1`, HEAD=`1b8d0eb`), 원본 `/home/jay/projects/insuwiki`, 원격 `origin` = `git@github.com:JonghyukJeon/InsuWiki.git`
- ★ 선행 보고(ANU/dev1)를 승계하지 않고 전 항목 **직접 명령 실행**으로 재측정함. 값은 이 문서 어디에도 없음(전부 sha256 앞12자로만 표기).

## V-1: 현재 브랜치(worktree) HEAD 실측

```
git ls-files | grep -c '\.pyc$'          → 0
git ls-tree -r HEAD --name-only | grep -c '\.pyc$'  → 0
git ls-tree -r HEAD --name-only | wc -l  → 13742 (전체 추적 파일)
```

전 파일(13,742개, 확장자 무관, `git grep -a`로 바이너리 포함) 대상 시크릿 패턴 스캔 결과:

| 패턴 | 매치 파일수 | 판정 |
|---|---|---|
| `AIza[0-9A-Za-z_-]{35}` (Google API key) | 2 | **거짓양성** — `functions/src/crawlYoutubeChannels.ts` / `functions/lib/crawlYoutubeChannels.js` 의 `INNERTUBE_PUBLIC_WEB_KEY`. 코드 주석에 "YouTube InnerTube WEB 클라이언트의 **공개** API 키... 비공개 자격증명이 아니다 (회전 대상 아님)"로 명시. YouTube 자사 JS가 그대로 배포하는 공개 상수 — 프로젝트 시크릿 아님. |
| `sk-[A-Za-z0-9]{20,}` | 1 | **거짓양성** — `docs/specs/260219-project-spec.md:193`, UI 목업 박스그림 안 placeholder 문자열(`[sk-...]`), 실제 키 아님. |
| `ya29.` / `1//0` / `GOCSPX-` / `sk-ant-` / PEM PRIVATE KEY / Telegram 토큰 | 0 (현재 HEAD) | 없음 |

**HEAD 평문 시크릿 개수 = 0건.** (추적 .pyc 0건, 진짜 시크릿 문자열 0건 — 거짓양성 2건 제외)

실행 명령 예:
```
git ls-tree -r HEAD --name-only | grep -c '\.pyc$'
git grep -a -E -l '<pattern>' HEAD -- .
```

## V-2: `.gitignore` 실효성 검증

`.gitignore` 45~58행 실측:
```
# Python bytecode / test cache (never commit compiled artifacts — may embed literal secrets)
__pycache__/
*.py[cod]
*$py.class
.pytest_cache/
```
→ `__pycache__/`, `*.py[cod]`, `.pytest_cache/` 전부 존재. 누락 없음.

`git check-ignore -v` 실제 출력:
```
$ git check-ignore -v scripts/youtube-pipeline/youtube_pipeline/__pycache__/config.cpython-312.pyc
.gitignore:54:__pycache__/	scripts/youtube-pipeline/youtube_pipeline/__pycache__/config.cpython-312.pyc
```
exit=0 (매치 확인).

디스크 상태: `find . -name '*.pyc' | grep -v node_modules | wc -l` → 44개 (여전히 디스크엔 존재, 정상 — 컴파일 산출물). `git status --porcelain | grep -i pyc` → **빈 출력**(untracked로도 노출 안 됨, ignore 정상 작동).

→ **.gitignore 보강 불필요** (V-6 스킵, 파일 미수정).

## V-3: published 히스토리 생존 여부 실측

- 기본 브랜치: `git remote show origin` → `HEAD branch: master`
- `git fetch origin` 성공, `origin/master` = `d5859a94ab941d65e0b331245d75e4f1b3e0a38c`
- `git ls-tree -r origin/master --name-only | grep -c '\.pyc$'` → **36건** (선행보고와 일치)
- 도입 커밋: `git log --all --oneline --diff-filter=A -- '*config.cpython-312.pyc'` → `d8aa4c1` (2026-04-08 13:18:41 +0900, "패키지 업데이트 + verify-underline-tags + youtube-pipeline 스크립트 추가")
- Ancestor 판정:
  - `git merge-base --is-ancestor d8aa4c1 origin/master` → **YES** (published 조상 확정)
  - `git merge-base --is-ancestor d8aa4c1 HEAD` → YES (worktree도 동일 조상 보유, 당연 — 히스토리 공유)
  - `git merge-base --is-ancestor bbfdc41 origin/master` → **NO** (untrack 수정 커밋은 아직 origin/master에 없음 → 머지해도 과거 커밋 자체는 안 지워짐)

### ★★ 신규 발견 (선행 보고에 없던 항목) — 전체 트리 스캔으로 드러남

선행 보고는 `.pyc` 만 언급했으나, 지시된 대로 **파일 확장자로 거르지 않고** origin/master 전체 트리를 스캔한 결과, 동일한 4개 시크릿 값이 **컴파일 산출물(.pyc)뿐 아니라 원본 소스 파일에도 평문으로 존재**함을 확인:

```
scripts/youtube-pipeline/youtube_pipeline/config.py   (origin/master blob 683f3dd5)
```
이 파일의 `os.getenv("KEY", "<하드코딩값>")` 형태 기본값 안에 4개 시크릿이 그대로 박혀있음 (line 14, 18, 33, 37).

sha256(12) 대조 결과 **`.pyc`와 `.py` 양쪽의 값이 완전히 동일**함을 확인(같은 4개 시크릿, 별개 유출 아님):

| 위치 | AIza#1 | AIza#2 | GOCSPX | 1//0 |
|---|---|---|---|---|
| `__pycache__/config.cpython-312.pyc` (origin/master) | a89c386ab5e3 | c50d0068dcff | 17bb444d7e36 | f3300debddf9 |
| `config.py` (origin/master, 소스) | a89c386ab5e3 | c50d0068dcff | 17bb444d7e36 | f3300debddf9 |

★ `c50d0068dcff` = 메모리에 기 기록된 GEMINI_API_KEY 해시와 **일치** — 교차검증 성공(같은 값을 독립적으로 재발견).

**중요: 현재 worktree HEAD(`config.py` blob `588a27b7`)는 이미 클린**(`os.getenv` 기본값이 전부 제거되고 "★ 하드코딩 금지" 주석으로 대체됨, 커밋 `bbfdc41`에서 `.pyc` untrack과 함께 처리됨). 즉 이 worktree 브랜치 안에서는 `.pyc`뿐 아니라 `.py` 소스도 이미 수정 완료 상태.

→ **결론: 머지해도 해소되지 않는 published 노출 = 2건**(같은 4개 시크릿 값을 담은 blob 2개: `.pyc` + `.py`). 두 blob 모두 `d8aa4c1`이 조상이며 origin/master의 과거 커밋 히스토리에 여전히 살아있음. **머지는 현재 HEAD 상태를 반영할 뿐 과거 커밋의 blob을 삭제하지 않음** — history rewrite(BFG/filter-repo)는 금지 지시대로 미실행.

## V-4: 로테이션 대상 목록 (값 없음, sha256 앞12자만)

| 키 종류 | sha256(12) | 노출 위치 | 생존 ref | 로테이션 필요 |
|---|---|---|---|---|
| YOUTUBE_API_KEY | `a89c386ab5e3` | `scripts/youtube-pipeline/youtube_pipeline/__pycache__/config.cpython-312.pyc` + `config.py` (동일 값) | `origin/master` (d8aa4c1 조상, published) | **예** |
| GEMINI_API_KEY | `c50d0068dcff` | 상동 | `origin/master` (published) | 예 — 단, 메모리 기록상 이미 폐기(400 API_KEY_INVALID)됨으로 보고되어 있음. **키 값을 쓰지 않고는 실제 폐기 여부를 재확인할 수 없어 이 항목만 "폐기 보고됨(미확인 직접검증)"으로 표기** |
| DRIVE_CLIENT_SECRET | `17bb444d7e36` | 상동 | `origin/master` (published) | **예** |
| DRIVE_REFRESH_TOKEN | `f3300debddf9` | 상동 | `origin/master` (published) | **예** |

거짓양성(로테이션 불필요, 참고용):
- `INNERTUBE_PUBLIC_WEB_KEY` (functions/src·lib/crawlYoutubeChannels.*) — YouTube 자사 공개 상수, 코드 주석에 회전대상 아님 명시
- `docs/specs/260219-project-spec.md:193` 의 `sk-...` — UI 목업 placeholder 텍스트

## V-5: 보고서

경로: `/home/jay/workspace/memory/reports/task-2980-phase3-secrets.md`
`wc -l` = (본 파일 자체 — 아래 실행 결과 참조)

## 머지 판정 (코드 평문 제거 관점)

**worktree 브랜치(`task/task-2980-dev1`) 자체는 머지 가능** — HEAD에 추적 `.pyc` 0건, `config.py` 소스도 이미 env-var 전용으로 정리됨, 진짜 시크릿 패턴 0건(거짓양성 2건 제외), `.gitignore` 규칙 완비.

**단, 남은 차단 요인(코드 머지와 별개, 반드시 병행 처리 필요):**
1. **published `origin/master`에 동일 4개 시크릿이 2개 blob(.pyc + 소스 .py)로 이미 생존** — 이 브랜치를 머지해도 과거 커밋의 blob은 지워지지 않으므로 **키 로테이션은 머지 여부와 무관하게 별도 실행 필수** (V-3 결론).
2. **history rewrite(BFG/filter-repo)는 본 작업 범위 밖**(파괴적 작업, 회장 승인 필요) — 로테이션이 완료되기 전까지는 과거 커밋에 남은 값 자체가 계속 유효한 자격증명으로 취급되어야 함.
3. GEMINI_API_KEY의 실제 폐기 여부는 값을 쓰지 않고는 직접 재검증 불가 — "폐기 보고됨" 상태로만 확인, 회장/담당자의 콘솔 재확인 필요.

## 미해결 항목
- GEMINI_API_KEY 실제 폐기 여부 직접 재검증 불가(값 미노출 원칙상 API 호출 테스트 불가) → "미확인, 선행보고만 존재"로 남김
- YOUTUBE_API_KEY / DRIVE_CLIENT_SECRET / DRIVE_REFRESH_TOKEN 로테이션 여부는 실행되지 않음(아르고스는 검증만, 로테이션 실행은 별도 담당)
- history rewrite 필요성 여부 최종 판단은 회장 승인 사안

---
★★ 본 보고서 작성 및 위 모든 터미널 출력 과정에서 시크릿 원문 값은 단 한 글자도 표시되지 않았음을 확인함. 모든 매치는 sed 치환으로 즉시 redaction 처리되었고, 값이 필요한 경우 sha256 해시로만 파이프 처리하여 화면에 노출된 적 없음.
