보안 & 관리¶
API 키 섹션¶
API 키는 라우터 엔드포인트에 대한 클라이언트 접근을 제어합니다. 키는 여러 소스를 통해 설정할 수 있습니다.
인증 모드¶
mode 설정은 API 엔드포인트에 API 인증이 필요한지 제어합니다:
| 모드 | 동작 |
|---|---|
permissive (기본) |
API 키 없이 요청 허용. 유효한 API 키가 있는 요청은 인증됨. |
blocking |
API 키 인증을 통과한 요청만 처리. 인증되지 않은 요청은 401 수신. |
대상 엔드포인트 (mode가 blocking일 때):
/v1/chat/completions/v1/completions/v1/responses/v1/images/generations/v1/images/edits/v1/images/variations/v1/models
참고: Admin, Files, Metrics 엔드포인트는 별도의 인증 메커니즘이 있으며 이 설정의 영향을 받지 않습니다.
섹션 설정 속성:
| 속성 | 타입 | 필수 | 기본값 | 설명 |
|---|---|---|---|---|
mode |
string | 아니오 | permissive |
인증 모드: permissive 또는 blocking |
api_keys |
array | 아니오 | [] |
인라인 API 키 정의 |
api_keys_file |
string | 아니오 | - | 외부 API 키 파일 경로 |
api_keys:
# 인증 모드: "permissive" (기본) 또는 "blocking"
mode: permissive
# 인라인 API 키 정의
api_keys:
- key: "${API_KEY_1}" # 환경 변수 치환
id: "key-production-1" # 고유 식별자
user_id: "user-admin" # 연결된 사용자
organization_id: "org-main" # 연결된 조직
name: "Production Admin Key" # 사람이 읽을 수 있는 이름
scopes: # 권한
- read
- write
- files
- admin
rate_limit: 1000 # 분당 요청 수 (선택 사항)
enabled: true # 활성 상태
expires_at: "2025-12-31T23:59:59Z" # 선택적 만료 (ISO 8601)
- key: "${API_KEY_2}"
id: "key-service-1"
user_id: "service-bot"
organization_id: "org-main"
name: "Service Account"
scopes: [read, write, files]
rate_limit: 500
enabled: true
# 보안 향상을 위한 외부 키 파일
api_keys_file: "/etc/continuum-router/api-keys.yaml"
키 속성:
| 속성 | 타입 | 필수 | 설명 |
|---|---|---|---|
key |
string | 예 | API 키 값 (${ENV_VAR} 치환 지원) |
id |
string | 예 | admin 작업을 위한 고유 식별자 |
user_id |
string | 예 | 이 키와 연결된 사용자 |
organization_id |
string | 예 | 사용자가 속한 조직 |
name |
string | 아니오 | 사람이 읽을 수 있는 이름 |
description |
string | 아니오 | 키에 대한 메모 |
scopes |
array | 예 | 권한: read, write, files, admin |
rate_limit |
integer | 아니오 | 분당 최대 요청 수 |
enabled |
boolean | 아니오 | 활성 상태 (기본값: true) |
expires_at |
string | 아니오 | ISO 8601 만료 타임스탬프 |
allowed_backends |
array | 아니오 | 키별 백엔드 허용 목록. 비어 있거나 없으면 제한 없음. 아래 참조. |
외부 키 파일 형식:
# /etc/continuum-router/api-keys.yaml
keys:
- key: "sk-prod-xxxxxxxxxxxxxxxxxxxxx"
id: "key-external-1"
user_id: "external-user"
organization_id: "external-org"
scopes: [read, write, files]
enabled: true
보안 기능:
- 키 마스킹: 전체 키는 로깅되지 않음 (
sk-***last4로 표시) - 만료 적용: 만료된 키는 자동으로 거부
- 핫 리로드: 서버 재시작 없이 키 업데이트
- 감사 로깅: 모든 키 관리 작업 로깅
- 상수 시간 검증: 타이밍 공격 방지
- 최대 키 제한: DoS 방지를 위해 최대 10,000개 키
Admin API 엔드포인트 (admin 인증 필요):
| 엔드포인트 | 메서드 | 설명 |
|---|---|---|
/admin/api-keys |
GET | 모든 키 목록 (마스킹됨) |
/admin/api-keys/:id |
GET | 키 세부 정보 가져오기 |
/admin/api-keys |
POST | 새 키 생성 |
/admin/api-keys/:id |
PUT | 키 속성 업데이트 |
/admin/api-keys/:id |
DELETE | 키 삭제 |
/admin/api-keys/:id/rotate |
POST | 새 키 값 생성 |
/admin/api-keys/:id/enable |
POST | 키 활성화 |
/admin/api-keys/:id/disable |
POST | 키 비활성화 |
키별 백엔드 접근 제어¶
allowed_backends는 클라이언트 키를 설정된 백엔드 중 일부로 제한합니다. 이는 클라이언트 키에서 백엔드로 향하는 접근 제어로, 라우터가 OpenAI나 Anthropic 등을 호출할 때 사용하는 업스트림 자격 증명인 backends[].api_key와는 별개입니다.
목록이 비어 있지 않으면, 해당 키로 인증된 요청은 명시된 백엔드로만 라우팅됩니다. 허용되지 않은 백엔드만 제공하는 모델을 요청하면 permission_error 본문과 함께 403 Forbidden으로 거부되죠. 목록이 비어 있거나 없으면 키는 제한 없이 동작하는데, 이 동작이 기본값이고 이 필드가 도입되기 전에 만든 키와도 동일합니다.
동작 방식:
- 비어 있음 / 없음 ⇒ 제한 없음 (키는 요청한 모델을 제공하는 모든 백엔드로 라우팅 가능).
- 비어 있지 않음 ⇒
backends[].name값의 허용 목록. 매칭은 다른 백엔드 이름 해석과 마찬가지로 정확히 일치하며 대소문자를 구분합니다. - 제공 불가 요청 ⇒
403 Forbidden({"error": {"type": "permission_error", ...}}). 403 메시지에 모델 이름이 포함되어 404("모델 없음")와 구분할 수 있습니다. - 교차 제공자 폴백도 같은 목록으로 필터링되므로, 제한된 키가 폴백을 거쳐 자신의 범위를 벗어날 수 없습니다.
api_keys:
mode: blocking
api_keys:
- key: "${PARTNER_KEY}"
id: "key-partner-1"
user_id: "partner-acme"
organization_id: "org-acme"
scopes: [read, write]
allowed_backends: [openai, anthropic] # 이 백엔드로만 라우팅 가능
- key: "${INTERNAL_KEY}"
id: "key-internal-1"
user_id: "team-ml"
organization_id: "org-internal"
scopes: [read, write]
allowed_backends: [vllm-local] # 자체 호스팅 백엔드로 제한
모드 상호작용: 제한은 blocking과 permissive 모드 모두에서 적용되지만, permissive 모드에서는 유효한 키를 제시한 호출자에게만 효력이 있습니다. permissive 모드에서 인증된 키는 거부하지 않는 best-effort optional-auth 단계를 통해 자신의 정책을 그대로 적용받고, 키가 없거나 잘못된 요청은 제한 없이 통과해 permissive 모드의 '익명 허용' 동작을 유지합니다.
모델 목록: 제한된 키로 인증하면 /v1/models, /v1/models/extended, /anthropic/v1/models가 필터링되어, 키는 허용된 백엔드 중 하나 이상이 제공하는 모델만 노출합니다. GET /v1/models/{model}은 키가 접근할 수 없는 모델에 대해 404 Not Found를 반환하죠. 인증되지 않은 호출자와 제한 없는 키는 전체 목록을 봅니다.
설정 검증: 시작 시점과 핫 리로드 시점에, allowed_backends의 이름이 어떤 backends[].name과도 일치하지 않으면 경고를 출력합니다. 하드 에러가 아니므로, 백엔드 이름을 변경해도 운영자가 해당 키를 갱신하기 전까지 라우터가 멈추지 않습니다.
Admin API: allowed_backends는 생성, 업데이트, 조회, 목록 엔드포인트를 통해 왕복합니다. 생성 시 필드가 없으면 제한 없음이 기본값이고, 업데이트 시 null(없음)은 목록을 그대로 두며, 빈 배열은 모든 제한을 해제하고, 비어 있지 않은 배열은 목록을 교체합니다. 런타임 키는 영속화 파일을 통해 이 필드를 보존합니다.
Anthropic API 헤더: /anthropic/v1/messages, /anthropic/v1/messages/count_tokens, /anthropic/v1/models는 Authorization: Bearer <key>로 인증된 호출자에게 같은 키별 백엔드 정책을 적용합니다. Bearer 기반 AuthContext가 없으면, 라우터의 클라이언트 키와 일치하는 유효한 Anthropic 네이티브 x-api-key도 자신의 allowed_backends 정책을 제공합니다. permissive 모드에서 키가 없거나 잘못된 경우에는 위 optional-auth 동작과 동일하게 키별 정책 없이 통과합니다.
가드레일: PII 탐지 및 마스킹¶
PII 탐지는 여러 가드레일 프로바이더 중 하나입니다. 전체 가드레일 시스템(개념, 다섯 가지 프로바이더, 스트리밍 게이팅, 관리자 제어, 메트릭, 임계값 튜닝 워크플로)은 가드레일 가이드를 참고하세요.
pii 가드레일 프로바이더는 요청 프롬프트와 모델 응답에서 개인 식별 정보(PII)와 고가치 비밀 값을 탐지한 뒤, 해당 부분을 그 자리에서 마스킹하거나 요청을 차단합니다. OWASP LLM02(민감 정보 노출)에 대응합니다. 분류 전용 프로바이더와 달리 이 프로바이더의 주된 동작은 콘텐츠 변환입니다. 일치한 구간을 자리 표시자로 치환하고 정제된 텍스트를 그대로 흘려보냅니다.
내장 스캐너는 외부 의존성 없이 로컬에서 동작하며 이메일, 미국 사회보장번호(SSN), 신용카드/PAN 번호(오탐을 줄이기 위해 Luhn 체크섬으로 검증), 전화번호, AWS 액세스 키 ID, PEM 개인 키 블록, bearer / sk- 형식 API 키를 다룹니다. 더 풍부한 NER 기반 PII 탐지를 위해 Microsoft Presidio 호환 분석기를 선택적으로 설정할 수 있으며, 그 결과 구간은 내장 탐지 결과와 병합됩니다. 분석기를 사용할 수 없을 때는 on_error 정책에 따라 동작이 저하됩니다.
탐지된 각 엔티티 유형은 다음 동작에 매핑됩니다:
mask(기본값): 일치한 구간을 자리 표시자(예:<REDACTED:EMAIL>)로 치환하고 정제된 텍스트로 진행합니다(Transform판정).block: 해당 엔티티 유형이 존재하면 요청/응답을 차단합니다.allow: 해당 엔티티 유형을 무시합니다(마스킹도 차단도 하지 않음).
탐지된 원본 값은 로그에 기록되지 않습니다. 감사를 위해 엔티티 유형과 개수만 기록됩니다.
guardrails:
enabled: true
mode: enforce
providers:
- name: pii-redaction
type: pii
enabled: true
# 두 단계 모두에서 실행(기본값): 백엔드로 가기 전 프롬프트와 클라이언트로
# 가기 전 응답을 모두 정제합니다.
stages: [input, output]
options:
# `actions`에 없는 엔티티 유형에 적용할 동작. 기본값: mask.
default_action: mask
# 엔티티 유형별 동작 재정의.
actions:
email: mask
phone: mask
ssn: block
credit_card: block
aws_access_key: block
private_key: block
api_key: block
# 마스킹 구간의 자리 표시자 템플릿. `{TYPE}`는 대문자 엔티티 유형으로
# 치환됩니다. 기본값: "<REDACTED:{TYPE}>".
placeholder_format: "<REDACTED:{TYPE}>"
# 선택적 외부 Presidio 호환 분석기(내장 스캐너는 항상 실행되며, 외부
# 구간이 병합됩니다).
# external:
# endpoint: "http://presidio-analyzer:3000/analyze"
# language: en
# entities: ["PERSON", "LOCATION", "IBAN_CODE"]
on_error: fail_open
옵션:
| 속성 | 타입 | 기본값 | 설명 |
|---|---|---|---|
default_action |
string | mask |
actions에 없는 엔티티 유형에 적용할 동작: mask, block, allow. |
actions |
map | {} |
엔티티 유형별 동작 재정의. 키는 엔티티 유형(email, phone, ssn, credit_card, aws_access_key, private_key, api_key). |
placeholder_format |
string | <REDACTED:{TYPE}> |
마스킹 구간 템플릿. {TYPE}는 대문자 엔티티 유형으로 치환됩니다. |
external.endpoint |
string | - | Presidio 호환 분석기 URL. 프로바이더 상위 endpoint로 폴백합니다. |
external.entities |
array | [] |
외부 분석기를 이 엔티티 유형으로 제한(비어 있으면 분석기 기본값). |
external.language |
string | en |
분석기에 전달하는 언어 힌트. |
이 프로바이더는 기본적으로 두 단계 모두에 참여합니다. 여러 엔티티 유형이 동시에 존재하면 차단 엔티티가 마스킹보다 우선합니다(가장 심각한 판정 우선). 차단 엔티티가 있으면 판정은 차단이고, 그렇지 않고 마스킹된 항목이 있으면 Transform이 정제된 텍스트를 담아 반환되며, 둘 다 없으면 콘텐츠는 허용됩니다.
WebUI 섹션¶
선택적 webui 섹션은 내장 브라우저 기반 관리 인터페이스를 제어합니다. WebUI는 바이너리에 컴파일되어 관리자 인증으로 보호되는 정적 에셋으로 제공됩니다.
구성 속성:
| 속성 | 타입 | 기본값 | 설명 |
|---|---|---|---|
enabled |
boolean | true |
WebUI 활성화 또는 비활성화 |
path_prefix |
string | /webui |
URL 경로 접두사. /로 시작해야 하며 ..을 포함해서는 안 됩니다. |
webui가 생략되면 기본값이 적용됩니다: WebUI는 /webui에서 활성화됩니다. WebUI를 비활성화하려면:
브라우저 인터페이스 사용에 대한 전체 가이드는 내장 WebUI를 참조하세요.
Admin 섹션¶
admin 섹션은 인증 및 통계 수집을 포함한 Admin REST API를 설정합니다.
인증¶
admin:
auth:
method: bearer_token # 인증 방법: none, bearer_token, basic, api_key
token: "${ADMIN_TOKEN}" # bearer_token 방식의 토큰
모든 인증 옵션에 대해서는 Admin REST API 레퍼런스를 참조하세요.
통계 수집¶
admin.stats 하위 섹션은 요청 메트릭 수집 및 영속화를 제어합니다. 통계 수집은 기본적으로 활성화되어 있습니다.
admin:
stats:
enabled: true # 수집 활성화/비활성화 (기본값: true)
retention_window: 24h # 윈도우 쿼리용 링 버퍼 보존 기간 (기본값: 24h)
token_tracking: true # 응답 본문에서 토큰 사용량 파싱 (기본값: true)
persistence:
enabled: true # 재시작 시 통계 영속 활성화 (기본값: true)
path: ./data/stats.json # 스냅샷 파일 경로 (기본값: ./data/stats.json)
snapshot_interval: 5m # 주기적 스냅샷 간격 (기본값: 5m)
max_age: 7d # 시작 시 이보다 오래된 스냅샷은 폐기 (기본값: 7d)
설정 속성:
| 속성 | 타입 | 기본값 | 설명 |
|---|---|---|---|
enabled |
boolean | true |
통계 수집 활성화 또는 비활성화 |
retention_window |
string | 24h |
윈도우 쿼리를 위한 링 버퍼 보존 기간 |
token_tracking |
boolean | true |
토큰 사용량 추출을 위한 응답 본문 파싱 |
영속화 속성:
| 속성 | 타입 | 기본값 | 설명 |
|---|---|---|---|
persistence.enabled |
boolean | true |
재시작 시 통계 영속화 활성화 |
persistence.path |
string | ./data/stats.json |
영속화 스냅샷 파일 경로 |
persistence.snapshot_interval |
string | 5m |
주기적 스냅샷 간격 |
persistence.max_age |
string | 7d |
시작 시 스냅샷 복원 최대 기간 |
영속화가 활성화되면:
- 시작 시 라우터는 스냅샷 파일에서 카운터와 링 버퍼 레코드를 복원합니다. 업타임은 항상 0으로 리셋됩니다.
- 백그라운드 태스크가 설정된 간격으로 원자적(임시 파일 + 이름 변경)으로 스냅샷을 기록합니다.
- 정상 종료(SIGTERM/SIGINT) 시 최종 스냅샷이 저장됩니다.
- 누락, 손상, 또는 오래된 스냅샷은 정상적으로 처리됩니다. 라우터는 새 카운터로 시작하고 경고를 로깅합니다.
핫 리로드: retention_window와 token_tracking은 즉시 핫 리로드를 지원합니다. 영속화 설정(path, snapshot_interval, max_age)은 재시작이 필요합니다.
지원되는 기간 형식 (retention_window, snapshot_interval, max_age):
| 형식 | 예제 | 의미 |
|---|---|---|
Xs |
30s |
30초 |
Xm |
5m |
5분 |
Xh |
1h |
1시간 |
Xd |
7d |
7일 |
전체 엔드포인트 문서는 Admin REST API 레퍼런스 — 통계 API를 참조하세요.
ACP (Agent Communication Protocol) 섹션¶
acp 섹션은 Agent Communication Protocol 하위 시스템을 설정합니다. ACP는 IDE 및 도구 통합이 stdio를 통한 JSON-RPC 2.0으로 라우터와 통신할 수 있도록 합니다. ACP는 하위 호환성을 위해 기본적으로 비활성화되어 있습니다.
ACP를 사용하려면 --mode stdio로 라우터를 실행하세요.
acp:
enabled: true
transport:
stdio:
enabled: true
agent:
name: "Continuum Router"
version: "1.0.0"
description: "Local LLM inference agent"
capabilities:
load_session: true
image: false
audio: false
embedded_context: false
mcp: true
default_model: "gpt-4o"
system_prompt: "You are a helpful coding assistant."
coding_agent_mode: true
permissions:
default_policy: ask_always
auto_allow:
- read
- search
- think
always_ask:
- edit
- delete
- execute
sessions:
max_concurrent: 10
idle_timeout: "1h"
storage: "memory"
mcp:
max_connections_per_session: 5
allowed_servers: []
server_spawn_timeout: "10s"
최상위 옵션¶
| 속성 | 타입 | 기본값 | 설명 |
|---|---|---|---|
enabled |
bool | false |
ACP 하위 시스템 활성화/비활성화 |
default_model |
string | 없음 | ACP 세션의 모델 선택 재정의 |
system_prompt |
string | 없음 | 모든 ACP 요청에 시스템 프롬프트 주입 |
coding_agent_mode |
bool | false |
코딩 에이전트 시스템 프롬프트 활성화 |
전송 옵션¶
| 속성 | 타입 | 기본값 | 설명 |
|---|---|---|---|
transport.stdio.enabled |
bool | true |
stdio 전송 활성화 |
권한 옵션¶
| 속성 | 타입 | 기본값 | 설명 |
|---|---|---|---|
permissions.default_policy |
enum | ask_always |
기본 정책: ask_always, allow_read, allow_all |
permissions.auto_allow |
list | [read, search, think] |
확인 없이 자동 허용되는 도구 종류 |
permissions.always_ask |
list | [edit, delete, execute] |
항상 권한을 요청하는 도구 종류 |
세션 옵션¶
| 속성 | 타입 | 기본값 | 설명 |
|---|---|---|---|
sessions.max_concurrent |
int | 10 |
최대 동시 세션 수 |
sessions.idle_timeout |
string | "1h" |
세션 정리 전 유휴 타임아웃 |
sessions.storage |
string | "memory" |
저장 백엔드: memory 또는 file |
sessions.storage_path |
string | 없음 | 파일 기반 저장 경로 |
MCP 브릿지 옵션¶
| 속성 | 타입 | 기본값 | 설명 |
|---|---|---|---|
mcp.max_connections_per_session |
int | 5 |
세션당 최대 MCP 연결 수 |
mcp.allowed_servers |
list | [] |
허용된 서버 ID (빈 값 = 모두 허용) |
mcp.server_spawn_timeout |
string | "10s" |
MCP 서버 프로세스 생성 타임아웃 |
프로토콜 세부 사항은 ACP 아키텍처를, 실용 예제는 ACP 사용 가이드를 참조하세요.