Add platform infrastructure configuration
This commit is contained in:
@@ -0,0 +1,25 @@
|
||||
# Argo CD 초기 구축 경계
|
||||
|
||||
Argo CD는 1차 Gitea 수동 구축과 플랫폼 저장소 푸시가 끝난 뒤 설치한다. 이
|
||||
디렉터리는 그 인계 지점을 보존하며, 현재 단계에는 설치 매니페스트가 없다.
|
||||
|
||||
후속 단계의 기준은 다음과 같다.
|
||||
|
||||
- Argo CD `v3.4.2`를 검토·고정한다.
|
||||
- 해당 버전에 포함된 Kustomize `v5.8.1`, Helm `v3.19.4`와 로컬 검증 버전을
|
||||
일치시킨다.
|
||||
- `argocd-cm`의 `kustomize.buildOptions`에는 `--enable-helm`만 설정한다.
|
||||
- `--load-restrictor LoadRestrictionsNone`는 사용하지 않는다.
|
||||
- 공식 Chart 원본을 수정하거나 저장소에 복제하지 않는다.
|
||||
- Application의 소스 경로는 각 실제 빌드 루트를 가리킨다.
|
||||
- `infrastructure/controllers/cloudnative-pg`
|
||||
- `services/platform-postgres`
|
||||
- `services/gitea`
|
||||
- Namespace, CRD/Operator, PostgreSQL, Gitea의 의존 순서를 별도 Application과
|
||||
동기화 순서(sync wave)로 표현한다.
|
||||
- 수동 생성한 Secret은 Secret 관리 ADR이 확정되기 전까지 Argo CD 관리 대상으로
|
||||
가져오지 않는다.
|
||||
|
||||
기존 수동 리소스를 인계하기 전에는 적용할 차이가 삭제나 재생성을 유발하지 않는지 확인하고,
|
||||
특히 `Retain` Local PV/PVC와 PostgreSQL Cluster를 먼저 보호한다.
|
||||
|
||||
@@ -0,0 +1,127 @@
|
||||
# k3s Secret 암호화 복구 drill
|
||||
|
||||
이 문서는 승인된 k3s Secret hardening의 post recovery bundle을 실제 복구할 수 있는지
|
||||
검증하고, 파기 확인 뒤 운영 host에 비민감 evidence를 등록하는 수동 진입점이다. 운영
|
||||
server에서 restore를 실행하는 runbook이 아니며 서비스, datastore, backup 또는 Secret을
|
||||
자동으로 변경하지 않는다.
|
||||
|
||||
## 승인과 책임 경계
|
||||
|
||||
- 운영 장애 복구와 drill은 서로 다른 변경 승인으로 다룬다.
|
||||
- SQLite와 embedded etcd restore 명령은 이 저장소의 자동 실행 스크립트로 제공하지 않는다.
|
||||
- drill은 운영 host와 network, datastore, hostname이 격리된 일회용 VM/host에서 post bundle
|
||||
복사본으로만 한다.
|
||||
- recovery bundle에는 복구·복호화에 필요한 자료가 함께 있으므로 bundle 전체를 Secret으로
|
||||
취급한다. 경로, token/config 내용, payload, hash와 escrow 위치를 terminal 결과나 원장에
|
||||
기록하지 않는다.
|
||||
- result만 반출하고 일회용 환경과 bundle 복사본을 파기한 뒤 운영 host에서 evidence를
|
||||
등록한다.
|
||||
|
||||
## backend별 수동 복구 기준
|
||||
|
||||
SQLite 장애 복구 순서는 k3s 중지, 현재 DB의 timestamp quarantine 이동, 같은 server
|
||||
token·config와 검증된 backup DB 복원, k3s 시작이다. 현재 DB를 즉시 삭제하거나 backup으로
|
||||
덮어쓰지 않는다. 모든 단계는 별도 승인된 장애 runbook에서 사람이 대상과 rollback 지점을
|
||||
확인한다.
|
||||
|
||||
embedded etcd는 현재 K3s 공식
|
||||
[`--cluster-reset-restore-path` snapshot 복구 절차](https://docs.k3s.io/cli/etcd-snapshot#restoring-snapshots)를
|
||||
사용한다. 단일 server와 다중 server 절차, token/config 일치 조건, reset 후 정상 시작 조건을
|
||||
실행 시점의 공식 문서와 다시 대조한다. 보조 tutorial을 복구 권위로 사용하지 않는다.
|
||||
|
||||
## 일회용 환경의 격리 조건
|
||||
|
||||
복구 전에 다음 조건을 모두 만족시킨다.
|
||||
|
||||
- 운영 server와 다른 hostname, network와 datastore를 사용한다.
|
||||
- hypervisor private switch 또는 network namespace에 default route와 upstream DNS가 없다.
|
||||
- 원본 운영 API/datastore로 향하는 route가 없다.
|
||||
- host firewall egress가 default-deny다.
|
||||
- LAN, Internet, Slack, AIStor와 운영 API 연결 시험이 모두 실패한다.
|
||||
- 복구된 workload/controller가 격리망 밖으로 통신할 수 없고 외부 host port를 열 수 없다.
|
||||
- 운영과 같은 정확한 k3s version, server token과 config를 post bundle 복사본에서 사용한다.
|
||||
|
||||
복구 뒤에는 다음을 확인한다.
|
||||
|
||||
- k3s API ready
|
||||
- node Ready
|
||||
- encryption status가 `Enabled/reencrypt_finished`
|
||||
- server hash와 local integrity가 모두 일치
|
||||
- `kubectl get secrets --all-namespaces -o json` 조회가 오류 없이 완료
|
||||
- bundle metadata의 복구 전 전체 Secret object count와 복구 후 count가 일치
|
||||
- 위 격리 연결 시험을 다시 실행해 모두 실패
|
||||
|
||||
격리 조건과 live 검사가 모두 끝나기 전에는 result의 `isolation=pass`를 만들지 않는다.
|
||||
|
||||
## 세 mode 사용
|
||||
|
||||
모든 mode는 먼저 일반 사용자의 현재 kube context를 확정한다. 전체 script를 `sudo`로
|
||||
실행하지 않는다. root 권한은 운영 host의 권위 파일 stat/read/install에만 좁게 사용되므로
|
||||
필요하면 같은 terminal에서 사전에 sudo credential을 갱신한다.
|
||||
|
||||
격리 복구 host에서 post metadata와 결과 출력 위치를 직접 지정한다. 출력은 기존 파일을
|
||||
덮어쓰지 않는다.
|
||||
|
||||
```sh
|
||||
bash scripts/validate/k3s-secret-encryption-restore-evidence.sh \
|
||||
--emit-result --bundle-metadata BUNDLE_METADATA_FILE --output RESULT_FILE
|
||||
```
|
||||
|
||||
script는 live API ready, node Ready, `Enabled/reencrypt_finished`, server hash, local integrity,
|
||||
version/backend와 Secret object count를 직접 검사한다. 수동 격리 시험을 마친 현재 context를
|
||||
추가 확인한 뒤에만 result를 만든다. result에는 credential, 경로, 민감 filename이나 hash가
|
||||
없고 `destroyed`도 없다.
|
||||
|
||||
결과 파일을 운영 host로 반입한 뒤 일회용 환경과 bundle 복사본을 먼저 파기한다. 그 다음
|
||||
운영 host에서 등록한다.
|
||||
|
||||
```sh
|
||||
bash scripts/validate/k3s-secret-encryption-restore-evidence.sh \
|
||||
--record --bundle-metadata BUNDLE_METADATA_FILE --result-file RESULT_FILE
|
||||
```
|
||||
|
||||
`--record`는 post phase, metadata/result의 bundle-id·version·backend, 24시간 age와 운영
|
||||
host 권위 post metadata의 일곱 field 전체를 확인한다. evidence 대상이 이미 있으면 중단한다.
|
||||
prompt에는 정확히 `DESTROYED default`를 입력한다. 파기하지 않았거나 확인할 수 없으면
|
||||
등록하지 않는다.
|
||||
|
||||
Phase 4의 읽기 전용 gate는 다음과 같다.
|
||||
|
||||
```sh
|
||||
bash scripts/validate/k3s-secret-encryption-restore-evidence.sh --check
|
||||
```
|
||||
|
||||
`--check`는 exact evidence field, 현재 권위 bundle-id/version/backend, live
|
||||
`reencrypt_finished`와 local integrity, 30일 age, `destroyed=confirmed`를 모두 요구한다.
|
||||
2026-08-09 live 전환은 `Enabled/reencrypt_finished`, hash·integrity·API·node 검사,
|
||||
post bundle 기록과 최신 marker 검증까지 통과했다. 그러나 격리 restore·파기 evidence가
|
||||
아직 없고 lifecycle 자동화, off-host 복제와 암호화 escrow 검증도 미완료다. 따라서 실제
|
||||
운영 `--check`는 아직 성공으로 기록하지 않으며, 이 gate들이 완료되기 전에는 관측성
|
||||
Phase 4를 열지 않는다.
|
||||
|
||||
## 입력 파일 보안
|
||||
|
||||
외부 metadata/result는 현재 사용자 소유 regular non-symlink, mode `0600`, non-empty여야
|
||||
한다. parser는 exact allowlist `KEY=VALUE` 한 줄만 받아들이며 duplicate/unknown/empty key,
|
||||
control character, malformed line, trailing data, `$(`와 backtick을 거부한다. 파일을 shell로
|
||||
source하거나 `eval`하지 않는다. 입력 실패는 어떤 privileged install도 수행하기 전에
|
||||
종료한다.
|
||||
|
||||
result와 evidence는 같은 directory의 mode `0600` temporary file을 완성하고 exact field를
|
||||
다시 검사한 뒤 기존 target을 덮어쓰지 않는 atomic install로 만든다.
|
||||
|
||||
## 실행 원장
|
||||
|
||||
완료된 live hardening의 비민감 정정은
|
||||
[2026-08-08 복구 명령 원장](/home/donghyeon/workspace/docs/platform/runbooks/2026-08-08-k3s-local-recovery-command-log.md)의
|
||||
2026-08-09 addendum과
|
||||
[2026-08-09 로컬 정책 기록](/home/donghyeon/workspace/docs/platform/runbooks/2026-08-09-k3s-secret-encryption-local-policy.md)에
|
||||
있다. 아래 표는 아직 수행하지 않은 격리 restore drill의 실행 원장 형식이며, 완료된
|
||||
live hardening에 별도 Task 6 실행 runbook은 만들지 않았고, 이를 암시하지 않는다.
|
||||
|
||||
| 시각 | 사전 상태 | backend | backup 검증 | 명령 | 종료 코드 | 후속 상태 |
|
||||
|---|---|---|---|---|---:|---|
|
||||
|
||||
backup 위치, token/hash payload, 민감 파일 hash, encryption config 내용과 escrow 위치는
|
||||
`검증 완료` 또는 `미완료`로만 쓴다. bundle-id, restore drill 시각과 pass/fail은 비밀값이
|
||||
아니므로 기록할 수 있다.
|
||||
@@ -0,0 +1,118 @@
|
||||
# k3s Secret 암호화 수동 운영 절차
|
||||
|
||||
> **현재 경계(2026-08-09):** bootstrap의
|
||||
> `--recovery-policy local-separate-disk-luks`, 물리 디스크 lineage validator,
|
||||
> `LOCAL_RISK_ACCEPTED`, pre/post 동적 용량 gate는 구현·fixture 검증을 마쳤다.
|
||||
> 수동 LUKS header 복구 proof, recovery close·잔류 없음 검사와 closed validator를
|
||||
> 통과한 뒤 live 전환도 완료했다. 현재 Secret encryption은
|
||||
> `Enabled/reencrypt_finished`이며 hash·integrity·API·node 검사와 post bundle 기록,
|
||||
> 최신 bundle marker 검증이 모두 통과했다. lifecycle 자동화, 격리 restore drill,
|
||||
> off-host 복제와 암호화 escrow 검증은 별도 미완료 gate이므로 관측성 Phase 4를 열지 않는다.
|
||||
|
||||
이 문서는 단일 k3s server에서 Kubernetes Secret 저장 암호화를 fail-stop 방식으로
|
||||
전환하는 수동 절차다. 아래 상태 순서의 **live 전 역사적 기준선**은
|
||||
`v1.36.2+k3s1`, 단일 Ready 노드 `donghyeon-system-product-name`, SQLite, Secret
|
||||
encryption Disabled, API `readyz` pass였다. 현재 live 상태는 위 banner의
|
||||
`Enabled/reencrypt_finished`다. 모든 단계는 운영자가 명시적으로 실행하고 확인한다.
|
||||
자동화는 상태가 불명확하거나 검증이 실패하면 다음 단계로 진행하지 않는다.
|
||||
|
||||
## 상태 순서
|
||||
|
||||
다음 순서와 각 화살표 사이의 검증을 바꾸지 않는다.
|
||||
|
||||
```text
|
||||
disabled_no_config
|
||||
-> k3s secrets-encrypt enable
|
||||
-> drop-in install + restart
|
||||
-> transition_start + hashes_match
|
||||
-> k3s secrets-encrypt rotate-keys
|
||||
-> bounded wait for reencrypt_finished
|
||||
-> final restart
|
||||
-> Enabled/reencrypt_finished + server_hashes_match + local_integrity_match
|
||||
```
|
||||
|
||||
각 상태 전환 전후에는 Task 2 validator를 해당 기대 상태로 실행해 version, 단일
|
||||
Ready server, datastore, API `readyz`, status와 hash/integrity를 확인한다. validator
|
||||
출력이나 운영 로그에 server token, 비밀번호, encryption config 본문을 기록하지
|
||||
않는다.
|
||||
|
||||
## 사전 판정과 소유 위치
|
||||
|
||||
이미 `enabled_stable`로 분류되면 Task 1 effective-source resolver를 사용하여
|
||||
`ExecStart`, `Environment`/`EnvironmentFile`, systemd drop-in, default 또는
|
||||
alternate config 및 그 config drop-in을 순서대로 판정한다. 여기서
|
||||
`secrets-encryption`과 provider의 **유효 소유 위치만** 확인한다.
|
||||
|
||||
계획한 `40-secrets-encryption.yaml`이 아닌 기존 위치가 owner이면 그 위치를 그대로
|
||||
보존하고 config rewrite를 하지 않는다. 서로 충돌하는 두 owner, provider 판정 불가,
|
||||
또는 기존 provider가 `aescbc`가 아닌 경우에는 자동 변경하지 않는다. 이 경우에는
|
||||
별도 ADR을 먼저 승인해야 한다.
|
||||
|
||||
`enabled_stable`의 stage가 `reencrypt_finished`이면 rotation을 건너뛴다. stage가
|
||||
`start`이면 Phase 4 전에 `--rotate-existing`을 사용한 명시 승인 재암호화만 수행한다.
|
||||
|
||||
## Bootstrap 진입 명령
|
||||
|
||||
인자 없는 명령은 상태만 읽고 변경하지 않는다.
|
||||
|
||||
```sh
|
||||
cd /home/donghyeon/workspace/platform
|
||||
bash scripts/bootstrap/apply-k3s-secret-encryption.sh
|
||||
```
|
||||
|
||||
실제 전환 명령은 recovery volume이 열린 상태에서 backup root를 직접 지정하고 로컬
|
||||
정책을 명시한다. 다음 명령은 maintenance 승인 전에는 실행하지 않는다.
|
||||
|
||||
```sh
|
||||
cd /home/donghyeon/workspace/platform
|
||||
bash scripts/bootstrap/apply-k3s-secret-encryption.sh \
|
||||
--execute \
|
||||
--backup-root /srv/recovery/k3s \
|
||||
--recovery-policy local-separate-disk-luks
|
||||
```
|
||||
|
||||
실행 확인 순서는 `APPLY <context>` → 자동 root·lineage·용량 검증 →
|
||||
`RECOVERY <context>` → `ENCRYPTED <context>` →
|
||||
`LOCAL_RISK_ACCEPTED <context>`다. 어느 검사나 확인이 실패해도 다음 mutation으로
|
||||
진행하지 않는다. 비밀번호나 복구 키를 이 명령의 인자·환경변수로 전달하지 않는다.
|
||||
|
||||
## 실행 단계
|
||||
|
||||
1. `disabled_no_config`을 validator로 확인한 뒤에만 `k3s secrets-encrypt enable`을
|
||||
실행한다.
|
||||
2. repository의 host artifact를 root 소유, mode `0644`로 설치한다.
|
||||
|
||||
```sh
|
||||
install -o root -g root -m 0644 \
|
||||
infrastructure/security/k3s/40-secrets-encryption.yaml \
|
||||
/etc/rancher/k3s/config.yaml.d/40-secrets-encryption.yaml
|
||||
```
|
||||
|
||||
3. k3s를 재시작하고, `transition_start + hashes_match`가 validator로 확인될 때까지
|
||||
중단한다.
|
||||
4. 확인 뒤에만 `k3s secrets-encrypt rotate-keys`를 한 번 실행한다.
|
||||
5. 제한된 시간 동안 `reencrypt_finished`를 기다린다. 시간 초과, API 실패, hash
|
||||
mismatch 또는 local integrity mismatch이면 중단하고 조사한다.
|
||||
6. 완료 상태를 확인한 후 final restart를 하고,
|
||||
`Enabled/reencrypt_finished + server_hashes_match + local_integrity_match`를
|
||||
다시 확인한다.
|
||||
|
||||
> **금지 및 중단 조건**
|
||||
>
|
||||
> - `transition_start` 확인 전에는 `rotate-keys`를 실행하지 않는다.
|
||||
> - 중간 stage에서는 다른 rotation 명령을 실행하지 않는다.
|
||||
> - drop-in을 자동 삭제하지 않는다.
|
||||
> - live datastore 자동 restore 금지: 복구 판단과 수행은 별도 승인 절차다.
|
||||
> - server token 또는 encryption key material을 명령 인자, 로그, ticket, Git에
|
||||
> 남기지 않는다.
|
||||
|
||||
post bundle의 격리 복구, 결과 반출·파기와 evidence 등록은
|
||||
[k3s Secret 암호화 복구 drill](k3s-secret-encryption-restore-drill.md)을 따른다.
|
||||
|
||||
## 재시도와 복구 경계
|
||||
|
||||
상태가 `disabled_no_config`, `transition_start`, `enabled_stable` 중 하나로 명확히
|
||||
판정되지 않으면 재시도나 설정 변경을 하지 않는다. 특히 hash mismatch, provider
|
||||
불명확, owner 충돌, API `readyz` 실패는 자동 보정 대상이 아니다. 라이브 datastore를
|
||||
되돌리거나 Secret을 변경하는 동작도 이 절차의 권한 밖이며, 별도 ADR과 명시 승인을
|
||||
필요로 한다.
|
||||
@@ -0,0 +1,50 @@
|
||||
# Phase 1 Gitea 수동 부트스트랩 진입점
|
||||
|
||||
상세 절차와 검증·롤백 기준의 단일 원본은
|
||||
[중앙 Phase 1 Gitea bootstrap runbook](/home/donghyeon/workspace/docs/platform/runbooks/2026-07-22-phase1-gitea-bootstrap.md)입니다.
|
||||
이 파일은 저장소에서 실행 명령을 찾기 위한 짧은 진입점만 제공합니다.
|
||||
|
||||
2026-07-23 기준 Phase 1 클러스터 적용과 내부 health, Gitea restricted
|
||||
securityContext 검증을 완료했습니다. 이 진입점은 **신규 클러스터의 Gitea
|
||||
baseline 최초 구축 전용**입니다. baseline에는 Keycloak OIDC, `id` host alias,
|
||||
Keycloak 전용 egress 및 선언형 브랜딩을 포함하지 않습니다.
|
||||
|
||||
기존 `Deployment/gitea` 또는 `Secret/gitea-keycloak-oidc`가 있는 클러스터에는 이
|
||||
스크립트를 재실행하지 않습니다. 전용 안전장치도 둘 중 하나를 발견하면 적용을
|
||||
거부합니다. Keycloak OIDC와 브랜딩을 포함한 현재 목표 상태는 public discovery
|
||||
전환을 마친 뒤 `scripts/bootstrap/apply-gitea-oidc.sh --execute`로만 적용하며,
|
||||
2026-07-23 실제 클러스터에는 이 전용 경로로 적용을 완료했습니다. 현재
|
||||
OIDC-enabled 클러스터에서 baseline 스크립트를 복구 수단으로 재실행하지 않습니다.
|
||||
|
||||
## 실행 순서
|
||||
|
||||
저장소 루트 `/home/donghyeon/workspace/platform`에서 중앙 runbook을 확인한
|
||||
뒤 다음 순서를 지킵니다.
|
||||
|
||||
1. 전체 렌더·정적 검증
|
||||
|
||||
```sh
|
||||
bash scripts/validate/render-phase1.sh
|
||||
```
|
||||
|
||||
2. 신규 클러스터 Phase 1 baseline 최초 적용
|
||||
|
||||
```sh
|
||||
bash scripts/bootstrap/apply-phase1-gitea.sh --execute
|
||||
```
|
||||
|
||||
`apply-phase1-gitea.sh`는 안전장치로 검증을 다시 실행합니다. 검증기가 Chart
|
||||
SHA-256 확인을 마친 여섯 manifest를 제한된 `/tmp` handoff 경로로 받아
|
||||
`gitea.yaml` baseline만 적용하며, 함께 검증한 OIDC 목표 상태
|
||||
`gitea-oidc.yaml`은 적용하지 않습니다. 검증 뒤 Chart를 다시 내려받거나
|
||||
Kustomize를 다시 렌더링하지 않으며 취소·실패·신호 종료 때 임시 파일을
|
||||
정리합니다.
|
||||
|
||||
내부 5단계에서는 `create-phase1-secrets.sh --execute`를 호출합니다. helper는 세
|
||||
Secret이 모두 없을 때에만 최초 생성하고, 모두 있으면 값이 같은 완전한 계약인지
|
||||
검증한 뒤 그대로 재사용합니다. 일부만 존재하면 중단합니다. 자격 증명 회전은
|
||||
별도 runbook이 마련될 때까지 지원하지 않으므로 이 helper를 갱신 용도로 단독
|
||||
실행하지 않습니다.
|
||||
|
||||
OIDC 적용·자격 증명 회전·클러스터 검증·Nginx 전환·롤백 방법은 이 파일에
|
||||
복제하지 않고 중앙 runbook을 따릅니다.
|
||||
@@ -0,0 +1,139 @@
|
||||
# Phase 2 Keycloak·MinIO AIStor 수동 배포 진입점
|
||||
|
||||
상세 설계는 중앙 문서 `/home/donghyeon/workspace/docs/platform`에서 관리한다.
|
||||
이 파일은 선언의 의존 순서와 검증 진입점만 기록한다.
|
||||
|
||||
## 상태
|
||||
|
||||
2026-07-23 기준 Keycloak-only 경로는 DB Secret 두 사본, DatabaseRole·Database,
|
||||
additive NetworkPolicy, Operator·Server `26.7.0`, Ingress, `hyeonworks` realm,
|
||||
confidential `gitea` client와 OIDC Secret까지 실제 적용했다. Traefik 최소 trust도
|
||||
완료했다. Host Nginx의 `id.learn.hyeonworks.com` proxy와 Gitea OIDC·브랜딩 live
|
||||
적용도 완료했고 OAuth source·정책·redirect·브랜딩 자동 검증을 통과했다. 실제
|
||||
realm 사용자의 브라우저 login/callback/logout, 비상 관리자 로그인과 재시작
|
||||
지속성 검증은 남아 있다. MinIO AIStor는 NetworkPolicy, namespace, AIStor 전용
|
||||
두 Secret, 900Gi Retain Local PV, 공식 Operator와 단일 ObjectStore까지 실제
|
||||
적용했다. `Initialized/green`, PVC Bound, ClusterIP-only와 인증된 S3
|
||||
put/get/delete checksum 검증을 통과했다.
|
||||
|
||||
## 적용 전 렌더 검증
|
||||
|
||||
Helm `3.19.4`가 `PATH`에 없다면 검증된 실행 파일의 절대 경로를 지정한다.
|
||||
|
||||
```sh
|
||||
cd /home/donghyeon/workspace/platform
|
||||
PLATFORM_HELM_BIN=/absolute/path/to/helm \
|
||||
bash scripts/validate/render-phase2.sh
|
||||
```
|
||||
|
||||
이 명령은 Phase 1 검증을 먼저 수행한 뒤 Phase 2 전체를 로컬 렌더한다. 실제
|
||||
클러스터의 ObjectStore CRD 설치 여부와 무관하며 어떤 리소스도 적용하지 않는다.
|
||||
|
||||
## Keycloak-only 적용 진입점
|
||||
|
||||
AIStor license와 ObjectStore 준비를 기다리지 않고 Keycloak만 먼저 배포할 때는
|
||||
다음 전용 스크립트를 사용한다. Helm과 AIStor Secret은 요구하지 않는다.
|
||||
|
||||
```sh
|
||||
cd /home/donghyeon/workspace/platform
|
||||
bash scripts/bootstrap/apply-keycloak.sh --execute
|
||||
```
|
||||
|
||||
스크립트는 Keycloak namespace, Keycloak DB Secret 두 개, 공용 PostgreSQL의
|
||||
`DatabaseRole`·`Database`·`NetworkPolicy`, Keycloak Operator, Keycloak CR과
|
||||
Ingress를 순서대로 렌더·검증·적용하고 각 readiness를 기다린다. 먼저 현재
|
||||
Kubernetes context와 API server를 출력한 뒤 `APPLY`를 정확히 입력해야 한다.
|
||||
두 DB Secret이 모두 없으면 비밀번호를 화면에 표시하지 않고 두 번 입력받은 뒤
|
||||
`APPLY KEYCLOAK SECRETS`를 한 번 더 확인한다. 두 Secret이 이미 있으면 값을
|
||||
바꾸지 않고 계약만 검증해 재사용한다.
|
||||
|
||||
자동 적용 전에 두 Secret을 안전하게 미리 생성하려면 다음 명시적 비대화형 모드를
|
||||
사용할 수 있다.
|
||||
|
||||
```sh
|
||||
kubectl apply --filename=infrastructure/namespaces/phase2/keycloak.yaml
|
||||
bash scripts/bootstrap/create-keycloak-secrets.sh --generate --execute
|
||||
bash scripts/bootstrap/apply-keycloak.sh --execute
|
||||
```
|
||||
|
||||
`--generate --execute`는 OpenSSL CSPRNG로 32바이트 난수를 생성해 mode `0600`
|
||||
임시 파일에만 기록한다. 생성값은 argv나 화면에 출력하지 않으며 두 플래그 자체를
|
||||
자동 생성 승인으로 취급한다. 두 Secret이 이미 있으면 새 값을 만들거나 회전하지
|
||||
않고 기존 계약을 검증해 그대로 재사용한다.
|
||||
독립 실행하는 Secret 생성기는 `platform-data`와 `keycloak` namespace가 먼저
|
||||
존재해야 하므로 위 첫 명령을 생략하지 않는다.
|
||||
|
||||
위 Keycloak-only 명령은 실제 완료됐으며 재실행 시 기존 Secret을 회전하지 않고
|
||||
계약과 readiness를 다시 검증한다. 실제 결과는 중앙
|
||||
`runbooks/2026-07-23-keycloak-gitea-oidc-cutover.md`에 기록한다.
|
||||
|
||||
|
||||
중간 단계가 실패해도 namespace, Secret, DB 리소스, Operator 또는 Keycloak
|
||||
리소스를 자동 삭제하지 않는다. DB reclaim policy는 `Retain`으로 유지하며 원인을
|
||||
해결한 뒤 같은 명령을 다시 실행하는 것이 복구 경로다. Host Nginx의 Keycloak
|
||||
정적 hold 제거와 Gitea OIDC 인증 소스 등록은 이 스크립트 범위가 아니다.
|
||||
|
||||
## AIStor 실제 적용 및 재검증
|
||||
|
||||
실제 적용에는 다음 진입점을 사용했다.
|
||||
|
||||
```sh
|
||||
cd /home/donghyeon/workspace/platform
|
||||
PLATFORM_HELM_BIN=/home/donghyeon/.local/bin/helm \
|
||||
bash scripts/bootstrap/apply-aistor.sh \
|
||||
--license-file /home/donghyeon/.secrets/aistor/minio.license \
|
||||
--root-config-file /home/donghyeon/.secrets/aistor/root.env \
|
||||
--execute
|
||||
|
||||
bash scripts/validate/aistor-s3-smoke.sh --execute
|
||||
```
|
||||
|
||||
첫 스크립트가 수행하고 검증한 순서는 다음과 같다.
|
||||
|
||||
1. Phase 1과 Keycloak-only 리소스가 정상이며 Host Nginx·Gitea OIDC 작업과 AIStor
|
||||
작업이 서로 독립임을 확인한다.
|
||||
2. AIStor Operator·ObjectStore용 저장소 소유 NetworkPolicy를 먼저 렌더하고
|
||||
k3s CNI의 승인 흐름과 차단 흐름을 검증한다. webhook,
|
||||
Kubernetes API, license 확인, DNS, ObjectStore 내부 통신과 승인된 S3 소비자를
|
||||
빠뜨리지 않는다.
|
||||
|
||||
- `infrastructure/controllers/aistor-operator`
|
||||
- `services/minio-aistor`
|
||||
|
||||
3. 정확히 `aistor`, `object-storage` namespace만 검토·적용한다.
|
||||
4. `/srv/k3s/aistor`의 XFS mount·권한과 예상 밖 StorageClass 소비자가 없음을
|
||||
확인한 뒤 `infrastructure/storage/aistor-local-pv`를 적용한다.
|
||||
5. AIStor 두 Secret만 two-or-none으로 관리하는 전용 helper를 실행한다.
|
||||
기존 four-or-none `create-phase2-secrets.sh`는 현재 Keycloak-only 상태에서
|
||||
실행하지 않는다.
|
||||
6. NetworkPolicy gate 통과 후 `infrastructure/controllers/aistor-operator`와 해당
|
||||
정책을 적용하고 CRD, 두 Operator, admission webhook이 준비될 때까지 기다린다.
|
||||
7. `services/minio-aistor`와 해당 정책을 적용하고 하나의 서버·하나의 900Gi 볼륨,
|
||||
`ClusterIP` S3/Console, 예상 PVC·PV binding을 확인한다. 실제 workload에서도
|
||||
승인·차단 흐름을 다시 시험한다.
|
||||
8. S3 put/get/delete와 checksum, 관리 포트 비노출을 확인한다.
|
||||
|
||||
1~8의 자동 검증은 완료했다. ObjectStore Pod 재생성 뒤 기존 영구 객체가 유지되는지
|
||||
보는 별도 지속성 시험은 운영 데이터와 구분된 시험 버킷·승인 창을 정한 뒤 수행한다.
|
||||
|
||||
Argo CD 인계와 Host Nginx 변경은 이 Phase 2 수동 적용 범위에 포함하지 않는다.
|
||||
Istio와 sidecar injection도 적용하지 않는다.
|
||||
|
||||
## Secret 안전 조건
|
||||
|
||||
- Keycloak-only 경로의 두 DB Secret 중 일부만 존재하면
|
||||
`create-keycloak-secrets.sh`가 생성·회전을 거부한다. 두 Secret은
|
||||
`platform-data`와 `keycloak` namespace에 동일한 `keycloak` 사용자와 동일한
|
||||
비밀번호로 존재해야 한다.
|
||||
- Keycloak-only 스크립트는 AIStor license와 ObjectStore root Secret을 읽거나
|
||||
요구하거나 수정하지 않는다.
|
||||
- Keycloak DB Secret과 AIStor 두 Secret은 모두 존재하지만 서로 독립 계약이다.
|
||||
- 기존 four-or-none helper는 이 상태에서 중단하는 것이 정상이며 우회하거나
|
||||
Keycloak Secret을 삭제하지 않는다.
|
||||
- AIStor 전용 helper는 AIStor 두 Secret이 모두 없거나 모두 존재할 때만
|
||||
진행하고 one-of-two 상태에서는 자동 삭제·덮어쓰기 없이 중단한다.
|
||||
- 두 AIStor Secret이 모두 존재하면 타입·정확한 키, license file 동일성, root
|
||||
configuration 형식만 검증하고 값을 바꾸지 않는다.
|
||||
- license payload, root 사용자·비밀번호, DB 비밀번호는 Git, argv, 표준 출력에
|
||||
기록하지 않는다.
|
||||
- bootstrap helper는 credential 또는 license 회전을 수행하지 않는다.
|
||||
@@ -0,0 +1,189 @@
|
||||
# Phase 3 비공개 관리 UI 적용 절차
|
||||
|
||||
상세 설계와 실행 원장은 /home/donghyeon/workspace/docs/platform에서 관리합니다.
|
||||
이 문서는 실제 적용 순서와 입력 문자열만 요약합니다.
|
||||
|
||||
## 현재 완료 경계
|
||||
|
||||
2026-07-24 기준 선언형 YAML, 고정 차트 검증, dry-run 스크립트는 구현·검증했습니다.
|
||||
pgAdmin bootstrap password는 다음 경로에 `0600`으로 생성했습니다.
|
||||
|
||||
- /home/donghyeon/.secrets/pgadmin/bootstrap-password
|
||||
|
||||
private DNS, Gitea CoreDNS 전환, 공유기 DNS와 Tailscale split DNS 등록,
|
||||
Cloudflare DNS-01 관리 인증서, Keycloak 관리 OIDC, AIStor Console OIDC
|
||||
profile, pgAdmin, Host Nginx cutover와 자동 수용 시험을 실제 적용하고
|
||||
검증했습니다.
|
||||
|
||||
현재 AIStor는 `green`, pgAdmin Deployment는 `1/1`, PVC는 2Gi SSD Local PV에
|
||||
`Bound`입니다. 인증서 갱신 dry-run과 S3 bucket/object CRUD도 통과했습니다.
|
||||
남은 단계는 브라우저 OIDC 수동 시험뿐입니다.
|
||||
|
||||
- /home/donghyeon/.secrets/certbot/cloudflare.ini
|
||||
|
||||
공유기 DHCP DNS와 Tailscale 관리 화면도 서버에서 자동 변경하지 않습니다.
|
||||
|
||||
## 1. 정적 렌더
|
||||
|
||||
cd /home/donghyeon/workspace/platform
|
||||
PLATFORM_HELM_BIN=/home/donghyeon/.local/bin/helm \
|
||||
bash scripts/validate/render-admin-services.sh
|
||||
|
||||
pgAdmin Chart archive와 OCI digest, AIStor ObjectStore Chart archive를 검증한 뒤
|
||||
Secret 리소스가 없는 manifest만 렌더합니다.
|
||||
|
||||
## 2. Keycloak 외부망 시간 초과 대조
|
||||
|
||||
실패 단말에서 시각을 기록하고 다음을 실행합니다.
|
||||
|
||||
date --iso-8601=seconds
|
||||
dig @1.1.1.1 A id.learn.hyeonworks.com
|
||||
dig @1.1.1.1 AAAA id.learn.hyeonworks.com
|
||||
curl -4 -vkI --connect-timeout 10 https://id.learn.hyeonworks.com/
|
||||
curl -4 -vk --connect-timeout 10 \
|
||||
https://id.learn.hyeonworks.com/realms/hyeonworks/.well-known/openid-configuration
|
||||
|
||||
같은 시간대 Host Nginx access log에 요청이 없으면 서버 설정이 아니라 실패
|
||||
단말의 VPN·보안 필터·통신사 경로 문제로 판정합니다.
|
||||
|
||||
## 3. private DNS
|
||||
|
||||
상태: 2026-07-24 실제 적용 완료. Gitea live `hostAliases` 제거도 완료.
|
||||
|
||||
먼저 dry-run을 실행한 뒤 실제 적용합니다.
|
||||
|
||||
bash scripts/bootstrap/apply-private-dns.sh
|
||||
bash scripts/bootstrap/apply-private-dns.sh --execute
|
||||
|
||||
확인 문자열은 APPLY default입니다. 이후 공유기 DHCP DNS를 192.168.0.107,
|
||||
Tailscale의 learn.hyeonworks.com 제한 nameserver를 100.92.240.34로 등록합니다.
|
||||
공개 DNS에는 두 admin 도메인을 만들지 않습니다.
|
||||
|
||||
서버의 공유기는 TP-Link 계열로 식별됐습니다. 정확한 TP-Link·Tailscale 메뉴와
|
||||
검증 방법은 infrastructure/networking/private-dns/host/README.md를 따릅니다.
|
||||
|
||||
CoreDNS 전환 뒤 Gitea OIDC profile을 다시 적용하면 기존 Pod의 임시
|
||||
hostAliases도 제거됩니다.
|
||||
|
||||
PLATFORM_HELM_BIN=/home/donghyeon/.local/bin/helm \
|
||||
bash scripts/bootstrap/apply-gitea-oidc.sh --execute
|
||||
|
||||
## 4. 운영자 Secret 파일
|
||||
|
||||
Cloudflare Dashboard의 `My Profile > API Tokens > Create Token`에서
|
||||
`Edit Zone DNS` 템플릿을 선택합니다. 권한은 `Zone:DNS:Edit`, Zone resource는
|
||||
`Include > Specific zone > hyeonworks.com`으로 제한합니다.
|
||||
|
||||
공식 절차:
|
||||
https://developers.cloudflare.com/fundamentals/api/get-started/create-token/
|
||||
|
||||
파일 내용은 `dns_cloudflare_api_token = ...` 한 줄이며 저장소에 만들지 않습니다.
|
||||
|
||||
sudo install -d -o root -g root -m 0700 /home/donghyeon/.secrets/certbot
|
||||
sudoedit /home/donghyeon/.secrets/certbot/cloudflare.ini
|
||||
sudo chown root:root /home/donghyeon/.secrets/certbot/cloudflare.ini
|
||||
sudo chmod 0600 /home/donghyeon/.secrets/certbot/cloudflare.ini
|
||||
|
||||
pgAdmin 내부 비상 관리자 비밀번호는 이미 준비했습니다. 재생성이 필요한 경우에만
|
||||
다음을 실행합니다. 실행하면 기존 비밀번호 파일이 교체됩니다.
|
||||
|
||||
install -d -m 0700 /home/donghyeon/.secrets/pgadmin
|
||||
umask 077
|
||||
openssl rand -base64 32 | tr -d '\n' \
|
||||
> /home/donghyeon/.secrets/pgadmin/bootstrap-password
|
||||
chmod 0600 /home/donghyeon/.secrets/pgadmin/bootstrap-password
|
||||
|
||||
파일 값은 출력하거나 runbook에 복사하지 않습니다.
|
||||
|
||||
## 5. admin 인증서
|
||||
|
||||
상태: 2026-07-24 실제 발급 완료. 만료일 2026-10-21, 자동 갱신 등록.
|
||||
|
||||
서비스 적용 전에 exact SAN 두 개만 발급합니다.
|
||||
|
||||
bash scripts/bootstrap/apply-host-nginx-admin.sh \
|
||||
--execute \
|
||||
--certificate-only \
|
||||
--certbot-email you@example.com
|
||||
|
||||
확인 문자열은 APPLY입니다. 최초 실행은 공식 Cloudflare Certbot snap plugin을
|
||||
설치하고 deploy hook을 등록합니다. wildcard 인증서는 만들지 않습니다.
|
||||
|
||||
## 6. Keycloak client와 그룹
|
||||
|
||||
상태: 2026-07-24 실제 적용 완료. `donghyeon.kang` 두 관리자 그룹 배정.
|
||||
|
||||
먼저 dry-run으로 대상을 확인합니다. donghyeon.kang은 실제 realm 사용자명으로
|
||||
바꿀 수 있습니다.
|
||||
|
||||
bash scripts/bootstrap/configure-keycloak-admin-oidc.sh
|
||||
bash scripts/bootstrap/configure-keycloak-admin-oidc.sh \
|
||||
--execute \
|
||||
--object-admin donghyeon.kang \
|
||||
--db-admin donghyeon.kang
|
||||
|
||||
확인 문자열은 APPLY default입니다. 두 OIDC Secret의 값은 출력하지 않습니다.
|
||||
|
||||
## 7. 관리 서비스
|
||||
|
||||
상태: 2026-07-24 실제 적용 완료.
|
||||
|
||||
최종 검증 결과:
|
||||
|
||||
object-storage/minio-aistor: green, drivesOnline=1
|
||||
platform-admin/pgadmin: 1/1
|
||||
platform-admin/pgadmin PVC: Bound, pgadmin-data-local-pv
|
||||
storage-admin Traefik HTTP: 200
|
||||
db-admin Traefik HTTP: 302
|
||||
|
||||
PLATFORM_HELM_BIN=/home/donghyeon/.local/bin/helm \
|
||||
bash scripts/bootstrap/apply-admin-services.sh \
|
||||
--execute \
|
||||
--pgadmin-password-file \
|
||||
/home/donghyeon/.secrets/pgadmin/bootstrap-password
|
||||
|
||||
확인 문자열은 APPLY default입니다. 실패 시 AIStor spec은 복원하고 pgAdmin은
|
||||
0 replica로 내리지만 pgAdmin PVC/PV와 AIStor 데이터는 삭제하지 않습니다.
|
||||
|
||||
적용 중 pgAdmin rollout 뒤 출력이 잠시 없으면 AIStor health를 확인하는
|
||||
구간입니다. 현재 스크립트는 15초마다 상태와 경과 시간을 출력합니다. 300초 뒤
|
||||
실패하면 자동 rollback하므로 강제로 다시 적용하기 전에 중앙 runbook의
|
||||
`pgAdmin 완료 뒤 AIStor health 대기` 항목을 확인합니다.
|
||||
|
||||
## 8. Nginx 최종 전환
|
||||
|
||||
상태: 2026-07-24 실제 적용 완료.
|
||||
|
||||
활성 설정 SHA-256:
|
||||
|
||||
f07d558d7ebbe09bdd61f3422fe8dcdaaaa6d1160d0872c06538d2bee193908f
|
||||
|
||||
bash scripts/bootstrap/apply-host-nginx-admin.sh --execute
|
||||
|
||||
확인 문자열은 APPLY입니다. 설정을 timestamp backup한 뒤 LAN 요청의 정상 응답,
|
||||
허용 목록 밖 loopback 요청의 403, unknown SNI 거부와 인증서 갱신 dry-run을
|
||||
검사합니다.
|
||||
|
||||
## 9. 수용 시험
|
||||
|
||||
상태: 2026-07-24 자동 시험 완료.
|
||||
|
||||
READ-ONLY CHECK PASS
|
||||
AISTOR_S3_SMOKE_PASS
|
||||
ADMIN UI SMOKE PASS
|
||||
|
||||
bash scripts/validate/admin-ui-smoke.sh --execute --run-s3
|
||||
|
||||
확인 문자열은 APPLY이며 S3 하위 검사에서도 APPLY를 한 번 더 입력합니다.
|
||||
재검증이 필요할 때만 다시 실행합니다.
|
||||
|
||||
마지막으로 LAN 또는 Tailscale 연결 상태에서 브라우저로 다음을 시험합니다.
|
||||
|
||||
- https://storage-admin.learn.hyeonworks.com
|
||||
- `/platform-object-admins` 사용자의 Keycloak OIDC 로그인 성공
|
||||
- 해당 group이 없는 사용자의 관리 기능 접근 거부
|
||||
- https://db-admin.learn.hyeonworks.com
|
||||
- `/platform-db-admins` 사용자의 Keycloak OIDC 로그인 성공
|
||||
- 해당 group이 없는 사용자의 로그인 거부
|
||||
|
||||
pgAdmin 내부 비상 관리자 로그인은 OIDC 장애 대응용으로만 유지합니다.
|
||||
@@ -0,0 +1,701 @@
|
||||
# Phase 4 관측성 접근·알림 전환 절차
|
||||
|
||||
상세 설계와 실행 원장은 `/home/donghyeon/workspace/docs/platform`에서 관리합니다.
|
||||
이 문서는 현재 live substrate를 보존하면서 rules·alerts와 Host Nginx를 마지막에
|
||||
전환하는 권위 실행 순서를 요약합니다. Secret 값, token, Cookie, 사용자 ID와
|
||||
webhook 원문은 출력하거나 문서에 복사하지 않습니다.
|
||||
|
||||
## 현재 완료 경계
|
||||
|
||||
2026-08-15 기준 Blackbox substrate, metric target과 rules-alerts는 live입니다.
|
||||
|
||||
- Prometheus active target `30`, healthy `30`, unhealthy `0`
|
||||
- Grafana와 Blackbox Exporter Deployment 각각 `1/1` Ready
|
||||
- Host Nginx는 Grafana deny-only guard 상태
|
||||
- Grafana OIDC workload와 `grafana-keycloak-oidc` Secret 참조는 live
|
||||
- Slack Secret과 risk deployment evidence는 live이며 off-host Slack DR은 deferred
|
||||
- platform dashboard ConfigMap `5`, platform PrometheusRule `4`,
|
||||
`AlertmanagerConfig/platform-alertmanager` `1`이 rollback ID `20260814T145009Z`에서 accepted
|
||||
- generated receiver는
|
||||
`observability/platform-alertmanager/platform-slack` exact singleton
|
||||
- full Nginx cutover와 browser OIDC·Slack firing/resolved·external-client acceptance는 미실행
|
||||
|
||||
rules-alerts 실행 때 다음 두 선행 조건은 모두 충족됐습니다. 이후 재실행이나 복구에서도
|
||||
같은 gate를 생략하지 않습니다.
|
||||
|
||||
1. 아래 runbook URL이 HTTPS `200`으로 도달하고 모든 alert의 `runbook_url`과 일치한다.
|
||||
2. 운영자가 만든 Slack webhook 입력 파일과 recovery evidence가 준비된다.
|
||||
|
||||
```text
|
||||
https://git.learn.hyeonworks.com/donghyeon.kang/project-infra/src/branch/main/docs/runbooks/2026-07-31-observability-access-cutover.md
|
||||
```
|
||||
|
||||
## 1. 공통 rollback transaction
|
||||
|
||||
하나의 shell에서 같은 rollback ID를 끝까지 유지합니다.
|
||||
|
||||
```bash
|
||||
cd /home/donghyeon/workspace/platform
|
||||
|
||||
OBS_ROLLBACK_ID="$(date -u +%Y%m%dT%H%M%SZ)"
|
||||
[[ "$OBS_ROLLBACK_ID" =~ ^[0-9]{8}T[0-9]{6}Z$ ]]
|
||||
[[ "$OBS_ROLLBACK_ID" != 20260814T080303Z ]]
|
||||
OBS_ROLLBACK_ROOT="/var/lib/hyeonworks/platform-rollbacks/observability-$OBS_ROLLBACK_ID"
|
||||
sudo -n /usr/bin/test ! -e "$OBS_ROLLBACK_ROOT"
|
||||
sudo -n /usr/bin/test ! -L "$OBS_ROLLBACK_ROOT"
|
||||
sudo -n /usr/bin/mkdir --mode=0700 -- "$OBS_ROLLBACK_ROOT"
|
||||
[[ "$(sudo -n /usr/bin/stat -c '%F|%u:%g|%a' -- "$OBS_ROLLBACK_ROOT")" == \
|
||||
'directory|0:0|700' ]]
|
||||
export PLATFORM_OBSERVABILITY_ROLLBACK_ID="$OBS_ROLLBACK_ID"
|
||||
[[ "$PLATFORM_OBSERVABILITY_ROLLBACK_ID" == "$OBS_ROLLBACK_ID" ]]
|
||||
```
|
||||
|
||||
pre-absence, 단 한 번의 `mkdir`, metadata 또는 active-ID equality가 실패하면 STOP하고 그
|
||||
collision/error ID를 재사용하지 않습니다. 자동 rollback 뒤에도 생성된 root를 삭제하지 않습니다.
|
||||
|
||||
## 2. 기존 substrate와 deny guard 확인
|
||||
|
||||
```bash
|
||||
bash scripts/bootstrap/apply-private-dns.sh
|
||||
bash scripts/bootstrap/apply-host-nginx-observability.sh
|
||||
bash scripts/validate/validate-blackbox-edge-source.sh
|
||||
```
|
||||
|
||||
private DNS와 metrics/deny guard 자체를 다시 적용해야 할 때만 다음 mutation을
|
||||
순서대로 실행합니다. 각 명령의 화면 지시와 정확히 일치하는 확인 문자열만 입력합니다.
|
||||
|
||||
```bash
|
||||
: "${CERTBOT_EMAIL:?set the operator-managed Certbot contact email}"
|
||||
|
||||
bash scripts/bootstrap/apply-private-dns.sh --execute
|
||||
bash scripts/bootstrap/apply-host-nginx-observability.sh \
|
||||
--execute --metrics-guard-only
|
||||
|
||||
metrics_status="$(curl --disable --silent --show-error --output /dev/null \
|
||||
--write-out '%{http_code}' \
|
||||
--resolve git.learn.hyeonworks.com:443:127.0.0.1 \
|
||||
https://git.learn.hyeonworks.com/metrics)"
|
||||
[[ "$metrics_status" == 404 ]]
|
||||
|
||||
bash scripts/bootstrap/apply-host-nginx-observability.sh \
|
||||
--execute --certificate-only --certbot-email "$CERTBOT_EMAIL"
|
||||
bash scripts/bootstrap/apply-host-nginx-observability.sh \
|
||||
--execute --grafana-deny-guard-only
|
||||
```
|
||||
|
||||
DNS/guard를 다시 적용했는지와 무관하게, **새 rollback ID마다** Blackbox source
|
||||
proof를 반드시 새로 만듭니다. Task 6 retry에서는 failed ID `20260814T080303Z`를 거부하고
|
||||
controller가 발급한 fresh ID와 현재 active environment가 exact equality인지 먼저 확인합니다.
|
||||
|
||||
```bash
|
||||
FRESH_TASK6_ROLLBACK_ID="$OBS_ROLLBACK_ID"
|
||||
[[ "$FRESH_TASK6_ROLLBACK_ID" != 20260814T080303Z ]]
|
||||
[[ "${PLATFORM_OBSERVABILITY_ROLLBACK_ID:?active rollback ID is required}" == \
|
||||
"$FRESH_TASK6_ROLLBACK_ID" ]]
|
||||
set +e
|
||||
bash scripts/validate/validate-blackbox-edge-source.sh \
|
||||
--execute --context default
|
||||
BLACKBOX_RC=$?
|
||||
set -e
|
||||
printf 'BLACKBOX_RC=%d\n' "$BLACKBOX_RC"
|
||||
[[ "$BLACKBOX_RC" -eq 0 ]]
|
||||
[[ "$(sudo -n /usr/bin/stat -c '%F|%u:%g|%a|%h' -- \
|
||||
"/var/lib/hyeonworks/platform-rollbacks/observability-${FRESH_TASK6_ROLLBACK_ID}/blackbox-source-proof.env")" == \
|
||||
'regular file|0:0|600|1' ]]
|
||||
```
|
||||
|
||||
이 proof는 같은 rollback ID, active deny hash, 24시간 이내 시각과 세 private
|
||||
hostname의 exact `403`에 결속되어야 합니다. validator는 한 번만 호출하고 operator가 exact
|
||||
`PROVE BLACKBOX PRIVATE EDGE default`를 입력한 뒤 `BLACKBOX PRIVATE EDGE SOURCE PASS`, immediate
|
||||
RC `0`, normalized proof metadata `regular|0:0|600|1`를 모두 확인합니다. 하나라도 실패하거나
|
||||
불명확하면 STOP하고 new ID를 보존하며 같은 ID로 validator나 Task 6를 재시도하지 않습니다.
|
||||
proof content는 읽지 않습니다.
|
||||
|
||||
preflight/live residue는 absolute zero가 아니라 다음 attested preexisting name-only baseline의
|
||||
unchanged 계약입니다.
|
||||
|
||||
```text
|
||||
/tmp/platform-k3s-encryption.Mskzy3
|
||||
/tmp/platform-observability-access-apply.oeNcfI
|
||||
/tmp/platform-observability-access-apply.Im02dz
|
||||
/tmp/platform-observability-slack-gate.LYhYbv
|
||||
```
|
||||
|
||||
`Mskzy3`는 8/1 empty evidence, `oeNcfI`/`Im02dz`는 8/12 recorded evidence,
|
||||
`LYhYbv`는 private filename 두 개만 attested된 failed-live evidence입니다. 네 root는 content를
|
||||
읽거나 삭제하지 않습니다. baseline name set unchanged, matching executable process `0`, current
|
||||
preflight/live newly-created matching-root delta `0`을 요구합니다. unknown/new root는 broad
|
||||
delete하지 않고 STOP/identity review합니다.
|
||||
|
||||
## 3. Secret과 Grafana OIDC recovery evidence
|
||||
|
||||
먼저 K3s encryption과 restore evidence를 각각 새 process에서 검사합니다.
|
||||
|
||||
```bash
|
||||
bash scripts/validate/k3s-secret-encryption.sh --expect-reencrypted
|
||||
bash scripts/validate/k3s-secret-encryption-restore-evidence.sh --check
|
||||
```
|
||||
|
||||
입력 파일은 현재 사용자 소유 `0600`, non-symlink, link count 1이어야 합니다.
|
||||
값을 shell 변수, argv, stdout 또는 runbook에 넣지 않습니다.
|
||||
|
||||
```bash
|
||||
bash scripts/bootstrap/create-observability-secrets.sh \
|
||||
--execute --grafana-admin \
|
||||
--grafana-admin-user-file /home/donghyeon/.secrets/grafana/admin-user \
|
||||
--grafana-admin-password-file /home/donghyeon/.secrets/grafana/admin-password
|
||||
```
|
||||
|
||||
Slack Secret bootstrap 직전에 별도 내장 Windows SSD의 기존 KDBX에 same-host encrypted
|
||||
disaster-recovery copy를 준비합니다. 이것은 일반 K3s restart나 host reboot용 사본이 아니라
|
||||
datastore·Secret·bootstrap state 손실 때를 위한 것입니다. 기본 no-argument 실행은 고정
|
||||
contract만 출력하며 SSD, KDBX, webhook, sudo에 접근하지 않습니다. 지원되는 interface는
|
||||
다음 두 개뿐입니다.
|
||||
|
||||
```bash
|
||||
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에서 `SLACK_KEEPASS_RECOVERY=NOOP`는 exact entry의 verified read-only no-op이고,
|
||||
`SLACK_KEEPASS_RECOVERY=COMMITTED`는 durable pre-change backup을 만든 verified commit입니다.
|
||||
둘 다 source-based unmount proof와 private work/socket/helper cleanup 뒤에만 성공하며
|
||||
`WINDOWS_SSD_UNMOUNTED=PASS`, `OFF_HOST_RECOVERY_SATISFIED=NO`를 출력합니다. lost response,
|
||||
post-commit verification failure 또는 cleanup/unmount ambiguity는 자동 재시도하지 않고
|
||||
`SLACK_KEEPASS_RECOVERY=MANUAL_RECOVERY_REQUIRED`로 중단하며 main과 backup을 보존합니다.
|
||||
webhook payload, KeePassXC master password, hash·encoding·size·URL component 또는 protected
|
||||
KDBX output을 terminal, argv, environment, log, runbook이나 plaintext 파일에 남기지 않습니다.
|
||||
|
||||
This local encrypted copy does not authorize RECOVERY SLACK default when the
|
||||
approved gate requires off-host escrow. Do not continue the Secret bootstrap
|
||||
until that independent prerequisite is literally true.
|
||||
|
||||
Slack에는 서로 다른 두 경로가 있습니다. off-host disaster recovery를 완료로 판정할 때만
|
||||
strict recovery evidence를 검사합니다.
|
||||
|
||||
```bash
|
||||
bash scripts/bootstrap/create-observability-secrets.sh \
|
||||
--check-slack-recovery-evidence
|
||||
```
|
||||
|
||||
현재 사용자가 승인한 operational risk path는 off-host Slack DR이 아직 deferred인 사실을
|
||||
기록하고 deployment evidence를 만듭니다. 이 경로는 DR-complete을 주장하지 않습니다.
|
||||
|
||||
```bash
|
||||
bash scripts/bootstrap/create-observability-secrets.sh \
|
||||
--execute --slack-webhook \
|
||||
--slack-webhook-file /home/donghyeon/.secrets/alertmanager/slack-webhook \
|
||||
--accept-no-off-host-slack-recovery
|
||||
```
|
||||
|
||||
도구가 요구하는 정확한 확인은 `ACCEPT NO OFF-HOST SLACK RECOVERY default`입니다.
|
||||
이 risk path 밖에서 kubectl로 Secret을 수동 생성하지 않습니다. off-host 복구 증거가
|
||||
없을 때 거짓 `RECOVERY SLACK default` 확인을 입력하지 않습니다.
|
||||
|
||||
위 risk path 또는 실제 off-host recovery evidence가 준비된 경우 Slack Secret bootstrap은
|
||||
이미 완료된 상태이므로, deployment gate와 다음 checker만 실행합니다.
|
||||
|
||||
```bash
|
||||
bash scripts/bootstrap/configure-keycloak-grafana-oidc.sh --execute
|
||||
bash scripts/bootstrap/create-observability-secrets.sh \
|
||||
--check-grafana-recovery-evidence
|
||||
bash scripts/bootstrap/create-observability-secrets.sh \
|
||||
--check-slack-deployment-evidence
|
||||
bash scripts/bootstrap/configure-keycloak-grafana-oidc.sh \
|
||||
--check-recovery-evidence
|
||||
```
|
||||
|
||||
기존 Secret의 payload가 다르면 자동 rotation하지 않고 중단합니다. UID drift나 API
|
||||
결과 불명도 자동 삭제로 처리하지 않습니다.
|
||||
|
||||
Grafana admin/OIDC object가 이미 exact live state이면 도구는 credential을 회전하거나
|
||||
workload를 다시 쓰지 않고 기존 payload를 재사용하며 recovery evidence만 검증·갱신합니다.
|
||||
exact state가 아닌데 권위 prior와 ownership을 증명할 수 없으면 자동 수렴시키지 않습니다.
|
||||
|
||||
## 4. 권위 inventory로 rules-alerts handoff 생성
|
||||
|
||||
현재 Blackbox substrate가 이미 live이므로 `target-initial`을 지금 다시 캡처하지
|
||||
않습니다. 실행 당시 보존한 두 phase만 새 `0700` output root에 복제하고 renderer가
|
||||
schema, phase, mode, link count와 hash를 다시 검증하게 합니다.
|
||||
|
||||
먼저 공개 runbook이 실제로 게시되었는지 확인합니다. `200`이 아니면 renderer와
|
||||
rules-alerts apply를 실행하지 않습니다.
|
||||
|
||||
```bash
|
||||
RUNBOOK_URL='https://git.learn.hyeonworks.com/donghyeon.kang/project-infra/src/branch/main/docs/runbooks/2026-07-31-observability-access-cutover.md'
|
||||
runbook_status="$(curl --disable --silent --show-error --location --output /dev/null \
|
||||
--write-out '%{http_code}' --connect-timeout 3 --max-time 10 \
|
||||
"$RUNBOOK_URL")"
|
||||
[[ "$runbook_status" == 200 ]]
|
||||
```
|
||||
|
||||
```bash
|
||||
SOURCE_METRIC_ROOT=/tmp/platform-observability-metrics.VUpsZn
|
||||
METRIC_ROOT="$(mktemp -d /tmp/platform-observability-metrics.XXXXXX)"
|
||||
chmod 0700 "$METRIC_ROOT"
|
||||
|
||||
[[ "$(realpath --canonicalize-existing -- "$SOURCE_METRIC_ROOT")" == "$SOURCE_METRIC_ROOT" ]]
|
||||
[[ "$(stat -c '%F|%u:%g|%a|%h' -- "$SOURCE_METRIC_ROOT")" == \
|
||||
'directory|1000:1000|700|4' ]]
|
||||
declare -A SOURCE_METRIC_IDENTITY=()
|
||||
for phase in target-initial post-substrate; do
|
||||
[[ -d "$SOURCE_METRIC_ROOT/$phase" && ! -L "$SOURCE_METRIC_ROOT/$phase" ]]
|
||||
[[ "$(realpath --canonicalize-existing -- "$SOURCE_METRIC_ROOT/$phase")" == \
|
||||
"$SOURCE_METRIC_ROOT/$phase" ]]
|
||||
[[ "$(stat -c '%F|%u:%g|%a|%h' -- "$SOURCE_METRIC_ROOT/$phase")" == \
|
||||
'directory|1000:1000|700|2' ]]
|
||||
install -d -m 0700 -- "$METRIC_ROOT/$phase"
|
||||
for file in inventory.json inventory.sha256; do
|
||||
source_file="$SOURCE_METRIC_ROOT/$phase/$file"
|
||||
destination_file="$METRIC_ROOT/$phase/$file"
|
||||
[[ -f "$source_file" && ! -L "$source_file" ]]
|
||||
[[ "$(realpath --canonicalize-existing -- "$source_file")" == "$source_file" ]]
|
||||
[[ "$(stat -c '%F|%u:%g|%a|%h' -- "$source_file")" == \
|
||||
'regular file|1000:1000|600|1' ]]
|
||||
SOURCE_METRIC_IDENTITY["$phase/$file"]="$(stat -c '%d:%i|%F|%u:%g|%a|%h|%s|%Y|%Z' -- \
|
||||
"$source_file")|$(sha256sum -- "$source_file" | awk '{print $1}')"
|
||||
cp --no-dereference --reflink=never -- "$source_file" "$destination_file"
|
||||
chmod 0600 "$destination_file"
|
||||
[[ "$(stat -c '%F|%u:%g|%a|%h' -- "$destination_file")" == \
|
||||
'regular file|1000:1000|600|1' ]]
|
||||
cmp -s -- "$source_file" "$destination_file"
|
||||
done
|
||||
done
|
||||
|
||||
[[ "$(find "$METRIC_ROOT" -mindepth 1 -maxdepth 2 -printf '%P\n' | LC_ALL=C sort)" == \
|
||||
$'post-substrate\npost-substrate/inventory.json\npost-substrate/inventory.sha256\ntarget-initial\ntarget-initial/inventory.json\ntarget-initial/inventory.sha256' ]]
|
||||
for phase in target-initial post-substrate; do
|
||||
for file in inventory.json inventory.sha256; do
|
||||
source_file="$SOURCE_METRIC_ROOT/$phase/$file"
|
||||
[[ "$(stat -c '%d:%i|%F|%u:%g|%a|%h|%s|%Y|%Z' -- "$source_file")|$(sha256sum -- \
|
||||
"$source_file" | awk '{print $1}')" == "${SOURCE_METRIC_IDENTITY["$phase/$file"]}" ]]
|
||||
done
|
||||
done
|
||||
|
||||
PLATFORM_HELM_BIN=/home/donghyeon/.local/bin/helm \
|
||||
bash scripts/validate/render-observability-access.sh \
|
||||
--component rules-alerts --verified-output-dir "$METRIC_ROOT"
|
||||
```
|
||||
|
||||
failed transaction handoff `/tmp/platform-observability-metrics.LNzksC`는 read-only immutable
|
||||
evidence로 보존하고 source, destination 또는 live apply input으로 재사용하지 않습니다.
|
||||
기존 attested path/fingerprint identity만 보존·비교하며 inventory body나 private file content를
|
||||
다시 읽지 않습니다. fresh destination은 renderer 전 exact two phase directories만 가집니다.
|
||||
renderer와 live apply는 이 절에서 byte-preserving copy와 metadata/hash/count revalidation을
|
||||
마친 fresh `$METRIC_ROOT`만 사용합니다.
|
||||
|
||||
위 exact six-entry gate는 renderer 전 destination이 두 phase directory와 네 file만 갖는지
|
||||
확인합니다. 각 source/destination pair는 byte-equal이며 copy 뒤 source inode/metadata/size/hash가
|
||||
copy 전 fingerprint와 같아야 합니다. 이어지는 renderer는 current production pins와 exact target
|
||||
counts `21/30`, checksum/schema/semantic contract, 그리고 reviewed rendered manifest set을 다시
|
||||
검증합니다. source/destination metadata, fingerprint, byte equality, pins, counts 또는 entry set
|
||||
중 하나라도 다르면 fresh root를 apply input으로 사용하지 않고 STOP합니다.
|
||||
|
||||
권위 inventory hash는 다음과 같습니다.
|
||||
|
||||
```text
|
||||
target-initial: 79688d017d38eec9a6f100f8d0f784a5474e79802046ef1c2c11b30d170b0b0c
|
||||
post-substrate: b1c3049206a1a88165ee672ae9aceac7945673a3bb9c3cf3670b7f0d56c3f291
|
||||
```
|
||||
|
||||
이 두 SHA는 현재 cluster freshness artifact가 아니라 변경할 수 없는 historical
|
||||
metric/label provenance pair입니다. `target-initial`과 `post-substrate`는 각각 exact
|
||||
target count `21`과 `30`을 initial gate와 confirmation 뒤 first mutation 직전
|
||||
last gate 모두에서 다시 검증합니다. `captured_at_utc`는 exact UTC-second
|
||||
형식과 유효한 UTC calendar로 parse되어야 하며, 현재보다 300초를 초과해
|
||||
미래인 시각만 거부합니다. 이 exact pair에는 과거 방향 24시간 상한을
|
||||
적용하지 않으며, timestamp나 checksum을 현재 시각에 맞게 다시 쓰거나
|
||||
inventory를 재수집해서는 안 됩니다. 이 예외는 2절 Blackbox source proof의
|
||||
기존 24시간 freshness 계약에는 적용되지 않습니다.
|
||||
|
||||
## 5. rules-alerts 적용 — 2026-08-15 terminal PASS
|
||||
|
||||
먼저 no-argument dry-run과 focused test를 실행합니다. 둘 중 하나라도 끝나지 않거나
|
||||
실패하면 mutation을 실행하지 않습니다.
|
||||
|
||||
```bash
|
||||
PLATFORM_HELM_BIN=/home/donghyeon/.local/bin/helm \
|
||||
bash scripts/bootstrap/apply-observability-access.sh
|
||||
bash scripts/validate/test-apply-observability-access.sh
|
||||
```
|
||||
|
||||
성공한 뒤에만 다음을 실행합니다.
|
||||
|
||||
```bash
|
||||
case $- in *e*) TASK6_APPLY_ERREXIT_WAS_SET=1 ;; *) TASK6_APPLY_ERREXIT_WAS_SET=0 ;; esac
|
||||
set +e
|
||||
PLATFORM_HELM_BIN=/home/donghyeon/.local/bin/helm bash scripts/bootstrap/apply-observability-access.sh --execute --rules-alerts --verified-output-dir "$METRIC_ROOT"
|
||||
TASK6_APPLY_RC=$?
|
||||
printf 'TASK6_APPLY_RC=%d\n' "$TASK6_APPLY_RC"
|
||||
(( TASK6_APPLY_ERREXIT_WAS_SET == 0 )) || set -e
|
||||
[[ "$TASK6_APPLY_RC" -eq 0 ]]
|
||||
```
|
||||
|
||||
operator만 exact `APPLY` confirmation을 입력합니다. apply는 위 exact one-line command로 한 번만
|
||||
실행하고, 바로 다음 statement가 다른 command 없이 `TASK6_APPLY_RC=$?`를 capture합니다.
|
||||
immediate printed RC `0`과 exact `OBSERVABILITY_ACCESS_RULES_ALERTS=PASS`를 모두 확인한 경우에만
|
||||
후속 acceptance를 진행합니다. nonzero, missing/ambiguous RC 또는 PASS, response loss, rollback
|
||||
ambiguity는 STOP하고 rollback ID와 evidence를 보존하며 같은 ID로 재시도하지 않습니다.
|
||||
|
||||
성공 조건은 다음 전부입니다.
|
||||
|
||||
- Prometheus와 Alertmanager owner/controller Ready
|
||||
- Prometheus API의 desired alert·record exact set과 evaluation health 정상
|
||||
- Alertmanager generated config의 `observability/platform-alertmanager/platform-slack` receiver exact singleton
|
||||
- Grafana sidecar의 exact dashboard 5개와 source content hash 일치
|
||||
- 기존 target·Grafana·Blackbox·Probe·Ingress·PVC·Secret 보존
|
||||
- acceptance marker는 모든 증거 뒤에만 root-only로 기록
|
||||
|
||||
apply 도구는 deployment checker를 confirmation 전과 Slack Secret-consuming mutation 직전에
|
||||
두 번 호출한다. rules-alerts acceptance ledger schema는
|
||||
`platform-observability-rules-alerts-v2`이며 Slack deployment gate 값은 bare `RECOVERY` 또는
|
||||
`RISK_ACCEPTED`만 기록한다. 이는 operational acceptance이며 off-host Slack DR-complete을
|
||||
의미하지 않는다.
|
||||
|
||||
실제 terminal transaction은 fresh rollback ID `20260814T145009Z`와 fresh handoff
|
||||
`/tmp/platform-observability-metrics.dw5gLZ`를 사용했다. argv는 exact six-element array로
|
||||
attest됐고, operator가 exact `APPLY`를 입력한 단 한 번의 실행에서 다음 safe marker를 확인했다.
|
||||
|
||||
```text
|
||||
target-initial SHA-256 = 79688d017d38eec9a6f100f8d0f784a5474e79802046ef1c2c11b30d170b0b0c
|
||||
post-substrate SHA-256 = b1c3049206a1a88165ee672ae9aceac7945673a3bb9c3cf3670b7f0d56c3f291
|
||||
OBSERVABILITY_ACCESS_RULES_ALERTS=PASS
|
||||
TASK6_APPLY_RC=0
|
||||
```
|
||||
|
||||
payload-free terminal audit는 dashboard `5`, platform PrometheusRule `4`, 전체 desired rule
|
||||
`23`(`22` alerts + `1` recording) healthy, runbook URL `22/22`, AlertmanagerConfig `1`, exact
|
||||
NetworkPolicy, target `30/30`, Grafana·Blackbox·Prometheus·Alertmanager Ready와 qualified receiver
|
||||
exact singleton을 확인했다. acceptance schema와 `RISK_ACCEPTED` gate, root-only ledger의
|
||||
object/mutation line `13/13` 및 metadata contract도 통과했다. 성공 transaction에는 rollback이
|
||||
호출되지 않았고 rollback root와 handoff는 Task 7 종료까지 보존한다.
|
||||
|
||||
failed rollback ID `20260814T080303Z`와 argument paste가 파싱 전에 중단된
|
||||
`20260814T140953Z`는 immutable evidence로 보존하고 재사용하지 않는다. 후자는
|
||||
`--verified-output-dir` token이 줄바꿈으로 분리돼 usage RC `2`, shell-level RC `127`로 끝났으며
|
||||
ledger·acceptance·Kubernetes mutation은 생성되지 않았다.
|
||||
|
||||
## 6. Task 7 operator boundary와 Host Nginx first cutover
|
||||
|
||||
Task 7은 성공 rollback ID `20260814T145009Z`와 original handoff
|
||||
`/tmp/platform-observability-metrics.dw5gLZ`를 그대로 보존합니다. active state는 exact deny-only
|
||||
SHA-256 `dbef6d443bcba58b26a5351ea76f6d09f6da8c2ef07a806e22745cf26c88f518`,
|
||||
desired full은 `7d2de2a92c3597a0859775da1d2ccbf5a3d72c0b2af5cac2439c82311361f801`여야
|
||||
합니다. full이 이미 active이거나 third state이면 STOP합니다.
|
||||
|
||||
어떤 external preparation command보다 먼저 fixed PATH를 export하고 command cache를 비운 뒤
|
||||
reviewed command/launcher inventory를 byte-equal로 재검증합니다. ambient PATH command로 prep를
|
||||
시작하지 않습니다. exact canonical `/usr/bin/sudo`는 `regular|0:0|4755|1`을 요구하는
|
||||
유일한 owner-setuid 예외입니다. setgid·group/world write는 금지되고 다른 allowlisted
|
||||
executable은 setuid/setgid를 모두 금지합니다.
|
||||
|
||||
```bash
|
||||
TASK7_OPERATOR_PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
|
||||
PATH=$TASK7_OPERATOR_PATH
|
||||
export PATH
|
||||
hash -r
|
||||
|
||||
cd /home/donghyeon/workspace/platform
|
||||
TASK7_ID=20260814T145009Z
|
||||
TASK7_METRIC_ROOT=/tmp/platform-observability-metrics.dw5gLZ
|
||||
METRIC_ROOT=$TASK7_METRIC_ROOT
|
||||
export PLATFORM_OBSERVABILITY_ROLLBACK_ID="$TASK7_ID"
|
||||
[[ "$PLATFORM_OBSERVABILITY_ROLLBACK_ID" == "$TASK7_ID" ]]
|
||||
[[ "$METRIC_ROOT" == "$TASK7_METRIC_ROOT" ]]
|
||||
[[ "$(/usr/bin/readlink -f -- "$TASK7_METRIC_ROOT")" == "$TASK7_METRIC_ROOT" ]]
|
||||
|
||||
TASK7_HOST_DRY=(/usr/bin/bash)
|
||||
TASK7_HOST_DRY+=(/home/donghyeon/workspace/platform/scripts/bootstrap/apply-host-nginx-observability.sh)
|
||||
[[ "${#TASK7_HOST_DRY[@]}" -eq 2 ]]
|
||||
case $- in *e*) TASK7_HOST_DRY_ERREXIT=1 ;; *) TASK7_HOST_DRY_ERREXIT=0 ;; esac
|
||||
set +e
|
||||
"${TASK7_HOST_DRY[@]}"
|
||||
TASK7_HOST_DRY_RC=$?
|
||||
printf 'TASK7_HOST_DRY_RC=%d\n' "$TASK7_HOST_DRY_RC"
|
||||
(( TASK7_HOST_DRY_ERREXIT == 0 )) || set -e
|
||||
```
|
||||
|
||||
no-arg는 source/hash/no-contact만 검증합니다. certificate/SAN, DNS, Kubernetes, proof
|
||||
ID/age, NodePort와 network boundary는 execute 내부의 `APPLY` prompt 전 live gate입니다.
|
||||
dry-run은 Host mutation `0`과 다음 exact output을 요구합니다.
|
||||
|
||||
```text
|
||||
HOST_NGINX_ACTIVE_SHA256=dbef6d443bcba58b26a5351ea76f6d09f6da8c2ef07a806e22745cf26c88f518
|
||||
HOST_NGINX_GRAFANA_DENY_GUARD_SHA256=dbef6d443bcba58b26a5351ea76f6d09f6da8c2ef07a806e22745cf26c88f518
|
||||
HOST_NGINX_FULL_SHA256=7d2de2a92c3597a0859775da1d2ccbf5a3d72c0b2af5cac2439c82311361f801
|
||||
HOST_NGINX_CERTIFICATE_EXPECTED_SAN=grafana.learn.hyeonworks.com
|
||||
HOST_NGINX_CERTIFICATE_SAN=NOT_CHECKED_DRY_RUN
|
||||
HOST_NGINX_GRAFANA_PUBLIC_DNS=NOT_CHECKED_DRY_RUN
|
||||
HOST_NGINX_OBSERVABILITY_DRY_RUN=PASS
|
||||
TASK7_HOST_DRY_RC=0
|
||||
```
|
||||
|
||||
`HOST_NGINX_CERTIFICATE_EXACT_SAN=`나 `HOST_NGINX_GRAFANA_PUBLIC_DNS=ABSENT`를 dry-run
|
||||
결과로 받으면 STOP합니다. source proof는 exact ID/deny hash/status/IP/time에 bind되며
|
||||
과거 24시간, 미래 300초 경계를 벗어나면 fresh rollback ID, fresh proof, complete Task 6를
|
||||
다시 수행합니다. 같은 ID에서 proof만 바꾸거나 timestamp를 다시 쓰지 않습니다.
|
||||
|
||||
execute가 `APPLY`를 표시하기 전에 다음 네 path가 각각 `test -e`/`test -L` 모두에서
|
||||
absent임을 no-follow, name-only 순서로 입증해야 합니다.
|
||||
|
||||
```text
|
||||
/var/lib/hyeonworks/platform-rollbacks/observability-20260814T145009Z/host-nginx
|
||||
/var/lib/hyeonworks/platform-rollbacks/observability-20260814T145009Z/host-nginx/stages.tsv
|
||||
/var/lib/hyeonworks/platform-rollbacks/observability-20260814T145009Z/host-nginx/payloads
|
||||
/var/lib/hyeonworks/platform-rollbacks/observability-20260814T145009Z/host-nginx/payloads/full-prior-0001.conf
|
||||
```
|
||||
|
||||
일반 directory/file로 남은 ledger/payload도 reusable recovery state가 아니라 STOP residue입니다.
|
||||
reviewed sudo identity를 다시 확인한 뒤에만 operator가 credential를 refresh합니다.
|
||||
|
||||
```bash
|
||||
/usr/bin/sudo -v
|
||||
/usr/bin/sudo -n /usr/bin/true
|
||||
printf 'SUDO_READY\n'
|
||||
|
||||
TASK7_VOD=--verified
|
||||
TASK7_VOD+=-output-dir
|
||||
TASK7_HOST=(/usr/bin/bash)
|
||||
TASK7_HOST+=(/home/donghyeon/workspace/platform/scripts/bootstrap/apply-host-nginx-observability.sh)
|
||||
TASK7_HOST+=(--execute)
|
||||
TASK7_HOST+=("$TASK7_VOD")
|
||||
TASK7_HOST+=("$TASK7_METRIC_ROOT")
|
||||
[[ "${#TASK7_HOST[@]}" -eq 5 ]]
|
||||
printf 'TASK7_HOST_ARGC=%d\n' "${#TASK7_HOST[@]}"
|
||||
case $- in *e*) TASK7_HOST_ERREXIT=1 ;; *) TASK7_HOST_ERREXIT=0 ;; esac
|
||||
set +e
|
||||
"${TASK7_HOST[@]}"
|
||||
TASK7_HOST_RC=$?
|
||||
printf 'TASK7_HOST_RC=%d\n' "$TASK7_HOST_RC"
|
||||
(( TASK7_HOST_ERREXIT == 0 )) || set -e
|
||||
```
|
||||
|
||||
operator만 exact `APPLY`를 입력합니다. 성공은 exact `HOST_NGINX_FULL_STAGE=PASS`와
|
||||
`TASK7_HOST_RC=0`이 모두 있을 때뿐입니다. `ALREADY_ACTIVE`, missing/ambiguous marker,
|
||||
nonzero RC, response loss는 모두 실패이며 같은 ID로 재실행하지 않습니다.
|
||||
|
||||
1. prompt 전 실패: active deny unchanged, Host ledger/mutation `0`, rollback N/A.
|
||||
2. prompt 뒤 `rollback_armed=true` 전 실패: active config/reload mutation `0`, root-owned
|
||||
ledger/payload 또는 timestamp backup staging은 남을 수 있으며 rollback N/A. 전체 evidence를
|
||||
보존합니다.
|
||||
3. active install 뒤 실패: exact `HOST_NGINX_OBSERVABILITY_ROLLBACK=PASS`와 deny hash
|
||||
복원을 요구합니다.
|
||||
4. `ROLLBACK=FAIL`, `MANUAL_RECOVERY_REQUIRED=YES`, unknown stage/hash: 모든 후속 gate를
|
||||
STOP합니다.
|
||||
|
||||
`stages.tsv`는 prior-payload recovery ledger이지 success marker가 아닙니다. ledger/payload/timestamp
|
||||
staging 생성·검증·설치 중 하나라도 실패하면 현 ID/root를 보존하고 fresh ID,
|
||||
source proof, complete Task 6를 다시 수행합니다. staged evidence를 repair/reuse하거나
|
||||
failed fresh ID를 재사용하지 않습니다.
|
||||
|
||||
## 7. OIDC membership과 사람/external readiness
|
||||
|
||||
Host PASS 뒤 mutation 전에 서로 다른 admin, viewer, no-group, membership-removal test
|
||||
identity, local break-glass 접근, Slack firing/resolved view, LAN/Tailscale 밖 proxy-disabled
|
||||
external client를 모두 준비합니다. 하나라도 없으면 membership와 smoke를 시작하지
|
||||
않습니다. username은 stdin으로만 받고 기록하지 않습니다.
|
||||
|
||||
```bash
|
||||
read -r -p 'Grafana organization admin realm username: ' OBS_ADMIN_USER
|
||||
read -r -p 'Grafana viewer realm username: ' OBS_VIEWER_USER
|
||||
TASK7_OIDC=(/usr/bin/bash /home/donghyeon/workspace/platform/scripts/bootstrap/configure-keycloak-grafana-oidc.sh)
|
||||
TASK7_OIDC+=(--execute)
|
||||
TASK7_OIDC+=(--admin "$OBS_ADMIN_USER")
|
||||
TASK7_OIDC+=(--viewer "$OBS_VIEWER_USER")
|
||||
case $- in *e*) TASK7_OIDC_ERREXIT=1 ;; *) TASK7_OIDC_ERREXIT=0 ;; esac
|
||||
set +e
|
||||
"${TASK7_OIDC[@]}"
|
||||
TASK7_OIDC_RC=$?
|
||||
unset OBS_ADMIN_USER OBS_VIEWER_USER TASK7_OIDC
|
||||
printf 'TASK7_OIDC_RC=%d\n' "$TASK7_OIDC_RC"
|
||||
(( TASK7_OIDC_ERREXIT == 0 )) || set -e
|
||||
```
|
||||
|
||||
operator는 exact `APPLY default`와 `RECOVERY KEYCLOAK default`를 입력합니다.
|
||||
`GRAFANA_OIDC_TRANSACTION=PASS`와 RC `0`을 모두 요구합니다.
|
||||
`transaction_active=true` 전 실패는 managed Keycloak/OIDC Secret/membership mutation `0`,
|
||||
rollback N/A입니다. active failure는 exact `GRAFANA_OIDC_ROLLBACK=PASS`를 요구합니다.
|
||||
rollback FAIL, manual recovery 또는 unknown stage는 STOP입니다. OIDC rollback은 실행 중 private
|
||||
snapshot을 사용하는 in-process rollback입니다. 성공 뒤 복원용 persistent Task 6 Keycloak
|
||||
reversal ledger가 있다고 주장하거나 탐색하지 않습니다.
|
||||
|
||||
## 8. monolithic observability smoke exactly once
|
||||
|
||||
no-arg는 acceptance가 아닌 계획 확인으로 한 번만 실행합니다.
|
||||
|
||||
```bash
|
||||
TASK7_SMOKE_DRY=(/usr/bin/bash)
|
||||
TASK7_SMOKE_DRY+=(/home/donghyeon/workspace/platform/scripts/validate/observability-smoke.sh)
|
||||
[[ "${#TASK7_SMOKE_DRY[@]}" -eq 2 ]]
|
||||
case $- in *e*) TASK7_SMOKE_DRY_ERREXIT=1 ;; *) TASK7_SMOKE_DRY_ERREXIT=0 ;; esac
|
||||
set +e
|
||||
"${TASK7_SMOKE_DRY[@]}"
|
||||
TASK7_SMOKE_DRY_RC=$?
|
||||
printf 'TASK7_SMOKE_DRY_RC=%d\n' "$TASK7_SMOKE_DRY_RC"
|
||||
(( TASK7_SMOKE_DRY_ERREXIT == 0 )) || set -e
|
||||
```
|
||||
|
||||
RC `0`, `OBSERVABILITY_SMOKE_DRY_RUN=PASS`, `HUMAN_EXTERNAL_CLIENT=required`,
|
||||
`MUTATION=NOT_REQUESTED`를 요구합니다. 그 뒤 machine, OIDC human/session, Slack
|
||||
firing/resolved, true external-client attestation을 하나의 execute에서만 수행합니다.
|
||||
|
||||
```bash
|
||||
TASK7_SMOKE=(/usr/bin/bash)
|
||||
TASK7_SMOKE+=(/home/donghyeon/workspace/platform/scripts/validate/observability-smoke.sh)
|
||||
TASK7_SMOKE+=(--execute)
|
||||
case $- in *e*) TASK7_SMOKE_ERREXIT=1 ;; *) TASK7_SMOKE_ERREXIT=0 ;; esac
|
||||
set +e
|
||||
"${TASK7_SMOKE[@]}"
|
||||
TASK7_SMOKE_RC=$?
|
||||
printf 'TASK7_SMOKE_RC=%d\n' "$TASK7_SMOKE_RC"
|
||||
(( TASK7_SMOKE_ERREXIT == 0 )) || set -e
|
||||
```
|
||||
|
||||
operator만 requested identity와 exact dynamic confirmation을 입력합니다. 성공은 다음 전체
|
||||
marker와 immediate RC를 요구합니다.
|
||||
|
||||
```text
|
||||
OBSERVABILITY_MACHINE_ACCEPTANCE=PASS
|
||||
OBSERVABILITY_OIDC_ACCEPTANCE=PASS
|
||||
OBSERVABILITY_SLACK_ACCEPTANCE=PASS
|
||||
OBSERVABILITY_EXTERNAL_BOUNDARY=PASS
|
||||
OBSERVABILITY_SMOKE=PASS
|
||||
TASK7_SMOKE_RC=0
|
||||
```
|
||||
|
||||
RC `2` 또는 `OBSERVABILITY_EXTERNAL_BOUNDARY=PARTIAL`은 Task 7을 `부분 구현`으로 남깁니다.
|
||||
external result를 server-side, LAN 또는 Tailscale probe로 대체하지 않습니다. cleanup ambiguity는
|
||||
owned-object review 전 automatic rerun을 금지합니다.
|
||||
|
||||
## 9. fresh inventory-only renderer root와 단일 회귀 pass
|
||||
|
||||
fixed PATH를 다시 설치하고 `hash -r`, command inventory byte equality를 external prep 전에
|
||||
확인합니다. original `dw5gLZ`의 canonical path, owner/mode/nlink, exact entry set, two
|
||||
inventory hash와 three Task 6 YAML fingerprint를 보존합니다. complete publication에 original
|
||||
root를 사용하지 않습니다.
|
||||
|
||||
```bash
|
||||
TASK7_RENDER_ROOT="$(/usr/bin/mktemp -d /tmp/platform-observability-metrics.XXXXXX)"
|
||||
/usr/bin/chmod 0700 "$TASK7_RENDER_ROOT"
|
||||
for phase in target-initial post-substrate; do
|
||||
/usr/bin/install -d -m 0700 -- "$TASK7_RENDER_ROOT/$phase"
|
||||
for file in inventory.json inventory.sha256; do
|
||||
source_file="$TASK7_METRIC_ROOT/$phase/$file"
|
||||
destination_file="$TASK7_RENDER_ROOT/$phase/$file"
|
||||
[[ -f "$source_file" && ! -L "$source_file" ]]
|
||||
/usr/bin/cp --no-dereference --reflink=never -- "$source_file" "$destination_file"
|
||||
/usr/bin/chmod 0600 "$destination_file"
|
||||
/usr/bin/cmp -s -- "$source_file" "$destination_file"
|
||||
done
|
||||
done
|
||||
unset source_file destination_file
|
||||
```
|
||||
|
||||
destination은 exact six-entry topology, current owner, root/phase `0700`, file `0600`, nlink `1`,
|
||||
byte equality와 known inventory hash를 요구합니다. copy 뒤 original fingerprint가 unchanged여야
|
||||
합니다. 실패한 fresh root는 evidence로 보존하고 repair/reuse하지 않습니다.
|
||||
|
||||
core와 complete renderer, admin UI, AIStor S3, phase1, phase2, admin renderer를 다음 exact
|
||||
array/envelope로 각각 한 번만 실행합니다.
|
||||
|
||||
```bash
|
||||
TASK7_CORE=(/usr/bin/bash)
|
||||
TASK7_CORE+=(/home/donghyeon/workspace/platform/scripts/validate/render-observability-core.sh)
|
||||
[[ "${#TASK7_CORE[@]}" -eq 2 ]]
|
||||
case $- in *e*) TASK7_CORE_ERREXIT=1 ;; *) TASK7_CORE_ERREXIT=0 ;; esac
|
||||
set +e
|
||||
PLATFORM_HELM_BIN=/home/donghyeon/.local/bin/helm "${TASK7_CORE[@]}"
|
||||
TASK7_CORE_RC=$?
|
||||
(( TASK7_CORE_ERREXIT == 0 )) || set -e
|
||||
printf 'TASK7_CORE_RC=%d\n' "$TASK7_CORE_RC"
|
||||
|
||||
TASK7_VOD=--verified
|
||||
TASK7_VOD+=-output-dir
|
||||
TASK7_COMPLETE=(/usr/bin/bash)
|
||||
TASK7_COMPLETE+=(/home/donghyeon/workspace/platform/scripts/validate/render-observability-access.sh)
|
||||
TASK7_COMPLETE+=(--component complete)
|
||||
TASK7_COMPLETE+=("$TASK7_VOD" "$TASK7_RENDER_ROOT")
|
||||
[[ "${#TASK7_COMPLETE[@]}" -eq 6 ]]
|
||||
case $- in *e*) TASK7_COMPLETE_ERREXIT=1 ;; *) TASK7_COMPLETE_ERREXIT=0 ;; esac
|
||||
set +e
|
||||
PLATFORM_HELM_BIN=/home/donghyeon/.local/bin/helm "${TASK7_COMPLETE[@]}"
|
||||
TASK7_COMPLETE_RC=$?
|
||||
(( TASK7_COMPLETE_ERREXIT == 0 )) || set -e
|
||||
printf 'TASK7_COMPLETE_RC=%d\n' "$TASK7_COMPLETE_RC"
|
||||
|
||||
TASK7_ADMIN_UI=(/usr/bin/bash)
|
||||
TASK7_ADMIN_UI+=(/home/donghyeon/workspace/platform/scripts/validate/admin-ui-smoke.sh)
|
||||
[[ "${#TASK7_ADMIN_UI[@]}" -eq 2 ]]
|
||||
case $- in *e*) TASK7_ADMIN_UI_ERREXIT=1 ;; *) TASK7_ADMIN_UI_ERREXIT=0 ;; esac
|
||||
set +e
|
||||
"${TASK7_ADMIN_UI[@]}"
|
||||
TASK7_ADMIN_UI_RC=$?
|
||||
(( TASK7_ADMIN_UI_ERREXIT == 0 )) || set -e
|
||||
printf 'TASK7_ADMIN_UI_RC=%d\n' "$TASK7_ADMIN_UI_RC"
|
||||
|
||||
TASK7_AISTOR_S3=(/usr/bin/bash)
|
||||
TASK7_AISTOR_S3+=(/home/donghyeon/workspace/platform/scripts/validate/aistor-s3-smoke.sh)
|
||||
TASK7_AISTOR_S3+=(--execute)
|
||||
[[ "${#TASK7_AISTOR_S3[@]}" -eq 3 ]]
|
||||
case $- in *e*) TASK7_AISTOR_S3_ERREXIT=1 ;; *) TASK7_AISTOR_S3_ERREXIT=0 ;; esac
|
||||
set +e
|
||||
"${TASK7_AISTOR_S3[@]}"
|
||||
TASK7_AISTOR_S3_RC=$?
|
||||
(( TASK7_AISTOR_S3_ERREXIT == 0 )) || set -e
|
||||
printf 'TASK7_AISTOR_S3_RC=%d\n' "$TASK7_AISTOR_S3_RC"
|
||||
|
||||
TASK7_PHASE1=(/usr/bin/bash)
|
||||
TASK7_PHASE1+=(/home/donghyeon/workspace/platform/scripts/validate/render-phase1.sh)
|
||||
[[ "${#TASK7_PHASE1[@]}" -eq 2 ]]
|
||||
case $- in *e*) TASK7_PHASE1_ERREXIT=1 ;; *) TASK7_PHASE1_ERREXIT=0 ;; esac
|
||||
set +e
|
||||
PLATFORM_HELM_BIN=/home/donghyeon/.local/bin/helm "${TASK7_PHASE1[@]}"
|
||||
TASK7_PHASE1_RC=$?
|
||||
(( TASK7_PHASE1_ERREXIT == 0 )) || set -e
|
||||
printf 'TASK7_PHASE1_RC=%d\n' "$TASK7_PHASE1_RC"
|
||||
|
||||
TASK7_PHASE2=(/usr/bin/bash)
|
||||
TASK7_PHASE2+=(/home/donghyeon/workspace/platform/scripts/validate/render-phase2.sh)
|
||||
[[ "${#TASK7_PHASE2[@]}" -eq 2 ]]
|
||||
case $- in *e*) TASK7_PHASE2_ERREXIT=1 ;; *) TASK7_PHASE2_ERREXIT=0 ;; esac
|
||||
set +e
|
||||
PLATFORM_HELM_BIN=/home/donghyeon/.local/bin/helm "${TASK7_PHASE2[@]}"
|
||||
TASK7_PHASE2_RC=$?
|
||||
(( TASK7_PHASE2_ERREXIT == 0 )) || set -e
|
||||
printf 'TASK7_PHASE2_RC=%d\n' "$TASK7_PHASE2_RC"
|
||||
|
||||
TASK7_ADMIN_RENDER=(/usr/bin/bash)
|
||||
TASK7_ADMIN_RENDER+=(/home/donghyeon/workspace/platform/scripts/validate/render-admin-services.sh)
|
||||
[[ "${#TASK7_ADMIN_RENDER[@]}" -eq 2 ]]
|
||||
case $- in *e*) TASK7_ADMIN_RENDER_ERREXIT=1 ;; *) TASK7_ADMIN_RENDER_ERREXIT=0 ;; esac
|
||||
set +e
|
||||
PLATFORM_HELM_BIN=/home/donghyeon/.local/bin/helm "${TASK7_ADMIN_RENDER[@]}"
|
||||
TASK7_ADMIN_RENDER_RC=$?
|
||||
(( TASK7_ADMIN_RENDER_ERREXIT == 0 )) || set -e
|
||||
printf 'TASK7_ADMIN_RENDER_RC=%d\n' "$TASK7_ADMIN_RENDER_RC"
|
||||
```
|
||||
|
||||
다섯 renderer의 exact Helm assignment을 생략하지 않습니다. 특히 phase1/phase2는 fixed
|
||||
PATH에 Helm이 없으므로 `command -v helm` fallback을 허용하지 않습니다. 전체 RC `0`,
|
||||
expected terminal PASS, complete seven-artifact publication, original fingerprint unchanged와 new
|
||||
residue `0`을 요구합니다. 존재하지 않는 core smoke를 호출하지 않고 monolithic smoke
|
||||
execute를 다시 실행하지 않습니다.
|
||||
|
||||
## 10. rollback·STOP 경계
|
||||
|
||||
Host/rules recovery evidence는 성공 Task 6 root에서 각 transaction 소유 범위만 사용합니다.
|
||||
OIDC는 persistent Task 6 reversal ledger가 아니라 in-process private snapshot으로만 rollback합니다.
|
||||
PVC, Secret, CRD, PV, Loki/Tempo object·bucket은 자동 삭제하지 않습니다. API timeout,
|
||||
response loss, UID drift, third-state, controller 비수렴, ledger mismatch, rollback ambiguity는
|
||||
`MANUAL_RECOVERY_REQUIRED=YES`로 STOP하고 evidence/root를 보존합니다.
|
||||
|
||||
## 11. 전체 완료 판정
|
||||
|
||||
Task 7은 Host/OIDC/smoke/renderer의 실제 RC·marker·cleanup과 independent review가 모두 있을
|
||||
때만 완료로 표시합니다. admin, viewer, no-group, membership-removal, break-glass, Slack
|
||||
firing/resolved, true external client 중 하나라도 미실행/실패면 `부분 구현`을 유지합니다.
|
||||
Slack off-host DR은 Task 7 PASS와 무관하게 `deferred / not complete`이며 active exception을
|
||||
유지합니다. 실제 terminal evidence의 independent review 전에는 중앙 Task 7 Step 1–6
|
||||
checkbox를 체크하지 않고 Task 8을 시작하지 않습니다.
|
||||
Reference in New Issue
Block a user