282 lines
10 KiB
Bash
Executable File
282 lines
10 KiB
Bash
Executable File
#!/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 scripts -g '*.sh')
|
|
|
|
jq empty renovate.json
|
|
jq empty gitops/apps/systems/auth-system/base/files/keycloak/project-auth-realm.json
|
|
|
|
overlays=(
|
|
bootstrap/gitops/argocd
|
|
gitops/clusters/dev-k3s
|
|
gitops/platform/control-plane/argocd
|
|
gitops/clusters/dev-k3s/overlays/platform/vault
|
|
gitops/clusters/dev-k3s/overlays/systems/auth-system
|
|
gitops/clusters/dev-k3s/overlays/workloads/auth-server
|
|
gitops/clusters/dev-k3s/overlays/workloads/api-server
|
|
)
|
|
for overlay in "${overlays[@]}"; do
|
|
kubectl kustomize "$overlay" >/dev/null
|
|
done
|
|
|
|
control_plane_render="$(kubectl kustomize gitops/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/gitops/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/gitops/argocd/root-application.yaml ||
|
|
! rg -q '^[[:space:]]+path:[[:space:]]+gitops/clusters/dev-k3s$' \
|
|
bootstrap/gitops/argocd/root-application.yaml ||
|
|
rg -n '^[[:space:]]+project:[[:space:]]+default$' \
|
|
bootstrap gitops/platform/control-plane; then
|
|
echo "The root and generated Applications must use explicit least-privilege AppProjects." >&2
|
|
exit 1
|
|
fi
|
|
|
|
applicationsets=(
|
|
gitops/platform/control-plane/argocd/application-sets/platform-addons.yaml
|
|
gitops/platform/control-plane/argocd/application-sets/platform-services.yaml
|
|
gitops/platform/control-plane/argocd/application-sets/systems.yaml
|
|
gitops/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$' \
|
|
gitops/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:]]+"?\{\{' gitops/platform/control-plane/argocd/application-sets; then
|
|
echo "ApplicationSet projects are privilege boundaries and must never be templated." >&2
|
|
exit 1
|
|
fi
|
|
|
|
git_applicationsets=(
|
|
gitops/platform/control-plane/argocd/application-sets/platform-services.yaml
|
|
gitops/platform/control-plane/argocd/application-sets/systems.yaml
|
|
gitops/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"' gitops/platform/control-plane/argocd/application-sets |
|
|
wc -l |
|
|
tr -d ' '
|
|
)"
|
|
disabled_gates="$(
|
|
rg -o 'autoSync: "false"' gitops/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 gitops/clusters/dev-k3s/overlays/systems/auth-system
|
|
kubectl kustomize gitops/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=(
|
|
gitops/clusters/dev-k3s/overlays/workloads/auth-server/kustomization.yaml
|
|
gitops/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 infrastructure
|
|
terraform_roots=(
|
|
infrastructure/live/dev-k3s/vault-foundation
|
|
infrastructure/live/dev-k3s/vault-workloads
|
|
infrastructure/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)|gitops/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 '!scripts/project-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:]]+"[^"]*[+*]' infrastructure/live/dev-k3s/vault-workloads/policies; then
|
|
echo "Workload Vault policies must use exact paths; wildcard paths require a security review." >&2
|
|
exit 1
|
|
fi
|
|
|
|
automation_policies=(
|
|
infrastructure/live/dev-k3s/vault-foundation/policies/vault-workloads-automation-dev.hcl
|
|
infrastructure/live/dev-k3s/vault-foundation/policies/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)' \
|
|
infrastructure/live/dev-k3s/vault-foundation/policies; 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)' \
|
|
infrastructure/live/dev-k3s/vault-workloads/policies; 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 gitops/clusters gitops/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 scripts; 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."
|