Files

562 lines
33 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Phase 1·2 검증
`render-phase1.sh``render-phase2.sh`는 클러스터를 변경하지 않고 각 단계의
구성을 렌더링한 뒤 핵심 불변 조건을 확인한다. Phase 2 검증은 먼저 Phase 1
검증을 재실행하므로 두 단계의 경계도 함께 확인한다.
```sh
bash scripts/validate/render-phase1.sh
bash scripts/validate/render-phase2.sh
```
`helm``PATH`에 없거나 검증용 바이너리를 별도로 내려받았다면 절대 경로를
지정할 수 있다.
```sh
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.sh``apply-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`인지 확인
- `sha256sum``tar`가 설치되어 있는지 확인
- `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/gitea``gitea.yaml`: 신규 설치용 baseline. OIDC Secret·ID host
alias·Keycloak egress·브랜딩이 없어야 하고 자체 가입을 닫은 로컬 비상 로그인만
유지한다.
- `services/gitea/profiles/oidc``gitea-oidc.yaml`: 공통
`values/baseline.yaml``values/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 시험만
실행한다.
```sh
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가 존재할 때 다음과 같이 사용한다.
```sh
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를 변경하지 않는다.
```sh
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를 남기지 않는다.
```sh
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는 다음 두 형식뿐이다.
```sh
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=PASS`
`OFF_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-admin`
`alertmanager-slack-webhook`의 exact `Opaque` key set과 기존 payload 일치를 확인하며,
기존 payload가 다르면 rotation 없이 중단한다. 둘을 함께 선택하면 한 create-only
transaction으로 처리한다.
```sh
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에는 접근하지 않는다.
```sh
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만 사용한다.
```sh
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 default`
`RECOVERY 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을 읽거나 변경하지 않는다.
```sh
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가
유지되는지 확인한다.
```sh
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를 설치하지 않는다.
```sh
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 한 곳에만 연결되고, `ntfs3``nodev,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를 정확히 한 번씩 가져야 한다.
```text
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를
표시하지 않는다.
```text
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를 만들지
않는다.
```sh
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 조건을 만족해야 한다.
```sh
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를 완료할 수 있게 한다.
```text
/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의 `add``attachment-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를
충족한다고 가정하지 않는다.
```sh
sudo -v
```
credential validation이 실패하면 validator는 root-only evidence를 하나도 읽지 않고
`sudo -v`를 같은 terminal에서 실행한 뒤 다시 시도하라는 오류로 즉시 종료한다.
```sh
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/start`
`Enabled/reencrypt_finished`를 모두 허용한다. Phase 4 완료 gate인
`--expect-reencrypted``Enabled/reencrypt_finished`이면서 local config, state,
server annotation hash가 모두 일치할 때만 성공한다. `--expect-transition-start`
status의 hash match가 확인된 exact transition만 허용한다. 기대 상태 불일치와 분류할 수
없는 결과, API readyz 실패는 non-zero로 종료한다.
검증된 비민감 handoff가 필요하면 caller가 먼저 제한된 임시 디렉터리를 만든다.
```sh
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를 제공한다.
```sh
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](../../bootstrap/manual/k3s-secret-encryption-restore-drill.md)을 따른다.
parser와 세 mode의 fixture 회귀는 live cluster에 연결하지 않고 다음 명령에 포함된다.
```sh
bash scripts/validate/test-k3s-secret-encryption-status.sh
```