콘텐츠로 이동

시크릿 매니저 통합: 설계 기록

이 문서는 시크릿 매니저에서 credential 참조를 해석하기 위해 출시된 설계를 기록합니다 (이슈 #2413). 첫 릴리스는 참조 체계, 교체 가능한 리졸버 트레이트, 그리고 HashiCorp Vault 프로바이더 하나를 제공합니다. AWS Secrets Manager, AWS KMS, GCP Secret Manager 리졸버는 후속 작업으로 남아 있습니다.

현재

시크릿은 다음 방법으로 라우터에 도달합니다.

  • 환경 변수 대체: src/infrastructure/config/secrets.rs의 resolve_secret_value가 설정 값 안의 ${VAR}, $VAR, ${VAR:-default}를 확장합니다.
  • 로컬 파일: 백엔드 auth.key_file(예: 서비스 계정 JSON)과, 클라이언트 키를 위한 api_keys.api_keys_file입니다.
  • 허브가 관리하는 백엔드 credential: 백엔드 동기화를 켠 허브 등록 라우터에서는 허브가 작성한 백엔드 스냅샷이 credential 참조를 담고, 허브가 이를 해석해 전달합니다(src/control_plane/backend_sync.rs, #1261). 이 경로는 아래 설계와 별개로 유지됩니다.
  • 시크릿 매니저 참조: credential 필드에 secret://<path>#<field>를 넣으면, 라우터가 설정 로드 시점과 핫 리로드 시점에 설정된 프로바이더에서 값을 해석합니다. 프로바이더는 이 두 시점에만 접속하며, 요청마다 접속하지 않습니다.

출시된 설계

설정

secrets:
  provider: vault
  vault:
    address: https://vault.example.com
    token: ${VAULT_TOKEN}
    mount: secret
    kv_version: 2
    namespace: null
    timeout_seconds: 10
    ca_cert_file: null
  • 이 섹션은 선택 사항입니다. 섹션이 없고 참조도 없으면 라우터는 프로바이더를 호출하지 않으며 기존과 똑같이 동작합니다.
  • provider의 기본값은 vault이며, 현재 구현된 프로바이더는 이것 하나입니다.
  • address는 https URL이어야 합니다. 평문 http는 루프백 호스트(localhost, 127.0.0.1, ::1)에서만 허용됩니다.
  • token 자체에 ${VAR} 대체를 사용할 수 있습니다. 이 값은 모든 Admin 보고서와 Debug 출력에서 마스킹됩니다.
  • mount의 기본값은 secret이며, 앞이나 뒤에 /를 붙일 수 없고 ..나 공백, \, %를 포함할 수 없습니다. 뒤의 두 문자는 참조 경로와 동일한 URL 정규화 규칙으로 거부합니다.
  • kv_version은 1 또는 2입니다. timeout_seconds는 1부터 120까지이고 기본값은 10입니다. namespace는 X-Vault-Namespace로 전송됩니다. ca_cert_file은 사설 CA용 PEM 루트를 추가합니다.
  • 형식이 잘못된 섹션은 경고가 아니라 로드 오류입니다.

참조 문법

  • 체계 마커는 정확히 secret://입니다. 문자열 전체가 이 마커로 시작할 때만 참조로 취급하므로, Bearer secret://...는 리터럴로 남습니다.
  • 문법은 secret://<path>[#<field>]입니다. <path>는 mount 기준 상대 경로이며, 비어 있지 않고, 공백이나 제어 문자, 선행 /, .. 세그먼트, ?, \, %를 포함하지 않습니다. 뒤의 두 문자를 거부하는 이유는 URL 정규화가 백슬래시를 경로 구분자로, 퍼센트 인코딩된 점 세그먼트를 ..로 바꾸어 요청을 mount 밖으로 옮길 수 있기 때문입니다.
  • <field>는 시크릿 데이터 객체의 최상위 키를 선택하며, Vault에서는 필수입니다. 라우터는 필드가 하나뿐이라고 추측하지 않습니다.
  • 해석된 값은 JSON 문자열이어야 합니다. 필드가 없거나, 문자열이 아니거나, 경로가 비었거나, 객체가 아닌 값에 필드를 지정하면 모두 오류입니다.
  • Vault KV v2는 GET <address>/v1/<mount>/data/<path>를 읽고 data.data에서 필드를 꺼냅니다. KV v1은 GET <address>/v1/<mount>/<path>를 읽고 data에서 꺼냅니다. 두 경우 모두 X-Vault-Token과, 설정된 경우 X-Vault-Namespace를 보냅니다.

참조를 허용하는 필드

  • 설정을 마스킹하는 규칙과 동일한 기준으로 판단한 credential 위치에만 허용합니다. api_key, secret, token, password, key_file, AWS 및 OAuth credential 필드, redis.url, 헤더 맵, prompt_logging 엔드포인트와 패턴, alerting.webhook.url, rate_limiting.scoped_bypass_keys[].key가 여기에 해당합니다.
  • api_keys 섹션은 별도로 처리합니다. 항목의 key, api_key, value, secret은 해석하지만, id, name 등 메타데이터는 해석하지 않습니다.
  • 그 밖의 위치에 있는 secret:// 값은 필드 경로를 밝히는 로드 오류입니다. 해석되지도, 리터럴로 남지도 않습니다.
  • secrets 섹션 자체 안의 참조는 부트스트랩 순환이 되므로 로드 오류입니다.

해석 시점

  • 설정 로드 중, ${VAR} 대체 뒤이자 타입 역직렬화 앞에서 해석합니다. 시작, 핫 리로드, Admin 히스토리 복원, 그리고 CLI의 유효 설정 경로(config validate, config show --resolved, config diff)가 모두 참조를 해석합니다.
  • 기본 config show, MCP 경로, 마스킹된 설정 히스토리 화면은 참조를 리터럴로 유지하며 프로바이더에 접속하지 않습니다.
  • 타입이 지정된 Config에 secret://로 시작하는 문자열이 남아 있으면 최종 검증 가드가 이를 거부합니다. 파일 로더를 우회하는 Admin API, WebUI, 컨트롤 플레인 변경을 잡기 위한 장치입니다.

실패 정책

  • 모든 실패는 점 표기 필드 경로와 참조 경로 및 필드를 밝히는 로드 오류입니다. 메시지에는 해석된 값이 절대 포함되지 않습니다. 예시는 backends[0].api_key: secret reference secret://openai#api_key could not be resolved: ...입니다.
  • 해석할 수 없는 참조는 리터럴 문자열로 대체되지 않으므로, 라우터는 실패 시 차단(fail closed) 방식으로 동작합니다.
  • 리로드가 실패하면 이전 설정을 유지하므로, 트래픽은 마지막으로 정상 동작한 credential을 계속 사용합니다.
  • secrets 섹션이 없는데 참조가 있으면 로드 오류입니다.
  • 런타임은 프로바이더 이름과 해석 건수만 기록하며, 값은 기록하지 않습니다.

마스킹

  • 마스킹 규칙은 참조가 나타날 수 있는 모든 위치를 이미 포함합니다. 따라서 credential이 아닌 위치의 참조는 보고서에 마스킹 없이 노출되는 값이 아니라 오류가 됩니다.
  • secrets.vault.token은 기존 token 규칙으로 마스킹되며, provider, address 등 나머지 비밀 아닌 설정은 그대로 읽을 수 있습니다.
  • 해석된 값은 로그, Debug 출력, Admin 설정 GET 및 내보내기, 설정 보고서, config diff 출력에 나타나지 않습니다.

로테이션

  • 로드할 때마다 시크릿을 다시 읽습니다. 로드 간 캐시도, TTL 캐시도 없습니다.
  • 로테이션된 시크릿은 재시작 없이 다음 리로드에서 반영됩니다.
  • 해석된 값은 실행 중인 Config에만 존재하며 디스크에 다시 기록되지 않습니다.

빌드 피처

  • secret-manager는 Cargo 피처로, full과 공식 릴리스 바이너리에 포함되며 크레이트 의존성을 추가하지 않습니다. 리졸버는 기존 reqwest를 사용합니다.
  • secrets 설정 타입은 항상 컴파일되므로, 설정 파일은 어떤 피처 조합에서도 동일하게 검증됩니다.
  • 피처 없이 빌드한 바이너리는 런타임이 비활성임을 경고하며, secret:// 값은 여전히 로드 오류입니다.
  • 이 피처는 embed에 포함되지 않으며, embed의 의존성 그래프는 바뀌지 않습니다.

Admin과 MCP

  • secrets 섹션은 시작 시점 소유입니다. PUT과 PATCH /admin/config/secrets는 409 STARTUP_OWNED_SECTION을 반환하며, 이 섹션을 바꾸는 적용 및 롤백 후보도 같은 응답을 받습니다.
  • 설정 내보내기에서는 이 섹션을 제외하고, 가져오기 후보가 섹션을 생략하면 실행 중인 섹션을 유지합니다.
  • MCP 핫 리로드 표면은 secrets를 requires_restart로 분류합니다.
  • Admin API와 WebUI 변경으로는 참조를 설정할 수 없습니다. 런타임 API가 참조를 해석할 수 없기 때문입니다. 소스 설정을 수정한 뒤 리로드하십시오.

남은 프로바이더

  • AWS Secrets Manager, AWS KMS, GCP Secret Manager 리졸버는 구현되어 있지 않습니다.
  • 새 프로바이더를 추가하려면 SecretResolver 트레이트를 구현하고 provider 열거형과 팩토리를 확장하면 됩니다. 참조 체계, 트리 순회, 실패 정책, 마스킹 규칙은 그대로 유지됩니다.