T-0.1 Flux GitOps layout + dev/prod overlays + platform component skeletons (R-GITOPS-01/03, R-ENV-02, R-PLAT-01)

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
This commit is contained in:
jakman85andClaude Opus 4.8 committed 2026-08-16 00:08:31 -05:00
1 parent bd8de96456
commit af4f6c07ae
75 files changed
+1638 -4

No files matched your search

+77 -2
View File
@@ -1,3 +1,78 @@
# clusters
# clusters/
_Placeholder — populated in Phase 0. See ../home-network-k3s (read-only spec)._
Flux entrypoints and the environment-overlay model (**R-ENV-02**, **R-GITOPS-01/02/03**).
Governing spec: [`docs/05-gitops-and-delivery.md`](../../home-network-k3s/docs/05-gitops-and-delivery.md),
[`docs/02-environments-and-provisioning.md`](../../home-network-k3s/docs/02-environments-and-provisioning.md),
[ADR-0004 (Flux)](../../home-network-k3s/decisions/adr-0004-gitops-flux.md).
> Phase 0 scaffolding only. These are structurally-correct stubs. **No cluster exists** — nothing
> here reconciles until WS-INFRA delivers a cluster and bootstraps Flux (roadmap Phase 1).
## Layout — base + overlay (never fork the base)
```
clusters/
base/ shared Flux objects (one authoring, both environments)
flux-system.yaml GitRepository — the reconciliation source (bootstrap → Gitea)
platform.yaml Flux Kustomization → ./platform (reconciles first)
apps.yaml Flux Kustomization → ./apps (dependsOn platform)
cluster-vars.yaml ConfigMap of overlay knobs (BASE defaults)
kustomization.yaml
dev/ ACTIVE build target — patches cluster-vars with dev values only
kustomization.yaml
prod/ DESIGN-ONLY until roadmap Phase 7 — patches cluster-vars with prod values only
kustomization.yaml
```
## How the overlay model satisfies R-ENV-02
The base authors the reconciliation graph **once**. `platform.yaml` and `apps.yaml` (Flux
`Kustomization`s) both declare `postBuild.substituteFrom` the `cluster-vars` ConfigMap. Platform and
app manifests use `${TOKEN}` placeholders for every value that legitimately differs between
environments (issuer, replica counts, storage sizes, retention, node placement).
**`clusters/dev` and `clusters/prod` differ ONLY by the values they patch into `cluster-vars`** —
they add no resources of their own and never copy/fork a base manifest. Promotion to prod
([`docs/02`](../../home-network-k3s/docs/02-environments-and-provisioning.md)) is a change to the
prod overlay's variables, nothing else.
## How Flux is pointed at an environment
`flux bootstrap` (run by WS-INFRA in Phase 1) targets one environment's path per cluster:
```
# dev cluster
flux bootstrap ... --path=clusters/dev
# prod cluster (Phase 7 only — design-only until then)
flux bootstrap ... --path=clusters/prod
```
Bootstrap materializes the `flux-system` `Kustomization` that renders the chosen overlay, which pulls
in `base/` and applies the env-scoped `cluster-vars`.
## Reconciliation order
```
GitRepository (flux-system)
│
▼
Kustomization: platform (./platform, substituteFrom cluster-vars)
│ healthy
▼
Kustomization: apps (./apps, dependsOn platform, substituteFrom cluster-vars)
```
`apps` `dependsOn` `platform` so CNI/storage/TLS/DNS/gateway exist before any workload reconciles.
## TODOs (later phases — do not action in Phase 0)
- **Source cutover (WS-GITOPS, Phase 6):** `base/flux-system.yaml` currently points at the bootstrap
source. After Gitea is onboarded, repoint `spec.url` to `gitea.home.keithjackson.dev` and keep an
off-cluster mirror (spec `docs/05` "Source-of-truth", `CONTRIBUTING.md`).
- **SOPS decryption (WS-SEC, Phase 1):** add `spec.decryption.provider: sops` (age secret name only,
never plaintext) to the platform/apps Kustomizations once the age key is bootstrapped
([ADR-0007](../../home-network-k3s/decisions/adr-0007-secrets-sops-age.md)).
- **Split ordered Flux Kustomizations** if intra-platform ordering needs to be explicit (e.g. a
separate `cilium`/`longhorn` Kustomization others `dependsOn`).
</content>
+26
View File
@@ -0,0 +1,26 @@
---
# Flux Kustomization: APPLICATION workloads. Reconciles ./apps AFTER platform is healthy.
# Phase 0 STUB. apps/ is owned by WS-APPS; this entrypoint is the GitOps contract that renders it.
apiVersion: kustomize.toolkit.fluxcd.io/v1
kind: Kustomization
metadata:
name: apps
namespace: flux-system
spec:
interval: 10m
retryInterval: 2m
timeout: 5m
prune: true
wait: true
# Ordering: platform (CNI, storage, TLS, DNS, gateway, identity) must be healthy first.
dependsOn:
- name: platform
sourceRef:
kind: GitRepository
name: flux-system
path: ./apps
postBuild:
substituteFrom:
- kind: ConfigMap
name: cluster-vars
# TODO(WS-SEC, Phase 1): enable SOPS decryption (ADR-0007) — reference age secret by name only.
+41
View File
@@ -0,0 +1,41 @@
---
# 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" # same in both envs (split-horizon per docs/09)
acme_cluster_issuer: "letsencrypt-staging" # dev: staging | prod: letsencrypt-prod (ADR-0006)
# --- 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"
monitoring_loki_pvc_size: "10Gi"
# --- 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"
+25
View File
@@ -0,0 +1,25 @@
---
# GitRepository — the single source of truth Flux reconciles from (R-GITOPS-01).
# Phase 0 STUB: not applied until Flux is bootstrapped in Phase 1 (WS-INFRA).
#
# Bootstrapping sequence (spec docs/05 "Source-of-truth", CONTRIBUTING.md):
# 1. Initially points at the BOOTSTRAP SOURCE (this local repo / out-of-band).
# 2. Gitea is onboarded early (roadmap Phase 6).
# 3. TODO(WS-GITOPS, Phase 6): repoint spec.url -> https://gitea.home.keithjackson.dev/<org>/home-network-k3s-impl
# and keep an off-cluster mirror so a source-of-truth outlives the cluster.
apiVersion: source.toolkit.fluxcd.io/v1
kind: GitRepository
metadata:
name: flux-system
namespace: flux-system
spec:
interval: 1m
# TODO(WS-INFRA/WS-GITOPS): set to the bootstrap source URL at Phase-1 bootstrap.
url: https://REPLACE-ME.invalid/home-network-k3s-impl.git
ref:
# Reconcile the tracked branch. Overlay may patch this if dev/prod track different branches;
# the default model tracks a single branch and differentiates by clusters/<env> path.
branch: main
# TODO(WS-SEC, Phase 1): reference the SOPS/age or deploy-key secret BY NAME only (no plaintext).
# secretRef:
# name: flux-system # created by `flux bootstrap`
+10
View File
@@ -0,0 +1,10 @@
---
# Base Flux entrypoint set. Shared by BOTH environment overlays (clusters/dev, clusters/prod).
# Overlays reference this base and patch ONLY the cluster-vars ConfigMap (R-ENV-02).
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- flux-system.yaml # GitRepository (reconciliation source)
- platform.yaml # Flux Kustomization -> ./platform
- apps.yaml # Flux Kustomization -> ./apps (dependsOn platform)
- cluster-vars.yaml # overlay knob surface (base defaults)
+32
View File
@@ -0,0 +1,32 @@
---
# Flux Kustomization: PLATFORM services (cluster-wide). Reconciles ./platform.
# Phase 0 STUB. Realizes R-GITOPS-01/03 (pull-based reconcile + drift correction) and
# R-ENV-02 (env differences supplied ONLY via substituteFrom cluster-vars).
apiVersion: kustomize.toolkit.fluxcd.io/v1
kind: Kustomization
metadata:
name: platform
namespace: flux-system
spec:
interval: 10m
retryInterval: 2m
timeout: 5m
prune: true # drift correction: delete resources removed from git (R-GITOPS-03)
wait: true # block `apps` until platform is healthy
sourceRef:
kind: GitRepository
name: flux-system
path: ./platform
# R-ENV-02: the ONLY place environment differences enter platform manifests.
# dev/prod overlays patch the cluster-vars ConfigMap; the base manifests are never forked.
postBuild:
substituteFrom:
- kind: ConfigMap
name: cluster-vars
# TODO(WS-SEC, Phase 1): enable SOPS decryption once the age key exists (ADR-0007).
# decryption:
# provider: sops
# secretRef:
# name: sops-age
# TODO(PM): the top-level ./platform/kustomization.yaml must include platform/kyverno/
# (owned by the policy engineer, WS-TEST). See platform/README.md integration TODO.
+46
View File
@@ -0,0 +1,46 @@
---
# clusters/dev — the ACTIVE build target overlay.
#
# `flux bootstrap ... --path=clusters/dev` points the dev cluster's Flux here.
# This overlay adds NO resources of its own. Its ONLY job is to patch the shared cluster-vars
# ConfigMap with dev-scoped values (R-ENV-02). The base reconciliation graph is reused verbatim.
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- ../base
patches:
- target:
kind: ConfigMap
name: cluster-vars
patch: |-
- op: replace
path: /data/cluster_env
value: "dev"
# DNS-01 against LE staging while iterating (avoids prod rate limits) — ADR-0006.
- op: replace
path: /data/acme_cluster_issuer
value: "letsencrypt-staging"
# dev mirrors prod's 3-node topology, so the replicated tier keeps 3 replicas.
- op: replace
path: /data/longhorn_default_replica_count
value: "3"
# smaller retention / volumes to keep the dev workstation footprint light.
- op: replace
path: /data/monitoring_prometheus_retention
value: "3d"
- op: replace
path: /data/monitoring_prometheus_pvc_size
value: "5Gi"
- op: replace
path: /data/monitoring_loki_pvc_size
value: "5Gi"
# single-replica control-plane addons in dev (no HA needed to iterate).
- op: replace
path: /data/keycloak_replicas
value: "1"
- op: replace
path: /data/oauth2_proxy_replicas
value: "1"
- op: replace
path: /data/velero_retention
value: "72h"
+47
View File
@@ -0,0 +1,47 @@
---
# clusters/prod — DESIGN-ONLY overlay. NOT a build target until roadmap Phase 7 opens prod
# (spec roadmap/phases.md, AGENTS.md §6: "Never build directly on production").
#
# When Phase 7 is opened, `flux bootstrap ... --path=clusters/prod` points the prod cluster's Flux
# here. Like dev, this overlay adds NO resources — it ONLY patches cluster-vars with prod-scoped
# values (R-ENV-02). Promotion to prod is a change to THESE variables, never a base edit.
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- ../base
patches:
- target:
kind: ConfigMap
name: cluster-vars
patch: |-
- op: replace
path: /data/cluster_env
value: "prod"
# trusted, rate-limited real certs in prod — ADR-0006.
- op: replace
path: /data/acme_cluster_issuer
value: "letsencrypt-prod"
- op: replace
path: /data/longhorn_default_replica_count
value: "3"
# longer retention / larger volumes on prod NVMe.
- op: replace
path: /data/monitoring_prometheus_retention
value: "30d"
- op: replace
path: /data/monitoring_prometheus_pvc_size
value: "50Gi"
- op: replace
path: /data/monitoring_loki_pvc_size
value: "50Gi"
# HA control-plane addons in prod.
- op: replace
path: /data/keycloak_replicas
value: "2"
- op: replace
path: /data/oauth2_proxy_replicas
value: "2"
- op: replace
path: /data/velero_retention
value: "720h"
# TODO(WS-INFRA, Phase 7): pin prod node placement once the bare-metal role labels exist.
+86 -2
View File
@@ -1,3 +1,87 @@
# platform
# platform/
_Placeholder — populated in Phase 0. See ../home-network-k3s (read-only spec)._
Cluster-wide **platform services**, Flux-managed (**R-PLAT-01**, **R-GITOPS-01/02/03**). Reconciled
by the `platform` Flux Kustomization defined in [`clusters/base/platform.yaml`](../clusters/base/platform.yaml),
**before** `apps/`.
Governing spec: [`docs/05-gitops-and-delivery.md`](../../home-network-k3s/docs/05-gitops-and-delivery.md),
[`docs/01-architecture-overview.md`](../../home-network-k3s/docs/01-architecture-overview.md).
> **Phase 0 scaffolding only.** Every component here is a structurally-correct **stub** — a Flux
> HelmRelease and/or Kustomization with a values placeholder, a README citing the governing ADR/spec,
> and TODOs for the phase that fills it in. **No cluster exists**; nothing reconciles yet. Do not add
> working config, secrets, or provisioning under Phase 0 (spec `AGENTS.md` §1/§5/§6, `roadmap/phases.md`).
## Reconciliation flow (how a change lands)
```
git (source of truth)
│ operator PR → CI (lint · schema · Kyverno L0 · Chainsaw) → merge to tracked branch
▼
Flux GitRepository (clusters/base/flux-system.yaml)
▼
Flux Kustomization "platform" → ./platform (this dir) ── substituteFrom cluster-vars ──┐
│ prune=true (drift correction, R-GITOPS-03) · wait=true │ R-ENV-02
▼ │
Flux Kustomization "apps" → ./apps (dependsOn platform) ─ substituteFrom cluster-vars ──┘
```
Git is authoritative; the cluster conforms to git and drift is auto-corrected (R-GITOPS-01/03).
Manual `kubectl apply` of platform state is prohibited (spec `AGENTS.md` §5).
## The dev ↔ prod overlay model (R-ENV-02)
Platform manifests are authored **once**. Every value that legitimately differs between environments
is a `${TOKEN}` resolved from the `cluster-vars` ConfigMap via Flux `postBuild.substituteFrom`. The
`clusters/dev` and `clusters/prod` overlays differ **only** by the values they patch into
`cluster-vars` (issuer, replica counts, storage sizes, retention). See
[`clusters/README.md`](../clusters/README.md). Base manifests are **never forked per environment**.
## Components (one directory each)
| Dir | Component | Governing ADR / spec | Serves |
|---|---|---|---|
| `cilium/` | CNI + kube-proxy replacement + Gateway API + Egress Gateway + Hubble | [ADR-0002](../../home-network-k3s/decisions/adr-0002-cni-cilium.md), docs/03 | R-NET-* |
| `longhorn/` | Replicated block storage; 3-replica default StorageClass + bulk tier | [ADR-0003](../../home-network-k3s/decisions/adr-0003-storage-longhorn.md), docs/04 | R-STATE-* |
| `cert-manager/` | TLS via Let's Encrypt DNS-01 (Cloudflare); staging + prod ClusterIssuers | [ADR-0006](../../home-network-k3s/decisions/adr-0006-tls-cert-manager-letsencrypt.md), docs/08 | R-TLS-* |
| `externaldns/` | Automatic DNS records for public services (Cloudflare) | docs/09 | R-DNS-* |
| `gateway/` | Gateway API GatewayClass + shared HTTPS Gateway + wildcard cert | [ADR-0002](../../home-network-k3s/decisions/adr-0002-cni-cilium.md)/[ADR-0006](../../home-network-k3s/decisions/adr-0006-tls-cert-manager-letsencrypt.md), docs/09 | R-DNS-01, R-TLS-01 |
| `monitoring/` | kube-prometheus-stack + Loki + Grafana Alloy + Alertmanager | docs/10 | R-OBS-* |
| `velero/` | Off-cluster backup/DR of volumes + resource state | [ADR-0003](../../home-network-k3s/decisions/adr-0003-storage-longhorn.md), docs/04 | R-STATE-03 |
| `kubevirt/` | KubeVirt + CDI (IaaS: application VMs on the same K8s surface) | [ADR-0005](../../home-network-k3s/decisions/adr-0005-vm-workloads-kubevirt.md), docs/06 | R-PLAT-03 |
| `image-automation/` | Flux image-update-automation (hands-off image tag advance) | [ADR-0009](../../home-network-k3s/decisions/adr-0009-automated-image-updates.md), docs/11 | R-UPD-02 |
| `keycloak/` | Platform IdP (OIDC + SAML), config-as-code, PostgreSQL on Longhorn | [ADR-0014](../../home-network-k3s/decisions/adr-0014-idp-keycloak.md), docs/15 | R-IDP-* |
| `oauth2-proxy/` | Forward-auth at the Gateway for non-OIDC apps → Keycloak | [ADR-0014](../../home-network-k3s/decisions/adr-0014-idp-keycloak.md), docs/15 | R-IDP-* |
| `kyverno/` | **L0 policy-as-code gate — OWNED BY THE POLICY ENGINEER (WS-TEST).** | [ADR-0011](../../home-network-k3s/decisions/adr-0011-test-tooling-kyverno-chainsaw.md), docs/14 | R-TEST-* |
## ⚠️ INTEGRATION TODO(PM): wire kyverno into the platform kustomization
`platform/kyverno/` is authored by a **different agent** (the policy engineer, WS-TEST). This
workstream did **not** create any files under `platform/kyverno/` and must not.
[`platform/kustomization.yaml`](kustomization.yaml) intentionally **omits `- kyverno`**. Once the
policy engineer's files land, the **PM** must add `- kyverno` to that list (recommended first, so
policies are present before other components reconcile). The line is present as a commented TODO in
the kustomization.
## Secrets
Every secret referenced below (Cloudflare API token, Velero backup-target creds, Keycloak admin/DB,
oauth2-proxy cookie/client secrets) is referenced **by name only** and will be committed
**SOPS-encrypted** (WS-SEC, [ADR-0007](../../home-network-k3s/decisions/adr-0007-secrets-sops-age.md)).
**No plaintext secret appears anywhere in this tree.**
## Cross-workstream contracts surfaced by these stubs
- **cilium (WS-NET ↔ WS-INFRA):** Cilium's *initial* CNI install is done by Ansible at cluster
bootstrap (docs/02, Phase 1). The HelmRelease here manages Cilium *config* thereafter; its Helm
values MUST match the Ansible bootstrap values (kubeProxyReplacement, k8sServiceHost/Port, API
server host) or reconciliation will fight the bootstrap. Contract owner: WS-NET + WS-INFRA.
- **gateway + cert-manager + externaldns (WS-NET + WS-SEC):** the shared Gateway listener, the
wildcard `*.home.keithjackson.dev` cert, and the ExternalDNS source all reference each other; they
form the "Issuer + Gateway TLS" interface in `roadmap/workstreams.md`.
- **image-automation (WS-UPDATE):** this dir holds only the *cluster-wide* ImageUpdateAutomation +
git write-back. Per-service `ImageRepository`/`ImagePolicy` are rendered from each
ServiceDefinition's `update` block (WS-GITOPS/WS-UPDATE), living with `apps/`.
- **keycloak/oauth2-proxy (WS-SEC):** the SSO/auth contract (`auth` block → Keycloak client /
oauth2-proxy protection) is owned by WS-SEC; these are placeholders pending that contract.
+34
View File
@@ -0,0 +1,34 @@
# platform/cert-manager
**cert-manager** — trusted TLS via **Let's Encrypt**, **ACME DNS-01** (Cloudflare).
- **Governing ADR:** [ADR-0006 — TLS: cert-manager + Let's Encrypt (DNS-01)](../../../home-network-k3s/decisions/adr-0006-tls-cert-manager-letsencrypt.md)
- **Spec:** [docs/08 — TLS & Certificates](../../../home-network-k3s/docs/08-tls-and-certificates.md),
[docs/09 — DNS & Ingress](../../../home-network-k3s/docs/09-dns-and-ingress.md)
- **Serves:** R-TLS-01/02/03, R-DNS-01
- **Owner:** WS-SEC
## What this stub is
Flux `HelmRelease` + **two `ClusterIssuer`s** (`letsencrypt-staging`, `letsencrypt-prod` — both
mandated by ADR-0006). DNS-01 against the `keithjackson.dev` zone validates domain control **without
exposing the service** (works for internal-only services, R-TLS-03) and enables the **wildcard**
`*.home.keithjackson.dev` cert defined in [`../gateway`](../gateway).
## Contract (Issuer + Gateway TLS)
Part of the "Issuer + Gateway TLS" interface (`roadmap/workstreams.md`, WS-SEC + WS-NET): the wildcard
`Certificate` (in `gateway/`) references the issuer named by `${ACME_CLUSTER_ISSUER}` and its secret
is consumed by the Gateway HTTPS listener.
## Secrets
The Cloudflare API token is referenced **by name** (`cloudflare-api-token`) and committed
**SOPS-encrypted** (WS-SEC, OQ-4, ADR-0007). **No plaintext token in git.**
## TODO (later phases)
- **OQ-4 (resolved to Cloudflare):** WS-SEC provisions the scoped Cloudflare token, SOPS-encrypted.
- Set the ACME account email.
- **Phase 2 (WS-SEC):** issue the wildcard cert; prove a trusted, non-self-signed cert at
`*.home.keithjackson.dev` (R-TLS-01/03, Phase-2 exit gate).
+48
View File
@@ -0,0 +1,48 @@
---
# ClusterIssuers — Let's Encrypt via ACME DNS-01 against the keithjackson.dev zone (Cloudflare).
# ADR-0006 mandates BOTH a staging issuer (bring-up) and a prod issuer. DNS-01 enables wildcard
# certs and does NOT require exposing the service (works for internal-only services, R-TLS-03).
#
# Phase 0 STUB: emails/tokens are placeholders. The Cloudflare API token is referenced BY NAME only
# and will be committed SOPS-encrypted (OQ-4, ADR-0007) — NO plaintext secret here.
apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
name: letsencrypt-staging
spec:
acme:
server: https://acme-staging-v02.api.letsencrypt.org/directory
email: [email protected] # TODO(WS-SEC): ACME account email
privateKeySecretRef:
name: letsencrypt-staging-account-key
solvers:
- dns01:
cloudflare:
apiTokenSecretRef:
name: cloudflare-api-token # SOPS-encrypted (WS-SEC) — name only
key: api-token
selector:
dnsZones:
- "keithjackson.dev"
---
apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
name: letsencrypt-prod
spec:
acme:
server: https://acme-v02.api.letsencrypt.org/directory
email: [email protected] # TODO(WS-SEC): ACME account email
privateKeySecretRef:
name: letsencrypt-prod-account-key
solvers:
- dns01:
cloudflare:
apiTokenSecretRef:
name: cloudflare-api-token # SOPS-encrypted (WS-SEC) — name only
key: api-token
selector:
dnsZones:
- "keithjackson.dev"
# The active issuer per environment is chosen via ${ACME_CLUSTER_ISSUER} (cluster-vars):
# dev -> letsencrypt-staging, prod -> letsencrypt-prod (R-ENV-02). See gateway/ wildcard Certificate.
+28
View File
@@ -0,0 +1,28 @@
---
# cert-manager — issues trusted Let's Encrypt certs via ACME DNS-01 (ADR-0006). Phase 0 STUB.
apiVersion: helm.toolkit.fluxcd.io/v2
kind: HelmRelease
metadata:
name: cert-manager
namespace: cert-manager
spec:
interval: 30m
chart:
spec:
chart: cert-manager
version: ">=1.16.0 <1.17.0" # TODO(WS-SEC): pin at bring-up; Renovate maintains
sourceRef:
kind: HelmRepository
name: jetstack
namespace: cert-manager
install:
remediation:
retries: 3
upgrade:
remediation:
retries: 3
values:
crds:
enabled: true # install CRDs with the chart
# TODO(WS-SEC, Phase 2): DNS-01 recursive nameserver config if split-horizon DNS interferes
# with propagation checks (docs/09 split-horizon note).
@@ -0,0 +1,9 @@
---
apiVersion: source.toolkit.fluxcd.io/v1
kind: HelmRepository
metadata:
name: jetstack
namespace: cert-manager
spec:
interval: 1h
url: https://charts.jetstack.io
+8
View File
@@ -0,0 +1,8 @@
---
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- namespace.yaml
- helmrepository.yaml
- helmrelease.yaml
- clusterissuers.yaml
+5
View File
@@ -0,0 +1,5 @@
---
apiVersion: v1
kind: Namespace
metadata:
name: cert-manager
+6
View File
@@ -0,0 +1,6 @@
# cert-manager Helm values — PLACEHOLDER (Phase 0). Authoritative values inline in helmrelease.yaml.
#
# crds.enabled: true # install CRDs with the chart
# dns01RecursiveNameservers / -Only # TODO(WS-SEC): may be needed with split-horizon DNS
# ClusterIssuers (letsencrypt-staging/-prod) live in clusterissuers.yaml, not here.
# Cloudflare API token: SOPS-encrypted secret `cloudflare-api-token` (WS-SEC, OQ-4) — name only.
+30
View File
@@ -0,0 +1,30 @@
# platform/cilium
**Cilium** — CNI, kube-proxy replacement, Gateway API ingress, Egress Gateway, and Hubble.
- **Governing ADR:** [ADR-0002 — CNI, network policy & ingress: Cilium + Gateway API](../../../home-network-k3s/decisions/adr-0002-cni-cilium.md)
- **Spec:** [docs/03 — Networking](../../../home-network-k3s/docs/03-networking-isolation-and-egress.md),
[docs/09 — DNS & Ingress](../../../home-network-k3s/docs/09-dns-and-ingress.md)
- **Serves:** R-NET-01..07, R-DNS-01, R-SEC-02
- **Owner:** WS-NET (bootstrap shared with WS-INFRA)
## What this stub is
A Flux `HelmRelease` + `HelmRepository` stub with placeholder values. **No cluster exists** (Phase 0);
this does not reconcile. Cilium replaces k3s' bundled **flannel + kube-proxy + Traefik**
(ADR-0001/0002).
## Bootstrap vs. GitOps (important)
Per [docs/02](../../../home-network-k3s/docs/02-environments-and-provisioning.md), Cilium's **initial**
install is done by the **Ansible k3s bootstrap** (Phase 1, WS-INFRA), because a cluster needs a CNI
before Flux can run. This HelmRelease **manages Cilium configuration thereafter**. The Helm values
here MUST stay consistent with the Ansible bootstrap values (`kubeProxyReplacement`, `k8sServiceHost`,
`k8sServicePort`) — otherwise Flux and the bootstrap fight. **Coordinate this contract with WS-INFRA.**
## TODO (later phases — not Phase 0)
- **Phase 1 (WS-INFRA/WS-NET):** align Helm values with the Ansible bootstrap; pin chart version.
- **Phase 2 (WS-NET):** default-deny network baseline; Egress Gateway policies; L2 announcement IP
pool + `CiliumL2AnnouncementPolicy` for the ingress VIP; Hubble metrics → kube-prometheus-stack.
- **Phase 3 (WS-NET):** egress-class routing integration with the VPN gateway (ADR-0013).
+55
View File
@@ -0,0 +1,55 @@
---
# Cilium — CNI, kube-proxy replacement, Gateway API, Egress Gateway, Hubble (ADR-0002).
# Phase 0 STUB. Values are PLACEHOLDERS; fill in during Phase 1/2 (WS-NET).
#
# IMPORTANT (cross-workstream contract): Cilium's INITIAL install is performed by the Ansible
# bootstrap (spec docs/02, Phase 1), replacing k3s flannel/kube-proxy. This HelmRelease manages
# Cilium CONFIG going forward. Its values MUST match the Ansible bootstrap values (kubeProxyReplacement,
# k8sServiceHost/Port) or Flux and the bootstrap will fight. Owner: WS-NET + WS-INFRA.
apiVersion: helm.toolkit.fluxcd.io/v2
kind: HelmRelease
metadata:
name: cilium
namespace: kube-system
spec:
interval: 30m
chart:
spec:
chart: cilium
version: ">=1.16.0 <1.17.0" # TODO(WS-NET): pin exact version at bring-up; Renovate maintains
sourceRef:
kind: HelmRepository
name: cilium
namespace: kube-system
install:
remediation:
retries: 3
upgrade:
remediation:
retries: 3
# Placeholder values — see values.yaml for the annotated skeleton. Inlined here so the intent
# (Cilium REPLACES flannel + kube-proxy + Traefik) is visible at the release.
values:
# --- kube-proxy replacement (ADR-0001/0002: k3s kube-proxy disabled) ---
kubeProxyReplacement: true
# TODO(WS-INFRA): k8sServiceHost/k8sServicePort must equal the Ansible bootstrap's API endpoint.
k8sServiceHost: "REPLACE-ME" # e.g. the k3s API VIP/host
k8sServicePort: 6443
# --- Gateway API (ingress; replaces Traefik) — docs/09, ADR-0002 ---
gatewayAPI:
enabled: true
# --- Egress Gateway (egress-class steering; part of VPN model with ADR-0013) ---
egressGateway:
enabled: true
# --- Hubble (flow observability / isolation audit) ---
hubble:
enabled: true
relay:
enabled: true
ui:
enabled: true # exposure gated behind oauth2-proxy forward-auth later (ADR-0014)
# --- L2 announcement for the ingress VIP on the flat LAN (docs/09, OQ-1 resolved) ---
l2announcements:
enabled: true
# TODO(WS-NET, Phase 2): default-deny baseline, CiliumL2AnnouncementPolicy + IP pool,
# Egress Gateway policies, Hubble metrics export to kube-prometheus-stack.
+9
View File
@@ -0,0 +1,9 @@
---
apiVersion: source.toolkit.fluxcd.io/v1
kind: HelmRepository
metadata:
name: cilium
namespace: kube-system
spec:
interval: 1h
url: https://helm.cilium.io/
+8
View File
@@ -0,0 +1,8 @@
---
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
namespace: kube-system
resources:
- namespace.yaml
- helmrepository.yaml
- helmrelease.yaml
+6
View File
@@ -0,0 +1,6 @@
---
# Cilium runs in kube-system (it is the CNI). Declared for completeness; k3s provides it.
apiVersion: v1
kind: Namespace
metadata:
name: kube-system
+13
View File
@@ -0,0 +1,13 @@
# Cilium Helm values — PLACEHOLDER skeleton (Phase 0). Authoritative values live inline in
# helmrelease.yaml for now; this file documents the knobs WS-NET must set in Phase 1/2.
#
# MUST match the Ansible bootstrap (docs/02) for the fields the bootstrap also sets.
#
# kubeProxyReplacement: true # k3s kube-proxy disabled (ADR-0001/0002)
# k8sServiceHost / k8sServicePort # = Ansible bootstrap API endpoint (WS-INFRA contract)
# gatewayAPI.enabled: true # ingress via Gateway API, no Traefik (docs/09)
# egressGateway.enabled: true # egress-class steering (ADR-0002, VPN model ADR-0013)
# hubble.{enabled,relay,ui} # flow observability + isolation audit
# l2announcements.enabled: true # ingress VIP on flat LAN (docs/09, OQ-1)
# ipam / ipv4NativeRoutingCIDR # TODO: pin to the cluster pod/LAN CIDRs at bring-up
# encryption (wireguard/ipsec) # TODO(WS-SEC/WS-NET): decide east-west encryption later
+32
View File
@@ -0,0 +1,32 @@
# platform/externaldns
**ExternalDNS** — automatic public DNS records for `public` services (Cloudflare).
- **Spec:** [docs/09 — DNS & Ingress](../../../home-network-k3s/docs/09-dns-and-ingress.md)
(ExternalDNS is named in [docs/01](../../../home-network-k3s/docs/01-architecture-overview.md); no
dedicated ADR)
- **Serves:** R-DNS-01/02/03
- **Owner:** WS-NET (token from WS-SEC)
## What this stub is
Flux `HelmRelease` watching Gateway/HTTPRoute + Service resources, publishing
`<service>.home.keithjackson.dev` records in Cloudflare from declared intent (no manual record
editing, R-DNS-02).
## Split-horizon note (docs/09)
A wildcard `*.keithjackson.dev` matches only one label deep, so `<svc>.home.keithjackson.dev` needs
its own record. **`internal` services need NO public record** — PiHole resolves them to the internal
Gateway VIP (split-horizon). ExternalDNS manages **`public`** services' records only.
## Secrets
Cloudflare API token referenced **by name** (`cloudflare-api-token`, shared with cert-manager),
**SOPS-encrypted** (WS-SEC, OQ-4).
## TODO (later phases)
- **Phase 2 (WS-NET):** confirm `gateway-httproute` source; prove an auto record for a test route
(R-DNS-02, Phase-2 exit gate).
- **Phase 6 (WS-APPS):** PiHole provides the internal split-horizon answers as it onboards.
+45
View File
@@ -0,0 +1,45 @@
---
# ExternalDNS — publishes public DNS records for `public` services automatically (docs/09, R-DNS-02).
# Watches Gateway/HTTPRoute (+Service); manages records in Cloudflare. Phase 0 STUB.
apiVersion: helm.toolkit.fluxcd.io/v2
kind: HelmRelease
metadata:
name: external-dns
namespace: external-dns
spec:
interval: 30m
chart:
spec:
chart: external-dns
version: ">=1.15.0 <1.16.0" # TODO(WS-NET): pin at bring-up; Renovate maintains
sourceRef:
kind: HelmRepository
name: external-dns
namespace: external-dns
install:
remediation:
retries: 3
upgrade:
remediation:
retries: 3
values:
provider:
name: cloudflare
# Watch Gateway API routes (Cilium Gateway) + Services. docs/09: records rendered from intent.
sources:
- gateway-httproute
- service
# Only manage the platform zone. `internal` services need no public record (split-horizon via
# PiHole) — docs/09 "DNS record depth".
domainFilters:
- "home.keithjackson.dev"
policy: sync # create + update + delete records as services come and go
# Cloudflare API token: SOPS-encrypted secret, referenced BY NAME only (OQ-4, ADR-0007).
env:
- name: CF_API_TOKEN
valueFrom:
secretKeyRef:
name: cloudflare-api-token
key: api-token
# TODO(WS-NET, Phase 2): confirm gateway-httproute source wiring; prove a record appears
# automatically for a test route (R-DNS-02, Phase-2 exit gate).
+9
View File
@@ -0,0 +1,9 @@
---
apiVersion: source.toolkit.fluxcd.io/v1
kind: HelmRepository
metadata:
name: external-dns
namespace: external-dns
spec:
interval: 1h
url: https://kubernetes-sigs.github.io/external-dns/
+7
View File
@@ -0,0 +1,7 @@
---
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- namespace.yaml
- helmrepository.yaml
- helmrelease.yaml
+5
View File
@@ -0,0 +1,5 @@
---
apiVersion: v1
kind: Namespace
metadata:
name: external-dns
+8
View File
@@ -0,0 +1,8 @@
# ExternalDNS Helm values — PLACEHOLDER (Phase 0). Authoritative values inline in helmrelease.yaml.
#
# provider.name: cloudflare
# sources: [gateway-httproute, service] # Cilium Gateway API + Services (docs/09)
# domainFilters: [home.keithjackson.dev] # only the platform zone
# policy: sync # create/update/delete records with services
# Cloudflare token: SOPS-encrypted `cloudflare-api-token` (WS-SEC, OQ-4) — name only.
# NOTE: `internal` services need NO public record (PiHole split-horizon, docs/09).
+34
View File
@@ -0,0 +1,34 @@
# platform/gateway
**Gateway API** (north-south ingress) implemented by **Cilium** + the shared HTTPS Gateway and the
wildcard TLS certificate.
- **Governing ADRs:** [ADR-0002 — Cilium + Gateway API](../../../home-network-k3s/decisions/adr-0002-cni-cilium.md),
[ADR-0006 — cert-manager/Let's Encrypt](../../../home-network-k3s/decisions/adr-0006-tls-cert-manager-letsencrypt.md)
- **Spec:** [docs/09 — DNS & Ingress](../../../home-network-k3s/docs/09-dns-and-ingress.md)
- **Serves:** R-DNS-01, R-TLS-01
- **Owner:** WS-NET (+ WS-SEC for the cert)
## What this stub is
- **`GatewayClass` `cilium`** — Gateway API implemented by Cilium; no separate ingress controller
(ADR-0002).
- **`Gateway` `shared`** — HTTPS listener on `*.home.keithjackson.dev`, TLS terminated with the
wildcard cert. App HTTPRoutes attach cross-namespace via a namespace label + ReferenceGrant
(rendered by WS-APPS from each ServiceDefinition's `exposure`).
- **`Certificate` `wildcard-home`** — `*.home.keithjackson.dev` issued by `${ACME_CLUSTER_ISSUER}`
(staging in dev, prod in prod — R-ENV-02). Makes onboarding certificate-free (ADR-0006 default).
## Contract (Issuer + Gateway TLS)
The "Issuer + Gateway TLS" interface (`roadmap/workstreams.md`): this Gateway + wildcard cert are what
every service's HTTPRoute attaches to. ExternalDNS ([`../externaldns`](../externaldns)) publishes the
`public` records for hostnames routed here. Ingress opens **north-south** only — it does **not** relax
east-west default-deny (docs/09).
## TODO (later phases)
- **Phase 2 (WS-NET):** pin the L2-announced ingress VIP / IP pool (Cilium L2 announcement, docs/09);
add the HTTP→HTTPS redirect route; confirm the cross-namespace ReferenceGrant pattern with WS-APPS.
- **Public exposure** is a **manual edge port-forward** of 80/443 to the Gateway VIP (single SOHO
router, docs/09) — a documented manual step, the analogue of the old pfSense exception.
+42
View File
@@ -0,0 +1,42 @@
---
# Shared Gateway — HTTPS listeners on *.home.keithjackson.dev, TLS terminated with the wildcard cert
# (docs/09). Implemented by Cilium (GatewayClass `cilium`). The ingress VIP is announced via Cilium
# L2 announcement on the flat LAN (docs/09, OQ-1 resolved). Phase 0 STUB.
apiVersion: gateway.networking.k8s.io/v1
kind: Gateway
metadata:
name: shared
namespace: gateway
spec:
gatewayClassName: cilium
listeners:
- name: https
protocol: HTTPS
port: 443
hostname: "*.home.keithjackson.dev"
tls:
mode: Terminate
certificateRefs:
- kind: Secret
name: wildcard-home-tls # produced by wildcard-certificate.yaml
allowedRoutes:
namespaces:
from: Selector
selector:
matchLabels:
# App namespaces opt in to attach HTTPRoutes here (WS-APPS renders the label +
# ReferenceGrant from ServiceDefinition `exposure`). docs/09.
gateway-access: "shared"
- name: http-redirect
protocol: HTTP
port: 80
hostname: "*.home.keithjackson.dev"
allowedRoutes:
namespaces:
from: Selector
selector:
matchLabels:
gateway-access: "shared"
# TODO(WS-NET, Phase 2): pin the L2-announced VIP / IP pool; add HTTP->HTTPS redirect HTTPRoute;
# confirm cross-namespace ReferenceGrant pattern with WS-APPS. Ingress opens NORTH-SOUTH only —
# it does not relax east-west default-deny (docs/09).
+9
View File
@@ -0,0 +1,9 @@
---
# GatewayClass implemented by Cilium (ADR-0002: Gateway API via Cilium, no separate ingress
# controller). Phase 0 STUB. The controllerName is provided by the Cilium Gateway API integration.
apiVersion: gateway.networking.k8s.io/v1
kind: GatewayClass
metadata:
name: cilium
spec:
controllerName: io.cilium/gateway-controller
+8
View File
@@ -0,0 +1,8 @@
---
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- namespace.yaml
- gatewayclass.yaml
- wildcard-certificate.yaml
- gateway.yaml
+7
View File
@@ -0,0 +1,7 @@
---
# Shared Gateway lives in its own namespace; HTTPRoutes from app namespaces attach cross-namespace
# via ReferenceGrant (rendered by WS-APPS from ServiceDefinitions).
apiVersion: v1
kind: Namespace
metadata:
name: gateway
@@ -0,0 +1,18 @@
---
# Wildcard certificate `*.home.keithjackson.dev`, terminated at the shared Gateway (ADR-0006 default).
# Makes onboarding certificate-free. Issued by the env-selected ClusterIssuer (R-ENV-02).
# Phase 0 STUB — cannot issue without a cluster + Cloudflare token.
apiVersion: cert-manager.io/v1
kind: Certificate
metadata:
name: wildcard-home
namespace: gateway
spec:
secretName: wildcard-home-tls # consumed by the Gateway HTTPS listener below
dnsNames:
- "*.home.keithjackson.dev"
- "home.keithjackson.dev" # apex for the C2 UI (docs/09)
issuerRef:
kind: ClusterIssuer
# ${ACME_CLUSTER_ISSUER}: dev -> letsencrypt-staging, prod -> letsencrypt-prod (cluster-vars).
name: ${ACME_CLUSTER_ISSUER}
+29
View File
@@ -0,0 +1,29 @@
# platform/image-automation
**Flux image-update-automation** — hands-off container image tag advances driven by declared policy.
- **Governing ADR:** [ADR-0009 — Automated updates: Flux image automation + Renovate](../../../home-network-k3s/decisions/adr-0009-automated-image-updates.md)
- **Spec:** [docs/11 — Automated Updates](../../../home-network-k3s/docs/11-automated-updates.md),
[docs/05 §Automated updates interplay](../../../home-network-k3s/docs/05-gitops-and-delivery.md)
- **Serves:** R-UPD-02 (with R-UPD-01/03 safety rails)
- **Owner:** WS-UPDATE (mechanism), WS-GITOPS (policy rendering from ServiceDefinition)
## What this stub is
The **cluster-wide** `ImageUpdateAutomation` that scans registries and **commits the resolved version
back to git** (machine-authored, auditable — ADR-0009). No human or AI authors a tag bump (R-UPD-02).
## Scope boundary (contract)
This dir holds **only** the cluster-wide automation + git write-back settings. **Per-service**
`ImageRepository` / `ImagePolicy` (semver range / stable channel) are rendered from each
ServiceDefinition's `update` block by WS-GITOPS/WS-UPDATE and live with `apps/` — the "Update policy
contract" in `roadmap/workstreams.md`. **Helm chart / manifest bumps** are Renovate's job (separate),
not this controller's.
## TODO (later phases — Phase 4)
- Confirm write-back strategy (direct commit to tracked branch vs. Renovate-style PRs).
- Wire the safety rails that are **acceptance gates** (ADR-0009): stable-only, health-gated rollout,
auto-rollback on failed health, `pin` escape hatch, alert on update failure.
- Requires the Flux **image-reflector** + **image-automation** controllers (Flux bootstrap component).
@@ -0,0 +1,39 @@
---
# Flux image-update-automation — the CLUSTER-WIDE write-back engine for hands-off image tag advances
# (ADR-0009, R-UPD-02). It scans registries per each service's ImagePolicy and commits the resolved
# version back to git — no human/AI authors the bump. Phase 0 STUB.
#
# SCOPE: this dir holds ONLY the cluster-wide ImageUpdateAutomation + git write-back settings.
# Per-service `ImageRepository`/`ImagePolicy` are rendered from each ServiceDefinition's `update`
# block (WS-GITOPS/WS-UPDATE) and live with apps/ — NOT here. (Contract: roadmap/workstreams.md
# "Update policy contract".)
apiVersion: image.toolkit.fluxcd.io/v1beta2
kind: ImageUpdateAutomation
metadata:
name: flux-image-automation
namespace: flux-system
spec:
interval: 30m # the "schedule" on which stable versions are picked up (R-UPD-02)
sourceRef:
kind: GitRepository
name: flux-system
git:
checkout:
ref:
branch: main # TODO(WS-UPDATE): confirm write-back branch (may open PRs instead)
commit:
author:
name: fluxcdbot # machine author — the auditable, intended write-back (ADR-0009)
email: [email protected]
messageTemplate: |
chore(images): automated update
Machine-authored image tag advance per ImagePolicy (R-UPD-02, ADR-0009).
push:
branch: main
update:
strategy: Setters # image-automation setter markers in per-service manifests (apps/)
path: ./apps
# TODO(WS-UPDATE, Phase 4): safety rails are acceptance gates (ADR-0009) — stable-only, health-gated
# rollout, auto-rollback on failed health, pin escape hatch, alert on update failure. Prove a test
# service auto-advances with no human/AI edit; a bad update auto-rolls-back + alerts (R-UPD-01/02/03).
@@ -0,0 +1,5 @@
---
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- image-update-automation.yaml
+30
View File
@@ -0,0 +1,30 @@
# platform/keycloak
**Keycloak** — the platform Identity Provider (SSO via OIDC + SAML).
- **Governing ADR:** [ADR-0014 — IdP: Keycloak](../../../home-network-k3s/decisions/adr-0014-idp-keycloak.md)
- **Spec:** [docs/15 — Identity & Access](../../../home-network-k3s/docs/15-identity-and-access.md)
- **Serves:** R-IDP-01..06
- **Owner:** WS-SEC
## What this stub is
Flux `HelmRelease` for the Keycloak instance with **PostgreSQL on Longhorn** (DR-critical). Replica
count is an overlay knob (`${KEYCLOAK_REPLICAS}`, R-ENV-02). Admin/DB credentials referenced **by
name**, **SOPS-encrypted** (WS-SEC). Realms/clients/groups are config-as-code (Phase 6).
> The ADR allows the **Keycloak Operator OR Helm** for the instance; this stub uses Helm — swap to
> the Operator if WS-SEC prefers CR-driven config.
## Contract (SSO/auth)
The "SSO/auth contract" (`roadmap/workstreams.md`, WS-SEC): a ServiceDefinition's `auth` block maps to
a Keycloak client (native OIDC) or oauth2-proxy protection (forward-auth — see
[`../oauth2-proxy`](../oauth2-proxy)). Consumed by WS-APPS/WS-C2.
## TODO (later phases — onboarded EARLY in Phase 6)
- Realms/clients/groups as code (keycloak-config-cli / realm-import); MFA for privileged groups.
- **DR-critical:** off-cluster DB backup (velero/) + documented **break-glass** local-admin path.
- Exceptions that keep native auth (do not force SSO): Plex/Seerr (Plex auth), Minecraft
(Mojang/Microsoft), UniFi (native) — ADR-0014.
+40
View File
@@ -0,0 +1,40 @@
---
# Keycloak — platform IdP (OIDC + SAML), config-as-code, PostgreSQL on Longhorn (ADR-0014).
# Phase 0 STUB. DR-CRITICAL: DB backed up off-cluster; documented break-glass local-admin path.
apiVersion: helm.toolkit.fluxcd.io/v2
kind: HelmRelease
metadata:
name: keycloak
namespace: keycloak
spec:
interval: 30m
chart:
spec:
chart: keycloak
version: ">=24.0.0 <25.0.0" # TODO(WS-SEC): pin at bring-up; Renovate maintains
sourceRef:
kind: HelmRepository
name: bitnami
namespace: keycloak
install:
remediation:
retries: 3
upgrade:
remediation:
retries: 3
values:
replicaCount: ${KEYCLOAK_REPLICAS} # overlay knob: dev 1 / prod 2 (HA) — R-ENV-02
production: true # TLS/proxy-aware; served behind the Gateway (public)
proxyHeaders: xforwarded
# PostgreSQL on Longhorn (ADR-0014) — DR-critical, backed up off-cluster (velero/).
postgresql:
enabled: true
primary:
persistence:
storageClass: longhorn
# Admin + DB credentials: SOPS-encrypted secrets referenced BY NAME only (WS-SEC, ADR-0007).
auth:
existingSecret: keycloak-admin-credentials
# TODO(WS-SEC, Phase 6): realms/clients/groups as code (keycloak-config-cli / realm-import);
# MFA for privileged groups; break-glass local-admin path (e.g. on Gitea). Onboarded EARLY
# (with PiHole/Gitea) so later services adopt SSO as they come up (roadmap Phase 6).
+11
View File
@@ -0,0 +1,11 @@
---
# TODO(WS-SEC): the ADR permits Keycloak Operator OR Helm for the instance. This stub uses a Helm
# chart source; swap to the Keycloak Operator if WS-SEC prefers CR-driven config. (ADR-0014)
apiVersion: source.toolkit.fluxcd.io/v1
kind: HelmRepository
metadata:
name: bitnami
namespace: keycloak
spec:
interval: 1h
url: https://charts.bitnami.com/bitnami
+7
View File
@@ -0,0 +1,7 @@
---
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- namespace.yaml
- helmrepository.yaml
- helmrelease.yaml
+5
View File
@@ -0,0 +1,5 @@
---
apiVersion: v1
kind: Namespace
metadata:
name: keycloak
+8
View File
@@ -0,0 +1,8 @@
# Keycloak Helm values — PLACEHOLDER (Phase 0). Authoritative values inline in helmrelease.yaml.
#
# replicaCount: ${KEYCLOAK_REPLICAS} # overlay knob (dev 1 / prod 2 HA)
# postgresql on longhorn # DR-critical (ADR-0014), backed up off-cluster
# auth.existingSecret: keycloak-admin-credentials (SOPS-encrypted, WS-SEC — name only)
# realm/clients/groups as code # keycloak-config-cli / realm-import (Phase 6)
# public (hardened) exposure so login flows work for internet-exposed services (ADR-0014).
# Exceptions (do NOT force SSO): Plex/Seerr (Plex auth), Minecraft (Mojang), UniFi (native).
+31
View File
@@ -0,0 +1,31 @@
# platform/kubevirt
**KubeVirt + CDI** — run application VMs (IaaS) as Kubernetes resources on the same control plane,
network, storage, and GitOps flow as containers.
- **Governing ADR:** [ADR-0005 — VM workloads: KubeVirt + CDI](../../../home-network-k3s/decisions/adr-0005-vm-workloads-kubevirt.md)
- **Spec:** [docs/06 — VM Workloads (KubeVirt)](../../../home-network-k3s/docs/06-vm-workloads-kubevirt.md)
- **Serves:** R-PLAT-03/04, R-STATE-01
- **Owner:** WS-VM
## What this stub is
A Kustomization that will pull the pinned **KubeVirt operator** + **CDI operator** release manifests
(commented TODO — added in Phase 5), plus the **`KubeVirt`** and **`CDI`** custom resources that tune
them. VM disks land on **Longhorn** PVCs via CDI; VMs inherit Cilium default-deny + egress classes.
> **Layer distinction (docs/01):** these are **Layer-2 application VMs** (a feature the platform
> offers), NOT the Layer-1 cluster-node VMs (which WS-INFRA provisions with OpenTofu/libvirt). Do not
> conflate them.
## Cross-workstream prerequisite
Nodes must expose **virtualization extensions** (nested virt in dev KVMs; native on prod) — a node
baseline prerequisite owned by **WS-INFRA** (docs/02). Coordinate before Phase 5.
## TODO (later phases — Phase 5, gated on Phases 2–3)
- Add pinned operator release manifests as Kustomize bases; let Renovate maintain versions.
- Run one representative application VM via a ServiceDefinition `kind: vm`; prove create/start/stop/
destroy via `kubectl`/`virtctl`, Longhorn-backed disk survives rebuild, default-deny + egress apply,
guest auto-updates (Phase-5 exit gates).
+11
View File
@@ -0,0 +1,11 @@
---
# CDI CR — Containerized Data Importer, provisions VM disks onto Longhorn PVCs (ADR-0005).
# Phase 0 STUB; requires the CDI operator (added in Phase 5).
apiVersion: cdi.kubevirt.io/v1beta1
kind: CDI
metadata:
name: cdi
namespace: cdi
spec:
# TODO(WS-VM, Phase 5): scratch space storageClass (longhorn), upload/import proxy settings.
config: {}
+16
View File
@@ -0,0 +1,16 @@
---
# KubeVirt CR — tunes the KubeVirt install (ADR-0005). Phase 0 STUB; requires the KubeVirt operator
# (added in Phase 5) and virtualization extensions on the nodes (nested virt in dev KVMs — a node
# baseline prerequisite from docs/02, owned by WS-INFRA).
apiVersion: kubevirt.io/v1
kind: KubeVirt
metadata:
name: kubevirt
namespace: kubevirt
spec:
configuration:
developerConfiguration:
# TODO(WS-VM/WS-INFRA, Phase 5): enable only if hardware virt is unavailable in dev KVMs;
# prefer real (nested) virt so the IaaS path mirrors prod.
featureGates: []
# TODO(WS-VM, Phase 5): workload placement, live-migration, evictionStrategy for the 3-node cluster.
+13
View File
@@ -0,0 +1,13 @@
---
# KubeVirt + CDI. Installed via their OPERATORS (plain release manifests), then the KubeVirt/CDI
# custom resources tune the deployment. Phase 0 STUB.
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- namespaces.yaml
# TODO(WS-VM, Phase 5): add the pinned operator release manifests, e.g.
# - https://github.com/kubevirt/kubevirt/releases/download/vX.Y.Z/kubevirt-operator.yaml
# - https://github.com/kubevirt/containerized-data-importer/releases/download/vX.Y.Z/cdi-operator.yaml
# (Flux can pull remote bases; pin versions and let Renovate maintain them.)
- kubevirt-cr.yaml
- cdi-cr.yaml
+10
View File
@@ -0,0 +1,10 @@
---
apiVersion: v1
kind: Namespace
metadata:
name: kubevirt
---
apiVersion: v1
kind: Namespace
metadata:
name: cdi
+25
View File
@@ -0,0 +1,25 @@
---
# Top-level platform kustomization — the entrypoint the Flux `platform` Kustomization renders
# (clusters/base/platform.yaml, path: ./platform).
#
# Phase 0 scaffolding: lists every platform component EXCEPT kyverno (owned by the policy engineer,
# WS-TEST — see platform/README.md).
#
# ⚠️ INTEGRATION TODO(PM): once platform/kyverno/ is authored by the policy engineer, uncomment the
# `- kyverno` line below. Keep it FIRST so admission policies are present before other components
# reconcile. This workstream must NOT create files under platform/kyverno/.
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
# - kyverno # TODO(PM): enable when the policy engineer's kyverno stub lands (WS-TEST)
- cilium
- longhorn
- cert-manager
- externaldns
- gateway
- monitoring
- velero
- kubevirt
- image-automation
- keycloak
- oauth2-proxy
+31
View File
@@ -0,0 +1,31 @@
# platform/longhorn
**Longhorn** — distributed replicated block storage across the 3 nodes.
- **Governing ADR:** [ADR-0003 — Distributed storage: Longhorn (+ bulk tier + Velero)](../../../home-network-k3s/decisions/adr-0003-storage-longhorn.md)
- **Spec:** [docs/04 — Storage & State](../../../home-network-k3s/docs/04-storage-and-state.md)
- **Serves:** R-STATE-01..04
- **Owner:** WS-STORAGE
## What this stub is
Flux `HelmRelease` + two `StorageClass`es:
- **`longhorn`** — the **default**, **3-replica** replicated class (replica count is the
`${LONGHORN_DEFAULT_REPLICA_COUNT}` overlay knob). Durable home for app/config state and KubeVirt
VM disks (via CDI).
- **`bulk`** — non-replicated tier for large media (Plex library, download staging). **STUB**: its
backing store is **OQ-2 (unresolved)** — escalate before finalizing. Plex's metadata DB stays on
the 3-replica class regardless (ADR-0003).
## Contract (consumed by everyone with state)
Per `roadmap/workstreams.md`, WS-STORAGE exposes the `longhorn` (replicated) and `bulk` (media)
StorageClasses; WS-APPS/WS-VM/WS-OBS reference them from ServiceDefinitions. Do not place bulk media
on the replicated tier or app DBs on the bulk tier.
## TODO (later phases)
- **OQ-2 (human):** decide the bulk-tier backing (NAS/NFS vs dedicated disk). Blocks `bulk` finalize.
- **Phase 2 (WS-STORAGE):** dedicated Longhorn data disk path (docs/02); prove a PVC survives
pod destroy/rebuild (R-STATE-01, Phase-2 exit gate).
- **Phase 4 (WS-STORAGE):** wire off-cluster backup target with Velero (R-STATE-03).
+36
View File
@@ -0,0 +1,36 @@
---
# Longhorn — distributed replicated block storage (ADR-0003). Phase 0 STUB.
apiVersion: helm.toolkit.fluxcd.io/v2
kind: HelmRelease
metadata:
name: longhorn
namespace: longhorn-system
spec:
interval: 30m
chart:
spec:
chart: longhorn
version: ">=1.7.0 <1.8.0" # TODO(WS-STORAGE): pin at bring-up; Renovate maintains
sourceRef:
kind: HelmRepository
name: longhorn
namespace: longhorn-system
install:
createNamespace: false
remediation:
retries: 3
upgrade:
remediation:
retries: 3
values:
# We define StorageClasses ourselves (storageclasses.yaml) so the default 3-replica SC and the
# bulk tier are explicit and GitOps-managed. Disable the chart's built-in default SC.
persistence:
defaultClass: false
# ${LONGHORN_DEFAULT_REPLICA_COUNT} resolved from cluster-vars (R-ENV-02).
defaultClassReplicaCount: ${LONGHORN_DEFAULT_REPLICA_COUNT}
defaultSettings:
# TODO(WS-STORAGE, Phase 2): set defaultDataPath to the dedicated Longhorn data disk
# (docs/02 — a dedicated data disk is presented to Longhorn on each node).
defaultDataPath: "/var/lib/longhorn"
# TODO(WS-STORAGE, Phase 4): configure off-cluster backup target here (see velero/ + ADR-0003).
+9
View File
@@ -0,0 +1,9 @@
---
apiVersion: source.toolkit.fluxcd.io/v1
kind: HelmRepository
metadata:
name: longhorn
namespace: longhorn-system
spec:
interval: 1h
url: https://charts.longhorn.io
+8
View File
@@ -0,0 +1,8 @@
---
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- namespace.yaml
- helmrepository.yaml
- helmrelease.yaml
- storageclasses.yaml
+5
View File
@@ -0,0 +1,5 @@
---
apiVersion: v1
kind: Namespace
metadata:
name: longhorn-system
+34
View File
@@ -0,0 +1,34 @@
---
# Default replicated StorageClass — 3 replicas (ADR-0003). The durable home for app/config state
# and KubeVirt VM disks. Replica count is an overlay knob (R-ENV-02).
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
name: longhorn
annotations:
storageclass.kubernetes.io/is-default-class: "true"
provisioner: driver.longhorn.io
allowVolumeExpansion: true
reclaimPolicy: Delete
volumeBindingMode: Immediate
parameters:
numberOfReplicas: "${LONGHORN_DEFAULT_REPLICA_COUNT}" # from cluster-vars (default 3)
staleReplicaTimeout: "30"
fsType: "ext4"
---
# Bulk media tier — large, NON-replicated data (Plex library, download staging). ADR-0003 / docs/04.
# NOTE: backing store (NAS/NFS vs dedicated large disk) is OQ-2, unresolved — this is a STUB.
# Plex's metadata DB stays on the 3-replica `longhorn` class regardless (ADR-0003).
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
name: bulk
provisioner: driver.longhorn.io # TODO(WS-STORAGE): may become an NFS provisioner per OQ-2
allowVolumeExpansion: true
reclaimPolicy: Retain
volumeBindingMode: Immediate
parameters:
numberOfReplicas: "1" # non-replicated bulk tier (capacity, not durability)
staleReplicaTimeout: "30"
fsType: "ext4"
# TODO(WS-STORAGE): finalize once OQ-2 (bulk backing) is answered by the human operator.
+7
View File
@@ -0,0 +1,7 @@
# Longhorn Helm values — PLACEHOLDER (Phase 0). Authoritative values inline in helmrelease.yaml.
#
# persistence.defaultClass: false # we own StorageClasses (storageclasses.yaml)
# persistence.defaultClassReplicaCount: ${LONGHORN_DEFAULT_REPLICA_COUNT} # overlay knob
# defaultSettings.defaultDataPath: <data disk> # TODO(WS-STORAGE): dedicated Longhorn disk (docs/02)
# defaultSettings.backupTarget: <off-cluster> # TODO(WS-STORAGE, Phase 4): DR target (ADR-0003)
# Longhorn UI exposure gated behind oauth2-proxy forward-auth (ADR-0014).
+31
View File
@@ -0,0 +1,31 @@
# platform/monitoring
**Observability stack** — kube-prometheus-stack (Prometheus + Alertmanager + Grafana) + **Loki** +
**Grafana Alloy**.
- **Spec:** [docs/10 — Observability & Alerting](../../../home-network-k3s/docs/10-observability-and-alerting.md)
(named in [docs/01](../../../home-network-k3s/docs/01-architecture-overview.md); no dedicated ADR)
- **Serves:** R-OBS-01..04
- **Owner:** WS-OBS
## What this stub is
Three Flux `HelmRelease`s (kube-prometheus-stack, loki, alloy) with placeholder values. Retention and
PVC sizes are overlay knobs (`${MONITORING_*}`, R-ENV-02): short/small in dev, long/large in prod.
Persistent data uses the replicated `longhorn` StorageClass.
## Contract (observability)
The "observability contract" (`roadmap/workstreams.md`): how a service declares metrics/SLIs/alerts
and gets dashboards. ServiceMonitors/dashboards are rendered from ServiceDefinitions by WS-APPS/WS-VM
against this stack. Grafana is part of the C2 UI composition (docs/12), behind Keycloak SSO
(ADR-0014).
## TODO (later phases)
- **Phase 4 (WS-OBS):** Alertmanager → Gmail SMTP receiver (creds SOPS-encrypted, name only);
Loki retention via compactor (`retention_enabled: true` — a period alone does nothing); Alloy log
scrape config → Loki; platform-health alerts. **Reconciliation errors + drift are alerting
conditions (R-OBS-04).**
- **Exit gate (Phase 4):** an injected fault emails an alert; dashboards render (R-OBS-01/02/03/04).
- **Grafana OIDC** client via Keycloak (WS-SEC/WS-C2).
+25
View File
@@ -0,0 +1,25 @@
---
# Grafana Alloy — telemetry collector / log shipping to Loki (docs/10). Phase 0 STUB.
apiVersion: helm.toolkit.fluxcd.io/v2
kind: HelmRelease
metadata:
name: alloy
namespace: monitoring
spec:
interval: 30m
chart:
spec:
chart: alloy
version: ">=0.10.0 <1.0.0" # TODO(WS-OBS): pin at bring-up; Renovate maintains
sourceRef:
kind: HelmRepository
name: grafana
namespace: monitoring
install:
remediation:
retries: 3
upgrade:
remediation:
retries: 3
values: {}
# TODO(WS-OBS, Phase 4): Alloy config to scrape pod/node logs -> Loki; K8s events; metrics relay.
+18
View File
@@ -0,0 +1,18 @@
---
apiVersion: source.toolkit.fluxcd.io/v1
kind: HelmRepository
metadata:
name: prometheus-community
namespace: monitoring
spec:
interval: 1h
url: https://prometheus-community.github.io/helm-charts
---
apiVersion: source.toolkit.fluxcd.io/v1
kind: HelmRepository
metadata:
name: grafana
namespace: monitoring
spec:
interval: 1h
url: https://grafana.github.io/helm-charts
@@ -0,0 +1,44 @@
---
# kube-prometheus-stack — Prometheus + Alertmanager + Grafana (docs/10). Phase 0 STUB.
# Retention/sizes are overlay knobs (R-ENV-02). Alertmanager emails via SMTP.
apiVersion: helm.toolkit.fluxcd.io/v2
kind: HelmRelease
metadata:
name: kube-prometheus-stack
namespace: monitoring
spec:
interval: 30m
chart:
spec:
chart: kube-prometheus-stack
version: ">=65.0.0 <66.0.0" # TODO(WS-OBS): pin at bring-up; Renovate maintains
sourceRef:
kind: HelmRepository
name: prometheus-community
namespace: monitoring
install:
remediation:
retries: 3
upgrade:
remediation:
retries: 3
values:
prometheus:
prometheusSpec:
retention: ${MONITORING_PROMETHEUS_RETENTION} # overlay knob (dev short / prod long)
storageSpec:
volumeClaimTemplate:
spec:
storageClassName: longhorn # replicated tier (ADR-0003)
resources:
requests:
storage: ${MONITORING_PROMETHEUS_PVC_SIZE}
grafana:
# Grafana behind Keycloak SSO (native OIDC) — ADR-0014. C2 UI composition (docs/12).
# TODO(WS-SEC/WS-C2): OIDC client + admin password from SOPS secret (name only).
enabled: true
alertmanager:
alertmanagerSpec: {}
# TODO(WS-OBS, Phase 4): Alertmanager → Gmail SMTP receiver; SMTP creds SOPS-encrypted (name
# only). Reconciliation errors + drift are alerting conditions (R-OBS-04). Prove: injected
# fault emails an alert (Phase-4 exit gate).
+9
View File
@@ -0,0 +1,9 @@
---
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- namespace.yaml
- helmrepositories.yaml
- kube-prometheus-stack.yaml
- loki.yaml
- alloy.yaml
+33
View File
@@ -0,0 +1,33 @@
---
# Loki — log aggregation (docs/10). Phase 0 STUB. PVC size is an overlay knob (R-ENV-02).
apiVersion: helm.toolkit.fluxcd.io/v2
kind: HelmRelease
metadata:
name: loki
namespace: monitoring
spec:
interval: 30m
chart:
spec:
chart: loki
version: ">=6.0.0 <7.0.0" # TODO(WS-OBS): pin at bring-up; Renovate maintains
sourceRef:
kind: HelmRepository
name: grafana
namespace: monitoring
install:
remediation:
retries: 3
upgrade:
remediation:
retries: 3
values:
# TODO(WS-OBS, Phase 4): single-binary vs scalable deployment mode; retention via compactor
# (retention_enabled: true — a period alone does nothing); storage backing.
loki:
storage:
type: filesystem
singleBinary:
persistence:
storageClass: longhorn
size: ${MONITORING_LOKI_PVC_SIZE}
+5
View File
@@ -0,0 +1,5 @@
---
apiVersion: v1
kind: Namespace
metadata:
name: monitoring
+28
View File
@@ -0,0 +1,28 @@
# platform/oauth2-proxy
**oauth2-proxy** — forward-auth at the Gateway for apps **without usable OIDC**, authenticating
against Keycloak.
- **Governing ADR:** [ADR-0014 — IdP: Keycloak](../../../home-network-k3s/decisions/adr-0014-idp-keycloak.md)
- **Spec:** [docs/15 — Identity & Access](../../../home-network-k3s/docs/15-identity-and-access.md)
- **Serves:** R-IDP-01..06 (esp. R-IDP-02: management UIs never unauthenticated)
- **Owner:** WS-SEC (Gateway wiring shared with WS-NET)
## What this stub is
Flux `HelmRelease` for oauth2-proxy as the forward-auth provider for non-OIDC apps (PiHole, *arr UIs,
qBittorrent, Homepage, Prometheus, Alertmanager, Hubble UI — ADR-0014). Replica count is an overlay
knob (`${OAUTH2_PROXY_REPLICAS}`, R-ENV-02). Client/cookie secrets referenced **by name**,
**SOPS-encrypted** (WS-SEC).
## Contract (SSO/auth + Gateway)
Pairs with [`../keycloak`](../keycloak) to realize the "SSO/auth contract": a ServiceDefinition `auth:
forward-auth` service is protected by oauth2-proxy at the Gateway (ext-auth); `auth: oidc` services go
native to Keycloak. Gateway ext-auth wiring is a **WS-NET** contract.
## TODO (later phases — Phase 6)
- OIDC issuer URL to the Keycloak realm; client registration.
- Gateway forward-auth / ext-authz wiring (WS-NET); group-based authorization; MFA enforced in
Keycloak upstream.
+38
View File
@@ -0,0 +1,38 @@
---
# oauth2-proxy — forward-auth at the Gateway for apps WITHOUT usable OIDC (PiHole, *arr UIs,
# qBittorrent, Homepage, Prometheus, Alertmanager, Hubble UI). Authenticates against Keycloak.
# ADR-0014. Phase 0 STUB. Replica count is an overlay knob (R-ENV-02).
apiVersion: helm.toolkit.fluxcd.io/v2
kind: HelmRelease
metadata:
name: oauth2-proxy
namespace: oauth2-proxy
spec:
interval: 30m
chart:
spec:
chart: oauth2-proxy
version: ">=7.0.0 <8.0.0" # TODO(WS-SEC): pin at bring-up; Renovate maintains
sourceRef:
kind: HelmRepository
name: oauth2-proxy
namespace: oauth2-proxy
install:
remediation:
retries: 3
upgrade:
remediation:
retries: 3
values:
replicaCount: ${OAUTH2_PROXY_REPLICAS} # overlay knob: dev 1 / prod 2 (HA) — R-ENV-02
config:
# OIDC provider = Keycloak. Client id/secret + cookie secret: SOPS-encrypted secret, name only.
existingSecret: oauth2-proxy-credentials
extraArgs:
provider: oidc
# TODO(WS-SEC, Phase 6): oidc-issuer-url = https://keycloak.home.keithjackson.dev/realms/<realm>
oidc-issuer-url: "REPLACE-ME"
email-domain: "*"
# Forward-auth integration wired at the Gateway (HTTPRoute + ext-auth). Contract with WS-NET.
# TODO(WS-SEC/WS-NET, Phase 6): Gateway forward-auth/ext-authz wiring; group-based authorization;
# MFA enforced upstream in Keycloak. Management UIs must never be unauthenticated (R-IDP-02).
@@ -0,0 +1,9 @@
---
apiVersion: source.toolkit.fluxcd.io/v1
kind: HelmRepository
metadata:
name: oauth2-proxy
namespace: oauth2-proxy
spec:
interval: 1h
url: https://oauth2-proxy.github.io/manifests
+7
View File
@@ -0,0 +1,7 @@
---
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- namespace.yaml
- helmrepository.yaml
- helmrelease.yaml
+5
View File
@@ -0,0 +1,5 @@
---
apiVersion: v1
kind: Namespace
metadata:
name: oauth2-proxy
+7
View File
@@ -0,0 +1,7 @@
# oauth2-proxy Helm values — PLACEHOLDER (Phase 0). Authoritative values inline in helmrelease.yaml.
#
# replicaCount: ${OAUTH2_PROXY_REPLICAS} # overlay knob (dev 1 / prod 2 HA)
# provider: oidc, oidc-issuer-url = Keycloak realm (Phase 6)
# config.existingSecret: oauth2-proxy-credentials (client id/secret + cookie secret; SOPS — name only)
# Forward-auth wired at the Gateway (ext-auth) for non-OIDC apps (ADR-0014) — contract with WS-NET.
# Protects: PiHole, *arr UIs, qBittorrent, Homepage, Prometheus, Alertmanager, Hubble UI.
+22
View File
@@ -0,0 +1,22 @@
# platform/velero
**Velero** — off-cluster backup and DR of volumes + Kubernetes resource state.
- **Governing ADR:** [ADR-0003 — Longhorn (+ bulk tier + Velero)](../../../home-network-k3s/decisions/adr-0003-storage-longhorn.md)
- **Spec:** [docs/04 — Storage & State](../../../home-network-k3s/docs/04-storage-and-state.md)
- **Serves:** R-STATE-03
- **Owner:** WS-STORAGE
## What this stub is
Flux `HelmRelease` with a placeholder off-cluster backup target (S3-compatible/NFS) and scheduled
backups whose cadence/retention are overlay knobs (`${VELERO_*}`, R-ENV-02). Backup credentials are
referenced **by name** (`velero-backup-credentials`), **SOPS-encrypted** (WS-SEC). Works with Longhorn
volume backups.
## TODO (later phases)
- **OQ-2 (human):** concrete off-cluster target (endpoint, bucket, resilience) is a prerequisite.
- **Phase 4 (WS-STORAGE):** wire the target + Longhorn backups; prove a **DR drill** — restore a
namespace + volumes from off-cluster backup (R-STATE-03, Phase-4 exit gate).
- **DR-critical services** (Gitea, Keycloak, Nextcloud) get off-cluster backup + mirror per docs/05.
+46
View File
@@ -0,0 +1,46 @@
---
# Velero — off-cluster backup/DR of volumes + resource state (ADR-0003, R-STATE-03). Phase 0 STUB.
apiVersion: helm.toolkit.fluxcd.io/v2
kind: HelmRelease
metadata:
name: velero
namespace: velero
spec:
interval: 30m
chart:
spec:
chart: velero
version: ">=8.0.0 <9.0.0" # TODO(WS-STORAGE): pin at bring-up; Renovate maintains
sourceRef:
kind: HelmRepository
name: vmware-tanzu
namespace: velero
install:
remediation:
retries: 3
upgrade:
remediation:
retries: 3
values:
# Off-cluster backup target — S3-compatible/NFS (ADR-0003). STUB: concrete target + creds are
# OQ-2 / prerequisites. Credentials referenced BY NAME only, SOPS-encrypted (WS-SEC).
configuration:
backupStorageLocation:
- name: default
provider: aws # TODO(WS-STORAGE): S3-compatible endpoint per OQ-2
bucket: REPLACE-ME
config:
region: REPLACE-ME
s3Url: REPLACE-ME
s3ForcePathStyle: "true"
volumeSnapshotLocation:
- name: default
provider: REPLACE-ME # Longhorn CSI snapshots / Velero fs-backup
credentials:
existingSecret: velero-backup-credentials # SOPS-encrypted (WS-SEC) — name only
schedules:
daily:
schedule: "${VELERO_BACKUP_SCHEDULE}" # overlay knob (cron)
template:
ttl: "${VELERO_RETENTION}" # overlay knob (dev short / prod long)
# TODO(WS-STORAGE, Phase 4): tested restore path (DR drill) is an acceptance gate (R-STATE-03).
+9
View File
@@ -0,0 +1,9 @@
---
apiVersion: source.toolkit.fluxcd.io/v1
kind: HelmRepository
metadata:
name: vmware-tanzu
namespace: velero
spec:
interval: 1h
url: https://vmware-tanzu.github.io/helm-charts
+7
View File
@@ -0,0 +1,7 @@
---
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- namespace.yaml
- helmrepository.yaml
- helmrelease.yaml
+5
View File
@@ -0,0 +1,5 @@
---
apiVersion: v1
kind: Namespace
metadata:
name: velero
+7
View File
@@ -0,0 +1,7 @@
# Velero Helm values — PLACEHOLDER (Phase 0). Authoritative values inline in helmrelease.yaml.
#
# backupStorageLocation: off-cluster S3-compatible/NFS (ADR-0003; concrete target = OQ-2)
# volumeSnapshotLocation: Longhorn CSI snapshots / Velero fs-backup
# credentials.existingSecret: velero-backup-credentials (SOPS-encrypted, WS-SEC — name only)
# schedules.daily: ${VELERO_BACKUP_SCHEDULE} / ttl ${VELERO_RETENTION} (overlay knobs)
# DR drill (tested restore) is a Phase-4 acceptance gate (R-STATE-03).