--- # cluster-vars — the overlay knob surface (R-ENV-02). # # This ConfigMap holds BASE DEFAULTS. Flux `postBuild.substituteFrom` injects these into every # `${TOKEN}` in platform/ and apps/ manifests at reconcile time. dev/ and prod/ overlays STRATEGIC- # MERGE PATCH this single ConfigMap — that patch is the *entire* difference between environments. # # Add a knob here (with a base default) before referencing `${KNOB}` in a component manifest, then # give each overlay its env-scoped value. Never fork a base manifest to differ per environment. apiVersion: v1 kind: ConfigMap metadata: name: cluster-vars namespace: flux-system data: # --- identity of the environment --- CLUSTER_ENV: "base" # dev | prod (overlays set this) # --- DNS / TLS --- BASE_DOMAIN: "home.keithjackson.dev" # platform hostname suffix (same both envs; split-horizon) DNS_ZONE: "keithjackson.dev" # the actual Cloudflare ZONE (BASE_DOMAIN is a subdomain of it); # ExternalDNS domain-filter must be the zone, not the subdomain ACME_CLUSTER_ISSUER: "letsencrypt-staging" # dev: staging | prod: letsencrypt-prod (ADR-0006) GATEWAY_VIP: "192.168.1.53" # in-cluster split-horizon target (R-DNS-01/03): the L2-announced # ingress VIP of the `shared` Gateway. MUST equal the FIRST address of # ${CILIUM_LB_POOL_START}..${CILIUM_LB_POOL_STOP} below — the Gateway # takes the pool's first free IP, and coredns-custom answers # *.home.keithjackson.dev with THIS so pod -> public-hostname traffic # hairpins through the Gateway (correct SNI -> wildcard cert). # Base default = the DEV LAN VIP (was 10.73.0.240, an address on the # long-RETIRED libvirt network — corrected 2026-08-31 so an overlay that # forgets this knob at least lands on a REAL network). A prod overlay # MUST still set the prod LAN VIP alongside the prod L2 pool range. # --- LAN addressing (R-ENV-02) — the ONE block every new environment must re-supply ------------- # These were HARDCODED into base manifests until 2026-08-31, which broke the golden constraint: # `clusters/prod` would have rendered the DEV LAN (192.168.1.0/24) onto the prod LAN (192.168.2.0/24) # — a silent, cross-environment IP collision. They are knobs now. Base defaults ARE the dev values # (same convention as GATEWAY_VIP above: the base default is the known-good working value), so the # dev overlay restates them verbatim and a prod overlay MUST override EVERY knob in this block # before `flux bootstrap --path=clusters/prod`. # # Substitution coverage (verify before adding a knob here): every path that consumes these declares # postBuild.substituteFrom cluster-vars — platform (K3S_API_HOST), platform-config # (CILIUM_LB_POOL_*, S3_BACKUP_ENDPOINT), crafty (CRAFTY_L4_VIP), apps-wave-0 (S3_BACKUP_ENDPOINT), # velero (S3_BACKUP_ENDPOINT), apps-vms (UNIFI_L4_VIP). A ${TOKEN} in an UNSUBSTITUTED path would # ship the literal string into the cluster. # k3s API endpoint Cilium's kube-proxy replacement dials (platform/cilium/helmrelease.yaml). # [parity] MUST equal the Ansible bootstrap's k3s_api_host (infra/ansible/roles/cilium/defaults/ # main.yml -> cilium-values.yaml.j2, already inventory-parameterized). Wrong value = Cilium cannot # reach the API server on adopt and the CNI breaks. Port stays 6443 in both envs, so it is NOT a knob. K3S_API_HOST: "192.168.1.50" # CiliumLoadBalancerIPPool `default-pool` (platform-config/cilium/l2-ip-pool.yaml): the VIP range # L2-announced on the flat LAN. Must sit OUTSIDE the node IPs and any DHCP scope, and must match the # tofu reservation (cilium_l2_vip_range). INVARIANT: ${GATEWAY_VIP} must be INSIDE this range and in # practice EQUAL to START — the `shared` Gateway pins no address, so LB-IPAM hands it the pool's # first free IP. (⚠ the operator-supplied PROD values currently break this; see l2-ip-pool.yaml and # the flagged block in clusters/prod/kustomization.yaml.) CILIUM_LB_POOL_START: "192.168.1.53" CILIUM_LB_POOL_STOP: "192.168.1.60" # Pinned VIPs carved out of that pool by name (lbipam.cilium.io/ips). These are L4 workloads the # HTTPS Gateway cannot express (raw TCP/UDP), so they hold their own stable address: UNIFI_L4_VIP: "192.168.1.54" # UniFi controller VM: inform/STUN/adoption (apps/vms/unifi) CRAFTY_L4_VIP: "192.168.1.55" # Crafty: Minecraft Java (TCP) + Bedrock (UDP) game ports # Off-cluster S3 backup target (R-STATE-03, R-IDP-05) — the on-Proxmox MinIO, plain HTTP, path-style. # Consumed by BOTH backup paths: every CNPG Cluster's barmanObjectStore.endpointURL (keycloak-db, # nextcloud-db, semaphore-db) and Velero's backupStorageLocation s3Url. Full URL incl. scheme+port. # Prod needs its OWN target: a backup that lands on the dev LAN's MinIO is not off-cluster DR. S3_BACKUP_ENDPOINT: "http://192.168.1.9:9000" # --- storage (ADR-0003) --- LONGHORN_DEFAULT_REPLICA_COUNT: "3" # default StorageClass replica count (3 on 3 nodes) # --- observability sizing (docs/10) --- MONITORING_PROMETHEUS_RETENTION: "7d" # dev short | prod long MONITORING_PROMETHEUS_PVC_SIZE: "10Gi" # SIZE-based retention cap for Prometheus (Prometheus *iB units, NOT the K8s `Gi` of the PVC knob). # MUST stay below MONITORING_PROMETHEUS_PVC_SIZE — leaves headroom for the WAL + head + compaction # scratch (retentionSize counts only persisted blocks). Prevents a busy TSDB from filling /prometheus # (a full disk wedges all WAL commits and makes every absent()/==0 alert fire falsely). Base 10Gi PVC. MONITORING_PROMETHEUS_RETENTION_SIZE: "8GiB" MONITORING_LOKI_PVC_SIZE: "10Gi" # --- admission enforcement (Kyverno, ADR-0011). Base defaults are the STRICT/prod posture; # the dev overlay softens them for bootstrap (Audit / Ignore / 1) so a cold cluster can # come up before policies are proven and admission is HA. --- KYVERNO_VALIDATION_ACTION: "Enforce" # dev: Audit (observe before enforcing) KYVERNO_FORCE_FAILURE_POLICY_IGNORE: "false" # dev: "true" (chart forceFailurePolicyIgnore = fail-open) KYVERNO_ADMISSION_REPLICAS: "3" # dev: 1 (no HA needed to iterate) # --- egress / VPN (Phase 3, ADR-0015) — node-level host OpenVPN tunnel steered by Cilium Egress # Gateway (platform/vpn-egress/egress-steering.yaml). No in-cluster gateway pod. --- PIA_REGION: "SE Stockholm" # PF-capable PIA region (Sweden). Under ADR-0015 the tunnel is # host-side (Ansible vault / the node's .ovpn per docs/07), so this # is now a reference default only — retained, not substituted into # any manifest. ("SE Stockholm" is PF-capable; NOT "SE Streaming # Optimized" / a US region, which lack PF.) CLUSTER_POD_CIDR: "10.0.0.0/16" # Documents the Cilium cluster-pool pod CIDR that the egress-steering # CiliumEgressGatewayPolicy carves out of the tunnel via excludedCIDRs # (kept on the LOCAL datapath, never SNAT'd out tun0). The policy # currently excludes the whole /8 RFC1918 range; this /16 records the # actual pod allocations (Cilium assigns /24 per node from 10.0.0.0 up). # --- identity / auth (ADR-0014) --- KEYCLOAK_REPLICAS: "1" # dev 1 | prod 2 (HA) OAUTH2_PROXY_REPLICAS: "1" # dev 1 | prod 2 (HA) # --- backup / DR (ADR-0003, R-STATE-03) --- VELERO_BACKUP_SCHEDULE: "0 3 * * *" # cron; overlays may differ VELERO_RETENTION: "168h" # dev short | prod long # --- placement --- PLATFORM_NODE_SELECTOR_KEY: "kubernetes.io/os" # base no-op selector; prod may pin roles PLATFORM_NODE_SELECTOR_VALUE: "linux"