Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a6f6c663e0 | ||
|
|
293ee6fc97 |
@@ -0,0 +1,114 @@
|
|||||||
|
name: Promote Dev Image by Pull Request
|
||||||
|
|
||||||
|
on:
|
||||||
|
workflow_dispatch:
|
||||||
|
inputs:
|
||||||
|
service:
|
||||||
|
description: Workload to promote
|
||||||
|
required: true
|
||||||
|
type: choice
|
||||||
|
options:
|
||||||
|
- auth-server
|
||||||
|
- api-server
|
||||||
|
image_digest:
|
||||||
|
description: Immutable OCI digest including the sha256 prefix
|
||||||
|
required: true
|
||||||
|
type: string
|
||||||
|
|
||||||
|
concurrency:
|
||||||
|
group: promote-dev-${{ inputs.service }}
|
||||||
|
cancel-in-progress: false
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
promote:
|
||||||
|
runs-on:
|
||||||
|
- self-hosted
|
||||||
|
- linux
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
token: ${{ secrets.GITOPS_BOT_TOKEN }}
|
||||||
|
|
||||||
|
- name: Update immutable digest
|
||||||
|
env:
|
||||||
|
SERVICE: ${{ inputs.service }}
|
||||||
|
IMAGE_DIGEST: ${{ inputs.image_digest }}
|
||||||
|
run: |
|
||||||
|
case "$SERVICE" in
|
||||||
|
auth-server)
|
||||||
|
image_name="ghcr.io/donghyeonka/project-auth-server"
|
||||||
|
;;
|
||||||
|
api-server)
|
||||||
|
image_name="ghcr.io/donghyeonka/project-api-server"
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
echo "Unsupported service: $SERVICE" >&2
|
||||||
|
exit 1
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
if [[ ! "$IMAGE_DIGEST" =~ ^sha256:[a-f0-9]{64}$ ]]; then
|
||||||
|
echo "image_digest must be an immutable sha256 digest" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
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) }
|
||||||
|
target && $1 == "newTag:" {
|
||||||
|
match($0, /^[[:space:]]*/)
|
||||||
|
print substr($0, RSTART, RLENGTH) "digest: " image_digest
|
||||||
|
target = 0
|
||||||
|
updated = 1
|
||||||
|
next
|
||||||
|
}
|
||||||
|
target && $1 == "digest:" {
|
||||||
|
match($0, /^[[:space:]]*/)
|
||||||
|
print substr($0, RSTART, RLENGTH) "digest: " image_digest
|
||||||
|
target = 0
|
||||||
|
updated = 1
|
||||||
|
next
|
||||||
|
}
|
||||||
|
{ print }
|
||||||
|
END { if (!updated) exit 42 }
|
||||||
|
' "$manifest" >"$temporary"
|
||||||
|
mv "$temporary" "$manifest"
|
||||||
|
kubectl kustomize "$(dirname "$manifest")" >/dev/null
|
||||||
|
|
||||||
|
- name: Create promotion branch
|
||||||
|
env:
|
||||||
|
SERVICE: ${{ inputs.service }}
|
||||||
|
RUN_NUMBER: ${{ gitea.run_number }}
|
||||||
|
run: |
|
||||||
|
branch="gitops/promote-${SERVICE}-${RUN_NUMBER}"
|
||||||
|
git config user.name "gitops-bot"
|
||||||
|
git config user.email "gitops-bot@hyeonworks.local"
|
||||||
|
git switch -c "$branch"
|
||||||
|
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"
|
||||||
|
|
||||||
|
- name: Open Gitea pull request
|
||||||
|
env:
|
||||||
|
GITEA_TOKEN: ${{ secrets.GITOPS_BOT_TOKEN }}
|
||||||
|
GITEA_API_URL: ${{ gitea.api_url }}
|
||||||
|
REPOSITORY: ${{ gitea.repository }}
|
||||||
|
DEFAULT_BRANCH: ${{ gitea.event.repository.default_branch }}
|
||||||
|
SERVICE: ${{ inputs.service }}
|
||||||
|
IMAGE_DIGEST: ${{ inputs.image_digest }}
|
||||||
|
run: |
|
||||||
|
payload="$(
|
||||||
|
jq -n \
|
||||||
|
--arg base "$DEFAULT_BRANCH" \
|
||||||
|
--arg head "$PROMOTION_BRANCH" \
|
||||||
|
--arg title "chore(gitops): promote ${SERVICE} dev digest" \
|
||||||
|
--arg body "Promotes ${SERVICE} to immutable digest ${IMAGE_DIGEST}." \
|
||||||
|
'{base: $base, head: $head, title: $title, body: $body}'
|
||||||
|
)"
|
||||||
|
curl -fsS \
|
||||||
|
-H "Authorization: token ${GITEA_TOKEN}" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d "$payload" \
|
||||||
|
"${GITEA_API_URL}/repos/${REPOSITORY}/pulls"
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
name: Validate GitOps Repository
|
||||||
|
|
||||||
|
on:
|
||||||
|
pull_request:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
|
||||||
|
concurrency:
|
||||||
|
group: validate-gitops-${{ gitea.ref }}
|
||||||
|
cancel-in-progress: true
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
validate:
|
||||||
|
runs-on:
|
||||||
|
- self-hosted
|
||||||
|
- linux
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||||
|
|
||||||
|
- name: Validate
|
||||||
|
run: ./hack/validate.sh
|
||||||
+22
@@ -0,0 +1,22 @@
|
|||||||
|
# Terraform working directories and local state
|
||||||
|
**/.terraform/*
|
||||||
|
**/.terraform-state/*
|
||||||
|
*.tfstate
|
||||||
|
*.tfstate.*
|
||||||
|
*.tfplan
|
||||||
|
*.tfvars
|
||||||
|
!*.tfvars.example
|
||||||
|
crash.log
|
||||||
|
crash.*.log
|
||||||
|
|
||||||
|
# Bootstrap material and local operator files
|
||||||
|
.local/*
|
||||||
|
!.local/.gitkeep
|
||||||
|
*init*.json
|
||||||
|
|
||||||
|
# Tool and editor output
|
||||||
|
.DS_Store
|
||||||
|
.idea/
|
||||||
|
.vscode/
|
||||||
|
*.swp
|
||||||
|
*.tmp
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
# Repository operating rules
|
||||||
|
|
||||||
|
- Treat the Gitea `origin` as the canonical deployment repository.
|
||||||
|
- Production is disabled; do not create or enable production Applications
|
||||||
|
without a complete production design and explicit approval.
|
||||||
|
- Never commit Terraform state, provider directories, plan files, tfvars,
|
||||||
|
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.
|
||||||
|
- Do not apply to a live cluster unless the user explicitly requests live
|
||||||
|
deployment and the kube context has been verified.
|
||||||
@@ -0,0 +1,61 @@
|
|||||||
|
# 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.
|
||||||
|
- Replaced staged Argo roots with one bootstrap seed and one cluster-owned
|
||||||
|
root Application.
|
||||||
|
- Reorganized the repository around `clusters/dev-k3s`, environment-neutral
|
||||||
|
`platform`/`workloads` bases and separate `iac/terraform`.
|
||||||
|
- Corrected the Sealed Secrets Helm repository and added controller resources
|
||||||
|
and key renewal configuration.
|
||||||
|
- Removed incomplete production skeletons; production remains unsupported.
|
||||||
|
- Changed application promotion to a Gitea pull request carrying an immutable
|
||||||
|
OCI digest and removed direct writes to `main`.
|
||||||
|
- Pinned Vault, PostgreSQL, Keycloak, Vault Injector and Sealed Secrets images
|
||||||
|
to verified multi-architecture digests.
|
||||||
|
- Removed routine `kubectl apply`, port-forward orchestration and reusable
|
||||||
|
Vault operator-token scripts.
|
||||||
|
- Added a guarded, dev-only Vault init/unseal/root-revoke entrypoint.
|
||||||
|
- Fixed auth migration and Keycloak sync Job lifecycle and ordering.
|
||||||
|
- Removed selective sync from Applications containing Sync hooks.
|
||||||
|
- Enabled ConfigMap hash rollouts and projected Vault authentication tokens.
|
||||||
|
- Removed the same-cluster Transit Vault and its credential rotation cycle.
|
||||||
|
- Consolidated Terraform into `vault-core` and `vault-database` remote states
|
||||||
|
with write-only/ephemeral credential inputs.
|
||||||
|
- Removed tracked Terraform providers and backend metadata.
|
||||||
|
- Moved machine-consumed Vault policies out of runbooks and narrowed routine
|
||||||
|
automation permissions.
|
||||||
|
- Recorded Gateway API first and deferred Istio ambient adoption criteria.
|
||||||
|
- Reduced shell entrypoints from 18 files/1,205 lines to 3 files/300 lines.
|
||||||
|
- Split current documentation from archived historical material.
|
||||||
+81
-3009
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,42 @@
|
|||||||
|
.PHONY: bootstrap validate vault-init terraform-init terraform-plan terraform-apply check-terraform-inputs
|
||||||
|
|
||||||
|
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)
|
||||||
|
./hack/bootstrap-argocd.sh --context "$(KUBE_CONTEXT)"
|
||||||
|
|
||||||
|
validate:
|
||||||
|
./hack/validate.sh
|
||||||
|
|
||||||
|
vault-init:
|
||||||
|
./hack/vault-init.sh init
|
||||||
|
|
||||||
|
check-terraform-inputs:
|
||||||
|
@case "$(TF_ROOT)" in \
|
||||||
|
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)
|
||||||
|
|
||||||
|
terraform-init: check-terraform-inputs
|
||||||
|
terraform -chdir="$(TF_DIR)" init -input=false -reconfigure -backend-config="$(abspath $(BACKEND_CONFIG))"
|
||||||
|
|
||||||
|
terraform-plan: terraform-init
|
||||||
|
@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)
|
||||||
|
@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)"
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
apiVersion: v1
|
|
||||||
kind: ConfigMap
|
|
||||||
metadata:
|
|
||||||
name: api-server-config
|
|
||||||
data:
|
|
||||||
APP_SERVER_PORT: "8082"
|
|
||||||
APP_SECURITY_OAUTH2_RESOURCESERVER_JWT_ISSUER_URI: http://auth-public.auth-dev.svc.cluster.local
|
|
||||||
@@ -1,13 +0,0 @@
|
|||||||
apiVersion: kustomize.config.k8s.io/v1beta1
|
|
||||||
kind: Kustomization
|
|
||||||
|
|
||||||
namespace: api-prod
|
|
||||||
|
|
||||||
resources:
|
|
||||||
- ../../base
|
|
||||||
- namespace.yaml
|
|
||||||
|
|
||||||
images:
|
|
||||||
- name: ghcr.io/donghyeonka/project-api-server
|
|
||||||
newName: ghcr.io/donghyeonka/project-api-server
|
|
||||||
newTag: fd097c9
|
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
apiVersion: v1
|
|
||||||
kind: Namespace
|
|
||||||
metadata:
|
|
||||||
name: api-prod
|
|
||||||
labels:
|
|
||||||
pod-security.kubernetes.io/enforce: baseline
|
|
||||||
pod-security.kubernetes.io/enforce-version: latest
|
|
||||||
pod-security.kubernetes.io/warn: restricted
|
|
||||||
pod-security.kubernetes.io/warn-version: latest
|
|
||||||
pod-security.kubernetes.io/audit: restricted
|
|
||||||
pod-security.kubernetes.io/audit-version: latest
|
|
||||||
@@ -1,26 +0,0 @@
|
|||||||
apiVersion: v1
|
|
||||||
kind: ConfigMap
|
|
||||||
metadata:
|
|
||||||
name: auth-server-config
|
|
||||||
data:
|
|
||||||
SPRING_PROFILES_ACTIVE: dev
|
|
||||||
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_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_CLIENT_ID: project-auth-server
|
|
||||||
APP_SECURITY_OAUTH2_GOOGLE_REGISTRATION_ID: keycloak-google
|
|
||||||
APP_SECURITY_OAUTH2_GOOGLE_IDP_HINT: google
|
|
||||||
APP_SECURITY_OAUTH2_GITHUB_REGISTRATION_ID: keycloak-github
|
|
||||||
APP_SECURITY_OAUTH2_GITHUB_IDP_HINT: github
|
|
||||||
APP_SECURITY_JWT_ISSUER: http://auth-public.auth-dev.svc.cluster.local
|
|
||||||
APP_SECURITY_JWT_ACTIVE_KEY_ID: dev-vault-rsa-1
|
|
||||||
APP_SECURITY_JWT_GENERATE_KEY_PAIR_ON_STARTUP: "false"
|
|
||||||
APP_SECURITY_JWT_ACCESS_TOKEN_EXPIRATION: PT30M
|
|
||||||
APP_SECURITY_JWT_VAULT_ENABLED: "true"
|
|
||||||
APP_SECURITY_JWT_VAULT_ADDRESS: http://vault.vault.svc.cluster.local:8200
|
|
||||||
APP_SECURITY_JWT_VAULT_MOUNT_PATH: transit
|
|
||||||
APP_SECURITY_JWT_VAULT_TRANSIT_KEY_NAME: project-auth-jwt
|
|
||||||
@@ -1,13 +0,0 @@
|
|||||||
apiVersion: kustomize.config.k8s.io/v1beta1
|
|
||||||
kind: Kustomization
|
|
||||||
|
|
||||||
namespace: auth-prod
|
|
||||||
|
|
||||||
resources:
|
|
||||||
- ../../base
|
|
||||||
- namespace.yaml
|
|
||||||
|
|
||||||
images:
|
|
||||||
- name: ghcr.io/donghyeonka/project-auth-server
|
|
||||||
newName: ghcr.io/donghyeonka/project-auth-server
|
|
||||||
newTag: 5648fd2
|
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
apiVersion: v1
|
|
||||||
kind: Namespace
|
|
||||||
metadata:
|
|
||||||
name: auth-prod
|
|
||||||
labels:
|
|
||||||
pod-security.kubernetes.io/enforce: baseline
|
|
||||||
pod-security.kubernetes.io/enforce-version: latest
|
|
||||||
pod-security.kubernetes.io/warn: restricted
|
|
||||||
pod-security.kubernetes.io/warn-version: latest
|
|
||||||
pod-security.kubernetes.io/audit: restricted
|
|
||||||
pod-security.kubernetes.io/audit-version: latest
|
|
||||||
@@ -1,18 +0,0 @@
|
|||||||
## Argo CD Structure
|
|
||||||
|
|
||||||
`argocd/` 디렉터리는 환경(`dev`, `prod`)과 성격(`apps`, `infra`) 기준으로 나눠 관리합니다.
|
|
||||||
|
|
||||||
- `applications/<env>/apps`: 서비스 애플리케이션 선언
|
|
||||||
- `applications/<env>/infra`: 공용 인프라/컨트롤러 선언
|
|
||||||
- `projects/<env>/apps-project.yaml`: 서비스 애플리케이션용 AppProject
|
|
||||||
- `projects/<env>/infra-project.yaml`: 공용 인프라용 AppProject
|
|
||||||
|
|
||||||
현재 `dev`에는 실제 선언을 두고, `prod`는 이후 운영 확장을 위한 구조와 프로젝트 골격을 먼저 유지합니다.
|
|
||||||
|
|
||||||
현재 dev `infra`에는 대표적으로 아래 Application이 포함됩니다.
|
|
||||||
|
|
||||||
- `vault-transit`: workload Vault transit auto-unseal provider
|
|
||||||
- `vault`: workload Vault
|
|
||||||
- `platform`: postgres, keycloak
|
|
||||||
- `vault-agent-injector`: workload secret injection
|
|
||||||
- `sealed-secrets`: image pull secret 같은 예외 secret 처리
|
|
||||||
@@ -1,34 +0,0 @@
|
|||||||
apiVersion: argoproj.io/v1alpha1
|
|
||||||
kind: Application
|
|
||||||
metadata:
|
|
||||||
name: api-server-dev
|
|
||||||
namespace: argocd
|
|
||||||
annotations:
|
|
||||||
argocd.argoproj.io/sync-wave: "40"
|
|
||||||
finalizers:
|
|
||||||
- resources-finalizer.argocd.argoproj.io
|
|
||||||
spec:
|
|
||||||
project: apps-dev
|
|
||||||
source:
|
|
||||||
repoURL: https://github.com/DongHyeonka/Project-Auth-GitOps
|
|
||||||
targetRevision: main
|
|
||||||
path: apps/api-server/overlays/dev
|
|
||||||
destination:
|
|
||||||
server: https://kubernetes.default.svc
|
|
||||||
namespace: api-dev
|
|
||||||
syncPolicy:
|
|
||||||
automated:
|
|
||||||
enabled: true
|
|
||||||
prune: true
|
|
||||||
selfHeal: true
|
|
||||||
syncOptions:
|
|
||||||
- CreateNamespace=true
|
|
||||||
- PruneLast=true
|
|
||||||
- ApplyOutOfSyncOnly=true
|
|
||||||
retry:
|
|
||||||
limit: 5
|
|
||||||
backoff:
|
|
||||||
duration: 5s
|
|
||||||
factor: 2
|
|
||||||
maxDuration: 3m
|
|
||||||
revisionHistoryLimit: 5
|
|
||||||
@@ -1,34 +0,0 @@
|
|||||||
apiVersion: argoproj.io/v1alpha1
|
|
||||||
kind: Application
|
|
||||||
metadata:
|
|
||||||
name: auth-server-dev
|
|
||||||
namespace: argocd
|
|
||||||
annotations:
|
|
||||||
argocd.argoproj.io/sync-wave: "30"
|
|
||||||
finalizers:
|
|
||||||
- resources-finalizer.argocd.argoproj.io
|
|
||||||
spec:
|
|
||||||
project: apps-dev
|
|
||||||
source:
|
|
||||||
repoURL: https://github.com/DongHyeonka/Project-Auth-GitOps
|
|
||||||
targetRevision: main
|
|
||||||
path: apps/auth-server/overlays/dev
|
|
||||||
destination:
|
|
||||||
server: https://kubernetes.default.svc
|
|
||||||
namespace: auth-dev
|
|
||||||
syncPolicy:
|
|
||||||
automated:
|
|
||||||
enabled: true
|
|
||||||
prune: true
|
|
||||||
selfHeal: true
|
|
||||||
syncOptions:
|
|
||||||
- CreateNamespace=true
|
|
||||||
- PruneLast=true
|
|
||||||
- ApplyOutOfSyncOnly=true
|
|
||||||
retry:
|
|
||||||
limit: 5
|
|
||||||
backoff:
|
|
||||||
duration: 5s
|
|
||||||
factor: 2
|
|
||||||
maxDuration: 3m
|
|
||||||
revisionHistoryLimit: 5
|
|
||||||
@@ -1,34 +0,0 @@
|
|||||||
apiVersion: argoproj.io/v1alpha1
|
|
||||||
kind: Application
|
|
||||||
metadata:
|
|
||||||
name: platform-dev
|
|
||||||
namespace: argocd
|
|
||||||
annotations:
|
|
||||||
argocd.argoproj.io/sync-wave: "20"
|
|
||||||
finalizers:
|
|
||||||
- resources-finalizer.argocd.argoproj.io
|
|
||||||
spec:
|
|
||||||
project: infra-dev
|
|
||||||
source:
|
|
||||||
repoURL: https://github.com/DongHyeonka/Project-Auth-GitOps
|
|
||||||
targetRevision: main
|
|
||||||
path: infra/platform/overlays/dev
|
|
||||||
destination:
|
|
||||||
server: https://kubernetes.default.svc
|
|
||||||
namespace: platform
|
|
||||||
syncPolicy:
|
|
||||||
automated:
|
|
||||||
enabled: true
|
|
||||||
prune: true
|
|
||||||
selfHeal: true
|
|
||||||
syncOptions:
|
|
||||||
- CreateNamespace=true
|
|
||||||
- PruneLast=true
|
|
||||||
- ApplyOutOfSyncOnly=true
|
|
||||||
retry:
|
|
||||||
limit: 5
|
|
||||||
backoff:
|
|
||||||
duration: 5s
|
|
||||||
factor: 2
|
|
||||||
maxDuration: 3m
|
|
||||||
revisionHistoryLimit: 5
|
|
||||||
@@ -1,35 +0,0 @@
|
|||||||
apiVersion: argoproj.io/v1alpha1
|
|
||||||
kind: Application
|
|
||||||
metadata:
|
|
||||||
name: sealed-secrets-dev
|
|
||||||
namespace: argocd
|
|
||||||
finalizers:
|
|
||||||
- resources-finalizer.argocd.argoproj.io
|
|
||||||
spec:
|
|
||||||
project: infra-dev
|
|
||||||
source:
|
|
||||||
repoURL: https://bitnami-labs.github.io/sealed-secrets
|
|
||||||
chart: sealed-secrets
|
|
||||||
targetRevision: 2.17.9
|
|
||||||
helm:
|
|
||||||
values: |
|
|
||||||
fullnameOverride: sealed-secrets-controller
|
|
||||||
destination:
|
|
||||||
server: https://kubernetes.default.svc
|
|
||||||
namespace: kube-system
|
|
||||||
syncPolicy:
|
|
||||||
automated:
|
|
||||||
enabled: true
|
|
||||||
prune: true
|
|
||||||
selfHeal: true
|
|
||||||
syncOptions:
|
|
||||||
- CreateNamespace=true
|
|
||||||
- PruneLast=true
|
|
||||||
- ApplyOutOfSyncOnly=true
|
|
||||||
retry:
|
|
||||||
limit: 5
|
|
||||||
backoff:
|
|
||||||
duration: 5s
|
|
||||||
factor: 2
|
|
||||||
maxDuration: 3m
|
|
||||||
revisionHistoryLimit: 5
|
|
||||||
@@ -1,59 +0,0 @@
|
|||||||
apiVersion: argoproj.io/v1alpha1
|
|
||||||
kind: Application
|
|
||||||
metadata:
|
|
||||||
name: vault-agent-injector-dev
|
|
||||||
namespace: argocd
|
|
||||||
annotations:
|
|
||||||
argocd.argoproj.io/sync-wave: "10"
|
|
||||||
finalizers:
|
|
||||||
- resources-finalizer.argocd.argoproj.io
|
|
||||||
spec:
|
|
||||||
project: infra-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
|
|
||||||
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"
|
|
||||||
destination:
|
|
||||||
server: https://kubernetes.default.svc
|
|
||||||
namespace: vault
|
|
||||||
syncPolicy:
|
|
||||||
automated:
|
|
||||||
enabled: true
|
|
||||||
prune: true
|
|
||||||
selfHeal: true
|
|
||||||
syncOptions:
|
|
||||||
- CreateNamespace=true
|
|
||||||
- PruneLast=true
|
|
||||||
- ApplyOutOfSyncOnly=true
|
|
||||||
retry:
|
|
||||||
limit: 5
|
|
||||||
backoff:
|
|
||||||
duration: 5s
|
|
||||||
factor: 2
|
|
||||||
maxDuration: 3m
|
|
||||||
revisionHistoryLimit: 5
|
|
||||||
@@ -1,34 +0,0 @@
|
|||||||
apiVersion: argoproj.io/v1alpha1
|
|
||||||
kind: Application
|
|
||||||
metadata:
|
|
||||||
name: vault-transit-dev
|
|
||||||
namespace: argocd
|
|
||||||
annotations:
|
|
||||||
argocd.argoproj.io/sync-wave: "10"
|
|
||||||
finalizers:
|
|
||||||
- resources-finalizer.argocd.argoproj.io
|
|
||||||
spec:
|
|
||||||
project: infra-dev
|
|
||||||
source:
|
|
||||||
repoURL: https://github.com/DongHyeonka/Project-Auth-GitOps
|
|
||||||
targetRevision: main
|
|
||||||
path: infra/vault-transit/overlays/dev
|
|
||||||
destination:
|
|
||||||
server: https://kubernetes.default.svc
|
|
||||||
namespace: vault-transit
|
|
||||||
syncPolicy:
|
|
||||||
automated:
|
|
||||||
enabled: true
|
|
||||||
prune: true
|
|
||||||
selfHeal: true
|
|
||||||
syncOptions:
|
|
||||||
- CreateNamespace=true
|
|
||||||
- PruneLast=true
|
|
||||||
- ApplyOutOfSyncOnly=true
|
|
||||||
retry:
|
|
||||||
limit: 5
|
|
||||||
backoff:
|
|
||||||
duration: 5s
|
|
||||||
factor: 2
|
|
||||||
maxDuration: 3m
|
|
||||||
revisionHistoryLimit: 5
|
|
||||||
@@ -1,34 +0,0 @@
|
|||||||
apiVersion: argoproj.io/v1alpha1
|
|
||||||
kind: Application
|
|
||||||
metadata:
|
|
||||||
name: vault-dev
|
|
||||||
namespace: argocd
|
|
||||||
annotations:
|
|
||||||
argocd.argoproj.io/sync-wave: "10"
|
|
||||||
finalizers:
|
|
||||||
- resources-finalizer.argocd.argoproj.io
|
|
||||||
spec:
|
|
||||||
project: infra-dev
|
|
||||||
source:
|
|
||||||
repoURL: https://github.com/DongHyeonka/Project-Auth-GitOps
|
|
||||||
targetRevision: main
|
|
||||||
path: infra/vault/overlays/dev
|
|
||||||
destination:
|
|
||||||
server: https://kubernetes.default.svc
|
|
||||||
namespace: vault
|
|
||||||
syncPolicy:
|
|
||||||
automated:
|
|
||||||
enabled: true
|
|
||||||
prune: true
|
|
||||||
selfHeal: true
|
|
||||||
syncOptions:
|
|
||||||
- CreateNamespace=true
|
|
||||||
- PruneLast=true
|
|
||||||
- ApplyOutOfSyncOnly=true
|
|
||||||
retry:
|
|
||||||
limit: 5
|
|
||||||
backoff:
|
|
||||||
duration: 5s
|
|
||||||
factor: 2
|
|
||||||
maxDuration: 3m
|
|
||||||
revisionHistoryLimit: 5
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
|
|
||||||
@@ -1,68 +0,0 @@
|
|||||||
apiVersion: argoproj.io/v1alpha1
|
|
||||||
kind: AppProject
|
|
||||||
metadata:
|
|
||||||
name: infra-dev
|
|
||||||
namespace: argocd
|
|
||||||
finalizers:
|
|
||||||
- resources-finalizer.argocd.argoproj.io
|
|
||||||
spec:
|
|
||||||
description: Dev shared infrastructure managed by Argo CD
|
|
||||||
sourceRepos:
|
|
||||||
- https://github.com/DongHyeonka/Project-Auth-GitOps
|
|
||||||
- https://bitnami-labs.github.io/sealed-secrets
|
|
||||||
- https://helm.releases.hashicorp.com
|
|
||||||
destinations:
|
|
||||||
- namespace: platform
|
|
||||||
server: https://kubernetes.default.svc
|
|
||||||
- namespace: vault
|
|
||||||
server: https://kubernetes.default.svc
|
|
||||||
- namespace: vault-transit
|
|
||||||
server: https://kubernetes.default.svc
|
|
||||||
- namespace: kube-system
|
|
||||||
server: https://kubernetes.default.svc
|
|
||||||
clusterResourceWhitelist:
|
|
||||||
- group: ""
|
|
||||||
kind: Namespace
|
|
||||||
- group: "apiextensions.k8s.io"
|
|
||||||
kind: CustomResourceDefinition
|
|
||||||
- group: "rbac.authorization.k8s.io"
|
|
||||||
kind: ClusterRole
|
|
||||||
- group: "rbac.authorization.k8s.io"
|
|
||||||
kind: ClusterRoleBinding
|
|
||||||
- group: "admissionregistration.k8s.io"
|
|
||||||
kind: MutatingWebhookConfiguration
|
|
||||||
namespaceResourceWhitelist:
|
|
||||||
- group: ""
|
|
||||||
kind: ConfigMap
|
|
||||||
- group: ""
|
|
||||||
kind: Secret
|
|
||||||
- group: ""
|
|
||||||
kind: Service
|
|
||||||
- group: ""
|
|
||||||
kind: ServiceAccount
|
|
||||||
- group: ""
|
|
||||||
kind: PersistentVolumeClaim
|
|
||||||
- group: "bitnami.com"
|
|
||||||
kind: SealedSecret
|
|
||||||
- group: "rbac.authorization.k8s.io"
|
|
||||||
kind: Role
|
|
||||||
- group: "rbac.authorization.k8s.io"
|
|
||||||
kind: RoleBinding
|
|
||||||
- group: "apps"
|
|
||||||
kind: Deployment
|
|
||||||
- group: "apps"
|
|
||||||
kind: StatefulSet
|
|
||||||
- group: "apps"
|
|
||||||
kind: ReplicaSet
|
|
||||||
- group: "autoscaling"
|
|
||||||
kind: HorizontalPodAutoscaler
|
|
||||||
- group: "batch"
|
|
||||||
kind: Job
|
|
||||||
- group: "networking.k8s.io"
|
|
||||||
kind: Ingress
|
|
||||||
- group: "networking.k8s.io"
|
|
||||||
kind: NetworkPolicy
|
|
||||||
- group: "policy"
|
|
||||||
kind: PodDisruptionBudget
|
|
||||||
orphanedResources:
|
|
||||||
warn: true
|
|
||||||
@@ -1,50 +0,0 @@
|
|||||||
apiVersion: argoproj.io/v1alpha1
|
|
||||||
kind: AppProject
|
|
||||||
metadata:
|
|
||||||
name: apps-prod
|
|
||||||
namespace: argocd
|
|
||||||
finalizers:
|
|
||||||
- resources-finalizer.argocd.argoproj.io
|
|
||||||
spec:
|
|
||||||
description: Prod application workloads managed by Argo CD
|
|
||||||
sourceRepos:
|
|
||||||
- https://github.com/DongHyeonka/Project-Auth-GitOps
|
|
||||||
destinations:
|
|
||||||
- namespace: auth-prod
|
|
||||||
server: https://kubernetes.default.svc
|
|
||||||
- namespace: api-prod
|
|
||||||
server: https://kubernetes.default.svc
|
|
||||||
clusterResourceWhitelist:
|
|
||||||
- group: ""
|
|
||||||
kind: Namespace
|
|
||||||
namespaceResourceWhitelist:
|
|
||||||
- group: ""
|
|
||||||
kind: ConfigMap
|
|
||||||
- group: ""
|
|
||||||
kind: Secret
|
|
||||||
- group: ""
|
|
||||||
kind: Service
|
|
||||||
- group: ""
|
|
||||||
kind: ServiceAccount
|
|
||||||
- group: ""
|
|
||||||
kind: PersistentVolumeClaim
|
|
||||||
- group: "bitnami.com"
|
|
||||||
kind: SealedSecret
|
|
||||||
- group: "apps"
|
|
||||||
kind: Deployment
|
|
||||||
- group: "apps"
|
|
||||||
kind: StatefulSet
|
|
||||||
- group: "apps"
|
|
||||||
kind: ReplicaSet
|
|
||||||
- group: "autoscaling"
|
|
||||||
kind: HorizontalPodAutoscaler
|
|
||||||
- group: "batch"
|
|
||||||
kind: Job
|
|
||||||
- group: "networking.k8s.io"
|
|
||||||
kind: Ingress
|
|
||||||
- group: "networking.k8s.io"
|
|
||||||
kind: NetworkPolicy
|
|
||||||
- group: "policy"
|
|
||||||
kind: PodDisruptionBudget
|
|
||||||
orphanedResources:
|
|
||||||
warn: true
|
|
||||||
@@ -1,61 +0,0 @@
|
|||||||
apiVersion: argoproj.io/v1alpha1
|
|
||||||
kind: AppProject
|
|
||||||
metadata:
|
|
||||||
name: infra-prod
|
|
||||||
namespace: argocd
|
|
||||||
finalizers:
|
|
||||||
- resources-finalizer.argocd.argoproj.io
|
|
||||||
spec:
|
|
||||||
description: Prod shared infrastructure managed by Argo CD
|
|
||||||
sourceRepos:
|
|
||||||
- https://github.com/DongHyeonka/Project-Auth-GitOps
|
|
||||||
- https://bitnami-labs.github.io/sealed-secrets
|
|
||||||
destinations:
|
|
||||||
- namespace: platform-prod
|
|
||||||
server: https://kubernetes.default.svc
|
|
||||||
- namespace: kube-system
|
|
||||||
server: https://kubernetes.default.svc
|
|
||||||
clusterResourceWhitelist:
|
|
||||||
- group: ""
|
|
||||||
kind: Namespace
|
|
||||||
- group: "apiextensions.k8s.io"
|
|
||||||
kind: CustomResourceDefinition
|
|
||||||
- group: "rbac.authorization.k8s.io"
|
|
||||||
kind: ClusterRole
|
|
||||||
- group: "rbac.authorization.k8s.io"
|
|
||||||
kind: ClusterRoleBinding
|
|
||||||
namespaceResourceWhitelist:
|
|
||||||
- group: ""
|
|
||||||
kind: ConfigMap
|
|
||||||
- group: ""
|
|
||||||
kind: Secret
|
|
||||||
- group: ""
|
|
||||||
kind: Service
|
|
||||||
- group: ""
|
|
||||||
kind: ServiceAccount
|
|
||||||
- group: ""
|
|
||||||
kind: PersistentVolumeClaim
|
|
||||||
- group: "bitnami.com"
|
|
||||||
kind: SealedSecret
|
|
||||||
- group: "rbac.authorization.k8s.io"
|
|
||||||
kind: Role
|
|
||||||
- group: "rbac.authorization.k8s.io"
|
|
||||||
kind: RoleBinding
|
|
||||||
- group: "apps"
|
|
||||||
kind: Deployment
|
|
||||||
- group: "apps"
|
|
||||||
kind: StatefulSet
|
|
||||||
- group: "apps"
|
|
||||||
kind: ReplicaSet
|
|
||||||
- group: "autoscaling"
|
|
||||||
kind: HorizontalPodAutoscaler
|
|
||||||
- group: "batch"
|
|
||||||
kind: Job
|
|
||||||
- group: "networking.k8s.io"
|
|
||||||
kind: Ingress
|
|
||||||
- group: "networking.k8s.io"
|
|
||||||
kind: NetworkPolicy
|
|
||||||
- group: "policy"
|
|
||||||
kind: PodDisruptionBudget
|
|
||||||
orphanedResources:
|
|
||||||
warn: true
|
|
||||||
@@ -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
|
||||||
@@ -1,8 +1,6 @@
|
|||||||
apiVersion: kustomize.config.k8s.io/v1beta1
|
apiVersion: kustomize.config.k8s.io/v1beta1
|
||||||
kind: Kustomization
|
kind: Kustomization
|
||||||
|
|
||||||
namespace: vault-prod
|
|
||||||
|
|
||||||
resources:
|
resources:
|
||||||
- ../../base
|
- control-plane-project.yaml
|
||||||
- namespace.yaml
|
- root-application.yaml
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
apiVersion: argoproj.io/v1alpha1
|
||||||
|
kind: Application
|
||||||
|
metadata:
|
||||||
|
name: project-gitops-control-plane
|
||||||
|
namespace: argocd
|
||||||
|
spec:
|
||||||
|
project: gitops-control-plane
|
||||||
|
source:
|
||||||
|
repoURL: https://git.learn.hyeonworks.com/donghyeon.kang/project-gitops
|
||||||
|
targetRevision: main
|
||||||
|
path: platform/control-plane/argocd
|
||||||
|
destination:
|
||||||
|
server: https://kubernetes.default.svc
|
||||||
|
namespace: argocd
|
||||||
|
syncPolicy:
|
||||||
|
automated:
|
||||||
|
enabled: true
|
||||||
|
prune: true
|
||||||
|
selfHeal: true
|
||||||
|
syncOptions:
|
||||||
|
- PruneLast=true
|
||||||
|
- FailOnSharedResource=true
|
||||||
|
revisionHistoryLimit: 10
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
ARGOCD_VERSION=v3.4.2
|
||||||
|
ARGOCD_INSTALL_SHA256=69114b8c9eb48a1d08598e6f654a0869b10ae902456ea4b70796cb563760f5ec
|
||||||
@@ -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/<cluster>/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를 먼저 별도 설계해야 합니다.
|
||||||
+1
-1
@@ -4,6 +4,6 @@ kind: Kustomization
|
|||||||
namespace: vault
|
namespace: vault
|
||||||
|
|
||||||
resources:
|
resources:
|
||||||
- ../../base
|
- ../../../../../platform/shared-services/vault/base
|
||||||
- namespace.yaml
|
- namespace.yaml
|
||||||
- networkpolicy.yaml
|
- networkpolicy.yaml
|
||||||
+2
@@ -2,6 +2,8 @@ apiVersion: v1
|
|||||||
kind: Namespace
|
kind: Namespace
|
||||||
metadata:
|
metadata:
|
||||||
name: vault
|
name: vault
|
||||||
|
annotations:
|
||||||
|
argocd.argoproj.io/sync-options: Prune=confirm,Delete=confirm
|
||||||
labels:
|
labels:
|
||||||
pod-security.kubernetes.io/enforce: baseline
|
pod-security.kubernetes.io/enforce: baseline
|
||||||
pod-security.kubernetes.io/enforce-version: latest
|
pod-security.kubernetes.io/enforce-version: latest
|
||||||
+2
-24
@@ -36,28 +36,6 @@ spec:
|
|||||||
---
|
---
|
||||||
apiVersion: networking.k8s.io/v1
|
apiVersion: networking.k8s.io/v1
|
||||||
kind: NetworkPolicy
|
kind: NetworkPolicy
|
||||||
metadata:
|
|
||||||
name: vault-server-allow-transit-egress
|
|
||||||
spec:
|
|
||||||
podSelector:
|
|
||||||
matchLabels:
|
|
||||||
app: vault
|
|
||||||
policyTypes:
|
|
||||||
- Egress
|
|
||||||
egress:
|
|
||||||
- to:
|
|
||||||
- namespaceSelector:
|
|
||||||
matchLabels:
|
|
||||||
kubernetes.io/metadata.name: vault-transit
|
|
||||||
podSelector:
|
|
||||||
matchLabels:
|
|
||||||
app: vault-transit
|
|
||||||
ports:
|
|
||||||
- protocol: TCP
|
|
||||||
port: 8200
|
|
||||||
---
|
|
||||||
apiVersion: networking.k8s.io/v1
|
|
||||||
kind: NetworkPolicy
|
|
||||||
metadata:
|
metadata:
|
||||||
name: vault-server-allow-postgres-egress
|
name: vault-server-allow-postgres-egress
|
||||||
spec:
|
spec:
|
||||||
@@ -70,7 +48,7 @@ spec:
|
|||||||
- to:
|
- to:
|
||||||
- namespaceSelector:
|
- namespaceSelector:
|
||||||
matchLabels:
|
matchLabels:
|
||||||
kubernetes.io/metadata.name: platform
|
kubernetes.io/metadata.name: auth-system-dev
|
||||||
podSelector:
|
podSelector:
|
||||||
matchLabels:
|
matchLabels:
|
||||||
app: postgres
|
app: postgres
|
||||||
@@ -119,7 +97,7 @@ spec:
|
|||||||
kubernetes.io/metadata.name: auth-dev
|
kubernetes.io/metadata.name: auth-dev
|
||||||
- namespaceSelector:
|
- namespaceSelector:
|
||||||
matchLabels:
|
matchLabels:
|
||||||
kubernetes.io/metadata.name: platform
|
kubernetes.io/metadata.name: auth-system-dev
|
||||||
ports:
|
ports:
|
||||||
- protocol: TCP
|
- protocol: TCP
|
||||||
port: 8200
|
port: 8200
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
POSTGRES_SUPERUSER=postgres
|
||||||
|
POSTGRES_DEFAULT_DB=postgres
|
||||||
|
AUTH_DB_NAME=project_auth
|
||||||
|
AUTH_DB_USER=project_auth
|
||||||
|
KEYCLOAK_DB_NAME=keycloak
|
||||||
|
KEYCLOAK_DB_USER=keycloak
|
||||||
|
KEYCLOAK_BOOTSTRAP_ADMIN_USERNAME=admin
|
||||||
|
KEYCLOAK_CLIENT_ID=project-auth-server
|
||||||
|
AUTH_SERVER_BASE_URL=http://auth-public.auth-dev.svc.cluster.local
|
||||||
+28
-12
@@ -7,20 +7,30 @@ spec:
|
|||||||
metadata:
|
metadata:
|
||||||
annotations:
|
annotations:
|
||||||
vault.hashicorp.com/agent-inject: "true"
|
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-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: |
|
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 }}
|
export KC_BOOTSTRAP_ADMIN_PASSWORD={{ printf "%q" .Data.data.KC_BOOTSTRAP_ADMIN_PASSWORD }}
|
||||||
{{ end }}
|
{{ 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 }}
|
export KEYCLOAK_CLIENT_SECRET={{ printf "%q" .Data.data.KEYCLOAK_CLIENT_SECRET }}
|
||||||
{{ end }}
|
{{ end }}
|
||||||
vault.hashicorp.com/agent-pre-populate-only: "true"
|
vault.hashicorp.com/agent-pre-populate-only: "true"
|
||||||
vault.hashicorp.com/role: keycloak-client-sync-dev
|
vault.hashicorp.com/role: keycloak-client-sync-dev
|
||||||
spec:
|
spec:
|
||||||
automountServiceAccountToken: true
|
automountServiceAccountToken: false
|
||||||
|
volumes:
|
||||||
|
- name: vault-token
|
||||||
|
projected:
|
||||||
|
defaultMode: 0444
|
||||||
|
sources:
|
||||||
|
- serviceAccountToken:
|
||||||
|
audience: vault
|
||||||
|
expirationSeconds: 3600
|
||||||
|
path: token
|
||||||
containers:
|
containers:
|
||||||
- name: keycloak-client-sync
|
- name: keycloak-client-sync
|
||||||
command:
|
command:
|
||||||
@@ -31,13 +41,19 @@ spec:
|
|||||||
. /vault/secrets/keycloak-sync-env
|
. /vault/secrets/keycloak-sync-env
|
||||||
set -eu
|
set -eu
|
||||||
|
|
||||||
until /opt/keycloak/bin/kcadm.sh config credentials \
|
ready=false
|
||||||
--server http://keycloak.platform.svc.cluster.local \
|
for _ in $(seq 1 60); do
|
||||||
--realm master \
|
if /opt/keycloak/bin/kcadm.sh config credentials \
|
||||||
--user "$KC_BOOTSTRAP_ADMIN_USERNAME" \
|
--server http://keycloak \
|
||||||
--password "$KC_BOOTSTRAP_ADMIN_PASSWORD" >/dev/null 2>&1; do
|
--realm master \
|
||||||
|
--user "$KC_BOOTSTRAP_ADMIN_USERNAME" \
|
||||||
|
--password "$KC_BOOTSTRAP_ADMIN_PASSWORD" >/dev/null 2>&1; then
|
||||||
|
ready=true
|
||||||
|
break
|
||||||
|
fi
|
||||||
sleep 5
|
sleep 5
|
||||||
done
|
done
|
||||||
|
test "$ready" = true
|
||||||
|
|
||||||
CLIENT_UUID=$(/opt/keycloak/bin/kcadm.sh get clients \
|
CLIENT_UUID=$(/opt/keycloak/bin/kcadm.sh get clients \
|
||||||
-r project-auth \
|
-r project-auth \
|
||||||
@@ -56,15 +72,15 @@ spec:
|
|||||||
- name: KC_BOOTSTRAP_ADMIN_USERNAME
|
- name: KC_BOOTSTRAP_ADMIN_USERNAME
|
||||||
valueFrom:
|
valueFrom:
|
||||||
configMapKeyRef:
|
configMapKeyRef:
|
||||||
name: platform-config
|
name: auth-system-config
|
||||||
key: KEYCLOAK_BOOTSTRAP_ADMIN_USERNAME
|
key: KEYCLOAK_BOOTSTRAP_ADMIN_USERNAME
|
||||||
- name: KEYCLOAK_CLIENT_ID
|
- name: KEYCLOAK_CLIENT_ID
|
||||||
valueFrom:
|
valueFrom:
|
||||||
configMapKeyRef:
|
configMapKeyRef:
|
||||||
name: platform-config
|
name: auth-system-config
|
||||||
key: KEYCLOAK_CLIENT_ID
|
key: KEYCLOAK_CLIENT_ID
|
||||||
- name: AUTH_SERVER_BASE_URL
|
- name: AUTH_SERVER_BASE_URL
|
||||||
valueFrom:
|
valueFrom:
|
||||||
configMapKeyRef:
|
configMapKeyRef:
|
||||||
name: platform-config
|
name: auth-system-config
|
||||||
key: AUTH_SERVER_BASE_URL
|
key: AUTH_SERVER_BASE_URL
|
||||||
+1
-1
@@ -7,7 +7,7 @@ metadata:
|
|||||||
spec:
|
spec:
|
||||||
ingressClassName: traefik
|
ingressClassName: traefik
|
||||||
rules:
|
rules:
|
||||||
- host: keycloak-public.platform.svc.cluster.local
|
- host: keycloak-public.auth-system-dev.svc.cluster.local
|
||||||
http:
|
http:
|
||||||
paths:
|
paths:
|
||||||
- path: /
|
- path: /
|
||||||
+1
-1
@@ -9,7 +9,7 @@ spec:
|
|||||||
- name: keycloak
|
- name: keycloak
|
||||||
env:
|
env:
|
||||||
- name: KC_HOSTNAME
|
- name: KC_HOSTNAME
|
||||||
value: keycloak-public.platform.svc.cluster.local
|
value: keycloak-public.auth-system-dev.svc.cluster.local
|
||||||
- name: KC_HOSTNAME_STRICT
|
- name: KC_HOSTNAME_STRICT
|
||||||
value: "false"
|
value: "false"
|
||||||
- name: KC_PROXY_HEADERS
|
- name: KC_PROXY_HEADERS
|
||||||
+17
-7
@@ -7,19 +7,29 @@ spec:
|
|||||||
metadata:
|
metadata:
|
||||||
annotations:
|
annotations:
|
||||||
vault.hashicorp.com/agent-inject: "true"
|
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-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: |
|
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 }}
|
export KC_DB_PASSWORD={{ printf "%q" .Data.data.KEYCLOAK_DB_PASSWORD }}
|
||||||
{{ end }}
|
{{ 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 }}
|
export KC_BOOTSTRAP_ADMIN_PASSWORD={{ printf "%q" .Data.data.KC_BOOTSTRAP_ADMIN_PASSWORD }}
|
||||||
{{ end }}
|
{{ end }}
|
||||||
vault.hashicorp.com/role: keycloak-dev
|
vault.hashicorp.com/role: keycloak-dev
|
||||||
spec:
|
spec:
|
||||||
automountServiceAccountToken: true
|
automountServiceAccountToken: false
|
||||||
|
volumes:
|
||||||
|
- name: vault-token
|
||||||
|
projected:
|
||||||
|
defaultMode: 0444
|
||||||
|
sources:
|
||||||
|
- serviceAccountToken:
|
||||||
|
audience: vault
|
||||||
|
expirationSeconds: 3600
|
||||||
|
path: token
|
||||||
containers:
|
containers:
|
||||||
- name: keycloak
|
- name: keycloak
|
||||||
command:
|
command:
|
||||||
@@ -34,16 +44,16 @@ spec:
|
|||||||
- name: KC_DB
|
- name: KC_DB
|
||||||
value: postgres
|
value: postgres
|
||||||
- name: KC_DB_URL
|
- name: KC_DB_URL
|
||||||
value: jdbc:postgresql://postgres.platform.svc.cluster.local:5432/keycloak
|
value: jdbc:postgresql://postgres:5432/keycloak
|
||||||
- name: KC_DB_USERNAME
|
- name: KC_DB_USERNAME
|
||||||
valueFrom:
|
valueFrom:
|
||||||
configMapKeyRef:
|
configMapKeyRef:
|
||||||
name: platform-config
|
name: auth-system-config
|
||||||
key: KEYCLOAK_DB_USER
|
key: KEYCLOAK_DB_USER
|
||||||
- name: KC_HEALTH_ENABLED
|
- name: KC_HEALTH_ENABLED
|
||||||
value: "true"
|
value: "true"
|
||||||
- name: KC_BOOTSTRAP_ADMIN_USERNAME
|
- name: KC_BOOTSTRAP_ADMIN_USERNAME
|
||||||
valueFrom:
|
valueFrom:
|
||||||
configMapKeyRef:
|
configMapKeyRef:
|
||||||
name: platform-config
|
name: auth-system-config
|
||||||
key: KEYCLOAK_BOOTSTRAP_ADMIN_USERNAME
|
key: KEYCLOAK_BOOTSTRAP_ADMIN_USERNAME
|
||||||
+11
-3
@@ -1,16 +1,24 @@
|
|||||||
apiVersion: kustomize.config.k8s.io/v1beta1
|
apiVersion: kustomize.config.k8s.io/v1beta1
|
||||||
kind: Kustomization
|
kind: Kustomization
|
||||||
|
|
||||||
namespace: platform
|
namespace: auth-system-dev
|
||||||
|
|
||||||
resources:
|
resources:
|
||||||
- ../../base
|
- ../../../../../systems/auth-system/base
|
||||||
- namespace.yaml
|
- namespace.yaml
|
||||||
- configmap.yaml
|
|
||||||
- keycloak-ingress.yaml
|
- keycloak-ingress.yaml
|
||||||
- public-access.yaml
|
- public-access.yaml
|
||||||
- networkpolicy.yaml
|
- networkpolicy.yaml
|
||||||
|
|
||||||
|
generatorOptions:
|
||||||
|
annotations:
|
||||||
|
argocd.argoproj.io/sync-wave: "0"
|
||||||
|
|
||||||
|
configMapGenerator:
|
||||||
|
- name: auth-system-config
|
||||||
|
envs:
|
||||||
|
- config.env
|
||||||
|
|
||||||
patches:
|
patches:
|
||||||
- path: postgres.vault-patch.yaml
|
- path: postgres.vault-patch.yaml
|
||||||
- path: keycloak.vault-patch.yaml
|
- path: keycloak.vault-patch.yaml
|
||||||
+3
-1
@@ -1,7 +1,9 @@
|
|||||||
apiVersion: v1
|
apiVersion: v1
|
||||||
kind: Namespace
|
kind: Namespace
|
||||||
metadata:
|
metadata:
|
||||||
name: platform
|
name: auth-system-dev
|
||||||
|
annotations:
|
||||||
|
argocd.argoproj.io/sync-options: Prune=confirm,Delete=confirm
|
||||||
labels:
|
labels:
|
||||||
vault-injection: enabled
|
vault-injection: enabled
|
||||||
pod-security.kubernetes.io/enforce: baseline
|
pod-security.kubernetes.io/enforce: baseline
|
||||||
+7
-7
@@ -1,7 +1,7 @@
|
|||||||
apiVersion: networking.k8s.io/v1
|
apiVersion: networking.k8s.io/v1
|
||||||
kind: NetworkPolicy
|
kind: NetworkPolicy
|
||||||
metadata:
|
metadata:
|
||||||
name: platform-default-deny
|
name: auth-system-default-deny
|
||||||
spec:
|
spec:
|
||||||
podSelector: {}
|
podSelector: {}
|
||||||
policyTypes:
|
policyTypes:
|
||||||
@@ -11,7 +11,7 @@ spec:
|
|||||||
apiVersion: networking.k8s.io/v1
|
apiVersion: networking.k8s.io/v1
|
||||||
kind: NetworkPolicy
|
kind: NetworkPolicy
|
||||||
metadata:
|
metadata:
|
||||||
name: platform-allow-dns-egress
|
name: auth-system-allow-dns-egress
|
||||||
spec:
|
spec:
|
||||||
podSelector: {}
|
podSelector: {}
|
||||||
policyTypes:
|
policyTypes:
|
||||||
@@ -33,7 +33,7 @@ spec:
|
|||||||
apiVersion: networking.k8s.io/v1
|
apiVersion: networking.k8s.io/v1
|
||||||
kind: NetworkPolicy
|
kind: NetworkPolicy
|
||||||
metadata:
|
metadata:
|
||||||
name: platform-allow-vault-egress
|
name: auth-system-allow-vault-egress
|
||||||
spec:
|
spec:
|
||||||
podSelector: {}
|
podSelector: {}
|
||||||
policyTypes:
|
policyTypes:
|
||||||
@@ -53,7 +53,7 @@ spec:
|
|||||||
apiVersion: networking.k8s.io/v1
|
apiVersion: networking.k8s.io/v1
|
||||||
kind: NetworkPolicy
|
kind: NetworkPolicy
|
||||||
metadata:
|
metadata:
|
||||||
name: platform-allow-keycloak-egress-to-postgres
|
name: auth-system-allow-keycloak-egress-to-postgres
|
||||||
spec:
|
spec:
|
||||||
podSelector:
|
podSelector:
|
||||||
matchLabels:
|
matchLabels:
|
||||||
@@ -72,7 +72,7 @@ spec:
|
|||||||
apiVersion: networking.k8s.io/v1
|
apiVersion: networking.k8s.io/v1
|
||||||
kind: NetworkPolicy
|
kind: NetworkPolicy
|
||||||
metadata:
|
metadata:
|
||||||
name: platform-allow-keycloak-client-sync-to-keycloak
|
name: auth-system-allow-keycloak-client-sync-to-keycloak
|
||||||
spec:
|
spec:
|
||||||
podSelector:
|
podSelector:
|
||||||
matchLabels:
|
matchLabels:
|
||||||
@@ -91,7 +91,7 @@ spec:
|
|||||||
apiVersion: networking.k8s.io/v1
|
apiVersion: networking.k8s.io/v1
|
||||||
kind: NetworkPolicy
|
kind: NetworkPolicy
|
||||||
metadata:
|
metadata:
|
||||||
name: platform-allow-keycloak-ingress
|
name: auth-system-allow-keycloak-ingress
|
||||||
spec:
|
spec:
|
||||||
podSelector:
|
podSelector:
|
||||||
matchLabels:
|
matchLabels:
|
||||||
@@ -120,7 +120,7 @@ spec:
|
|||||||
apiVersion: networking.k8s.io/v1
|
apiVersion: networking.k8s.io/v1
|
||||||
kind: NetworkPolicy
|
kind: NetworkPolicy
|
||||||
metadata:
|
metadata:
|
||||||
name: platform-allow-postgres-ingress
|
name: auth-system-allow-postgres-ingress
|
||||||
spec:
|
spec:
|
||||||
podSelector:
|
podSelector:
|
||||||
matchLabels:
|
matchLabels:
|
||||||
+21
-11
@@ -7,23 +7,33 @@ spec:
|
|||||||
metadata:
|
metadata:
|
||||||
annotations:
|
annotations:
|
||||||
vault.hashicorp.com/agent-inject: "true"
|
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-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: |
|
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 }}
|
export POSTGRES_PASSWORD={{ printf "%q" .Data.data.POSTGRES_SUPERUSER_PASSWORD }}
|
||||||
{{ end }}
|
{{ 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 }}
|
export AUTH_DB_PASSWORD={{ printf "%q" .Data.data.AUTH_DB_PASSWORD }}
|
||||||
{{ end }}
|
{{ 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 }}
|
export KEYCLOAK_DB_PASSWORD={{ printf "%q" .Data.data.KEYCLOAK_DB_PASSWORD }}
|
||||||
{{ end }}
|
{{ end }}
|
||||||
vault.hashicorp.com/role: postgres-dev
|
vault.hashicorp.com/role: postgres-dev
|
||||||
spec:
|
spec:
|
||||||
automountServiceAccountToken: true
|
automountServiceAccountToken: false
|
||||||
|
volumes:
|
||||||
|
- name: vault-token
|
||||||
|
projected:
|
||||||
|
defaultMode: 0444
|
||||||
|
sources:
|
||||||
|
- serviceAccountToken:
|
||||||
|
audience: vault
|
||||||
|
expirationSeconds: 3600
|
||||||
|
path: token
|
||||||
containers:
|
containers:
|
||||||
- name: postgres
|
- name: postgres
|
||||||
command:
|
command:
|
||||||
@@ -40,30 +50,30 @@ spec:
|
|||||||
- name: POSTGRES_USER
|
- name: POSTGRES_USER
|
||||||
valueFrom:
|
valueFrom:
|
||||||
configMapKeyRef:
|
configMapKeyRef:
|
||||||
name: platform-config
|
name: auth-system-config
|
||||||
key: POSTGRES_SUPERUSER
|
key: POSTGRES_SUPERUSER
|
||||||
- name: POSTGRES_DB
|
- name: POSTGRES_DB
|
||||||
valueFrom:
|
valueFrom:
|
||||||
configMapKeyRef:
|
configMapKeyRef:
|
||||||
name: platform-config
|
name: auth-system-config
|
||||||
key: POSTGRES_DEFAULT_DB
|
key: POSTGRES_DEFAULT_DB
|
||||||
- name: AUTH_DB_NAME
|
- name: AUTH_DB_NAME
|
||||||
valueFrom:
|
valueFrom:
|
||||||
configMapKeyRef:
|
configMapKeyRef:
|
||||||
name: platform-config
|
name: auth-system-config
|
||||||
key: AUTH_DB_NAME
|
key: AUTH_DB_NAME
|
||||||
- name: AUTH_DB_USER
|
- name: AUTH_DB_USER
|
||||||
valueFrom:
|
valueFrom:
|
||||||
configMapKeyRef:
|
configMapKeyRef:
|
||||||
name: platform-config
|
name: auth-system-config
|
||||||
key: AUTH_DB_USER
|
key: AUTH_DB_USER
|
||||||
- name: KEYCLOAK_DB_NAME
|
- name: KEYCLOAK_DB_NAME
|
||||||
valueFrom:
|
valueFrom:
|
||||||
configMapKeyRef:
|
configMapKeyRef:
|
||||||
name: platform-config
|
name: auth-system-config
|
||||||
key: KEYCLOAK_DB_NAME
|
key: KEYCLOAK_DB_NAME
|
||||||
- name: KEYCLOAK_DB_USER
|
- name: KEYCLOAK_DB_USER
|
||||||
valueFrom:
|
valueFrom:
|
||||||
configMapKeyRef:
|
configMapKeyRef:
|
||||||
name: platform-config
|
name: auth-system-config
|
||||||
key: KEYCLOAK_DB_USER
|
key: KEYCLOAK_DB_USER
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
APP_SERVER_PORT=8082
|
||||||
|
APP_SECURITY_OAUTH2_RESOURCESERVER_JWT_ISSUER_URI=http://auth-public.auth-dev.svc.cluster.local
|
||||||
+1
@@ -3,6 +3,7 @@ kind: Ingress
|
|||||||
metadata:
|
metadata:
|
||||||
name: api-server
|
name: api-server
|
||||||
annotations:
|
annotations:
|
||||||
|
argocd.argoproj.io/sync-wave: "20"
|
||||||
traefik.ingress.kubernetes.io/router.entrypoints: web
|
traefik.ingress.kubernetes.io/router.entrypoints: web
|
||||||
spec:
|
spec:
|
||||||
ingressClassName: traefik
|
ingressClassName: traefik
|
||||||
+8
-3
@@ -4,16 +4,21 @@ kind: Kustomization
|
|||||||
namespace: api-dev
|
namespace: api-dev
|
||||||
|
|
||||||
resources:
|
resources:
|
||||||
- ../../base
|
- ../../../../../workloads/api-server/base
|
||||||
- namespace.yaml
|
- namespace.yaml
|
||||||
- configmap.yaml
|
|
||||||
- ingress.yaml
|
- ingress.yaml
|
||||||
- public-access.yaml
|
- public-access.yaml
|
||||||
- networkpolicy.yaml
|
- networkpolicy.yaml
|
||||||
- ghcr-regcred.sealedsecret.yaml
|
- ghcr-regcred.sealedsecret.yaml
|
||||||
|
|
||||||
generatorOptions:
|
generatorOptions:
|
||||||
disableNameSuffixHash: true
|
annotations:
|
||||||
|
argocd.argoproj.io/sync-wave: "0"
|
||||||
|
|
||||||
|
configMapGenerator:
|
||||||
|
- name: api-server-config
|
||||||
|
envs:
|
||||||
|
- config.env
|
||||||
|
|
||||||
images:
|
images:
|
||||||
- name: ghcr.io/donghyeonka/project-api-server
|
- name: ghcr.io/donghyeonka/project-api-server
|
||||||
+2
@@ -2,6 +2,8 @@ apiVersion: v1
|
|||||||
kind: Namespace
|
kind: Namespace
|
||||||
metadata:
|
metadata:
|
||||||
name: api-dev
|
name: api-dev
|
||||||
|
annotations:
|
||||||
|
argocd.argoproj.io/sync-options: Prune=confirm,Delete=confirm
|
||||||
labels:
|
labels:
|
||||||
pod-security.kubernetes.io/enforce: baseline
|
pod-security.kubernetes.io/enforce: baseline
|
||||||
pod-security.kubernetes.io/enforce-version: latest
|
pod-security.kubernetes.io/enforce-version: latest
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
SPRING_PROFILES_ACTIVE=dev
|
||||||
|
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.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.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
|
||||||
|
APP_SECURITY_OAUTH2_GITHUB_REGISTRATION_ID=keycloak-github
|
||||||
|
APP_SECURITY_OAUTH2_GITHUB_IDP_HINT=github
|
||||||
|
APP_SECURITY_JWT_ISSUER=http://auth-public.auth-dev.svc.cluster.local
|
||||||
|
APP_SECURITY_JWT_ACTIVE_KEY_ID=dev-vault-rsa-1
|
||||||
|
APP_SECURITY_JWT_GENERATE_KEY_PAIR_ON_STARTUP=false
|
||||||
|
APP_SECURITY_JWT_ACCESS_TOKEN_EXPIRATION=PT30M
|
||||||
|
APP_SECURITY_JWT_VAULT_ENABLED=true
|
||||||
|
APP_SECURITY_JWT_VAULT_ADDRESS=http://vault.vault.svc.cluster.local:8200
|
||||||
|
APP_SECURITY_JWT_VAULT_MOUNT_PATH=transit
|
||||||
|
APP_SECURITY_JWT_VAULT_TRANSIT_KEY_NAME=project-auth-jwt
|
||||||
+11
-1
@@ -7,6 +7,7 @@ spec:
|
|||||||
metadata:
|
metadata:
|
||||||
annotations:
|
annotations:
|
||||||
vault.hashicorp.com/agent-inject: "true"
|
vault.hashicorp.com/agent-inject: "true"
|
||||||
|
vault.hashicorp.com/agent-service-account-token-volume-name: vault-token
|
||||||
vault.hashicorp.com/agent-inject-secret-migration-env: database/creds/auth-db-migration-dev
|
vault.hashicorp.com/agent-inject-secret-migration-env: database/creds/auth-db-migration-dev
|
||||||
vault.hashicorp.com/agent-inject-template-migration-env: |
|
vault.hashicorp.com/agent-inject-template-migration-env: |
|
||||||
{{- with secret "database/creds/auth-db-migration-dev" -}}
|
{{- with secret "database/creds/auth-db-migration-dev" -}}
|
||||||
@@ -18,7 +19,16 @@ spec:
|
|||||||
vault.hashicorp.com/agent-run-as-user: "10001"
|
vault.hashicorp.com/agent-run-as-user: "10001"
|
||||||
vault.hashicorp.com/role: auth-db-migration-dev
|
vault.hashicorp.com/role: auth-db-migration-dev
|
||||||
spec:
|
spec:
|
||||||
automountServiceAccountToken: true
|
automountServiceAccountToken: false
|
||||||
|
volumes:
|
||||||
|
- name: vault-token
|
||||||
|
projected:
|
||||||
|
defaultMode: 0444
|
||||||
|
sources:
|
||||||
|
- serviceAccountToken:
|
||||||
|
audience: vault
|
||||||
|
expirationSeconds: 3600
|
||||||
|
path: token
|
||||||
containers:
|
containers:
|
||||||
- name: auth-db-migration
|
- name: auth-db-migration
|
||||||
command:
|
command:
|
||||||
+14
-4
@@ -8,14 +8,15 @@ spec:
|
|||||||
annotations:
|
annotations:
|
||||||
vault.hashicorp.com/agent-cache-enable: "true"
|
vault.hashicorp.com/agent-cache-enable: "true"
|
||||||
vault.hashicorp.com/agent-inject: "true"
|
vault.hashicorp.com/agent-inject: "true"
|
||||||
vault.hashicorp.com/agent-inject-secret-runtime-env: kv/data/dev/platform/postgres/auth-server
|
vault.hashicorp.com/agent-service-account-token-volume-name: vault-token
|
||||||
|
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: |
|
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_USERNAME={{ printf "%q" .Data.data.APP_DATASOURCE_USERNAME }}
|
||||||
export APP_DATASOURCE_PASSWORD={{ printf "%q" .Data.data.APP_DATASOURCE_PASSWORD }}
|
export APP_DATASOURCE_PASSWORD={{ printf "%q" .Data.data.APP_DATASOURCE_PASSWORD }}
|
||||||
{{ end }}
|
{{ 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 }}
|
export APP_SECURITY_OAUTH2_KEYCLOAK_CLIENT_SECRET={{ printf "%q" .Data.data.APP_SECURITY_OAUTH2_KEYCLOAK_CLIENT_SECRET }}
|
||||||
{{ end }}
|
{{ end }}
|
||||||
vault.hashicorp.com/agent-inject-token: "true"
|
vault.hashicorp.com/agent-inject-token: "true"
|
||||||
@@ -23,7 +24,16 @@ spec:
|
|||||||
vault.hashicorp.com/agent-run-as-user: "10001"
|
vault.hashicorp.com/agent-run-as-user: "10001"
|
||||||
vault.hashicorp.com/role: auth-server-dev
|
vault.hashicorp.com/role: auth-server-dev
|
||||||
spec:
|
spec:
|
||||||
automountServiceAccountToken: true
|
automountServiceAccountToken: false
|
||||||
|
volumes:
|
||||||
|
- name: vault-token
|
||||||
|
projected:
|
||||||
|
defaultMode: 0444
|
||||||
|
sources:
|
||||||
|
- serviceAccountToken:
|
||||||
|
audience: vault
|
||||||
|
expirationSeconds: 3600
|
||||||
|
path: token
|
||||||
containers:
|
containers:
|
||||||
- name: auth-server
|
- name: auth-server
|
||||||
command:
|
command:
|
||||||
+1
@@ -3,6 +3,7 @@ kind: Ingress
|
|||||||
metadata:
|
metadata:
|
||||||
name: auth-server
|
name: auth-server
|
||||||
annotations:
|
annotations:
|
||||||
|
argocd.argoproj.io/sync-wave: "20"
|
||||||
traefik.ingress.kubernetes.io/router.entrypoints: web
|
traefik.ingress.kubernetes.io/router.entrypoints: web
|
||||||
spec:
|
spec:
|
||||||
ingressClassName: traefik
|
ingressClassName: traefik
|
||||||
+11
-6
@@ -4,9 +4,8 @@ kind: Kustomization
|
|||||||
namespace: auth-dev
|
namespace: auth-dev
|
||||||
|
|
||||||
resources:
|
resources:
|
||||||
- ../../base
|
- ../../../../../workloads/auth-server/base
|
||||||
- namespace.yaml
|
- namespace.yaml
|
||||||
- configmap.yaml
|
|
||||||
- ingress.yaml
|
- ingress.yaml
|
||||||
- public-access.yaml
|
- public-access.yaml
|
||||||
- networkpolicy.yaml
|
- networkpolicy.yaml
|
||||||
@@ -17,9 +16,15 @@ patches:
|
|||||||
- path: db-migration-job.vault-patch.yaml
|
- path: db-migration-job.vault-patch.yaml
|
||||||
|
|
||||||
generatorOptions:
|
generatorOptions:
|
||||||
disableNameSuffixHash: true
|
annotations:
|
||||||
|
argocd.argoproj.io/sync-wave: "0"
|
||||||
|
|
||||||
|
configMapGenerator:
|
||||||
|
- name: auth-server-config
|
||||||
|
envs:
|
||||||
|
- config.env
|
||||||
|
|
||||||
images:
|
images:
|
||||||
- name: ghcr.io/donghyeonka/project-auth-server
|
- name: ghcr.io/donghyeonka/project-auth-server
|
||||||
newName: ghcr.io/donghyeonka/project-auth-server
|
newName: ghcr.io/donghyeonka/project-auth-server
|
||||||
newTag: 1f47f2c
|
newTag: 1f47f2c
|
||||||
+2
@@ -2,6 +2,8 @@ apiVersion: v1
|
|||||||
kind: Namespace
|
kind: Namespace
|
||||||
metadata:
|
metadata:
|
||||||
name: auth-dev
|
name: auth-dev
|
||||||
|
annotations:
|
||||||
|
argocd.argoproj.io/sync-options: Prune=confirm,Delete=confirm
|
||||||
labels:
|
labels:
|
||||||
vault-injection: enabled
|
vault-injection: enabled
|
||||||
pod-security.kubernetes.io/enforce: baseline
|
pod-security.kubernetes.io/enforce: baseline
|
||||||
+2
-2
@@ -33,7 +33,7 @@ spec:
|
|||||||
apiVersion: networking.k8s.io/v1
|
apiVersion: networking.k8s.io/v1
|
||||||
kind: NetworkPolicy
|
kind: NetworkPolicy
|
||||||
metadata:
|
metadata:
|
||||||
name: auth-dev-allow-platform-and-vault-egress
|
name: auth-dev-allow-auth-system-and-vault-egress
|
||||||
spec:
|
spec:
|
||||||
podSelector: {}
|
podSelector: {}
|
||||||
policyTypes:
|
policyTypes:
|
||||||
@@ -42,7 +42,7 @@ spec:
|
|||||||
- to:
|
- to:
|
||||||
- namespaceSelector:
|
- namespaceSelector:
|
||||||
matchLabels:
|
matchLabels:
|
||||||
kubernetes.io/metadata.name: platform
|
kubernetes.io/metadata.name: auth-system-dev
|
||||||
podSelector:
|
podSelector:
|
||||||
matchLabels:
|
matchLabels:
|
||||||
app: postgres
|
app: postgres
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
# ADR 0001: Internal Gitea is canonical
|
||||||
|
|
||||||
|
Status: accepted
|
||||||
|
|
||||||
|
The internal repository
|
||||||
|
`https://git.learn.hyeonworks.com/donghyeon.kang/project-gitops` is the only
|
||||||
|
writable deployment source.
|
||||||
|
|
||||||
|
Argo CD and Gitea Actions use this URL. A GitHub copy may exist only as a
|
||||||
|
read-only mirror with monitored replication; it must never be an independent
|
||||||
|
deployment branch. GHCR remains an image registry and does not require GitHub
|
||||||
|
to host the GitOps source.
|
||||||
@@ -0,0 +1,55 @@
|
|||||||
|
# ADR 0002: Terraform state ownership
|
||||||
|
|
||||||
|
Status: accepted
|
||||||
|
|
||||||
|
Updated: 2026-07-26
|
||||||
|
|
||||||
|
Terraform의 현재 범위는 Vault API 객체입니다. Kubernetes 리소스는 Argo
|
||||||
|
CD가 소유하며, machine/network provisioning은 provider와 운영 경계가
|
||||||
|
확정될 때 별도 root로 추가합니다.
|
||||||
|
|
||||||
|
`dev-k3s`는 정확히 세 state를 사용합니다.
|
||||||
|
|
||||||
|
| 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 |
|
||||||
|
|
||||||
|
Resource 또는 Vault API path 하나는 한 state에만 속합니다. State 사이는
|
||||||
|
이름 contract와 실행 순서만 공유하며 `terraform_remote_state`로 서로의
|
||||||
|
snapshot을 읽지 않습니다. Backend는 encryption, versioning, access
|
||||||
|
control, locking을 제공해야 합니다.
|
||||||
|
|
||||||
|
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에서 제거합니다.
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
# ADR 0003: Vault topology
|
||||||
|
|
||||||
|
Status: accepted for dev, production decision pending
|
||||||
|
|
||||||
|
동일한 단일 노드 K3s 안의 두 Vault는 failure domain을 분리하지 못하면서
|
||||||
|
초기화, Transit credential, rotation과 staged apply 절차를 추가했다.
|
||||||
|
따라서 `dev-k3s`는 단일 self-hosted Vault로 단순화한다.
|
||||||
|
|
||||||
|
Dev profile:
|
||||||
|
|
||||||
|
- single-node integrated Raft
|
||||||
|
- Shamir 1-of-1 init/unseal
|
||||||
|
- TLS 미적용
|
||||||
|
- 명시적 backup/recovery runbook
|
||||||
|
|
||||||
|
이 구성은 production에 사용할 수 없다. production은 다음 중 하나를
|
||||||
|
선택해야 한다.
|
||||||
|
|
||||||
|
- managed Vault
|
||||||
|
- workload cluster 밖의 독립 HA Vault
|
||||||
|
- 최소 3-node integrated-Raft + TLS + KMS/HSM auto-unseal + PDB,
|
||||||
|
anti-affinity와 정기 restore exercise
|
||||||
|
|
||||||
|
production Vault가 결정되기 전에는 production manifest와 Terraform root를
|
||||||
|
만들지 않는다.
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
# ADR 0004: Single Argo CD root and stage-gated ApplicationSets
|
||||||
|
|
||||||
|
Status: accepted
|
||||||
|
|
||||||
|
Updated: 2026-07-26
|
||||||
|
|
||||||
|
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만 허용합니다.
|
||||||
|
|
||||||
|
반복 `kubectl apply`와 category별 root wrapper는 사용하지 않습니다.
|
||||||
|
Routine deployment는 Git merge만으로 시작합니다.
|
||||||
|
|
||||||
|
각 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`를 사용하지 않습니다.
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
# ADR 0005: Cluster-first repository layout
|
||||||
|
|
||||||
|
Status: accepted
|
||||||
|
|
||||||
|
Updated: 2026-07-26
|
||||||
|
|
||||||
|
현재는 단일 `dev-k3s`와 소수 workload를 다루므로 GitOps configuration
|
||||||
|
monorepo를 유지합니다. Application source repository와 deployment
|
||||||
|
configuration repository는 분리합니다. 이 저장소 자체는 독립 reference
|
||||||
|
lab이며 범용 platform product로 간주하지 않습니다.
|
||||||
|
|
||||||
|
- `platform/`, `systems/`, `workloads/`: ownership별 base; 환경 중립은
|
||||||
|
목표 contract
|
||||||
|
- `clusters/<cluster>/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
|
||||||
|
|
||||||
|
`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은 유지하지 않습니다.
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
# ADR 0006: Gateway API first, Istio deferred
|
||||||
|
|
||||||
|
Status: accepted
|
||||||
|
|
||||||
|
현재 단일 노드 K3s와 auth/api 중심 workload에는 service mesh 운영 비용을
|
||||||
|
정당화할 mTLS identity, L7 authorization, canary traffic policy 또는
|
||||||
|
multi-team 요구가 없다. 이번 개편에는 Istio를 설치하지 않는다.
|
||||||
|
|
||||||
|
선행 작업:
|
||||||
|
|
||||||
|
1. Traefik Gateway API provider와 GatewayClass 검증
|
||||||
|
2. Ingress를 Gateway/HTTPRoute로 이관
|
||||||
|
3. north-south TLS
|
||||||
|
4. 내부 호출의 ingress hairpin 제거
|
||||||
|
5. Vault/PostgreSQL native TLS
|
||||||
|
6. NetworkPolicy regression test와 observability/SLO
|
||||||
|
|
||||||
|
Istio 요구가 실제화되면 sidecar가 아니라 ambient mode로 제한 pilot한다.
|
||||||
|
초기 범위는 api-server와 auth-server이며 Vault, Vault injector,
|
||||||
|
PostgreSQL은 제외한다. ztunnel L4부터 시작하고 L7 정책이 필요할 때만
|
||||||
|
waypoint를 추가한다.
|
||||||
|
|
||||||
|
다음 기능 요구 중 두 개 이상과 운영 선행조건이 모두 충족될 때 ADR을
|
||||||
|
재검토한다.
|
||||||
|
|
||||||
|
- ServiceAccount identity 기반 east-west mTLS
|
||||||
|
- path/JWT 기반 L7 authorization
|
||||||
|
- canary traffic split/retry/timeout/outlier detection
|
||||||
|
- 지속적인 서비스·namespace·팀 증가
|
||||||
|
- application instrumentation만으로 해결하기 어려운 장애 분석
|
||||||
|
|
||||||
|
현재 Ingress를 즉시 제거하지 않는다. TLS, DNS, GatewayClass 계약이
|
||||||
|
확정되기 전 가상의 Gateway 설정을 배포하지 않기 위함이다.
|
||||||
@@ -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/<cluster>/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`를 따른다.
|
||||||
@@ -0,0 +1,159 @@
|
|||||||
|
# Argo CD architecture
|
||||||
|
|
||||||
|
## 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
|
||||||
|
platform/control-plane/argocd
|
||||||
|
├── projects
|
||||||
|
│ ├── platform-addons.yaml
|
||||||
|
│ ├── platform-services.yaml
|
||||||
|
│ ├── systems.yaml
|
||||||
|
│ └── workloads.yaml
|
||||||
|
└── application-sets
|
||||||
|
├── platform-addons.yaml
|
||||||
|
├── platform-services.yaml
|
||||||
|
├── systems.yaml
|
||||||
|
└── workloads.yaml
|
||||||
|
```
|
||||||
|
|
||||||
|
Root는 AppProject와 ApplicationSet까지만 직접 소유합니다. 각
|
||||||
|
ApplicationSet의 list inventory가 실제 child Application을 생성합니다.
|
||||||
|
Routine 변경에 category별 root나 직접 `kubectl apply`를 추가하지 않습니다.
|
||||||
|
|
||||||
|
## AppProject boundary
|
||||||
|
|
||||||
|
| 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` |
|
||||||
|
|
||||||
|
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/<cluster>/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는 이 리팩터링 리뷰 범위에 포함되지 않습니다.
|
||||||
@@ -0,0 +1,117 @@
|
|||||||
|
# Deployment architecture
|
||||||
|
|
||||||
|
## Reconciliation boundaries
|
||||||
|
|
||||||
|
```text
|
||||||
|
Gitea main
|
||||||
|
|
|
||||||
|
+-- Argo CD root
|
||||||
|
| -> AppProjects + ApplicationSets
|
||||||
|
| -> generated Applications
|
||||||
|
| -> Kubernetes
|
||||||
|
|
|
||||||
|
+-- approved Terraform runner
|
||||||
|
-> one of three Vault states
|
||||||
|
-> Vault API
|
||||||
|
```
|
||||||
|
|
||||||
|
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가 아닙니다.
|
||||||
|
|
||||||
|
## Kubernetes ownership
|
||||||
|
|
||||||
|
| 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를 합친 최종 구성 |
|
||||||
|
|
||||||
|
현재 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 안에서는 다음 ordering을 사용합니다.
|
||||||
|
|
||||||
|
- generated ConfigMap과 일반 리소스: wave `0`
|
||||||
|
- database migration Sync hook: wave `5`
|
||||||
|
- Deployment: wave `10`
|
||||||
|
- north-south route: wave `20`
|
||||||
|
|
||||||
|
`auth-system`의 Keycloak client sync도 idempotent Sync hook이며 deadline,
|
||||||
|
backoff, `BeforeHookCreation,HookSucceeded` cleanup을 사용합니다.
|
||||||
|
Application 간 준비 순서와 Application 내부 hook 순서를 혼동하지
|
||||||
|
않습니다.
|
||||||
|
|
||||||
|
## Stateful lifecycle
|
||||||
|
|
||||||
|
Vault PVC에는 `Prune=confirm,Delete=confirm`이 명시되어 있습니다.
|
||||||
|
PostgreSQL PVC는 StatefulSet `volumeClaimTemplates`가 생성하며 현재
|
||||||
|
manifest에 별도 Argo prune annotation이 없습니다. Namespace와 generated
|
||||||
|
Application 삭제 보호만 믿지 말고 PostgreSQL retention/backup을 직접
|
||||||
|
확인해야 합니다. Path, namespace, Application 이름을 이동할 때는 다음을
|
||||||
|
별도 migration으로 다룹니다.
|
||||||
|
|
||||||
|
1. 기존 live object와 owner를 inventory합니다.
|
||||||
|
2. 새 owner가 같은 object를 안전하게 추적할 수 있는지 render/diff로
|
||||||
|
확인합니다.
|
||||||
|
3. Stateful data backup과 rollback 지점을 확보합니다.
|
||||||
|
4. 기존 owner를 non-cascading 방식으로 제거한 뒤 새 owner를 연결합니다.
|
||||||
|
|
||||||
|
이번 리팩터링에서는 `platform` namespace의 auth-system을
|
||||||
|
`auth-system-dev`로 옮기는 live 작업을 실행하지 않았습니다.
|
||||||
|
|
||||||
|
## 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로
|
||||||
|
교체합니다.
|
||||||
@@ -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/<name>/base
|
||||||
|
systems/<system>/base
|
||||||
|
workloads/<workload>/base
|
||||||
|
|
||||||
|
clusters/<cluster>/overlays/platform/<name>
|
||||||
|
clusters/<cluster>/overlays/systems/<system>
|
||||||
|
clusters/<cluster>/overlays/workloads/<workload>
|
||||||
|
|
||||||
|
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/<worker>/base` |
|
||||||
|
| dev worker digest/secret annotation | `clusters/dev-k3s/overlays/workloads/<worker>` |
|
||||||
|
| Vault workload policy/role | `vault-workloads` Terraform state와 `policies/vault/` |
|
||||||
|
| Vault auth backend | `vault-foundation` Terraform state |
|
||||||
|
|
||||||
|
분류가 애매하면 설치 순서가 아니라 owner와 소비자 경계를 ADR에 먼저
|
||||||
|
기록합니다.
|
||||||
@@ -0,0 +1,127 @@
|
|||||||
|
# Secret trust boundaries
|
||||||
|
|
||||||
|
## Dev Vault
|
||||||
|
|
||||||
|
`dev-k3s`는 workload 클러스터 안의 단일 self-hosted Vault를 사용합니다.
|
||||||
|
별도 Transit Vault는 두지 않습니다. Vault는 다음 API 객체를 제공합니다.
|
||||||
|
|
||||||
|
- KV-v2 runtime secret path
|
||||||
|
- Kubernetes auth와 workload role
|
||||||
|
- 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이 필요합니다.
|
||||||
|
|
||||||
|
## Three Terraform states
|
||||||
|
|
||||||
|
```text
|
||||||
|
vault-foundation
|
||||||
|
-> mounts/auth configuration
|
||||||
|
-> delegated automation policies
|
||||||
|
-> optional, separated CI JWT login roles
|
||||||
|
|
||||||
|
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가 사용하며
|
||||||
|
application container에 Kubernetes bearer token을 직접 노출하지 않습니다.
|
||||||
|
|
||||||
|
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은 다음 작업에만 사용합니다.
|
||||||
|
|
||||||
|
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
|
||||||
|
- Kubernetes API egress CIDR가 현재 dev cluster에 종속
|
||||||
|
- 정적 bootstrap secret은 coordinated rotation 필요
|
||||||
|
- Namespace/path migration이 아직 실제 cluster에 적용되지 않음
|
||||||
|
|
||||||
|
이 제약은 production에서 허용되지 않습니다.
|
||||||
@@ -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/<cluster>/machine
|
||||||
|
iac/terraform/live/<cluster>/vault-foundation
|
||||||
|
iac/terraform/live/<cluster>/vault-workloads
|
||||||
|
iac/terraform/live/<cluster>/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)
|
||||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,6 @@
|
|||||||
|
# Archived documentation
|
||||||
|
|
||||||
|
Files in this directory describe historical repository states. Paths,
|
||||||
|
workflows, credentials and operational commands may no longer exist.
|
||||||
|
|
||||||
|
Do not execute archived procedures. Use the root README and `docs/runbooks/`.
|
||||||
@@ -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 <application-name> \
|
||||||
|
-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 <application-name>
|
||||||
|
```
|
||||||
|
|
||||||
|
삭제 후 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과 별도 승인 없이는
|
||||||
|
사용하지 않습니다.
|
||||||
@@ -0,0 +1,341 @@
|
|||||||
|
# Bootstrap an empty dev-k3s cluster
|
||||||
|
|
||||||
|
이 runbook은 폐기 가능한 빈 개발 클러스터만 대상으로 합니다. Production과
|
||||||
|
기존 live cluster migration에는 사용하지 않습니다.
|
||||||
|
|
||||||
|
> 2026-07-26 repository 리팩터링 중에는 아래 절차를 실행하지 않았습니다.
|
||||||
|
> 이 문서는 승인된 future bootstrap 절차이며 명령 예시는 자동 실행 대상이
|
||||||
|
> 아닙니다.
|
||||||
|
|
||||||
|
## 1. Preflight
|
||||||
|
|
||||||
|
```bash
|
||||||
|
kubectl config current-context
|
||||||
|
kubectl cluster-info
|
||||||
|
make validate
|
||||||
|
```
|
||||||
|
|
||||||
|
의도한 빈 dev cluster가 아니면 중단합니다. 내부 Gitea가 private이면 Argo
|
||||||
|
CD가 root repository를 읽을 수 있는 read-only credential을 외부 secret
|
||||||
|
authority에서 먼저 provision해야 합니다. Credential은 이 저장소에
|
||||||
|
commit하지 않습니다.
|
||||||
|
|
||||||
|
세 remote state backend 파일을 준비합니다.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
mkdir -p .local/terraform-backend/dev-k3s
|
||||||
|
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은 파일에 넣지 않습니다. 세 backend key가 서로 다르고 locking이
|
||||||
|
활성화됐는지 확인합니다.
|
||||||
|
|
||||||
|
## 2. Argo CD와 root Application
|
||||||
|
|
||||||
|
```bash
|
||||||
|
make bootstrap KUBE_CONTEXT="$(kubectl config current-context)"
|
||||||
|
kubectl -n argocd get appproject gitops-control-plane
|
||||||
|
kubectl -n argocd get application project-gitops-control-plane
|
||||||
|
kubectl -n argocd get applicationsets
|
||||||
|
```
|
||||||
|
|
||||||
|
직접 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에서
|
||||||
|
port-forward합니다. 이는 최초 dev bootstrap용이며 routine runner 모델이
|
||||||
|
아닙니다.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
kubectl -n vault wait --for=create pod -l app=vault --timeout=300s
|
||||||
|
kubectl -n vault port-forward deployment/vault 8200:8200
|
||||||
|
```
|
||||||
|
|
||||||
|
별도 terminal:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
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 working copy는 root revoke 때까지 mode `0600`으로
|
||||||
|
유지하며 shell history나 CI log에 token을 출력하지 않습니다.
|
||||||
|
|
||||||
|
## 4. Vault foundation
|
||||||
|
|
||||||
|
Foundation은 초기 root token으로 한 번 적용합니다.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
export TF_VAR_vault_addr="$VAULT_ADDR"
|
||||||
|
export TF_VAR_vault_token="$(
|
||||||
|
jq -r '.root_token' .local/vault/dev-k3s-init.json
|
||||||
|
)"
|
||||||
|
|
||||||
|
make terraform-plan \
|
||||||
|
TF_ROOT=vault-foundation \
|
||||||
|
BACKEND_CONFIG=.local/terraform-backend/dev-k3s/vault-foundation.s3.hcl
|
||||||
|
|
||||||
|
make terraform-apply \
|
||||||
|
TF_ROOT=vault-foundation \
|
||||||
|
BACKEND_CONFIG=.local/terraform-backend/dev-k3s/vault-foundation.s3.hcl \
|
||||||
|
APPROVE_APPLY=dev-k3s/vault-foundation
|
||||||
|
```
|
||||||
|
|
||||||
|
Plan에는 mounts, auth configuration, workloads/database automation
|
||||||
|
policy와, OIDC/JWT를 명시적으로 구성한 경우에만 분리된 CI JWT role이
|
||||||
|
있어야 합니다. Workload runtime policy, workload Kubernetes role,
|
||||||
|
application Transit key, database connection이 보이면 중단합니다.
|
||||||
|
|
||||||
|
CI JWT를 구성할 때 workloads/database exact claim map은 repository와
|
||||||
|
protected ref를 묶고, 최소 한 공통 job discriminator key에 서로 다른 값을
|
||||||
|
가져야 합니다. 실제 issuer token payload로 그 claim을 확인하지 못하면
|
||||||
|
OIDC/JWT 입력을 비워 둡니다.
|
||||||
|
|
||||||
|
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
|
||||||
|
echo
|
||||||
|
read -r -s -p "Keycloak database password: " KEYCLOAK_DB_PASSWORD
|
||||||
|
echo
|
||||||
|
read -r -s -p "Keycloak bootstrap admin password: " KEYCLOAK_ADMIN_PASSWORD
|
||||||
|
echo
|
||||||
|
read -r -s -p "Auth-server Keycloak client secret: " KEYCLOAK_CLIENT_SECRET
|
||||||
|
echo
|
||||||
|
|
||||||
|
secret_file="$(mktemp)"
|
||||||
|
trap 'rm -f "$secret_file"' EXIT
|
||||||
|
chmod 0600 "$secret_file"
|
||||||
|
|
||||||
|
jq -n --arg password "$POSTGRES_SUPERUSER_PASSWORD" \
|
||||||
|
'{POSTGRES_SUPERUSER_PASSWORD: $password}' >"$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/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/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/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/workloads/auth-server/keycloak-client @"$secret_file"
|
||||||
|
|
||||||
|
rm -f "$secret_file"
|
||||||
|
trap - EXIT
|
||||||
|
unset VAULT_TOKEN
|
||||||
|
```
|
||||||
|
|
||||||
|
## 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 auth-system-dev rollout status statefulset/postgres --timeout=600s
|
||||||
|
kubectl -n auth-system-dev rollout status deployment/keycloak --timeout=600s
|
||||||
|
```
|
||||||
|
|
||||||
|
## 9. Vault database state
|
||||||
|
|
||||||
|
`TF_VAR_vault_token`을 database 전용 short-lived token으로 교체하고
|
||||||
|
PostgreSQL credential을 실행 시점에만 전달합니다.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
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
|
||||||
|
|
||||||
|
make terraform-plan \
|
||||||
|
TF_ROOT=vault-database \
|
||||||
|
BACKEND_CONFIG=.local/terraform-backend/dev-k3s/vault-database.s3.hcl
|
||||||
|
|
||||||
|
make terraform-apply \
|
||||||
|
TF_ROOT=vault-database \
|
||||||
|
BACKEND_CONFIG=.local/terraform-backend/dev-k3s/vault-database.s3.hcl \
|
||||||
|
APPROVE_APPLY=dev-k3s/vault-database
|
||||||
|
```
|
||||||
|
|
||||||
|
Plan에는 `database/config/auth-system-postgres-dev` connection과
|
||||||
|
`auth-db-migration-dev` dynamic role만 있어야 합니다. Dynamic credential
|
||||||
|
발급과 revoke를 검증합니다.
|
||||||
|
|
||||||
|
## 10. Root token 폐기
|
||||||
|
|
||||||
|
Database state와 delegated login/recovery 절차를 검증한 뒤, application
|
||||||
|
gate를 열기 전에 initial root token을 폐기합니다.
|
||||||
|
Script는 두 replacement 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
|
||||||
|
|
||||||
|
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
|
||||||
|
```
|
||||||
|
|
||||||
|
`revoke-root`는 local init JSON에서도 root token field를 제거합니다. 두
|
||||||
|
bootstrap orphan token도 검증 직후 self-revoke하며 routine credential로
|
||||||
|
재사용하지 않습니다.
|
||||||
|
|
||||||
|
## 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 auth-system-dev get pods
|
||||||
|
kubectl -n auth-dev get pods
|
||||||
|
kubectl -n api-dev get pods
|
||||||
|
```
|
||||||
|
|
||||||
|
Encrypted custody로 옮긴 init material의 local working copy는 조직의 dev
|
||||||
|
recovery 정책에 따라 제거합니다. 실패를 고치기 위해 live child manifest를
|
||||||
|
직접 수정하지 말고 Git PR을 사용합니다.
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
# Sealed Secrets backup and recovery
|
||||||
|
|
||||||
|
Existing SealedSecrets are decryptable only with a controller private key.
|
||||||
|
Back up every key after first install and after rotation.
|
||||||
|
|
||||||
|
## Backup
|
||||||
|
|
||||||
|
Use an encrypted operator workstation:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
umask 077
|
||||||
|
kubectl -n kube-system get secret \
|
||||||
|
-l sealedsecrets.bitnami.com/sealed-secrets-key \
|
||||||
|
-o json > .local/sealed-secrets-keys.json
|
||||||
|
chmod 0600 .local/sealed-secrets-keys.json
|
||||||
|
```
|
||||||
|
|
||||||
|
Encrypt the file with the organization's recovery mechanism, store at least
|
||||||
|
two independently controlled copies, and delete the plaintext working copy.
|
||||||
|
Record cluster, date and checksum without recording private key data.
|
||||||
|
|
||||||
|
## Restore
|
||||||
|
|
||||||
|
Restore keys before application SealedSecrets sync:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
kubectl -n kube-system apply -f .local/sealed-secrets-keys.json
|
||||||
|
kubectl -n kube-system rollout restart deployment/sealed-secrets-controller
|
||||||
|
kubectl -n kube-system rollout status deployment/sealed-secrets-controller
|
||||||
|
```
|
||||||
|
|
||||||
|
Verify both `auth-dev/ghcr-regcred` and `api-dev/ghcr-regcred` are created.
|
||||||
|
|
||||||
|
If no key backup exists, old ciphertext cannot be recovered. Create a new
|
||||||
|
controller key and reseal every Secret from the original credential source.
|
||||||
|
|
||||||
|
## Rotation
|
||||||
|
|
||||||
|
The chart requests periodic key renewal. Old keys must remain until all
|
||||||
|
ciphertext has been resealed and verified. GHCR credential rotation requires:
|
||||||
|
|
||||||
|
1. issue an organization-owned read-only package credential;
|
||||||
|
2. create namespace-scoped SealedSecrets with the current controller cert;
|
||||||
|
3. merge and verify image pulls;
|
||||||
|
4. revoke the previous credential;
|
||||||
|
5. update the encrypted key backup.
|
||||||
@@ -0,0 +1,244 @@
|
|||||||
|
# Terraform state migration to three Vault states
|
||||||
|
|
||||||
|
새 클러스터에는 이 runbook이 필요하지 않습니다. Legacy `vault-core` 또는
|
||||||
|
더 오래된 `provider-foundation`, `workload-foundation`, `workload-config`,
|
||||||
|
`database-config` state가 실제로 존재할 때만 사용합니다.
|
||||||
|
|
||||||
|
> 2026-07-26 리팩터링에서는 이 절차를 실제 backend나 cluster에 실행하지
|
||||||
|
> 않았습니다. Maintenance window, 독립 backup과 승인 없이 시작하지
|
||||||
|
> 않습니다.
|
||||||
|
|
||||||
|
State 이동은 live object 삭제보다 위험할 수 있습니다. State split,
|
||||||
|
Terraform module refactor, Vault KV path/namespace cutover를 한 apply에
|
||||||
|
섞지 않습니다.
|
||||||
|
|
||||||
|
## 목표 ownership
|
||||||
|
|
||||||
|
| Legacy ownership | 목표 state |
|
||||||
|
|---|---|
|
||||||
|
| `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 절차 |
|
||||||
|
|
||||||
|
목표 backend key는 각각 달라야 합니다.
|
||||||
|
|
||||||
|
```text
|
||||||
|
dev-k3s/vault-foundation.tfstate
|
||||||
|
dev-k3s/vault-workloads.tfstate
|
||||||
|
dev-k3s/vault-database.tfstate
|
||||||
|
```
|
||||||
|
|
||||||
|
동일 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 <policy-name>
|
||||||
|
vault_kubernetes_auth_backend_role auth/kubernetes/role/<role-name>
|
||||||
|
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을 먼저
|
||||||
|
이동합니다.
|
||||||
|
|
||||||
|
## 6. Retired broad/unused access
|
||||||
|
|
||||||
|
다음 legacy object는 새 state의 desired ownership이 아닙니다.
|
||||||
|
|
||||||
|
- 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를
|
||||||
|
삭제하지 않습니다.
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
# Vault backup and recovery
|
||||||
|
|
||||||
|
`dev-k3s` Vault는 single-node integrated Raft입니다. upgrade, Terraform
|
||||||
|
state migration, storage 작업 전에 snapshot을 생성합니다.
|
||||||
|
|
||||||
|
## Snapshot
|
||||||
|
|
||||||
|
short-lived authorized token과 Vault API 연결을 준비합니다.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
umask 077
|
||||||
|
vault operator raft snapshot save .local/vault/dev-k3s.snap
|
||||||
|
sha256sum .local/vault/dev-k3s.snap
|
||||||
|
chmod 0600 .local/vault/dev-k3s.snap
|
||||||
|
```
|
||||||
|
|
||||||
|
즉시 암호화해 init/unseal material과 다른 위치에 보관합니다. snapshot에는
|
||||||
|
secret payload가 포함됩니다.
|
||||||
|
|
||||||
|
## Recovery material
|
||||||
|
|
||||||
|
- Raft snapshot
|
||||||
|
- unseal key
|
||||||
|
- 현재 manifest revision
|
||||||
|
- Terraform remote state backup
|
||||||
|
- Sealed Secrets controller key
|
||||||
|
|
||||||
|
Initial root token은 recovery material이 아닙니다. bootstrap 완료 후
|
||||||
|
폐기해야 합니다.
|
||||||
|
|
||||||
|
## Restore exercise
|
||||||
|
|
||||||
|
분기마다 isolated disposable cluster에서 다음을 검증합니다.
|
||||||
|
|
||||||
|
1. 동일 Vault version과 storage 설정 배포
|
||||||
|
2. Vault init 후 snapshot restore
|
||||||
|
3. 복구 key로 unseal
|
||||||
|
4. Kubernetes auth 재연결 확인
|
||||||
|
5. KV read, JWT signing, dynamic database lease issue/revoke 확인
|
||||||
|
6. recovery time과 누락된 의존성 기록
|
||||||
|
|
||||||
|
현재 dev 구성은 production recovery 설계가 아닙니다. production은
|
||||||
|
independent HA Vault, TLS, auto-unseal과 더 엄격한 backup custody가
|
||||||
|
필요합니다.
|
||||||
Executable
+61
@@ -0,0 +1,61 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
REPO_ROOT="$(cd "${SCRIPT_DIR}/.." && pwd)"
|
||||||
|
|
||||||
|
usage() {
|
||||||
|
echo "Usage: $0 --context <kube-context>" >&2
|
||||||
|
}
|
||||||
|
|
||||||
|
if [[ "${1:-}" != "--context" || -z "${2:-}" || -n "${3:-}" ]]; then
|
||||||
|
usage
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
expected_context="$2"
|
||||||
|
|
||||||
|
for cmd in curl kubectl sha256sum; do
|
||||||
|
if ! command -v "$cmd" >/dev/null 2>&1; then
|
||||||
|
echo "$cmd is required" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
current_context="$(kubectl config current-context)"
|
||||||
|
if [[ "$current_context" != "$expected_context" ]]; then
|
||||||
|
echo "Refusing bootstrap: current context is ${current_context}, expected ${expected_context}." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
source "${REPO_ROOT}/bootstrap/argocd/version.env"
|
||||||
|
|
||||||
|
manifest="$(mktemp)"
|
||||||
|
cleanup() {
|
||||||
|
rm -f "$manifest"
|
||||||
|
}
|
||||||
|
trap cleanup EXIT INT TERM
|
||||||
|
|
||||||
|
curl -fsSL \
|
||||||
|
"https://raw.githubusercontent.com/argoproj/argo-cd/${ARGOCD_VERSION}/manifests/install.yaml" \
|
||||||
|
-o "$manifest"
|
||||||
|
printf '%s %s\n' "$ARGOCD_INSTALL_SHA256" "$manifest" | sha256sum -c -
|
||||||
|
|
||||||
|
kubectl --context "$expected_context" create namespace argocd --dry-run=client -o yaml |
|
||||||
|
kubectl --context "$expected_context" apply -f -
|
||||||
|
kubectl --context "$expected_context" apply \
|
||||||
|
--server-side \
|
||||||
|
--force-conflicts \
|
||||||
|
-n argocd \
|
||||||
|
-f "$manifest"
|
||||||
|
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}, its control-plane project, and the root Application are installed."
|
||||||
Executable
+277
@@ -0,0 +1,277 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
REPO_ROOT="$(cd "${SCRIPT_DIR}/.." && pwd)"
|
||||||
|
|
||||||
|
for cmd in bash git helm jq kubectl rg terraform; do
|
||||||
|
if ! command -v "$cmd" >/dev/null 2>&1; then
|
||||||
|
echo "$cmd is required" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
cd "$REPO_ROOT"
|
||||||
|
|
||||||
|
forbidden_files="$(
|
||||||
|
git ls-files --cached --others --exclude-standard |
|
||||||
|
rg '(^|/)(\\.terraform|\\.terraform-state|\\.local)(/|$)|(^|/)terraform\\.tfstate($|\\.)|\\.tfplan$' ||
|
||||||
|
true
|
||||||
|
)"
|
||||||
|
if [[ -n "$forbidden_files" ]]; then
|
||||||
|
echo "Generated or state files are tracked:" >&2
|
||||||
|
echo "$forbidden_files" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
while IFS= read -r script; do
|
||||||
|
bash -n "$script"
|
||||||
|
done < <(rg --files -g '*.sh')
|
||||||
|
|
||||||
|
while IFS= read -r script; do
|
||||||
|
if [[ ! -x "$script" ]]; then
|
||||||
|
echo "Shell entry point is not executable: ${script}" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
done < <(rg --files hack -g '*.sh')
|
||||||
|
|
||||||
|
jq empty renovate.json
|
||||||
|
jq empty systems/auth-system/base/files/keycloak/project-auth-realm.json
|
||||||
|
|
||||||
|
overlays=(
|
||||||
|
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 \
|
||||||
|
--version 2.17.9 \
|
||||||
|
--namespace kube-system \
|
||||||
|
--set fullnameOverride=sealed-secrets-controller \
|
||||||
|
--set image.repository=bitnami/sealed-secrets-controller \
|
||||||
|
--set-string image.tag=0.33.1@sha256:e7fad65c2d2f47e48d9ca17408ed56961bfa6a6dd74ccd4a1a214664156534bc
|
||||||
|
)"
|
||||||
|
if [[ "$sealed_secrets_render" != *'image: docker.io/bitnami/sealed-secrets-controller:0.33.1@sha256:e7fad65c2d2f47e48d9ca17408ed56961bfa6a6dd74ccd4a1a214664156534bc'* ]]; then
|
||||||
|
echo "Sealed Secrets image digest was not rendered by the Helm chart." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
vault_injector_render="$(
|
||||||
|
helm template vault-agent-injector vault \
|
||||||
|
--repo https://helm.releases.hashicorp.com \
|
||||||
|
--version 0.32.0 \
|
||||||
|
--namespace vault \
|
||||||
|
--set server.enabled=false \
|
||||||
|
--set injector.enabled=true \
|
||||||
|
--set global.externalVaultAddr=http://vault.vault.svc.cluster.local:8200 \
|
||||||
|
--set global.tlsDisable=true \
|
||||||
|
--set injector.image.repository=hashicorp/vault-k8s \
|
||||||
|
--set-string injector.image.tag=1.7.2@sha256:ae3d307658b72a1cf35dab9bdf92c995d45cdc7183af0516857714b5bd0ba84d \
|
||||||
|
--set injector.agentImage.repository=hashicorp/vault \
|
||||||
|
--set-string injector.agentImage.tag=1.18.5@sha256:750bb37c1638fa194ab37053a81618c61bb0491ddec6fccac87c07a8e6cd8166
|
||||||
|
)"
|
||||||
|
if [[ "$vault_injector_render" != *'image: "hashicorp/vault-k8s:1.7.2@sha256:ae3d307658b72a1cf35dab9bdf92c995d45cdc7183af0516857714b5bd0ba84d"'* ||
|
||||||
|
"$vault_injector_render" != *'value: "hashicorp/vault:1.18.5@sha256:750bb37c1638fa194ab37053a81618c61bb0491ddec6fccac87c07a8e6cd8166"'* ]]; then
|
||||||
|
echo "Vault injector or Agent image digest was not rendered by the Helm chart." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
rendered_images="$(
|
||||||
|
{
|
||||||
|
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
|
||||||
|
echo "A third-party runtime image is not pinned by digest." >&2
|
||||||
|
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-foundation
|
||||||
|
iac/terraform/live/dev-k3s/vault-workloads
|
||||||
|
iac/terraform/live/dev-k3s/vault-database
|
||||||
|
)
|
||||||
|
for root in "${terraform_roots[@]}"; do
|
||||||
|
data_dir="$(mktemp -d)"
|
||||||
|
TF_DATA_DIR="$data_dir" terraform -chdir="$root" init \
|
||||||
|
-backend=false \
|
||||||
|
-input=false \
|
||||||
|
-lockfile=readonly >/dev/null
|
||||||
|
TF_DATA_DIR="$data_dir" terraform -chdir="$root" validate
|
||||||
|
rm -rf "$data_dir"
|
||||||
|
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)|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 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
|
||||||
|
|
||||||
|
if rg -n 'uses:[[:space:]]+[^#[:space:]]+@v[0-9]' .gitea/workflows; then
|
||||||
|
echo "Gitea Actions must be pinned to an immutable commit SHA." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
if rg -n 'git[[:space:]]+push[^#]*(HEAD:)?main([[:space:]]|$)' .gitea hack; then
|
||||||
|
echo "Automation must promote changes through a branch and review, not push directly to main." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
git diff --check
|
||||||
|
echo "Repository validation passed."
|
||||||
Executable
+156
@@ -0,0 +1,156 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
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 rg vault; do
|
||||||
|
if ! command -v "$cmd" >/dev/null 2>&1; then
|
||||||
|
echo "$cmd is required" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
export VAULT_ADDR
|
||||||
|
|
||||||
|
status_json() {
|
||||||
|
local output=""
|
||||||
|
local status_code=0
|
||||||
|
|
||||||
|
set +e
|
||||||
|
output="$(vault status -format=json 2>/dev/null)"
|
||||||
|
status_code=$?
|
||||||
|
set -e
|
||||||
|
|
||||||
|
if [[ "$status_code" -ne 0 && "$status_code" -ne 2 ]]; then
|
||||||
|
echo "Vault is not reachable at ${VAULT_ADDR}." >&2
|
||||||
|
return "$status_code"
|
||||||
|
fi
|
||||||
|
printf '%s\n' "$output"
|
||||||
|
}
|
||||||
|
|
||||||
|
unseal() {
|
||||||
|
local status=""
|
||||||
|
local unseal_key=""
|
||||||
|
|
||||||
|
status="$(status_json)"
|
||||||
|
if [[ "$(jq -r '.sealed' <<<"$status")" == "false" ]]; then
|
||||||
|
echo "Vault is already unsealed."
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
if [[ ! -f "$VAULT_INIT_OUTPUT" ]]; then
|
||||||
|
echo "Init material is unavailable: ${VAULT_INIT_OUTPUT}" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
unseal_key="$(jq -er '.unseal_keys_b64[0]' "$VAULT_INIT_OUTPUT")"
|
||||||
|
vault operator unseal "$unseal_key" >/dev/null
|
||||||
|
echo "Vault is unsealed."
|
||||||
|
}
|
||||||
|
|
||||||
|
init() {
|
||||||
|
local status=""
|
||||||
|
|
||||||
|
status="$(status_json)"
|
||||||
|
if [[ "$(jq -r '.initialized' <<<"$status")" == "true" ]]; then
|
||||||
|
echo "Refusing initialization: Vault is already initialized." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
if [[ -e "$VAULT_INIT_OUTPUT" ]]; then
|
||||||
|
echo "Refusing to overwrite existing init material: ${VAULT_INIT_OUTPUT}" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
umask 077
|
||||||
|
mkdir -p "$(dirname "$VAULT_INIT_OUTPUT")"
|
||||||
|
vault operator init \
|
||||||
|
-key-shares=1 \
|
||||||
|
-key-threshold=1 \
|
||||||
|
-format=json >"$VAULT_INIT_OUTPUT"
|
||||||
|
chmod 0600 "$VAULT_INIT_OUTPUT"
|
||||||
|
unseal
|
||||||
|
|
||||||
|
echo "Dev Vault was initialized with a dev-only 1-of-1 Shamir key."
|
||||||
|
echo "Move ${VAULT_INIT_OUTPUT} to encrypted custody before continuing."
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
temporary="$(mktemp "${VAULT_INIT_OUTPUT}.XXXXXX")"
|
||||||
|
jq 'del(.root_token)' "$VAULT_INIT_OUTPUT" >"$temporary"
|
||||||
|
chmod 0600 "$temporary"
|
||||||
|
mv "$temporary" "$VAULT_INIT_OUTPUT"
|
||||||
|
echo "The initial root token was revoked and removed from the local init file."
|
||||||
|
}
|
||||||
|
|
||||||
|
case "${1:-}" in
|
||||||
|
init)
|
||||||
|
init
|
||||||
|
;;
|
||||||
|
unseal)
|
||||||
|
unseal
|
||||||
|
;;
|
||||||
|
revoke-root)
|
||||||
|
revoke_root
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
echo "Usage: $0 <init|unseal|revoke-root>" >&2
|
||||||
|
exit 1
|
||||||
|
;;
|
||||||
|
esac
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
bucket = "project-gitops-terraform-state"
|
||||||
|
key = "dev-k3s/vault-database.tfstate"
|
||||||
|
region = "us-east-1"
|
||||||
|
encrypt = true
|
||||||
|
use_lockfile = true
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
bucket = "project-gitops-terraform-state"
|
||||||
|
key = "dev-k3s/vault-foundation.tfstate"
|
||||||
|
region = "us-east-1"
|
||||||
|
encrypt = true
|
||||||
|
use_lockfile = true
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
bucket = "project-gitops-terraform-state"
|
||||||
|
key = "dev-k3s/vault-workloads.tfstate"
|
||||||
|
region = "us-east-1"
|
||||||
|
encrypt = true
|
||||||
|
use_lockfile = true
|
||||||
@@ -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",
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,83 @@
|
|||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
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 {
|
||||||
|
database_config_name = "auth-system-postgres-dev"
|
||||||
|
database_mount_path = "database"
|
||||||
|
migration_role_name = "auth-db-migration-dev"
|
||||||
|
|
||||||
|
creation_statements = [
|
||||||
|
<<-EOT
|
||||||
|
CREATE ROLE "{{name}}" WITH LOGIN PASSWORD '{{password}}' VALID UNTIL '{{expiration}}';
|
||||||
|
GRANT "${var.auth_db_role}" TO "{{name}}";
|
||||||
|
EOT
|
||||||
|
]
|
||||||
|
|
||||||
|
revocation_statements = [
|
||||||
|
<<-EOT
|
||||||
|
REASSIGN OWNED BY "{{name}}" TO "${var.auth_db_role}";
|
||||||
|
DROP OWNED BY "{{name}}";
|
||||||
|
REVOKE "${var.auth_db_role}" FROM "{{name}}";
|
||||||
|
DROP ROLE IF EXISTS "{{name}}";
|
||||||
|
EOT
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
postgresql {
|
||||||
|
connection_url = "postgresql://{{username}}:{{password}}@${var.postgres_host}:${var.postgres_port}/${var.postgres_database}?sslmode=${var.postgres_sslmode}"
|
||||||
|
password_authentication = "scram-sha-256"
|
||||||
|
password_wo = var.postgres_admin_password
|
||||||
|
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 = local.database_mount_path
|
||||||
|
creation_statements = local.creation_statements
|
||||||
|
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
|
||||||
|
}
|
||||||
@@ -0,0 +1,77 @@
|
|||||||
|
variable "auth_db_migration_default_ttl_seconds" {
|
||||||
|
description = "Default TTL for migration credentials."
|
||||||
|
type = number
|
||||||
|
default = 3600
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "auth_db_migration_max_ttl_seconds" {
|
||||||
|
description = "Maximum TTL for migration credentials."
|
||||||
|
type = number
|
||||||
|
default = 86400
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "auth_db_role" {
|
||||||
|
description = "Stable PostgreSQL owner role used by dynamic users."
|
||||||
|
type = string
|
||||||
|
default = "project_auth"
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "postgres_admin_password" {
|
||||||
|
description = "PostgreSQL admin password passed only through a write-only provider field."
|
||||||
|
type = string
|
||||||
|
sensitive = true
|
||||||
|
ephemeral = true
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "postgres_admin_password_version" {
|
||||||
|
description = "Increment whenever postgres_admin_password is rotated."
|
||||||
|
type = number
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "postgres_admin_username" {
|
||||||
|
description = "Dedicated database administration username."
|
||||||
|
type = string
|
||||||
|
default = "postgres"
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "postgres_database" {
|
||||||
|
description = "Database in which dynamic migration objects are owned and revoked."
|
||||||
|
type = string
|
||||||
|
default = "project_auth"
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "postgres_host" {
|
||||||
|
description = "Auth system PostgreSQL service DNS name."
|
||||||
|
type = string
|
||||||
|
default = "postgres.auth-system-dev.svc.cluster.local"
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "postgres_port" {
|
||||||
|
description = "Auth system PostgreSQL service port."
|
||||||
|
type = number
|
||||||
|
default = 5432
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "postgres_sslmode" {
|
||||||
|
description = "PostgreSQL SSL mode. Dev currently uses disable; production must use verify-full."
|
||||||
|
type = string
|
||||||
|
default = "disable"
|
||||||
|
|
||||||
|
validation {
|
||||||
|
condition = contains(["disable", "require", "verify-ca", "verify-full"], var.postgres_sslmode)
|
||||||
|
error_message = "postgres_sslmode must be disable, require, verify-ca, or verify-full."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "vault_addr" {
|
||||||
|
description = "Workload Vault API address."
|
||||||
|
type = string
|
||||||
|
default = "http://127.0.0.1:8200"
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "vault_token" {
|
||||||
|
description = "Short-lived token carrying vault-database-automation-dev."
|
||||||
|
type = string
|
||||||
|
sensitive = true
|
||||||
|
ephemeral = true
|
||||||
|
}
|
||||||
@@ -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",
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -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",
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
resource "vault_kubernetes_auth_backend_role" "this" {
|
||||||
|
for_each = var.roles
|
||||||
|
|
||||||
|
audience = one(each.value.audiences)
|
||||||
|
backend = var.backend
|
||||||
|
bound_service_account_names = each.value.service_account_names
|
||||||
|
bound_service_account_namespaces = each.value.service_account_namespaces
|
||||||
|
role_name = each.key
|
||||||
|
token_policies = each.value.token_policies
|
||||||
|
token_ttl = each.value.token_ttl
|
||||||
|
}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
variable "backend" {
|
||||||
|
description = "Kubernetes auth backend path."
|
||||||
|
type = string
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "roles" {
|
||||||
|
description = "Kubernetes auth roles keyed by Vault role name."
|
||||||
|
type = map(object({
|
||||||
|
audiences = set(string)
|
||||||
|
service_account_names = set(string)
|
||||||
|
service_account_namespaces = set(string)
|
||||||
|
token_policies = set(string)
|
||||||
|
token_ttl = number
|
||||||
|
}))
|
||||||
|
}
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
terraform {
|
||||||
|
required_version = ">= 1.11.0"
|
||||||
|
|
||||||
|
required_providers {
|
||||||
|
vault = {
|
||||||
|
source = "hashicorp/vault"
|
||||||
|
version = "~> 5.7.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user