diff --git a/.gitea/workflows/promote-image.yaml b/.gitea/workflows/promote-image.yaml index be7bbf6..c9cfa7d 100644 --- a/.gitea/workflows/promote-image.yaml +++ b/.gitea/workflows/promote-image.yaml @@ -52,7 +52,7 @@ jobs: echo "image_digest must be an immutable sha256 digest" >&2 exit 1 fi - manifest="clusters/dev-k3s/manifests/${SERVICE}/kustomization.yaml" + manifest="clusters/dev-k3s/overlays/workloads/${SERVICE}/kustomization.yaml" temporary="$(mktemp "${manifest}.XXXXXX")" awk -v image_name="$image_name" -v image_digest="$IMAGE_DIGEST" ' $1 == "-" && $2 == "name:" { target = ($3 == image_name) } @@ -85,7 +85,7 @@ jobs: git config user.name "gitops-bot" git config user.email "gitops-bot@hyeonworks.local" git switch -c "$branch" - git add "clusters/dev-k3s/manifests/${SERVICE}/kustomization.yaml" + git add "clusters/dev-k3s/overlays/workloads/${SERVICE}/kustomization.yaml" git commit -m "chore(gitops): promote ${SERVICE} dev digest" git push --set-upstream origin "$branch" echo "PROMOTION_BRANCH=$branch" >>"$GITHUB_ENV" diff --git a/AGENTS.md b/AGENTS.md index f8c15b5..7e65be9 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -7,6 +7,16 @@ Vault init JSON, unseal/recovery material or plaintext credentials. - A Vault API object may be owned by only one Terraform state. - Keep secret payloads outside Terraform resources and data sources. +- Classify shared capabilities under `platform`, bounded-context backing + services under `systems`, and first-party runtimes under `workloads`. +- Manage child Argo CD Applications through the permission-scoped + ApplicationSets in `platform/control-plane/argocd`; do not add explicit + child Applications or use the `default` AppProject. +- Add new ApplicationSet entries with `autoSync: "false"` and open each gate + only after its documented external prerequisites have been verified. +- Keep `vault-foundation`, `vault-workloads`, and `vault-database` as separate + states. A delegated state must not own the policy or login role that grants + its own execution identity. - Routine GitOps automation changes Git only; direct cluster mutation is reserved for documented bootstrap and recovery runbooks. - Run `make validate` before handing off repository changes. diff --git a/CHANGELOG.md b/CHANGELOG.md index 1b10e31..0394dbf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,35 @@ # Changelog +## 2026-07-26 + +- Clarified that this repository is an independent GitOps reference lab, not a + shared production platform or an application source monorepo. +- Reclassified Kubernetes ownership as `platform`, `systems` and `workloads`; + moved the Project Auth PostgreSQL/Keycloak boundary to + `systems/auth-system`. +- Replaced generic cluster `manifests` with ownership-aligned + `clusters/dev-k3s/overlays`. +- Moved the Argo control-plane inventory to + `platform/control-plane/argocd` and separated permission-scoped AppProjects + for addons, shared services, systems and workloads. +- Added a bootstrap-only `gitops-control-plane` AppProject so the root no + longer reconciles through Argo CD's unrestricted `default` project. +- Replaced repeated child Application definitions with strict + list-generated ApplicationSets and explicit `autoSync` bootstrap gates. +- Renamed the Project Auth backing-system namespace to `auth-system-dev` and + aligned service DNS, NetworkPolicy and ConfigMap ownership. +- Changed Vault KV ownership from legacy `dev/platform` paths to + `dev/systems/auth-system` and `dev/workloads/auth-server` paths. +- Split the broad `vault-core` ownership into `vault-foundation` and + `vault-workloads`, retaining `vault-database` as a third isolated state. +- Documented delegated Terraform identities, stage-by-stage bootstrap and + non-destructive state/path migration procedures. +- Kept GHCR as the image artifact boundary and made immutable digests the + promotion target; existing short commit tags remain until a registry-verified + promotion PR replaces them. +- Performed repository-only refactoring and static validation; no Kubernetes, + Vault, Argo CD, registry or remote Terraform backend was mutated. + ## 2026-07-25 - Established the internal Gitea repository as the single GitOps source. diff --git a/INTERN_GUIDE.md b/INTERN_GUIDE.md index aff377b..43747f1 100644 --- a/INTERN_GUIDE.md +++ b/INTERN_GUIDE.md @@ -1,74 +1,116 @@ -# Intern Guide +# Intern guide -## 먼저 이해할 것 +## 이 저장소의 역할 -이 저장소에는 서로 다른 세 개의 reconciliation 경계가 있습니다. +이 저장소는 Project Auth를 예제로 한 독립 GitOps reference lab입니다. +애플리케이션 소스나 범용 production platform이 아닙니다. 현재 지원하는 +환경은 `dev-k3s` 하나입니다. -1. Gitea가 승인된 desired-state revision을 저장합니다. +서로 다른 세 reconciliation 경계를 먼저 구분합니다. + +1. Gitea `main`이 승인된 desired-state revision을 저장합니다. 2. Argo CD가 그 revision의 Kubernetes 리소스를 지속적으로 맞춥니다. 3. Terraform이 승인된 실행 환경에서 Vault API 객체를 관리합니다. -Argo CD가 Terraform을 실행하지 않으며 CI가 routine deployment를 위해 -`kubectl apply`를 호출하지 않습니다. secret 값도 Git이나 Terraform을 +GHCR은 빌드된 image를 보관할 뿐 desired-state source가 아닙니다. Argo +CD가 Terraform을 실행하지 않으며 CI가 routine deployment를 위해 +`kubectl apply`를 호출하지 않습니다. Secret 값도 Git이나 Terraform을 통과하지 않습니다. +## 디렉터리를 고르는 법 + +- 여러 system이 공유하는 cluster capability: `platform` +- Project Auth bounded context 전용 backing service: `systems/auth-system` +- 별도 source repository에서 빌드하는 서버: `workloads` +- Dev namespace, digest, host, Vault annotation: `clusters/dev-k3s/overlays` +- Vault API 객체: `iac/terraform` + +현재 Vault는 platform shared service, PostgreSQL과 Keycloak은 auth-system, +`auth-server`와 `api-server`는 workload입니다. 설치 순서나 중요도로 +`foundation`을 만들지 않습니다. 자세한 기준은 +`docs/architecture/repository-taxonomy.md`에 있습니다. + ## 안전한 변경 흐름 -1. `refactor/...`, `feat/...`, `fix/...` 브랜치에서 변경합니다. +1. `refactor/...`, `feat/...`, `fix/...` branch에서 변경합니다. 2. `make validate`를 실행합니다. -3. rendered manifest 또는 Terraform plan을 검토합니다. +3. Rendered manifest 또는 Terraform plan을 검토합니다. 4. 내부 Gitea에 PR을 생성합니다. 5. 승인 후 `main`에 merge합니다. -6. Kubernetes 변경은 Argo CD가 자동 반영합니다. -7. Terraform 변경은 별도 승인 후 실행합니다. +6. Kubernetes 변경은 열린 `autoSync` gate에서 Argo CD가 반영합니다. +7. Terraform 변경은 해당 state identity로 별도 승인 후 실행합니다. + +새 ApplicationSet element는 기본적으로 `autoSync: "false"`로 추가합니다. +선행 controller, Vault 구성, secret과 database 준비를 확인한 별도 PR에서 +gate를 엽니다. Gate가 닫혀도 수동 Sync는 가능하므로 임의로 누르지 +않습니다. 금지 사항: - `.terraform`, state, plan, tfvars, Vault init JSON, token commit - 동일 Vault path/resource를 두 state에서 관리 -- image promotion 자동화의 `main` 직접 push -- routine CI의 직접 `kubectl apply` -- production skeleton이나 이름뿐인 production Application 추가 -- hook을 사용하는 Application에 `ApplyOutOfSyncOnly=true` 적용 +- Secret payload를 Terraform resource/data source로 관리 +- Image promotion workflow의 `main` 직접 push +- Routine CI 또는 사람의 직접 `kubectl apply` +- Production skeleton이나 이름뿐인 production Application 추가 +- Hook을 사용하는 Application에 `ApplyOutOfSyncOnly=true` 적용 +- `autoSync: "true"` 전환 PR에서 누적 live diff를 확인하지 않음 -## 자주 쓰는 명령 +## 자주 쓰는 읽기 전용 명령 최종 dev manifest 렌더링: ```bash -kubectl kustomize clusters/dev-k3s/manifests/auth-server -kubectl kustomize clusters/dev-k3s/manifests/auth-system +kubectl kustomize clusters/dev-k3s/overlays/workloads/auth-server +kubectl kustomize clusters/dev-k3s/overlays/systems/auth-system +kubectl kustomize clusters/dev-k3s/overlays/platform/vault ``` -전체 검증: +전체 정적 검증: ```bash make validate ``` -backend 없이 Terraform configuration 검증: +Backend 없이 Terraform configuration 검증: ```bash -terraform -chdir=iac/terraform/live/dev-k3s/vault-core init -backend=false -terraform -chdir=iac/terraform/live/dev-k3s/vault-core validate +for terraform_root in vault-foundation vault-workloads vault-database; do + terraform_data_dir="$(mktemp -d)" + TF_DATA_DIR="$terraform_data_dir" \ + terraform -chdir="iac/terraform/live/dev-k3s/${terraform_root}" \ + init -backend=false -input=false -lockfile=readonly + TF_DATA_DIR="$terraform_data_dir" \ + terraform -chdir="iac/terraform/live/dev-k3s/${terraform_root}" validate + rm -rf "$terraform_data_dir" +done ``` -실제 plan: +일반적으로는 같은 검사를 포함한 `make validate`를 사용합니다. 위 예는 +provider data를 repository의 `.terraform`에 남기지 않습니다. + +실제 plan은 승인된 backend와 identity를 준비한 뒤 수행합니다. ```bash make terraform-plan \ - TF_ROOT=vault-core \ - BACKEND_CONFIG=.local/terraform-backend/dev-k3s/vault-core.s3.hcl + TF_ROOT=vault-workloads \ + BACKEND_CONFIG=.local/terraform-backend/dev-k3s/vault-workloads.s3.hcl ``` -image 승격은 Gitea `Promote Dev Image by Pull Request` workflow에 정확한 -`sha256:` digest를 전달합니다. workflow는 전용 브랜치와 PR을 만들며 +Image 승격은 Gitea `Promote Dev Image by Pull Request` workflow에 정확한 +`sha256:` digest를 전달합니다. Workflow는 전용 branch와 PR을 만들며 `main`에 직접 쓰지 않습니다. ## 읽는 순서 1. `README.md` -2. `docs/architecture/deployment.md` -3. `docs/architecture/secret-trust.md` -4. `docs/adr/` -5. 수행하려는 작업의 runbook +2. `docs/architecture/repository-taxonomy.md` +3. `docs/architecture/deployment.md` +4. `docs/architecture/argocd.md` +5. `docs/architecture/secret-trust.md` +6. `docs/architecture/terraform.md` +7. `docs/adr/` +8. 수행하려는 작업의 runbook + +2026-07-26 리팩터링은 repository에서만 구현·검증했으며 실제 cluster에 +적용하지 않았습니다. Live migration을 연습 과제로 실행하지 않습니다. diff --git a/Makefile b/Makefile index 43c4735..5200e3b 100644 --- a/Makefile +++ b/Makefile @@ -4,6 +4,7 @@ KUBE_CONTEXT ?= TF_ROOT ?= BACKEND_CONFIG ?= TF_DIR = iac/terraform/live/dev-k3s/$(TF_ROOT) +PLAN_FILE ?= $(CURDIR)/.local/terraform-plans/dev-k3s-$(TF_ROOT).tfplan bootstrap: @test -n "$(KUBE_CONTEXT)" || (echo "KUBE_CONTEXT is required" >&2; exit 1) @@ -17,8 +18,8 @@ vault-init: check-terraform-inputs: @case "$(TF_ROOT)" in \ - vault-core|vault-database) ;; \ - *) echo "TF_ROOT must be vault-core or vault-database" >&2; exit 1 ;; \ + vault-foundation|vault-workloads|vault-database) ;; \ + *) echo "TF_ROOT must be vault-foundation, vault-workloads, or vault-database" >&2; exit 1 ;; \ esac @test -f "$(BACKEND_CONFIG)" || (echo "BACKEND_CONFIG must point to a readable backend file" >&2; exit 1) @@ -26,9 +27,16 @@ terraform-init: check-terraform-inputs terraform -chdir="$(TF_DIR)" init -input=false -reconfigure -backend-config="$(abspath $(BACKEND_CONFIG))" terraform-plan: terraform-init - terraform -chdir="$(TF_DIR)" plan -input=false -lock-timeout=5m + @mkdir -p "$(dir $(PLAN_FILE))" + @umask 077; terraform -chdir="$(TF_DIR)" plan \ + -input=false \ + -lock-timeout=5m \ + -out="$(PLAN_FILE)" terraform-apply: terraform-init @test "$(APPROVE_APPLY)" = "dev-k3s/$(TF_ROOT)" || \ (echo "Set APPROVE_APPLY=dev-k3s/$(TF_ROOT) to continue" >&2; exit 1) - terraform -chdir="$(TF_DIR)" apply -input=false -lock-timeout=5m + @test -f "$(PLAN_FILE)" || \ + (echo "Run terraform-plan first; approved plan is missing: $(PLAN_FILE)" >&2; exit 1) + terraform -chdir="$(TF_DIR)" apply -input=false -lock-timeout=5m "$(PLAN_FILE)" + @rm -f "$(PLAN_FILE)" diff --git a/README.md b/README.md index ab18ce2..0d9c110 100644 --- a/README.md +++ b/README.md @@ -1,78 +1,122 @@ -# Project GitOps +# Project GitOps reference lab -Project Auth의 `dev-k3s` 배포 상태를 관리하는 GitOps configuration -저장소입니다. 배포 기준 저장소는 내부 Gitea 한 곳입니다. +Project Auth를 예제로 사용해 작은 팀의 GitOps 운영 경계를 학습하고 검증하는 +독립 reference lab입니다. 범용 사내 플랫폼이나 애플리케이션 소스 +monorepo가 아닙니다. 현재 지원 대상은 단일 `dev-k3s` 클러스터뿐입니다. + +배포 기준 저장소는 내부 Gitea 한 곳입니다. ```text https://git.learn.hyeonworks.com/donghyeon.kang/project-gitops ``` -애플리케이션 소스 저장소는 이미지를 빌드해 GHCR에 올리고, 이 저장소에는 -배포할 immutable digest를 변경하는 PR만 생성합니다. Kubernetes 리소스는 -Argo CD만 반영하며, Terraform은 Vault API만 관리합니다. +`auth-server`와 `api-server`의 소스 저장소는 이미지를 빌드해 GHCR에 +올립니다. 정상 promotion 계약은 검증된 immutable digest를 바꾸는 PR입니다. +현재 overlay에는 이관 전의 짧은 commit tag가 남아 있으며, registry를 +검증할 credential 없이 임의 digest로 바꾸지 않았습니다. 다음 정상 +promotion workflow가 digest로 전환합니다. + +Kubernetes desired state는 Argo CD가, Vault API 객체는 Terraform이 각각 +관리합니다. KV secret 값은 어느 쪽에도 저장하지 않습니다. ## 지원 범위 | 대상 | 상태 | |---|---| | `dev-k3s` | 지원하는 단일 노드 개발 환경 | -| production | 설계되지 않았으며 manifest/Application이 존재하지 않음 | -| Istio | 보류; 향후 ambient mode 후보 | +| production | 설계되지 않았으며 overlay/Application이 없음 | +| Istio | 보류; 향후 ambient mode 도입 조건만 기록 | -현재 dev Vault와 PostgreSQL, ingress는 TLS가 적용되지 않은 개발 프로파일입니다. -production 용도로 사용할 수 없습니다. +현재 Vault, PostgreSQL, ingress는 TLS가 없는 개발 프로파일이며 single-node +failure domain을 공유합니다. production 용도로 사용할 수 없습니다. + +2026-07-26의 repository/ownership 리팩터링은 Git 작업 트리에만 설계하고 +검증했습니다. 실제 클러스터에는 적용하지 않았으며, 기존 리소스의 소유권 +이관은 관련 runbook과 별도 승인 없이 실행하면 안 됩니다. ## 제어 흐름 ```text -app CI -> GHCR digest -> GitOps PR -> validation -> main - | - v - Argo CD - | - v - Kubernetes +application CI -> GHCR digest -> GitOps PR -> validation -> main + | + v + Argo CD ApplicationSet + | + v + Kubernetes -IaC PR -> Terraform plan -> approval -> Terraform apply -> Vault API +IaC PR -> Terraform plan -> approval -> one Vault state apply -> Vault API ``` -Argo CD 최초 설치와 root Application seed만 클러스터에 직접 적용합니다. -그 이후 routine deployment는 Git 변경으로만 수행합니다. +Argo CD 최초 설치 뒤 bootstrap 전용 `gitops-control-plane` AppProject와 +단일 root Application만 순서대로 직접 적용합니다. 이후 routine deployment는 +Git 변경으로만 수행합니다. Terraform은 Argo CD hook이나 Config Management +Plugin 안에서 실행하지 않습니다. -## 저장소 구조 +## 저장소 분류 ```text -bootstrap/argocd/ 최초 Argo CD 설치 버전과 단일 root Application +bootstrap/argocd/ controller 버전, 제한된 AppProject와 단일 root seed clusters/dev-k3s/ - projects/ AppProject 권한 경계 - applications/ foundation, platform, workload child Applications - manifests/ dev-k3s가 실제 소비하는 최종 Kustomize 구성 -platform/ Vault와 auth platform의 환경 중립 base -workloads/ auth-server와 api-server의 환경 중립 base + overlays/ + platform/ 공유 서비스의 dev-k3s 최종 구성 + systems/ bounded context system의 dev-k3s 최종 구성 + workloads/ first-party workload의 dev-k3s 최종 구성 +platform/ + control-plane/argocd/ AppProject와 ApplicationSet inventory + shared-services/ 여러 system이 사용할 수 있는 cluster capability base +systems/ 특정 bounded context가 소유하는 backing system base +workloads/ source repository가 따로 있는 실행 애플리케이션 base iac/terraform/ modules/ 재사용 Vault 모듈 - live/dev-k3s/ vault-core, vault-database state - backend/dev-k3s/ secret을 포함하지 않는 backend 예시 -policies/ Terraform이 소비하는 Vault ACL -hack/ bootstrap, validation, 조건부 Vault init entrypoint -docs/ architecture, ADR, recovery/operation runbook + live/dev-k3s/ vault-foundation, vault-workloads, vault-database roots + backend/dev-k3s/ secret 없는 remote backend 예시 +policies/vault/ Terraform이 읽는 Vault ACL 문서 +hack/ bootstrap, validation, dev Vault init entrypoint +docs/ architecture, ADR, migration/operation runbook ``` -cluster-specific namespace, host, image, secret reference는 -`clusters/dev-k3s/manifests/`에서 완결합니다. `platform/`과 `workloads/`의 -base는 클러스터를 알지 못합니다. +분류는 제품 종류가 아니라 소비자, 소유자, 변경 주기로 결정합니다. +Vault는 공유 capability이므로 `platform/`, Project Auth 전용 PostgreSQL과 +Keycloak은 `systems/auth-system/`, 직접 빌드하는 서버는 `workloads/`에 +속합니다. namespace, host, image digest, Vault role, NetworkPolicy 같은 +클러스터별 값은 `clusters/dev-k3s/overlays/`에서 완결합니다. + +자세한 판단 기준은 +[repository taxonomy](docs/architecture/repository-taxonomy.md)와 +[ADR 0007](docs/adr/0007-repository-ownership-boundaries.md)을 따릅니다. + +## Argo CD 단계 gate + +Root Application은 `platform/control-plane/argocd`의 AppProject와 +ApplicationSet을 소유합니다. 각 inventory 항목은 `autoSync`를 quoted +string으로 명시해야 합니다. 새 항목이나 외부 준비 조건이 있는 항목은 +`autoSync: "false"`로 시작하고, 선행 controller, Vault 구성, runtime +secret, database 준비를 확인한 PR에서만 `autoSync: "true"`로 바꿉니다. + +Control-plane sync wave는 AppProject(`-10`)를 ApplicationSet(`-5`)보다 +먼저 생성할 뿐, generated Application의 readiness를 보장하지 않습니다. +`autoSync` gate와 workload의 retry/idempotency가 실제 단계 전환을 +담당합니다. ## Terraform state -| Root | 소유 범위 | 실행 권한 | +| Root | 소유 범위 | routine 실행 권한 | |---|---|---| -| `vault-core` | mounts, auth backends, policies, Kubernetes/JWT roles, app Transit key | 제한된 관리자 | -| `vault-database` | PostgreSQL connection과 dynamic database roles | `vault-database-automation-dev` | +| `vault-foundation` | mounts, auth backends/config, delegated automation policy와 선택적 CI JWT roles | 없음; bootstrap 또는 보안 관리자 승인 실행 | +| `vault-workloads` | workload ACL, Kubernetes auth role, 애플리케이션 Transit key | workload 전용 short-lived identity | +| `vault-database` | PostgreSQL connection과 `auth-db-migration-dev` dynamic role | database 전용 short-lived identity | -Vault API 객체 하나는 정확히 한 state만 소유합니다. KV secret payload, -Vault init JSON, token, PostgreSQL password는 Git이나 Terraform state에 -저장하지 않습니다. backend는 암호화, versioning, access control, -locking을 제공해야 합니다. +중요한 규칙은 다음과 같습니다. + +- Vault API 객체 하나는 정확히 한 state만 소유합니다. +- Delegated state는 자신에게 권한을 부여하는 policy/login role을 만들지 + 않습니다. 그것은 `vault-foundation`이 소유합니다. +- State 사이에 `terraform_remote_state`를 사용하지 않습니다. +- KV payload, Vault init JSON, token, PostgreSQL password를 Git이나 + Terraform state에 저장하지 않습니다. +- Backend는 encryption, versioning, access control, locking을 제공해야 + 합니다. ## 주요 명령 @@ -81,20 +125,25 @@ make validate make bootstrap KUBE_CONTEXT= make terraform-plan \ - TF_ROOT=vault-core \ - BACKEND_CONFIG=.local/terraform-backend/dev-k3s/vault-core.s3.hcl + TF_ROOT=vault-foundation \ + BACKEND_CONFIG=.local/terraform-backend/dev-k3s/vault-foundation.s3.hcl ``` -`terraform-apply`는 동일한 입력과 +`terraform-apply`는 동일 입력과 `APPROVE_APPLY=dev-k3s/`가 모두 있어야 실행되며 `auto-approve`를 -사용하지 않습니다. +사용하지 않습니다. 실제 bootstrap과 state 이동은 먼저 runbook의 중단 +조건을 확인합니다. ## 문서 -- [빈 dev 클러스터 bootstrap](docs/runbooks/dev-bootstrap.md) +- [Repository taxonomy](docs/architecture/repository-taxonomy.md) - [배포 구조](docs/architecture/deployment.md) -- [secret trust boundary](docs/architecture/secret-trust.md) -- [Terraform v2 state 이관](docs/runbooks/terraform-state-migration.md) +- [Argo CD 구조와 stage gate](docs/architecture/argocd.md) +- [Secret trust boundary](docs/architecture/secret-trust.md) +- [Terraform 사용 경계](docs/architecture/terraform.md) +- [빈 dev 클러스터 bootstrap](docs/runbooks/dev-bootstrap.md) +- [Application 안전한 decommission](docs/runbooks/application-decommission.md) +- [Terraform state 이관](docs/runbooks/terraform-state-migration.md) - [Vault backup/recovery](docs/runbooks/vault-backup-restore.md) - [Sealed Secrets recovery](docs/runbooks/sealed-secrets-recovery.md) - [입문 가이드](INTERN_GUIDE.md) diff --git a/bootstrap/argocd/control-plane-project.yaml b/bootstrap/argocd/control-plane-project.yaml new file mode 100644 index 0000000..c3953c5 --- /dev/null +++ b/bootstrap/argocd/control-plane-project.yaml @@ -0,0 +1,23 @@ +apiVersion: argoproj.io/v1alpha1 +kind: AppProject +metadata: + name: gitops-control-plane + namespace: argocd + annotations: + argocd.argoproj.io/sync-options: Prune=confirm,Delete=confirm + finalizers: + - resources-finalizer.argocd.argoproj.io +spec: + description: Bootstrap-only boundary for the project-gitops control plane + sourceRepos: + - https://git.learn.hyeonworks.com/donghyeon.kang/project-gitops + destinations: + - namespace: argocd + server: https://kubernetes.default.svc + namespaceResourceWhitelist: + - group: argoproj.io + kind: AppProject + - group: argoproj.io + kind: ApplicationSet + orphanedResources: + warn: true diff --git a/clusters/dev-k3s/applications/kustomization.yaml b/bootstrap/argocd/kustomization.yaml similarity index 57% rename from clusters/dev-k3s/applications/kustomization.yaml rename to bootstrap/argocd/kustomization.yaml index 2057204..a597acc 100644 --- a/clusters/dev-k3s/applications/kustomization.yaml +++ b/bootstrap/argocd/kustomization.yaml @@ -2,6 +2,5 @@ apiVersion: kustomize.config.k8s.io/v1beta1 kind: Kustomization resources: - - foundation - - platform - - workloads + - control-plane-project.yaml + - root-application.yaml diff --git a/bootstrap/argocd/root-application.yaml b/bootstrap/argocd/root-application.yaml index f4d6af3..572d8e6 100644 --- a/bootstrap/argocd/root-application.yaml +++ b/bootstrap/argocd/root-application.yaml @@ -1,14 +1,14 @@ apiVersion: argoproj.io/v1alpha1 kind: Application metadata: - name: project-gitops-dev-k3s + name: project-gitops-control-plane namespace: argocd spec: - project: default + project: gitops-control-plane source: repoURL: https://git.learn.hyeonworks.com/donghyeon.kang/project-gitops targetRevision: main - path: clusters/dev-k3s + path: platform/control-plane/argocd destination: server: https://kubernetes.default.svc namespace: argocd diff --git a/clusters/dev-k3s/README.md b/clusters/dev-k3s/README.md new file mode 100644 index 0000000..0d5fc88 --- /dev/null +++ b/clusters/dev-k3s/README.md @@ -0,0 +1,32 @@ +# dev-k3s cluster profile + +`dev-k3s`는 이 reference lab이 현재 지원하는 유일한 cluster profile입니다. +ApplicationSet의 `server`는 in-cluster API +`https://kubernetes.default.svc`를 사용합니다. + +이 디렉터리는 base 복사본이 아니라 다음 cluster-specific composition만 +소유합니다. + +- `platform/vault`: dev Vault namespace, NetworkPolicy와 single-node profile +- `systems/auth-system`: `auth-system-dev` namespace, internal/public host, + Vault injection path와 system NetworkPolicy +- `workloads/*`: dev namespace, image reference, ingress, pull + SealedSecret과 workload NetworkPolicy + +현재 Vault NetworkPolicy의 Kubernetes API CIDR와 node address는 +`dev-k3s`에 종속됩니다. 다른 클러스터에 그대로 복사하지 말고 해당 +클러스터의 service/node network를 확인해야 합니다. + +두 번째 클러스터를 추가할 때는 다음 순서를 사용합니다. + +1. 실제 차이가 있는 overlay만 `clusters//overlays`에 추가합니다. +2. Argo CD cluster credential을 Git 밖에서 등록합니다. +3. 각 AppProject destination에 정확한 API server/namespace를 추가합니다. +4. 권한별 ApplicationSet inventory에 `autoSync: "false"` element를 + 추가합니다. +5. Render, live diff와 외부 prerequisite를 검증한 단계별 PR로 gate를 + 엽니다. + +Production은 이 profile의 이름 변경이나 복사로 만들지 않습니다. TLS, +availability, secret authority, storage/backup, approval과 recovery +contract를 먼저 별도 설계해야 합니다. diff --git a/clusters/dev-k3s/applications/foundation/kustomization.yaml b/clusters/dev-k3s/applications/foundation/kustomization.yaml deleted file mode 100644 index c3adfb7..0000000 --- a/clusters/dev-k3s/applications/foundation/kustomization.yaml +++ /dev/null @@ -1,7 +0,0 @@ -apiVersion: kustomize.config.k8s.io/v1beta1 -kind: Kustomization - -resources: - - sealed-secrets.yaml - - vault.yaml - - vault-agent-injector.yaml diff --git a/clusters/dev-k3s/applications/foundation/sealed-secrets.yaml b/clusters/dev-k3s/applications/foundation/sealed-secrets.yaml deleted file mode 100644 index d20ad30..0000000 --- a/clusters/dev-k3s/applications/foundation/sealed-secrets.yaml +++ /dev/null @@ -1,49 +0,0 @@ -apiVersion: argoproj.io/v1alpha1 -kind: Application -metadata: - name: sealed-secrets-dev - namespace: argocd - annotations: - argocd.argoproj.io/sync-wave: "0" - argocd.argoproj.io/sync-options: Prune=confirm,Delete=confirm - finalizers: - - resources-finalizer.argocd.argoproj.io -spec: - project: cluster-addons-dev - source: - repoURL: https://bitnami.github.io/sealed-secrets - chart: sealed-secrets - targetRevision: 2.17.9 - helm: - values: | - fullnameOverride: sealed-secrets-controller - keyrenewperiod: 720h - image: - repository: bitnami/sealed-secrets-controller - tag: "0.33.1@sha256:e7fad65c2d2f47e48d9ca17408ed56961bfa6a6dd74ccd4a1a214664156534bc" - resources: - requests: - cpu: 50m - memory: 64Mi - limits: - cpu: 250m - memory: 256Mi - destination: - server: https://kubernetes.default.svc - namespace: kube-system - syncPolicy: - automated: - enabled: true - prune: true - selfHeal: true - syncOptions: - - PruneLast=true - - FailOnSharedResource=true - retry: - limit: 5 - refresh: true - backoff: - duration: 5s - factor: 2 - maxDuration: 3m - revisionHistoryLimit: 5 diff --git a/clusters/dev-k3s/applications/foundation/vault-agent-injector.yaml b/clusters/dev-k3s/applications/foundation/vault-agent-injector.yaml deleted file mode 100644 index adfa1b0..0000000 --- a/clusters/dev-k3s/applications/foundation/vault-agent-injector.yaml +++ /dev/null @@ -1,63 +0,0 @@ -apiVersion: argoproj.io/v1alpha1 -kind: Application -metadata: - name: vault-agent-injector-dev - namespace: argocd - annotations: - argocd.argoproj.io/sync-wave: "2" - argocd.argoproj.io/sync-options: Prune=confirm,Delete=confirm - finalizers: - - resources-finalizer.argocd.argoproj.io -spec: - project: cluster-addons-dev - source: - repoURL: https://helm.releases.hashicorp.com - chart: vault - targetRevision: 0.32.0 - helm: - values: | - global: - externalVaultAddr: http://vault.vault.svc.cluster.local:8200 - tlsDisable: true - server: - enabled: false - injector: - enabled: true - authPath: auth/kubernetes - image: - repository: hashicorp/vault-k8s - tag: "1.7.2@sha256:ae3d307658b72a1cf35dab9bdf92c995d45cdc7183af0516857714b5bd0ba84d" - webhook: - failurePolicy: Fail - namespaceSelector: - matchLabels: - vault-injection: enabled - resources: - requests: - cpu: 100m - memory: 128Mi - limits: - cpu: 250m - memory: 256Mi - agentImage: - repository: hashicorp/vault - tag: "1.18.5@sha256:750bb37c1638fa194ab37053a81618c61bb0491ddec6fccac87c07a8e6cd8166" - destination: - server: https://kubernetes.default.svc - namespace: vault - syncPolicy: - automated: - enabled: true - prune: true - selfHeal: true - syncOptions: - - PruneLast=true - - FailOnSharedResource=true - retry: - limit: 5 - refresh: true - backoff: - duration: 5s - factor: 2 - maxDuration: 3m - revisionHistoryLimit: 5 diff --git a/clusters/dev-k3s/applications/foundation/vault.yaml b/clusters/dev-k3s/applications/foundation/vault.yaml deleted file mode 100644 index a5a4c96..0000000 --- a/clusters/dev-k3s/applications/foundation/vault.yaml +++ /dev/null @@ -1,34 +0,0 @@ -apiVersion: argoproj.io/v1alpha1 -kind: Application -metadata: - name: vault-dev - namespace: argocd - annotations: - argocd.argoproj.io/sync-wave: "1" - argocd.argoproj.io/sync-options: Prune=confirm,Delete=confirm -spec: - project: platform-dev - source: - repoURL: https://git.learn.hyeonworks.com/donghyeon.kang/project-gitops - targetRevision: main - path: clusters/dev-k3s/manifests/vault - destination: - server: https://kubernetes.default.svc - namespace: vault - syncPolicy: - automated: - enabled: true - prune: true - selfHeal: true - syncOptions: - - CreateNamespace=true - - PruneLast=true - - FailOnSharedResource=true - retry: - limit: 5 - refresh: true - backoff: - duration: 5s - factor: 2 - maxDuration: 3m - revisionHistoryLimit: 5 diff --git a/clusters/dev-k3s/applications/platform/auth-system.yaml b/clusters/dev-k3s/applications/platform/auth-system.yaml deleted file mode 100644 index 821aef0..0000000 --- a/clusters/dev-k3s/applications/platform/auth-system.yaml +++ /dev/null @@ -1,34 +0,0 @@ -apiVersion: argoproj.io/v1alpha1 -kind: Application -metadata: - name: platform-dev - namespace: argocd - annotations: - argocd.argoproj.io/sync-wave: "10" - argocd.argoproj.io/sync-options: Prune=confirm,Delete=confirm -spec: - project: platform-dev - source: - repoURL: https://git.learn.hyeonworks.com/donghyeon.kang/project-gitops - targetRevision: main - path: clusters/dev-k3s/manifests/auth-system - destination: - server: https://kubernetes.default.svc - namespace: platform - syncPolicy: - automated: - enabled: true - prune: true - selfHeal: true - syncOptions: - - CreateNamespace=true - - PruneLast=true - - FailOnSharedResource=true - retry: - limit: 5 - refresh: true - backoff: - duration: 5s - factor: 2 - maxDuration: 3m - revisionHistoryLimit: 5 diff --git a/clusters/dev-k3s/applications/platform/kustomization.yaml b/clusters/dev-k3s/applications/platform/kustomization.yaml deleted file mode 100644 index 4098a31..0000000 --- a/clusters/dev-k3s/applications/platform/kustomization.yaml +++ /dev/null @@ -1,5 +0,0 @@ -apiVersion: kustomize.config.k8s.io/v1beta1 -kind: Kustomization - -resources: - - auth-system.yaml diff --git a/clusters/dev-k3s/applications/workloads/api-server.yaml b/clusters/dev-k3s/applications/workloads/api-server.yaml deleted file mode 100644 index d17281b..0000000 --- a/clusters/dev-k3s/applications/workloads/api-server.yaml +++ /dev/null @@ -1,36 +0,0 @@ -apiVersion: argoproj.io/v1alpha1 -kind: Application -metadata: - name: api-server-dev - namespace: argocd - annotations: - argocd.argoproj.io/sync-wave: "20" - argocd.argoproj.io/sync-options: Prune=confirm,Delete=confirm - finalizers: - - resources-finalizer.argocd.argoproj.io -spec: - project: apps-dev - source: - repoURL: https://git.learn.hyeonworks.com/donghyeon.kang/project-gitops - targetRevision: main - path: clusters/dev-k3s/manifests/api-server - destination: - server: https://kubernetes.default.svc - namespace: api-dev - syncPolicy: - automated: - enabled: true - prune: true - selfHeal: true - syncOptions: - - CreateNamespace=true - - PruneLast=true - - FailOnSharedResource=true - retry: - limit: 5 - refresh: true - backoff: - duration: 5s - factor: 2 - maxDuration: 3m - revisionHistoryLimit: 5 diff --git a/clusters/dev-k3s/applications/workloads/auth-server.yaml b/clusters/dev-k3s/applications/workloads/auth-server.yaml deleted file mode 100644 index 482b6ef..0000000 --- a/clusters/dev-k3s/applications/workloads/auth-server.yaml +++ /dev/null @@ -1,36 +0,0 @@ -apiVersion: argoproj.io/v1alpha1 -kind: Application -metadata: - name: auth-server-dev - namespace: argocd - annotations: - argocd.argoproj.io/sync-wave: "20" - argocd.argoproj.io/sync-options: Prune=confirm,Delete=confirm - finalizers: - - resources-finalizer.argocd.argoproj.io -spec: - project: apps-dev - source: - repoURL: https://git.learn.hyeonworks.com/donghyeon.kang/project-gitops - targetRevision: main - path: clusters/dev-k3s/manifests/auth-server - destination: - server: https://kubernetes.default.svc - namespace: auth-dev - syncPolicy: - automated: - enabled: true - prune: true - selfHeal: true - syncOptions: - - CreateNamespace=true - - PruneLast=true - - FailOnSharedResource=true - retry: - limit: 5 - refresh: true - backoff: - duration: 5s - factor: 2 - maxDuration: 3m - revisionHistoryLimit: 5 diff --git a/clusters/dev-k3s/applications/workloads/kustomization.yaml b/clusters/dev-k3s/applications/workloads/kustomization.yaml deleted file mode 100644 index e3acd63..0000000 --- a/clusters/dev-k3s/applications/workloads/kustomization.yaml +++ /dev/null @@ -1,6 +0,0 @@ -apiVersion: kustomize.config.k8s.io/v1beta1 -kind: Kustomization - -resources: - - auth-server.yaml - - api-server.yaml diff --git a/clusters/dev-k3s/manifests/vault/kustomization.yaml b/clusters/dev-k3s/overlays/platform/vault/kustomization.yaml similarity index 71% rename from clusters/dev-k3s/manifests/vault/kustomization.yaml rename to clusters/dev-k3s/overlays/platform/vault/kustomization.yaml index c2c4d8d..bcf0595 100644 --- a/clusters/dev-k3s/manifests/vault/kustomization.yaml +++ b/clusters/dev-k3s/overlays/platform/vault/kustomization.yaml @@ -4,6 +4,6 @@ kind: Kustomization namespace: vault resources: - - ../../../../platform/security/vault/base + - ../../../../../platform/shared-services/vault/base - namespace.yaml - networkpolicy.yaml diff --git a/clusters/dev-k3s/manifests/vault/namespace.yaml b/clusters/dev-k3s/overlays/platform/vault/namespace.yaml similarity index 85% rename from clusters/dev-k3s/manifests/vault/namespace.yaml rename to clusters/dev-k3s/overlays/platform/vault/namespace.yaml index e5d9708..76916fe 100644 --- a/clusters/dev-k3s/manifests/vault/namespace.yaml +++ b/clusters/dev-k3s/overlays/platform/vault/namespace.yaml @@ -3,7 +3,7 @@ kind: Namespace metadata: name: vault annotations: - argocd.argoproj.io/sync-options: Prune=false + argocd.argoproj.io/sync-options: Prune=confirm,Delete=confirm labels: pod-security.kubernetes.io/enforce: baseline pod-security.kubernetes.io/enforce-version: latest diff --git a/clusters/dev-k3s/manifests/vault/networkpolicy.yaml b/clusters/dev-k3s/overlays/platform/vault/networkpolicy.yaml similarity index 95% rename from clusters/dev-k3s/manifests/vault/networkpolicy.yaml rename to clusters/dev-k3s/overlays/platform/vault/networkpolicy.yaml index 24d7ed1..8eb3fdc 100644 --- a/clusters/dev-k3s/manifests/vault/networkpolicy.yaml +++ b/clusters/dev-k3s/overlays/platform/vault/networkpolicy.yaml @@ -48,7 +48,7 @@ spec: - to: - namespaceSelector: matchLabels: - kubernetes.io/metadata.name: platform + kubernetes.io/metadata.name: auth-system-dev podSelector: matchLabels: app: postgres @@ -97,7 +97,7 @@ spec: kubernetes.io/metadata.name: auth-dev - namespaceSelector: matchLabels: - kubernetes.io/metadata.name: platform + kubernetes.io/metadata.name: auth-system-dev ports: - protocol: TCP port: 8200 diff --git a/clusters/dev-k3s/manifests/auth-system/config.env b/clusters/dev-k3s/overlays/systems/auth-system/config.env similarity index 100% rename from clusters/dev-k3s/manifests/auth-system/config.env rename to clusters/dev-k3s/overlays/systems/auth-system/config.env diff --git a/clusters/dev-k3s/manifests/auth-system/keycloak-client-sync.vault-patch.yaml b/clusters/dev-k3s/overlays/systems/auth-system/keycloak-client-sync.vault-patch.yaml similarity index 88% rename from clusters/dev-k3s/manifests/auth-system/keycloak-client-sync.vault-patch.yaml rename to clusters/dev-k3s/overlays/systems/auth-system/keycloak-client-sync.vault-patch.yaml index b91caa6..193a9e6 100644 --- a/clusters/dev-k3s/manifests/auth-system/keycloak-client-sync.vault-patch.yaml +++ b/clusters/dev-k3s/overlays/systems/auth-system/keycloak-client-sync.vault-patch.yaml @@ -9,13 +9,13 @@ spec: vault.hashicorp.com/agent-inject: "true" vault.hashicorp.com/agent-service-account-token-volume-name: vault-token vault.hashicorp.com/agent-inject-perms-keycloak-sync-env: "0644" - vault.hashicorp.com/agent-inject-secret-keycloak-sync-env: kv/data/dev/platform/keycloak/bootstrap-admin + vault.hashicorp.com/agent-inject-secret-keycloak-sync-env: kv/data/dev/systems/auth-system/keycloak/bootstrap-admin vault.hashicorp.com/agent-inject-template-keycloak-sync-env: | - {{ with secret "kv/data/dev/platform/keycloak/bootstrap-admin" }} + {{ with secret "kv/data/dev/systems/auth-system/keycloak/bootstrap-admin" }} export KC_BOOTSTRAP_ADMIN_PASSWORD={{ printf "%q" .Data.data.KC_BOOTSTRAP_ADMIN_PASSWORD }} {{ end }} - {{ with secret "kv/data/dev/platform/keycloak/client-auth-server" }} + {{ with secret "kv/data/dev/workloads/auth-server/keycloak-client" }} export KEYCLOAK_CLIENT_SECRET={{ printf "%q" .Data.data.KEYCLOAK_CLIENT_SECRET }} {{ end }} vault.hashicorp.com/agent-pre-populate-only: "true" @@ -44,7 +44,7 @@ spec: ready=false for _ in $(seq 1 60); do if /opt/keycloak/bin/kcadm.sh config credentials \ - --server http://keycloak.platform.svc.cluster.local \ + --server http://keycloak \ --realm master \ --user "$KC_BOOTSTRAP_ADMIN_USERNAME" \ --password "$KC_BOOTSTRAP_ADMIN_PASSWORD" >/dev/null 2>&1; then @@ -72,15 +72,15 @@ spec: - name: KC_BOOTSTRAP_ADMIN_USERNAME valueFrom: configMapKeyRef: - name: platform-config + name: auth-system-config key: KEYCLOAK_BOOTSTRAP_ADMIN_USERNAME - name: KEYCLOAK_CLIENT_ID valueFrom: configMapKeyRef: - name: platform-config + name: auth-system-config key: KEYCLOAK_CLIENT_ID - name: AUTH_SERVER_BASE_URL valueFrom: configMapKeyRef: - name: platform-config + name: auth-system-config key: AUTH_SERVER_BASE_URL diff --git a/clusters/dev-k3s/manifests/auth-system/keycloak-ingress.yaml b/clusters/dev-k3s/overlays/systems/auth-system/keycloak-ingress.yaml similarity index 86% rename from clusters/dev-k3s/manifests/auth-system/keycloak-ingress.yaml rename to clusters/dev-k3s/overlays/systems/auth-system/keycloak-ingress.yaml index a53dcd6..5914dba 100644 --- a/clusters/dev-k3s/manifests/auth-system/keycloak-ingress.yaml +++ b/clusters/dev-k3s/overlays/systems/auth-system/keycloak-ingress.yaml @@ -7,7 +7,7 @@ metadata: spec: ingressClassName: traefik rules: - - host: keycloak-public.platform.svc.cluster.local + - host: keycloak-public.auth-system-dev.svc.cluster.local http: paths: - path: / diff --git a/clusters/dev-k3s/manifests/auth-system/keycloak.public-url-patch.yaml b/clusters/dev-k3s/overlays/systems/auth-system/keycloak.public-url-patch.yaml similarity index 81% rename from clusters/dev-k3s/manifests/auth-system/keycloak.public-url-patch.yaml rename to clusters/dev-k3s/overlays/systems/auth-system/keycloak.public-url-patch.yaml index 128254d..2ac38b1 100644 --- a/clusters/dev-k3s/manifests/auth-system/keycloak.public-url-patch.yaml +++ b/clusters/dev-k3s/overlays/systems/auth-system/keycloak.public-url-patch.yaml @@ -9,7 +9,7 @@ spec: - name: keycloak env: - name: KC_HOSTNAME - value: keycloak-public.platform.svc.cluster.local + value: keycloak-public.auth-system-dev.svc.cluster.local - name: KC_HOSTNAME_STRICT value: "false" - name: KC_PROXY_HEADERS diff --git a/clusters/dev-k3s/manifests/auth-system/keycloak.vault-patch.yaml b/clusters/dev-k3s/overlays/systems/auth-system/keycloak.vault-patch.yaml similarity index 83% rename from clusters/dev-k3s/manifests/auth-system/keycloak.vault-patch.yaml rename to clusters/dev-k3s/overlays/systems/auth-system/keycloak.vault-patch.yaml index ff8b531..2dae292 100644 --- a/clusters/dev-k3s/manifests/auth-system/keycloak.vault-patch.yaml +++ b/clusters/dev-k3s/overlays/systems/auth-system/keycloak.vault-patch.yaml @@ -9,13 +9,13 @@ spec: vault.hashicorp.com/agent-inject: "true" vault.hashicorp.com/agent-service-account-token-volume-name: vault-token vault.hashicorp.com/agent-inject-perms-keycloak-env: "0644" - vault.hashicorp.com/agent-inject-secret-keycloak-env: kv/data/dev/platform/postgres/keycloak + vault.hashicorp.com/agent-inject-secret-keycloak-env: kv/data/dev/systems/auth-system/postgres/keycloak vault.hashicorp.com/agent-inject-template-keycloak-env: | - {{ with secret "kv/data/dev/platform/postgres/keycloak" }} + {{ with secret "kv/data/dev/systems/auth-system/postgres/keycloak" }} export KC_DB_PASSWORD={{ printf "%q" .Data.data.KEYCLOAK_DB_PASSWORD }} {{ end }} - {{ with secret "kv/data/dev/platform/keycloak/bootstrap-admin" }} + {{ with secret "kv/data/dev/systems/auth-system/keycloak/bootstrap-admin" }} export KC_BOOTSTRAP_ADMIN_PASSWORD={{ printf "%q" .Data.data.KC_BOOTSTRAP_ADMIN_PASSWORD }} {{ end }} vault.hashicorp.com/role: keycloak-dev @@ -44,16 +44,16 @@ spec: - name: KC_DB value: postgres - name: KC_DB_URL - value: jdbc:postgresql://postgres.platform.svc.cluster.local:5432/keycloak + value: jdbc:postgresql://postgres:5432/keycloak - name: KC_DB_USERNAME valueFrom: configMapKeyRef: - name: platform-config + name: auth-system-config key: KEYCLOAK_DB_USER - name: KC_HEALTH_ENABLED value: "true" - name: KC_BOOTSTRAP_ADMIN_USERNAME valueFrom: configMapKeyRef: - name: platform-config + name: auth-system-config key: KEYCLOAK_BOOTSTRAP_ADMIN_USERNAME diff --git a/clusters/dev-k3s/manifests/auth-system/kustomization.yaml b/clusters/dev-k3s/overlays/systems/auth-system/kustomization.yaml similarity index 82% rename from clusters/dev-k3s/manifests/auth-system/kustomization.yaml rename to clusters/dev-k3s/overlays/systems/auth-system/kustomization.yaml index c04234d..72dec2d 100644 --- a/clusters/dev-k3s/manifests/auth-system/kustomization.yaml +++ b/clusters/dev-k3s/overlays/systems/auth-system/kustomization.yaml @@ -1,10 +1,10 @@ apiVersion: kustomize.config.k8s.io/v1beta1 kind: Kustomization -namespace: platform +namespace: auth-system-dev resources: - - ../../../../platform/auth-system/base + - ../../../../../systems/auth-system/base - namespace.yaml - keycloak-ingress.yaml - public-access.yaml @@ -15,7 +15,7 @@ generatorOptions: argocd.argoproj.io/sync-wave: "0" configMapGenerator: - - name: platform-config + - name: auth-system-config envs: - config.env diff --git a/clusters/dev-k3s/manifests/auth-system/namespace.yaml b/clusters/dev-k3s/overlays/systems/auth-system/namespace.yaml similarity index 81% rename from clusters/dev-k3s/manifests/auth-system/namespace.yaml rename to clusters/dev-k3s/overlays/systems/auth-system/namespace.yaml index 9ecfb54..137abb9 100644 --- a/clusters/dev-k3s/manifests/auth-system/namespace.yaml +++ b/clusters/dev-k3s/overlays/systems/auth-system/namespace.yaml @@ -1,9 +1,9 @@ apiVersion: v1 kind: Namespace metadata: - name: platform + name: auth-system-dev annotations: - argocd.argoproj.io/sync-options: Prune=false + argocd.argoproj.io/sync-options: Prune=confirm,Delete=confirm labels: vault-injection: enabled pod-security.kubernetes.io/enforce: baseline diff --git a/clusters/dev-k3s/manifests/auth-system/networkpolicy.yaml b/clusters/dev-k3s/overlays/systems/auth-system/networkpolicy.yaml similarity index 90% rename from clusters/dev-k3s/manifests/auth-system/networkpolicy.yaml rename to clusters/dev-k3s/overlays/systems/auth-system/networkpolicy.yaml index 6c88f78..4148619 100644 --- a/clusters/dev-k3s/manifests/auth-system/networkpolicy.yaml +++ b/clusters/dev-k3s/overlays/systems/auth-system/networkpolicy.yaml @@ -1,7 +1,7 @@ apiVersion: networking.k8s.io/v1 kind: NetworkPolicy metadata: - name: platform-default-deny + name: auth-system-default-deny spec: podSelector: {} policyTypes: @@ -11,7 +11,7 @@ spec: apiVersion: networking.k8s.io/v1 kind: NetworkPolicy metadata: - name: platform-allow-dns-egress + name: auth-system-allow-dns-egress spec: podSelector: {} policyTypes: @@ -33,7 +33,7 @@ spec: apiVersion: networking.k8s.io/v1 kind: NetworkPolicy metadata: - name: platform-allow-vault-egress + name: auth-system-allow-vault-egress spec: podSelector: {} policyTypes: @@ -53,7 +53,7 @@ spec: apiVersion: networking.k8s.io/v1 kind: NetworkPolicy metadata: - name: platform-allow-keycloak-egress-to-postgres + name: auth-system-allow-keycloak-egress-to-postgres spec: podSelector: matchLabels: @@ -72,7 +72,7 @@ spec: apiVersion: networking.k8s.io/v1 kind: NetworkPolicy metadata: - name: platform-allow-keycloak-client-sync-to-keycloak + name: auth-system-allow-keycloak-client-sync-to-keycloak spec: podSelector: matchLabels: @@ -91,7 +91,7 @@ spec: apiVersion: networking.k8s.io/v1 kind: NetworkPolicy metadata: - name: platform-allow-keycloak-ingress + name: auth-system-allow-keycloak-ingress spec: podSelector: matchLabels: @@ -120,7 +120,7 @@ spec: apiVersion: networking.k8s.io/v1 kind: NetworkPolicy metadata: - name: platform-allow-postgres-ingress + name: auth-system-allow-postgres-ingress spec: podSelector: matchLabels: diff --git a/clusters/dev-k3s/manifests/auth-system/postgres.vault-patch.yaml b/clusters/dev-k3s/overlays/systems/auth-system/postgres.vault-patch.yaml similarity index 80% rename from clusters/dev-k3s/manifests/auth-system/postgres.vault-patch.yaml rename to clusters/dev-k3s/overlays/systems/auth-system/postgres.vault-patch.yaml index bb56bba..2fa0d74 100644 --- a/clusters/dev-k3s/manifests/auth-system/postgres.vault-patch.yaml +++ b/clusters/dev-k3s/overlays/systems/auth-system/postgres.vault-patch.yaml @@ -9,17 +9,17 @@ spec: vault.hashicorp.com/agent-inject: "true" vault.hashicorp.com/agent-service-account-token-volume-name: vault-token vault.hashicorp.com/agent-inject-perms-postgres-env: "0644" - vault.hashicorp.com/agent-inject-secret-postgres-env: kv/data/dev/platform/postgres/superuser + vault.hashicorp.com/agent-inject-secret-postgres-env: kv/data/dev/systems/auth-system/postgres/superuser vault.hashicorp.com/agent-inject-template-postgres-env: | - {{ with secret "kv/data/dev/platform/postgres/superuser" }} + {{ with secret "kv/data/dev/systems/auth-system/postgres/superuser" }} export POSTGRES_PASSWORD={{ printf "%q" .Data.data.POSTGRES_SUPERUSER_PASSWORD }} {{ end }} - {{ with secret "kv/data/dev/platform/postgres/auth-server" }} + {{ with secret "kv/data/dev/systems/auth-system/postgres/auth-server" }} export AUTH_DB_PASSWORD={{ printf "%q" .Data.data.AUTH_DB_PASSWORD }} {{ end }} - {{ with secret "kv/data/dev/platform/postgres/keycloak" }} + {{ with secret "kv/data/dev/systems/auth-system/postgres/keycloak" }} export KEYCLOAK_DB_PASSWORD={{ printf "%q" .Data.data.KEYCLOAK_DB_PASSWORD }} {{ end }} vault.hashicorp.com/role: postgres-dev @@ -50,30 +50,30 @@ spec: - name: POSTGRES_USER valueFrom: configMapKeyRef: - name: platform-config + name: auth-system-config key: POSTGRES_SUPERUSER - name: POSTGRES_DB valueFrom: configMapKeyRef: - name: platform-config + name: auth-system-config key: POSTGRES_DEFAULT_DB - name: AUTH_DB_NAME valueFrom: configMapKeyRef: - name: platform-config + name: auth-system-config key: AUTH_DB_NAME - name: AUTH_DB_USER valueFrom: configMapKeyRef: - name: platform-config + name: auth-system-config key: AUTH_DB_USER - name: KEYCLOAK_DB_NAME valueFrom: configMapKeyRef: - name: platform-config + name: auth-system-config key: KEYCLOAK_DB_NAME - name: KEYCLOAK_DB_USER valueFrom: configMapKeyRef: - name: platform-config + name: auth-system-config key: KEYCLOAK_DB_USER diff --git a/clusters/dev-k3s/manifests/auth-system/public-access.yaml b/clusters/dev-k3s/overlays/systems/auth-system/public-access.yaml similarity index 100% rename from clusters/dev-k3s/manifests/auth-system/public-access.yaml rename to clusters/dev-k3s/overlays/systems/auth-system/public-access.yaml diff --git a/clusters/dev-k3s/manifests/api-server/config.env b/clusters/dev-k3s/overlays/workloads/api-server/config.env similarity index 100% rename from clusters/dev-k3s/manifests/api-server/config.env rename to clusters/dev-k3s/overlays/workloads/api-server/config.env diff --git a/clusters/dev-k3s/manifests/api-server/ghcr-regcred.sealedsecret.yaml b/clusters/dev-k3s/overlays/workloads/api-server/ghcr-regcred.sealedsecret.yaml similarity index 100% rename from clusters/dev-k3s/manifests/api-server/ghcr-regcred.sealedsecret.yaml rename to clusters/dev-k3s/overlays/workloads/api-server/ghcr-regcred.sealedsecret.yaml diff --git a/clusters/dev-k3s/manifests/api-server/ingress.yaml b/clusters/dev-k3s/overlays/workloads/api-server/ingress.yaml similarity index 100% rename from clusters/dev-k3s/manifests/api-server/ingress.yaml rename to clusters/dev-k3s/overlays/workloads/api-server/ingress.yaml diff --git a/clusters/dev-k3s/manifests/api-server/kustomization.yaml b/clusters/dev-k3s/overlays/workloads/api-server/kustomization.yaml similarity index 91% rename from clusters/dev-k3s/manifests/api-server/kustomization.yaml rename to clusters/dev-k3s/overlays/workloads/api-server/kustomization.yaml index 132dc98..70728e3 100644 --- a/clusters/dev-k3s/manifests/api-server/kustomization.yaml +++ b/clusters/dev-k3s/overlays/workloads/api-server/kustomization.yaml @@ -4,7 +4,7 @@ kind: Kustomization namespace: api-dev resources: - - ../../../../workloads/api-server/base + - ../../../../../workloads/api-server/base - namespace.yaml - ingress.yaml - public-access.yaml diff --git a/clusters/dev-k3s/manifests/api-server/namespace.yaml b/clusters/dev-k3s/overlays/workloads/api-server/namespace.yaml similarity index 82% rename from clusters/dev-k3s/manifests/api-server/namespace.yaml rename to clusters/dev-k3s/overlays/workloads/api-server/namespace.yaml index 208d429..06ec28f 100644 --- a/clusters/dev-k3s/manifests/api-server/namespace.yaml +++ b/clusters/dev-k3s/overlays/workloads/api-server/namespace.yaml @@ -2,6 +2,8 @@ apiVersion: v1 kind: Namespace metadata: name: api-dev + annotations: + argocd.argoproj.io/sync-options: Prune=confirm,Delete=confirm labels: pod-security.kubernetes.io/enforce: baseline pod-security.kubernetes.io/enforce-version: latest diff --git a/clusters/dev-k3s/manifests/api-server/networkpolicy.yaml b/clusters/dev-k3s/overlays/workloads/api-server/networkpolicy.yaml similarity index 100% rename from clusters/dev-k3s/manifests/api-server/networkpolicy.yaml rename to clusters/dev-k3s/overlays/workloads/api-server/networkpolicy.yaml diff --git a/clusters/dev-k3s/manifests/api-server/public-access.yaml b/clusters/dev-k3s/overlays/workloads/api-server/public-access.yaml similarity index 100% rename from clusters/dev-k3s/manifests/api-server/public-access.yaml rename to clusters/dev-k3s/overlays/workloads/api-server/public-access.yaml diff --git a/clusters/dev-k3s/manifests/auth-server/config.env b/clusters/dev-k3s/overlays/workloads/auth-server/config.env similarity index 80% rename from clusters/dev-k3s/manifests/auth-server/config.env rename to clusters/dev-k3s/overlays/workloads/auth-server/config.env index b3ec253..106c48a 100644 --- a/clusters/dev-k3s/manifests/auth-server/config.env +++ b/clusters/dev-k3s/overlays/workloads/auth-server/config.env @@ -3,9 +3,9 @@ SERVER_FORWARD_HEADERS_STRATEGY=framework APP_DOCS_TITLE=Project Auth Server API APP_DOCS_DESCRIPTION=dev auth-server OpenAPI APP_DOCS_VERSION=v1 -APP_DATASOURCE_URL=jdbc:postgresql://postgres.platform.svc.cluster.local:5432/project_auth +APP_DATASOURCE_URL=jdbc:postgresql://postgres.auth-system-dev.svc.cluster.local:5432/project_auth APP_PERSISTENCE_MIGRATION_RUN_ON_STARTUP=false -APP_SECURITY_OAUTH2_KEYCLOAK_ISSUER_URI=http://keycloak-public.platform.svc.cluster.local/realms/project-auth +APP_SECURITY_OAUTH2_KEYCLOAK_ISSUER_URI=http://keycloak-public.auth-system-dev.svc.cluster.local/realms/project-auth APP_SECURITY_OAUTH2_KEYCLOAK_CLIENT_ID=project-auth-server APP_SECURITY_OAUTH2_GOOGLE_REGISTRATION_ID=keycloak-google APP_SECURITY_OAUTH2_GOOGLE_IDP_HINT=google diff --git a/clusters/dev-k3s/manifests/auth-server/db-migration-job.vault-patch.yaml b/clusters/dev-k3s/overlays/workloads/auth-server/db-migration-job.vault-patch.yaml similarity index 100% rename from clusters/dev-k3s/manifests/auth-server/db-migration-job.vault-patch.yaml rename to clusters/dev-k3s/overlays/workloads/auth-server/db-migration-job.vault-patch.yaml diff --git a/clusters/dev-k3s/manifests/auth-server/deployment.vault-patch.yaml b/clusters/dev-k3s/overlays/workloads/auth-server/deployment.vault-patch.yaml similarity index 88% rename from clusters/dev-k3s/manifests/auth-server/deployment.vault-patch.yaml rename to clusters/dev-k3s/overlays/workloads/auth-server/deployment.vault-patch.yaml index 6457579..932a00e 100644 --- a/clusters/dev-k3s/manifests/auth-server/deployment.vault-patch.yaml +++ b/clusters/dev-k3s/overlays/workloads/auth-server/deployment.vault-patch.yaml @@ -9,14 +9,14 @@ spec: vault.hashicorp.com/agent-cache-enable: "true" vault.hashicorp.com/agent-inject: "true" vault.hashicorp.com/agent-service-account-token-volume-name: vault-token - vault.hashicorp.com/agent-inject-secret-runtime-env: kv/data/dev/platform/postgres/auth-server + vault.hashicorp.com/agent-inject-secret-runtime-env: kv/data/dev/systems/auth-system/postgres/auth-server vault.hashicorp.com/agent-inject-template-runtime-env: | - {{ with secret "kv/data/dev/platform/postgres/auth-server" }} + {{ with secret "kv/data/dev/systems/auth-system/postgres/auth-server" }} export APP_DATASOURCE_USERNAME={{ printf "%q" .Data.data.APP_DATASOURCE_USERNAME }} export APP_DATASOURCE_PASSWORD={{ printf "%q" .Data.data.APP_DATASOURCE_PASSWORD }} {{ end }} - {{ with secret "kv/data/dev/platform/keycloak/client-auth-server" }} + {{ with secret "kv/data/dev/workloads/auth-server/keycloak-client" }} export APP_SECURITY_OAUTH2_KEYCLOAK_CLIENT_SECRET={{ printf "%q" .Data.data.APP_SECURITY_OAUTH2_KEYCLOAK_CLIENT_SECRET }} {{ end }} vault.hashicorp.com/agent-inject-token: "true" diff --git a/clusters/dev-k3s/manifests/auth-server/ghcr-regcred.sealedsecret.yaml b/clusters/dev-k3s/overlays/workloads/auth-server/ghcr-regcred.sealedsecret.yaml similarity index 100% rename from clusters/dev-k3s/manifests/auth-server/ghcr-regcred.sealedsecret.yaml rename to clusters/dev-k3s/overlays/workloads/auth-server/ghcr-regcred.sealedsecret.yaml diff --git a/clusters/dev-k3s/manifests/auth-server/ingress.yaml b/clusters/dev-k3s/overlays/workloads/auth-server/ingress.yaml similarity index 100% rename from clusters/dev-k3s/manifests/auth-server/ingress.yaml rename to clusters/dev-k3s/overlays/workloads/auth-server/ingress.yaml diff --git a/clusters/dev-k3s/manifests/auth-server/kustomization.yaml b/clusters/dev-k3s/overlays/workloads/auth-server/kustomization.yaml similarity index 92% rename from clusters/dev-k3s/manifests/auth-server/kustomization.yaml rename to clusters/dev-k3s/overlays/workloads/auth-server/kustomization.yaml index e342a8d..7dd7527 100644 --- a/clusters/dev-k3s/manifests/auth-server/kustomization.yaml +++ b/clusters/dev-k3s/overlays/workloads/auth-server/kustomization.yaml @@ -4,7 +4,7 @@ kind: Kustomization namespace: auth-dev resources: - - ../../../../workloads/auth-server/base + - ../../../../../workloads/auth-server/base - namespace.yaml - ingress.yaml - public-access.yaml diff --git a/clusters/dev-k3s/manifests/auth-server/namespace.yaml b/clusters/dev-k3s/overlays/workloads/auth-server/namespace.yaml similarity index 83% rename from clusters/dev-k3s/manifests/auth-server/namespace.yaml rename to clusters/dev-k3s/overlays/workloads/auth-server/namespace.yaml index 8a61529..fefcb64 100644 --- a/clusters/dev-k3s/manifests/auth-server/namespace.yaml +++ b/clusters/dev-k3s/overlays/workloads/auth-server/namespace.yaml @@ -2,6 +2,8 @@ apiVersion: v1 kind: Namespace metadata: name: auth-dev + annotations: + argocd.argoproj.io/sync-options: Prune=confirm,Delete=confirm labels: vault-injection: enabled pod-security.kubernetes.io/enforce: baseline diff --git a/clusters/dev-k3s/manifests/auth-server/networkpolicy.yaml b/clusters/dev-k3s/overlays/workloads/auth-server/networkpolicy.yaml similarity index 95% rename from clusters/dev-k3s/manifests/auth-server/networkpolicy.yaml rename to clusters/dev-k3s/overlays/workloads/auth-server/networkpolicy.yaml index 62f73c6..8219b14 100644 --- a/clusters/dev-k3s/manifests/auth-server/networkpolicy.yaml +++ b/clusters/dev-k3s/overlays/workloads/auth-server/networkpolicy.yaml @@ -33,7 +33,7 @@ spec: apiVersion: networking.k8s.io/v1 kind: NetworkPolicy metadata: - name: auth-dev-allow-platform-and-vault-egress + name: auth-dev-allow-auth-system-and-vault-egress spec: podSelector: {} policyTypes: @@ -42,7 +42,7 @@ spec: - to: - namespaceSelector: matchLabels: - kubernetes.io/metadata.name: platform + kubernetes.io/metadata.name: auth-system-dev podSelector: matchLabels: app: postgres diff --git a/clusters/dev-k3s/manifests/auth-server/public-access.yaml b/clusters/dev-k3s/overlays/workloads/auth-server/public-access.yaml similarity index 100% rename from clusters/dev-k3s/manifests/auth-server/public-access.yaml rename to clusters/dev-k3s/overlays/workloads/auth-server/public-access.yaml diff --git a/clusters/dev-k3s/projects/kustomization.yaml b/clusters/dev-k3s/projects/kustomization.yaml deleted file mode 100644 index d591a85..0000000 --- a/clusters/dev-k3s/projects/kustomization.yaml +++ /dev/null @@ -1,7 +0,0 @@ -apiVersion: kustomize.config.k8s.io/v1beta1 -kind: Kustomization - -resources: - - apps.yaml - - cluster-addons.yaml - - platform.yaml diff --git a/docs/adr/0002-terraform-ownership.md b/docs/adr/0002-terraform-ownership.md index c3186d1..f4e2efb 100644 --- a/docs/adr/0002-terraform-ownership.md +++ b/docs/adr/0002-terraform-ownership.md @@ -2,23 +2,54 @@ Status: accepted -Terraform은 VM/네트워크뿐 아니라 provider가 제공되는 Vault API 객체도 -관리할 수 있다. 현재 저장소의 Terraform 범위는 Vault API이고 실제 -machine provisioning은 provider가 확정될 때 별도 root로 추가한다. +Updated: 2026-07-26 -`dev-k3s`는 두 state만 사용한다. +Terraform의 현재 범위는 Vault API 객체입니다. Kubernetes 리소스는 Argo +CD가 소유하며, machine/network provisioning은 provider와 운영 경계가 +확정될 때 별도 root로 추가합니다. -- `vault-core`: mounts, auth backends, policies, Kubernetes/JWT roles, - application Transit key -- `vault-database`: PostgreSQL connection과 dynamic roles +`dev-k3s`는 정확히 세 state를 사용합니다. -resource/API path 하나는 한 state에만 속한다. state는 암호화, versioning, -access control, locking이 가능한 remote backend에 저장한다. +| State | 소유 객체 | +|---|---| +| `vault-foundation` | KV/database/Transit mounts, Kubernetes auth backend/config, delegated automation policy와 선택적 분리 CI JWT auth role | +| `vault-workloads` | workload ACL policy, Kubernetes auth role, `project-auth-jwt` Transit key | +| `vault-database` | `database/config/auth-system-postgres-dev` connection과 `auth-db-migration-dev` dynamic role | -`vault-core`는 privilege-escalation 가능한 객체를 포함하므로 제한된 -관리자 실행만 허용한다. `vault-database`는 core가 생성한 -`vault-database-automation-dev` 정책의 short-lived identity로 실행한다. +Resource 또는 Vault API path 하나는 한 state에만 속합니다. State 사이는 +이름 contract와 실행 순서만 공유하며 `terraform_remote_state`로 서로의 +snapshot을 읽지 않습니다. Backend는 encryption, versioning, access +control, locking을 제공해야 합니다. -Secret payload는 Terraform resource/data source로 관리하지 않는다. -필수 credential은 ephemeral variable과 provider write-only argument를 -통해서만 apply에 전달한다. +Privilege delegation의 경계는 다음과 같습니다. + +- `vault-foundation`은 bootstrap 또는 보안 관리자 승인 때만 실행합니다. + Routine CI identity를 두지 않습니다. +- `vault-foundation`이 workload/database 전용 automation policy와, + OIDC/JWT trust가 검증된 경우 서로 분리된 CI JWT login role을 생성합니다. + 두 role의 exact claim map은 최소 한 공통 discriminator key에서 서로 다른 + 값을 가져야 하므로 동일 scalar-claim JWT가 둘 다 선택할 수 없습니다. +- `vault-workloads`와 `vault-database`는 각각의 short-lived identity를 + 소비할 뿐 자신에게 권한을 부여하는 객체를 소유하지 않습니다. +- Delegated identity는 자신이 맡은 정확한 policy, auth role, database + path만 CRUD할 수 있습니다. +- Broad `platform-admin` 또는 상시 cluster-internal Vault administrator를 + routine automation에 연결하지 않습니다. + +실제 CI issuer가 repository, protected ref와 job discriminator claim을 +어떤 형식으로 발행하는지 먼저 검증합니다. 그 계약을 확인할 수 없으면 JWT +auth를 활성화하지 않고 bootstrap용 short-lived token만 사용합니다. +Foundation의 future change는 routine identity가 아니라 encrypted unseal +custody를 사용한 승인된 generated-root ceremony가 필요합니다. + +Secret payload는 Terraform resource/data source로 관리하지 않습니다. +Provider token과 PostgreSQL credential은 ephemeral variable과 write-only +argument를 통해 실행 시점에만 전달합니다. Vault init material, token, +password, plan과 state를 Git에 저장하지 않습니다. + +기존 `vault-core`에서 세 state로 바꾸는 작업은 선언 이동과 state ownership +이관을 분리해 수행합니다. Source에서는 `removed { destroy = false }`, +destination에서는 import를 사용하고, 양쪽 plan의 destroy가 0인지 확인하기 +전에는 apply하지 않습니다. Broad `platform-admin`/`vault-operator`와 +미사용 Keycloak/PostgreSQL operator policy/role은 새 state로 옮기지 +않으며 consumer가 없음을 확인한 별도 decommission에서 제거합니다. diff --git a/docs/adr/0004-argo-bootstrap-stages.md b/docs/adr/0004-argo-bootstrap-stages.md index 3a76796..a5abc5f 100644 --- a/docs/adr/0004-argo-bootstrap-stages.md +++ b/docs/adr/0004-argo-bootstrap-stages.md @@ -1,18 +1,43 @@ -# ADR 0004: Single Argo CD root +# ADR 0004: Single Argo CD root and stage-gated ApplicationSets Status: accepted -Argo CD 설치 후 `bootstrap/argocd/root-application.yaml` 하나만 seed한다. -root는 `clusters/dev-k3s`의 AppProject와 모든 child Application을 소유한다. +Updated: 2026-07-26 -반복 `kubectl apply`와 foundation/platform/application별 root wrapper는 -제거한다. routine deployment는 Git merge만으로 시작한다. +Argo CD 설치 후 bootstrap 전용 `gitops-control-plane` AppProject와 단일 +root Application을 순서대로 수동 seed합니다. Root는 +`platform/control-plane/argocd`의 AppProject와 ApplicationSet을 소유하고, +ApplicationSet이 platform addon/shared service, system, workload +Application을 생성합니다. Bootstrap Project는 canonical repository, +`argocd` namespace와 AppProject/ApplicationSet kind만 허용합니다. -Child Application의 sync wave는 객체 생성 순서를 가독성 있게 표현하지만 -서로 다른 Application의 readiness dependency로 간주하지 않는다. -Workload와 hook은 Vault/DB가 늦게 준비되는 상황을 retry할 수 있어야 한다. +반복 `kubectl apply`와 category별 root wrapper는 사용하지 않습니다. +Routine deployment는 Git merge만으로 시작합니다. -Root가 child Application을 prune하거나 삭제하려면 확인이 필요하다. -shared resource 소유권 충돌은 sync를 실패시킨다. 현재 규모에서는 명시적 -Application을 사용하고 두 번째 클러스터가 생길 때 ApplicationSet을 -검토한다. +각 ApplicationSet inventory 항목은 다음 계약을 명시합니다. + +- 고유 이름, AppProject, source, destination namespace +- component/cluster/path와 automated reconciliation 허용 여부인 quoted + string `autoSync` + +새 항목과 외부 준비 조건이 있는 항목은 `autoSync: "false"`로 시작합니다. +현재 bootstrap은 Sealed Secrets와 Vault만 열린 상태에서 시작해 +foundation/workloads state와 secret seed, injector, auth-system, database, +first-party workload 순서로 별도 PR gate를 엽니다. Template은 +`autoSync: "true"`인 항목에만 automated sync, prune, self-heal을 +생성합니다. +Gate가 닫힌 Application의 수동 sync도 change record와 명시적 operator +판단을 요구합니다. + +Sync wave는 AppProject(`-10`)를 ApplicationSet(`-5`)보다 먼저 생성합니다. +모든 ApplicationSet은 같은 wave이며 element별 stage field는 없습니다. +서로 다른 generated Application의 readiness는 gate와 runbook이 +제어합니다. Workload와 hook은 Vault/DB가 늦게 준비되는 상황을 retry할 +수 있고 idempotent해야 합니다. + +Root는 AppProject와 ApplicationSet만 prune 대상으로 봅니다. Generated +Application의 owner는 ApplicationSet이며 `create-update`에서는 element +제거만으로 삭제되지 않습니다. Application/resource 해체는 별도 +decommission runbook과 확인 승인을 사용합니다. Shared resource 소유권 +충돌은 sync를 실패시킵니다. Sync hook을 사용하는 Application에는 +`ApplyOutOfSyncOnly=true`를 사용하지 않습니다. diff --git a/docs/adr/0005-repository-layout.md b/docs/adr/0005-repository-layout.md index 3cc9a18..fc70414 100644 --- a/docs/adr/0005-repository-layout.md +++ b/docs/adr/0005-repository-layout.md @@ -2,16 +2,31 @@ Status: accepted -현재는 하나의 platform 팀, 하나의 dev cluster와 소수 workload를 가지므로 -GitOps configuration monorepo를 유지한다. application source repository와 -deployment configuration repository는 분리한다. +Updated: 2026-07-26 -- `platform/`, `workloads/`: 환경 중립 base -- `clusters//manifests`: cluster-specific final composition -- `clusters//applications`: Argo reconciliation inventory +현재는 단일 `dev-k3s`와 소수 workload를 다루므로 GitOps configuration +monorepo를 유지합니다. Application source repository와 deployment +configuration repository는 분리합니다. 이 저장소 자체는 독립 reference +lab이며 범용 platform product로 간주하지 않습니다. + +- `platform/`, `systems/`, `workloads/`: ownership별 base; 환경 중립은 + 목표 contract +- `clusters//overlays`: cluster-specific final composition +- `platform/control-plane/argocd/projects`: Argo 권한 경계 +- `platform/control-plane/argocd/application-sets`: reconciliation inventory - `iac/terraform`: Kubernetes manifest와 분리된 external API IaC - `bootstrap`: controller가 존재하기 전의 최소 seed -production 접근권한, 소유 팀, Terraform backend 또는 release cadence가 -실제로 갈라질 때 platform GitOps, workload GitOps, IaC repo 분리를 -재검토한다. 존재하지 않는 환경의 skeleton은 유지하지 않는다. +`clusters`가 배포 가능한 최종 상태를 소유합니다. Argo CD는 top-level +base를 직접 source로 사용하지 않습니다. `foundation`은 directory +taxonomy가 아니라 bootstrap ordering/stage이고, 구체적인 ownership 분류는 +ADR 0007을 따릅니다. + +현재 Keycloak base의 `start-dev`와 Vault base의 +TLS-off/single-node identity는 이 contract를 위반하는 알려진 리팩터링 +부채입니다. 다른 환경을 추가하기 전에 해당 값을 component overlay나 +configuration input으로 분리합니다. + +Production 접근권한, 소유 팀, Terraform backend 또는 release cadence가 +실제로 갈라질 때 platform GitOps, workload GitOps, IaC repository 분리를 +재검토합니다. 존재하지 않는 환경의 skeleton은 유지하지 않습니다. diff --git a/docs/adr/0007-repository-ownership-boundaries.md b/docs/adr/0007-repository-ownership-boundaries.md new file mode 100644 index 0000000..77e8c8f --- /dev/null +++ b/docs/adr/0007-repository-ownership-boundaries.md @@ -0,0 +1,65 @@ +# ADR 0007: Repository ownership boundaries + +Status: accepted + +Date: 2026-07-26 + +## Context + +기존 layout은 Vault, PostgreSQL, Keycloak과 Project Auth 구성을 모두 +`platform` 또는 `foundation`으로 표현했습니다. 이 이름은 설치 순서를 +보여 주지만 누가 소비하고 변경을 책임지는지 구분하지 못했습니다. +클러스터별 최종 구성도 `manifests`라는 일반 이름 아래 섞여 있어 base와 +overlay의 관계가 불명확했습니다. + +이 저장소는 하나의 실제 사내 플랫폼을 배포하는 저장소가 아니라 Project +Auth를 예제로 한 독립 GitOps reference lab입니다. 따라서 존재하지 않는 +팀/환경을 가정한 추상화보다 현재 리소스의 실제 owner와 lifecycle을 +명확히 해야 합니다. + +## Decision + +최상위 Kubernetes desired state를 다음 소유권으로 분류합니다. + +- `platform`: 여러 system이 사용할 수 있고 독립 lifecycle을 가진 cluster + capability +- `systems`: 특정 bounded context가 소유하는 backing services와 domain + configuration +- `workloads`: 별도 source repository와 release digest를 가진 first-party + 실행 애플리케이션 +- `clusters//overlays`: 위 base에 namespace, image, host, secret + reference, network boundary를 결합한 최종 구성 + +Vault는 `platform/shared-services/vault`에 둡니다. Sealed Secrets와 Vault +Agent Injector는 cluster addon inventory로 관리합니다. PostgreSQL, +Keycloak, realm/client sync는 Project Auth 전용이므로 +`systems/auth-system`으로 이동합니다. `auth-server`와 `api-server`는 +`workloads`에 유지합니다. + +Project Auth backing system의 namespace는 `auth-system-dev`로 정하고, +Vault KV 경로도 `systems/auth-system` 또는 실제 workload owner를 +반영하도록 바꿉니다. + +`foundation`은 ownership directory로 사용하지 않습니다. 준비 순서는 +분리된 ApplicationSet category, 명시적 `autoSync` gate, runbook과 workload +retry/idempotency로 표현합니다. + +## Consequences + +- 디렉터리 경로만 보고 owner와 blast radius를 추론할 수 있습니다. +- Base는 환경 중립 contract를 목표로 하고 Argo CD는 cluster overlay만 + source로 사용합니다. 현재 Keycloak/Vault base의 dev-only 값은 알려진 + 후속 리팩터링 대상입니다. +- auth-system 이동은 namespace, DNS, NetworkPolicy, Vault policy/path와 + Terraform role binding을 함께 바꾸는 migration입니다. 단순 파일 이동으로 + 취급하면 안 됩니다. +- ApplicationSet 도입은 반복 YAML을 줄이지만 각 파일에 project를 고정하고 + Git 항목에는 component, cluster, destination, path와 quoted `autoSync`를 + 명시하도록 요구합니다. Git revision은 template의 `main`으로 고정합니다. +- 새 capability가 공용인지 system 전용인지 애매하면 소비자 수, owner, + release cadence가 분리되는지를 먼저 검토합니다. +- 실제 production 요구가 생기기 전에는 production skeleton을 만들지 + 않습니다. + +세부 path와 예시는 +`docs/architecture/repository-taxonomy.md`를 따른다. diff --git a/docs/architecture/argocd.md b/docs/architecture/argocd.md index 7b0e9d3..cceecb8 100644 --- a/docs/architecture/argocd.md +++ b/docs/architecture/argocd.md @@ -1,38 +1,159 @@ -# Argo CD layout +# Argo CD architecture -`bootstrap/argocd/root-application.yaml`이 유일한 수동 seed입니다. 이 -Application은 `clusters/dev-k3s`를 source로 사용하고 다음 리소스를 -소유합니다. +## Control-plane ownership + +Controller 설치 후 다음 두 bootstrap object를 순서대로 수동 seed합니다. + +1. `bootstrap/argocd/control-plane-project.yaml` +2. `bootstrap/argocd/root-application.yaml` + +`gitops-control-plane` AppProject는 canonical Gitea repository와 in-cluster +`argocd` namespace, AppProject/ApplicationSet kind만 허용합니다. 단일 root +Application은 이 Project를 사용하고 다음 control-plane 구성을 source로 +사용합니다. ```text -clusters/dev-k3s +platform/control-plane/argocd ├── projects -└── applications - ├── foundation - │ ├── sealed-secrets - │ ├── vault - │ └── vault-agent-injector - ├── platform - │ └── auth-system - └── workloads - ├── auth-server - └── api-server +│ ├── platform-addons.yaml +│ ├── platform-services.yaml +│ ├── systems.yaml +│ └── workloads.yaml +└── application-sets + ├── platform-addons.yaml + ├── platform-services.yaml + ├── systems.yaml + └── workloads.yaml ``` -AppProject는 root sync wave `-10`, foundation은 `0~1`, platform은 `10`, -workload는 `20`입니다. 이 wave는 child Application 객체 생성 순서만 -표현하며 서로 다른 Application의 readiness dependency로 사용하지 -않습니다. Vault Agent와 workload는 필요한 Vault/DB API가 준비될 때까지 -자체 retry 가능한 형태여야 합니다. +Root는 AppProject와 ApplicationSet까지만 직접 소유합니다. 각 +ApplicationSet의 list inventory가 실제 child Application을 생성합니다. +Routine 변경에 category별 root나 직접 `kubectl apply`를 추가하지 않습니다. -모든 child Application은 auto-sync, prune, self-heal을 사용합니다. -Application 삭제와 parent prune은 확인이 필요하며, shared resource -소유권 충돌은 `FailOnSharedResource=true`로 실패시킵니다. +## AppProject boundary -Sync hook이 있는 `auth-server`와 `auth-system`에는 selective sync 옵션을 -사용하지 않습니다. DB migration과 Keycloak client sync는 같은 -Application 내부 wave로 순서를 제어합니다. +| AppProject | 소유 범위 | 허용 destination | +|---|---|---| +| `platform-addons` | Sealed Secrets, Vault Agent Injector 같은 외부 cluster addon | `kube-system`, `vault` | +| `platform-services` | 저장소가 소유하는 Vault shared service | `vault` | +| `systems` | Project Auth 전용 PostgreSQL, Keycloak, sync job | `auth-system-dev` | +| `workloads` | first-party `auth-server`, `api-server` | `auth-dev`, `api-dev` | -클러스터가 하나이고 child Application 수가 적으므로 현재는 명시적 -Application을 사용합니다. 두 번째 클러스터나 실제 production이 생길 -때 foundation/platform/workload별 ApplicationSet 도입을 검토합니다. +Project는 ApplicationSet 파일마다 고정하며 inventory 값으로 template하지 +않습니다. 이렇게 해야 element 변경으로 권한 경계를 넘을 수 없습니다. +각 Project는 필요한 source repository, destination, resource kind만 +allowlist합니다. + +## ApplicationSet contract + +ApplicationSet은 strict Go template와 list generator를 사용합니다. + +- `goTemplate: true` +- `goTemplateOptions: ["missingkey=error"]` +- 공통 element: `component`, `cluster`, `server`, `namespace`, quoted string + `autoSync` +- Git source element: ownership grammar를 따르는 `path`; `targetRevision`은 + template의 `main`으로 고정 +- Helm addon element: allowlisted `repoURL`, `chart`, chart `revision`, + `helmValues` +- 파일별 고정 project + +필수 key가 빠지면 빈 문자열로 잘못 배포하지 않고 render가 실패해야 합니다. +Application 이름, destination, source path는 같은 element에서 파생하되 +project와 Git repository/revision trust boundary는 template하지 않습니다. +외부 addon의 Helm `repoURL`은 element에서 template되지만 고정 AppProject의 +`sourceRepos` allowlist 밖 URL은 sync할 수 없습니다. + +## `autoSync` stage gate + +`autoSync`는 bootstrap 준비 상태와 routine reconciliation을 분리합니다. +Inventory schema는 Go template 비교를 위해 boolean이 아닌 quoted string +`"true"`/`"false"`를 사용합니다. Template patch는 값이 `"true"`인 +element에만 다음 정책을 추가합니다. + +```yaml +syncPolicy: + automated: + enabled: true + prune: true + selfHeal: true +``` + +초기 gate는 다음과 같습니다. + +| Application | 초기 `autoSync` | 열기 전 확인 | +|---|---:|---| +| Sealed Secrets | `"true"` | Argo가 chart source를 읽을 수 있음 | +| Vault | `"true"` | dev PVC와 NetworkPolicy 변경 검토 | +| Vault Agent Injector | `"false"` | Vault init, `vault-foundation`, `vault-workloads`, runtime secret seed 완료 | +| `auth-system` | `"false"` | Injector Healthy와 Vault login/secret capability 확인 | +| `auth-server` | `"false"` | PostgreSQL Healthy, `vault-database`, Keycloak/sync 준비 완료 | +| `api-server` | `"false"` | `auth-server` Healthy와 호출 경로 확인 | + +Gate는 단계별 PR로 하나씩 엽니다. `false`여도 Application 생성과 diff +표시는 계속되며 수동 Sync 자체를 기술적으로 막지는 않습니다. 따라서 +Argo RBAC에서 sync 권한을 제한하고, 수동 Sync에는 명시적 change record를 +요구합니다. + +비활성 기간 동안 쌓인 모든 diff가 gate를 여는 순간 함께 반영됩니다. +`autoSync: "true"` PR은 현재 live-to-desired 전체 diff를 검토한 뒤 +승인해야 합니다. + +## Ordering과 failure handling + +Control-plane sync wave는 AppProject(`-10`)를 ApplicationSet(`-5`)보다 +먼저 생성합니다. 네 ApplicationSet은 모두 같은 wave이고 element별 +stage/wave field는 없습니다. Generated Application의 실제 readiness +순서는 `autoSync` 전환과 health 확인이 담당합니다. + +Vault Agent, workload와 hook은 선행 API가 늦게 준비될 때 retry할 수 있어야 +합니다. `auth-system`의 Keycloak client sync와 `auth-server`의 database +migration은 idempotent Sync hook입니다. Hook을 사용하는 Application에는 +`ApplyOutOfSyncOnly=true`를 설정하지 않습니다. + +Generated Application은 automated 상태에서 `PruneLast=true`와 +`FailOnSharedResource=true`를 사용합니다. ApplicationSet은 +`applicationsSync: create-update`와 `preserveResourcesOnDeletion: true`를 +사용합니다. Parent prune과 Application 삭제에는 확인을 요구합니다. +CRD와 cluster-wide RBAC를 포함할 수 있는 `platform-addons`는 +Application-level `Prune=confirm`도 사용하므로 chart upgrade의 삭제는 +별도 승인이 필요합니다. +Stateful path rename이나 ownership 이동은 별도 migration으로 수행하며 +ApplicationSet element를 먼저 삭제하지 않습니다. + +`create-update`에서는 generator element를 제거해도 기존 generated +Application이 자동 삭제되지 않고 stale 상태로 남습니다. Element 제거와 +Application/resource decommission은 +[application decommission runbook](../runbooks/application-decommission.md)의 +inventory, gate, backup, 명시적 삭제 절차를 따릅니다. + +## Generator 확장 기준 + +현재는 cluster가 `dev-k3s` 하나이므로 네 ApplicationSet의 explicit List +generator가 가장 쉽게 검토됩니다. 존재하지 않는 production이나 미래 +cluster를 위해 Matrix abstraction을 미리 만들지 않습니다. + +두 번째 실제 cluster가 생겨 `cluster`, `server`와 cluster별 gate를 여러 +component에서 반복하게 될 때 `clusters//config.yaml`을 Git files +generator로 읽고 component inventory와 Matrix generator로 결합합니다. +그때도 AppProject는 ApplicationSet template에 고정하고, cluster별 +`autoSync`는 quoted string과 승인 gate로 유지합니다. + +## Further reading + +- Argo CD: [cluster bootstrapping](https://argo-cd.readthedocs.io/en/stable/operator-manual/cluster-bootstrapping/), + [ApplicationSet modification policy](https://argo-cd.readthedocs.io/en/stable/operator-manual/applicationset/Controlling-Resource-Modification/), + [ApplicationSet deletion](https://argo-cd.readthedocs.io/en/stable/operator-manual/applicationset/Application-Deletion/), + [automated sync semantics](https://argo-cd.readthedocs.io/en/stable/user-guide/auto_sync/) +- Kubernetes: + [Kustomize](https://kubernetes.io/docs/tasks/manage-kubernetes-objects/kustomization/) +- Terraform: [state refactoring](https://developer.hashicorp.com/terraform/language/state/refactor), + [`terraform_remote_state` security warning](https://developer.hashicorp.com/terraform/language/state/remote-state-data), + [write-only arguments](https://developer.hashicorp.com/terraform/language/manage-sensitive-data/write-only) +- Vault: + [JWT/OIDC authentication](https://developer.hashicorp.com/vault/docs/auth/jwt) + +## Repository-only change + +이 구조와 gate 설계는 2026-07-26 현재 Git에서만 작성·검증했습니다. 실제 +cluster migration이나 sync는 이 리팩터링 리뷰 범위에 포함되지 않습니다. diff --git a/docs/architecture/deployment.md b/docs/architecture/deployment.md index c11accb..01bd717 100644 --- a/docs/architecture/deployment.md +++ b/docs/architecture/deployment.md @@ -5,29 +5,71 @@ ```text Gitea main | - +-- Argo CD root -> AppProjects + child Applications -> Kubernetes + +-- Argo CD root + | -> AppProjects + ApplicationSets + | -> generated Applications + | -> Kubernetes | - +-- approved Terraform runner -> Vault API + +-- approved Terraform runner + -> one of three Vault states + -> Vault API ``` -Argo CD는 Kubernetes desired state만 관리합니다. 최초 Argo 설치/root -seed와 문서화된 recovery 외에는 직접 cluster mutation을 하지 않습니다. -Terraform은 Config Management Plugin이나 Argo hook 안에서 실행하지 -않습니다. +Argo CD는 Kubernetes desired state만 관리합니다. 최초 Argo 설치와 +bootstrap 전용 AppProject/root seed, 문서화된 recovery 외에는 직접 +cluster mutation을 하지 않습니다. Terraform은 Config Management +Plugin이나 Argo hook 안에서 실행하지 않습니다. GHCR은 image artifact +registry이며 desired state source가 아닙니다. -## Kustomize ownership +## Kubernetes ownership -- `platform/`, `workloads/`: 환경 중립 base -- `clusters/dev-k3s/manifests/`: namespace, host, image, Vault role 및 - NetworkPolicy를 포함하는 최종 cluster composition -- Argo CD Application: final composition만 source로 사용 +| Layer | 역할 | +|---|---| +| `platform/control-plane/argocd` | AppProject와 ApplicationSet control plane | +| `platform/shared-services/*/base` | 환경 중립을 목표로 하는 공유 cluster service base | +| `systems/*/base` | 환경 중립을 목표로 하는 bounded-context backing system base | +| `workloads/*/base` | first-party 애플리케이션 base | +| `clusters/dev-k3s/overlays/*` | dev namespace, host, digest, Vault role/path, NetworkPolicy를 합친 최종 구성 | -지원하지 않는 production overlay는 존재하지 않습니다. production -계약과 승인 경계가 확정될 때 별도로 생성합니다. +현재 concrete ownership은 Vault가 platform shared service, +PostgreSQL/Keycloak이 `systems/auth-system`, 두 서버가 workload입니다. +Argo CD Application은 base가 아니라 최종 cluster overlay만 source로 +사용합니다. + +현재 Keycloak base의 `start-dev`와 Vault base의 +TLS-off/single-node identity는 dev-specific 예외입니다. 내부 Service +참조는 짧은 DNS로 namespace 중립화했지만, 남은 값을 overlay로 추출하는 +작업은 후속 리팩터링입니다. + +지원하지 않는 production overlay는 존재하지 않습니다. Production trust, +approval, TLS, availability contract가 확정될 때 별도로 설계합니다. + +## Bootstrap progression + +```text +Argo root + -> Sealed Secrets + Vault autoSync + -> Vault init + -> vault-foundation + -> vault-workloads + -> runtime secret seed + -> Vault Agent Injector autoSync gate + -> auth-system autoSync gate + -> PostgreSQL Healthy + -> vault-database + -> Keycloak/client sync ready + -> auth-server autoSync gate + -> auth-server Healthy + -> api-server autoSync gate +``` + +이 순서는 Application sync wave로 강제하지 않습니다. 각 전환은 health와 +plan/diff를 확인한 별도 PR입니다. Gate가 닫힌 동안에도 generated +Application은 OutOfSync diff를 보여 줍니다. ## In-application ordering -`auth-server`의 한 sync operation 안에서: +`auth-server`의 한 sync operation 안에서는 다음 ordering을 사용합니다. - generated ConfigMap과 일반 리소스: wave `0` - database migration Sync hook: wave `5` @@ -36,20 +78,40 @@ Terraform은 Config Management Plugin이나 Argo hook 안에서 실행하지 `auth-system`의 Keycloak client sync도 idempotent Sync hook이며 deadline, backoff, `BeforeHookCreation,HookSucceeded` cleanup을 사용합니다. +Application 간 준비 순서와 Application 내부 hook 순서를 혼동하지 +않습니다. ## Stateful lifecycle -Vault와 PostgreSQL PVC는 `Prune=false`로 보호합니다. child Application -prune/delete는 확인이 필요합니다. path 이동이나 Application rename 전에는 -새 owner가 동일 live resource를 정상적으로 추적하는지 확인한 후 이전 -owner를 non-cascading 방식으로 제거합니다. +Vault PVC에는 `Prune=confirm,Delete=confirm`이 명시되어 있습니다. +PostgreSQL PVC는 StatefulSet `volumeClaimTemplates`가 생성하며 현재 +manifest에 별도 Argo prune annotation이 없습니다. Namespace와 generated +Application 삭제 보호만 믿지 말고 PostgreSQL retention/backup을 직접 +확인해야 합니다. Path, namespace, Application 이름을 이동할 때는 다음을 +별도 migration으로 다룹니다. -## Image promotion +1. 기존 live object와 owner를 inventory합니다. +2. 새 owner가 같은 object를 안전하게 추적할 수 있는지 render/diff로 + 확인합니다. +3. Stateful data backup과 rollback 지점을 확보합니다. +4. 기존 owner를 non-cascading 방식으로 제거한 뒤 새 owner를 연결합니다. -첫-party image는 애플리케이션 CI가 얻은 정확한 GHCR digest를 Gitea -workflow에 전달합니다. workflow는 digest 변경 PR을 만들고, validation과 -승인을 거쳐 merge된 뒤 Argo CD가 배포합니다. +이번 리팩터링에서는 `platform` namespace의 auth-system을 +`auth-system-dev`로 옮기는 live 작업을 실행하지 않았습니다. -현재 short-SHA tag는 migration 시점의 예외입니다. private GHCR을 읽을 -자격증명이 이 저장소 실행 환경에 없으므로 임의 digest로 바꾸지 않았고, -다음 정상 promotion에서 `digest:`로 교체됩니다. +## Image promotion과 GHCR + +정상 promotion에서 first-party image CI는 검증한 정확한 GHCR digest를 +Gitea workflow에 전달합니다. Workflow는 전용 branch와 digest 변경 PR을 +만들고 validation과 승인을 거쳐 merge된 뒤 Argo CD가 배포합니다. +Renovate는 외부 chart, third-party image와 Terraform provider만 갱신하며 +두 first-party GHCR package는 비활성화합니다. 따라서 동일 image field를 +promotion workflow와 Renovate가 동시에 쓰지 않습니다. + +Private GHCR pull credential만 SealedSecret으로 Git에 저장합니다. 평문 +credential이나 registry token은 manifest, Actions log, Terraform state에 +남기지 않습니다. + +현재 short-SHA tag는 migration 시점의 예외입니다. Registry 검증 없이 +임의 digest를 만들지 않고 다음 정상 promotion에서 immutable digest로 +교체합니다. diff --git a/docs/architecture/repository-taxonomy.md b/docs/architecture/repository-taxonomy.md new file mode 100644 index 0000000..6e0c722 --- /dev/null +++ b/docs/architecture/repository-taxonomy.md @@ -0,0 +1,157 @@ +# Repository taxonomy + +이 문서는 새 리소스를 어느 디렉터리에 둘지 결정하는 기준입니다. 이 +저장소는 Project Auth를 예제로 삼는 독립 reference lab이며, 디렉터리 +이름은 조직의 중요도나 설치 순서가 아니라 소유권을 표현합니다. + +## 분류 기준 + +| 분류 | 판단 질문 | 현재 예 | +|---|---|---| +| `platform` | GitOps control plane이거나, 둘 이상의 system이 독립 lifecycle로 소비할 cluster capability인가? | Argo inventory, Vault shared service | +| `systems` | 하나의 bounded context가 함께 소유하는 backing system인가? | Project Auth의 PostgreSQL, Keycloak, realm/client sync | +| `workloads` | 별도 source repository에서 빌드하는 first-party 실행 단위인가? | `auth-server`, `api-server` | +| `clusters` | 특정 클러스터의 최종 composition 값인가? | namespace, host, digest, Vault role, NetworkPolicy | +| `iac` | Kubernetes가 아닌 외부 API 객체를 선언하는가? | Vault mounts, policies, auth roles, database roles | +| `bootstrap` | GitOps controller가 존재하기 전에 필요한 최소 seed인가? | Argo CD 설치 버전, 제한된 control-plane AppProject와 root Application | + +다음 세 질문을 순서대로 사용합니다. + +1. 누가 소비하고 장애 영향을 받는가? +2. 누가 변경을 승인하고 lifecycle을 책임지는가? +3. 다른 bounded context와 독립적으로 교체하거나 배포할 수 있는가? + +제품 이름만으로 분류하지 않습니다. 예를 들어 Keycloak이 여러 system의 +공용 identity service가 되고 별도 owner와 release cadence를 갖게 되면 +실행 서비스는 `platform/`으로 이동할 수 있습니다. 그래도 Project Auth +realm/client 구성은 `systems/auth-system/`에 남습니다. 현재 Keycloak과 +PostgreSQL은 Project Auth 전용이므로 모두 system 소유입니다. + +## Path contract + +환경 중립 base와 cluster-specific overlay를 분리하는 것이 목표 +contract입니다. + +```text +platform/shared-services//base +systems//base +workloads//base + +clusters//overlays/platform/ +clusters//overlays/systems/ +clusters//overlays/workloads/ + +platform/control-plane/argocd/projects +platform/control-plane/argocd/application-sets +``` + +Base에는 재사용 가능한 workload 구조, Service, ServiceAccount와 기본 +configuration contract를 둡니다. Overlay에는 다음처럼 클러스터와 환경을 +알아야 하는 값을 둡니다. + +- namespace와 public/internal host +- image reference; 정상 promotion의 목표는 immutable digest +- Vault auth role과 KV path annotation +- NetworkPolicy의 namespace/CIDR +- dev-only resource profile와 TLS 차이 + +Argo CD는 base를 직접 source로 사용하지 않고 반드시 최종 overlay를 +reconcile합니다. + +현재 first-party overlay의 짧은 commit tag는 이관 예외입니다. Registry를 +검증할 credential 없이 임의 digest로 바꾸지 않고 다음 정상 promotion +PR에서 immutable digest로 전환합니다. + +### 현재 base의 알려진 예외 + +Base 내부의 PostgreSQL·Keycloak 참조는 namespace를 포함하지 않은 짧은 +Service DNS를 사용하므로 overlay namespace에 재사용할 수 있습니다. 다만 +아직 다음 dev/single-node 가정은 남아 있습니다. + +- `systems/auth-system/base`의 Keycloak 실행 command가 `start-dev`입니다. +- `platform/shared-services/vault/base/files/vault/vault.hcl`이 + `tls_disable = 1`과 고정된 single-node `node_id`를 사용합니다. + +이는 숨겨진 환경 중립성이 아니라 명시적인 리팩터링 부채입니다. 두 번째 +환경이나 replica를 만들기 전에 dev 전용 command, TLS와 node identity를 +overlay 또는 입력 가능한 configuration으로 옮깁니다. + +```text +base -> dev-k3s overlay -> ApplicationSet inventory -> generated Application + -> Argo CD -> Kubernetes +``` + +## Platform 안의 두 역할 + +`platform` ownership에는 다음 두 종류가 있습니다. + +- Cluster addon: Kubernetes API를 확장하거나 admission/control-plane + 기능을 제공하는 외부 chart. 현재 Sealed Secrets와 Vault Agent Injector가 + 해당합니다. Inventory는 + `platform/control-plane/argocd/application-sets/platform-addons.yaml`에 + 둡니다. +- Shared service: 일반 workload처럼 namespace에서 실행되지만 여러 system이 + 사용할 수 있는 capability. 현재 Vault가 해당합니다. + +외부 Helm chart를 복사해 base처럼 유지하지 않습니다. chart version과 +values는 Argo inventory에서 pin합니다. 저장소가 직접 소유하는 shared +service manifest만 `platform/shared-services/`에 둡니다. + +`foundation`은 소유권 분류가 아닙니다. Bootstrap 때 먼저 필요하다는 뜻은 +분리된 ApplicationSet category, `autoSync` gate와 runbook 순서로 +표현합니다. 따라서 새로운 `foundation/` business directory를 만들지 +않습니다. + +## System와 workload의 경계 + +`systems/auth-system`은 인증 bounded context가 함께 책임지는 데이터와 +identity backing services입니다. + +- PostgreSQL StatefulSet와 초기 database contract +- Keycloak server와 Project Auth realm +- Keycloak client synchronization + +`workloads/auth-server`와 `workloads/api-server`는 각각 별도 source +repository와 release digest가 있는 애플리케이션입니다. Workload가 +auth-system을 사용하더라도 두 lifecycle을 합치지 않습니다. + +Dev namespace도 소유권을 드러냅니다. + +| 소유 단위 | Namespace | +|---|---| +| Vault shared service와 injector | `vault` | +| Project Auth backing system | `auth-system-dev` | +| Auth workload | `auth-dev` | +| API workload | `api-dev` | + +## Vault path grammar + +KV path도 같은 소유권 언어를 사용합니다. + +```text +kv/dev/systems/auth-system/postgres/superuser +kv/dev/systems/auth-system/postgres/auth-server +kv/dev/systems/auth-system/postgres/keycloak +kv/dev/systems/auth-system/keycloak/bootstrap-admin +kv/dev/workloads/auth-server/keycloak-client +``` + +Vault policy 파일에는 KV-v2 API path인 `kv/data/...`를 사용하고, CLI에는 +mount-relative path인 `kv/dev/...`를 사용합니다. 이전 +`kv/dev/platform/...` 경로는 legacy migration source일 뿐 새 desired +state가 아닙니다. + +## 새 항목 배치 예 + +| 변경 | 위치 | +|---|---| +| 또 다른 공용 admission controller | `platform/control-plane/argocd/application-sets/platform-addons.yaml` | +| 공용 object storage service base | `platform/shared-services/object-storage/base` | +| Project Auth 전용 Redis | `systems/auth-system/base` | +| 새 first-party worker | `workloads//base` | +| dev worker digest/secret annotation | `clusters/dev-k3s/overlays/workloads/` | +| Vault workload policy/role | `vault-workloads` Terraform state와 `policies/vault/` | +| Vault auth backend | `vault-foundation` Terraform state | + +분류가 애매하면 설치 순서가 아니라 owner와 소비자 경계를 ADR에 먼저 +기록합니다. diff --git a/docs/architecture/secret-trust.md b/docs/architecture/secret-trust.md index 481b6d4..fea4b3c 100644 --- a/docs/architecture/secret-trust.md +++ b/docs/architecture/secret-trust.md @@ -2,54 +2,126 @@ ## Dev Vault -`dev-k3s`는 단일 self-hosted Vault를 사용합니다. 동일 workload -클러스터에 별도의 Transit Vault를 두지 않습니다. 단일 Vault는 다음을 -소유합니다. +`dev-k3s`는 workload 클러스터 안의 단일 self-hosted Vault를 사용합니다. +별도 Transit Vault는 두지 않습니다. Vault는 다음 API 객체를 제공합니다. - KV-v2 runtime secret path - Kubernetes auth와 workload role -- dynamic PostgreSQL credential +- Dynamic PostgreSQL credential - 애플리케이션 JWT signing용 Transit key -dev Vault는 Shamir 1-of-1로 한 번 초기화하고 재시작 시 명시적으로 -unseal합니다. 이 방식은 개발 환경 전용입니다. production에서는 managed -Vault 또는 독립 failure domain의 HA integrated-Raft와 KMS/HSM -auto-unseal을 사용해야 합니다. +Dev Vault는 Shamir 1-of-1로 한 번 초기화하고 재시작 때 명시적으로 +unseal합니다. 이는 폐기 가능한 개발 환경 전용입니다. Production에서는 +managed Vault 또는 독립 failure domain의 HA integrated-Raft와 KMS/HSM +auto-unseal이 필요합니다. -## Terraform +## Three Terraform states -`vault-core` state는 mounts, auth, policies, roles와 JWT key를 소유하며 -제한된 관리자만 적용합니다. `vault-database`는 PostgreSQL connection과 -dynamic roles만 소유하고 `vault-database-automation-dev` 정책을 사용합니다. +```text +vault-foundation + -> mounts/auth configuration + -> delegated automation policies + -> optional, separated CI JWT login roles -Terraform variable로 전달되는 token과 PostgreSQL password는 ephemeral/ -write-only 경계를 사용합니다. KV payload는 Terraform resource/data -source로 읽거나 쓰지 않습니다. +vault-workloads + -> workload policies and Kubernetes auth roles + -> project-auth-jwt Transit key + +vault-database + -> auth-system PostgreSQL connection + -> auth-db-migration-dev dynamic role +``` + +`vault-foundation`은 routine runner가 아니라 bootstrap 또는 승인된 보안 +관리자가 실행합니다. 이 state가 workloads/database automation policy를 +만들고, OIDC/JWT trust가 설정됐을 때만 두 login role을 분리해 만듭니다. +Workloads/database exact claim map은 최소 한 공통 discriminator key에서 +다른 값을 가져야 합니다. 실제 issuer가 그 repository/ref/job claim을 +신뢰할 수 있게 발행하는지 확인하지 못하면 CI JWT auth를 활성화하지 +않습니다. Delegated state는 자신에게 권한을 추가할 수 없고 맡은 정확한 +Vault API path만 변경합니다. + +Exact API path 허용이 runner를 완전한 sandbox로 만들지는 않습니다. +`vault-workloads` runner가 허용된 ACL policy 내용이나 Kubernetes auth role +payload를 악의적으로 바꾸면 더 강한 policy를 연결하는 권한 상승이 +가능합니다. 따라서 이 runner는 신뢰된 security automation으로 취급하고, +protected branch, policy lint, saved-plan 승인과 Vault audit log를 함께 +trust boundary로 사용합니다. + +세 state는 `terraform_remote_state`로 연결하지 않습니다. Policy/role 이름은 +checked-in contract로 공유하고 runbook 또는 CI stage가 실행 순서를 +보장합니다. + +Provider token과 PostgreSQL password는 ephemeral/write-only 입력으로만 +전달합니다. KV payload는 Terraform resource/data source로 읽거나 쓰지 +않습니다. + +## KV ownership paths + +Secret path는 repository taxonomy와 같은 owner를 표현합니다. + +| Consumer | Vault CLI path | +|---|---| +| PostgreSQL bootstrap | `kv/dev/systems/auth-system/postgres/superuser` | +| Auth database bootstrap/runtime | `kv/dev/systems/auth-system/postgres/auth-server` | +| Keycloak database | `kv/dev/systems/auth-system/postgres/keycloak` | +| Keycloak bootstrap admin | `kv/dev/systems/auth-system/keycloak/bootstrap-admin` | +| Auth-server Keycloak client | `kv/dev/workloads/auth-server/keycloak-client` | + +Vault ACL과 Agent annotation은 KV-v2 API path인 `kv/data/...`를 사용합니다. +CLI의 `vault kv put`은 `kv/dev/...`를 사용합니다. 이전 +`kv/dev/platform/...` 값은 migration source이며 새 policy가 계속 +허용하면 안 됩니다. ## Workload authentication -workload는 audience `vault`, TTL 1시간의 projected ServiceAccount token으로 -Vault Kubernetes auth에 로그인합니다. token은 Vault Agent가 사용하며 +Workload는 audience `vault`, TTL 1시간의 projected ServiceAccount token으로 +Vault Kubernetes auth에 로그인합니다. Token은 Vault Agent가 사용하며 application container에 Kubernetes bearer token을 직접 노출하지 않습니다. -Secret payload는 승인된 운영자가 Vault에 직접 기록합니다. 값은 Git, -Gitea Actions log, Terraform state, Kubernetes manifest에 남기지 않습니다. +Role은 ServiceAccount, namespace, audience, token policy와 TTL을 정확히 +묶습니다. 현재 role은 Project Auth backing service의 `auth-system-dev`와 +Vault를 사용하는 `auth-dev` ServiceAccount에만 존재합니다. `api-dev`에는 +Vault role도 Vault NetworkPolicy ingress도 없으며 필요가 생기기 전에는 +권한을 추가하지 않습니다. + +Secret 값은 승인된 운영자가 Vault에 직접 기록합니다. 값은 Git, Gitea +Actions log, Terraform state, Kubernetes manifest에 남기지 않습니다. ## Bootstrap material Vault init output은 기본적으로 `.local/vault/dev-k3s-init.json`에 mode -`0600`으로 생성됩니다. encrypted custody로 이동한 후 working copy를 -제거합니다. initial root token은 `vault-core`와 operator auth 검증 직후 -폐기합니다. +`0600`으로 생성됩니다. Encrypted custody로 이동한 뒤 working copy를 +제거합니다. -Sealed Secrets는 private GHCR pull credential에만 사용합니다. controller -private key는 별도 복구 저장소에 백업해야 합니다. +Initial root token은 다음 작업에만 사용합니다. + +1. `vault-foundation` apply +2. OIDC/JWT가 없는 빈 lab의 짧은 TTL bootstrap token 발급과 capability + 검증 +3. 최초 runtime secret seed +4. 필요한 break-glass/recovery 절차 확인 + +`vault-database` 적용까지 끝나면 replacement token의 대표 update +capability와 root policy 부재를 확인한 뒤 initial root와 bootstrap token을 +폐기합니다. 상시 cluster ServiceAccount에 broad `platform-admin` 정책을 +연결하지 않습니다. `vault-workloads`와 `vault-database`의 routine 실행은 +각각 짧은 TTL identity를 사용합니다. + +Initial root 폐기 뒤에는 상시 foundation administrator가 없습니다. Future +foundation 변경은 encrypted unseal custody의 승인을 받아 Vault +generated-root ceremony를 수행하고, 승인된 plan 적용 뒤 생성한 root를 +즉시 폐기해야 합니다. + +Sealed Secrets는 private GHCR pull credential에만 사용합니다. Controller +private key는 Git과 분리된 recovery custody에 백업합니다. ## Dev limitations -- Vault, PostgreSQL, ingress가 아직 TLS를 사용하지 않음 -- single-node Vault와 PostgreSQL +- Vault, PostgreSQL, ingress가 TLS를 사용하지 않음 +- Single-node Vault와 PostgreSQL - Kubernetes API egress CIDR가 현재 dev cluster에 종속 - 정적 bootstrap secret은 coordinated rotation 필요 +- Namespace/path migration이 아직 실제 cluster에 적용되지 않음 이 제약은 production에서 허용되지 않습니다. diff --git a/docs/architecture/terraform.md b/docs/architecture/terraform.md new file mode 100644 index 0000000..67bac19 --- /dev/null +++ b/docs/architecture/terraform.md @@ -0,0 +1,95 @@ +# Terraform boundary + +Terraform은 VM만 정의하는 도구가 아니라 provider가 노출하는 API 객체의 +desired state를 선언하고 plan/apply하는 framework입니다. 이 저장소에는 +machine/cloud provider가 없으므로 서버, 네트워크, k3s 설치를 Terraform이 +소유하지 않습니다. 현재 적용 범위는 Vault API 객체뿐입니다. + +## 도구별 소유권 + +| 대상 | 소유 도구 | 이유 | +|---|---|---| +| Kubernetes manifest와 rollout | Argo CD | Git revision을 지속적으로 reconcile | +| Vault mount, auth, policy, role, Transit key, DB connection | Terraform Vault provider | API 객체의 plan과 state ownership 필요 | +| Vault init/unseal, initial secret seed | 승인된 operator runbook | 일회성 ceremony와 secret material을 state에서 제외 | +| KV secret payload | 외부 secret authority/operator | Git과 Terraform state에 값이 남지 않아야 함 | +| VM, network, k3s | 현재 소유자 없음 | 실제 provider와 lifecycle이 정해지지 않음 | + +Kubernetes와 Helm을 Terraform에 다시 넣지 않습니다. 같은 object를 Argo +CD와 Terraform이 동시에 소유하면 두 reconciler가 충돌합니다. 반대로 +Terraform을 Argo hook에서 실행하면 cluster reconciliation이 Vault state +lock과 privileged credential lifecycle까지 떠안게 됩니다. + +## State 경계 + +```text +vault-foundation + creates delegation + | | + v v +vault-workloads vault-database +runtime access PostgreSQL integration +``` + +- `vault-foundation`은 mount, Kubernetes auth와 위임 policy/login role을 + 소유합니다. Routine CI apply 대상이 아닙니다. +- `vault-workloads`는 runtime ACL/Kubernetes role과 애플리케이션 Transit + key만 소유합니다. +- `vault-database`는 실제 PostgreSQL이 준비된 뒤 connection과 migration + dynamic role만 소유합니다. + +위임받은 state는 자기 runner policy나 login role을 만들지 않습니다. +State 간 이름은 checked-in contract로 공유하며 `terraform_remote_state`로 +다른 state snapshot을 읽지 않습니다. + +Policy HCL이 정확한 API path를 허용하므로 `kv`, `database`, `transit`, +`kubernetes`, `project-auth-jwt`, `auth-system-postgres-dev` 같은 보안 +경계 이름은 각 root의 local contract로 고정합니다. 변수로 한쪽만 +override해 plan은 성공하지만 권한이 어긋나는 상태를 허용하지 않습니다. +이 이름을 바꿀 때는 foundation policy, delegated root와 runtime consumer를 +하나의 migration 설계에서 함께 변경합니다. + +Mount, Kubernetes auth와 선택적 CI JWT auth에는 `prevent_destroy`를 +적용합니다. 입력 누락이 기존 auth mount 삭제로 이어지지 않으며, 실제 +제거는 consumer/token inventory를 거친 별도 decommission revision에서만 +보호를 명시적으로 해제합니다. + +## 실행 계약 + +1. Remote backend는 encryption, versioning, locking과 root별 access + control을 제공해야 합니다. +2. `terraform-plan`이 만든 saved plan을 검토하고, 같은 `PLAN_FILE`만 + `terraform-apply`가 사용합니다. +3. Plan은 sensitive artifact로 취급하며 apply 성공 후 제거합니다. +4. Provider token과 PostgreSQL password는 Terraform 1.11 이상의 + ephemeral variable/write-only argument로 실행 시점에 다시 주입합니다. +5. Delegated runner token은 짧은 TTL, no-default-policy와 정확한 object + path만 사용합니다. Capability 확인, self lookup과 명시적 self revoke에 + 필요한 세 self-service API만 별도로 허용합니다. +6. Foundation, workloads, database apply는 서로 다른 승인 단계입니다. + +## 두 번째 클러스터 또는 machine IaC + +두 번째 클러스터가 생겨도 state를 합치지 않습니다. Cluster별 backend와 +Vault instance ownership이 독립이면 같은 세 root contract를 reusable +module로 승격합니다. 실제 VM/network provider, account, failure domain과 +destroy/backup 책임이 정해졌을 때만 다음처럼 별도 machine root를 +추가합니다. + +```text +iac/terraform/live//machine +iac/terraform/live//vault-foundation +iac/terraform/live//vault-workloads +iac/terraform/live//vault-database +``` + +Machine root output을 읽기 위해 Vault state 전체를 공유하지 않습니다. +필요한 endpoint는 명시적 configuration contract나 최소 권한의 별도 +configuration store로 전달합니다. + +## Further reading + +- [Terraform ephemeral values and write-only arguments](https://developer.hashicorp.com/terraform/language/manage-sensitive-data/ephemeral) +- [Terraform state refactoring](https://developer.hashicorp.com/terraform/language/state/refactor) +- [Remote state data security warning](https://developer.hashicorp.com/terraform/language/state/remote-state-data) +- [Vault provider write-only attributes](https://registry.terraform.io/providers/hashicorp/vault/latest/docs/guides/using_write_only_attributes) diff --git a/docs/runbooks/application-decommission.md b/docs/runbooks/application-decommission.md new file mode 100644 index 0000000..9b2cfda --- /dev/null +++ b/docs/runbooks/application-decommission.md @@ -0,0 +1,71 @@ +# ApplicationSet decommission + +이 절차는 `applicationsSync: create-update`를 사용하는 generated +Application을 안전하게 해체하기 위한 runbook입니다. List element를 지우는 +것만으로 Application이 삭제되지 않는 것은 오류가 아니라 삭제 보호 +동작입니다. + +## 중단 조건 + +다음 중 하나라도 만족하면 진행하지 않습니다. + +- 대상 Application 이름, ApplicationSet, 클러스터가 명확하지 않음 +- 최신 backup과 복구 테스트가 없음 +- PVC/PV와 StorageClass의 reclaim policy를 확인하지 않음 +- Application에 예상하지 못한 finalizer가 있음 +- live-to-target diff에 대상 밖 리소스가 포함됨 +- `argocd-cmd-params-cm`의 전역 ApplicationSet policy가 저장소의 + `create-update` 의도를 덮어쓰는지 확인하지 않음 + +Generated Application template에는 resource finalizer를 두지 않습니다. +`preserveResourcesOnDeletion: true`도 유지합니다. ApplicationSet 전체를 +삭제해서 개별 component를 해체하지 않습니다. + +## 공통 준비 + +1. 대상 element의 `autoSync`를 `false`로 바꾸는 PR을 먼저 병합합니다. +2. 비활성 기간에 누적된 live-to-target 전체 diff를 저장합니다. +3. 대상이 stateful이면 application-level backup과 restore test를 + 완료합니다. +4. List element를 제거하는 별도 PR을 병합합니다. `create-update` 정책 + 때문에 기존 Application은 의도적으로 남아야 합니다. +5. 남은 Application의 소유 관계와 finalizer를 확인합니다. + +```bash +kubectl -n argocd get application \ + -o json | + jq '{ownerReferences: .metadata.ownerReferences, finalizers: (.metadata.finalizers // [])}' +``` + +예상하지 못한 finalizer를 강제로 제거하지 않습니다. + +## 리소스를 보존하고 관리만 중단 + +List element를 제거한 뒤 generated Application이 더 이상 재생성되지 않는 +것을 확인합니다. Template에 resource finalizer가 없으므로 Application +객체 삭제는 workload를 orphan으로 남깁니다. + +```bash +kubectl -n argocd delete application +``` + +삭제 후 workload가 그대로 존재하고 Argo CD에 다시 나타나지 않는지 +확인합니다. 보존된 리소스는 더 이상 drift correction을 받지 않으므로, +다른 소유자에게 즉시 인계하거나 별도 정리 계획을 기록합니다. + +## 리소스까지 제거 + +리소스 삭제는 List element 제거와 같은 PR에 섞지 않습니다. + +1. 대상 Application은 inventory에 남기고 `autoSync: "false"` 상태를 + 유지합니다. +2. 별도 PR에서 component의 desired state를 해체용 빈 구성으로 바꿉니다. +3. Argo CD diff에서 삭제 대상이 정확한지 검토합니다. +4. Namespace, PVC 등 `Prune=confirm,Delete=confirm` 대상의 backup과 + reclaim policy를 다시 확인하고 승인된 prune을 수동 실행합니다. +5. 리소스가 제거된 뒤 List element 제거 PR을 병합합니다. +6. 남은 Application 객체를 삭제합니다. + +승인 시각 annotation을 자동화하거나 우회하지 않습니다. `kubectl delete +applicationset` 및 finalizer 강제 제거는 복구 runbook과 별도 승인 없이는 +사용하지 않습니다. diff --git a/docs/runbooks/dev-bootstrap.md b/docs/runbooks/dev-bootstrap.md index 962b5af..831ba17 100644 --- a/docs/runbooks/dev-bootstrap.md +++ b/docs/runbooks/dev-bootstrap.md @@ -1,7 +1,11 @@ # Bootstrap an empty dev-k3s cluster -이 runbook은 폐기 가능한 개발 클러스터만 대상으로 합니다. production에 -사용하지 않습니다. +이 runbook은 폐기 가능한 빈 개발 클러스터만 대상으로 합니다. Production과 +기존 live cluster migration에는 사용하지 않습니다. + +> 2026-07-26 repository 리팩터링 중에는 아래 절차를 실행하지 않았습니다. +> 이 문서는 승인된 future bootstrap 절차이며 명령 예시는 자동 실행 대상이 +> 아닙니다. ## 1. Preflight @@ -11,38 +15,59 @@ kubectl cluster-info make validate ``` -의도한 dev cluster가 아니면 중단합니다. 내부 Gitea가 private이면 Argo CD가 -root repository를 읽을 수 있는 read-only credential을 외부 secret -authority에서 먼저 provision해야 합니다. credential은 이 저장소에 +의도한 빈 dev cluster가 아니면 중단합니다. 내부 Gitea가 private이면 Argo +CD가 root repository를 읽을 수 있는 read-only credential을 외부 secret +authority에서 먼저 provision해야 합니다. Credential은 이 저장소에 commit하지 않습니다. -remote state backend 파일을 준비합니다. +세 remote state backend 파일을 준비합니다. ```bash mkdir -p .local/terraform-backend/dev-k3s -cp iac/terraform/backend/dev-k3s/vault-core.s3.hcl.example \ - .local/terraform-backend/dev-k3s/vault-core.s3.hcl +cp iac/terraform/backend/dev-k3s/vault-foundation.s3.hcl.example \ + .local/terraform-backend/dev-k3s/vault-foundation.s3.hcl +cp iac/terraform/backend/dev-k3s/vault-workloads.s3.hcl.example \ + .local/terraform-backend/dev-k3s/vault-workloads.s3.hcl cp iac/terraform/backend/dev-k3s/vault-database.s3.hcl.example \ .local/terraform-backend/dev-k3s/vault-database.s3.hcl ``` -실제 bucket, endpoint와 workload identity를 설정합니다. backend credential은 -파일에 넣지 않습니다. +실제 bucket, endpoint와 workload identity를 설정합니다. Backend +credential은 파일에 넣지 않습니다. 세 backend key가 서로 다르고 locking이 +활성화됐는지 확인합니다. ## 2. Argo CD와 root Application ```bash make bootstrap KUBE_CONTEXT="$(kubectl config current-context)" -kubectl -n argocd get application project-gitops-dev-k3s +kubectl -n argocd get appproject gitops-control-plane +kubectl -n argocd get application project-gitops-control-plane +kubectl -n argocd get applicationsets ``` -이 명령이 수행하는 직접 cluster mutation은 Argo CD 설치와 root seed뿐입니다. -Child Application은 root가 생성합니다. +직접 cluster mutation은 pinned Argo CD 설치, 제한된 +`gitops-control-plane` AppProject, root Application seed뿐입니다. +Bootstrap script는 이 순서를 지킵니다. Root가 네 AppProject와 네 +ApplicationSet을 만들고, ApplicationSet이 child Application을 생성합니다. + +초기 inventory에서 Sealed Secrets와 Vault만 `autoSync: "true"`입니다. +Vault Agent Injector, `auth-system`, `auth-server`, `api-server` gate는 +닫힌 상태여야 합니다. + +### Sealed Secrets key 준비 + +Checked-in `ghcr-regcred` ciphertext는 암호화에 사용한 controller private +key로만 복호화할 수 있습니다. 기존 key backup이 있으면 workload gate를 +열기 전에 복원하고, 없으면 새 controller certificate와 원본 credential +authority를 사용해 두 SealedSecret을 다시 seal한 PR을 merge합니다. +[Sealed Secrets recovery runbook](sealed-secrets-recovery.md)을 따르며 평문 +GHCR credential을 Git이나 log에 남기지 않습니다. ## 3. Dev Vault 초기화 -Vault Pod가 생성될 때까지 기다린 뒤 operator workstation에서 forward합니다. -이 port-forward는 최초 dev bootstrap용이며 routine runner 모델이 아닙니다. +Vault Pod가 생성될 때까지 기다린 뒤 operator workstation에서 +port-forward합니다. 이는 최초 dev bootstrap용이며 routine runner 모델이 +아닙니다. ```bash kubectl -n vault wait --for=create pod -l app=vault --timeout=300s @@ -56,13 +81,14 @@ export VAULT_ADDR=http://127.0.0.1:8200 ./hack/vault-init.sh init ``` -`.local/vault/dev-k3s-init.json`을 즉시 encrypted custody로 복사합니다. -dev-only 1-of-1 unseal key와 initial root token이 있으므로 일반 backup과 -분리합니다. +`.local/vault/dev-k3s-init.json`을 즉시 encrypted custody에 복사합니다. +Dev-only 1-of-1 unseal key와 initial root token이 있으므로 일반 backup과 +분리합니다. Local working copy는 root revoke 때까지 mode `0600`으로 +유지하며 shell history나 CI log에 token을 출력하지 않습니다. -## 4. Vault core +## 4. Vault foundation -초기 root token을 shell history에 직접 적지 않습니다. +Foundation은 초기 root token으로 한 번 적용합니다. ```bash export TF_VAR_vault_addr="$VAULT_ADDR" @@ -71,24 +97,89 @@ export TF_VAR_vault_token="$( )" make terraform-plan \ - TF_ROOT=vault-core \ - BACKEND_CONFIG=.local/terraform-backend/dev-k3s/vault-core.s3.hcl + TF_ROOT=vault-foundation \ + BACKEND_CONFIG=.local/terraform-backend/dev-k3s/vault-foundation.s3.hcl make terraform-apply \ - TF_ROOT=vault-core \ - BACKEND_CONFIG=.local/terraform-backend/dev-k3s/vault-core.s3.hcl \ - APPROVE_APPLY=dev-k3s/vault-core + TF_ROOT=vault-foundation \ + BACKEND_CONFIG=.local/terraform-backend/dev-k3s/vault-foundation.s3.hcl \ + APPROVE_APPLY=dev-k3s/vault-foundation ``` -plan에서 mount, auth backend, policy, role, JWT Transit key 이외 객체가 -나오면 apply하지 않습니다. +Plan에는 mounts, auth configuration, workloads/database automation +policy와, OIDC/JWT를 명시적으로 구성한 경우에만 분리된 CI JWT role이 +있어야 합니다. Workload runtime policy, workload Kubernetes role, +application Transit key, database connection이 보이면 중단합니다. -## 5. Runtime secret seed +CI JWT를 구성할 때 workloads/database exact claim map은 repository와 +protected ref를 묶고, 최소 한 공통 job discriminator key에 서로 다른 값을 +가져야 합니다. 실제 issuer token payload로 그 claim을 확인하지 못하면 +OIDC/JWT 입력을 비워 둡니다. -Secret 값은 Git/Terraform을 통과하지 않습니다. 아래 변수는 terminal -session에만 유지합니다. +CI JWT auth를 구성했다면 Foundation이 만든 두 delegated identity에 실제 +로그인해 허용/거부 capability를 확인합니다. 구성하지 않았다면 정책 +capability를 검사하고 승인된 관리자가 발급한 bootstrap용 short-lived +token을 사용합니다. + +- Workloads identity는 승인된 workload policy/role와 + `transit/keys/project-auth-jwt`만 변경할 수 있어야 합니다. +- Database identity는 승인된 `database/config`와 `database/roles` 경로만 + 변경할 수 있어야 합니다. +- 둘 다 mount, auth backend, 임의 policy, token 발급 경로를 변경할 수 + 없어야 합니다. + +장기 token이나 임의 AppRole을 대신 만들지 않습니다. 실제 Gitea +OIDC/JWT가 검증되지 않았다면 승인된 관리자가 발급한 short-lived bootstrap +token을 사용합니다. + +## 5. Vault workload access + +`TF_VAR_vault_token`을 workloads 전용 short-lived token으로 교체한 뒤 +적용합니다. 다음은 OIDC/JWT를 아직 구성하지 않은 빈 dev lab의 bootstrap +예시입니다. Root token의 child revocation에 묶이지 않도록 짧은 TTL orphan +token을 발급하며, routine automation에서는 사용하지 않습니다. ```bash +export VAULT_WORKLOADS_TOKEN="$( + VAULT_TOKEN="$TF_VAR_vault_token" \ + vault token create \ + -orphan \ + -no-default-policy \ + -renewable=false \ + -explicit-max-ttl=2h \ + -policy=vault-workloads-automation-dev \ + -ttl=2h \ + -format=json | + jq -r '.auth.client_token' +)" +export TF_VAR_vault_token="$VAULT_WORKLOADS_TOKEN" + +make terraform-plan \ + TF_ROOT=vault-workloads \ + BACKEND_CONFIG=.local/terraform-backend/dev-k3s/vault-workloads.s3.hcl + +make terraform-apply \ + TF_ROOT=vault-workloads \ + BACKEND_CONFIG=.local/terraform-backend/dev-k3s/vault-workloads.s3.hcl \ + APPROVE_APPLY=dev-k3s/vault-workloads +``` + +Plan에는 workload ACL, Kubernetes auth role와 `project-auth-jwt` Transit +key만 있어야 합니다. `auth-system-dev`와 `auth-dev`의 정확한 +ServiceAccount binding과 audience `vault`를 검토합니다. Vault를 사용하지 +않는 `api-dev`에는 role이 생성되지 않아야 합니다. + +## 6. Runtime secret seed + +Secret 값은 Git/Terraform을 통과하지 않습니다. 아래 변수는 operator +terminal session에만 유지합니다. 빈 dev lab의 최초 seed는 initial root +token을 잠깐 사용하고 seed 직후 CLI 환경에서 제거합니다. + +```bash +export VAULT_TOKEN="$( + jq -r '.root_token' .local/vault/dev-k3s-init.json +)" + read -r -s -p "PostgreSQL superuser password: " POSTGRES_SUPERUSER_PASSWORD echo read -r -s -p "Auth database password: " AUTH_DB_PASSWORD @@ -106,47 +197,72 @@ chmod 0600 "$secret_file" jq -n --arg password "$POSTGRES_SUPERUSER_PASSWORD" \ '{POSTGRES_SUPERUSER_PASSWORD: $password}' >"$secret_file" -vault kv put kv/dev/platform/postgres/superuser @"$secret_file" +vault kv put kv/dev/systems/auth-system/postgres/superuser @"$secret_file" jq -n \ --arg password "$AUTH_DB_PASSWORD" \ '{AUTH_DB_PASSWORD: $password, APP_DATASOURCE_USERNAME: "project_auth", APP_DATASOURCE_PASSWORD: $password}' >"$secret_file" -vault kv put kv/dev/platform/postgres/auth-server @"$secret_file" +vault kv put kv/dev/systems/auth-system/postgres/auth-server @"$secret_file" jq -n --arg password "$KEYCLOAK_DB_PASSWORD" \ '{KEYCLOAK_DB_PASSWORD: $password}' >"$secret_file" -vault kv put kv/dev/platform/postgres/keycloak @"$secret_file" +vault kv put kv/dev/systems/auth-system/postgres/keycloak @"$secret_file" jq -n --arg password "$KEYCLOAK_ADMIN_PASSWORD" \ '{KC_BOOTSTRAP_ADMIN_PASSWORD: $password}' >"$secret_file" -vault kv put kv/dev/platform/keycloak/bootstrap-admin @"$secret_file" +vault kv put kv/dev/systems/auth-system/keycloak/bootstrap-admin @"$secret_file" jq -n --arg secret "$KEYCLOAK_CLIENT_SECRET" \ '{KEYCLOAK_CLIENT_SECRET: $secret, APP_SECURITY_OAUTH2_KEYCLOAK_CLIENT_SECRET: $secret}' >"$secret_file" -vault kv put kv/dev/platform/keycloak/client-auth-server @"$secret_file" +vault kv put kv/dev/workloads/auth-server/keycloak-client @"$secret_file" rm -f "$secret_file" trap - EXIT +unset VAULT_TOKEN ``` -PostgreSQL이 Vault Agent 주입 후 시작하는지 확인합니다. +## 7. Vault Agent Injector gate + +Vault auth, workload policy/role와 secret metadata를 확인한 뒤 Git PR에서 +`vault-agent-injector` element만 `autoSync: "true"`로 바꿉니다. Merge 후 +injector Deployment와 webhook health를 확인합니다. 이 단계에서도 +`auth-system`과 두 workload gate는 닫혀 있어야 합니다. + +## 8. Auth system gate + +Secret metadata와 workload policy를 확인한 뒤 Git PR에서 `auth-system` +inventory element만 `autoSync: "true"`로 바꿉니다. PR에는 현재 전체 Argo +diff를 첨부합니다. Child manifest를 직접 apply하거나 Argo UI에서 임의로 +Sync하지 않습니다. + +Merge 후 PostgreSQL과 Keycloak health를 확인합니다. ```bash -kubectl -n platform rollout status statefulset/postgres --timeout=600s +kubectl -n auth-system-dev rollout status statefulset/postgres --timeout=600s +kubectl -n auth-system-dev rollout status deployment/keycloak --timeout=600s ``` -## 6. Vault database state +## 9. Vault database state -초기 root token으로 TTL이 짧은 database 전용 token을 발급합니다. +`TF_VAR_vault_token`을 database 전용 short-lived token으로 교체하고 +PostgreSQL credential을 실행 시점에만 전달합니다. ```bash -export TF_VAR_vault_token="$( - vault token create \ - -policy=vault-database-automation-dev \ - -ttl=30m \ - -format=json | +export VAULT_DATABASE_TOKEN="$( + VAULT_TOKEN="$( + jq -r '.root_token' .local/vault/dev-k3s-init.json + )" \ + vault token create \ + -orphan \ + -no-default-policy \ + -renewable=false \ + -explicit-max-ttl=2h \ + -policy=vault-database-automation-dev \ + -ttl=2h \ + -format=json | jq -r '.auth.client_token' )" +export TF_VAR_vault_token="$VAULT_DATABASE_TOKEN" export TF_VAR_postgres_admin_password="$POSTGRES_SUPERUSER_PASSWORD" export TF_VAR_postgres_admin_password_version=1 @@ -160,50 +276,66 @@ make terraform-apply \ APPROVE_APPLY=dev-k3s/vault-database ``` -## 7. Root token 폐기 +Plan에는 `database/config/auth-system-postgres-dev` connection과 +`auth-db-migration-dev` dynamic role만 있어야 합니다. Dynamic credential +발급과 revoke를 검증합니다. -Kubernetes auth operator login이 동작하는지 먼저 검증합니다. +## 10. Root token 폐기 -```bash -operator_jwt="$( - kubectl -n vault create token vault-operator \ - --audience=vault \ - --duration=10m -)" -operator_token="$( - VAULT_TOKEN= vault write \ - -format=json \ - auth/kubernetes/login \ - role=vault-operator-dev \ - jwt="$operator_jwt" | - jq -r '.auth.client_token' -)" -VAULT_TOKEN="$operator_token" vault token lookup >/dev/null -``` +Database state와 delegated login/recovery 절차를 검증한 뒤, application +gate를 열기 전에 initial root token을 폐기합니다. +Script는 두 replacement token을 반드시 요구하고 다음을 자동 검증합니다. -검증 후 initial root token을 폐기합니다. +- `VAULT_WORKLOADS_TOKEN`이 `sys/policies/acl/auth-server-dev`에 `update` + capability를 가짐 +- `VAULT_DATABASE_TOKEN`이 + `database/config/auth-system-postgres-dev`에 `update` capability를 가짐 +- 어느 replacement token도 `root` policy를 갖지 않음 + +이후 foundation을 다시 적용할 routine administrator는 없습니다. Encrypted +unseal custody의 담당자와 승인된 Vault generated-root recovery 절차를 +확인할 수 없으면 root를 폐기하지 않습니다. ```bash ./hack/vault-init.sh revoke-root -unset operator_jwt operator_token + +VAULT_TOKEN="$VAULT_WORKLOADS_TOKEN" vault token revoke -self +VAULT_TOKEN="$VAULT_DATABASE_TOKEN" vault token revoke -self + unset TF_VAR_vault_token TF_VAR_postgres_admin_password +unset VAULT_WORKLOADS_TOKEN VAULT_DATABASE_TOKEN unset POSTGRES_SUPERUSER_PASSWORD AUTH_DB_PASSWORD KEYCLOAK_DB_PASSWORD unset KEYCLOAK_ADMIN_PASSWORD KEYCLOAK_CLIENT_SECRET ``` -encrypted custody로 옮긴 init material의 local working copy는 조직의 -dev recovery 정책에 따라 제거합니다. +`revoke-root`는 local init JSON에서도 root token field를 제거합니다. 두 +bootstrap orphan token도 검증 직후 self-revoke하며 routine credential로 +재사용하지 않습니다. -## 8. 확인 +## 11. Workload gates + +Keycloak realm/client sync와 database migration credential이 준비된 것을 +확인한 뒤 단계별 PR을 사용합니다. + +1. `auth-server`만 `autoSync: "true"`로 변경합니다. +2. `auth-dev/ghcr-regcred` Secret 생성, migration hook 성공과 Deployment + health를 확인합니다. +3. `api-server`만 `autoSync: "true"`로 변경합니다. +4. `api-dev/ghcr-regcred` Secret 생성, north-south와 service-to-service + 경로를 확인합니다. + +한 PR에서 모든 gate를 동시에 열지 않습니다. + +## 12. 최종 확인 ```bash kubectl -n argocd get applications kubectl -n vault get pods -kubectl -n platform get pods +kubectl -n auth-system-dev get pods kubectl -n auth-dev get pods kubectl -n api-dev get pods ``` -모든 Application의 sync/health를 확인하고 DB migration 및 Keycloak client -sync hook 결과를 검토합니다. 실패한 hook을 고치기 위해 child manifest를 -직접 apply하지 말고 Git PR을 사용합니다. +Encrypted custody로 옮긴 init material의 local working copy는 조직의 dev +recovery 정책에 따라 제거합니다. 실패를 고치기 위해 live child manifest를 +직접 수정하지 말고 Git PR을 사용합니다. diff --git a/docs/runbooks/terraform-state-migration.md b/docs/runbooks/terraform-state-migration.md index 01fdb4e..1a460fa 100644 --- a/docs/runbooks/terraform-state-migration.md +++ b/docs/runbooks/terraform-state-migration.md @@ -1,88 +1,244 @@ -# Terraform v2 state migration +# Terraform state migration to three Vault states -새 클러스터에는 이 runbook이 필요하지 않습니다. legacy local state 또는 -이전 `provider-foundation`, `workload-foundation`, `workload-config`, -`database-config` remote state가 실제로 존재할 때만 수행합니다. +새 클러스터에는 이 runbook이 필요하지 않습니다. Legacy `vault-core` 또는 +더 오래된 `provider-foundation`, `workload-foundation`, `workload-config`, +`database-config` state가 실제로 존재할 때만 사용합니다. -State 이동은 live object 삭제보다 위험할 수 있습니다. maintenance window와 -독립 backup 없이 진행하지 않습니다. +> 2026-07-26 리팩터링에서는 이 절차를 실제 backend나 cluster에 실행하지 +> 않았습니다. Maintenance window, 독립 backup과 승인 없이 시작하지 +> 않습니다. -## 목표 +State 이동은 live object 삭제보다 위험할 수 있습니다. State split, +Terraform module refactor, Vault KV path/namespace cutover를 한 apply에 +섞지 않습니다. -| 이전 state | 목표 | +## 목표 ownership + +| Legacy ownership | 목표 state | |---|---| -| provider Transit Vault state | archive 후 provider Vault 폐기 절차에서 별도 처리 | -| workload foundation | `vault-core`의 기준 state | -| workload config | 소유 객체를 `vault-core`로 이동 | -| database config | `vault-database`로 backend key migration | +| `vault-core`의 mounts/auth/delegation 객체 | `vault-foundation` | +| `vault-core` 또는 `workload-config`의 workload policy/role와 Transit key | `vault-workloads` | +| `vault-database` 또는 `database-config`의 auth-system connection과 migration role | `vault-database` | +| 별도 same-cluster Transit provider Vault | State archive 후 별도 decommission 절차 | -동일 클러스터 Transit Vault는 더 이상 desired state가 아닙니다. Terraform -state에서 먼저 삭제하거나 destroy하지 않습니다. snapshot과 seal dependency -해제 확인 후 별도 decommission 승인을 받아 처리합니다. - -## 1. Inventory와 backup - -모든 operator machine, runner, remote backend에서 state 위치를 확인합니다. - -```bash -find . -type f \ - \( -name 'terraform.tfstate*' -o -name '*.tfplan' \) \ - -not -path './.git/*' -``` - -각 state를 `terraform state pull`로 encrypted offline custody에 저장하고 -checksum을 기록합니다. backup에는 secret data가 포함될 수 있습니다. - -## 2. Backend key migration - -`workload-foundation` backend에 연결한 상태에서 새 `vault-core` backend -configuration으로 `terraform init -migrate-state`를 수행합니다. -`database-config`도 같은 방식으로 `vault-database` key로 이동합니다. - -실제 backend 파일과 이전 key는 환경마다 다르므로 명령에 값을 하드코딩하지 -않습니다. migration 전후 `terraform state pull` checksum과 `state list`를 -비교합니다. - -## 3. Workload configuration ownership 이동 - -이전 `workload-config` state의 다음 객체를 `vault-core` state의 선언된 -address로 이동합니다. - -- application Vault policies -- workload Kubernetes auth roles - -`terraform state mv -state= -state-out=`를 -사용해 offline copy에서 먼저 연습합니다. target address는 현재 -`module.workload_policies`와 `module.workload_roles`의 `terraform state list` -결과를 기준으로 합니다. resource 이름을 추측하지 않습니다. - -이동 후 두 state 모두 plan합니다. - -- `vault-core`: 변경 없음 또는 address-only 이동 -- legacy workload-config: 삭제할 live object 없음 - -두 plan 중 하나라도 destroy를 제안하면 중단하고 backup state를 복원합니다. - -## 4. Database state - -기존 database state가 없고 live Vault 객체만 존재할 때만 다음 import ID를 -사용합니다. +목표 backend key는 각각 달라야 합니다. ```text -vault_database_secret_backend_connection.platform_postgres database/config/platform-postgres-dev -vault_database_secret_backend_role.auth_db_migration database/roles/auth-db-migration-dev -vault_database_secret_backend_role.postgres_operator database/roles/postgres-operator-dev +dev-k3s/vault-foundation.tfstate +dev-k3s/vault-workloads.tfstate +dev-k3s/vault-database.tfstate ``` -이미 다른 state에 address가 있으면 import하지 말고 state ownership을 먼저 +동일 Vault API path가 두 state에 동시에 남아 있으면 cutover가 끝난 것이 +아닙니다. State끼리 `terraform_remote_state`를 추가하지 않습니다. + +## 1. Freeze, inventory, backup + +모든 Terraform apply와 관련 image/config promotion을 중단합니다. +ApplicationSet의 injector, `auth-system`, `auth-server`, `api-server` +autoSync gate도 닫습니다. 이 gate는 Argo의 자동 sync만 멈추며 이미 실행 +중인 Pod의 재시작, node drain 또는 controller 동작을 막지 않습니다. +Migration 중 legacy path를 병행 유지하고, 완전한 quiesce가 필요하면 +workload별 scale/maintenance 절차를 별도로 승인합니다. + +각 legacy backend에서 다음을 확보합니다. + +- `terraform state pull` 원본 +- state checksum +- `terraform state list` +- 이동할 각 address의 `terraform state show` +- 현재 Vault object ID/path와 provider version + +Backup에는 credential과 secret data가 포함될 수 있으므로 encrypted +offline custody에 보관합니다. Backend lock이 작동하는지 확인하고 source +state를 수정할 runner를 하나로 제한합니다. + +## 2. Migration code 준비 + +먼저 실제로 배포된 legacy Git revision에서 임시 migration branch를 +만듭니다. 그 revision의 policy 문서, namespace binding, role payload를 +그대로 유지한 `vault-workloads` root와 import block만 추가합니다. 현재 +branch의 새 KV path와 `auth-system-dev` binding을 이 단계에 복사하면 +ownership 이동과 live policy 변경이 섞이므로 사용할 수 없습니다. + +Source와 destination은 같은 legacy object payload를 선언해야 합니다. +실제 import ID는 backup의 `state show`로 확정하며 이름을 추측하지 +않습니다. Ownership split이 양쪽 no-op으로 끝난 뒤에만 현재 desired +revision을 별도 path/namespace cutover PR로 적용합니다. + +일반적인 import ID 형식은 다음과 같습니다. + +```text +vault_policy +vault_kubernetes_auth_backend_role auth/kubernetes/role/ +vault_transit_secret_backend_key transit/keys/project-auth-jwt +``` + +Legacy core/foundation source에는 이동 대상별 `removed` block을 둡니다. + +```hcl +removed { + from = module.workload_policies + + lifecycle { + destroy = false + } +} + +removed { + from = module.workload_roles + + lifecycle { + destroy = false + } +} + +removed { + from = vault_transit_secret_backend_key.project_auth_jwt + + lifecycle { + destroy = false + } +} +``` + +실제 address가 다르면 현재 state list를 사용합니다. 위 예를 그대로 +복사하지 않습니다. + +Module 구조 개선은 아직 하지 않습니다. 우선 기존 address/선언으로 +ownership만 옮기고 후속 PR에서 `moved` block을 사용합니다. + +## 3. 양쪽 plan 검토 + +Source와 destination을 같은 revision에서 plan합니다. + +| Plan | 허용 결과 | +|---|---| +| Source core/foundation | 대상 object를 state에서만 제거, live destroy `0` | +| Destination workloads | 기존 live object import, create/change/destroy `0` | + +둘 중 하나라도 live create, update, delete를 제안하면 중단합니다. Policy +내용이나 Vault path rename은 이 단계에 포함하지 않습니다. + +## 4. Workload ownership split + +승인된 maintenance window에서 다음 순서로 진행합니다. + +1. Source의 `removed { destroy = false }` apply +2. 즉시 destination import apply +3. 양쪽 `state list`에서 object가 정확히 한 번만 나타나는지 확인 +4. 양쪽 plan이 no-op인지 확인 + +Manual remote `state push`나 offline `state mv -state/-state-out`을 primary +절차로 사용하지 않습니다. Source 제거와 destination import 사이에 문제가 +생기면 다른 apply를 진행하지 말고 backup과 승인된 rollback 절차를 +사용합니다. + +## 5. Foundation backend key 전환 + +Workload object를 분리한 뒤 남은 legacy `vault-core` state 전체를 +`vault-foundation` backend key로 `terraform init -migrate-state` 합니다. +Migration 전후의 state list와 serial/lineage, checksum을 기록합니다. + +`vault-database`의 state 경계는 유지하지만 connection address와 live +object 이름은 모두 바뀝니다. + +```text +vault_database_secret_backend_connection.platform_postgres + -> vault_database_secret_backend_connection.auth_system_postgres + +database/config/platform-postgres-dev + -> database/config/auth-system-postgres-dev +``` + +Checked-in `moved` block은 Terraform address만 이관합니다. Vault connection +이름 변경은 replacement이며 `create_before_destroy`도 생성/삭제 사이에 +operator 승인 대기 시간을 만들지 않습니다. 기존 cluster에서는 final +configuration을 바로 apply하지 말고 별도 blue/green cutover를 준비합니다. + +1. 임시 migration revision에서 old connection을 유지하고 new connection을 + 별도 resource로 추가합니다. +2. Database runner policy가 maintenance window 동안 old/new 두 exact + `database/config` path를 모두 허용하게 합니다. +3. New connection 검증 뒤 `auth-db-migration-dev` role을 new connection으로 + 전환하고 credential issue/revoke를 시험합니다. +4. Old connection에 연결된 lease를 inventory하고 만료 또는 명시적 revoke를 + 확인합니다. +5. 후속 승인에서 old connection과 임시 policy path를 제거합니다. + +기존 이름이 `database-config`이거나 backend key가 다를 때는 이 cutover와 +분리해 `vault-database` backend key로 migration합니다. + +Database object를 새로 import해야 하는 경우의 target address와 ID는 +다음과 같습니다. + +```text +vault_database_secret_backend_connection.auth_system_postgres + database/config/auth-system-postgres-dev +vault_database_secret_backend_role.auth_db_migration + database/roles/auth-db-migration-dev +``` + +이미 다른 state에 address가 있으면 import하지 말고 ownership을 먼저 이동합니다. -## 5. Cutover 완료 조건 +## 6. Retired broad/unused access -- 두 목표 state가 remote backend와 locking을 사용 -- 동일 Vault path가 두 state list에 나타나지 않음 -- plan에 예상하지 않은 create/delete가 없음 -- legacy state와 backup은 immutable archive -- repo와 runner에 local state/provider directory가 없음 +다음 legacy object는 새 state의 desired ownership이 아닙니다. -검증이 끝나기 전 legacy backend를 삭제하지 않습니다. +- Broad `platform-admin-dev` policy와 이를 사용한 `vault-operator-dev` role +- Legacy 단일 `project-gitops-dev` CI JWT role +- 미사용 `keycloak-operator-dev`, `postgres-operator-dev` policies +- 미사용 `database/roles/postgres-operator-dev` dynamic role + +State split 중 자동 destroy하지 않습니다. 우선 `removed { destroy = false }` +로 legacy source ownership에서 분리하고, Vault audit/consumer inventory로 +사용자가 없음을 확인합니다. Token/lease revoke와 live object 삭제는 +별도 보안 decommission 승인으로 수행합니다. + +기존 `jwt-ci` auth mount가 state에 있으면 ownership 이동 동안 실제 +issuer/discovery 입력을 유지합니다. 입력을 누락해 `count = 0`이 되어도 +`prevent_destroy`가 mount 삭제를 차단해야 합니다. Mount와 그 하위 role을 +제거할 때만 token/accessor와 consumer를 확인한 별도 decommission +revision에서 명시적으로 보호를 해제합니다. + +## 7. Vault path와 namespace cutover + +State split이 no-op인 것을 확인한 뒤 별도 PR/maintenance window에서 +다음 legacy path를 새 owner path로 이관합니다. + +```text +kv/dev/platform/postgres/* -> kv/dev/systems/auth-system/postgres/* +kv/dev/platform/keycloak/bootstrap-admin + -> kv/dev/systems/auth-system/keycloak/bootstrap-admin +kv/dev/platform/keycloak/client-auth-server + -> kv/dev/workloads/auth-server/keycloak-client +``` + +KV payload는 Terraform으로 이동하지 않습니다. 승인된 operator가 값을 +노출하지 않는 secret procedure로 새 path에 기록하고 metadata/version을 +확인합니다. Policy, Kubernetes role, Agent annotation, namespace/DNS +변경을 render와 Vault capability test로 검증합니다. + +`platform`에서 `auth-system-dev`로의 live namespace 이동은 Kubernetes +state/data migration입니다. Terraform state split과 별도로 backup, +non-cascading ownership transfer, rollback 계획을 가져야 합니다. + +새 consumer가 정상 동작하고 rollback 기간이 끝날 때까지 legacy KV value와 +policy를 삭제하지 않습니다. 삭제는 별도 승인 작업입니다. + +## 8. 완료 조건 + +- `vault-foundation`, `vault-workloads`, `vault-database`가 서로 다른 remote + backend와 lock을 사용 +- 각 Vault API path가 정확히 한 state list에만 존재 +- 세 plan에 예상하지 않은 create/change/delete가 없음 +- Delegated state가 자신의 automation policy/login role을 소유하지 않음 +- Workload/database identity의 허용·거부 capability test 통과 +- Legacy state와 backup이 immutable archive에 있음 +- Repository와 runner working directory에 local state, plan, provider + directory가 없음 +- New KV path와 `auth-system-dev` cutover 전에는 관련 autoSync gate가 닫힘 + +검증이 끝나기 전 legacy backend, Vault path, namespace 또는 PVC를 +삭제하지 않습니다. diff --git a/hack/bootstrap-argocd.sh b/hack/bootstrap-argocd.sh index d49e245..cc74069 100755 --- a/hack/bootstrap-argocd.sh +++ b/hack/bootstrap-argocd.sh @@ -53,7 +53,9 @@ kubectl --context "$expected_context" -n argocd \ rollout status deployment/argocd-server --timeout=300s kubectl --context "$expected_context" -n argocd \ rollout status deployment/argocd-repo-server --timeout=300s +kubectl --context "$expected_context" apply \ + -f "${REPO_ROOT}/bootstrap/argocd/control-plane-project.yaml" kubectl --context "$expected_context" apply \ -f "${REPO_ROOT}/bootstrap/argocd/root-application.yaml" -echo "Argo CD ${ARGOCD_VERSION} and the dev-k3s root Application are installed." +echo "Argo CD ${ARGOCD_VERSION}, its control-plane project, and the root Application are installed." diff --git a/hack/validate.sh b/hack/validate.sh index b6b513e..a95be2f 100755 --- a/hack/validate.sh +++ b/hack/validate.sh @@ -15,7 +15,7 @@ done cd "$REPO_ROOT" forbidden_files="$( - git ls-files | + git ls-files --cached --others --exclude-standard | rg '(^|/)(\\.terraform|\\.terraform-state|\\.local)(/|$)|(^|/)terraform\\.tfstate($|\\.)|\\.tfplan$' || true )" @@ -36,19 +36,106 @@ while IFS= read -r script; do fi done < <(rg --files hack -g '*.sh') -jq empty platform/auth-system/base/files/keycloak/project-auth-realm.json +jq empty renovate.json +jq empty systems/auth-system/base/files/keycloak/project-auth-realm.json overlays=( - clusters/dev-k3s - clusters/dev-k3s/manifests/api-server - clusters/dev-k3s/manifests/auth-server - clusters/dev-k3s/manifests/auth-system - clusters/dev-k3s/manifests/vault + bootstrap/argocd + platform/control-plane/argocd + clusters/dev-k3s/overlays/platform/vault + clusters/dev-k3s/overlays/systems/auth-system + clusters/dev-k3s/overlays/workloads/auth-server + clusters/dev-k3s/overlays/workloads/api-server ) for overlay in "${overlays[@]}"; do kubectl kustomize "$overlay" >/dev/null done +control_plane_render="$(kubectl kustomize platform/control-plane/argocd)" +if [[ "$(rg -c '^kind: AppProject$' <<<"$control_plane_render")" -ne 4 || + "$(rg -c '^kind: ApplicationSet$' <<<"$control_plane_render")" -ne 4 ]]; then + echo "The control plane must render exactly four AppProjects and four ApplicationSets." >&2 + exit 1 +fi + +application_manifests="$( + rg -l '^kind:[[:space:]]+Application$' \ + --glob '*.yaml' \ + --glob '*.yml' \ + . | + sort +)" +if [[ "$application_manifests" != "./bootstrap/argocd/root-application.yaml" ]]; then + echo "Only the bootstrap root may be an explicit Argo CD Application:" >&2 + echo "$application_manifests" >&2 + exit 1 +fi + +if ! rg -q '^[[:space:]]+project:[[:space:]]+gitops-control-plane$' \ + bootstrap/argocd/root-application.yaml || + rg -n '^[[:space:]]+project:[[:space:]]+default$' bootstrap platform/control-plane; then + echo "The root and generated Applications must use explicit least-privilege AppProjects." >&2 + exit 1 +fi + +applicationsets=( + platform/control-plane/argocd/application-sets/platform-addons.yaml + platform/control-plane/argocd/application-sets/platform-services.yaml + platform/control-plane/argocd/application-sets/systems.yaml + platform/control-plane/argocd/application-sets/workloads.yaml +) +for applicationset in "${applicationsets[@]}"; do + for safety_setting in \ + 'missingkey=error' \ + 'applicationsSync: create-update' \ + 'preserveResourcesOnDeletion: true' \ + 'Prune=confirm,Delete=confirm'; do + if ! rg -q "$safety_setting" "$applicationset"; then + echo "${applicationset} is missing ApplicationSet safety setting: ${safety_setting}" >&2 + exit 1 + fi + done +done + +if ! rg -q '^ - Prune=confirm$' \ + platform/control-plane/argocd/application-sets/platform-addons.yaml; then + echo "Platform addons must require approval before pruning chart resources." >&2 + exit 1 +fi + +if rg -n 'project:[[:space:]]+"?\{\{' platform/control-plane/argocd/application-sets; then + echo "ApplicationSet projects are privilege boundaries and must never be templated." >&2 + exit 1 +fi + +git_applicationsets=( + platform/control-plane/argocd/application-sets/platform-services.yaml + platform/control-plane/argocd/application-sets/systems.yaml + platform/control-plane/argocd/application-sets/workloads.yaml +) +for applicationset in "${git_applicationsets[@]}"; do + if ! rg -q '^[[:space:]]+targetRevision:[[:space:]]+main$' "$applicationset" || + rg -q '^[[:space:]]+(repoURL|targetRevision):[[:space:]]+"?\{\{' "$applicationset"; then + echo "${applicationset} must pin the canonical repository main branch in its template." >&2 + exit 1 + fi +done + +enabled_gates="$( + rg -o 'autoSync: "true"' platform/control-plane/argocd/application-sets | + wc -l | + tr -d ' ' +)" +disabled_gates="$( + rg -o 'autoSync: "false"' platform/control-plane/argocd/application-sets | + wc -l | + tr -d ' ' +)" +if [[ "$enabled_gates" -ne 2 || "$disabled_gates" -ne 4 ]]; then + echo "Initial sync gates must enable only Sealed Secrets and Vault." >&2 + exit 1 +fi + sealed_secrets_render="$( helm template sealed-secrets sealed-secrets \ --repo https://bitnami.github.io/sealed-secrets \ @@ -85,8 +172,8 @@ fi rendered_images="$( { - kubectl kustomize clusters/dev-k3s/manifests/auth-system - kubectl kustomize clusters/dev-k3s/manifests/vault + kubectl kustomize clusters/dev-k3s/overlays/systems/auth-system + kubectl kustomize clusters/dev-k3s/overlays/platform/vault } | rg '^[[:space:]]+image: (hashicorp/vault|postgres|quay\\.io/keycloak)' )" if printf '%s\n' "$rendered_images" | rg -v '@sha256:[a-f0-9]{64}$'; then @@ -94,9 +181,26 @@ if printf '%s\n' "$rendered_images" | rg -v '@sha256:[a-f0-9]{64}$'; then exit 1 fi +workload_kustomizations=( + clusters/dev-k3s/overlays/workloads/auth-server/kustomization.yaml + clusters/dev-k3s/overlays/workloads/api-server/kustomization.yaml +) +for workload_kustomization in "${workload_kustomizations[@]}"; do + if rg -q '^[[:space:]]+digest:[[:space:]]+sha256:[a-f0-9]{64}$' \ + "$workload_kustomization"; then + continue + fi + if ! rg -q '^[[:space:]]+newTag:[[:space:]]+[a-f0-9]{7,40}$' \ + "$workload_kustomization"; then + echo "${workload_kustomization} must use a verified digest or a temporary commit-shaped tag." >&2 + exit 1 + fi +done + terraform fmt -check -recursive iac/terraform terraform_roots=( - iac/terraform/live/dev-k3s/vault-core + iac/terraform/live/dev-k3s/vault-foundation + iac/terraform/live/dev-k3s/vault-workloads iac/terraform/live/dev-k3s/vault-database ) for root in "${terraform_roots[@]}"; do @@ -110,12 +214,47 @@ for root in "${terraform_roots[@]}"; do done if rg -n \ - 'github\\.com/DongHyeonka/Project-Auth-GitOps|bitnami-labs\\.github\\.io/sealed-secrets|/home/donghyeon/dev/Project-Auth-GitOps|terraform/vault(-transit)?/(dev|reconcile)' \ + 'github\\.com/DongHyeonka/Project-Auth-GitOps|bitnami-labs\\.github\\.io/sealed-secrets|/home/donghyeon/dev/Project-Auth-GitOps|terraform/vault(-transit)?/(dev|reconcile)|clusters/dev-k3s/manifests|platform/auth-system|platform/security/vault|platform-config|postgres\\.platform\\.svc|keycloak(-public)?\\.platform\\.svc|kv/data/dev/platform' \ --glob '!docs/archive/**' \ + --glob '!docs/runbooks/terraform-state-migration.md' \ --glob '!policies/legacy/**' \ --glob '!hack/validate.sh' \ .; then - echo "Current files contain a legacy repository URL, Helm URL, absolute path, or Terraform root." >&2 + echo "Current files contain a legacy URL, path, namespace, or Vault secret path." >&2 + exit 1 +fi + +if rg -n '^path[[:space:]]+"[^"]*[+*]' policies/vault/dev-k3s/workloads; then + echo "Workload Vault policies must use exact paths; wildcard paths require a security review." >&2 + exit 1 +fi + +automation_policies=( + policies/vault/dev-k3s/platform/vault-workloads-automation-dev.hcl + policies/vault/dev-k3s/platform/vault-database-automation-dev.hcl +) +for policy in "${automation_policies[@]}"; do + for self_path in \ + 'sys/capabilities-self' \ + 'auth/token/lookup-self' \ + 'auth/token/revoke-self'; do + if ! rg -q "^path \"${self_path}\"" "$policy"; then + echo "${policy} is missing required no-default-policy self service path: ${self_path}" >&2 + exit 1 + fi + done +done + +if rg -n \ + '^path[[:space:]]+"[^"]*[+*]|capabilities[[:space:]]*=.*"(sudo|list)"|^path[[:space:]]+"auth/token/(create|roles)' \ + policies/vault/dev-k3s/platform; then + echo "Delegated automation policies must not use wildcards, sudo/list, or token issuance paths." >&2 + exit 1 +fi + +if rg -n '^path[[:space:]]+"(sys/|auth/|database/config|database/roles)' \ + policies/vault/dev-k3s/workloads; then + echo "Runtime workload policies may not configure Vault control-plane objects." >&2 exit 1 fi @@ -124,7 +263,7 @@ if rg -n 'uses:[[:space:]]+[^#[:space:]]+@v[0-9]' .gitea/workflows; then exit 1 fi -if rg -n 'ApplyOutOfSyncOnly=true' bootstrap clusters; then +if rg -n 'ApplyOutOfSyncOnly=true' bootstrap clusters platform/control-plane; then echo "ApplyOutOfSyncOnly is incompatible with hook-based migrations and must not be enabled." >&2 exit 1 fi diff --git a/hack/vault-init.sh b/hack/vault-init.sh index 5f2a418..bd4d0e7 100755 --- a/hack/vault-init.sh +++ b/hack/vault-init.sh @@ -7,7 +7,7 @@ REPO_ROOT="$(cd "${SCRIPT_DIR}/.." && pwd)" VAULT_ADDR="${VAULT_ADDR:-http://127.0.0.1:8200}" VAULT_INIT_OUTPUT="${VAULT_INIT_OUTPUT:-${REPO_ROOT}/.local/vault/dev-k3s-init.json}" -for cmd in jq vault; do +for cmd in jq rg vault; do if ! command -v "$cmd" >/dev/null 2>&1; then echo "$cmd is required" >&2 exit 1 @@ -78,13 +78,56 @@ init() { } revoke_root() { + local database_lookup="" + local database_token="${VAULT_DATABASE_TOKEN:-}" local root_token="" local temporary="" + local workloads_lookup="" + local workloads_token="${VAULT_WORKLOADS_TOKEN:-}" if [[ ! -f "$VAULT_INIT_OUTPUT" ]]; then echo "Init material is unavailable: ${VAULT_INIT_OUTPUT}" >&2 exit 1 fi + if [[ -z "$workloads_token" || -z "$database_token" ]]; then + echo "Refusing root revocation: VAULT_WORKLOADS_TOKEN and VAULT_DATABASE_TOKEN are required." >&2 + exit 1 + fi + + if ! VAULT_TOKEN="$workloads_token" \ + vault token capabilities sys/policies/acl/auth-server-dev | + rg -q '(^|,|[[:space:]])update($|,|[[:space:]])'; then + echo "Refusing root revocation: the workloads replacement token failed its capability check." >&2 + exit 1 + fi + if ! VAULT_TOKEN="$database_token" \ + vault token capabilities database/config/auth-system-postgres-dev | + rg -q '(^|,|[[:space:]])update($|,|[[:space:]])'; then + echo "Refusing root revocation: the database replacement token failed its capability check." >&2 + exit 1 + fi + if ! workloads_lookup="$( + VAULT_TOKEN="$workloads_token" vault token lookup -format=json + )"; then + echo "Refusing root revocation: the workloads replacement token cannot look itself up." >&2 + exit 1 + fi + if ! database_lookup="$( + VAULT_TOKEN="$database_token" vault token lookup -format=json + )"; then + echo "Refusing root revocation: the database replacement token cannot look itself up." >&2 + exit 1 + fi + if ! jq -e '.data.policies | type == "array"' <<<"$workloads_lookup" >/dev/null || + ! jq -e '.data.policies | type == "array"' <<<"$database_lookup" >/dev/null; then + echo "Refusing root revocation: a replacement token returned an invalid lookup response." >&2 + exit 1 + fi + if jq -e '.data.policies | index("root") != null' <<<"$workloads_lookup" >/dev/null || + jq -e '.data.policies | index("root") != null' <<<"$database_lookup" >/dev/null; then + echo "Refusing root revocation: a replacement token unexpectedly carries the root policy." >&2 + exit 1 + fi root_token="$(jq -er '.root_token' "$VAULT_INIT_OUTPUT")" VAULT_TOKEN="$root_token" vault token revoke -self diff --git a/iac/terraform/backend/dev-k3s/vault-foundation.s3.hcl.example b/iac/terraform/backend/dev-k3s/vault-foundation.s3.hcl.example new file mode 100644 index 0000000..c65fc34 --- /dev/null +++ b/iac/terraform/backend/dev-k3s/vault-foundation.s3.hcl.example @@ -0,0 +1,5 @@ +bucket = "project-gitops-terraform-state" +key = "dev-k3s/vault-foundation.tfstate" +region = "us-east-1" +encrypt = true +use_lockfile = true diff --git a/iac/terraform/backend/dev-k3s/vault-core.s3.hcl.example b/iac/terraform/backend/dev-k3s/vault-workloads.s3.hcl.example similarity index 70% rename from iac/terraform/backend/dev-k3s/vault-core.s3.hcl.example rename to iac/terraform/backend/dev-k3s/vault-workloads.s3.hcl.example index b9dd22f..e431e33 100644 --- a/iac/terraform/backend/dev-k3s/vault-core.s3.hcl.example +++ b/iac/terraform/backend/dev-k3s/vault-workloads.s3.hcl.example @@ -1,5 +1,5 @@ bucket = "project-gitops-terraform-state" -key = "dev-k3s/vault-core.tfstate" +key = "dev-k3s/vault-workloads.tfstate" region = "us-east-1" encrypt = true use_lockfile = true diff --git a/iac/terraform/live/dev-k3s/vault-core/main.tf b/iac/terraform/live/dev-k3s/vault-core/main.tf deleted file mode 100644 index 2868576..0000000 --- a/iac/terraform/live/dev-k3s/vault-core/main.tf +++ /dev/null @@ -1,171 +0,0 @@ -terraform { - required_version = ">= 1.11.0" - - required_providers { - vault = { - source = "hashicorp/vault" - version = "~> 5.7.0" - } - } - - backend "s3" {} -} - -provider "vault" { - address = var.vault_addr - skip_child_token = true - token = var.vault_token -} - -locals { - policy_dir = "${path.module}/../../../../../policies/vault/dev-k3s" - - workload_policies = { - auth-server-dev = file("${local.policy_dir}/auth-server-dev.hcl") - auth-db-migration-dev = file("${local.policy_dir}/auth-db-migration-dev.hcl") - postgres-dev = file("${local.policy_dir}/postgres-dev.hcl") - keycloak-dev = file("${local.policy_dir}/keycloak-dev.hcl") - keycloak-client-sync-dev = file("${local.policy_dir}/keycloak-client-sync-dev.hcl") - postgres-operator-dev = file("${local.policy_dir}/postgres-operator-dev.hcl") - keycloak-operator-dev = file("${local.policy_dir}/keycloak-operator-dev.hcl") - } -} - -resource "vault_mount" "kv" { - path = var.kv_mount_path - type = "kv" - options = { - version = "2" - } - - lifecycle { - prevent_destroy = true - } -} - -resource "vault_mount" "database" { - path = var.database_mount_path - type = "database" - - lifecycle { - prevent_destroy = true - } -} - -resource "vault_mount" "transit" { - path = var.transit_mount_path - type = "transit" - - lifecycle { - prevent_destroy = true - } -} - -resource "vault_auth_backend" "kubernetes" { - path = var.kubernetes_auth_path - type = "kubernetes" -} - -resource "vault_kubernetes_auth_backend_config" "cluster" { - backend = vault_auth_backend.kubernetes.path - disable_iss_validation = true - disable_local_ca_jwt = false - kubernetes_host = var.kubernetes_host -} - -resource "vault_transit_secret_backend_key" "project_auth_jwt" { - backend = vault_mount.transit.path - name = var.jwt_transit_key_name - type = "rsa-2048" -} - -resource "vault_policy" "platform_admin" { - name = var.platform_admin_policy_name - policy = file("${local.policy_dir}/platform-admin-dev.hcl") -} - -resource "vault_policy" "database_automation" { - name = var.database_automation_policy_name - policy = file("${local.policy_dir}/vault-database-automation-dev.hcl") -} - -module "workload_policies" { - source = "../../../modules/vault-policy-set" - - policies = local.workload_policies -} - -resource "vault_kubernetes_auth_backend_role" "operator" { - audience = var.kubernetes_token_audience - backend = vault_auth_backend.kubernetes.path - bound_service_account_names = [var.operator_service_account_name] - bound_service_account_namespaces = [var.operator_service_account_namespace] - role_name = var.operator_role_name - token_policies = [vault_policy.platform_admin.name] - token_ttl = var.operator_token_ttl_seconds -} - -module "workload_roles" { - source = "../../../modules/vault-kubernetes-roles" - - backend = vault_auth_backend.kubernetes.path - roles = { - auth-server-dev = { - audiences = [var.kubernetes_token_audience] - service_account_names = ["auth-server"] - service_account_namespaces = ["auth-dev"] - token_policies = [module.workload_policies.names["auth-server-dev"]] - token_ttl = var.kubernetes_role_ttl_seconds - } - auth-db-migration-dev = { - audiences = [var.kubernetes_token_audience] - service_account_names = ["auth-db-migration"] - service_account_namespaces = ["auth-dev"] - token_policies = [module.workload_policies.names["auth-db-migration-dev"]] - token_ttl = var.kubernetes_role_ttl_seconds - } - postgres-dev = { - audiences = [var.kubernetes_token_audience] - service_account_names = ["postgres"] - service_account_namespaces = ["platform"] - token_policies = [module.workload_policies.names["postgres-dev"]] - token_ttl = var.kubernetes_role_ttl_seconds - } - keycloak-dev = { - audiences = [var.kubernetes_token_audience] - service_account_names = ["keycloak"] - service_account_namespaces = ["platform"] - token_policies = [module.workload_policies.names["keycloak-dev"]] - token_ttl = var.kubernetes_role_ttl_seconds - } - keycloak-client-sync-dev = { - audiences = [var.kubernetes_token_audience] - service_account_names = ["keycloak-client-sync"] - service_account_namespaces = ["platform"] - token_policies = [module.workload_policies.names["keycloak-client-sync-dev"]] - token_ttl = var.kubernetes_role_ttl_seconds - } - } -} - -resource "vault_jwt_auth_backend" "ci" { - count = var.ci_jwt_oidc_discovery_url == null ? 0 : 1 - - bound_issuer = var.ci_jwt_bound_issuer - oidc_discovery_url = var.ci_jwt_oidc_discovery_url - path = var.ci_jwt_auth_path -} - -resource "vault_jwt_auth_backend_role" "ci" { - count = var.ci_jwt_oidc_discovery_url == null ? 0 : 1 - - backend = vault_jwt_auth_backend.ci[0].path - bound_audiences = var.ci_jwt_bound_audiences - bound_claims = var.ci_jwt_bound_claims - bound_claims_type = "glob" - role_name = var.ci_jwt_role_name - role_type = "jwt" - token_explicit_max_ttl = var.ci_token_ttl_seconds - token_policies = [vault_policy.database_automation.name] - user_claim = var.ci_jwt_user_claim -} diff --git a/iac/terraform/live/dev-k3s/vault-core/variables.tf b/iac/terraform/live/dev-k3s/vault-core/variables.tf deleted file mode 100644 index 905c167..0000000 --- a/iac/terraform/live/dev-k3s/vault-core/variables.tf +++ /dev/null @@ -1,146 +0,0 @@ -variable "database_automation_policy_name" { - description = "Least-privilege policy used by the approved Vault database runner." - type = string - default = "vault-database-automation-dev" -} - -variable "ci_jwt_auth_path" { - description = "JWT auth mount used by external CI." - type = string - default = "jwt-ci" -} - -variable "ci_jwt_bound_audiences" { - description = "Accepted CI JWT audiences." - type = set(string) - default = [] -} - -variable "ci_jwt_bound_claims" { - description = "Claims that bind CI JWTs to the canonical repository and protected branch." - type = map(string) - default = {} -} - -variable "ci_jwt_bound_issuer" { - description = "Expected issuer for CI JWTs." - type = string - default = null - nullable = true -} - -variable "ci_jwt_oidc_discovery_url" { - description = "CI OIDC discovery URL. Null keeps JWT auth disabled until the issuer is confirmed." - type = string - default = null - nullable = true -} - -variable "ci_jwt_role_name" { - description = "Vault role used by the GitOps configuration workflow." - type = string - default = "project-gitops-dev" -} - -variable "ci_jwt_user_claim" { - description = "JWT claim used as the Vault identity alias." - type = string - default = "sub" -} - -variable "ci_token_ttl_seconds" { - description = "Maximum lifetime for a CI Vault token." - type = number - default = 3600 -} - -variable "database_mount_path" { - description = "Workload Vault database secrets mount path." - type = string - default = "database" -} - -variable "jwt_transit_key_name" { - description = "Transit key used for application JWT signing." - type = string - default = "project-auth-jwt" -} - -variable "kubernetes_auth_path" { - description = "Kubernetes auth backend path." - type = string - default = "kubernetes" -} - -variable "kubernetes_host" { - description = "Kubernetes TokenReview API address." - type = string - default = "https://kubernetes.default.svc.cluster.local:443" -} - -variable "kubernetes_role_ttl_seconds" { - description = "TTL for workload Kubernetes auth tokens." - type = number - default = 3600 -} - -variable "kubernetes_token_audience" { - description = "Audience used by projected service account tokens." - type = string - default = "vault" -} - -variable "kv_mount_path" { - description = "Workload Vault KV-v2 mount path." - type = string - default = "kv" -} - -variable "operator_role_name" { - description = "Workload Vault Kubernetes auth role for human operators." - type = string - default = "vault-operator-dev" -} - -variable "operator_service_account_name" { - description = "Service account authorized to open workload Vault operator sessions." - type = string - default = "vault-operator" -} - -variable "operator_service_account_namespace" { - description = "Namespace of the workload Vault operator service account." - type = string - default = "vault" -} - -variable "operator_token_ttl_seconds" { - description = "TTL for workload Vault operator sessions." - type = number - default = 1800 -} - -variable "platform_admin_policy_name" { - description = "Policy used only for short-lived break-glass administration." - type = string - default = "platform-admin-dev" -} - -variable "transit_mount_path" { - description = "Workload Vault Transit mount path for application cryptography." - type = string - default = "transit" -} - -variable "vault_addr" { - description = "Workload Vault API address reachable by the approved runner." - type = string - default = "http://127.0.0.1:8200" -} - -variable "vault_token" { - description = "Short-lived token used only for this Terraform run." - type = string - sensitive = true - ephemeral = true -} diff --git a/iac/terraform/live/dev-k3s/vault-database/main.tf b/iac/terraform/live/dev-k3s/vault-database/main.tf index 9490781..7e8809f 100644 --- a/iac/terraform/live/dev-k3s/vault-database/main.tf +++ b/iac/terraform/live/dev-k3s/vault-database/main.tf @@ -17,9 +17,23 @@ provider "vault" { token = var.vault_token } +moved { + from = vault_database_secret_backend_connection.platform_postgres + to = vault_database_secret_backend_connection.auth_system_postgres +} + +removed { + from = vault_database_secret_backend_role.postgres_operator + + lifecycle { + destroy = false + } +} + locals { - migration_role_name = "auth-db-migration-dev" - operator_role_name = "postgres-operator-dev" + database_config_name = "auth-system-postgres-dev" + database_mount_path = "database" + migration_role_name = "auth-db-migration-dev" creation_statements = [ <<-EOT @@ -38,10 +52,10 @@ locals { ] } -resource "vault_database_secret_backend_connection" "platform_postgres" { - allowed_roles = [local.migration_role_name, local.operator_role_name] - backend = var.database_mount_path - name = var.database_config_name +resource "vault_database_secret_backend_connection" "auth_system_postgres" { + allowed_roles = [local.migration_role_name] + backend = local.database_mount_path + name = local.database_config_name plugin_name = "postgresql-database-plugin" verify_connection = true @@ -52,24 +66,18 @@ resource "vault_database_secret_backend_connection" "platform_postgres" { password_wo_version = var.postgres_admin_password_version username = var.postgres_admin_username } + + lifecycle { + create_before_destroy = true + } } resource "vault_database_secret_backend_role" "auth_db_migration" { - backend = var.database_mount_path + backend = local.database_mount_path creation_statements = local.creation_statements - db_name = vault_database_secret_backend_connection.platform_postgres.name + db_name = vault_database_secret_backend_connection.auth_system_postgres.name default_ttl = var.auth_db_migration_default_ttl_seconds max_ttl = var.auth_db_migration_max_ttl_seconds name = local.migration_role_name revocation_statements = local.revocation_statements } - -resource "vault_database_secret_backend_role" "postgres_operator" { - backend = var.database_mount_path - creation_statements = local.creation_statements - db_name = vault_database_secret_backend_connection.platform_postgres.name - default_ttl = var.postgres_operator_default_ttl_seconds - max_ttl = var.postgres_operator_max_ttl_seconds - name = local.operator_role_name - revocation_statements = local.revocation_statements -} diff --git a/iac/terraform/live/dev-k3s/vault-database/variables.tf b/iac/terraform/live/dev-k3s/vault-database/variables.tf index 0503c0c..1260afc 100644 --- a/iac/terraform/live/dev-k3s/vault-database/variables.tf +++ b/iac/terraform/live/dev-k3s/vault-database/variables.tf @@ -16,18 +16,6 @@ variable "auth_db_role" { default = "project_auth" } -variable "database_config_name" { - description = "Vault database connection name." - type = string - default = "platform-postgres-dev" -} - -variable "database_mount_path" { - description = "Foundation-owned database secrets mount path." - type = string - default = "database" -} - variable "postgres_admin_password" { description = "PostgreSQL admin password passed only through a write-only provider field." type = string @@ -53,25 +41,13 @@ variable "postgres_database" { } variable "postgres_host" { - description = "Platform PostgreSQL service DNS name." + description = "Auth system PostgreSQL service DNS name." type = string - default = "postgres.platform.svc.cluster.local" -} - -variable "postgres_operator_default_ttl_seconds" { - description = "Default TTL for operator database credentials." - type = number - default = 3600 -} - -variable "postgres_operator_max_ttl_seconds" { - description = "Maximum TTL for operator database credentials." - type = number - default = 28800 + default = "postgres.auth-system-dev.svc.cluster.local" } variable "postgres_port" { - description = "Platform PostgreSQL service port." + description = "Auth system PostgreSQL service port." type = number default = 5432 } diff --git a/iac/terraform/live/dev-k3s/vault-core/.terraform.lock.hcl b/iac/terraform/live/dev-k3s/vault-foundation/.terraform.lock.hcl similarity index 100% rename from iac/terraform/live/dev-k3s/vault-core/.terraform.lock.hcl rename to iac/terraform/live/dev-k3s/vault-foundation/.terraform.lock.hcl diff --git a/iac/terraform/live/dev-k3s/vault-foundation/main.tf b/iac/terraform/live/dev-k3s/vault-foundation/main.tf new file mode 100644 index 0000000..cefa01f --- /dev/null +++ b/iac/terraform/live/dev-k3s/vault-foundation/main.tf @@ -0,0 +1,189 @@ +terraform { + required_version = ">= 1.11.0" + + required_providers { + vault = { + source = "hashicorp/vault" + version = "~> 5.7.0" + } + } + + backend "s3" {} +} + +provider "vault" { + address = var.vault_addr + token = var.vault_token +} + +removed { + from = module.workload_policies + + lifecycle { + destroy = false + } +} + +removed { + from = module.workload_roles + + lifecycle { + destroy = false + } +} + +removed { + from = vault_transit_secret_backend_key.project_auth_jwt + + lifecycle { + destroy = false + } +} + +removed { + from = vault_policy.platform_admin + + lifecycle { + destroy = false + } +} + +removed { + from = vault_kubernetes_auth_backend_role.operator + + lifecycle { + destroy = false + } +} + +removed { + from = vault_jwt_auth_backend_role.ci + + lifecycle { + destroy = false + } +} + +locals { + platform_policy_dir = "${path.module}/../../../../../policies/vault/dev-k3s/platform" + + database_automation_policy_name = "vault-database-automation-dev" + database_mount_path = "database" + ci_database_role_name = "project-gitops-vault-database-dev" + ci_jwt_auth_path = "jwt-ci" + ci_workloads_role_name = "project-gitops-vault-workloads-dev" + kubernetes_auth_path = "kubernetes" + kv_mount_path = "kv" + transit_mount_path = "transit" + workloads_automation_policy_name = "vault-workloads-automation-dev" + + automation_roles = var.ci_jwt_oidc_discovery_url == null ? {} : { + workloads = { + bound_claims = var.ci_workloads_bound_claims + name = local.ci_workloads_role_name + policy = vault_policy.workloads_automation.name + } + database = { + bound_claims = var.ci_database_bound_claims + name = local.ci_database_role_name + policy = vault_policy.database_automation.name + } + } +} + +resource "vault_mount" "kv" { + path = local.kv_mount_path + type = "kv" + options = { + version = "2" + } + + lifecycle { + prevent_destroy = true + } +} + +resource "vault_mount" "database" { + path = local.database_mount_path + type = "database" + + lifecycle { + prevent_destroy = true + } +} + +resource "vault_mount" "transit" { + path = local.transit_mount_path + type = "transit" + + lifecycle { + prevent_destroy = true + } +} + +resource "vault_auth_backend" "kubernetes" { + path = local.kubernetes_auth_path + type = "kubernetes" + + lifecycle { + prevent_destroy = true + } +} + +resource "vault_kubernetes_auth_backend_config" "cluster" { + backend = vault_auth_backend.kubernetes.path + disable_iss_validation = true + disable_local_ca_jwt = false + kubernetes_host = var.kubernetes_host +} + +resource "vault_policy" "workloads_automation" { + name = local.workloads_automation_policy_name + policy = file("${local.platform_policy_dir}/vault-workloads-automation-dev.hcl") +} + +resource "vault_policy" "database_automation" { + name = local.database_automation_policy_name + policy = file("${local.platform_policy_dir}/vault-database-automation-dev.hcl") +} + +resource "vault_jwt_auth_backend" "ci" { + count = var.ci_jwt_oidc_discovery_url == null ? 0 : 1 + + bound_issuer = var.ci_jwt_bound_issuer + oidc_discovery_url = var.ci_jwt_oidc_discovery_url + path = local.ci_jwt_auth_path + + lifecycle { + prevent_destroy = true + + precondition { + condition = ( + var.ci_jwt_bound_issuer != null && + length(var.ci_jwt_bound_audiences) > 0 && + length(var.ci_workloads_bound_claims) > 0 && + length(var.ci_database_bound_claims) > 0 && + length([ + for claim, value in var.ci_workloads_bound_claims : claim + if lookup(var.ci_database_bound_claims, claim, value) != value + ]) > 0 + ) + error_message = "Enabled CI JWT auth requires issuer/audience constraints and workload/database claim maps with at least one shared discriminator key carrying different values." + } + } +} + +resource "vault_jwt_auth_backend_role" "automation" { + for_each = local.automation_roles + + backend = vault_jwt_auth_backend.ci[0].path + bound_audiences = var.ci_jwt_bound_audiences + bound_claims = each.value.bound_claims + bound_claims_type = "string" + role_name = each.value.name + role_type = "jwt" + token_explicit_max_ttl = var.ci_token_ttl_seconds + token_no_default_policy = true + token_policies = [each.value.policy] + user_claim = var.ci_jwt_user_claim +} diff --git a/iac/terraform/live/dev-k3s/vault-foundation/variables.tf b/iac/terraform/live/dev-k3s/vault-foundation/variables.tf new file mode 100644 index 0000000..fda5c50 --- /dev/null +++ b/iac/terraform/live/dev-k3s/vault-foundation/variables.tf @@ -0,0 +1,71 @@ +variable "ci_database_bound_claims" { + description = "Exact repository, protected-ref, and database-job claims for the database role." + type = map(string) + default = {} +} + +variable "ci_jwt_bound_audiences" { + description = "Exact CI JWT audiences." + type = set(string) + default = [] +} + +variable "ci_jwt_bound_issuer" { + description = "Expected CI JWT issuer." + type = string + default = null + nullable = true +} + +variable "ci_jwt_oidc_discovery_url" { + description = "CI OIDC discovery URL. Null leaves external CI authentication disabled." + type = string + default = null + nullable = true +} + +variable "ci_jwt_user_claim" { + description = "JWT claim used as the Vault identity alias." + type = string + default = "sub" +} + +variable "ci_token_ttl_seconds" { + description = "Maximum lifetime for delegated CI tokens." + type = number + default = 1800 + + validation { + condition = ( + var.ci_token_ttl_seconds >= 60 && + var.ci_token_ttl_seconds <= 3600 && + floor(var.ci_token_ttl_seconds) == var.ci_token_ttl_seconds + ) + error_message = "ci_token_ttl_seconds must be a whole number between 60 and 3600." + } +} + +variable "ci_workloads_bound_claims" { + description = "Exact repository, protected-ref, and workloads-job claims for the workloads role." + type = map(string) + default = {} +} + +variable "kubernetes_host" { + description = "Kubernetes TokenReview API address." + type = string + default = "https://kubernetes.default.svc.cluster.local:443" +} + +variable "vault_addr" { + description = "Vault API address reachable by the foundation operator." + type = string + default = "http://127.0.0.1:8200" +} + +variable "vault_token" { + description = "Short-lived bootstrap or security-administrator token." + type = string + sensitive = true + ephemeral = true +} diff --git a/iac/terraform/live/dev-k3s/vault-workloads/.terraform.lock.hcl b/iac/terraform/live/dev-k3s/vault-workloads/.terraform.lock.hcl new file mode 100644 index 0000000..8b8acd8 --- /dev/null +++ b/iac/terraform/live/dev-k3s/vault-workloads/.terraform.lock.hcl @@ -0,0 +1,22 @@ +# This file is maintained automatically by "terraform init". +# Manual edits may be lost in future updates. + +provider "registry.terraform.io/hashicorp/vault" { + version = "5.7.0" + constraints = "~> 5.7.0" + hashes = [ + "h1:Pm0AcUSYmBPZgRahQX/ahiYcjtZODSAEc2rK8r8MQ18=", + "zh:1dd9ab6d23f61a5e522efcb462f1fd6f4a210c77b9038c8e12fa5fa663b45d01", + "zh:3c98d37ead857c980f7b9285f8c3e1eb7a8fd6d6799275c311c6997973389cc9", + "zh:3df895fbaed383e3748ba1b50f5f1046f75503483bc3d783992059f85c85ba31", + "zh:3e9faaa0a85c6f03c7fd7f8b7008bb3fbb8777f26c001875947cafa47f91c657", + "zh:52a057d0c6cde7cbfd9ceb78a3781dcfc81cf108c533f454530ea6bb87a9bea8", + "zh:78d5eefdd9e494defcb3c68d282b8f96630502cac21d1ea161f53cfe9bb483b3", + "zh:8521c3825254a5f7fbff8f42ca57cabf052366f0420f5f239ebebf8292c03d0e", + "zh:953563d429e40087eb34faf22f28e781e50eee27cfc9ac1ad04308ba592a647f", + "zh:a52dd76bb7f5b86cb8de7380d2e68b47ec4445782c16ee205e6a013be35a57b6", + "zh:bdad38c95a14c8cce1eeadcc539cf9bf74902ce7c662b79105ad993bb48ec073", + "zh:d3c676d7d12c15b58518fa3ee7fc398a13893b4057fe9bf4bc1fe635f3fb995a", + "zh:f8673b6c06da80e912c9e32dd4853f07bfca386968d5b33c9fceb6f68b519959", + ] +} diff --git a/iac/terraform/live/dev-k3s/vault-workloads/main.tf b/iac/terraform/live/dev-k3s/vault-workloads/main.tf new file mode 100644 index 0000000..879de8f --- /dev/null +++ b/iac/terraform/live/dev-k3s/vault-workloads/main.tf @@ -0,0 +1,95 @@ +terraform { + required_version = ">= 1.11.0" + + required_providers { + vault = { + source = "hashicorp/vault" + version = "~> 5.7.0" + } + } + + backend "s3" {} +} + +provider "vault" { + address = var.vault_addr + skip_child_token = true + token = var.vault_token +} + +locals { + workload_policy_dir = "${path.module}/../../../../../policies/vault/dev-k3s/workloads" + + jwt_transit_key_name = "project-auth-jwt" + kubernetes_auth_path = "kubernetes" + kubernetes_token_audience = "vault" + transit_mount_path = "transit" + + workload_policies = { + auth-server-dev = file("${local.workload_policy_dir}/auth-server-dev.hcl") + auth-db-migration-dev = file("${local.workload_policy_dir}/auth-db-migration-dev.hcl") + postgres-dev = file("${local.workload_policy_dir}/postgres-dev.hcl") + keycloak-dev = file("${local.workload_policy_dir}/keycloak-dev.hcl") + keycloak-client-sync-dev = file("${local.workload_policy_dir}/keycloak-client-sync-dev.hcl") + } +} + +module "workload_policies" { + source = "../../../modules/vault-policy-set" + + policies = local.workload_policies +} + +module "workload_roles" { + source = "../../../modules/vault-kubernetes-roles" + + backend = local.kubernetes_auth_path + roles = { + auth-server-dev = { + audiences = [local.kubernetes_token_audience] + service_account_names = ["auth-server"] + service_account_namespaces = ["auth-dev"] + token_policies = [module.workload_policies.names["auth-server-dev"]] + token_ttl = var.kubernetes_role_ttl_seconds + } + auth-db-migration-dev = { + audiences = [local.kubernetes_token_audience] + service_account_names = ["auth-db-migration"] + service_account_namespaces = ["auth-dev"] + token_policies = [module.workload_policies.names["auth-db-migration-dev"]] + token_ttl = var.kubernetes_role_ttl_seconds + } + postgres-dev = { + audiences = [local.kubernetes_token_audience] + service_account_names = ["postgres"] + service_account_namespaces = ["auth-system-dev"] + token_policies = [module.workload_policies.names["postgres-dev"]] + token_ttl = var.kubernetes_role_ttl_seconds + } + keycloak-dev = { + audiences = [local.kubernetes_token_audience] + service_account_names = ["keycloak"] + service_account_namespaces = ["auth-system-dev"] + token_policies = [module.workload_policies.names["keycloak-dev"]] + token_ttl = var.kubernetes_role_ttl_seconds + } + keycloak-client-sync-dev = { + audiences = [local.kubernetes_token_audience] + service_account_names = ["keycloak-client-sync"] + service_account_namespaces = ["auth-system-dev"] + token_policies = [module.workload_policies.names["keycloak-client-sync-dev"]] + token_ttl = var.kubernetes_role_ttl_seconds + } + } +} + +resource "vault_transit_secret_backend_key" "project_auth_jwt" { + backend = local.transit_mount_path + deletion_allowed = false + name = local.jwt_transit_key_name + type = "rsa-2048" + + lifecycle { + prevent_destroy = true + } +} diff --git a/iac/terraform/live/dev-k3s/vault-workloads/variables.tf b/iac/terraform/live/dev-k3s/vault-workloads/variables.tf new file mode 100644 index 0000000..f0add09 --- /dev/null +++ b/iac/terraform/live/dev-k3s/vault-workloads/variables.tf @@ -0,0 +1,18 @@ +variable "kubernetes_role_ttl_seconds" { + description = "TTL for workload Kubernetes auth tokens." + type = number + default = 3600 +} + +variable "vault_addr" { + description = "Vault API address reachable by the delegated runner." + type = string + default = "http://127.0.0.1:8200" +} + +variable "vault_token" { + description = "Short-lived token carrying only vault-workloads-automation-dev." + type = string + sensitive = true + ephemeral = true +} diff --git a/platform/control-plane/argocd/application-sets/kustomization.yaml b/platform/control-plane/argocd/application-sets/kustomization.yaml new file mode 100644 index 0000000..e3b8e22 --- /dev/null +++ b/platform/control-plane/argocd/application-sets/kustomization.yaml @@ -0,0 +1,8 @@ +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization + +resources: + - platform-addons.yaml + - platform-services.yaml + - systems.yaml + - workloads.yaml diff --git a/platform/control-plane/argocd/application-sets/platform-addons.yaml b/platform/control-plane/argocd/application-sets/platform-addons.yaml new file mode 100644 index 0000000..0dadea7 --- /dev/null +++ b/platform/control-plane/argocd/application-sets/platform-addons.yaml @@ -0,0 +1,122 @@ +apiVersion: argoproj.io/v1alpha1 +kind: ApplicationSet +metadata: + name: platform-addons + namespace: argocd + annotations: + argocd.argoproj.io/sync-wave: "-5" + argocd.argoproj.io/sync-options: Prune=confirm,Delete=confirm +spec: + goTemplate: true + goTemplateOptions: + - missingkey=error + generators: + - list: + elements: + - component: sealed-secrets + cluster: dev-k3s + server: https://kubernetes.default.svc + namespace: kube-system + repoURL: https://bitnami.github.io/sealed-secrets + chart: sealed-secrets + # renovate: datasource=helm depName=sealed-secrets registryUrl=https://bitnami.github.io/sealed-secrets + revision: 2.17.9 + autoSync: "true" + helmValues: | + fullnameOverride: sealed-secrets-controller + keyrenewperiod: 720h + image: + # renovate: datasource=docker depName=bitnami/sealed-secrets-controller + repository: bitnami/sealed-secrets-controller + tag: "0.33.1@sha256:e7fad65c2d2f47e48d9ca17408ed56961bfa6a6dd74ccd4a1a214664156534bc" + resources: + requests: + cpu: 50m + memory: 64Mi + limits: + cpu: 250m + memory: 256Mi + - component: vault-agent-injector + cluster: dev-k3s + server: https://kubernetes.default.svc + namespace: vault + repoURL: https://helm.releases.hashicorp.com + chart: vault + # renovate: datasource=helm depName=vault registryUrl=https://helm.releases.hashicorp.com + revision: 0.32.0 + autoSync: "false" + helmValues: | + global: + externalVaultAddr: http://vault.vault.svc.cluster.local:8200 + tlsDisable: true + server: + enabled: false + injector: + enabled: true + authPath: auth/kubernetes + image: + # renovate: datasource=docker depName=hashicorp/vault-k8s + repository: hashicorp/vault-k8s + tag: "1.7.2@sha256:ae3d307658b72a1cf35dab9bdf92c995d45cdc7183af0516857714b5bd0ba84d" + webhook: + failurePolicy: Fail + namespaceSelector: + matchLabels: + vault-injection: enabled + resources: + requests: + cpu: 100m + memory: 128Mi + limits: + cpu: 250m + memory: 256Mi + agentImage: + # renovate: datasource=docker depName=hashicorp/vault + repository: hashicorp/vault + tag: "1.18.5@sha256:750bb37c1638fa194ab37053a81618c61bb0491ddec6fccac87c07a8e6cd8166" + template: + metadata: + name: "{{ .component }}-{{ .cluster }}" + annotations: + argocd.argoproj.io/sync-options: Prune=confirm,Delete=confirm + project-gitops.io/auto-sync-gate: "{{ .autoSync }}" + labels: + project-gitops.io/cluster: "{{ .cluster }}" + project-gitops.io/owner: platform + spec: + project: platform-addons + source: + repoURL: "{{ .repoURL }}" + chart: "{{ .chart }}" + targetRevision: "{{ .revision }}" + helm: + values: "{{ .helmValues }}" + destination: + server: "{{ .server }}" + namespace: "{{ .namespace }}" + syncPolicy: + syncOptions: + - CreateNamespace=true + - Prune=confirm + - PruneLast=true + - FailOnSharedResource=true + retry: + limit: 5 + refresh: true + backoff: + duration: 5s + factor: 2 + maxDuration: 3m + revisionHistoryLimit: 5 + templatePatch: | + {{- if eq .autoSync "true" }} + spec: + syncPolicy: + automated: + enabled: true + prune: true + selfHeal: true + {{- end }} + syncPolicy: + applicationsSync: create-update + preserveResourcesOnDeletion: true diff --git a/platform/control-plane/argocd/application-sets/platform-services.yaml b/platform/control-plane/argocd/application-sets/platform-services.yaml new file mode 100644 index 0000000..90b70e6 --- /dev/null +++ b/platform/control-plane/argocd/application-sets/platform-services.yaml @@ -0,0 +1,64 @@ +apiVersion: argoproj.io/v1alpha1 +kind: ApplicationSet +metadata: + name: platform-services + namespace: argocd + annotations: + argocd.argoproj.io/sync-wave: "-5" + argocd.argoproj.io/sync-options: Prune=confirm,Delete=confirm +spec: + goTemplate: true + goTemplateOptions: + - missingkey=error + generators: + - list: + elements: + - component: vault + cluster: dev-k3s + server: https://kubernetes.default.svc + namespace: vault + path: clusters/dev-k3s/overlays/platform/vault + autoSync: "true" + template: + metadata: + name: "{{ .component }}-{{ .cluster }}" + annotations: + argocd.argoproj.io/sync-options: Prune=confirm,Delete=confirm + project-gitops.io/auto-sync-gate: "{{ .autoSync }}" + labels: + project-gitops.io/cluster: "{{ .cluster }}" + project-gitops.io/owner: platform + spec: + project: platform-services + source: + repoURL: https://git.learn.hyeonworks.com/donghyeon.kang/project-gitops + targetRevision: main + path: "{{ .path }}" + destination: + server: "{{ .server }}" + namespace: "{{ .namespace }}" + syncPolicy: + syncOptions: + - CreateNamespace=true + - PruneLast=true + - FailOnSharedResource=true + retry: + limit: 5 + refresh: true + backoff: + duration: 5s + factor: 2 + maxDuration: 3m + revisionHistoryLimit: 5 + templatePatch: | + {{- if eq .autoSync "true" }} + spec: + syncPolicy: + automated: + enabled: true + prune: true + selfHeal: true + {{- end }} + syncPolicy: + applicationsSync: create-update + preserveResourcesOnDeletion: true diff --git a/platform/control-plane/argocd/application-sets/systems.yaml b/platform/control-plane/argocd/application-sets/systems.yaml new file mode 100644 index 0000000..0ea75de --- /dev/null +++ b/platform/control-plane/argocd/application-sets/systems.yaml @@ -0,0 +1,64 @@ +apiVersion: argoproj.io/v1alpha1 +kind: ApplicationSet +metadata: + name: systems + namespace: argocd + annotations: + argocd.argoproj.io/sync-wave: "-5" + argocd.argoproj.io/sync-options: Prune=confirm,Delete=confirm +spec: + goTemplate: true + goTemplateOptions: + - missingkey=error + generators: + - list: + elements: + - component: auth-system + cluster: dev-k3s + server: https://kubernetes.default.svc + namespace: auth-system-dev + path: clusters/dev-k3s/overlays/systems/auth-system + autoSync: "false" + template: + metadata: + name: "{{ .component }}-{{ .cluster }}" + annotations: + argocd.argoproj.io/sync-options: Prune=confirm,Delete=confirm + project-gitops.io/auto-sync-gate: "{{ .autoSync }}" + labels: + project-gitops.io/cluster: "{{ .cluster }}" + project-gitops.io/owner: systems + spec: + project: systems + source: + repoURL: https://git.learn.hyeonworks.com/donghyeon.kang/project-gitops + targetRevision: main + path: "{{ .path }}" + destination: + server: "{{ .server }}" + namespace: "{{ .namespace }}" + syncPolicy: + syncOptions: + - CreateNamespace=true + - PruneLast=true + - FailOnSharedResource=true + retry: + limit: 5 + refresh: true + backoff: + duration: 5s + factor: 2 + maxDuration: 3m + revisionHistoryLimit: 5 + templatePatch: | + {{- if eq .autoSync "true" }} + spec: + syncPolicy: + automated: + enabled: true + prune: true + selfHeal: true + {{- end }} + syncPolicy: + applicationsSync: create-update + preserveResourcesOnDeletion: true diff --git a/platform/control-plane/argocd/application-sets/workloads.yaml b/platform/control-plane/argocd/application-sets/workloads.yaml new file mode 100644 index 0000000..2edd9a2 --- /dev/null +++ b/platform/control-plane/argocd/application-sets/workloads.yaml @@ -0,0 +1,70 @@ +apiVersion: argoproj.io/v1alpha1 +kind: ApplicationSet +metadata: + name: workloads + namespace: argocd + annotations: + argocd.argoproj.io/sync-wave: "-5" + argocd.argoproj.io/sync-options: Prune=confirm,Delete=confirm +spec: + goTemplate: true + goTemplateOptions: + - missingkey=error + generators: + - list: + elements: + - component: auth-server + cluster: dev-k3s + server: https://kubernetes.default.svc + namespace: auth-dev + path: clusters/dev-k3s/overlays/workloads/auth-server + autoSync: "false" + - component: api-server + cluster: dev-k3s + server: https://kubernetes.default.svc + namespace: api-dev + path: clusters/dev-k3s/overlays/workloads/api-server + autoSync: "false" + template: + metadata: + name: "{{ .component }}-{{ .cluster }}" + annotations: + argocd.argoproj.io/sync-options: Prune=confirm,Delete=confirm + project-gitops.io/auto-sync-gate: "{{ .autoSync }}" + labels: + project-gitops.io/cluster: "{{ .cluster }}" + project-gitops.io/owner: workloads + spec: + project: workloads + source: + repoURL: https://git.learn.hyeonworks.com/donghyeon.kang/project-gitops + targetRevision: main + path: "{{ .path }}" + destination: + server: "{{ .server }}" + namespace: "{{ .namespace }}" + syncPolicy: + syncOptions: + - CreateNamespace=true + - PruneLast=true + - FailOnSharedResource=true + retry: + limit: 5 + refresh: true + backoff: + duration: 5s + factor: 2 + maxDuration: 3m + revisionHistoryLimit: 5 + templatePatch: | + {{- if eq .autoSync "true" }} + spec: + syncPolicy: + automated: + enabled: true + prune: true + selfHeal: true + {{- end }} + syncPolicy: + applicationsSync: create-update + preserveResourcesOnDeletion: true diff --git a/clusters/dev-k3s/kustomization.yaml b/platform/control-plane/argocd/kustomization.yaml similarity index 80% rename from clusters/dev-k3s/kustomization.yaml rename to platform/control-plane/argocd/kustomization.yaml index d44d9ef..856d658 100644 --- a/clusters/dev-k3s/kustomization.yaml +++ b/platform/control-plane/argocd/kustomization.yaml @@ -3,4 +3,4 @@ kind: Kustomization resources: - projects - - applications + - application-sets diff --git a/platform/control-plane/argocd/projects/kustomization.yaml b/platform/control-plane/argocd/projects/kustomization.yaml new file mode 100644 index 0000000..e3b8e22 --- /dev/null +++ b/platform/control-plane/argocd/projects/kustomization.yaml @@ -0,0 +1,8 @@ +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization + +resources: + - platform-addons.yaml + - platform-services.yaml + - systems.yaml + - workloads.yaml diff --git a/clusters/dev-k3s/projects/cluster-addons.yaml b/platform/control-plane/argocd/projects/platform-addons.yaml similarity index 88% rename from clusters/dev-k3s/projects/cluster-addons.yaml rename to platform/control-plane/argocd/projects/platform-addons.yaml index bc0b68c..d77823c 100644 --- a/clusters/dev-k3s/projects/cluster-addons.yaml +++ b/platform/control-plane/argocd/projects/platform-addons.yaml @@ -1,7 +1,7 @@ apiVersion: argoproj.io/v1alpha1 kind: AppProject metadata: - name: cluster-addons-dev + name: platform-addons namespace: argocd annotations: argocd.argoproj.io/sync-wave: "-10" @@ -9,7 +9,7 @@ metadata: finalizers: - resources-finalizer.argocd.argoproj.io spec: - description: Cluster-scoped controllers required by the dev GitOps stack + description: Cluster-scoped controllers installed from approved Helm repositories sourceRepos: - https://bitnami.github.io/sealed-secrets - https://helm.releases.hashicorp.com @@ -40,6 +40,8 @@ spec: kind: ServiceAccount - group: apps kind: Deployment + - group: policy + kind: PodDisruptionBudget - group: rbac.authorization.k8s.io kind: Role - group: rbac.authorization.k8s.io diff --git a/clusters/dev-k3s/projects/platform.yaml b/platform/control-plane/argocd/projects/platform-services.yaml similarity index 58% rename from clusters/dev-k3s/projects/platform.yaml rename to platform/control-plane/argocd/projects/platform-services.yaml index 9dcb181..de071b4 100644 --- a/clusters/dev-k3s/projects/platform.yaml +++ b/platform/control-plane/argocd/projects/platform-services.yaml @@ -1,7 +1,7 @@ apiVersion: argoproj.io/v1alpha1 kind: AppProject metadata: - name: platform-dev + name: platform-services namespace: argocd annotations: argocd.argoproj.io/sync-wave: "-10" @@ -9,43 +9,29 @@ metadata: finalizers: - resources-finalizer.argocd.argoproj.io spec: - description: Dev shared infrastructure managed by Argo CD + description: Shared services owned by this standalone reference platform sourceRepos: - https://git.learn.hyeonworks.com/donghyeon.kang/project-gitops destinations: - - namespace: platform - server: https://kubernetes.default.svc - namespace: vault server: https://kubernetes.default.svc clusterResourceWhitelist: - group: "" kind: Namespace - - group: "rbac.authorization.k8s.io" - kind: ClusterRole - - group: "rbac.authorization.k8s.io" + - group: rbac.authorization.k8s.io kind: ClusterRoleBinding namespaceResourceWhitelist: - group: "" kind: ConfigMap + - group: "" + kind: PersistentVolumeClaim - group: "" kind: Service - group: "" kind: ServiceAccount - - group: "" - kind: PersistentVolumeClaim - - group: "rbac.authorization.k8s.io" - kind: Role - - group: "rbac.authorization.k8s.io" - kind: RoleBinding - - group: "apps" + - group: apps kind: Deployment - - group: "apps" - kind: StatefulSet - - group: "batch" - kind: Job - - group: "networking.k8s.io" - kind: Ingress - - group: "networking.k8s.io" + - group: networking.k8s.io kind: NetworkPolicy orphanedResources: warn: true diff --git a/platform/control-plane/argocd/projects/systems.yaml b/platform/control-plane/argocd/projects/systems.yaml new file mode 100644 index 0000000..78a006c --- /dev/null +++ b/platform/control-plane/argocd/projects/systems.yaml @@ -0,0 +1,39 @@ +apiVersion: argoproj.io/v1alpha1 +kind: AppProject +metadata: + name: systems + namespace: argocd + annotations: + argocd.argoproj.io/sync-wave: "-10" + argocd.argoproj.io/sync-options: Prune=confirm,Delete=confirm + finalizers: + - resources-finalizer.argocd.argoproj.io +spec: + description: Backing systems dedicated to an example product boundary + sourceRepos: + - https://git.learn.hyeonworks.com/donghyeon.kang/project-gitops + destinations: + - namespace: auth-system-dev + server: https://kubernetes.default.svc + clusterResourceWhitelist: + - group: "" + kind: Namespace + namespaceResourceWhitelist: + - group: "" + kind: ConfigMap + - group: "" + kind: Service + - group: "" + kind: ServiceAccount + - group: apps + kind: Deployment + - group: apps + kind: StatefulSet + - group: batch + kind: Job + - group: networking.k8s.io + kind: Ingress + - group: networking.k8s.io + kind: NetworkPolicy + orphanedResources: + warn: true diff --git a/clusters/dev-k3s/projects/apps.yaml b/platform/control-plane/argocd/projects/workloads.yaml similarity index 80% rename from clusters/dev-k3s/projects/apps.yaml rename to platform/control-plane/argocd/projects/workloads.yaml index d04104f..eb53d2c 100644 --- a/clusters/dev-k3s/projects/apps.yaml +++ b/platform/control-plane/argocd/projects/workloads.yaml @@ -1,7 +1,7 @@ apiVersion: argoproj.io/v1alpha1 kind: AppProject metadata: - name: apps-dev + name: workloads namespace: argocd annotations: argocd.argoproj.io/sync-wave: "-10" @@ -9,7 +9,7 @@ metadata: finalizers: - resources-finalizer.argocd.argoproj.io spec: - description: Dev application workloads managed by Argo CD + description: First-party application workloads sourceRepos: - https://git.learn.hyeonworks.com/donghyeon.kang/project-gitops destinations: @@ -27,15 +27,15 @@ spec: kind: Service - group: "" kind: ServiceAccount - - group: "bitnami.com" + - group: bitnami.com kind: SealedSecret - - group: "apps" + - group: apps kind: Deployment - - group: "batch" + - group: batch kind: Job - - group: "networking.k8s.io" + - group: networking.k8s.io kind: Ingress - - group: "networking.k8s.io" + - group: networking.k8s.io kind: NetworkPolicy orphanedResources: warn: true diff --git a/platform/security/vault/base/vault-operator-serviceaccount.yaml b/platform/security/vault/base/vault-operator-serviceaccount.yaml deleted file mode 100644 index a933a09..0000000 --- a/platform/security/vault/base/vault-operator-serviceaccount.yaml +++ /dev/null @@ -1,5 +0,0 @@ -apiVersion: v1 -kind: ServiceAccount -metadata: - name: vault-operator -automountServiceAccountToken: false diff --git a/platform/security/vault/base/files/vault/vault.hcl b/platform/shared-services/vault/base/files/vault/vault.hcl similarity index 55% rename from platform/security/vault/base/files/vault/vault.hcl rename to platform/shared-services/vault/base/files/vault/vault.hcl index 2cc0b15..278ff2f 100644 --- a/platform/security/vault/base/files/vault/vault.hcl +++ b/platform/shared-services/vault/base/files/vault/vault.hcl @@ -1,7 +1,5 @@ ui = true disable_mlock = true -api_addr = "http://vault.vault.svc.cluster.local:8200" -cluster_addr = "http://vault.vault.svc.cluster.local:8201" listener "tcp" { address = "0.0.0.0:8200" @@ -11,5 +9,5 @@ listener "tcp" { storage "raft" { path = "/vault/data" - node_id = "vault-dev-0" + node_id = "vault-0" } diff --git a/platform/security/vault/base/kustomization.yaml b/platform/shared-services/vault/base/kustomization.yaml similarity index 88% rename from platform/security/vault/base/kustomization.yaml rename to platform/shared-services/vault/base/kustomization.yaml index 6db08ab..8b9e877 100644 --- a/platform/security/vault/base/kustomization.yaml +++ b/platform/shared-services/vault/base/kustomization.yaml @@ -3,7 +3,6 @@ kind: Kustomization resources: - vault-serviceaccount.yaml - - vault-operator-serviceaccount.yaml - vault-auth-delegator.clusterrolebinding.yaml - vault-pvc.yaml - vault-service.yaml diff --git a/platform/security/vault/base/vault-auth-delegator.clusterrolebinding.yaml b/platform/shared-services/vault/base/vault-auth-delegator.clusterrolebinding.yaml similarity index 100% rename from platform/security/vault/base/vault-auth-delegator.clusterrolebinding.yaml rename to platform/shared-services/vault/base/vault-auth-delegator.clusterrolebinding.yaml diff --git a/platform/security/vault/base/vault-deployment.yaml b/platform/shared-services/vault/base/vault-deployment.yaml similarity index 89% rename from platform/security/vault/base/vault-deployment.yaml rename to platform/shared-services/vault/base/vault-deployment.yaml index 6a82e84..feb7e1b 100644 --- a/platform/security/vault/base/vault-deployment.yaml +++ b/platform/shared-services/vault/base/vault-deployment.yaml @@ -24,6 +24,14 @@ spec: env: - name: VAULT_ADDR value: http://127.0.0.1:8200 + - name: POD_IP + valueFrom: + fieldRef: + fieldPath: status.podIP + - name: VAULT_API_ADDR + value: http://$(POD_IP):8200 + - name: VAULT_CLUSTER_ADDR + value: http://$(POD_IP):8201 args: - | cp /vault/config/vault.hcl /tmp/vault.hcl diff --git a/platform/security/vault/base/vault-pvc.yaml b/platform/shared-services/vault/base/vault-pvc.yaml similarity index 72% rename from platform/security/vault/base/vault-pvc.yaml rename to platform/shared-services/vault/base/vault-pvc.yaml index 285ef68..41902e7 100644 --- a/platform/security/vault/base/vault-pvc.yaml +++ b/platform/shared-services/vault/base/vault-pvc.yaml @@ -3,7 +3,7 @@ kind: PersistentVolumeClaim metadata: name: vault-data annotations: - argocd.argoproj.io/sync-options: Prune=false + argocd.argoproj.io/sync-options: Prune=confirm,Delete=confirm spec: accessModes: - ReadWriteOnce diff --git a/platform/security/vault/base/vault-service.yaml b/platform/shared-services/vault/base/vault-service.yaml similarity index 100% rename from platform/security/vault/base/vault-service.yaml rename to platform/shared-services/vault/base/vault-service.yaml diff --git a/platform/security/vault/base/vault-serviceaccount.yaml b/platform/shared-services/vault/base/vault-serviceaccount.yaml similarity index 100% rename from platform/security/vault/base/vault-serviceaccount.yaml rename to platform/shared-services/vault/base/vault-serviceaccount.yaml diff --git a/policies/vault/dev-k3s/keycloak-client-sync-dev.hcl b/policies/vault/dev-k3s/keycloak-client-sync-dev.hcl deleted file mode 100644 index 4c81320..0000000 --- a/policies/vault/dev-k3s/keycloak-client-sync-dev.hcl +++ /dev/null @@ -1,7 +0,0 @@ -path "kv/data/dev/platform/keycloak/bootstrap-admin" { - capabilities = ["read"] -} - -path "kv/data/dev/platform/keycloak/client-auth-server" { - capabilities = ["read"] -} diff --git a/policies/vault/dev-k3s/keycloak-dev.hcl b/policies/vault/dev-k3s/keycloak-dev.hcl deleted file mode 100644 index e272070..0000000 --- a/policies/vault/dev-k3s/keycloak-dev.hcl +++ /dev/null @@ -1,7 +0,0 @@ -path "kv/data/dev/platform/postgres/keycloak" { - capabilities = ["read"] -} - -path "kv/data/dev/platform/keycloak/bootstrap-admin" { - capabilities = ["read"] -} diff --git a/policies/vault/dev-k3s/keycloak-operator-dev.hcl b/policies/vault/dev-k3s/keycloak-operator-dev.hcl deleted file mode 100644 index d83ae81..0000000 --- a/policies/vault/dev-k3s/keycloak-operator-dev.hcl +++ /dev/null @@ -1,3 +0,0 @@ -path "kv/data/dev/platform/keycloak/bootstrap-admin" { - capabilities = ["read"] -} diff --git a/policies/vault/dev-k3s/platform-admin-dev.hcl b/policies/vault/dev-k3s/platform-admin-dev.hcl deleted file mode 100644 index 1d60924..0000000 --- a/policies/vault/dev-k3s/platform-admin-dev.hcl +++ /dev/null @@ -1,43 +0,0 @@ -path "kv/*" { - capabilities = ["create", "read", "update", "delete", "list"] -} - -path "auth/kubernetes/*" { - capabilities = ["create", "read", "update", "delete", "list"] -} - -path "sys/auth/*" { - capabilities = ["create", "read", "update", "delete", "list", "sudo"] -} - -path "sys/mounts/*" { - capabilities = ["create", "read", "update", "delete", "list", "sudo"] -} - -path "sys/policies/acl/*" { - capabilities = ["create", "read", "update", "delete", "list"] -} - -path "database/*" { - capabilities = ["create", "read", "update", "delete", "list"] -} - -path "transit/*" { - capabilities = ["create", "read", "update", "delete", "list"] -} - -path "auth/token/create-orphan" { - capabilities = ["update"] -} - -path "auth/token/lookup" { - capabilities = ["update"] -} - -path "sys/storage/raft/snapshot" { - capabilities = ["read", "update", "sudo"] -} - -path "sys/storage/raft/snapshot-force" { - capabilities = ["update", "sudo"] -} diff --git a/policies/vault/dev-k3s/platform/vault-database-automation-dev.hcl b/policies/vault/dev-k3s/platform/vault-database-automation-dev.hcl new file mode 100644 index 0000000..6ac78ec --- /dev/null +++ b/policies/vault/dev-k3s/platform/vault-database-automation-dev.hcl @@ -0,0 +1,24 @@ +# Managed by vault-foundation. The database runner may reconcile only the named +# PostgreSQL connection, dynamic role, and minimal self-service token endpoints. + +path "database/config/auth-system-postgres-dev" { + capabilities = ["create", "read", "update", "delete"] +} + +path "database/roles/auth-db-migration-dev" { + capabilities = ["create", "read", "update", "delete"] +} + +# no-default-policy runner tokens retain only the self-service operations used +# for capability checks, identity verification, and explicit revocation. +path "sys/capabilities-self" { + capabilities = ["update"] +} + +path "auth/token/lookup-self" { + capabilities = ["read"] +} + +path "auth/token/revoke-self" { + capabilities = ["update"] +} diff --git a/policies/vault/dev-k3s/platform/vault-workloads-automation-dev.hcl b/policies/vault/dev-k3s/platform/vault-workloads-automation-dev.hcl new file mode 100644 index 0000000..7093ffb --- /dev/null +++ b/policies/vault/dev-k3s/platform/vault-workloads-automation-dev.hcl @@ -0,0 +1,65 @@ +# Managed by vault-foundation. This trusted security runner may reconcile only +# the named workload policies, Kubernetes auth roles, application Transit key, +# and the minimal self-service token endpoints declared below. + +path "sys/policies/acl/auth-server-dev" { + capabilities = ["create", "read", "update", "delete"] +} + +path "sys/policies/acl/auth-db-migration-dev" { + capabilities = ["create", "read", "update", "delete"] +} + +path "sys/policies/acl/postgres-dev" { + capabilities = ["create", "read", "update", "delete"] +} + +path "sys/policies/acl/keycloak-dev" { + capabilities = ["create", "read", "update", "delete"] +} + +path "sys/policies/acl/keycloak-client-sync-dev" { + capabilities = ["create", "read", "update", "delete"] +} + +path "auth/kubernetes/role/auth-server-dev" { + capabilities = ["create", "read", "update", "delete"] +} + +path "auth/kubernetes/role/auth-db-migration-dev" { + capabilities = ["create", "read", "update", "delete"] +} + +path "auth/kubernetes/role/postgres-dev" { + capabilities = ["create", "read", "update", "delete"] +} + +path "auth/kubernetes/role/keycloak-dev" { + capabilities = ["create", "read", "update", "delete"] +} + +path "auth/kubernetes/role/keycloak-client-sync-dev" { + capabilities = ["create", "read", "update", "delete"] +} + +path "transit/keys/project-auth-jwt" { + capabilities = ["create", "read", "update"] +} + +path "transit/keys/project-auth-jwt/config" { + capabilities = ["update"] +} + +# no-default-policy runner tokens retain only the self-service operations used +# for capability checks, identity verification, and explicit revocation. +path "sys/capabilities-self" { + capabilities = ["update"] +} + +path "auth/token/lookup-self" { + capabilities = ["read"] +} + +path "auth/token/revoke-self" { + capabilities = ["update"] +} diff --git a/policies/vault/dev-k3s/postgres-dev.hcl b/policies/vault/dev-k3s/postgres-dev.hcl deleted file mode 100644 index 7eda9eb..0000000 --- a/policies/vault/dev-k3s/postgres-dev.hcl +++ /dev/null @@ -1,11 +0,0 @@ -path "kv/data/dev/platform/postgres/superuser" { - capabilities = ["read"] -} - -path "kv/data/dev/platform/postgres/auth-server" { - capabilities = ["read"] -} - -path "kv/data/dev/platform/postgres/keycloak" { - capabilities = ["read"] -} diff --git a/policies/vault/dev-k3s/postgres-operator-dev.hcl b/policies/vault/dev-k3s/postgres-operator-dev.hcl deleted file mode 100644 index 527409e..0000000 --- a/policies/vault/dev-k3s/postgres-operator-dev.hcl +++ /dev/null @@ -1,3 +0,0 @@ -path "database/creds/postgres-operator-dev" { - capabilities = ["read"] -} diff --git a/policies/vault/dev-k3s/vault-database-automation-dev.hcl b/policies/vault/dev-k3s/vault-database-automation-dev.hcl deleted file mode 100644 index 7b59005..0000000 --- a/policies/vault/dev-k3s/vault-database-automation-dev.hcl +++ /dev/null @@ -1,14 +0,0 @@ -# Managed by vault-core. The database runner may reconcile only the named -# PostgreSQL connection and dynamic roles owned by the vault-database state. - -path "database/config/platform-postgres-dev" { - capabilities = ["create", "read", "update", "delete"] -} - -path "database/roles/auth-db-migration-dev" { - capabilities = ["create", "read", "update", "delete"] -} - -path "database/roles/postgres-operator-dev" { - capabilities = ["create", "read", "update", "delete"] -} diff --git a/policies/vault/dev-k3s/auth-db-migration-dev.hcl b/policies/vault/dev-k3s/workloads/auth-db-migration-dev.hcl similarity index 100% rename from policies/vault/dev-k3s/auth-db-migration-dev.hcl rename to policies/vault/dev-k3s/workloads/auth-db-migration-dev.hcl diff --git a/policies/vault/dev-k3s/auth-server-dev.hcl b/policies/vault/dev-k3s/workloads/auth-server-dev.hcl similarity index 61% rename from policies/vault/dev-k3s/auth-server-dev.hcl rename to policies/vault/dev-k3s/workloads/auth-server-dev.hcl index d0112f6..2b34fc5 100644 --- a/policies/vault/dev-k3s/auth-server-dev.hcl +++ b/policies/vault/dev-k3s/workloads/auth-server-dev.hcl @@ -1,8 +1,8 @@ -path "kv/data/dev/platform/postgres/auth-server" { +path "kv/data/dev/systems/auth-system/postgres/auth-server" { capabilities = ["read"] } -path "kv/data/dev/platform/keycloak/client-auth-server" { +path "kv/data/dev/workloads/auth-server/keycloak-client" { capabilities = ["read"] } diff --git a/policies/vault/dev-k3s/workloads/keycloak-client-sync-dev.hcl b/policies/vault/dev-k3s/workloads/keycloak-client-sync-dev.hcl new file mode 100644 index 0000000..8df9bd8 --- /dev/null +++ b/policies/vault/dev-k3s/workloads/keycloak-client-sync-dev.hcl @@ -0,0 +1,7 @@ +path "kv/data/dev/systems/auth-system/keycloak/bootstrap-admin" { + capabilities = ["read"] +} + +path "kv/data/dev/workloads/auth-server/keycloak-client" { + capabilities = ["read"] +} diff --git a/policies/vault/dev-k3s/workloads/keycloak-dev.hcl b/policies/vault/dev-k3s/workloads/keycloak-dev.hcl new file mode 100644 index 0000000..e0a0c2a --- /dev/null +++ b/policies/vault/dev-k3s/workloads/keycloak-dev.hcl @@ -0,0 +1,7 @@ +path "kv/data/dev/systems/auth-system/postgres/keycloak" { + capabilities = ["read"] +} + +path "kv/data/dev/systems/auth-system/keycloak/bootstrap-admin" { + capabilities = ["read"] +} diff --git a/policies/vault/dev-k3s/workloads/postgres-dev.hcl b/policies/vault/dev-k3s/workloads/postgres-dev.hcl new file mode 100644 index 0000000..f7a7a8e --- /dev/null +++ b/policies/vault/dev-k3s/workloads/postgres-dev.hcl @@ -0,0 +1,11 @@ +path "kv/data/dev/systems/auth-system/postgres/superuser" { + capabilities = ["read"] +} + +path "kv/data/dev/systems/auth-system/postgres/auth-server" { + capabilities = ["read"] +} + +path "kv/data/dev/systems/auth-system/postgres/keycloak" { + capabilities = ["read"] +} diff --git a/renovate.json b/renovate.json index 1af63fb..4b938cf 100644 --- a/renovate.json +++ b/renovate.json @@ -4,20 +4,55 @@ "config:recommended" ], "enabledManagers": [ - "argocd", + "custom.regex", + "kubernetes", "kustomize", "terraform", "terraform-version" ], + "customManagers": [ + { + "customType": "regex", + "managerFilePatterns": [ + "/^platform/control-plane/argocd/application-sets/platform-addons\\.yaml$/" + ], + "matchStrings": [ + "# renovate: datasource=(?\\S+) depName=(?\\S+) registryUrl=(?\\S+)\\s+revision: (?\\S+)" + ] + }, + { + "customType": "regex", + "datasourceTemplate": "docker", + "managerFilePatterns": [ + "/^platform/control-plane/argocd/application-sets/platform-addons\\.yaml$/" + ], + "matchStrings": [ + "# renovate: datasource=docker depName=(?\\S+)\\s+repository: \\S+\\s+tag: \"(?[^@\"]+)@(?sha256:[a-f0-9]{64})\"" + ] + } + ], "dependencyDashboard": true, "pinDigests": true, "packageRules": [ + { + "description": "First-party GHCR images are promoted only by the verified digest workflow", + "matchDatasources": [ + "docker" + ], + "matchPackageNames": [ + "ghcr.io/donghyeonka/project-auth-server", + "ghcr.io/donghyeonka/project-api-server" + ], + "enabled": false + }, { "description": "Require review for cluster controllers and stateful platform components", "matchFileNames": [ - "clusters/dev-k3s/applications/foundation/**", - "clusters/dev-k3s/applications/platform/**", + "platform/control-plane/**", + "clusters/dev-k3s/overlays/platform/**", + "clusters/dev-k3s/overlays/systems/**", "platform/**", + "systems/**", "iac/**" ], "automerge": false diff --git a/platform/auth-system/base/files/keycloak/project-auth-realm.json b/systems/auth-system/base/files/keycloak/project-auth-realm.json similarity index 100% rename from platform/auth-system/base/files/keycloak/project-auth-realm.json rename to systems/auth-system/base/files/keycloak/project-auth-realm.json diff --git a/platform/auth-system/base/files/postgres/01-init-project-auth-databases.sh b/systems/auth-system/base/files/postgres/01-init-project-auth-databases.sh similarity index 100% rename from platform/auth-system/base/files/postgres/01-init-project-auth-databases.sh rename to systems/auth-system/base/files/postgres/01-init-project-auth-databases.sh diff --git a/platform/auth-system/base/keycloak-client-sync-job.yaml b/systems/auth-system/base/keycloak-client-sync-job.yaml similarity index 94% rename from platform/auth-system/base/keycloak-client-sync-job.yaml rename to systems/auth-system/base/keycloak-client-sync-job.yaml index 3b95659..602f5f0 100644 --- a/platform/auth-system/base/keycloak-client-sync-job.yaml +++ b/systems/auth-system/base/keycloak-client-sync-job.yaml @@ -33,7 +33,7 @@ spec: ready=false for _ in $(seq 1 60); do if /opt/keycloak/bin/kcadm.sh config credentials \ - --server http://keycloak.platform.svc.cluster.local \ + --server http://keycloak \ --realm master \ --user "$KC_BOOTSTRAP_ADMIN_USERNAME" \ --password "$KC_BOOTSTRAP_ADMIN_PASSWORD" >/dev/null 2>&1; then @@ -73,7 +73,7 @@ spec: - name: KC_BOOTSTRAP_ADMIN_USERNAME valueFrom: configMapKeyRef: - name: platform-config + name: auth-system-config key: KEYCLOAK_BOOTSTRAP_ADMIN_USERNAME - name: KC_BOOTSTRAP_ADMIN_PASSWORD valueFrom: @@ -83,7 +83,7 @@ spec: - name: KEYCLOAK_CLIENT_ID valueFrom: configMapKeyRef: - name: platform-config + name: auth-system-config key: KEYCLOAK_CLIENT_ID - name: KEYCLOAK_CLIENT_SECRET valueFrom: @@ -93,5 +93,5 @@ spec: - name: AUTH_SERVER_BASE_URL valueFrom: configMapKeyRef: - name: platform-config + name: auth-system-config key: AUTH_SERVER_BASE_URL diff --git a/platform/auth-system/base/keycloak-client-sync-serviceaccount.yaml b/systems/auth-system/base/keycloak-client-sync-serviceaccount.yaml similarity index 100% rename from platform/auth-system/base/keycloak-client-sync-serviceaccount.yaml rename to systems/auth-system/base/keycloak-client-sync-serviceaccount.yaml diff --git a/platform/auth-system/base/keycloak-deployment.yaml b/systems/auth-system/base/keycloak-deployment.yaml similarity index 94% rename from platform/auth-system/base/keycloak-deployment.yaml rename to systems/auth-system/base/keycloak-deployment.yaml index 4fb6b27..3bca5be 100644 --- a/platform/auth-system/base/keycloak-deployment.yaml +++ b/systems/auth-system/base/keycloak-deployment.yaml @@ -35,11 +35,11 @@ spec: - name: KC_DB value: postgres - name: KC_DB_URL - value: jdbc:postgresql://postgres.platform.svc.cluster.local:5432/keycloak + value: jdbc:postgresql://postgres:5432/keycloak - name: KC_DB_USERNAME valueFrom: configMapKeyRef: - name: platform-config + name: auth-system-config key: KEYCLOAK_DB_USER - name: KC_DB_PASSWORD valueFrom: @@ -51,7 +51,7 @@ spec: - name: KC_BOOTSTRAP_ADMIN_USERNAME valueFrom: configMapKeyRef: - name: platform-config + name: auth-system-config key: KEYCLOAK_BOOTSTRAP_ADMIN_USERNAME - name: KC_BOOTSTRAP_ADMIN_PASSWORD valueFrom: diff --git a/platform/auth-system/base/keycloak-service.yaml b/systems/auth-system/base/keycloak-service.yaml similarity index 100% rename from platform/auth-system/base/keycloak-service.yaml rename to systems/auth-system/base/keycloak-service.yaml diff --git a/platform/auth-system/base/keycloak-serviceaccount.yaml b/systems/auth-system/base/keycloak-serviceaccount.yaml similarity index 100% rename from platform/auth-system/base/keycloak-serviceaccount.yaml rename to systems/auth-system/base/keycloak-serviceaccount.yaml diff --git a/platform/auth-system/base/kustomization.yaml b/systems/auth-system/base/kustomization.yaml similarity index 100% rename from platform/auth-system/base/kustomization.yaml rename to systems/auth-system/base/kustomization.yaml diff --git a/platform/auth-system/base/postgres-service.yaml b/systems/auth-system/base/postgres-service.yaml similarity index 100% rename from platform/auth-system/base/postgres-service.yaml rename to systems/auth-system/base/postgres-service.yaml diff --git a/platform/auth-system/base/postgres-serviceaccount.yaml b/systems/auth-system/base/postgres-serviceaccount.yaml similarity index 100% rename from platform/auth-system/base/postgres-serviceaccount.yaml rename to systems/auth-system/base/postgres-serviceaccount.yaml diff --git a/platform/auth-system/base/postgres-statefulset.yaml b/systems/auth-system/base/postgres-statefulset.yaml similarity index 93% rename from platform/auth-system/base/postgres-statefulset.yaml rename to systems/auth-system/base/postgres-statefulset.yaml index 11b3332..9cbac8c 100644 --- a/platform/auth-system/base/postgres-statefulset.yaml +++ b/systems/auth-system/base/postgres-statefulset.yaml @@ -32,7 +32,7 @@ spec: - name: POSTGRES_USER valueFrom: configMapKeyRef: - name: platform-config + name: auth-system-config key: POSTGRES_SUPERUSER - name: POSTGRES_PASSWORD valueFrom: @@ -42,17 +42,17 @@ spec: - name: POSTGRES_DB valueFrom: configMapKeyRef: - name: platform-config + name: auth-system-config key: POSTGRES_DEFAULT_DB - name: AUTH_DB_NAME valueFrom: configMapKeyRef: - name: platform-config + name: auth-system-config key: AUTH_DB_NAME - name: AUTH_DB_USER valueFrom: configMapKeyRef: - name: platform-config + name: auth-system-config key: AUTH_DB_USER - name: AUTH_DB_PASSWORD valueFrom: @@ -62,12 +62,12 @@ spec: - name: KEYCLOAK_DB_NAME valueFrom: configMapKeyRef: - name: platform-config + name: auth-system-config key: KEYCLOAK_DB_NAME - name: KEYCLOAK_DB_USER valueFrom: configMapKeyRef: - name: platform-config + name: auth-system-config key: KEYCLOAK_DB_USER - name: KEYCLOAK_DB_PASSWORD valueFrom: