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:
1 parent
bd8de96456
commit
af4f6c07ae
75 files changed
+1638
-4
No files matched your search
+77
-2
@@ -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>
|
||||
@@ -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.
|
||||
@@ -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"
|
||||
@@ -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`
|
||||
@@ -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)
|
||||
@@ -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.
|
||||
@@ -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"
|
||||
@@ -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
@@ -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.
|
||||
@@ -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).
|
||||
@@ -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.
|
||||
@@ -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
|
||||
@@ -0,0 +1,8 @@
|
||||
---
|
||||
apiVersion: kustomize.config.k8s.io/v1beta1
|
||||
kind: Kustomization
|
||||
resources:
|
||||
- namespace.yaml
|
||||
- helmrepository.yaml
|
||||
- helmrelease.yaml
|
||||
- clusterissuers.yaml
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: Namespace
|
||||
metadata:
|
||||
name: cert-manager
|
||||
@@ -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.
|
||||
@@ -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).
|
||||
@@ -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.
|
||||
@@ -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/
|
||||
@@ -0,0 +1,8 @@
|
||||
---
|
||||
apiVersion: kustomize.config.k8s.io/v1beta1
|
||||
kind: Kustomization
|
||||
namespace: kube-system
|
||||
resources:
|
||||
- namespace.yaml
|
||||
- helmrepository.yaml
|
||||
- helmrelease.yaml
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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.
|
||||
@@ -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).
|
||||
@@ -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/
|
||||
@@ -0,0 +1,7 @@
|
||||
---
|
||||
apiVersion: kustomize.config.k8s.io/v1beta1
|
||||
kind: Kustomization
|
||||
resources:
|
||||
- namespace.yaml
|
||||
- helmrepository.yaml
|
||||
- helmrelease.yaml
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: Namespace
|
||||
metadata:
|
||||
name: external-dns
|
||||
@@ -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).
|
||||
@@ -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.
|
||||
@@ -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).
|
||||
@@ -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
|
||||
@@ -0,0 +1,8 @@
|
||||
---
|
||||
apiVersion: kustomize.config.k8s.io/v1beta1
|
||||
kind: Kustomization
|
||||
resources:
|
||||
- namespace.yaml
|
||||
- gatewayclass.yaml
|
||||
- wildcard-certificate.yaml
|
||||
- gateway.yaml
|
||||
@@ -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}
|
||||
@@ -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
|
||||
@@ -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.
|
||||
@@ -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).
|
||||
@@ -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
|
||||
@@ -0,0 +1,7 @@
|
||||
---
|
||||
apiVersion: kustomize.config.k8s.io/v1beta1
|
||||
kind: Kustomization
|
||||
resources:
|
||||
- namespace.yaml
|
||||
- helmrepository.yaml
|
||||
- helmrelease.yaml
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: Namespace
|
||||
metadata:
|
||||
name: keycloak
|
||||
@@ -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).
|
||||
@@ -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).
|
||||
@@ -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: {}
|
||||
@@ -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.
|
||||
@@ -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
|
||||
@@ -0,0 +1,10 @@
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: Namespace
|
||||
metadata:
|
||||
name: kubevirt
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: Namespace
|
||||
metadata:
|
||||
name: cdi
|
||||
@@ -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
|
||||
@@ -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).
|
||||
@@ -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).
|
||||
@@ -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
|
||||
@@ -0,0 +1,8 @@
|
||||
---
|
||||
apiVersion: kustomize.config.k8s.io/v1beta1
|
||||
kind: Kustomization
|
||||
resources:
|
||||
- namespace.yaml
|
||||
- helmrepository.yaml
|
||||
- helmrelease.yaml
|
||||
- storageclasses.yaml
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: Namespace
|
||||
metadata:
|
||||
name: longhorn-system
|
||||
@@ -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.
|
||||
@@ -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).
|
||||
@@ -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).
|
||||
@@ -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.
|
||||
@@ -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).
|
||||
@@ -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
|
||||
@@ -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}
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: Namespace
|
||||
metadata:
|
||||
name: monitoring
|
||||
@@ -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.
|
||||
@@ -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
|
||||
@@ -0,0 +1,7 @@
|
||||
---
|
||||
apiVersion: kustomize.config.k8s.io/v1beta1
|
||||
kind: Kustomization
|
||||
resources:
|
||||
- namespace.yaml
|
||||
- helmrepository.yaml
|
||||
- helmrelease.yaml
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: Namespace
|
||||
metadata:
|
||||
name: oauth2-proxy
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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).
|
||||
@@ -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
|
||||
@@ -0,0 +1,7 @@
|
||||
---
|
||||
apiVersion: kustomize.config.k8s.io/v1beta1
|
||||
kind: Kustomization
|
||||
resources:
|
||||
- namespace.yaml
|
||||
- helmrepository.yaml
|
||||
- helmrelease.yaml
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: Namespace
|
||||
metadata:
|
||||
name: velero
|
||||
@@ -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).
|
||||
Reference in new issue
Block a user