infra/stacks/kyverno/modules/kyverno/security-policies.tf
Viktor Barzin 68d9058f85 cleanup: fully remove orphaned council-complaints app
The council-complaints app (Islington civic-reporting pilot) has been
abandoned. It was already dead in the cluster (deployments scaled 0/0,
image only on the decommissioned registry.viktorbarzin.me which 404s),
and it was never in Terraform — only docs + a kyverno comment referenced
it. Its live cluster resources (namespace, both NFS-backed PVs, ingresses)
were torn down out-of-band via kubectl (nothing in TF to drift from); the
DB-dump PVC was backed up to NFS first.

This removes the remaining repo references to the live app:
- service-catalog.md: drop the council-complaints row
- ci-cd.md + .claude/CLAUDE.md: drop it from the GHA->ghcr app list
- kyverno require-trusted-registries: the registry.viktorbarzin.me/*
  allowlist comment claimed council-complaints as the last referencer;
  rewrite it (no live workload pulls from that registry now; only stale
  completed Job records still carry the ref). The allowlist line itself
  is kept (registry-scoped, not app-specific).

Historical point-in-time plan docs (docs/plans/2026-05-16-auto-upgrade-
apps-{design,plan}.md) still mention it inside a frozen "10 GHA-migrated
repos (memory id=388)" snapshot; left as-is so the dated record stays
accurate.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-21 13:32:10 +00:00

375 lines
14 KiB
HCL

# =============================================================================
# Pod Security Policies
# =============================================================================
# Kyverno validate policies for pod security standards.
# Wave 1 (locked 2026-05-18, beads code-8ywc): deny-privileged-containers,
# deny-host-namespaces, restrict-sys-admin flipped from Audit → Enforce with
# a shared 32-namespace exclude list. require-trusted-registries followed on
# 2026-05-19 — also Enforce now, with an explicit registry allowlist (the
# `*/*` catch-all was removed so unknown registries fail closed at admission).
# To allow a new image source, add it to policy_require_trusted_registries below.
# failurePolicy stays Ignore (chart-level) to prevent admission webhook
# failures from cascading.
# Shared namespace exclude list — 31 critical namespaces from the Keel rollout
# (memory id=1970) + `frigate` (legitimately needs host access for camera RTSP).
locals {
security_policy_exclude_namespaces = [
"keel", "calico-system", "authentik", "vault", "cnpg-system", "dbaas",
"monitoring", "traefik", "technitium", "mailserver", "kyverno",
"metallb-system", "external-secrets", "proxmox-csi", "nfs-csi", "nvidia",
"kube-system", "cloudflared", "crowdsec", "reverse-proxy", "reloader",
"descheduler", "vpa", "redis", "sealed-secrets", "headscale", "wireguard",
"xray", "infra-maintenance", "metrics-server", "tigera-operator", "frigate",
# Additions discovered during wave 1 enforce flip — these contain workloads
# that legitimately need privileged / hostNetwork / SYS_ADMIN:
"kured", # kured DaemonSet is privileged (manages node reboots)
"default", # etcd backup + defrag CronJobs use hostNetwork
"changedetection", # uses SYS_ADMIN for chromium sandbox
"woodpecker", # CI pipeline pods (wp-*) run privileged docker builds
"android-emulator", # emulator pod is privileged for /dev/kvm (ADR-0001)
]
}
resource "kubectl_manifest" "policy_deny_privileged" {
yaml_body = yamlencode({
apiVersion = "kyverno.io/v1"
kind = "ClusterPolicy"
metadata = {
name = "deny-privileged-containers"
annotations = {
"policies.kyverno.io/title" = "Deny Privileged Containers"
"policies.kyverno.io/category" = "Pod Security"
"policies.kyverno.io/severity" = "high"
"policies.kyverno.io/description" = "Privileged containers have full host access. Deny unless explicitly exempted."
}
}
spec = {
validationFailureAction = "Enforce"
background = true
rules = [{
name = "deny-privileged"
match = {
any = [{
resources = {
kinds = ["Pod"]
}
}]
}
exclude = {
any = [{
resources = {
namespaces = local.security_policy_exclude_namespaces
}
}]
}
validate = {
message = "Privileged containers are not allowed. Use specific capabilities instead."
pattern = {
spec = {
containers = [{
"=(securityContext)" = {
"=(privileged)" = false
}
}]
"=(initContainers)" = [{
"=(securityContext)" = {
"=(privileged)" = false
}
}]
}
}
}
}]
}
})
depends_on = [helm_release.kyverno]
}
resource "kubectl_manifest" "policy_deny_host_namespaces" {
yaml_body = yamlencode({
apiVersion = "kyverno.io/v1"
kind = "ClusterPolicy"
metadata = {
name = "deny-host-namespaces"
annotations = {
"policies.kyverno.io/title" = "Deny Host Namespaces"
"policies.kyverno.io/category" = "Pod Security"
"policies.kyverno.io/severity" = "high"
"policies.kyverno.io/description" = "Sharing host namespaces enables container escapes. Deny hostNetwork, hostPID, hostIPC."
}
}
spec = {
validationFailureAction = "Enforce"
background = true
rules = [{
name = "deny-host-namespaces"
match = {
any = [{
resources = {
kinds = ["Pod"]
}
}]
}
exclude = {
any = [{
resources = {
namespaces = local.security_policy_exclude_namespaces
}
}]
}
validate = {
message = "Host namespaces (hostNetwork, hostPID, hostIPC) are not allowed."
pattern = {
spec = {
"=(hostNetwork)" = false
"=(hostPID)" = false
"=(hostIPC)" = false
}
}
}
}]
}
})
depends_on = [helm_release.kyverno]
}
resource "kubectl_manifest" "policy_restrict_capabilities" {
yaml_body = yamlencode({
apiVersion = "kyverno.io/v1"
kind = "ClusterPolicy"
metadata = {
name = "restrict-sys-admin"
annotations = {
"policies.kyverno.io/title" = "Restrict SYS_ADMIN Capability"
"policies.kyverno.io/category" = "Pod Security"
"policies.kyverno.io/severity" = "high"
"policies.kyverno.io/description" = "SYS_ADMIN is nearly equivalent to root. Restrict to explicitly exempted namespaces."
}
}
spec = {
validationFailureAction = "Enforce"
background = true
rules = [{
name = "restrict-sys-admin"
match = {
any = [{
resources = {
kinds = ["Pod"]
}
}]
}
exclude = {
any = [{
resources = {
namespaces = local.security_policy_exclude_namespaces
}
}]
}
validate = {
message = "Adding SYS_ADMIN capability is not allowed."
deny = {
conditions = {
any = [{
key = "{{ request.object.spec.containers[].securityContext.capabilities.add[] || `[]` }}"
operator = "AnyIn"
value = ["SYS_ADMIN"]
}]
}
}
}
}]
}
})
depends_on = [helm_release.kyverno]
}
# =============================================================================
# Image Pull Policy Governance
# =============================================================================
# Mutate imagePullPolicy to IfNotPresent for all containers with pinned tags
# (non-:latest). This prevents pods from getting stuck in ImagePullBackOff
# 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 "kubectl_manifest" "policy_set_image_pull_policy" {
yaml_body = yamlencode({
apiVersion = "kyverno.io/v1"
kind = "ClusterPolicy"
metadata = {
name = "set-image-pull-policy"
annotations = {
"policies.kyverno.io/title" = "Set Image Pull Policy"
"policies.kyverno.io/category" = "Best Practices"
"policies.kyverno.io/severity" = "medium"
"policies.kyverno.io/description" = "Set imagePullPolicy to IfNotPresent for pinned tags and Always for :latest to prevent ImagePullBackOff from transient cache failures."
}
}
spec = {
background = false
rules = [
{
name = "set-ifnotpresent-for-pinned-tags"
match = {
any = [{
resources = {
kinds = ["Pod"]
}
}]
}
mutate = {
foreach = [{
list = "request.object.spec.containers"
preconditions = {
all = [{
key = "{{ ends_with(element.image, ':latest') || !contains(element.image, ':') }}"
operator = "Equals"
value = false
}]
}
patchStrategicMerge = {
spec = {
containers = [{
name = "{{ element.name }}"
imagePullPolicy = "IfNotPresent"
}]
}
}
}]
}
},
{
name = "set-always-for-latest"
match = {
any = [{
resources = {
kinds = ["Pod"]
}
}]
}
mutate = {
foreach = [{
list = "request.object.spec.containers"
preconditions = {
all = [{
key = "{{ ends_with(element.image, ':latest') || !contains(element.image, ':') }}"
operator = "Equals"
value = true
}]
}
patchStrategicMerge = {
spec = {
containers = [{
name = "{{ element.name }}"
imagePullPolicy = "Always"
}]
}
}
}]
}
}
]
}
})
depends_on = [helm_release.kyverno]
}
resource "kubectl_manifest" "policy_require_trusted_registries" {
yaml_body = yamlencode({
apiVersion = "kyverno.io/v1"
kind = "ClusterPolicy"
metadata = {
name = "require-trusted-registries"
annotations = {
"policies.kyverno.io/title" = "Require Trusted Image Registries"
"policies.kyverno.io/category" = "Pod Security"
"policies.kyverno.io/severity" = "medium"
"policies.kyverno.io/description" = "Images must come from trusted registries to prevent supply chain attacks."
}
}
spec = {
# Wave 1 W1.5: flipped Audit → Enforce 2026-05-19 with explicit allowlist.
# Allowlist enumerated from `kubectl get pods -A -o jsonpath='{..image}'`
# on 2026-05-18; covers all in-cluster image sources. Update on adding new
# workloads from a registry NOT in this list (and ask if the new registry
# is trusted before opening it). The `*/*` catch-all was deliberately
# removed so unknown registries fail closed at admission.
validationFailureAction = "Enforce"
background = true
rules = [{
name = "validate-registries"
match = {
any = [{
resources = {
kinds = ["Pod"]
}
}]
}
exclude = {
any = [{
resources = {
namespaces = local.security_policy_exclude_namespaces
}
}]
}
validate = {
message = "Images must be from trusted registries. Allowlist defined in stacks/kyverno/modules/kyverno/security-policies.tf — add the new registry there if intentional, otherwise switch the workload to a trusted source."
pattern = {
spec = {
containers = [{
image = join(" | ", [
# Explicit registries
"docker.io/*", "ghcr.io/*", "quay.io/*", "registry.k8s.io/*",
"gcr.io/*", "us-docker.pkg.dev/*", "lscr.io/*",
"codeberg.org/*", "mcr.microsoft.com/*", "nvcr.io/*",
"oci.external-secrets.io/*", "reg.kyverno.io/*",
"docker.n8n.io/*", "registry.gitlab.com/*",
# Private
"forgejo.viktorbarzin.me/*", "10.0.20.10*",
# Legacy private registry (decommissioned 2026-05-07 per CLAUDE.md).
# No live workload pulls from it; only stale completed Job records
# (e.g. old wealthfolio-sync jobs) still carry the image ref.
"registry.viktorbarzin.me/*",
# DockerHub library (bare image names without slash)
"alpine*", "busybox*", "kong*", "mysql*", "nginx*", "postgres*", "python*",
# DockerHub user repos (no registry prefix, has slash) —
# enumerated from current cluster state. New entries added
# 2026-05-22 after Enforce caught these as unallowlisted:
# amruthpillai (resume), athomasson2 (ebook2audiobook),
# netboxcommunity (netbox), nousresearch (hermes-agent),
# opentripplanner (osm-routing), rhasspy (whisper/piper).
# 2026-06-05: mauriceboe (TREK group-trip planner trial).
"actualbudget/*", "afadil/*", "amruthpillai/*", "athomasson2/*",
"binwiederhier/*", "bitnami/*",
"clickhouse/*", "cloudflare/*", "coturn/*", "crowdsecurity/*",
"curlimages/*", "deluan/*", "dgtlmoon/*", "dolthub/*",
"dpage/*", "dperson/*", "edoburu/*", "esanchezm/*",
"freikin/*", "freshrss/*", "hackmdio/*", "hashicorp/*",
"headscale/*", "jhonderson/*", "kebe/*", "library/*",
"lissy93/*", "louislam/*", "matrixdotorg/*", "mauriceboe/*",
"mendhak/*",
"mghee/*", "mindflavor/*", "mpepping/*", "netboxcommunity/*",
"netsampler/*", "nousresearch/*", "nvidia/*", "onlyoffice/*",
"openresty/*", "opentripplanner/*", "owntracks/*",
"phpipam/*", "phpmyadmin/*", "privatebin/*", "prom/*",
"prompve/*", "rancher/*", "rhasspy/*", "roundcube/*", "sclevine/*",
"shadowsocks/*", "shlinkio/*", "stirlingtools/*",
"technitium/*", "teddysun/*", "temporalio/*",
"typhonragewind/*", "tzahi12345/*", "vabene1111/*",
"vaultwarden/*", "viktorbarzin/*", "viren070/*",
"woodpeckerci/*", "zelest/*",
])
}]
}
}
}
}]
}
})
depends_on = [helm_release.kyverno]
}