refactor(gitops): establish platform ownership boundaries
This commit is contained in:
@@ -53,7 +53,9 @@ kubectl --context "$expected_context" -n argocd \
|
||||
rollout status deployment/argocd-server --timeout=300s
|
||||
kubectl --context "$expected_context" -n argocd \
|
||||
rollout status deployment/argocd-repo-server --timeout=300s
|
||||
kubectl --context "$expected_context" apply \
|
||||
-f "${REPO_ROOT}/bootstrap/argocd/control-plane-project.yaml"
|
||||
kubectl --context "$expected_context" apply \
|
||||
-f "${REPO_ROOT}/bootstrap/argocd/root-application.yaml"
|
||||
|
||||
echo "Argo CD ${ARGOCD_VERSION} and the dev-k3s root Application are installed."
|
||||
echo "Argo CD ${ARGOCD_VERSION}, its control-plane project, and the root Application are installed."
|
||||
|
||||
+152
-13
@@ -15,7 +15,7 @@ done
|
||||
cd "$REPO_ROOT"
|
||||
|
||||
forbidden_files="$(
|
||||
git ls-files |
|
||||
git ls-files --cached --others --exclude-standard |
|
||||
rg '(^|/)(\\.terraform|\\.terraform-state|\\.local)(/|$)|(^|/)terraform\\.tfstate($|\\.)|\\.tfplan$' ||
|
||||
true
|
||||
)"
|
||||
@@ -36,19 +36,106 @@ while IFS= read -r script; do
|
||||
fi
|
||||
done < <(rg --files hack -g '*.sh')
|
||||
|
||||
jq empty platform/auth-system/base/files/keycloak/project-auth-realm.json
|
||||
jq empty renovate.json
|
||||
jq empty systems/auth-system/base/files/keycloak/project-auth-realm.json
|
||||
|
||||
overlays=(
|
||||
clusters/dev-k3s
|
||||
clusters/dev-k3s/manifests/api-server
|
||||
clusters/dev-k3s/manifests/auth-server
|
||||
clusters/dev-k3s/manifests/auth-system
|
||||
clusters/dev-k3s/manifests/vault
|
||||
bootstrap/argocd
|
||||
platform/control-plane/argocd
|
||||
clusters/dev-k3s/overlays/platform/vault
|
||||
clusters/dev-k3s/overlays/systems/auth-system
|
||||
clusters/dev-k3s/overlays/workloads/auth-server
|
||||
clusters/dev-k3s/overlays/workloads/api-server
|
||||
)
|
||||
for overlay in "${overlays[@]}"; do
|
||||
kubectl kustomize "$overlay" >/dev/null
|
||||
done
|
||||
|
||||
control_plane_render="$(kubectl kustomize platform/control-plane/argocd)"
|
||||
if [[ "$(rg -c '^kind: AppProject$' <<<"$control_plane_render")" -ne 4 ||
|
||||
"$(rg -c '^kind: ApplicationSet$' <<<"$control_plane_render")" -ne 4 ]]; then
|
||||
echo "The control plane must render exactly four AppProjects and four ApplicationSets." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
application_manifests="$(
|
||||
rg -l '^kind:[[:space:]]+Application$' \
|
||||
--glob '*.yaml' \
|
||||
--glob '*.yml' \
|
||||
. |
|
||||
sort
|
||||
)"
|
||||
if [[ "$application_manifests" != "./bootstrap/argocd/root-application.yaml" ]]; then
|
||||
echo "Only the bootstrap root may be an explicit Argo CD Application:" >&2
|
||||
echo "$application_manifests" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if ! rg -q '^[[:space:]]+project:[[:space:]]+gitops-control-plane$' \
|
||||
bootstrap/argocd/root-application.yaml ||
|
||||
rg -n '^[[:space:]]+project:[[:space:]]+default$' bootstrap platform/control-plane; then
|
||||
echo "The root and generated Applications must use explicit least-privilege AppProjects." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
applicationsets=(
|
||||
platform/control-plane/argocd/application-sets/platform-addons.yaml
|
||||
platform/control-plane/argocd/application-sets/platform-services.yaml
|
||||
platform/control-plane/argocd/application-sets/systems.yaml
|
||||
platform/control-plane/argocd/application-sets/workloads.yaml
|
||||
)
|
||||
for applicationset in "${applicationsets[@]}"; do
|
||||
for safety_setting in \
|
||||
'missingkey=error' \
|
||||
'applicationsSync: create-update' \
|
||||
'preserveResourcesOnDeletion: true' \
|
||||
'Prune=confirm,Delete=confirm'; do
|
||||
if ! rg -q "$safety_setting" "$applicationset"; then
|
||||
echo "${applicationset} is missing ApplicationSet safety setting: ${safety_setting}" >&2
|
||||
exit 1
|
||||
fi
|
||||
done
|
||||
done
|
||||
|
||||
if ! rg -q '^ - Prune=confirm$' \
|
||||
platform/control-plane/argocd/application-sets/platform-addons.yaml; then
|
||||
echo "Platform addons must require approval before pruning chart resources." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if rg -n 'project:[[:space:]]+"?\{\{' platform/control-plane/argocd/application-sets; then
|
||||
echo "ApplicationSet projects are privilege boundaries and must never be templated." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
git_applicationsets=(
|
||||
platform/control-plane/argocd/application-sets/platform-services.yaml
|
||||
platform/control-plane/argocd/application-sets/systems.yaml
|
||||
platform/control-plane/argocd/application-sets/workloads.yaml
|
||||
)
|
||||
for applicationset in "${git_applicationsets[@]}"; do
|
||||
if ! rg -q '^[[:space:]]+targetRevision:[[:space:]]+main$' "$applicationset" ||
|
||||
rg -q '^[[:space:]]+(repoURL|targetRevision):[[:space:]]+"?\{\{' "$applicationset"; then
|
||||
echo "${applicationset} must pin the canonical repository main branch in its template." >&2
|
||||
exit 1
|
||||
fi
|
||||
done
|
||||
|
||||
enabled_gates="$(
|
||||
rg -o 'autoSync: "true"' platform/control-plane/argocd/application-sets |
|
||||
wc -l |
|
||||
tr -d ' '
|
||||
)"
|
||||
disabled_gates="$(
|
||||
rg -o 'autoSync: "false"' platform/control-plane/argocd/application-sets |
|
||||
wc -l |
|
||||
tr -d ' '
|
||||
)"
|
||||
if [[ "$enabled_gates" -ne 2 || "$disabled_gates" -ne 4 ]]; then
|
||||
echo "Initial sync gates must enable only Sealed Secrets and Vault." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
sealed_secrets_render="$(
|
||||
helm template sealed-secrets sealed-secrets \
|
||||
--repo https://bitnami.github.io/sealed-secrets \
|
||||
@@ -85,8 +172,8 @@ fi
|
||||
|
||||
rendered_images="$(
|
||||
{
|
||||
kubectl kustomize clusters/dev-k3s/manifests/auth-system
|
||||
kubectl kustomize clusters/dev-k3s/manifests/vault
|
||||
kubectl kustomize clusters/dev-k3s/overlays/systems/auth-system
|
||||
kubectl kustomize clusters/dev-k3s/overlays/platform/vault
|
||||
} | rg '^[[:space:]]+image: (hashicorp/vault|postgres|quay\\.io/keycloak)'
|
||||
)"
|
||||
if printf '%s\n' "$rendered_images" | rg -v '@sha256:[a-f0-9]{64}$'; then
|
||||
@@ -94,9 +181,26 @@ if printf '%s\n' "$rendered_images" | rg -v '@sha256:[a-f0-9]{64}$'; then
|
||||
exit 1
|
||||
fi
|
||||
|
||||
workload_kustomizations=(
|
||||
clusters/dev-k3s/overlays/workloads/auth-server/kustomization.yaml
|
||||
clusters/dev-k3s/overlays/workloads/api-server/kustomization.yaml
|
||||
)
|
||||
for workload_kustomization in "${workload_kustomizations[@]}"; do
|
||||
if rg -q '^[[:space:]]+digest:[[:space:]]+sha256:[a-f0-9]{64}$' \
|
||||
"$workload_kustomization"; then
|
||||
continue
|
||||
fi
|
||||
if ! rg -q '^[[:space:]]+newTag:[[:space:]]+[a-f0-9]{7,40}$' \
|
||||
"$workload_kustomization"; then
|
||||
echo "${workload_kustomization} must use a verified digest or a temporary commit-shaped tag." >&2
|
||||
exit 1
|
||||
fi
|
||||
done
|
||||
|
||||
terraform fmt -check -recursive iac/terraform
|
||||
terraform_roots=(
|
||||
iac/terraform/live/dev-k3s/vault-core
|
||||
iac/terraform/live/dev-k3s/vault-foundation
|
||||
iac/terraform/live/dev-k3s/vault-workloads
|
||||
iac/terraform/live/dev-k3s/vault-database
|
||||
)
|
||||
for root in "${terraform_roots[@]}"; do
|
||||
@@ -110,12 +214,47 @@ for root in "${terraform_roots[@]}"; do
|
||||
done
|
||||
|
||||
if rg -n \
|
||||
'github\\.com/DongHyeonka/Project-Auth-GitOps|bitnami-labs\\.github\\.io/sealed-secrets|/home/donghyeon/dev/Project-Auth-GitOps|terraform/vault(-transit)?/(dev|reconcile)' \
|
||||
'github\\.com/DongHyeonka/Project-Auth-GitOps|bitnami-labs\\.github\\.io/sealed-secrets|/home/donghyeon/dev/Project-Auth-GitOps|terraform/vault(-transit)?/(dev|reconcile)|clusters/dev-k3s/manifests|platform/auth-system|platform/security/vault|platform-config|postgres\\.platform\\.svc|keycloak(-public)?\\.platform\\.svc|kv/data/dev/platform' \
|
||||
--glob '!docs/archive/**' \
|
||||
--glob '!docs/runbooks/terraform-state-migration.md' \
|
||||
--glob '!policies/legacy/**' \
|
||||
--glob '!hack/validate.sh' \
|
||||
.; then
|
||||
echo "Current files contain a legacy repository URL, Helm URL, absolute path, or Terraform root." >&2
|
||||
echo "Current files contain a legacy URL, path, namespace, or Vault secret path." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if rg -n '^path[[:space:]]+"[^"]*[+*]' policies/vault/dev-k3s/workloads; then
|
||||
echo "Workload Vault policies must use exact paths; wildcard paths require a security review." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
automation_policies=(
|
||||
policies/vault/dev-k3s/platform/vault-workloads-automation-dev.hcl
|
||||
policies/vault/dev-k3s/platform/vault-database-automation-dev.hcl
|
||||
)
|
||||
for policy in "${automation_policies[@]}"; do
|
||||
for self_path in \
|
||||
'sys/capabilities-self' \
|
||||
'auth/token/lookup-self' \
|
||||
'auth/token/revoke-self'; do
|
||||
if ! rg -q "^path \"${self_path}\"" "$policy"; then
|
||||
echo "${policy} is missing required no-default-policy self service path: ${self_path}" >&2
|
||||
exit 1
|
||||
fi
|
||||
done
|
||||
done
|
||||
|
||||
if rg -n \
|
||||
'^path[[:space:]]+"[^"]*[+*]|capabilities[[:space:]]*=.*"(sudo|list)"|^path[[:space:]]+"auth/token/(create|roles)' \
|
||||
policies/vault/dev-k3s/platform; then
|
||||
echo "Delegated automation policies must not use wildcards, sudo/list, or token issuance paths." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if rg -n '^path[[:space:]]+"(sys/|auth/|database/config|database/roles)' \
|
||||
policies/vault/dev-k3s/workloads; then
|
||||
echo "Runtime workload policies may not configure Vault control-plane objects." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
@@ -124,7 +263,7 @@ if rg -n 'uses:[[:space:]]+[^#[:space:]]+@v[0-9]' .gitea/workflows; then
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if rg -n 'ApplyOutOfSyncOnly=true' bootstrap clusters; then
|
||||
if rg -n 'ApplyOutOfSyncOnly=true' bootstrap clusters platform/control-plane; then
|
||||
echo "ApplyOutOfSyncOnly is incompatible with hook-based migrations and must not be enabled." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
+44
-1
@@ -7,7 +7,7 @@ REPO_ROOT="$(cd "${SCRIPT_DIR}/.." && pwd)"
|
||||
VAULT_ADDR="${VAULT_ADDR:-http://127.0.0.1:8200}"
|
||||
VAULT_INIT_OUTPUT="${VAULT_INIT_OUTPUT:-${REPO_ROOT}/.local/vault/dev-k3s-init.json}"
|
||||
|
||||
for cmd in jq vault; do
|
||||
for cmd in jq rg vault; do
|
||||
if ! command -v "$cmd" >/dev/null 2>&1; then
|
||||
echo "$cmd is required" >&2
|
||||
exit 1
|
||||
@@ -78,13 +78,56 @@ init() {
|
||||
}
|
||||
|
||||
revoke_root() {
|
||||
local database_lookup=""
|
||||
local database_token="${VAULT_DATABASE_TOKEN:-}"
|
||||
local root_token=""
|
||||
local temporary=""
|
||||
local workloads_lookup=""
|
||||
local workloads_token="${VAULT_WORKLOADS_TOKEN:-}"
|
||||
|
||||
if [[ ! -f "$VAULT_INIT_OUTPUT" ]]; then
|
||||
echo "Init material is unavailable: ${VAULT_INIT_OUTPUT}" >&2
|
||||
exit 1
|
||||
fi
|
||||
if [[ -z "$workloads_token" || -z "$database_token" ]]; then
|
||||
echo "Refusing root revocation: VAULT_WORKLOADS_TOKEN and VAULT_DATABASE_TOKEN are required." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if ! VAULT_TOKEN="$workloads_token" \
|
||||
vault token capabilities sys/policies/acl/auth-server-dev |
|
||||
rg -q '(^|,|[[:space:]])update($|,|[[:space:]])'; then
|
||||
echo "Refusing root revocation: the workloads replacement token failed its capability check." >&2
|
||||
exit 1
|
||||
fi
|
||||
if ! VAULT_TOKEN="$database_token" \
|
||||
vault token capabilities database/config/auth-system-postgres-dev |
|
||||
rg -q '(^|,|[[:space:]])update($|,|[[:space:]])'; then
|
||||
echo "Refusing root revocation: the database replacement token failed its capability check." >&2
|
||||
exit 1
|
||||
fi
|
||||
if ! workloads_lookup="$(
|
||||
VAULT_TOKEN="$workloads_token" vault token lookup -format=json
|
||||
)"; then
|
||||
echo "Refusing root revocation: the workloads replacement token cannot look itself up." >&2
|
||||
exit 1
|
||||
fi
|
||||
if ! database_lookup="$(
|
||||
VAULT_TOKEN="$database_token" vault token lookup -format=json
|
||||
)"; then
|
||||
echo "Refusing root revocation: the database replacement token cannot look itself up." >&2
|
||||
exit 1
|
||||
fi
|
||||
if ! jq -e '.data.policies | type == "array"' <<<"$workloads_lookup" >/dev/null ||
|
||||
! jq -e '.data.policies | type == "array"' <<<"$database_lookup" >/dev/null; then
|
||||
echo "Refusing root revocation: a replacement token returned an invalid lookup response." >&2
|
||||
exit 1
|
||||
fi
|
||||
if jq -e '.data.policies | index("root") != null' <<<"$workloads_lookup" >/dev/null ||
|
||||
jq -e '.data.policies | index("root") != null' <<<"$database_lookup" >/dev/null; then
|
||||
echo "Refusing root revocation: a replacement token unexpectedly carries the root policy." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
root_token="$(jq -er '.root_token' "$VAULT_INIT_OUTPUT")"
|
||||
VAULT_TOKEN="$root_token" vault token revoke -self
|
||||
|
||||
Reference in New Issue
Block a user