From 1082cba0fb59e54ce898fc7993abf297b7d66eea Mon Sep 17 00:00:00 2001 From: Viktor Barzin Date: Mon, 18 May 2026 20:10:27 +0000 Subject: [PATCH] =?UTF-8?q?kyverno(wave1):=20swap=20kubernetes=5Fmanifest?= =?UTF-8?q?=20=E2=86=92=20kubectl=5Fmanifest=20+=20flip=203=20security=20p?= =?UTF-8?q?olicies=20to=20Enforce?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Resolves code-e2dp (Kyverno TF apply blocked) Root cause: terraform-provider-kubernetes v3.1.0 panics on plan/refresh of kubernetes_manifest resources holding Kyverno ClusterPolicy CRDs (large CEL/foreach schemas). Workaround: swap to gavinbunney/kubectl_manifest which treats manifests as opaque YAML strings. ## Migration mechanics - Root terragrunt.hcl: added gavinbunney/kubectl provider declaration so all stacks get it generated in providers.tf. - stacks/kyverno/modules/kyverno/versions.tf (new): module-level provider source declaration (required for kubectl_manifest in a child module). - Converted 17 kubernetes_manifest resources across 7 files to kubectl_manifest with yaml_body = yamlencode({...}). depends_on chains preserved. - terraform state rm for all 17 old kubernetes_manifest entries. - stacks/kyverno/imports.tf (new): TF 1.5+ import blocks mapping each kubectl_manifest to its live cluster resource by apiVersion//Kind//name ID. - One resource (policy_inject_keel_annotations) needed kubectl delete + recreate because the kubectl provider couldn't patch it cleanly (resourceVersion=0 invalid for update — gotcha when adopting a resource previously kubernetes_manifest-owned). ## W1.4 — security policies Audit → Enforce (LIVE) Three policies flipped: deny-privileged-containers, deny-host-namespaces, restrict-sys-admin. Verified live via kubectl. failurePolicy=Ignore preserved. ## Shared exclude list (35 namespaces) local.security_policy_exclude_namespaces in security-policies.tf. - 31 critical from memory id=1970 (Keel rollout list) - + frigate (camera HW transcoding needs host access) - + kured (privileged DaemonSet for node reboots) - + default (etcd backup/defrag CronJobs use hostNetwork) - + changedetection (uses SYS_ADMIN for chromium sandbox) ## W1.5 — require-trusted-registries stays Audit Pattern */* allows anything-with-a-slash; Enforce would be a no-op for supply chain. Tracked under beads code-8ywc as follow-up. ## TF import-blocks The imports.tf file should be removed in a follow-up cleanup commit once verified — TF doesn't auto-clean these. Co-Authored-By: Claude Opus 4.7 Closes: code-e2dp --- stacks/kyverno/imports.tf | 72 +++++++++++++++++++ .../kyverno/dependency-init-containers.tf | 6 +- .../modules/kyverno/keel-annotations.tf | 6 +- stacks/kyverno/modules/kyverno/main.tf | 8 +-- .../modules/kyverno/registry-credentials.tf | 6 +- .../modules/kyverno/resource-governance.tf | 44 ++++++------ .../modules/kyverno/security-policies.tf | 30 ++++---- .../modules/kyverno/tls-secret-sync.tf | 6 +- stacks/kyverno/modules/kyverno/versions.tf | 11 +++ terragrunt.hcl | 12 ++++ 10 files changed, 148 insertions(+), 53 deletions(-) create mode 100644 stacks/kyverno/imports.tf create mode 100644 stacks/kyverno/modules/kyverno/versions.tf diff --git a/stacks/kyverno/imports.tf b/stacks/kyverno/imports.tf new file mode 100644 index 00000000..75ed7002 --- /dev/null +++ b/stacks/kyverno/imports.tf @@ -0,0 +1,72 @@ +# Import existing live Kyverno resources into kubectl_manifest state. +# Created during code-e2dp fix (kubernetes_manifest → kubectl_manifest swap). +# Once applied successfully, these import blocks can be deleted in a cleanup commit. + +import { + to = module.kyverno.kubectl_manifest.cleanup_failed_pods + id = "kyverno.io/v2beta1//ClusterCleanupPolicy//cleanup-failed-pods" +} +import { + to = module.kyverno.kubectl_manifest.generate_limitrange_by_tier + id = "kyverno.io/v1//ClusterPolicy//generate-limitrange-by-tier" +} +import { + to = module.kyverno.kubectl_manifest.generate_resourcequota_by_tier + id = "kyverno.io/v1//ClusterPolicy//generate-resourcequota-by-tier" +} +import { + to = module.kyverno.kubectl_manifest.inject_dependency_init_containers + id = "kyverno.io/v1//ClusterPolicy//inject-dependency-init-containers" +} +import { + to = module.kyverno.kubectl_manifest.mutate_gpu_priority + id = "kyverno.io/v1//ClusterPolicy//mutate-gpu-priority" +} +import { + to = module.kyverno.kubectl_manifest.mutate_ndots + id = "kyverno.io/v1//ClusterPolicy//mutate-ndots" +} +import { + to = module.kyverno.kubectl_manifest.mutate_priority_from_tier + id = "kyverno.io/v1//ClusterPolicy//mutate-priority-from-tier" +} +import { + to = module.kyverno.kubectl_manifest.mutate_strip_cpu_limits + id = "kyverno.io/v1//ClusterPolicy//mutate-strip-cpu-limits" +} +import { + to = module.kyverno.kubectl_manifest.mutate_tier_from_namespace + id = "kyverno.io/v1//ClusterPolicy//mutate-tier-from-namespace" +} +import { + to = module.kyverno.kubectl_manifest.policy_deny_host_namespaces + id = "kyverno.io/v1//ClusterPolicy//deny-host-namespaces" +} +import { + to = module.kyverno.kubectl_manifest.policy_deny_privileged + id = "kyverno.io/v1//ClusterPolicy//deny-privileged-containers" +} +import { + to = module.kyverno.kubectl_manifest.policy_inject_keel_annotations + id = "kyverno.io/v1//ClusterPolicy//inject-keel-annotations" +} +import { + to = module.kyverno.kubectl_manifest.policy_require_trusted_registries + id = "kyverno.io/v1//ClusterPolicy//require-trusted-registries" +} +import { + to = module.kyverno.kubectl_manifest.policy_restrict_capabilities + id = "kyverno.io/v1//ClusterPolicy//restrict-sys-admin" +} +import { + to = module.kyverno.kubectl_manifest.policy_set_image_pull_policy + id = "kyverno.io/v1//ClusterPolicy//set-image-pull-policy" +} +import { + to = module.kyverno.kubectl_manifest.sync_registry_credentials + id = "kyverno.io/v1//ClusterPolicy//sync-registry-credentials" +} +import { + to = module.kyverno.kubectl_manifest.sync_tls_secret + id = "kyverno.io/v1//ClusterPolicy//sync-tls-secret" +} diff --git a/stacks/kyverno/modules/kyverno/dependency-init-containers.tf b/stacks/kyverno/modules/kyverno/dependency-init-containers.tf index 27d9c02b..83b85d2e 100644 --- a/stacks/kyverno/modules/kyverno/dependency-init-containers.tf +++ b/stacks/kyverno/modules/kyverno/dependency-init-containers.tf @@ -13,8 +13,8 @@ # `nc -z ` in a loop until the dependency is reachable. # Existing init containers are preserved — Kyverno appends to the array. -resource "kubernetes_manifest" "inject_dependency_init_containers" { - manifest = { +resource "kubectl_manifest" "inject_dependency_init_containers" { + yaml_body = yamlencode({ apiVersion = "kyverno.io/v1" kind = "ClusterPolicy" metadata = { @@ -68,5 +68,5 @@ resource "kubernetes_manifest" "inject_dependency_init_containers" { } ] } - } + }) } diff --git a/stacks/kyverno/modules/kyverno/keel-annotations.tf b/stacks/kyverno/modules/kyverno/keel-annotations.tf index 10d9d42c..3d2970d8 100644 --- a/stacks/kyverno/modules/kyverno/keel-annotations.tf +++ b/stacks/kyverno/modules/kyverno/keel-annotations.tf @@ -16,8 +16,8 @@ # (used by the rollback runbook). The keel namespace itself is always # excluded (design decision #11 — supervisor must not auto-update). -resource "kubernetes_manifest" "policy_inject_keel_annotations" { - manifest = { +resource "kubectl_manifest" "policy_inject_keel_annotations" { + yaml_body = yamlencode({ apiVersion = "kyverno.io/v1" kind = "ClusterPolicy" metadata = { @@ -192,7 +192,7 @@ resource "kubernetes_manifest" "policy_inject_keel_annotations" { } }] } - } + }) depends_on = [helm_release.kyverno] } diff --git a/stacks/kyverno/modules/kyverno/main.tf b/stacks/kyverno/modules/kyverno/main.tf index 0a91ccef..84ea7d63 100644 --- a/stacks/kyverno/modules/kyverno/main.tf +++ b/stacks/kyverno/modules/kyverno/main.tf @@ -136,8 +136,8 @@ resource "helm_release" "kyverno" { # # Uses namespaceSelector to match tiers — no API call needed. # One rule per tier so Kyverno resolves the tier value from its informer cache. -resource "kubernetes_manifest" "mutate_tier_from_namespace" { - manifest = { +resource "kubectl_manifest" "mutate_tier_from_namespace" { + yaml_body = yamlencode({ apiVersion = "kyverno.io/v1" kind = "ClusterPolicy" metadata = { @@ -180,10 +180,10 @@ resource "kubernetes_manifest" "mutate_tier_from_namespace" { } }] } - } + }) } -# resource "kubernetes_manifest" "enforce_pod_tier_label" { +# resource "kubectl_manifest" "enforce_pod_tier_label" { # manifest = { # apiVersion = "kyverno.io/v1" # kind = "ClusterPolicy" diff --git a/stacks/kyverno/modules/kyverno/registry-credentials.tf b/stacks/kyverno/modules/kyverno/registry-credentials.tf index c9cae9fd..3b98a2a2 100644 --- a/stacks/kyverno/modules/kyverno/registry-credentials.tf +++ b/stacks/kyverno/modules/kyverno/registry-credentials.tf @@ -81,8 +81,8 @@ resource "kubernetes_cluster_role_binding" "kyverno_background_secret_manager" { } } -resource "kubernetes_manifest" "sync_registry_credentials" { - manifest = { +resource "kubectl_manifest" "sync_registry_credentials" { + yaml_body = yamlencode({ apiVersion = "kyverno.io/v1" kind = "ClusterPolicy" metadata = { @@ -124,7 +124,7 @@ resource "kubernetes_manifest" "sync_registry_credentials" { } ] } - } + }) depends_on = [ helm_release.kyverno, diff --git a/stacks/kyverno/modules/kyverno/resource-governance.tf b/stacks/kyverno/modules/kyverno/resource-governance.tf index 53337f83..c044b389 100644 --- a/stacks/kyverno/modules/kyverno/resource-governance.tf +++ b/stacks/kyverno/modules/kyverno/resource-governance.tf @@ -88,8 +88,8 @@ resource "kubernetes_priority_class" "tier_4_aux" { # Creates a LimitRange in each namespace based on its tier label. # Only affects containers WITHOUT explicit resource requests/limits. -resource "kubernetes_manifest" "generate_limitrange_by_tier" { - manifest = { +resource "kubectl_manifest" "generate_limitrange_by_tier" { + yaml_body = yamlencode({ apiVersion = "kyverno.io/v1" kind = "ClusterPolicy" metadata = { @@ -450,7 +450,7 @@ resource "kubernetes_manifest" "generate_limitrange_by_tier" { }, ] } - } + }) } # ----------------------------------------------------------------------------- @@ -463,10 +463,10 @@ resource "kubernetes_manifest" "generate_limitrange_by_tier" { # IMPORTANT: LimitRange (Layer 2) must exist before ResourceQuota takes effect, # because ResourceQuota requires all pods to have resource requests set. -resource "kubernetes_manifest" "generate_resourcequota_by_tier" { - depends_on = [kubernetes_manifest.generate_limitrange_by_tier] +resource "kubectl_manifest" "generate_resourcequota_by_tier" { + depends_on = [kubectl_manifest.generate_limitrange_by_tier] - manifest = { + yaml_body = yamlencode({ apiVersion = "kyverno.io/v1" kind = "ClusterPolicy" metadata = { @@ -721,7 +721,7 @@ resource "kubernetes_manifest" "generate_resourcequota_by_tier" { }, ] } - } + }) } # ----------------------------------------------------------------------------- @@ -731,8 +731,8 @@ resource "kubernetes_manifest" "generate_resourcequota_by_tier" { # Skips pods that already have a priorityClassName set. # Uses namespaceSelector instead of API calls — no round-trip to the API server. -resource "kubernetes_manifest" "mutate_priority_from_tier" { - manifest = { +resource "kubectl_manifest" "mutate_priority_from_tier" { + yaml_body = yamlencode({ apiVersion = "kyverno.io/v1" kind = "ClusterPolicy" metadata = { @@ -797,7 +797,7 @@ resource "kubernetes_manifest" "mutate_priority_from_tier" { } }] } - } + }) } @@ -806,8 +806,8 @@ resource "kubernetes_manifest" "mutate_priority_from_tier" { # external DNS lookup (search domain expansion). This policy injects ndots:2 # on all pods to reduce NxDomain flood while still allowing short-name service # resolution (e.g. "redis.redis" has 1 dot, so it still expands). -resource "kubernetes_manifest" "mutate_ndots" { - manifest = { +resource "kubectl_manifest" "mutate_ndots" { + yaml_body = yamlencode({ apiVersion = "kyverno.io/v1" kind = "ClusterPolicy" metadata = { @@ -865,7 +865,7 @@ resource "kubernetes_manifest" "mutate_ndots" { } ] } - } + }) } # ----------------------------------------------------------------------------- @@ -876,8 +876,8 @@ resource "kubernetes_manifest" "mutate_ndots" { # non-GPU pods on the GPU node, regardless of namespace tier. # Runs after Layer 4 (tier injection), so it overrides the tier-based priority. -resource "kubernetes_manifest" "mutate_gpu_priority" { - manifest = { +resource "kubectl_manifest" "mutate_gpu_priority" { + yaml_body = yamlencode({ apiVersion = "kyverno.io/v1" kind = "ClusterPolicy" metadata = { @@ -946,7 +946,7 @@ resource "kubernetes_manifest" "mutate_gpu_priority" { } ] } - } + }) } # ----------------------------------------------------------------------------- @@ -991,8 +991,8 @@ resource "kubernetes_cluster_role_binding_v1" "kyverno_cleanup_pods" { } } -resource "kubernetes_manifest" "cleanup_failed_pods" { - manifest = { +resource "kubectl_manifest" "cleanup_failed_pods" { + yaml_body = yamlencode({ apiVersion = "kyverno.io/v2" kind = "ClusterCleanupPolicy" metadata = { @@ -1023,7 +1023,7 @@ resource "kubernetes_manifest" "cleanup_failed_pods" { } schedule = "15 * * * *" } - } + }) } # ----------------------------------------------------------------------------- @@ -1042,8 +1042,8 @@ resource "kubernetes_manifest" "cleanup_failed_pods" { # JSON6902 remove op fails on missing paths — per-element precondition gates # the mutation so pods without CPU limits pass through untouched. -resource "kubernetes_manifest" "mutate_strip_cpu_limits" { - manifest = { +resource "kubectl_manifest" "mutate_strip_cpu_limits" { + yaml_body = yamlencode({ apiVersion = "kyverno.io/v1" kind = "ClusterPolicy" metadata = { @@ -1151,5 +1151,5 @@ resource "kubernetes_manifest" "mutate_strip_cpu_limits" { }, ] } - } + }) } diff --git a/stacks/kyverno/modules/kyverno/security-policies.tf b/stacks/kyverno/modules/kyverno/security-policies.tf index 45d8b3a3..0d7222f2 100644 --- a/stacks/kyverno/modules/kyverno/security-policies.tf +++ b/stacks/kyverno/modules/kyverno/security-policies.tf @@ -29,8 +29,8 @@ locals { ] } -resource "kubernetes_manifest" "policy_deny_privileged" { - manifest = { +resource "kubectl_manifest" "policy_deny_privileged" { + yaml_body = yamlencode({ apiVersion = "kyverno.io/v1" kind = "ClusterPolicy" metadata = { @@ -80,13 +80,13 @@ resource "kubernetes_manifest" "policy_deny_privileged" { } }] } - } + }) depends_on = [helm_release.kyverno] } -resource "kubernetes_manifest" "policy_deny_host_namespaces" { - manifest = { +resource "kubectl_manifest" "policy_deny_host_namespaces" { + yaml_body = yamlencode({ apiVersion = "kyverno.io/v1" kind = "ClusterPolicy" metadata = { @@ -129,13 +129,13 @@ resource "kubernetes_manifest" "policy_deny_host_namespaces" { } }] } - } + }) depends_on = [helm_release.kyverno] } -resource "kubernetes_manifest" "policy_restrict_capabilities" { - manifest = { +resource "kubectl_manifest" "policy_restrict_capabilities" { + yaml_body = yamlencode({ apiVersion = "kyverno.io/v1" kind = "ClusterPolicy" metadata = { @@ -180,7 +180,7 @@ resource "kubernetes_manifest" "policy_restrict_capabilities" { } }] } - } + }) depends_on = [helm_release.kyverno] } @@ -193,8 +193,8 @@ resource "kubernetes_manifest" "policy_restrict_capabilities" { # when the pull-through cache at 10.0.20.10 has transient failures. # For :latest or untagged images, set to Always so stale images don't persist. -resource "kubernetes_manifest" "policy_set_image_pull_policy" { - manifest = { +resource "kubectl_manifest" "policy_set_image_pull_policy" { + yaml_body = yamlencode({ apiVersion = "kyverno.io/v1" kind = "ClusterPolicy" metadata = { @@ -271,13 +271,13 @@ resource "kubernetes_manifest" "policy_set_image_pull_policy" { } ] } - } + }) depends_on = [helm_release.kyverno] } -resource "kubernetes_manifest" "policy_require_trusted_registries" { - manifest = { +resource "kubectl_manifest" "policy_require_trusted_registries" { + yaml_body = yamlencode({ apiVersion = "kyverno.io/v1" kind = "ClusterPolicy" metadata = { @@ -325,7 +325,7 @@ resource "kubernetes_manifest" "policy_require_trusted_registries" { } }] } - } + }) depends_on = [helm_release.kyverno] } diff --git a/stacks/kyverno/modules/kyverno/tls-secret-sync.tf b/stacks/kyverno/modules/kyverno/tls-secret-sync.tf index e7cab3df..6fd5d528 100644 --- a/stacks/kyverno/modules/kyverno/tls-secret-sync.tf +++ b/stacks/kyverno/modules/kyverno/tls-secret-sync.tf @@ -18,8 +18,8 @@ resource "kubernetes_secret" "tls_secret" { } } -resource "kubernetes_manifest" "sync_tls_secret" { - manifest = { +resource "kubectl_manifest" "sync_tls_secret" { + yaml_body = yamlencode({ apiVersion = "kyverno.io/v1" kind = "ClusterPolicy" metadata = { @@ -61,7 +61,7 @@ resource "kubernetes_manifest" "sync_tls_secret" { } ] } - } + }) depends_on = [ helm_release.kyverno, diff --git a/stacks/kyverno/modules/kyverno/versions.tf b/stacks/kyverno/modules/kyverno/versions.tf new file mode 100644 index 00000000..d223a8f6 --- /dev/null +++ b/stacks/kyverno/modules/kyverno/versions.tf @@ -0,0 +1,11 @@ +# kubectl provider — used by kubectl_manifest resources (swapped from +# hashicorp/kubernetes kubernetes_manifest due to provider crash on Kyverno +# ClusterPolicy CRDs, beads code-e2dp). +terraform { + required_providers { + kubectl = { + source = "gavinbunney/kubectl" + version = "~> 1.14" + } + } +} diff --git a/terragrunt.hcl b/terragrunt.hcl index 0376f6ed..55165749 100644 --- a/terragrunt.hcl +++ b/terragrunt.hcl @@ -66,6 +66,13 @@ terraform { source = "goauthentik/authentik" version = "~> 2024.10" } + # kubectl (gavinbunney) — workaround for hashicorp/kubernetes + # `kubernetes_manifest` panics on Kyverno CRDs. See beads code-e2dp. + # Declared for all stacks but only used where opted-in. + kubectl = { + source = "gavinbunney/kubectl" + version = "~> 1.14" + } } } @@ -88,6 +95,11 @@ provider "vault" { address = "https://vault.viktorbarzin.me" skip_child_token = true } + +provider "kubectl" { + config_path = var.kube_config_path + load_config_file = true +} EOF }