From 523e18c127e0fa6480ecf0752091c78ee1500b48 Mon Sep 17 00:00:00 2001 From: Viktor Barzin Date: Fri, 12 Jun 2026 20:28:11 +0000 Subject: [PATCH] kyverno: sync-ghcr-credentials to private-ghcr namespaces; tripit consumes the clone MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Viktor asked to unblock the ADR-0002 ghcr pull-secret work (infra#12) without waiting on a UI-minted token: GitHub has no token-mint API, so the admin PAT (aliased in Vault as secret/viktor/ghcr_pull_token — swap the alias value when a scoped token is ever minted) becomes the platform credential. Because the PAT is broad, the new ClusterPolicy clones ghcr-credentials ONLY to an explicit allowlist of namespaces running private ghcr images (tripit, f1-stream, job-hunter, instagram-poster, payslip-ingest, wealthfolio, fire-planner, recruiter-responder) — NOT cluster-wide like registry-credentials. generateExisting+synchronize so existing namespaces get the clone. tripit's hand-declared ns-scoped secret is removed in favour of the clone (imagePullSecrets now reference the name literally). Co-Authored-By: Claude Fable 5 --- .../modules/kyverno/ghcr-credentials.tf | 89 +++++++++++++++++++ stacks/tripit/main.tf | 36 ++------ 2 files changed, 95 insertions(+), 30 deletions(-) create mode 100644 stacks/kyverno/modules/kyverno/ghcr-credentials.tf diff --git a/stacks/kyverno/modules/kyverno/ghcr-credentials.tf b/stacks/kyverno/modules/kyverno/ghcr-credentials.tf new file mode 100644 index 00000000..9586163c --- /dev/null +++ b/stacks/kyverno/modules/kyverno/ghcr-credentials.tf @@ -0,0 +1,89 @@ +# ============================================================================= +# ghcr.io pull credentials — synced ONLY to namespaces running PRIVATE ghcr +# images (ADR-0002 off-infra builds) +# ============================================================================= +# The credential is Viktor's admin PAT (Vault secret/viktor/ghcr_pull_token — +# an alias of github_pat: GitHub has no API to mint tokens, so a UI-minted +# read:packages token can replace the alias value later with no TF change). +# Because the PAT is broad, this is a positive allowlist, NOT cluster-wide +# like registry-credentials: any workload in a listed namespace can read the +# secret, so every entry widens the blast radius. Public-image namespaces +# need no credentials — keep this list to private-image consumers only. + +locals { + ghcr_private_namespaces = [ + "tripit", + "f1-stream", + "job-hunter", + "instagram-poster", + "payslip-ingest", + "wealthfolio", + "fire-planner", + "recruiter-responder", + ] +} + +resource "kubernetes_secret" "ghcr_credentials" { + metadata { + name = "ghcr-credentials" + namespace = kubernetes_namespace.kyverno.metadata[0].name + } + type = "kubernetes.io/dockerconfigjson" + data = { + ".dockerconfigjson" = jsonencode({ + auths = { + "ghcr.io" = { + username = "ViktorBarzin" + password = try(data.vault_kv_secret_v2.viktor.data["ghcr_pull_token"], "") + auth = base64encode("ViktorBarzin:${try(data.vault_kv_secret_v2.viktor.data["ghcr_pull_token"], "")}") + } + } + }) + } +} + +resource "kubectl_manifest" "sync_ghcr_credentials" { + yaml_body = yamlencode({ + apiVersion = "kyverno.io/v1" + kind = "ClusterPolicy" + metadata = { + name = "sync-ghcr-credentials" + } + spec = { + rules = [ + { + name = "sync-ghcr-secret" + match = { + any = [ + { + resources = { + kinds = ["Namespace"] + names = local.ghcr_private_namespaces + } + } + ] + } + generate = { + generateExisting = true + apiVersion = "v1" + kind = "Secret" + name = "ghcr-credentials" + namespace = "{{request.object.metadata.name}}" + synchronize = true + clone = { + namespace = "kyverno" + name = "ghcr-credentials" + } + } + } + ] + } + }) + + depends_on = [ + helm_release.kyverno, + kubernetes_secret.ghcr_credentials, + kubernetes_cluster_role_binding.kyverno_admission_secret_manager, + kubernetes_cluster_role_binding.kyverno_background_secret_manager, + ] +} diff --git a/stacks/tripit/main.tf b/stacks/tripit/main.tf index b91f3940..f1748bb6 100644 --- a/stacks/tripit/main.tf +++ b/stacks/tripit/main.tf @@ -142,34 +142,10 @@ resource "kubernetes_namespace" "tripit" { } } -# GHCR pull secret (tripit ns only) for the private ghcr.io/viktorbarzin/tripit image -# now built off-infra by GitHub Actions. Uses viktor's github_pat as the pull -# credential — admin-scoped, accepted as an interim (rotate to a fine-grained -# read:packages token later). Scoped to this namespace to limit the broad token's -# blast radius (deliberately NOT folded into the cluster-wide registry-credentials). -data "vault_kv_secret_v2" "viktor" { - mount = "secret" - name = "viktor" -} - -resource "kubernetes_secret" "ghcr_credentials" { - metadata { - name = "ghcr-credentials" - namespace = kubernetes_namespace.tripit.metadata[0].name - } - type = "kubernetes.io/dockerconfigjson" - data = { - ".dockerconfigjson" = jsonencode({ - auths = { - "ghcr.io" = { - username = "ViktorBarzin" - password = data.vault_kv_secret_v2.viktor.data["github_pat"] - auth = base64encode("ViktorBarzin:${data.vault_kv_secret_v2.viktor.data["github_pat"]}") - } - } - }) - } -} +# GHCR pull secret: the ghcr-credentials Secret in this namespace is cloned in +# by the kyverno stack's sync-ghcr-credentials ClusterPolicy (allowlisted +# private-ghcr namespaces only — ADR-0002). Source of truth: +# stacks/kyverno/modules/kyverno/ghcr-credentials.tf. # App secrets — seed these in Vault before applying: # secret/tripit @@ -370,7 +346,7 @@ resource "kubernetes_deployment" "tripit" { name = "registry-credentials" } image_pull_secrets { - name = kubernetes_secret.ghcr_credentials.metadata[0].name + name = "ghcr-credentials" } init_container { @@ -650,7 +626,7 @@ resource "kubernetes_cron_job_v1" "tripit_worker" { name = "registry-credentials" } image_pull_secrets { - name = kubernetes_secret.ghcr_credentials.metadata[0].name + name = "ghcr-credentials" } container { name = "worker"