샌드박스 자격 증명 마스킹: 토큰은 계속 작동시키되, 비밀은 유지하기
- 언제 deny 대신 mask를 선호해야 하는지 — 그리고 deny가 여전히 더 안전한 경우
- 네 가지 마스킹 모드: 전체 값, extract, maskClaims를 사용한 decode: "jwt", SigV4용 awsPairs
- 환경 변수, JWT, AWS 자격 증명, ~/.config/gh/hosts.yml 같은 파일을 마스킹하기 위한 정확한 JSON
- tlsTerminate가 필수인 이유 — 그리고 이것이 없을 때 발생하는 조용한 실패 패턴
- 설정 소스 규칙: .claude/settings.json에서 mask 항목이 무시되는 이유
- Linux/WSL vs macOS 차이: macOS에서는 파일 마스킹이 deny로 다운그레이드됨
대부분의 비밀 누출은 토큰이 탈취되어서 발생하는 것이 아닙니다 — 선의의 스크립트가 로그, diff, 또는 서브에이전트의 트랜스크립트에 토큰을 출력했기 때문에 발생합니다. 샌드박스 자격 증명 마스킹은 Claude Code의 내장 해결책입니다: 샌드박스 명령은 실제 비밀 대신 세션별 센티널 값을 보고, 샌드박스 프록시는 여러분이 허용한 호스트로 나가는 요청에서 실제 값으로 대체합니다. 명령은 여전히 인증됩니다. 명령과 그것이 기록하는 어떤 것도 실제 자격 증명을 보유하지 않습니다.
이 페이지는 실무자 가이드입니다: 네 가지 마스킹 모드, 정확한 JSON, 함정, OS 매트릭스.
Mask vs deny: 어느 것을 원하는가?
샌드박스 명령이 자격 증명에 접근하지 못하게 하는 두 가지 방법. 비슷해 보이지만, 다릅니다.
"mode": "deny" | "mode": "mask" | |
|---|---|---|
| 환경 변수 | 샌드박스 환경에서 제거됨 | 세션별 센티널로 설정됨 |
| 파일 | 읽기 실패 | 샌드박스는 센티널 복사본을 봄 (Linux/WSL2) 또는 읽기 실패 (macOS) |
| 비밀이 필요한 도구 | 작동 안 함 (gh, npm, aws는 토큰 없이 실패) | 여전히 작동 — 프록시가 전송 중에 실제 값으로 교체 |
| 실제 값이 샌드박스에 있는가? | 없음 | 없음 (센티널만; 프록시가 실제 값 보유) |
tlsTerminate 필요? | 아니요 | 예 — 프록시가 대체를 위해 요청 내용을 봐야 함 |
| 저장소 설정에서 존중됨? | 예 | 아니요 — 사용자, 관리형, 또는 --settings만 |
경험 법칙. 도구가 자격 증명을 필요로 하지 않고 그것을 없애고 싶을 때 deny를 사용합니다. 도구가 인증해야 할 때 mask를 사용합니다 — 트랜스크립트, 서브에이전트, 또는 잘못된 env 덤프가 실제 GH_TOKEN을 결코 보유하지 않게 하면서 gh pr view가 작동하기를 원할 때.
전제 조건: tlsTerminate
mask는 나가는 HTTP 요청 헤더와 본문 내부에서 센티널을 실제 값으로 대체하여 작동합니다. 샌드박스 프록시는 그 바이트를 봐야 하므로 network.tlsTerminate가 필수입니다. 이것이 없으면 마스킹이 최악의 방식으로 실패합니다: 명령은 여전히 센티널만 보고, 센티널이 변경되지 않은 채 서버에 도달하며, 인증이 실패합니다. Claude Code는 시작 시 이 잘못된 구성을 보고합니다 — 경고를 읽으세요.
{
"sandbox": {
"network": {
"tlsTerminate": {},
"allowedDomains": ["api.github.com", "registry.npmjs.org"]
}
}
}
나중에 사용하는 모든 injectHosts 항목은 반드시 network.allowedDomains에도 나타나야 합니다. 호스트가 허용되지 않으면 프록시는 대체할 요청을 결코 보지 못합니다.
환경 변수 마스킹
기본 사례. 자격 증명당 하나의 envVars 항목.
GH_TOKEN과 NPM_TOKEN 마스킹
{
"sandbox": {
"network": {
"tlsTerminate": {},
"allowedDomains": ["api.github.com", "registry.npmjs.org"]
},
"credentials": {
"envVars": [
{ "name": "GH_TOKEN", "mode": "mask", "injectHosts": ["api.github.com"] },
{ "name": "NPM_TOKEN", "mode": "mask" }
]
}
}
}injectHosts는 대체를 특정 호스트로 범위 지정합니다.GH_TOKEN은api.github.com외 다른 곳에 결코 도달하지 않습니다.injectHosts를 생략하면 실제 값이network.allowedDomains의 모든 호스트로의 요청에서 대체됩니다. 토큰이 레지스트리로 범위가 지정된NPM_TOKEN에는 적합합니다.- 마스크가 활성화되었는지 확인하려면: Claude에게 샌드박스 명령에서
echo "$GH_TOKEN"을 실행하도록 요청하세요. 출력은 실제 토큰이 아닌 세션별 센티널이어야 합니다.
Extract: 구조화된 값 내부의 한 필드 마스킹
많은 "자격 증명"은 노출된 비밀이 아닙니다 — 비밀번호가 내부에 묻힌 연결 문자열입니다. extract는 정규식의 캡처 그룹만 마스킹하여 나머지는 파서가 계속 작동할 수 있도록 읽을 수 있게 유지합니다.
DATABASE_URL 내부의 비밀번호를 마스킹하고 나머지는 파싱 가능하게 유지
{
"name": "DATABASE_URL",
"mode": "mask",
"extract": "://[^:]+:([^@]+)@"
}- 패턴은 반드시 하나 이상의 캡처 그룹을 포함해야 합니다; 그룹 1의 텍스트만 대체됩니다.
onExtractNoMatch는 패턴이 아무것도 일치하지 않을 때 발생하는 일을 제어합니다:warn(기본값 — 경고와 함께 마스킹되지 않은 채 통과),deny(닫힘 실패), 또는error(샌드박스 실패). 비밀이 항상 존재해야 할 때deny를 사용하세요.
decode와 maskClaims를 사용한 JWT 마스킹
JWT 형태의 액세스 토큰(header.payload.signature)의 경우 전체 값 마스킹은 클레임을 확인하기 위해 토큰을 디코딩하는 샌드박스 내부의 모든 코드를 손상시킵니다. decode: "jwt"가 이를 해결합니다: Claude Code는 값이 유효한 JWT인지 확인하고 구조적으로 유효한 가짜 토큰으로 교체하므로, 샌드박스 내부의 jwt.decode(...)는 여전히 잘 형성된 페이로드를 반환합니다.
세션 JWT를 마스킹하되 디코딩 가능한 형태 유지
{
"name": "SESSION_JWT",
"mode": "mask",
"decode": "jwt",
"maskClaims": ["sub", "email"]
}maskClaims없이는 전체 가짜 토큰이 실제 토큰을 대체합니다 —iss나aud만 필요한 코드는 신경 쓰지 않지만,sub를 읽는 코드는 가짜 값을 얻습니다.maskClaims를 사용하면 다른 클레임은 읽을 수 있는 상태로 유지됩니다; 나열한 것들만 개별적으로 대체됩니다. 앱이 라우팅을 위해iat/exp/iss가 필요하지만sub/email을 결코 봐서는 안 될 때 유용합니다.decode는 같은 항목에서extract와 결합할 수 없습니다. 하나만 선택하세요.- 값이 JWT로 확인되지 않으면 (또는 나열된 클레임 중 어느 것도 일치하지 않으면), Claude Code는 경고와 함께 마스킹되지 않은 채 통과시킵니다. 닫힘 실패를 위해
onExtractNoMatch: "deny"를 사용하세요.
Claude Code v2.1.224 이상이 필요합니다.
AWS SigV4: awsPairs로 키를 함께 마스킹
AWS는 까다로운 경우입니다. SigV4 요청은 비밀 키에서 계산된 요청 내용에 대한 HMAC 서명을 포함합니다. 비밀은 마스킹하지만 액세스 키 ID는 마스킹하지 않으면 프록시는 어떤 요청이 AWS인지 감지할 방법이 없습니다 — 요청은 센티널로 서명되어 나가고, AWS는 이를 거부하며, 혼란스러운 실패가 발생합니다. 항상 액세스 키 ID와 비밀을 함께 마스킹하세요.
좋은 소식: 관례적인 변수 이름 AWS_ACCESS_KEY_ID / AWS_SECRET_ACCESS_KEY / AWS_SESSION_TOKEN의 경우, Claude Code는 세 개 모두가 전체 값 mask 항목일 때 자동으로 연결합니다. 프록시는 액세스 키의 센티널로 SigV4 요청을 감지하고 실제 값으로 대체한 후 다시 서명합니다.
AWS 자격 증명이 비관례적인 변수 이름에 있으면 awsPairs로 직접 그룹화하세요.
SigV4 재서명을 위한 비표준 AWS 변수 그룹화
{
"sandbox": {
"credentials": {
"envVars": [
{ "name": "MY_KEY_ID", "mode": "mask" },
{ "name": "MY_SECRET_KEY", "mode": "mask" },
{ "name": "MY_SESSION_TOKEN", "mode": "mask" }
],
"awsPairs": [
{
"accessKeyIdVar": "MY_KEY_ID",
"secretAccessKeyVar": "MY_SECRET_KEY",
"sessionTokenVar": "MY_SESSION_TOKEN"
}
]
}
}
}- 명명된 각 변수는 전체 값을 마스킹하는
mask항목이어야 합니다 —extract나decode는 안 됩니다. sessionTokenVar는 선택 사항입니다; 설정하면 프록시가 재서명된 요청에 실제 토큰을x-amz-security-token으로 보냅니다.- Claude Code v2.1.224 이상이 필요합니다.
프록시가 재서명할 수 없을 때: credentials.sigv4
세 가지 AWS 요청 형식은 프록시가 재계산할 수 없는 서명을 포함합니다 — 청크된 페이로드 서명, 사전 서명된 URL, SigV4A 비대칭 서명. 기본적으로 프록시는 손상된 서명을 전달하기보다는 이들을 실패시킵니다. 특정 도구가 그중 하나에 의존하고 프록시 오류보다 AWS 자체의 거부를 보기를 원한다면 credentials.sigv4로 그 형식을 완화하세요:
{
"sandbox": {
"credentials": {
"sigv4": {
"presignedUrl": "passthrough",
"chunkedPayload": "passthrough",
"sigv4a": "passthrough"
}
}
}
}
형식을 passthrough로 설정하면 자리 표시자로 서명된 요청을 변경 없이 전달하여 호출 도구가 AWS의 응답을 받습니다. 마스킹된 쌍의 자리 표시자로 서명된 요청에만 영향을 미칩니다 — 마스킹되지 않은 자격 증명으로 서명된 요청은 결코 건드리지 않습니다. 또한 v2.1.224+ 및 설정 소스 제한이 있습니다.
파일 마스킹: 디스크상의 자격 증명 마스킹
일부 도구는 환경 변수가 아닌 구성 파일에 토큰을 저장합니다(gh는 ~/.config/gh/hosts.yml에, docker는 ~/.docker/config.json에, SDK의 ~/.netrc). 파일 마스킹은 Linux 및 WSL2에서 샌드박스 프로세스에 파일의 센티널 복사본을 제공합니다. macOS에서는 파일 마스킹이 deny로 폴백됩니다 — 샌드박스 내부에서 파일을 읽을 수 없습니다.
~/.config/gh/hosts.yml 내부의 oauth_token 줄 마스킹
{
"sandbox": {
"network": {
"tlsTerminate": {},
"allowedDomains": ["api.github.com"]
},
"credentials": {
"files": [
{
"path": "~/.config/gh/hosts.yml",
"mode": "mask",
"extract": "oauth_token:\\s*(\\S+)",
"injectHosts": ["api.github.com"]
}
]
}
}
}extract패턴은hosts.yml의 나머지를 읽을 수 있게 유지하는 것입니다. 이것이 없으면 Claude Code는 전체 파일 내용을 하나의 센티널로 대체합니다 — 노출된 비밀만 포함하고 다른 것은 없는 파일에는 괜찮지만, 구조를 예상하는 파서를 손상시킵니다.- JWT를 포함하는 파일의 경우, 샌드박스 내부에서 토큰 형태를 디코딩 가능하게 유지하기 위해
decode: "jwt"(선택적으로maskClaims와 함께)를 추가하세요. maskDuplicates: true는 일치한 범위 밖에서 발견된 마스킹된 값의 있는 그대로의 복사본도 대체합니다. 길고 높은 엔트로피의 비밀을 위해 예약하세요 — 짧은 값은 나타나는 모든 곳에서 대체됩니다.- 각 자격 증명 파일을 개별적으로 나열하세요.
mask는 디렉토리 경로, 글로브 패턴, 8 MiB보다 큰 파일, 또는 UTF-8 텍스트가 아닌 파일에 대해deny로 폴백합니다.
OS 매트릭스
| 기능 | Linux | WSL2 | macOS |
|---|---|---|---|
환경 변수 mask | 예 | 예 | 예 |
파일 mask — 센티널 복사본 | 예 | 예 | 아니요 (deny로 폴백) |
파일용 extract / decode / maskClaims | 예 | 예 | 파일 시스템 격리가 꺼져 있을 때만 |
macOS에서는 파일 시스템 격리가 켜져 있을 때마다 mask 파일 항목이 패턴이 실행되기 전에 deny로 적용됩니다. macOS에서 extract/decode 동작을 얻으려면 파일 시스템 격리를 비활성화해야 합니다 — 대부분의 팀이 원하는 것보다 큰 트레이드오프입니다.
설정 소스 규칙 (이것이 모두를 걸려 넘어지게 함)
mask 항목은 샌드박스 프록시가 나열한 호스트로 여러분의 실제 자격 증명을 보내도록 승인합니다. 그것은 신뢰의 위임입니다. Claude Code는 여러분이나 관리자가 제어하는 설정 범위에서만 다음 키를 존중함으로써 이를 강제합니다 — 사용자 설정, 관리형 설정, 또는 --settings CLI 플래그. 저장소의 .claude/settings.json이나 .claude/settings.local.json에서는 조용히 무시됩니다:
mode: "mask"항목 (환경 변수 및 파일)network.tlsTerminatecredentials.allowPlaintextInject(프록시가 암호화되지 않은 요청에 주입하도록 허용)awsPairssigv4
실질적인 영향. 팀원을 위해 마스킹을 켜는 .claude/settings.json을 공유 저장소에 배포할 수 없습니다. 각 팀원은 마스크 항목을 자신의 사용자 설정에 넣거나, 관리자가 관리형 설정을 통해 푸시해야 합니다. 이것은 의도적입니다 — 여러분이 복제한 저장소가 여러분의 GH_TOKEN을 evil.example.com으로 이메일 보내도록 샌드박스에 명령할 수 있어서는 안 됩니다.
일반적인 함정
- tlsTerminate 없음 → mask가 조용히 실패합니다. 샌드박스는 센티널을 봅니다; 센티널이 서버로 가고; 인증이 실패합니다. 시작 경고를 확인하세요.
- injectHosts는 network.allowedDomains에 나타나야 합니다. 그렇지 않으면 프록시는 대체할 요청을 결코 보지 못합니다.
- AWS: 비밀만 마스킹하고 액세스 키 ID를 마스킹하지 않으면 프록시가 요청을 감지할 수 없습니다. 둘을 함께 마스킹하거나 awsPairs를 사용하세요.
- 저장소 수준 .claude/settings.json은 mask/tlsTerminate/awsPairs/sigv4에 대해 무시됩니다. 사용자 또는 관리형 설정에 넣으세요.
- macOS에서 파일 마스크는 deny가 됩니다. 앱이 파일을 읽어야 한다면 파일 시스템 격리를 비활성화하거나 Linux/WSL2에서 실행하세요.
- 캡처 그룹이 없는 extract는 구성 오류입니다 — 패턴에 그룹 1이 포함되어야 합니다.
- decode: "jwt"와 extract는 같은 항목에서 결합할 수 없습니다 — 하나만 선택하세요.
- 파일 마스크는 다음의 경우 deny로 폴백됩니다: 디렉토리 경로, 글로브 패턴, 8 MiB 초과 파일, 또는 UTF-8이 아닌 파일. 디렉토리를 파일별 항목으로 나누세요.
권장 구성
GitHub, npm, AWS에 대해 Claude Code 세션을 실행하는 개발자 노트북에 대한 합리적인 "허리띠와 멜빵" 시작점:
{
"sandbox": {
"network": {
"tlsTerminate": {},
"allowedDomains": [
"api.github.com",
"registry.npmjs.org",
"*.amazonaws.com"
]
},
"credentials": {
"envVars": [
{ "name": "GH_TOKEN", "mode": "mask", "injectHosts": ["api.github.com"] },
{ "name": "NPM_TOKEN", "mode": "mask", "injectHosts": ["registry.npmjs.org"] },
{ "name": "AWS_ACCESS_KEY_ID", "mode": "mask" },
{ "name": "AWS_SECRET_ACCESS_KEY", "mode": "mask" },
{ "name": "AWS_SESSION_TOKEN", "mode": "mask" },
{ "name": "ANTHROPIC_API_KEY", "mode": "deny" },
{ "name": "OPENAI_API_KEY", "mode": "deny" }
],
"files": [
{ "path": "~/.aws/credentials", "mode": "deny" },
{ "path": "~/.ssh", "mode": "deny" }
]
}
}
}
형태에 대한 참고 사항:
GH_TOKEN과NPM_TOKEN은 마스킹되고injectHosts로 범위가 지정됩니다.- 관례적인 AWS 3개 세트는 마스킹됩니다; Claude Code가 SigV4 재서명을 위해 자동으로 연결하므로
awsPairs가 필요하지 않습니다. - LLM API 키는
deny처리됩니다: 어떤 샌드박스 프로세스도 이를 필요로 하지 않아야 하며, 액세스 가능하게 두면 폭주하는 서브에이전트가 예산을 태울 수 있습니다. ~/.aws/credentials와~/.ssh는 디렉토리로deny목록에 있습니다 (마스킹이 디렉토리를 처리하지 않기 때문에mask가 아닌deny입니다).- 저장소가 아닌 사용자
settings.json에 속합니다.