RKE2 CySec Secret/PKI 공급 경계

이 변경은 bloodbag-cysec namespace의 실제 Secret/PKI를 반복 가능하고 감사 가능하게 공급하기 위한 lane을 등록한다. 기준은 GitOps tirosh-chain/tirosh-gitops source-revision b8408ca2e1db0cea8db8a9fe804fef824b41a2b2(canonical activation overlay merge)의 소비 계약과, 기존 registration-only preflight다. 계약은 SHA-256 digest(2bcc26f6...)로 고정해 source contract drift를 fail-closed로 감지한다.

핵심 원칙은 하나다. 비밀값(secret/cert/password byte)은 Git·로그·명령 출력·테스트 fixture 어디에도 평문으로 쓰지 않는다. 공급은 환경 변수를 통한 external-encrypted material에서만 이뤄지고, 그 material이 실제로 없으면 생성자를 임의로 만들지 않고 fail-closed로 막는다.

계약 요약 (source-bound)

registration(sites/tirosh-home/rke2/bloodbag-cysec-secret-pki-registration.yaml)은 다음을 고정한다.

  • namespace: bloodbag-cysec
  • source: tirosh-chain/tirosh-gitops, source-revision: b8408ca2e1db0cea8db8a9fe804fef824b41a2b2
  • contract-state: registration-only-blocked, activation-ready: false
  • rotation-owner-state: blocked-unassigned, rotation-runbook-state: blocked-unregistered, issuer-state: blocked-unassigned
  • secret-git-state: absent-by-design, pki-git-state: absent-by-design

Secret 22개와 principal 16개는 exact match로 검증된다.

  • registry pull secret 1: bloodbag-cysec-registry (kubernetes.io/dockerconfigjson, .dockerconfigjson)
  • PostgreSQL TLS 1: bloodbag-postgres-tls (SAN bloodbag-postgres.bloodbag-cysec.svc)
  • Redis TLS 2: bloodbag-signal-ingress-redis-tls, bloodbag-internal-processing-redis-tls (각 SAN은 해당 headless service FQDN)
  • nginx Ingress TLS 1: bloodbag-cysec-product-api-tls (SAN cysec.bloodbag.internal.tirosh.ai)
  • PostgreSQL bootstrap 1: bloodbag-postgres-bootstrap (admin-password)
  • DB principal 4: bloodbag_product_api, bloodbag_inference_context, bloodbag_super_admin_bootstrap, bloodbag_migration(blocked, bloodbag-database-migrationdatabase-password)
  • Redis principal 12: signal-ingress 4 + internal-processing 8 (각각 password key 1개, ACL user와 1:1)

실제 infra 조사 결과

  • rke2/gpu 클러스터에는 SOPS/age/Vault/ExternalSecrets/cert-manager/sealed-secrets/ClusterIssuer가 설치되어 있지 않다. 확인된 Secret/PKI 관련 CRD는 없다.
  • bloodbag-cysec namespace의 workload(Deployment/StatefulSet/Service/SA)는 GitOps로 이미 배포되어 0/0 상태로 Secret을 기다리고 있다. Secret은 아직 하나도 없다.
  • 기존 dev/stg 공급은 sites/tirosh-home/secrets.toml + 환경변수 + kubectl apply 방식이다. 이 매커니즘은 cysec에도 재사용 가능하지만, 22개 lease Secret은 별도 external-encrypted 계약을 따르므로 전용 material registry로 분리한다. 기존 dev/stg entry와 StorageClass, cluster context는 불변이다. Demo Console adjunct Secret(token·Product API User)만 secrets.toml에 추가하며, 절차는 RKE2 CySec Demo 운영자 Secret을 본다.

material registry

sites/tirosh-home/rke2/bloodbag-cysec-secret-pki-material.toml은 22개 Secret의 환경변수 참조만 선언한다. 값은 절대 없고, BLOODBAG_CYSEC_* 환경변수 이름만 있다. 실값은 apply 시점에 외부(운영자의 승인된 vault/secret manager)에서 주입한다.

  • TLS kubernetes.io/tls 4개: BLOODBAG_CYSEC_{POSTGRES,SIGNAL_INGRESS_REDIS,INTERNAL_PROCESSING_REDIS,PRODUCT_API}_TLS_{CRT,KEY,CA}
  • registry 1개: BLOODBAG_CYSEC_REGISTRY_USERNAME, BLOODBAG_CYSEC_REGISTRY_TOKEN (server ghcr.io)
  • PostgreSQL bootstrap/DB/Redis password 17개: BLOODBAG_CYSEC_*_PASSWORD (각 binding 1개)

공개 Make Interface

make rke2/cysec-secret-pki/preflight SITE=tirosh-home
make rke2/cysec-secret-pki/plan      SITE=tirosh-home
make rke2/cysec-secret-pki/apply     SITE=tirosh-home
make rke2/cysec-secret-pki/verify    SITE=tirosh-home
make rke2/cysec-secret-pki/rollback  SITE=tirosh-home \
  RKE2_BLOODBAG_CYSEC_SECRET_PKI_ROLLBACK_CONFIRMATION=bloodbag-cysec
  • preflight: registration만 검증. source/schema/keys/SANs/principals/blocked 상태 exact match, YAML alias·merge·duplicate key 거부, symlink·2MiB 경계·TOCTOU 방지.
  • plan: material registry가 계약과 정확히 일치하는지(strict closed schema) 검증하고, 값은 모두 REDACTED로 렌더링한다. materialReady가 false면 빠진 환경변수 이름만 보고한다.
  • apply: 정확한 context(tirosh-home/rke2/gpu)·pinned API server(https://172.31.0.20:6443)·검증된 kubeconfig descriptor를 요구하고, 모든 kubectl 호출에 --kubeconfig/--context를 명시한다. O_EXCL mode0400 operation lock을 획득한 뒤 material/TLS/docker semantics를 모두 preflight하고, 기존 Secret은 resourceVersion CAS(kubectl replace), 부재 Secret은 create-only(kubectl create)로 단일 writer를 보장한다(AlreadyExists race는 overwrite 없이 실패). 22개 전체에 --dry-run=server를 통과시킨 뒤 하나씩 적용하며, pre-apply uid/resourceVersionmetadata-only receipt(JSON, mode0400, 값 없음)에 기록한다. partial failure는 terminal failed receipt에 적용된 이름만 남기고 자동 rollback은 하지 않는다.
  • verify: 기본은 metadata-only다. 클러스터 Secret의 name/type/key names/managed-by label만 조회한다(go-template, -o json으로 value를 읽지 않는다). value equivalence는 --verify-values로 명시할 때만 정확히 22개 name을 하나씩(개별 GET) 로컬 digest로 비교한다.
  • rollback: 비활성화되어 있다. prior Secret byte를 안전한 authorized backup에 보관하지 않으므로, 현재 값을 재-materialize해 "rollback"으로 위장하지 않는다. --confirm 이후에도 항상 fail-closed로 종료하며 수동 복구를 안내한다. 복구 절차는 아래 "수동 복구"를 참고한다.

fail-closed 경계

  • 외부 material(환경변수/파일)이 하나라도 없으면 apply는 non-zero로 종료하고 빠진 이름만 출력한다. 임의 생성은 하지 않는다.
  • TLS 인증서를 발급해 줄 PKI issuer(cert-manager 등)가 설치·승인되어 있지 않으므로, 인증서 material은 외부에서 주입돼야 한다. issuer는 만들지 않는다.
  • TLS material은 cryptography로 PEM parse, key/cert match, CA chain(CA BasicConstraints ca=true + keyCertSign + validity), SAN exact equality(추가 SAN 금지), EKU exact [serverAuth], ECDSA P-256, validity/rotation overlap을 검증한다. password/registry는 nonempty·entropy bound·ghcr.io exact endpoint를 검증한다.
  • material registry는 closed schema다. root/secret/dockerconfigjson/환경변수 inline table의 모든 depth에서 임의 field, annotation, inline material, registry email, 환경변수 cross-principal 재사용, 32 refs 불일치를 거부한다. 파일 입력은 hardlink/parent symlink/FIFO/device/TOCTOU를 거부한다.
  • rotation owner/runbook/issuer assignment는 저장소 근거가 없어 blocked로 유지한다. 이는 외부 blocker이며, 결정 없이 추측하지 않는다.
  • plan/apply/verify/rollback 모두 값 평문을 stdout/stderr/log에 쓰지 않는다. stringData 값의 C0 control(0x00-0x1f)은 LF/TAB을 제외하고 전부 거부한다.

수동 복구 (rollback 비활성화)

prior Secret byte를 안전한 authorized backup에 보관하지 않으므로, 자동 rollback은 제공하지 않는다. 잘못된 적용을 되돌리려면:

  1. 문제가 된 Secret 22개 중 어떤 name이 오염됐는지 make rke2/cysec-secret-pki/verify로 확인한다.
  2. 승인된 material source(offline CA/secret manager)에서 원하는 버전의 값을 다시 준비한다.
  3. make rke2/cysec-secret-pki/apply로 재적용한다(기존 Secret은 resourceVersion CAS를 통과해야 한다).

자동 롤백을 원하면 prior byte의 authorized backup 체계를 별도로 승인·구현한 뒤 rollback lane을 다시 활성화해야 한다.

nginx Ingress 경로

bloodbag-cysec의 product-api는 nginx Ingress를 통해 내부 hostname cysec.bloodbag.internal.tirosh.ai로 노출된다. 이 hostname 전용 TLS leaf가 새 계약에 추가됐다.

  • Ingress hostname: cysec.bloodbag.internal.tirosh.ai (SAN exact, 추가 SAN 금지)
  • TLS Secret: bloodbag-cysec-product-api-tls (kubernetes.io/tls, tls.crt/tls.key/ca.crt)
  • leaf: ECDSA P-256 / SHA-256, EKU exact [serverAuth], full-chain(intermediate + root), validity 90일, rotation overlap 30일(T-30 갱신)
  • Ingress manifest는 tirosh-gitops가 관리한다. 여기서는 TLS Secret의 exact material만 공급하며, hostname↔Secret 바인딩은 bloodbag-cysec-product-api-tls의 SAN으로 고정된다.
# tirosh-gitops가 렌더링하는 Ingress (참고용, 이 repo에서 관리하지 않음)
spec:
  tls:
    - hosts: [cysec.bloodbag.internal.tirosh.ai]
      secretName: bloodbag-cysec-product-api-tls
  rules:
    - host: cysec.bloodbag.internal.tirosh.ai
      http:
        paths:
          - path: /
            pathType: Prefix
            backend:
              service:
                name: bloodbag-product-api
                port:
                  name: http

이 leaf도 나머지 3개 leaf와 동일한 plan/apply/verify/rotate 경계를 통과하며, SAN exact·serverAuth·P-256/SHA256·full-chain·rotation/overlap·descriptor-safe material 경계를 재사용한다.

검증

make interface/check SITE=tirosh-home
UV_CACHE_DIR=.uv-cache uv run ruff check packages/infra-tools/src packages/infra-tools/tests
UV_CACHE_DIR=.uv-cache uv run pytest packages/infra-tools/tests
make docs/build

테스트는 exact schema, duplicate/unknown/symlink/path/TOCTOU, wrong namespace/context, dry-run server 검증, no shell/env secret leakage를 Red→Green으로 고정한다.

현재 상태와 다음 단계

  • registration preflight와 공급 lane(plan/apply/verify/rollback)은 모두 구현·검증되어 있다.
  • 실제 배포는 외부 material(인증서·password·registry token)과 PKI issuer 정책이 결정된 뒤에만 진행한다. material 없이 apply를 실행하면 fail-closed가 정상이다.