개발하다 보면 누구나 한번쯤 이런 순간을 겪는다.
"이 프로젝트 API 키가 어디 있었더라..."
backend/.env를 열어봐도 없고, .env.local에도 없고, 혹시 Docker Compose 파일에 직접 박아놨나 뒤지다 보면 어느새 10분이 지나있다. 더 심각한 경우엔 키를 찾아서 복사한 파일을 실수로 git에 커밋하기도 한다.
이 글은 이 문제를 해결하기 위해 만든 Claude Code 플러그인 개발기다. 기능 설명보다는 왜 이렇게 설계했는지, 그 과정에서 배운 보안 원칙들을 중심으로 쓴다.
문제: 시크릿은 왜 항상 흩어져 있나
프로덕션 환경의 시크릿 분포를 그려보면 대략 이렇다:
~/.claude.json → MCP 서버 토큰 (Slack Bot Token 등)
~/.tccli/ → Tencent Cloud 자격증명
~/elon/ssh-keys/ → SSH 키, 인증서, AWS 액세스 키
~/elon/credentials/ → Google Drive OAuth 토큰
project-a/backend/.env → DB 비밀번호, API 키, JWT 시크릿
project-b/app/.env.local → Keycloak 시크릿, S3 자격증명
project-c/instance/.env → SMTP, Google Sheets API
이게 나쁜 것이 아니다. 시크릿은 분산되어 있어야 한다. 한 곳에 몰아두면 그 곳 하나가 뚫렸을 때 전체가 터진다. 문제는 분산 자체가 아니라, 어디에 무엇이 있는지 추적하기 어렵다는 것이다.
흔히 취하는 해결책들:
- 노션/컨플루언스에 정리 — 관리 주체가 사람이라 즉시 stale해진다
- 팀 1Password/Vault — 비용이 들고, 개인 프로젝트엔 과한 오버킬
- 그냥 기억한다 — 프로젝트가 늘면 한계에 봉착한다
- 다 한 곳에 모은다 — 보안상 최악이다
내가 선택한 방법은 지도를 만드는 것이다.
설계 철학: 지도 vs 금고
이 플러그인의 핵심 아이디어는 단순하다.
시크릿을 모으지 않는다. 위치를 인덱싱한다.
secrets-index.md라는 파일 하나가 중앙 허브 역할을 한다. 하지만 이 파일엔 실제 값이 없다:
| Anthropic API Key | contents-hub | backend/.env → ANTHROPIC_API_KEY |
| Slack Bot Token | Claude Code | ~/.claude.json → mcpServers.slack.env |
| AWS Access Key | — | ~/elon/ssh-keys/elon_accessKeys.csv |
"어디 있는지"만 안다. 실제 값은 원래 있어야 할 곳에 그대로 있다.
이 방식의 장점:
- 인덱스 파일을 git에 커밋해도 안전하다. 값이 없으니까.
- 시크릿 파일들을 이동시킬 필요가 없다. 기존 구조를 유지한다.
- 새 팀원 온보딩이 쉬워진다. "이 파일 보면 다 있어요"가 가능해진다.
Claude가 /secrets anthropic을 받으면:
- 인덱스에서 "anthropic" 키워드를 검색한다
- 파일 경로(
backend/.env)를 찾는다 - 그 파일을 읽어서
ANTHROPIC_API_KEY=sk-ant-...값을 가져온다
사용자는 파일 경로를 외울 필요도, 찾아 헤맬 필요도 없다.
보안 개념 1: 시크릿 열거(Secret Enumeration)의 위험
재미있는 사실이 있다. git history를 뒤져보니 실제 API 키 값은 없었다. 경로와 변수명만 있었다. 그런데도 이걸 public repo에 올리면 문제가 될까?
그렇다. 될 수 있다.
이걸 "시크릿 열거(Secret Enumeration)"라고 한다. 값을 몰라도 무엇이 존재하는지를 알면 공격 표면이 드러난다:
| Tencent SecretKey | ~/.tccli/default.credential |
| AWS Access Key | ~/elon/ssh-keys/elon_accessKeys.csv |
| Slack Bot Token | ~/.claude.json |
이걸 보면 공격자는 알 수 있다:
- 이 사람은 Tencent Cloud를 쓴다 →
tccli설정 파일을 탈취하면 된다 - Slack Bot Token이
.claude.json에 있다 → 이 파일을 노리면 된다 - AWS 키가 CSV 파일에 있다 → 해당 경로를 탐색하면 된다
인덱스 파일 자체는 private repo에 두는 게 맞다. 이 플러그인도 private repo로 관리한다.
보안 개념 2: 대칭 암호화 vs 비대칭 암호화
백업에 GPG를 쓴다. GPG에는 두 가지 모드가 있다.
대칭 암호화 (gpg --symmetric)
gpg --symmetric --cipher-algo AES256 secrets.tar.gz
하나의 패스프레이즈로 암호화하고, 같은 패스프레이즈로 복호화한다. 간단하다.
암호화: 파일 + 패스프레이즈 → 암호문
복호화: 암호문 + 패스프레이즈 → 파일
현재 이 방식을 쓴다. 이유는 단순함 때문이다 — 혼자 쓰는 개인 백업이라 키 쌍 관리가 오버킬이다.
단점: 패스프레이즈가 노출되면 모든 백업이 위험하다. 그래서 패스프레이즈를 ~/elon/secrets-backup/.passphrase에 저장하고 (chmod 600), 인덱스에 등록해서 Claude로 조회할 수 있게 했다.
비대칭 암호화 (gpg --encrypt)
gpg --encrypt --recipient my@email.com secrets.tar.gz
공개키로 암호화하고, 개인키(+ 패스프레이즈)로만 복호화한다.
암호화: 파일 + 공개키 → 암호문
복호화: 암호문 + 개인키 + 패스프레이즈 → 파일
패스프레이즈가 노출돼도 개인키가 없으면 복호화할 수 없다. 더 안전하다. 팀 환경이라면 각 팀원의 공개키로 암호화하는 방식도 가능하다.
현재는 대칭을 쓰지만, 향후 비대칭으로 마이그레이션하는 것이 Phase 2 계획이다.
보안 개념 3: 무결성 검증 — SHA-256
암호화 백업을 Drive에 올려두는데, "이 파일이 원본 그대로인가?"를 어떻게 보장할까?
여기서 해시 함수가 등장한다. SHA-256은 어떤 크기의 데이터든 256비트(64자리 16진수) 해시값으로 변환한다:
shasum -a 256 secrets-2026-02-23T1225.tar.gz
# 454191680bb580e0d509e94edea4834483c02962f044429b936dad847acaec8d
해시의 특성:
- 결정론적: 같은 파일은 항상 같은 해시
- 단방향: 해시에서 원본 파일 복구 불가
- 눈사태 효과: 파일 1비트만 바뀌어도 해시가 완전히 달라짐
백업 시 secrets-YYYY-MM-DDT####.tar.gz.sha256 파일도 함께 Drive에 업로드한다. 복원 시에 이걸 내려받아 복호화된 tar.gz의 해시와 비교한다. 값이 다르면:
무결성 실패 — 파일이 변조되었을 수 있습니다.
라고 경고하고 복원을 중단한다. Drive가 해킹되거나, 전송 중 파일이 손상되거나, 누군가가 몰래 파일을 교체했을 때 이걸로 감지할 수 있다.
보안 개념 4: 최소 권한 원칙 (Principle of Least Privilege)
복원이 완료된 파일에 이런 코드가 실행된다:
chmod 600 <복원된_파일>
600의 의미:
소유자: 읽기 + 쓰기 (rw-)
그룹: 없음 (---)
기타: 없음 (---)
SSH 키나 .env 파일은 소유자만 읽고 쓸 수 있어야 한다. 644(그룹/기타 읽기 허용)나 777(모두 읽기/쓰기/실행)로 설정되어 있으면 같은 서버를 쓰는 다른 사용자가 키를 훔칠 수 있다.
실제로 SSH는 키 파일 권한이 너무 열려있으면 아예 접속을 거부한다:
@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
@ WARNING: UNPROTECTED PRIVATE KEY FILE! @
@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
Permissions 0644 for 'id_rsa' are too open.
"필요한 것만, 필요한 사람에게만" — 이게 최소 권한 원칙이다.
보안 개념 5: 실패 시 안전(Fail-Safe) 설계
백업 도중 네트워크가 끊기거나, gpg 암호화가 실패하거나, Drive 업로드가 중단되면? 스테이징 디렉토리에 평문 시크릿 파일이 그대로 남는다.
이걸 방지하기 위해 bash trap을 사용한다:
cleanup() {
echo "오류 발생 — 임시 파일 정리 중..."
rm -rf "$BACKUP_DIR/$DATE"
rm -f "$BACKUP_DIR/secrets-$DATE.tar.gz"
rm -f "$BACKUP_DIR/secrets-$DATE.tar.gz.sha256"
}
trap cleanup ERR
trap ERR는 스크립트 어디서든 에러가 발생하면 cleanup 함수를 실행한다. 어떤 단계에서 실패하든 민감한 임시 파일은 자동으로 정리된다.
정상 완료 시엔 trap - ERR로 trap을 해제해서 cleanup이 실행되지 않도록 한다.
이런 설계를 "실패 시 안전(Fail-Safe)"이라고 한다. 잘 됐을 때를 위한 코드뿐 아니라, 잘못됐을 때 피해를 최소화하는 코드를 항상 같이 작성해야 한다.
개발하면서 배운 인사이트들
git history는 절대 안전하지 않다
처음엔 "시크릿 값이 git history에 있다"는 이전 메모를 믿었다. 직접 git show 9386022로 확인해보니 경로와 변수명만 있었다.
교훈: 보안 결정은 항상 직접 검증하라. 메모나 전언을 그대로 믿지 말고, 실제로 확인해라. 이번엔 키 교체 불필요가 결론이었지만, 반대의 경우였다면 놓쳤을 수도 있다.
# 이렇게 직접 확인한다
git log --all --oneline
git show <commit-hash> -- <file> | grep -i "token\|secret\|key"
두 터미널 워크플로는 실제로 불편하다
백업 설계 초안은 이랬다:
- Claude가 스테이징 준비
- 사용자가 별도 터미널에서
bash backup-upload.sh실행 - 완료 후 Claude에게 돌아와 결과 확인
이게 생각보다 많이 불편하다. Claude와 대화 흐름이 끊기고, 터미널을 왔다갔다 하다 보면 어디서 무슨 일이 일어나고 있는지 잊는다.
GPG가 대화형 패스프레이즈 입력을 요구해서 Claude가 직접 실행할 수 없다는 제약이 있지만, gpg --batch --passphrase-fd 0으로 stdin에서 패스프레이즈를 받으면 해결된다. 패스프레이즈 파일(~/.passphrase)을 만들어두면 완전 자동화도 가능하다. 다음 버전에서 개선할 부분이다.
rclone 토큰 관리는 생각보다 번거롭다
rclone의 Google Drive 토큰은 만료된다. 이번에 invalid_grant 에러를 만났는데, rclone config reconnect gdrive:를 실행했더니 브라우저가 열리지 않았다.
해결 과정:
# 브라우저 없이 URL만 얻기
printf "y\nn\n" | rclone config reconnect gdrive:
# → rclone authorize "drive" "<base64_token>" 명령이 출력됨
# 이 명령을 실행하면 브라우저가 열리고 인증 후 토큰이 출력됨
rclone authorize "drive" "<base64_token>"
# → 긴 base64 토큰 출력
# rclone 설정 파일 직접 수정
python3 -c "
import base64, json
# 토큰 디코딩 → rclone.conf의 token 라인 교체
"
rclone API가 config update 커맨드로 토큰을 업데이트하는 기능을 제공하지만, non-interactive 모드에서 동작하지 않아 설정 파일을 직접 수정했다. 이런 예외 케이스는 문서화해두는 게 나중에 도움이 된다.
Claude가 직접 실행할 수 없는 것들
이번 작업을 하면서 Claude가 직접 처리할 수 없는 것들이 명확해졌다:
| 불가능한 것 | 이유 | 우회 방법 |
|---|---|---|
| GPG 대화형 패스프레이즈 입력 | TTY stdin 없음 | --passphrase-fd 0으로 stdin 파이프 |
| 외부 서비스 키 교체 (Slack, GLM 등) | 웹 UI만 지원, API 없음 | 사용자가 직접 |
| 브라우저 인증 | UI 없음 | rclone authorize로 URL 획득 후 처리 |
Tencent Cloud는 tccli라는 CLI가 있어서 tccli cam CreateAccessKey로 새 키를 만들 수 있다. CLI가 있는 서비스는 Claude가 직접 처리 가능하다.
최종 아키텍처
secrets-index.md (지도)
↕ 조회
Claude (/secrets <keyword>)
↕ 읽기
실제 .env, CSV, JSON 파일들 (원본 위치)
↓ 백업
스테이징 디렉토리 (임시, trap으로 보호)
↓ tar.gz
SHA-256 체크섬 생성 (무결성 보장용)
↓ gpg --symmetric AES-256
암호화된 .tar.gz.gpg
↓ rclone
gdrive:secrets-backup/ (최대 5개 보존)
↓ 복원
gdrive:secrets-backup/
↓ rclone + gpg --decrypt
tar.gz → SHA-256 검증 → 원본 경로에 복원 + chmod 600
마무리: "직접 해줘"가 얼마나 효과적인가
이번 작업의 흥미로운 점은 사용자의 "직접 해줘" 한마디가 만들어내는 자율성이다.
패스프레이즈 생성도 직접 했고(openssl rand -base64 32), rclone 토큰 재발급도 직접 했고, 백업도 직접 실행했다. 사용자는 결과만 확인하면 됐다.
Claude Code가 단순한 코드 생성 도구가 아니라 실행 에이전트로 동작할 때 진짜 유용함이 나온다. 보안 작업처럼 "알고는 있는데 귀찮아서 미루는 것들"을 실제로 완수시키는 힘이 있다.
물론 한계도 있다. 브라우저가 필요한 OAuth, 웹 UI만 있는 서비스, 대화형 터미널 입력 — 이것들은 아직 사람의 손이 필요하다. 하지만 그 경계가 어디인지 파악하고, 가능한 부분을 최대한 자동화하는 것이 AI 네이티브 워크플로의 핵심이다.
시크릿 관리를 미루고 있다면, 지금 당장 인덱스 파일 하나만 만들어보자. 지도가 있으면 길을 잃지 않는다.
이 플러그인은 GitHub에서 볼 수 있다 (private).