보안¶
1. 개요¶
가상 키 인증, 팀 RBAC, 키·비밀 분리, 인라인 비밀 거부, 선택적 자체 TLS, 비밀을 누출하지 않는 메트릭이 공통 보안입니다. CLAUDE.md의 필수 불변 조건입니다.
2. 구성 요소¶
| 구성 | 경로 | 역할 |
|---|---|---|
| 데이터 인증 | internal/server/auth.go |
KeyAuth가 가상 키를 Principal로 해석 |
| 관리 인증 | internal/server/adminauth.go |
하나의 Bearer에서 정적 비상 토큰·OIDC를 완전 분기. 401·403 구분과 거부 감사 |
| OIDC 검증 | internal/adminauth/ |
형태 판정, 그룹→팀, 알고리즘 고정, aud/azp, ±60초 오차, 발견 부정 캐시. jwks.go는 캐시된 키로 먼저 검증하고, 실패 후에는 대기 시간창마다 한 번만 JWKS를 단일 요청으로 다시 받으므로 모르는 kid·위조 서명 범람이 캐시된 키를 막지 못하고, 새로 교체된 키는 대기 시간창 하나 안에 반영됨 |
| 콘솔 SSO | internal/server/adminui/static/app.js |
브라우저 Code+PKCE, 게이트웨이는 리소스 서버. sessionStorage에는 verifier/state/nonce만 일시 저장, ID 토큰은 메모리만. 오류보다 state를 먼저 확인하고 textContent·replaceState·nonce 검사를 적용. 발견 엔드포인트 HTTPS 필수 |
| 콘솔 CSP | adminui.go, gateway.go의 ssoConnectSrc |
login_origins가 있으면 self·issuer origin·허용 로그인 origin만 connect-src에 추가. script/style은 self. 없으면 기존 CSP·숨긴 SSO 유지 |
| CLI 로그인 | cmd/mayu/login.go, internal/server/authapi/ |
콘솔과 별도의 client_id·검증기, 루프백 PKCE. HTTPS 또는 루프백, 발견 주소는 issuer 동일 출처·하위 도메인. issuer/client_id TOFU 고정, reset 없는 변경 거부. refresh token 캐시 없음. 만료·owner는 서버 결정, 주체별 발급 제한 |
| 설정 조회 | internal/server/configapi/ |
비밀 값을 담을 수 없는 토폴로지 표현. 참조 이름·IAM 모드만 표시 |
| RBAC | internal/keystore/keystore.go |
팀·모델의 Principal.Allows |
| 교차 모델 폴백 재검사 | internal/router/router.go |
초기 허용 검사 뒤 추가된 폴백도 FilterModelAllowed로 다시 확인. 미설정 요청 이름만 허용된 키는 거부하며 다른 모델로 조용히 전환하지 않음 |
| 키 해시 | internal/keystore/sqlite.go |
SHA-256 저장·평문 1회 표시. 선언형 키도 key_ref 사용, 인라인 금지 |
| TLS 검증 | internal/server/tls.go |
인증서·키 한쪽만 설정하면 거부 |
| 비밀 참조 | internal/config/config.go |
env/file만 지원. 인라인 api_key와 미지원 secret 필드는 거부. Kubernetes Secret은 파일로 마운트하거나 환경 변수로 연결 |
| 자격 증명 중개 | controlplane/broker.go, proxy/credentials.go, bedrock/client.go |
최대 1시간 STS 세션. 읽을 수 없는 호스트 환경에서 노드 Bedrock IAM을 제거할 수 있음. 개발자 소유 호스트에서는 우회 방지가 아님. 하트비트와 다른 전용 토큰, JWT bearer는 검증 없이 403. 소스 누락·최초 조회 실패·알 수 없는 인증 모드·원격 평문 HTTP는 거부. 기본 AWS 체인으로 대체하지 않음 |
| 메트릭 보호 | internal/metrics/metrics.go |
key_id·비밀 라벨 없음, _rejected로 카디널리티 제한 |
중개 경로는 자격 증명 필드를 로그에 남기지 않습니다. 조회의 non-2xx·디코딩 오류에는 상태와 고정 문자열만 사용합니다. JSON 오류가 시간 값 등을 노출할 수 있어 그대로 감싸지 않습니다. STS 실패도 고정 502 본문입니다. v1 dataplane ID는 호출자 주장 값이므로 실제 장비 증거가 아니며 세션에는 전체 bedrock:Invoke 권한이 있습니다. 네트워크 조건 SourceIp·SourceVpce는 운영자가 역할에 적용해야 합니다.
3. 주요 결정¶
- 클라이언트 키를 업스트림에 전달하거나 공급자 키를 클라이언트에 노출하지 않습니다.
- 메트릭은 비인증이지만 비밀·key_id가 없고 라벨 수가 제한됩니다.
- 해석 전 403·404에는 센티널 모델 라벨을 사용해 공격자 입력으로 시계열이 늘어나지 않게 합니다.
- 미설정 요청 모델만 허용된 키는 RBAC에서 403이며 폴백으로 대신 처리하지 않습니다. 나중 추가한 모든 폴백도 재검사합니다.
- 중개 실패는 노드 AWS 신원으로 대체하지 않고 자격 증명 엔드포인트에는 OIDC 분기가 없습니다. 두 조건을 동작 변경에 실패하는 테스트로 고정합니다.
4. 코드 위치¶
internal/server/auth.go: 가상 키와 빈 키 우회 방지.internal/config/config.go: 비밀 참조 해석·인라인 거부.anthropicapi/messages.go·openaiapi/chat.go: 403·404의 _rejected 라벨.
5. 관련 문서¶
관련 모듈은 keystore·audit·metrics입니다. ADR-004, ADR-026, ADR-028, ADR-029, ADR-040과 운영 절차, CLI 로그인, 보안 정책을 참고하세요.
민감 정보 목적지 제한 · ADR-043¶
InternalOnly는 승인된 정규 모델과 매 시도의 명시적 internal 공급자를 모두 요구합니다. 누락·알 수 없는 경계는 신뢰하지 않습니다. 일치 규칙의 교집합을 사용하고 Block이 우선합니다. 안전한 체인이 없으면 마스킹·승인·공급자 호출 전에 거부합니다. Shadow도 개인정보를 집행하며 컨텍스트 선호 실패는 이미 안전한 경로를 유지합니다. 원래 모델 RBAC가 먼저 통과해야 하고 새 대안에는 가격·컨텍스트·기능·수신 프로토콜 검사가 필요합니다.
로컬 탐지 범위는 유한한 이메일·전화·Luhn 카드·SSN·IPv4·주민등록번호이며 보편적인 개인정보 탐지가 아닙니다. 불투명·알 수 없는 내용은 검사 불가이고 파싱·취소 실패는 민감 정책에서 거부합니다. 기존 선택적 마스킹은 별개로 모든 검사 대상을 다루지는 않습니다. 공급자 라벨은 운영자 선언이지 실제 엔드포인트 검증이 아닙니다. 새 감사·헤더·메트릭에 프롬프트·탐지 값을 넣지 않고 라우팅 메트릭은 팀·모드·사유만 사용합니다.
거부된 분산 개인정보 세대는 유효 복구 전까지 해당 주체를 차단합니다. 로컬 시작 정책도 토폴로지 검사 후 서비스합니다. 새 규칙 전에 모든 바이너리·CRD를 올리고 첫 CP 동기화 전 보호에는 require_sync, 정책 나이에는 max_policy_age를 사용합니다. 계산 API는 준비되지 않거나 오래되거나 거부된 상태에서 로컬/200입니다. 한계·배포를 참고하세요.
ADR-044의 유한한 완전 Mask는 외부 체인 반환 전에 수행하며 InternalOnly와 함께여도 필요합니다. 안전하지 않은 구조·숫자 변경, 남은 탐지, 불완전 변환은 거부합니다. 내부 모델 라벨만으로 Responses 원격 도구의 외부 호출을 허용할 수 없습니다. 엄격 예산은 거부된 정책 갱신을 차단하고 이후 컨텍스트·어피니티·폴백을 모두 필터링합니다. 세션 힌트는 범위가 지정된 HMAC 입력일 뿐 Principal이나 감사 차원이 아닙니다. 유일한 예외는 감사 기록과 분석 인덱스(메트릭 제외)의 ADR-057 키 범위 세션 다이제스트입니다(아래).
내장 인증서·키 TLS 설정은 데이터 영역에만 적용됩니다. 관리 영역의 원격 접속은 별도 TLS 종단 또는 보호된 터널을 필요로 합니다.
옵트인 PII 분류기 (systemone)¶
pii_classification은 자체 호스팅 분류기를 최대 4개까지 기존 sensitiveData
규칙에 연결합니다(명세:
docs/superpowers/specs/2026-10-16-systemone-korean-pii-sample-design.md).
입력. 분류기는 정규식 검사기가 읽는 텍스트를 모두 읽습니다. system 입력, 도구 스키마, 모든 turn과 tool result가 여기에 포함됩니다. 역할이나 기록 위치는 신뢰하지 않습니다.
제한. 라우팅을 제한하는 것은 Enforce 항목뿐입니다.
- 탐지되면 onDetected를 적용합니다. Mask 규칙은 판정에 지울 구간이 없으므로
onUninspectable로 격상합니다.
- Abstain, 타임아웃, 오류는 onUninspectable을 적용합니다. 즉 fail-closed입니다.
한 항목이 탐지하고 다른 항목이 abstain이나 오류를 내면 두 동작을 모두
적용하므로 더 엄격한 쪽이 이깁니다.
- Shadow 항목은 기록만 하고 라우팅을 바꾸지 않습니다.
- count_tokens는 분류기를 호출하지 않습니다. Enforce 항목과 적용되는
sensitiveData 규칙이 있는 팀의 count는 검사할 수 없는 것으로 보고
onUninspectable을 적용하며, 거부되더라도 로컬 추정치로 HTTP 200을
반환합니다.
잘림. 서버가 잘랐다고 보고한 윈도는 Clear가 되지 않습니다. 읽은 부분에서
탐지되지 않으면 Abstain입니다. laya-serve는 각 윈도를 체크포인트의 토큰 예산에
맞춰 오류 없이 자르고(샘플 체크포인트는 상태 토큰 약 980개), 잘랐다는 사실을
usage에 보고합니다. 1,000자는 한국어 산문 약 600토큰, 영어 산문 170토큰,
Go 코드 370토큰이므로 일반적인 코딩 텍스트는 기본 window_runes에 들어갑니다.
잠금 파일 해시, JSON 숫자, UUID 목록은 예산에 가깝고(1,000자당 805~934토큰),
전화번호·주민등록번호 목록(1,035토큰), 의도적으로 만든 윈도, 더 큰
window_runes는 예산을 넘을 수 있으며, 그런 윈도는 fail-closed가 됩니다. 게이트웨이는 토큰 수를 추정하지 않습니다. 기본값
truncation: "reported"에서는 usage.truncated가 없는 결과가 Abstain이므로,
laya-serve에서는 LAYA_JEV_STRICT를 꺼 두어야 합니다. 이 설정이 해당 필드를
지우기 때문입니다. truncation: "rejected"는 strands-decider
--strict-window처럼 너무 긴 입력을 HTTP 오류로 거부하는 서버용입니다. rejected에서는 게이트웨이가 그 주장을 그대로
믿습니다. 입력을 조용히 자르는 서버(예: --strict-window 없는
strands-decider)는 앞부분만 보고 판정하므로, 그 판정이 Clear로 캐시될 수
있습니다.
경계. internal 경계만 지원합니다. HTTP 클라이언트는 프록시 환경 변수와
리다이렉트를 따르지 않습니다.
판정 캐시. 노드 로컬 캐시에는 Clear 판정만 저장합니다. 키는 세대마다 새로 만드는 임의 키로 계산한 HMAC이며, 팀, 분류기 설정, 적용되는 정책 세대, 텍스트를 덮습니다. 윈도 텍스트 전체가 키이므로 따로 Clear된 조각이 그 조합을 Clear로 만들 수 없습니다. 원문은 저장하지 않고, 새 세대가 만들어질 때마다, 즉 재적재, UI 토폴로지 쓰기, 재시작 때마다 비웁니다.
증거. 감사와 메트릭에는 제한된 열거값과 선언된 분류기 이름·revision만 남깁니다. 텍스트, 윈도, 확률, digest, 엔드포인트는 남기지 않습니다.
알려진 한계.
- 서로 다른 윈도나 turn에 있는 이름과 속성은 함께 판정되지 않습니다.
- 키별 한도는 분류기 부하를 제한하지 않습니다. 분류는 거버넌스 승인보다 먼저
라우팅 안에서 실행되므로, 속도나 예산 한도를 넘은 키도 거부되기 전에
요청마다 최대 max_windows번의 사이드카 추론을 일으킵니다. 사이드카는
담당하는 팀의 요청률에 맞춰 크기를 정하세요. laya-serve는 동시에
LAYA_MAX_CONCURRENT개(기본 16) 요청까지 받고 나머지에는 503을 반환하며,
추론은 한 번에 하나씩 처리하므로 받은 요청은 대기합니다. batch 모드는
게이트웨이 요청마다 최대 max_windows개 윈도를 담은 사이드카 요청 하나를
보냅니다. 포화되면 Enforce에서는 fail-closed(onUninspectable)가 되고,
Shadow에서는 지연이 최대 timeout_ms까지 늘어납니다. 요청 간 분류기 호출
상한은 후속 작업입니다.
감사 콘솔 (ADR-057)¶
세션 다이제스트. Claude Code의 X-Claude-Code-Session-Id는 모델 호출에 실려 옵니다.
게이트웨이는 [A-Za-z0-9._:-] 1~128바이트를 받아
session_ref = "s1:" + hex(sha256("inferplane-session-v1\x00" + key_id + "\x00" + id)[:16])
를 감사 기록과 분석 인덱스에 기록합니다. 원본 ID는 저장·로그·메트릭 레이블·API 응답에
쓰이지 않습니다. 다이제스트는 키 범위이므로 같은 세션도 키가 다르면 다른 ref가 되며, 감사용
그룹화에만 쓰고 인가나 라우팅에는 쓰지 않습니다. 이는 세션 힌트를 감사에서 제외하는
ADR-044 규칙의 범위가 한정된 예외이며, ADR-044 친화도 고정은 그대로 제한된 로컬 저장소의
범위 HMAC 입력을 씁니다.
한 키 안에서는 같은 세션의 요청을 장기 감사 데이터에서 연결할 수 있습니다.
audit.client_evidence.session_ref: "off"는 새 ref 기록을 멈추지만 체인에 이미 있는
연결은 지우지 않습니다. 엔트로피가 낮은 ID를 보내는 클라이언트라면 다이제스트를 추측할 수
있으며, Claude Code는 무작위 UUID를 보냅니다.
자동 모드는 추정입니다. 판정은 Anthropic 형식 응답에서 읽으며 Claude Code의 현재 형식에
의존합니다. 게이트웨이는 응답당 최대 16 KiB 텍스트만 읽고, 응답 텍스트를 저장하지 않으며,
응답을 바꾸지 않습니다. 해석되지 않은 차단 호출 또는 서버 안전장치는 unknown으로
기록하고 레이블 없는 inferplane_audit_auto_mode_unparsed_total을 늘립니다.
</severity> 호출과 심각도 응답은 기록도 집계도 하지 않습니다. 콘솔은 모든 판정을
추정으로 표시합니다. audit.client_evidence.auto_mode: "off"로 끕니다.
단계별 규칙입니다.
s1, 차단 호출: 요청 중지 시퀀스에</block>이 있습니다.<thinking>텍스트를 제거한 뒤 응답에 일관된<block>yes또는<block>no가 하나 있어야 하며yes는block,no는allow입니다. 응답이</block>에서 멈췄거나 텍스트가</block>으로 태그를 닫을 때만 인정합니다. 게이트웨이가 변환한 Bedrock Converse 응답은 시퀀스 이름 없이stop_reasonstop_sequence만 보고하며, 이는</block>이 유일한 요청 중지 시퀀스일 때만</block>에서 멈춘 것으로 봅니다. 그 밖의 경우(값 충돌, 16 KiB 초과 텍스트 포함)는unknown입니다.s2, auxiliary 클래스 텍스트 규칙:x-claude-code-request-class가auxiliary인 요청에만 적용하며 중지 시퀀스가 필요 없습니다. Claude Code 2.1.312는 이 헤더를 보내지 않아 해당 클라이언트에서는 동작하지 않습니다.server, Bedrock 서버 안전장치:safeguard_results의dangerous_tool_use항목입니다. 모든 tool use가not_flagged이면allow, 그 밖의 상태나 결과는unknown, tool use가 없으면 기록하지 않습니다.
거버넌스 거부. 거부 기록은 governance에 거부한 한도를 적습니다. 범위, 한도 종류,
설정된 곳, 정책이면 정책 이름·세대·규칙입니다. 한도 값, 카운터, 키 자료, 사용자 식별자는
들어가지 않습니다. 정책과 규칙 이름은 운영자가 정한 텍스트로, 체인에는 그대로 저장되고 분석
인덱스에는 길이를 제한하고 정리해(256바이트로 자르고 NUL을 버리며 잘못된 UTF-8을 대체 문자로
바꿈) 저장되므로 비밀을 넣지 마세요.
접근. /admin/audit/* 다섯 경로는 읽기 전용이며 full-admin 전용입니다. 응답에는
다이제스트, 닫힌 코드, 저장된 감사 행만 있고 원본 세션 ID·프롬프트·응답 텍스트는 없습니다.
오류 본문은 닫힌 코드이며 입력을 되돌려 주지 않습니다. 이벤트의 hash는 체인 위치를 보여
줄 뿐 검증이 아닙니다.
관측성 콘솔 편집¶
/admin/observability*의 모든 경로는 읽기를 포함해 전체 관리자 전용입니다. 상태에는 헤더 이름만 나오고 값은 나오지 않습니다. 오류는 상태 코드나 고정 문구만 담고 수집기 응답은 담지 않습니다.- 실행 중 한도는 부트스트랩 exporter 상한을 좁히기만 합니다. 콘솔이나 제어 영역 입력으로 엔드포인트·프로토콜·TLS 설정·자격 증명을 지정할 수 없으므로 어떤 편집도 프롬프트를 다른 곳으로 보내지 못합니다. 콘솔 규칙이 본문 선택을 추가하거나 바꾸려면
observability.console.content_rules: true가 필요하고, 바뀌지 않은 선택을 유지하거나 제거하는 것은 언제나 허용됩니다. - 콘솔 규칙은 로컬 정책 파일 채널과 그 상한 검사를 거쳐 읽힙니다. 제어 영역이 있으면 콘솔이 두 번째 정책 출처가 될 수 없습니다.
- 감사 체인이 기록을 받지 않는 동안에는 편집을 거부하고, 적용한 편집마다 수행자와 전후 리비전을 기록합니다.
- mayu 호스트를 관리하는 사람은 여전히 설정과 두 파일을 고칠 수 있습니다. 사용자가 끌 수 없는 내보내기가 필요하면 사용자가 관리하지 않는 호스트가 필요합니다.