셀프호스티드 환경 세션 신원 검증 (Verify session identity)
기준일: 2026-10-23
공식 기준: Verify session identity in self-hosted environments
개요
셀프호스티드 환경에서는 Claude Code 클라우드 세션이 Anthropic이 아닌 내가 운영하는 인프라에서 실행되므로, Claude가 내부 서비스를 직접 호출할 수 있습니다. 내부 서비스는 요청이 내 환경의 Claude Code 세션에서 왔는지 확인하고, 그 세션을 만든 사용자나 서비스 신원을 식별할 방법이 필요합니다.
- 셀프호스티드 환경의 모든 세션은
CLAUDE_CODE_SESSION_ACCESS_TOKEN환경 변수로 서명된 JWT를 받습니다. - 세션은 이 토큰을 일반 bearer 자격 증명처럼 제시합니다. 예: Claude가 실행하는 스크립트에서
curl -H "Authorization: Bearer $CLAUDE_CODE_SESSION_ACCESS_TOKEN". - Anthropic이 토큰에 서명하고 검증 키를 공개 JWKS 엔드포인트에 게시합니다. 서비스는 키를 받아 서명을 검증하고 클레임을 읽어 접근 범위를 정합니다.
- Team·Enterprise 플랜 퍼블릭 베타이며, Owner가 Cloud environments admin 페이지에서 Allow self-hosted environments를 켜야 합니다. 설정은 Quickstart, 플릿 레시피는 Deploy to production을 보세요.
핵심 개념
| 항목 | 값 |
|---|---|
| 토큰 위치 | 세션 환경 변수 CLAUDE_CODE_SESSION_ACCESS_TOKEN |
| 접두사 | sk-ant-cc- (Anthropic 호스티드 세션 토큰은 sk-ant-si-, 다른 키 세트로 서명 → 거부) |
| 서명 알고리즘 | ES256 (P-256 곡선 ECDSA + SHA-256), 헤더 kid로 키 식별 |
| JWKS | https://api.anthropic.com/v1/code/.well-known/jwks.json (공개, 인증 없음, Cache-Control: public, max-age=300) |
iss |
정확히 ccr |
aud |
배열. 내 환경 ID(ccpool_...)를 포함해야 함 |
ccr:role |
정확히 session_worker |
| 수명 | 기본 4시간, 최대 8시간. 러너가 만료 전에 갱신 |
| 생성자 | act.sub가 user:<id>(사용자) 또는 agent:<id>(조직 서비스 신원, Claude Tag 채널 세션 등) |
상세
세션 토큰
토큰이 증명하는 것
유효한 토큰은 일부 사실만 증명하고 나머지는 의도적으로 증명하지 않습니다.
- 증명함: Anthropic이 특정 환경의 특정 세션에 대해 발급했다는 것, 그리고 세션이 어떻게 생성되었는지(조직의 사용자, 또는 Claude Tag 채널 세션이 시작되는 방식인 조직 서비스 신원).
- 증명하지 않음: 러너 호스트의 어떤 프로세스가 토큰을 제시하는지. 토큰은 세션 안 환경 변수에 있으므로 Claude가 실행하는 모든 코드, 세션이 시작한 모든 도구·MCP 서버가 읽고 제시할 수 있습니다.
서비스 입장에서의 결과는 두 가지입니다.
aud클레임을 내 환경 ID(admin 페이지에서 환경과 함께 표시되는ccpool_...값)와 대조해 다른 조직 환경에 발급된 토큰을 거부합니다.- 토큰으로 파생하는 자격 증명은 세션 생성자가 할 수 있는 모든 것이 아니라 코딩 세션 하나가 할 수 있어야 하는 범위로 제한합니다. 파생 자격 증명 범위 제한을 보세요.
토큰 형식
CLAUDE_CODE_SESSION_ACCESS_TOKEN 값은 sk-ant-cc- 접두사 + 표준 3파트 JWT입니다.
sk-ant-cc-<base64url header>.<base64url payload>.<base64url signature>
JWT 라이브러리에 넘기기 전에 접두사를 제거합니다. sk-ant-cc-로 시작하지 않는 값은 모두 거부하세요. 서명 알고리즘은 ES256이며 헤더의 kid가 JWKS의 어느 키로 서명했는지 나타냅니다.
토큰 검증
검증은 두 곳 중 하나에서 합니다. 내 네트워크의 서비스는 Anthropic이 게시한 키로 암호학적으로 검증하고, 세션 안의 wrapper script는 러너 바이너리의 내장 decoder를 쓸 수 있습니다.
서비스에서 검증
검증 키는 공개·비인증 엔드포인트에 있습니다.
https://api.anthropic.com/v1/code/.well-known/jwks.json
응답은 표준 JSON Web Key Set입니다. Anthropic은 서명 키를 주기적으로 rotate하며, rotate 이전 키도 그 키로 서명한 토큰이 계속 검증될 만큼 세트에 남아 있습니다. 키 하나를 pin하지 마세요. Cache-Control: public, max-age=300이므로 키 세트를 캐시하고 5분마다 다시 받아도 안전합니다.
들어오는 토큰마다 다음을 검사합니다.
| 단계 | 검사 |
|---|---|
| 1. 접두사 | sk-ant-cc-로 시작하지 않으면 거부, 이후 접두사 제거. 나머지는 표준 compact JWT |
| 2. 서명 | JWKS에서 헤더 kid와 일치하는 키를 골라 ES256 서명 검증. alg 헤더가 ES256이 아니면 거부. 캐시에 없는 kid면 거부 전에 JWKS를 한 번 다시 받음(rotate 직후 새 토큰은 캐시에 없는 키로 서명됨) |
| 3. 발급자 | iss가 정확히 ccr이 아니면 거부 |
| 4. 대상(환경) | aud 배열에 내 환경 ID(ccpool_...)가 없으면 거부. 환경 ID는 admin 페이지 환경 상세 대화상자에 표시되고, 그 환경 세션 토큰의 ccr:pool_id 클레임에도 나타남. 이 검사가 토큰을 내 환경으로 한정하고 다른 조직 토큰을 거부함 |
| 5. 역할 | ccr:role이 정확히 session_worker가 아니면 거부. environment secret, 러너 토큰, work order 등 셀프호스티드용 다른 토큰도 같은 키 세트로 서명되지만 역할이 다름 |
| 6. 만료 | exp가 과거면 거부. 세션 토큰은 기본 4시간, 최대 8시간. 러너가 만료 전 갱신해 세션에 새 값을 push하므로 갱신 후 시작된 하위 프로세스는 새 토큰을 상속함. 따라서 한 세션이 수명 동안 서로 다른 유효 토큰 여러 개를 제시할 수 있음 |
| 7. 신원 읽기 | 생성 사용자는 act 클레임: act.sub는 user:<id> 형태의 Anthropic 사용자 ID, act.email은 생성 surface가 기록한 경우의 이메일. 조직 서비스 신원이 만든 세션(Claude Tag 채널 세션 포함)은 agent: subject. 신원 클레임이 없는지로 판단하지 말고 act.sub에 user: 접두사가 있을 때만 사용자 생성으로 취급 |
이 검사들은 표준 JWT 라이브러리에 그대로 대응합니다. 아래 예제는 Node.js의 jose(JWKS fetch·캐시·kid 선택 처리)와 Python의 PyJWT 내장 JWKS client로 전체 순서를 구현합니다.
Node.js (jose)
import { createRemoteJWKSet, jwtVerify } from "jose";
const JWKS = createRemoteJWKSet(
new URL("https://api.anthropic.com/v1/code/.well-known/jwks.json")
);
const PREFIX = "sk-ant-cc-";
const EXPECTED_POOL_ID = "ccpool_...";
export async function verifySessionToken(raw: string) {
if (!raw.startsWith(PREFIX)) {
throw new Error("not a self-hosted runner session token");
}
const jwt = raw.slice(PREFIX.length);
const { payload } = await jwtVerify(jwt, JWKS, {
issuer: "ccr",
audience: EXPECTED_POOL_ID,
algorithms: ["ES256"],
});
if (payload["ccr:role"] !== "session_worker") {
throw new Error("token is not a session_worker token");
}
const act = payload.act as { email?: string; sub?: string };
return {
sessionId: payload["ccr:session_id"] as string,
poolId: payload["ccr:pool_id"] as string,
orgId: payload["ccr:org_id"] as string,
creatorEmail: act?.email,
creatorSub: act?.sub,
};
}
Python (PyJWT)
import jwt
from jwt import PyJWKClient
JWKS_URL = "https://api.anthropic.com/v1/code/.well-known/jwks.json"
PREFIX = "sk-ant-cc-"
EXPECTED_POOL_ID = "ccpool_..."
jwks = PyJWKClient(JWKS_URL)
def verify_session_token(raw: str) -> dict:
if not raw.startswith(PREFIX):
raise ValueError("not a self-hosted runner session token")
token = raw.removeprefix(PREFIX)
signing_key = jwks.get_signing_key_from_jwt(token)
payload = jwt.decode(
token,
signing_key.key,
algorithms=["ES256"],
issuer="ccr",
audience=EXPECTED_POOL_ID,
)
if payload.get("ccr:role") != "session_worker":
raise ValueError("token is not a session_worker token")
act = payload.get("act") or {}
return {
"session_id": payload["ccr:session_id"],
"pool_id": payload["ccr:pool_id"],
"org_id": payload["ccr:org_id"],
"creator_email": act.get("email"),
"creator_sub": act.get("sub"),
}
세션 안에서 검증
Wrapper script는 Claude가 시작하기 전에 세션 안에서 실행됩니다. JWT 라이브러리 대신 러너 바이너리의 self-hosted-runner decode-token 하위 명령을 쓸 수 있습니다.
- 토큰을 positional 인자 →
CLAUDE_CODE_SESSION_ACCESS_TOKEN→ 파이프 stdin 순으로 읽습니다. - 접두사 제거, JWKS 엔드포인트로 서명 검증, 만료 검사 후 클레임을 JSON으로 출력합니다.
- 서명과 만료만 검사합니다.
iss,aud,ccr:role은 확인하지 않으므로, wrapper의 인증 판단이 이 클레임에 달려 있다면 출력 JSON에서 읽어 명시적으로 비교하세요.
생성자 신원을 이메일 우선, 없으면 act.sub(user:<id> 또는 agent:<id>)로 추출하는 예:
"$CLAUDE_RUNNER_CLAUDE_BIN" self-hosted-runner decode-token | jq -re '.act.email // .act.sub'
- Wrapper는 러너 자체 바이너리의 절대 경로를
CLAUDE_RUNNER_CLAUDE_BIN으로 받습니다. PATH로 찾는claude대신 이 경로를 써야 러너가 쓰는 것과 같은 바이너리로 decode합니다. jq -r대신jq -re를 쓰세요.-r만 쓰면 클레임이 없을 때 문자열null을 출력하고 0으로 종료해 잘못된 값이 조용히 하위로 넘어갑니다.-e는 non-zero로 종료합니다.decode-token이 JWKS에서 키를 못 받거나 토큰을 검증하지 못하면 이유를 stderr에 출력하고 클레임 없이 종료 코드 1로 끝납니다.--no-verify는 JWKS 엔드포인트에 닿을 수 없는 오프라인 점검에만 쓰세요.
클레임 레퍼런스
- 신원은
ccr:*네임스페이스와act체인에서 읽습니다. 평면 클레임account_email,organization_uuid,account_uuid는 하위 호환용 중복이며 제거될 수 있습니다. - 조직 서비스 신원이 만든 세션(Claude Tag 채널 세션 포함)은
act.sub가agent:subject이고act.email,ccr:account_id,account_email,account_uuid가 없습니다. - 두 이메일 클레임은 사용자 생성 세션에서도 선택 사항입니다. 생성 요청의 자격 증명에 이메일이 있을 때만 기록되며 CLI에서 디스패치한 세션에는 둘 다 없을 수 있습니다. 신원은 이메일이 아니라
act.sub또는ccr:account_id로 식별하세요. - 이 표에 없는 추가 클레임이 있을 수 있으니 모르는 클레임은 무시합니다.
| 클레임 | 타입 | 설명 |
|---|---|---|
iss |
string | 항상 ccr |
sub |
string | ccr:session:<session_id> |
aud |
array of strings | 항상 anthropic-api 포함. 셀프호스티드 세션은 환경 ID(ccpool_...)도 포함. anthropic-api가 아니라 환경 ID를 검증 |
exp |
number | 만료 Unix 타임스탬프. 기본 4시간, 최대 8시간 |
iat |
number | 발급 Unix 타임스탬프 |
jti |
string | 고유 토큰 식별자 |
ccr:role |
string | 세션 토큰은 항상 session_worker |
ccr:session_id |
string | cse_... 형태 세션 ID. sub의 접미사, CLAUDE_CODE_REMOTE_SESSION_ID와 같은 문자열. lifecycle hook이 받는 CLAUDE_RUNNER_SESSION_ID와 맞추려면 cse_ 접두사를 session_으로 바꿈 |
ccr:pool_id |
string | 내 환경 ID. aud에 나오는 값과 같음 |
ccr:org_id |
string | Anthropic 조직 ID |
ccr:account_id |
string | 생성 사용자의 Anthropic 계정 ID. act.sub에서 user: 접두사를 뺀 값, 태그된 user_... ID. spawn-runner hook의 CLAUDE_RUNNER_ACCOUNT_ID, --lock-to-account가 받는 값과 같아 세 값을 문자열로 그대로 비교 가능 |
account_email |
string | act.email의 중복. act.email이 없으면 없음 |
organization_uuid |
string | Anthropic 조직 UUID |
account_uuid |
string | 생성 사용자의 Anthropic 계정 UUID |
act |
object | RFC 8693 위임 체인. 아래 참고 |
act 체인
act 클레임은 세션을 만든 사용자·서비스 신원부터, secret으로 러너를 받아들인 환경, 그 secret을 만든 신원까지의 위임 경로 전체를 기록합니다. 생성자가 가장 바깥 actor이므로 act.sub가 생성자를 바로 식별합니다.
| 경로 | 설명 |
|---|---|
act.sub |
생성 사용자의 Anthropic 사용자 ID(user:<id>), 또는 조직 서비스 신원이 만든 경우(Claude Tag 채널 세션 등) agent:<id> |
act.email |
세션 생성 시 기록된 경우 생성 사용자 이메일. 필수로 요구하지 말고 act.sub로 식별 |
act.attested_by |
생성 사용자에 대한 상위 IdP attestation용 예약 필드. 없다고 가정하고 의존하지 말 것. act.sub로 식별하고, 주소가 필요하면 act.email이 있을 때 읽음 |
act.act |
세션을 spawn한 러너. act.act.sub는 ccr:runner:<runner_id> |
act.act.act |
환경. act.act.act.sub는 ccr:pool:<pool_id> |
act.act.act.act |
러너가 등록에 쓴 environment secret을 만든 신원. 체인은 여기서 끝남 |
파생 자격 증명 범위 제한
세션 토큰은 세션을 만든 사용자·서비스 신원을 식별하지만, 그 생성자가 직접 로그인한 것과 동등하게 취급하면 안 됩니다. 토큰은 세션 안 환경 변수에 있어 Claude가 실행하는 코드, 세션이 시작한 도구·MCP 서버가 모두 읽고 제시할 수 있습니다.
검증도 오프라인입니다. JWKS로 검증되는 토큰은 세션에 무슨 일이 있었든 exp까지 유효하며, Anthropic은 세션 토큰 revocation feed를 게시하지 않습니다. 토큰에서 파생하는 모든 것을 이에 맞게 제한하세요.
서비스가 토큰을 내부 자격 증명으로 교환할 때는 코딩 세션 하나가 닿아야 할 범위로 발급합니다.
- 권한 제한: 생성자가 다른 곳에서 가진 관리 권한이 아니라 코딩 작업에 필요한 리소스의 읽기·쓰기만 부여
- 수명 제한: 파생 자격 증명을 토큰의
exp이하로 제한 - 세션 단위 감사: 생성자 신원과 함께
ccr:session_id와jti를 기록해 특정 세션까지 추적
관련 환경 변수
생성자 신원은 토큰을 검증하지 않는 두 곳에서 평문 환경 변수로도 나타납니다.
- orchestrator의
spawn-runnerhook: 큐에 쌓인 세션에 러너가 아직 없을 때 실행되며CLAUDE_RUNNER_ACCOUNT_EMAIL,CLAUDE_RUNNER_ACCOUNT_ID같은 변수로 생성자 신원을 받습니다. orchestrator는 이를 러너 하나의 spawn을 허가하는 서명된 1회용 토큰인 work order에서 읽지만 서명은 직접 검증하지 않습니다. work order가 environment secret으로 인증된 orchestrator↔Anthropic 연결로 도착하므로 클레임을 신뢰합니다. - 세션 안의 wrapper script: 서명 검증 없이 토큰에서 미리 추출한 생성자 이메일
CCR_SESSION_ACCOUNT_EMAIL을 받습니다. 커밋 트레일러 같은 라벨링에는 적합하지만 인증 판단에는 쓰지 마세요.
머신 이미지 선택 같은 orchestrator 측 판단에는 평문 변수를 쓰고, 하위 서비스가 러너 환경을 신뢰하지 않고 독립적인 암호학적 증명이 필요할 때는 CLAUDE_CODE_SESSION_ACCESS_TOKEN을 쓰세요.
체크리스트
-
sk-ant-cc-접두사가 아닌 값(특히sk-ant-si-)을 거부하고 접두사를 제거한 뒤 검증한다. - JWKS를 캐시(5분)하되 단일 키를 pin하지 않고, 모르는
kid는 한 번 재조회한다. -
alg가ES256,iss가ccr,ccr:role이session_worker인지 확인한다. -
aud에anthropic-api가 아니라 내 환경 IDccpool_...가 있는지 확인한다. -
exp를 검사하고, 한 세션이 여러 유효 토큰을 쓸 수 있음을 감안한다. - 신원은 이메일이 아니라
act.sub(또는ccr:account_id)로 식별하고,user:접두사일 때만 사용자 생성으로 본다. -
decode-token을 쓸 때iss·aud·ccr:role을 직접 비교하고jq -re를 쓴다. - 파생 자격 증명을 세션 범위·
exp이하로 제한하고ccr:session_id·jti를 감사 로그에 남긴다. -
CCR_SESSION_ACCOUNT_EMAIL같은 평문 변수를 인증 판단에 쓰지 않는다.
다음 단계
- 셀프호스티드 환경에서 환경·러너·세션 모델을 봅니다. 설정과 운영은 Quickstart와 Deploy to production에 있습니다.
- Customize sessions에서 토큰을 쓰는 wrapper script와
spawn-runnerhook을 봅니다. - Reference에서 CLI 플래그, 환경 변수, 메트릭을 확인합니다.
기준일
이 문서는 2026-10-23에 공식 Verify session identity in self-hosted environments 페이지를 기준으로 작성했습니다. 클레임 구성은 바뀔 수 있으니 공식 원문을 함께 확인하세요.