Files

33 KiB
Raw Permalink Blame History

Phase 1·2 검증

render-phase1.shrender-phase2.sh는 클러스터를 변경하지 않고 각 단계의 구성을 렌더링한 뒤 핵심 불변 조건을 확인한다. Phase 2 검증은 먼저 Phase 1 검증을 재실행하므로 두 단계의 경계도 함께 확인한다.

bash scripts/validate/render-phase1.sh
bash scripts/validate/render-phase2.sh

helmPATH에 없거나 검증용 바이너리를 별도로 내려받았다면 절대 경로를 지정할 수 있다.

PLATFORM_HELM_BIN=/tmp/helm-v3.19.4/helm \
  bash scripts/validate/render-phase2.sh

PLATFORM_HELM_BIN은 실행 가능한 일반 파일의 절대 경로여야 하며, 지정하지 않으면 PATH에서 helm을 찾는다. 어느 경로를 사용하든 버전은 정확히 v3.19.4여야 한다.

render-phase1.sh --verified-output-dir는 일반 사용자가 직접 호출하는 출력 옵션이 아니라 apply-phase1-gitea.shapply-gitea-oidc.sh가 함께 사용하는 내부 handoff 전용이다. 기존의 비어 있는 /tmp/platform-phase1-apply.* 디렉터리만 허용하며, 경로가 심볼릭 링크이거나 현재 사용자 소유가 아니거나 mode 0700이 아니면 중단한다. 검증을 모두 통과한 경우에만 다음 여섯 manifest를 mode 0600으로 복사하고 원본과 cmp로 다시 비교한다.

  • namespaces.yaml
  • ssd-local-pv.yaml
  • cnpg-operator.yaml
  • platform-postgres.yaml
  • gitea.yaml
  • gitea-oidc.yaml

apply-phase1-gitea.sh는 여섯 산출물의 SHA-256을 메모리에 고정하고 확인 후, 실제 적용 대상인 namespaces.yaml부터 gitea.yaml까지 다섯 manifest를 각각 apply하기 직전에 다시 검사한다. gitea-oidc.yaml은 적용하지 않는다.

apply-gitea-oidc.sh는 같은 handoff의 파일 수·권한·소유권을 확인하되, gitea-oidc.yaml의 SHA-256만 적용 대상으로 고정하고 재확인한 뒤 그 하나만 적용한다. 두 경로 모두 검증 뒤 Chart를 다시 내려받거나 Kustomize를 다시 실행하지 않는다. 취소·오류·INT·TERM 종료 시 각 적용 스크립트가 자신의 handoff 디렉터리 전체를 삭제한다.

공통 검증 기준은 다음과 같다.

  • 로컬 kubectl의 Kustomize가 Argo CD 3.4.2와 같은 v5.8.1인지 확인
  • Helm이 Argo CD 3.4.2와 같은 v3.19.4인지 확인
  • sha256sumtar가 설치되어 있는지 확인
  • helm pull로 고정 버전 패키지를 공식 저장소에서 임시 디렉터리로 받은 뒤 SHA-256으로 바이트 단위 무결성 확인
  • CloudNativePG Chart 0.29.0의 SHA-256이 668e065ff53508d58238788fd35b355a925060843629a951df0e6a9362e6d32f인지 확인
  • Gitea Chart 12.7.0의 SHA-256이 5881ef9c59400bee2d5547e77c4cd0efb925143c2f5d93fb4f38446db76b0167인지 확인
  • Helm Chart는 반드시 --enable-helm과 기본 LoadRestrictionsRootOnly로 렌더링

Phase 1은 CloudNativePG CRD, Gitea용 Cluster·DatabaseRole·Database, Gitea PVC·Ingress를 확인한다. Ingress의 git.learn.hyeonworks.com host, 내부 TLS 부재, 외부 HTTPS ROOT_URL, 애플리케이션 NodePort·LoadBalancer와 Gitea SSH Service 부재도 검증한다. Phase 1 PostgreSQL 빌드 루트에는 Keycloak 리소스·Secret 참조· namespace ingress 허용이 없어야 한다.

Gitea는 두 프로필을 모두 렌더하되 적용 경계를 분리해 검증한다.

  • services/giteagitea.yaml: 신규 설치용 baseline. OIDC Secret·ID host alias·Keycloak egress·브랜딩이 없어야 하고 자체 가입을 닫은 로컬 비상 로그인만 유지한다.
  • services/gitea/profiles/oidcgitea-oidc.yaml: 공통 values/baseline.yamlvalues/oidc.yaml을 병합한다. 정확한 OIDC Secret 참조, public issuer, 외부 인증 전용 가입, CoreDNS 기반 public issuer 접근, /32 egress와 해시된 브랜딩 ConfigMap·read-only mount를 확인한다.

공유 Cluster의 pg_hba도 네 규칙의 순서와 값을 검증한다. gitea Role은 gitea DB, keycloak Role은 keycloak DB에만 SCRAM으로 접속을 허용하고, 각 Role이 다른 DB에 접속하는 경우에는 바로 다음 규칙에서 reject해야 한다.

Phase 2는 다음을 추가로 검증한다.

  • 공식 Keycloak 26.7.0 원격 리소스와 Keycloak 단일 인스턴스·외부 HTTPS hostname·내부 HTTP·Traefik Ingress
  • Keycloak PostgreSQL 증분 루트에 DatabaseRole·Database·추가형 NetworkPolicy만 있고 Cluster 사본은 없는지 확인
  • AIStor Operator Chart 5.10.0 SHA-256 e5534f5ae4f6f12a3528a8cda5954d73280cba1a8e979e1628fbf3aac76babd1
  • AIStor ObjectStore Chart 1.0.16 SHA-256 50ffa6a4e014cdc48566b237593baab41d039f103cdc50c1b1ac173b8d8bf71e
  • AIStor XFS Local PV의 Retain·WaitForFirstConsumer·900Gi·node affinity
  • ObjectStore 서버 1개·볼륨 1개, PVC 보호, S3·Console ClusterIP, 외부 Ingress·NodePort·LoadBalancer 부재
  • Phase 2 소스와 렌더 결과의 자격 증명·라이선스 Secret 부재

Secret 검사는 관리 대상 YAML 전체에 적용하되, 렌더 과정에서 생성되고 Git에서 무시되는 업스트림 Chart 캐시인 **/.helm/****/charts/**는 제외한다.

스크립트는 /tmp/platform-phase1-render.* 아래에만 중간 산출물을 만들고 종료 시 삭제한다. 따라서 helm pull로 받은 임시 .tgz도 종료 시 제거된다. 각 Helm 렌더 직전에는 다이제스트를 통과한 패키지만 아래의 정확한 생성 캐시 버전 디렉터리에 압축 해제한다.

  • infrastructure/controllers/cloudnative-pg/.helm/charts/cloudnative-pg-0.29.0/cloudnative-pg/Chart.yaml
  • services/gitea/profiles/oidc/.helm/charts/gitea-12.7.0/gitea/Chart.yaml

Phase 2도 같은 방식으로 AIStor Operator와 ObjectStore의 정확한 버전 캐시만 일시 생성한다. 스크립트는 해당 버전 디렉터리가 이미 존재하거나 캐시 상위 디렉터리가 심볼릭 링크면 이를 임의 삭제·덮어쓰지 않고 중단한다. 패키지 다이제스트가 일치한 뒤에만 정확한 디렉터리를 생성하며, 자신이 생성한 버전 디렉터리는 EXIT 종료 정리에서 제거한다. .helm/charts.helm도 이번 실행에서 생성했고 비어 있을 때만 rmdir로 정리한다. 기존의 다른 캐시는 삭제하지 않는다.

정상 종료와 처리되는 INT·TERM에서는 생성한 압축 해제 캐시와 /tmp 렌더 산출물이 제거된다. 강제 종료 등으로 정확한 캐시가 남으면 다음 실행은 안전하게 거부하므로 운영자가 경로와 내용을 확인한 뒤에만 수동 정리한다. 패키지가 없거나 다이제스트가 다르거나 압축 해제 후 Chart.yaml이 없으면 렌더 전에 즉시 실패한다. 실제 적용, namespace 생성, Secret 변경, Nginx 변경은 수행하지 않는다.

내부 handoff 모드를 사용해도 검증 스크립트 자신의 Chart package, generated cache와 작업용 render 디렉터리는 동일하게 정리된다. 보존되는 것은 적용 스크립트가 만든 제한된 임시 디렉터리의 검증 완료 manifest 여섯 개뿐이며, 적용 스크립트 종료 시 함께 제거된다.

관측성 코어 정적 render 검증

render-observability-core.sh는 Helm v3.19.4, Kustomize v5.8.1, Kubernetes render target 1.36.2와 계획에 기록된 여섯 chart archive/name/version/appVersion, 열두 image digest를 고정한다. 기본 실행은 정적 render만 수행하며 Kubernetes resource를 적용하지 않는다. live 관측성 mutation gate가 닫힌 동안에는 fixture 시험만 실행한다.

bash -n scripts/validate/render-observability-core.sh
bash -n scripts/validate/test-render-observability-core.sh
bash scripts/validate/test-render-observability-core.sh

fixture 시험은 chart를 다운로드하지 않고 source된 assertion 함수에 변형 YAML을 전달한다. 성공 표식은 OBSERVABILITY CORE RENDER ASSERTION TEST PASS다. 전체 renderer는 Task 2 이후 child root가 존재할 때 다음과 같이 사용한다.

PLATFORM_HELM_BIN=/home/donghyeon/.local/bin/helm \
  bash scripts/validate/render-observability-core.sh

내부 --verified-output-dir는 비어 있는 current-user 소유 mode 0700/tmp/platform-observability-core-apply.*만 허용한다. 모든 검증과 child/aggregate canonical resource equivalence가 끝난 뒤에만 manifest 열네 개와 resource-index.tsv를 regular mode 0600 파일로 넘긴다. Grafana와 Blackbox child manifest도 각각 grafana.yaml, blackbox.yaml로 포함한다. handoff directory의 device/inode를 고정하고 쓰기 직전에 identity와 empty 상태를 다시 확인하며, 각 파일은 완성된 mode 0600 staging inode를 exact name에 no-clobber link하는 방식으로 공개한다. 최종 entry set은 열일곱 exact name뿐이어야 한다. 각 chart cache는 모든 render root의 physical lineage를 먼저 확인한 뒤 directory FD 기준 exclusive mkdir로 이번 실행의 정확한 version 경로만 만들고, 기존 경로를 덮어쓰지 않으며, 종료 시 실제로 생성·추적한 경로만 제거한다. 실행 전부터 존재한 빈 .helm이나 .helm/charts는 제거하지 않는다. cache cleanup은 tracked directory를 random no-replace quarantine name으로 옮긴 뒤 identity를 다시 확인하고, 모든 child를 directory FD 기준으로 같은 방식으로 격리해 제거한다. cleanup 도중 원래 path에 replacement가 생겨도 이를 순회하거나 삭제하지 않으며 identity mismatch나 unexpected entry는 성공 종료로 숨기지 않는다.

Alertmanager Slack source 계약은 Secret payload를 읽지 않는 focused validator로 검사한다. 기본 모드는 source와 Kustomize/KPS wiring만 검사하며, 선택적 server dry-run도 두 non-Secret source만 admission에 제출하고 live object를 변경하지 않는다.

bash scripts/validate/test-observability-alerting.sh
bash scripts/validate/test-observability-alerting.sh --server-dry-run

render-observability-access.sh는 core renderer의 검증된 Grafana·Blackbox·targets· dashboard·rule·Alertmanager bytes와 private DNS를 access 단계의 일곱 manifest로 분리한다. rules-alerts와 complete mode는 두 metric inventory phase가 있는 direct /tmp/platform-observability-metrics.XXXXXX mode 0700 디렉터리만 허용한다. 두 inventory directory는 mode 0700, JSON/checksum은 owner mode 0600, link count 1과 exact checksum이어야 한다. 게시 전 모든 destination absence와 root inode를 고정하고, 모든 source를 mode 0600 staging inode에 동기화한 뒤 no-clobber link한다. 한 파일이라도 실패하면 이번 실행이 생성한 파일을 전부 제거해 부분 handoff를 남기지 않는다.

bash -n scripts/validate/render-observability-access.sh
bash scripts/validate/test-render-observability-access.sh
bash scripts/validate/render-observability-access.sh \
  --component complete \
  --verified-output-dir /tmp/platform-observability-metrics.XXXXXX

출력에는 Secret kind나 credential payload가 없으며, 이미 게시된 output root를 재사용하면 no-clobber로 즉시 거부한다. live 적용은 별도 apply transaction과 Slack Secret, HTTPS runbook URL, source-proof/acceptance evidence가 모두 준비된 뒤에만 수행한다.

Slack webhook KeePass 복구 사본

backup-slack-webhook-recovery.sh는 Alertmanager Slack webhook의 재해 복구용 암호화 사본을 같은 host의 별도 내장 Windows SSD에 있는 기존 KDBX에 보관한다. 이 사본은 일반 K3s 재시작이나 host reboot에 필요하지 않으며, datastore·Secret·bootstrap state를 잃었을 때를 위한 것이다. 기본 no-argument 실행은 고정 contract만 출력하고 SSD, KDBX, webhook, sudo에 접근하지 않는다. 지원되는 public interface는 다음 두 형식뿐이다.

bash scripts/bootstrap/backup-slack-webhook-recovery.sh
bash scripts/bootstrap/backup-slack-webhook-recovery.sh \
  --execute \
  --slack-webhook-file /home/donghyeon/.secrets/alertmanager/slack-webhook

execute는 전용 TTY에서 Slack app name과 KeePassXC master password를 받고, write 또는 mismatch update 전에 정확한 확인 문자열을 요구한다. webhook payload, master password, 그 hash·encoding·size·URL component, KDBX protected output은 argv, environment, stdout, log 또는 plaintext 파일에 출력하거나 기록하지 않는다.

검증된 read-only exact match는 SLACK_KEEPASS_RECOVERY=NOOP, 검증된 변경은 SLACK_KEEPASS_RECOVERY=COMMITTED를 출력한다. 두 성공 분기 모두 SSD source가 실제로 unmount된 뒤 WINDOWS_SSD_UNMOUNTED=PASSOFF_HOST_RECOVERY_SATISFIED=NO를 출력한다. 변경 분기는 durable non-clobbering pre-change backup을 만든 뒤 KDBX_PRECHANGE_BACKUP=CREATED도 출력한다. rename 시도 뒤 응답 손실, post-commit 검증 실패, 또는 cleanup/unmount 불명은 재시도하지 않고 SLACK_KEEPASS_RECOVERY=MANUAL_RECOVERY_REQUIRED로 중단하며 main과 backup을 보존한다. private tmpfs work root, socket, helper process와 mount source의 제거·부재 증명 전에는 성공 token을 출력하지 않는다.

이 same-host encrypted copy는 off-host escrow가 아니며 Phase 4의 off-host recovery gate를 충족하거나 승인하지 않는다.

관측성 Secret create-only bootstrap

create-observability-secrets.sh는 기본 실행에서 고정 contract만 출력하며 cluster, sudo, payload file에 접근하지 않는다. execute는 선택한 grafana-adminalertmanager-slack-webhook의 exact Opaque key set과 기존 payload 일치를 확인하며, 기존 payload가 다르면 rotation 없이 중단한다. 둘을 함께 선택하면 한 create-only transaction으로 처리한다.

bash scripts/bootstrap/create-observability-secrets.sh
bash scripts/bootstrap/create-observability-secrets.sh \
  --execute --grafana-admin \
  --grafana-admin-user-file /absolute/current-user-0600/admin-user \
  --grafana-admin-password-file /absolute/current-user-0600/admin-password
bash scripts/bootstrap/create-observability-secrets.sh \
  --execute --slack-webhook \
  --slack-webhook-file /absolute/current-user-0600/slack-webhook
bash scripts/bootstrap/create-observability-secrets.sh \
  --check-grafana-recovery-evidence
bash scripts/bootstrap/create-observability-secrets.sh \
  --check-slack-recovery-evidence

execute는 confirmation 전과 create 직전에 k3s encryption/restore validator를 각각 새 process로 실행한다. 입력은 no-follow private snapshot으로만 create에 전달하고 값이나 hash를 출력하지 않는다. transaction 실패 시 이번 호출이 생성하고 UID를 캡처한 Secret만 Kubernetes API UID precondition으로 삭제한다. create 결과 또는 ownership이 모호하면 해당 object는 삭제하지 않고 MANUAL_RECOVERY_REQUIRED=YES로 중단한다. recovery marker는 root-owned mode 0600 네 필드만 허용하며 standalone check는 current-user kube context와 30일 age를 확인하고 marker stat/read에만 좁은 sudo를 사용한다.

fixture 회귀는 실제 production 함수와 private Unix-socket API precondition 경계를 실행하되 live Secret이나 실제 payload에는 접근하지 않는다.

bash -n scripts/bootstrap/create-observability-secrets.sh
bash scripts/validate/test-create-observability-secrets.sh

GrafanaKeycloak OIDC bootstrap 검증

configure-keycloak-grafana-oidc.sh는 인자 없이 실행하면 고정 client/group/mapper/Secret 계획만 출력하며 Kubernetes, Keycloak, sudo와 payload에 접근하지 않는다. execute는 정확히 default context, https://127.0.0.1:6443 API, Ready donghyeon-system-product-name node와 bounded authorization을 다시 고정한 뒤 loopback-only Keycloak Admin API와 private Unix-socket Kubernetes Secret API만 사용한다.

bash scripts/bootstrap/configure-keycloak-grafana-oidc.sh
bash scripts/bootstrap/configure-keycloak-grafana-oidc.sh --execute
bash scripts/bootstrap/configure-keycloak-grafana-oidc.sh \
  --execute --admin OBS_ADMIN_USER --viewer OBS_VIEWER_USER
bash scripts/bootstrap/configure-keycloak-grafana-oidc.sh \
  --check-recovery-evidence

--admin--viewer는 정확히 한 명의 기존 Keycloak user만 각 고정 group에 추가한다. 옵션이 없으면 임의 사용자를 만들거나 membership을 바꾸지 않는다. 기존 exact client, full-path groups mapper, 두 group과 matching observability/grafana-keycloak-oidc Secret은 credential rotation과 client/Secret rewrite 없이 재사용한다. declarative update가 필요한 기존 client는 현재 credential을 private file로 보존해 PUT과 rollback에 명시적으로 넣고, 전후 client-secret 관계를 payload 출력 없이 비교한다.

execute는 confirmation 전과 첫 mutation 직전에 encryption/restore validator를 각각 fresh /usr/bin/env -i process로 실행한다. APPLY defaultRECOVERY KEYCLOAK default 뒤에만 mutation을 시작한다. 실패나 HUP/INT/TERM은 exact snapshot과 transaction ledger로 이번 실행이 추가한 membership, mapper/client/group/Secret만 역순 복구한다. 생성 Secret 삭제는 UID와 resourceVersion precondition을 모두 사용한다. ownership 또는 response 결과가 모호하면 대상 삭제를 시도하지 않고 MANUAL_RECOVERY_REQUIRED=YES로 중단한다.

성공 시 /etc/hyeonworks/platform/recovery-evidence/keycloak.env에는 schema, context, resource, checked-at UTC 네 non-secret field만 root:root 0600으로 atomic 기록한다. standalone check는 current-user kube context와 exact marker schema, no-follow/link metadata와 30일 age를 검사하고 marker 접근에만 좁은 sudo를 사용한다.

focused fixture는 stateful fake Keycloak Admin API와 raw Kubernetes Secret API를 통해 실제 production state machine을 실행한다. create/update/no-op, duplicate cardinality, membership ownership, response loss, conflict, timeout, rollback과 signals를 검증하되 live cluster, sudo, 실제 Secret을 읽거나 변경하지 않는다.

bash -n scripts/bootstrap/configure-keycloak-grafana-oidc.sh
bash -n scripts/validate/test-configure-keycloak-grafana-oidc.sh
bash scripts/validate/test-configure-keycloak-grafana-oidc.sh

Phase 3 비공개 관리 UI 검증

render-admin-services.sh는 pgAdmin OCI Chart 9.16.0과 AIStor ObjectStore Chart 1.0.16 archive SHA-256을 다시 검증한다. pgAdmin main과 두 init image가 모두 9.16 amd64 digest로 바뀌었는지, 렌더 결과에 Secret kind가 없는지, Recreate·2Gi Retain Local PV·두 admin Ingress와 namespace 간 NetworkPolicy가 유지되는지 확인한다.

PLATFORM_HELM_BIN=/home/donghyeon/.local/bin/helm \
  bash scripts/validate/render-admin-services.sh

실제 적용 뒤 admin-ui-smoke.sh --execute --run-s3로 공개 admin DNS 부재, LAN·Tailscale·Pod DNS, 인증서 SAN, 외부 403, loopback NodePort, pgAdmin 비밀번호 저장 차단과 AIStor S3 회귀를 확인한다. 허용·비허용 Keycloak 사용자 브라우저 시험은 수동 수용 시험으로 남긴다.

임시 network smoke Pod는 BusyBox 1.37.0의 amd64 manifest digest를 고정하고 non-root UID, RuntimeDefault seccomp, 전체 capability drop, allowPrivilegeEscalation=false, read-only root filesystem과 service account token 미마운트를 적용한다. platform-admin의 Pod Security restricted 정책을 우회하지 않는다.

로컬 K3s recovery 저장소 읽기 전용 검증

k3s-local-recovery.sh는 recovery SSD의 고정 hardware identity, SMART/NTFS 상태와 mount → container → loop → LUKS2 → ext4 lineage를 읽기 전용으로 판정한다. 실행 전에 같은 terminal에서 sudo credential을 미리 준비해야 하며, validator 자체가 암호를 요구하거나 package를 설치하지 않는다.

sudo -v
bash scripts/validate/k3s-local-recovery.sh --expect-device-ready
bash scripts/validate/k3s-local-recovery.sh --expect-closed
bash scripts/validate/k3s-local-recovery.sh --expect-open
bash scripts/validate/k3s-local-recovery.sh --expect-open --check-latest-bundle

기대 상태는 정확히 하나만 지정한다. --expect-device-ready--expect-closed는 두 platform mount, 운영·proof mapping과 container 관련 loop가 모두 없는 상태만 허용한다. device-ready 검사는 canonical recovery partition이 다른 mountpoint에도 source로 쓰이지 않고 ntfs-3g.probe --readwrite가 성공해야 한다. --expect-open은 그 partition이 승인된 outer mount 한 곳에만 연결되고, ntfs3nodev,nosuid,noexec와 안전한 umask=077 또는 동등한 dmask=0077,fmask=0077을 가져야 한다. 또한 canonical parent chain과 container inode, loop backing inode/device, zero offset/size limit, LUKS2 mapper와 두 mount의 major:minor가 하나의 lineage여야 하며 마지막에 같은 snapshot을 다시 확인한다. 완전 할당된 고정 크기 container, ext4 label과 root:root 0700 inner root도 모두 필수다. dirty/hibernated NTFS를 고치거나 force mount하는 동작은 없다. root EUID나 상속된 xtrace 상태에서는 workspace path/config를 읽기 전에 거부한다.

--check-latest-bundle은 open 상태에서만 쓸 수 있다. inner root의 root-owned mode 0600 .latest-post-bundle.env는 다음 세 key를 정확히 한 번씩 가져야 한다.

schema=k3slr-latest-post-bundle-v1
relative_path=k3s-secrets-encryption-YYYYMMDDTHHMMSSZ/post
directory_identity=DEVICE:INODE

validator는 이 제한된 relative path와 directory identity를 전후로 재확인하고, root-owned post bundle.env schema와 안전한 relative-name manifest를 검사한다. manifest의 각 parent와 leaf는 symlink가 아닌 pinned directory 내부 object여야 하며, regular-file device/inode를 hash 전후에 확인하면서 각 target을 개별 sha256sum으로 검증한다. 성공 출력은 다음 네 분류뿐이며 stable ID, UUID, serial, WWN, loop/KDBX/bundle path와 payload를 표시하지 않는다.

Recovery device: match
Recovery state: device_ready|closed|open
Lineage: match
Latest bundle: verified|not_checked

fixture 회귀는 system command 경계만 argv log를 남기는 fake로 바꾸고 실제 collector와 parser를 호출한다. latest verifier는 임시 일반 directory의 valid, malformed, duplicate, symlink escape, hash mismatch bundle을 직접 검사하며 live mount나 block device를 만들지 않는다.

bash scripts/validate/test-k3s-local-recovery.sh

KeePassXC → cryptsetup anonymous-pipe feasibility

k3s-local-recovery-feasibility.sh는 recovery 저장소를 만들기 전에 고정 package와 KeePassXC CLI의 synthetic KDBX 동작을 확인한다. 요구 version은 keepassxc 2.7.6+dfsg.1-1build3, cryptsetup-bin 2:2.7.0-1ubuntu4.2이며 executable도 root-owned regular non-symlink, group/other non-writable 조건을 만족해야 한다.

bash scripts/validate/k3s-local-recovery-feasibility.sh
sudo -v
bash scripts/validate/k3s-local-recovery-feasibility.sh --execute

기본 실행은 package/executable prerequisite만 판정한다. --execute는 interactive stdin과 같은 terminal의 cached sudo credential을 요구하며, inherited xtrace나 root EUID에서는 workspace library를 읽기 전에 거부한다. 이 mode가 만드는 것은 current-user 0700 /tmp/k3slr-feasibility.* 아래 synthetic KDBX와 attachment round-trip fixture뿐이다. 실제 KeePass DB, LUKS file, loop device, mapping, mount와 K3s 상태는 읽거나 변경하지 않는다. 성공·실패·INT·TERM 모두 exact fixture directory를 정리한다.

실제 recovery open/format API는 고정 entry의 Password attribute를 shell 변수, command substitution, argv, environment, file, log 또는 tee에 넣지 않는다. producer stdout은 anonymous kernel pipe FD로만 전달된다. 검증된 caller TTY FD를 producer stdin에 명시적으로 연결하고 producer 내부에서도 TTY를 재검증한다. producer output은 streaming od와 bounded mawk validator가 전부 drain하며 non-shell process 안에 최대 40 byte만 유지한다. producer, EOF, exact 40자+LF/class 검증과 KDBX 전체 parent lineage 재검증이 모두 성공한 뒤에만 정확한 41 byte가 parent pipe로 전달되고 다음 두 고정 consumer 중 하나가 읽기 시작한다. producer supervisor는 시작 직후 STOP handshake를 수행하고 caller와 다른 실제 PGID를 trusted ps로 확인한 뒤에만 pipeline을 시작한다. 이때 /proc/PID/stat의 start time, direct parent, observed PGID를 함께 고정하며, negative group signal 직전마다 동일 identity와 caller PGID를 다시 확인한다. INT/TERM trap은 pending state만 기록하고 lifecycle checkpoint가 TTY/pipe FD를 닫은 뒤 bounded TERM, 필요 시 KILL, direct-child wait/reap과 group 소멸 확인을 수행한다. /proc reader는 정확히 LF 하나로 끝나는 단일 record와 EOF를 요구하고 CR, 추가 record, non-canonical PID/PPID/PGID/starttime을 거부한다. PID publication 직후 STOP query보다 먼저 direct-child identity를 pin하며, 이 최초 pin 자체가 실패하면 worker를 CONT하지 않고 freshly published direct PID에만 positive KILL한 뒤 wait한다. same-caller/query-failure에서는 group signal 없이 STOP된 exact direct child만 positive TERM/CONT/KILL한다. wait/reap과 PID/PGID/identity clear는 pending signal이 stale group state를 소비할 수 없는 하나의 transition으로 처리한다. coprocess PID가 PGID라고 가정하지 않는다. supervisor는 initial STOP 전에 child-side INT/TERM cancellation deferral을 설치한다. cleanup TERM을 받으면 worker를 시작하거나 종료하지 않고 ownership anchor로 남아, TERM-ignore descendant가 있더라도 fresh authority로 group KILL과 direct wait를 완료할 수 있게 한다.

/usr/bin/sudo --non-interactive -- /usr/sbin/cryptsetup luksFormat --batch-mode --type luks2 --key-file=- LOOP
/usr/bin/sudo --non-interactive -- /usr/sbin/cryptsetup open --type luks2 --key-file=- LOOP ALLOWED_MAPPING

production helper는 outer mount부터 exact current-user 0600 regular non-symlink KDBX까지 모든 component가 canonical non-symlink인 것, library-owned entry, canonical loop block device, allowlisted mapping과 cached sudo를 요구한다. fixture suite는 partial/zero producer failure의 consumer dispatch 0, xtrace 선차단, non-TTY, exact KeePassXC argv, process argv/environment와 stdout/stderr/tmp/run 누출, real blocking-child signal reap, exclusive synthetic lifecycle과 cleanup failure 전파를 low command-boundary fake로 검사한다. signal fixture는 launch, PID publish, STOP/query, CONT 전후, wait/reap-clear, consumer 직전 경계에서 INT/TERM을 결정적으로 주입하고 supervisor, process-substitution shell, KeePassXC, od, mawk와 feasibility nested roles의 종료, caller/unrelated sentinel 생존, consumer dispatch 0과 반복-run leak 0을 확인한다. synthetic KDBX의 addattachment-import는 atomic-save inode 교체를 허용하되, 각 mutation 직후 exact path/owner/mode/non-symlink를 다시 확인하여 secure 새 inode를 baseline으로 삼는다. read-only 단계에서는 baseline과 fixture parent identity가 바뀌면 즉시 실패한다. 고정 package가 없는 host에서는 --execute를 실행하거나 임의 version을 설치하지 않고 명시적으로 SKIP한다.

k3s Secret 암호화 live 읽기 전용 검증

k3s-secret-encryption.sh는 k3s 서비스나 Kubernetes 리소스를 변경하지 않고 현재 Secret 암호화 상태, 서버 annotation 일치 여부, local config 무결성과 API readiness를 분류한다. 일반 사용자 shell에서 실행하면 제한 시간과 sudo --non-interactive가 적용된 개별 read-only 명령만 root 전용 systemd 환경, datastore 증거와 credential 파일을 읽는다. workspace의 validator나 library 자체를 root shell에서 실행하거나 source하지 않는다. /usr/local/bin/k3s, /usr/bin/systemctl, /usr/bin/stat 등 허용된 절대경로의 root-owned·non-group/other-writable system binary만 검증한 뒤 실행한다. raw status, 환경 변수 원문, annotation, hash, active key, token, password 또는 config 내용은 사용자 terminal, 명령 인자나 handoff에 출력하지 않고 현재 사용자 프로세스의 메모리에서만 분류한다.

validator는 비대화형 sudo만 사용하므로 실행 직전에 같은 terminal에서 credential을 먼저 갱신해야 한다. 다른 terminal에서 실행한 sudo -v는 이 실행의 prerequisite를 충족한다고 가정하지 않는다.

sudo -v

credential validation이 실패하면 validator는 root-only evidence를 하나도 읽지 않고 sudo -v를 같은 terminal에서 실행한 뒤 다시 시도하라는 오류로 즉시 종료한다.

bash scripts/validate/k3s-secret-encryption.sh
bash scripts/validate/k3s-secret-encryption.sh --expect-disabled
bash scripts/validate/k3s-secret-encryption.sh --expect-transition-start
bash scripts/validate/k3s-secret-encryption.sh --expect-enabled
bash scripts/validate/k3s-secret-encryption.sh --expect-reencrypted

기대 상태 옵션은 최대 하나만 지정한다. 옵션이 없으면 inventory만 수행하며 status가 unsafe_transition, hash_mismatch, invalid로 분류되면 실패한다. 모든 성공 경로는 version이 정확히 v1.36.2+k3s1, server가 정확히 1개, server node가 정확히 donghyeon-system-product-name이고 Ready인 것을 요구한다. --expect-enabled는 운영 진단용으로 Enabled/startEnabled/reencrypt_finished를 모두 허용한다. Phase 4 완료 gate인 --expect-reencryptedEnabled/reencrypt_finished이면서 local config, state, server annotation hash가 모두 일치할 때만 성공한다. --expect-transition-start도 status의 hash match가 확인된 exact transition만 허용한다. 기대 상태 불일치와 분류할 수 없는 결과, API readyz 실패는 non-zero로 종료한다.

검증된 비민감 handoff가 필요하면 caller가 먼저 제한된 임시 디렉터리를 만든다.

VERIFY_DIR="$(mktemp -d /tmp/platform-k3s-encryption.XXXXXX)"
chmod 0700 "$VERIFY_DIR"
bash scripts/validate/k3s-secret-encryption.sh --expect-enabled \
  --verified-output-dir "$VERIFY_DIR"

validator는 handoff 디렉터리를 만들지 않는다. slash가 없는 nonempty suffix를 가진 /tmp/platform-k3s-encryption.<suffix> physical direct child 중 기존 empty, non-symlink, 현재 사용자 소유 mode 0700 디렉터리만 허용한다. 검증과 write는 같은 열린 directory FD에 묶는다. inventory와 기대 상태 검증이 성공한 뒤에만 noclobber와 umask 077로 정확히 inventory.env, status.sha256 두 파일을 만들며, 두 파일은 현재 사용자 소유 regular file mode 0600이어야 한다. status 원문과 active key 이름은 어느 handoff에도 쓰지 않는다. status.sha256은 status stdout의 공백이나 trailing newline이 아니라 jq -cS로 검증·정렬·압축한 single JSON value의 UTF-8 bytes(끝 newline 없음)에 대한 SHA-256이다. 기존 파일, 추가 파일, nested/symlink parent, 다른 owner 또는 group/other 권한이 있으면 실패한다.

k3s Secret 복구 증거 검증

k3s-secret-encryption-restore-evidence.sh는 다음 세 mode를 제공한다.

bash scripts/validate/k3s-secret-encryption-restore-evidence.sh \
  --emit-result --bundle-metadata BUNDLE_METADATA_FILE --output RESULT_FILE
bash scripts/validate/k3s-secret-encryption-restore-evidence.sh \
  --record --bundle-metadata BUNDLE_METADATA_FILE --result-file RESULT_FILE
bash scripts/validate/k3s-secret-encryption-restore-evidence.sh --check

--emit-result는 격리 복구 host에서 current-user kube context와 live API를 확인하고 Enabled/reencrypt_finished, server hash, local integrity, node Ready, version/backend와 복구 전후 Secret object count가 모두 맞을 때만 non-sensitive result를 만든다. operator가 default route, upstream DNS, 운영 API/datastore route와 모든 egress 차단 시험까지 완료했다고 현재 context로 확인해야 한다. 결과에는 destroyed가 없다.

--record는 외부 post metadata/result, 24시간 age와 운영 host의 권위 post metadata 일곱 field 전체를 비교한다. 일회용 복구 환경과 bundle 복사본 파기 후 정확한 확인 입력을 받아야 하며 기존 evidence를 덮어쓰지 않는다. --check는 현재 권위 bundle-id, version/backend, 30일 age, destroyed=confirmed, live re-encryption과 local integrity를 읽기 전용으로 다시 검사한다. 2026-08-09 live 전환은 Enabled/reencrypt_finished, hash·integrity·API·node 검사, post bundle 기록과 최신 marker 검증까지 통과했다. 다만 격리 restore·파기 evidence는 아직 없으므로 --check 성공을 기록하거나 관측성 Phase 4 gate를 열지 않는다.

외부 입력은 현재 사용자 소유 regular non-symlink, mode 0600, non-empty여야 한다. exact allowlist parser가 duplicate/unknown/empty key, control character, malformed/trailing data, $(와 backtick을 sudo 전에 거부한다. 전체 script를 sudo로 실행하지 않고 root 전용 파일의 stat/read/install만 좁게 승격한다. result/evidence target은 같은 directory의 0600 temporary file을 검증한 뒤 기존 파일을 덮어쓰지 않는 atomic install로 만든다.

전체 수동 절차와 격리 checklist는 k3s Secret 복구 drill을 따른다. parser와 세 mode의 fixture 회귀는 live cluster에 연결하지 않고 다음 명령에 포함된다.

bash scripts/validate/test-k3s-secret-encryption-status.sh