본문으로 건너뛰기

셀프서비스 시크릿 QA 가이드 — 워크스페이스 시크릿 매니저

이 문서는 Pipeline Studio의 워크스페이스 시크릿(Intel 셀프서비스 수집 Slice D, Epic #2286)을 QA가 검증할 수 있도록 정리한 것입니다. 사용자가 운영자 Vault CLI 없이 UI에서 인텔 소스용 API 키/토큰을 등록하고, rest_source 노드가 인증 헤더에서 {{secret.<name>}}로 참조하면 실행 시 egress-proxy가 복호화해 외부 호출에 주입합니다.

개요 — 왜 DB Fernet인가

기존엔 secret-bearing 소스(예: DART)를 쓰려면 운영자가 Vault에 키를 넣어야 했습니다. 메모리 교훈(#1853: prod에서 Vault-via 흐름이 실제로 동작하지 않아 AI 제공자 키를 DB Fernet 암호화 + UI 등록으로 전환)에 따라, 인텔 소스 시크릿도 이미 prod에서 도는 Fernet 스택을 재사용합니다. (Vault 경로 vault_secret_path/vault.py는 미래 고객 통합용으로 dormant 유지.)

사용자(ws-admin) → UI 시크릿 매니저 → POST /api/v1/ps/secrets {name, value}
→ encrypt_provider_key(value) → ws_secrets.value_encrypted (Fernet, ws-fence)
노드 config: headers={"Authorization":"Bearer {{secret.dart}}"}, secret_refs={dart:"db://dart"}
실행: Dagster rest_source → egress-proxy /fetch (secret_refs, ws_id, X-Internal-Token)
→ egress: secret_refs "db://" 분기 → POST /ps/internal/resolve-secret {ws_id, name}
→ gend-api: WHERE workspace_id AND name → decrypt → 평문 반환
→ egress: {{secret.dart}} 헤더 치환 + echo 탐지 → 외부 호출. 평문은 egress 메모리에만(I1).

UI 검증 (rest_source 노드)

  1. Pipeline Studio에서 rest_source 노드를 추가하고 설정 패널을 연다.
  2. "워크스페이스 시크릿" 접이식 섹션:
    • 등록: name(영문 시작, 영숫자/언더스코어 1~80자) + value(비밀번호 입력) → "추가". 등록 후 입력값은 즉시 비워지고, 목록에 name + "등록됨" 배지만 표시된다(평문/암호문은 절대 화면·로그에 안 나옴).
    • 참조 토큰 복사: 각 시크릿 옆 복사 버튼 → {{secret.<name>}}이 클립보드에 복사된다.
    • 삭제: 확인 창(참조 노드가 실행 시 실패할 수 있음 경고) 후 삭제.
  3. 노드 헤더 설정의 값에 {{secret.<name>}}을 붙여넣으면(예: Authorization: Bearer {{secret.dart}}), 저장 시 secret_refs{dart: "db://dart"}자동 병합된다(기존 항목은 보존).
  4. 권한: 시크릿 등록/삭제는 워크스페이스 관리자(/tenants/<slug>/admins 그룹) 또는 글로벌 admin이 가능. 비-관리자는 친절한 권한 토스트(서버 상세 미노출). 글로벌 admin 은 X-Workspace-Slug 헤더로 대상 워크스페이스를 명시적으로 선택해야 하며(상단바 선택 시 UI 가 자동 첨부 — 선택된 ws 로 귀속), 헤더 없는 호출은 JWT 폴백 멤버십이 있어도 422 로 거부된다(조용한 귀속·기본 테넌트 silent fallback 모두 없음). 모든 시크릿 요청은 감사 레코드에 tenant_slug 로 귀속이 기록되며, /admin/audit 화면과 GET /api/v1/audit/logs?tenant_slug=<slug> 필터로 조회할 수 있다(비멤버 ws 를 선택한 admin 은 impersonation 경고 추가). M2M 서비스 토큰은 admin 역할이 있어도 403 — 시크릿 관리는 사람(UI) 전용.

prod end-to-end 실증 (2026-06-25)

finance-invest 워크스페이스 + 임시 ws-admin 계정으로 실증(검증 후 전 아티팩트 정리):

단계결과
시크릿 등록(POST /ps/secrets)201, has_value: true (Fernet 암호문만 DB 저장)
복호화 콜백(POST /ps/internal/resolve-secret, 내부토큰)200, 반환 평문 == 등록 값 (register→Fernet→콜백→복호화 정확)
ws-fence(다른 ws로 같은 name)404 (cross-ws 복호화 구조적 불가)
egress 주입(/fetch + {{secret.x}} → echo 서비스)502 SECRET_ECHO — 복호화된 값이 외부 요청 헤더에 실제 주입돼 upstream 도달(echo 서비스가 반향) + 누출 가드가 차단. 실 인증 API는 키를 반향하지 않으므로 정상 200
무토큰 콜백403 (fail-closed)

읽는 법: SECRET_ECHO는 "시크릿이 주입됐다"는 증명입니다. 누출 가드(_detect_secret_echo)는 주입한 raw 값이 응답에 반사되면 차단해 키가 외부로 새는 것을 막습니다. echo 테스트 서비스는 모든 요청 헤더를 반향하므로 항상 차단되지만, 실제 인증 API는 응답에 키를 담지 않으므로 정상적으로 200을 반환합니다.

UI 단계별 가이드 — 실 prod 브라우저 풀플로우 (2026-06-25)

아래는 finance-invest 워크스페이스에서 실제 브라우저로 인텔 적재 파이프라인을 만들고 실행한 캡처입니다(검증 후 전 아티팩트 정리). 빌드→시크릿 등록→실행→적재→MCP 등록까지 UI만으로 완료됩니다.

1) rest_source + intel_bronze_sink 드래그 + 연결 — 팔레트에서 두 노드를 캔버스에 놓고 핸들을 드래그해 엣지 연결.

두 노드 드래그 엣지 연결

2) rest_source 설정 — URL(공개 JSON) + records_path. 헤더에 {{secret.<name>}}로 시크릿을 참조한다.

rest_source 설정

3) 워크스페이스 시크릿 등록 (Slice D) — rest_source 설정의 "워크스페이스 시크릿" 섹션을 펼쳐 name+value(비밀번호 입력) 등록. 등록 후 값은 즉시 비워지고 "등록됨" 배지만 표시(평문 미노출).

시크릿 섹션 시크릿 등록됨

4) intel_bronze_sink 설정source/domain/external_id_path + mcp_tool(JSON: tool_name/description). 실행 시 이 도구가 자동 등록된다.

sink 설정

5) 실행 → 적재 성공 — 저장·배포 후 실행하면 두 노드가 green(ok). bronze에 실데이터가 적재되고 MCP 도구가 등록된다.

실행 성공 실행 이력

이 풀플로우의 검증 결과(공개 JSON 소스): UI 빌드(2노드+1엣지) OK · 실행 두 노드 ok · bronze intel_<domain>_raw에 3행 실데이터 적재(workspace_id stamp) · MCP 도구 자동 등록(query_intel_<domain>). 적재 위치는 기존 intel 수집과 동일한 공유 iceberg.bronze.intel_* 테이블이며, 새 도메인은 새 테이블(intel_<domain>_raw)로 분리되고 workspace_id 컬럼으로 테넌트 격리됩니다.

E2E 스펙: ui/tests/self-service-secrets-e2e.spec.ts(시크릿 매니저 UI, QA_CAPTURE/E2E_PS_MUTATE 게이팅). 풀 적재 캡처는 토큰주입 스크립트로 수행.

보안 모델 (적대 리뷰 검증됨)

PR2(콜백)는 머지 전 적대 보안리뷰(5 렌즈 × 3-refuter, 11 공격)를 거쳤고, cross-ws 복호화·ws_id 위조·평문 누출·fail-open은 전부 반증됐습니다.

  • 암호화 at-rest: Fernet AES(GEND_PROVIDER_KEY_ENC_KEY). config_json 평문 저장 금지.
  • write-only: value는 응답·로그·감사로그·Dagster run 어디에도 안 나감. GET은 has_value만.
  • ws-fence(I5): WHERE workspace_id AND name — caller ws 소유 시크릿만 복호화(콜백 자체가 cross-ws 불가). ws_id는 run→ws 서버 파생(D10).
  • 콜백 fail-closed: X-Internal-Token 불일치/누락 → 403. 콜백 실패 → egress SecretResolveError → 외부 호출 안 함(fail-safe).
  • echo 누출 차단: 주입 값이 응답에 반사되면 502 차단.
  • 잔여(follow-up): resolve-secret rate-limit + 전용 토큰 분리(decrypt-oracle 하드닝). 현재 decrypt-oracle 접근은 감사 로그(ws+name)로 탐지.

운영 — 암호화 키 회전 런북

시크릿은 GEND_PROVIDER_KEY_ENC_KEY(env, K8s Secret gend-provider-key-enc, Azure KV kv-gend-prod-recovery 백업)로 암호화됩니다. 이 키는 LLM 제공자 키(llm_providers.api_key_encrypted)와 공유되므로, 회전 시 두 테이블의 모든 행을 재암호화해야 합니다.

⚠️ 키 분실 = 전 시크릿 복호화 불가(복구 불가). Azure KV 백업 무결성을 DB 백업만큼 엄격히 관리할 것.

회전 절차(운영자):

  1. 새 Fernet 키 생성: python -c "from cryptography.fernet import Fernet; print(Fernet.generate_key().decode())".
  2. MultiFernet로 무중단 회전(권장): 코드를 MultiFernet([new, old])로 구성하면 복호화는 old/new 둘 다, 암호화는 new로 수행 → 점진 재암호화 가능. (현 단일 Fernet에서 MultiFernet 전환은 코드 변경 후속 작업.)
  3. 재암호화 백필: 각 행을 value = decrypt(old_ct); new_ct = encrypt_with_new(value)로 UPDATE.
    • 대상: ws_secrets.value_encrypted(전 행) + llm_providers.api_key_encrypted(전 행, NULL 제외).
    • 평문은 메모리에만, 로그 금지. 트랜잭션·배치 처리.
  4. Secret 교체: gend-provider-key-enc를 new 키로 갱신 + Azure KV 백업 갱신 → gend-api 롤아웃.
  5. 검증: 회전 후 resolve-secret 콜백 + LLM provider healthcheck로 복호화 정상 확인. old 키는 전 행 재암호화 완료 + 검증 후에만 폐기.

후속

  • rate-limit + resolve-secret 전용 토큰(decrypt-oracle 하드닝).
  • MultiFernet 무중단 키 회전 + 자동 재암호화 잡.