562 lines
33 KiB
Markdown
562 lines
33 KiB
Markdown
# 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
|
||
```
|
||
|
||
## Grafana–Keycloak 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
|
||
```
|