···11# AGENTS.md
2233NixOS + k3s cluster IaC for cute.haus. Single operator. Flake-parts
44-NixOS configs on bare-metal/VPS hosts; an in-tree helm chart per app on k3s;
55-SOPS+age secrets; Cloudflare/B2 via Terraform.
44+NixOS configs on bare-metal/VPS hosts; Flux-managed in-tree Helm charts on
55+k3s; SOPS+age secrets; Cloudflare/B2 via Terraform.
6677## Environment
88···5959 `flake.modules.nixos.<name>` (see `nix/nixos/base/default.nix`). Disk
6060 profiles in `nix/disko.nix`. k3s node config in `nix/nixos/profiles/k3s.nix`
6161 (flannel on `tailscale0`; startup blocks until tailscale0 has an IP).
6262-- `k8s/` — `helmfile.yaml` is the ordered release graph (uses `needs:`).
6363- `charts/<name>/` are in-tree charts with **explicit manifests, no shared
6464- helpers** (see `k8s/charts/README.md`). `values/global.yaml` holds shared
6565- image pins + constants; `values/secrets/<name>.yaml` holds `ref+sops://`
6666- vals refs into `secrets/`. Secret flow: `secrets/*.yaml` → vals → helmfile
6767- → chart `templates/secret.yaml` → app `envFrom`.
6262+- `k8s/` — `flux/` is the ordered release graph. `flux/system/layers.yaml`
6363+ defines the Flux Kustomization DAG; `flux/*/*.yaml` hold HelmReleases;
6464+ `flux/secrets/*.sops.yaml` are first-class Kubernetes Secrets decrypted by
6565+ Flux. `charts/<name>/` are in-tree charts with **explicit manifests, no
6666+ shared helpers** (see `k8s/charts/README.md`). `values/global.yaml` holds
6767+ shared non-secret image pins + constants, mirrored into the Flux
6868+ `cute-haus-global` ConfigMap.
6869- `secrets/` — SOPS, multi-recipient age. `.sops.yaml` is generated; never
6970 hand-edit either.
7071- `keys/` — `aly_<host>.pub` (user key) + `root_<host>.pub` (host key) age
···9293 in `k8s/values/global.yaml`. Renovate keeps digests fresh (skips
9394 `tranquil`, which is on private atcr.io — use `just bump-tranquil`).
9495- **forward-auth OIDC slugs** must be `forward-auth-<app>` and match a key in
9595- `k8s/values/secrets/forward-auth.yaml` (enforced by `check-forward-auth.ts`).
9696+ the plaintext `apps:` map in `k8s/flux/apps/releases.yaml` (enforced by
9797+ `check-forward-auth.ts`).
9698- k3s node labels: `topology.kubernetes.io/zone` (cloud/home),
9799 `cute.haus/ingress=true`, and `cute.haus/intel-gpu=true` (NixOS module).
98100 For GPU pods request the `gpu.intel.com/i915` resource, not a nodeSelector.
···11# Charts
2233-In-tree Helm charts deployed by [`../helmfile.yaml`](../helmfile.yaml).
33+In-tree Helm charts deployed by Flux HelmReleases under [`../flux`](../flux).
4455Most app charts use explicit Kubernetes manifests. Helm is used for light
66-substitution, mostly `.Chart.Name` and secret values passed by helmfile. Avoid
77-shared Deployment/Service/PVC helpers; app-specific behavior should stay visible
88-in the app chart.
66+substitution, mostly `.Chart.Name` and shared non-secret values passed by Flux.
77+Avoid shared Deployment/Service/PVC helpers; app-specific behavior should stay
88+visible in the app chart.
991010## Layout
1111···7979Keep these:
80808181- `.Chart.Name` for names, labels, selectors, and PVC names.
8282-- `if .Values.secret` around Secret manifests populated by vals/SOPS.
8382- Small `range` loops for charts that are naturally data-driven.
84838584Avoid these:
···130129## Secret Flow
131130132131```text
133133-secrets/foo.yaml SOPS-encrypted, multi-recipient age
132132+k8s/flux/secrets/foo.sops.yaml SOPS-encrypted Kubernetes Secret
134133 |
135134 v
136136-values/foo.yaml ref+sops://... refs read by vals
135135+Flux kustomize-controller decrypts with flux-system/sops-age
137136 |
138137 v
139139-helmfile values: passes plaintext values during render
138138+Kubernetes Secret first-class object in target namespace
140139 |
141140 v
142142-templates/secret.yaml renders a chart-local Secret
143143- |
144144- v
145145-deployment envFrom app consumes the Secret
141141+Deployment envFrom/secretRef app consumes the Secret
146142```
147143148148-`vals` resolves `ref+sops://` URLs at render time using the local age key.
144144+Flux decrypts only `k8s/flux/secrets/*.sops.yaml`; host/Terraform secrets under
145145+`secrets/*.yaml` remain user/host-key scoped.
149146150147## Adding An App
151148···154151 `deployment.yaml`, `service.yaml`, `ingress.yaml`, optional `pvc.yaml`, and
155152 optional `secret.yaml`.
1561533. Put app-specific behavior directly in those manifests.
157157-4. If the app needs secrets, create `secrets/<name>.yaml` and
158158- `values/<name>.yaml`, then add that values file to the helmfile release.
159159-5. Add the release to `../helmfile.yaml`.
154154+4. If the app needs secrets, create first-class SOPS Secret manifests under
155155+ `../flux/secrets/` and reference those Secret names from the chart.
156156+5. Add a HelmRelease under `../flux/apps/`.
1601576. Render before applying:
161158162159```bash
163160helm template <name> charts/<name>
164164-helmfile -l name=<name> apply
161161+flux reconcile kustomization apps -n flux-system
165162```
+1-3
k8s/charts/cluster-tls/values.yaml
···11-# Filled by values/cluster-tls.yaml via vals refs into secrets/cluster-tls.yaml.
22-# Each entry creates a kubernetes.io/tls Secret named <key>-tls.
33-secret: {}
11+# TLS Secrets are first-class SOPS manifests under k8s/flux/secrets.
···11-# Filled by values/secrets/longhorn-creds.yaml via vals + sops.
22-# Longhorn's helm release references the rendered Secret via
33-# defaultBackupStore.backupTargetCredentialSecret = longhorn-b2.
44-secret:
55- name: longhorn-b2
66- endpoint: https://s3.us-east-005.backblazeb2.com
77- virtualHostedStyle: "true"
88- keyId: ""
99- appKey: ""
11+# Longhorn's helm release references the first-class longhorn-b2 Secret via
22+# defaultBackupStore.backupTargetCredentialSecret.
103114# Daily backup of every volume in the implicit `default` group (i.e. all of
125# them). retain is per-volume; concurrency caps parallel jobs cluster-wide.
+3-3
k8s/charts/pg-shared/values.yaml
···3434 random_page_cost: "1.1"
3535 effective_io_concurrency: "200"
36363737-# Declarative CNPG roles + databases. Passwords come from
3838-# values/secrets/pg-shared.yaml via vals + sops.
3737+# Declarative CNPG roles + databases. Password Secrets are first-class SOPS
3838+# manifests under k8s/flux/secrets and are referenced by role name.
3939roles: {}
40404141# B2 backup target for CNPG base backups + WAL archiving.
4242-# Filled by values/secrets/pg-shared.yaml.
4242+# Credentials come from the first-class pg-shared-b2 Secret.
4343backup:
4444 enabled: false
4545 destinationPath: s3://aly-backups/cute.haus/cnpg
···11-apiVersion: v1
22-kind: ConfigMap
33-metadata:
44- name: flux-layer-external-routes-placeholder
55- namespace: flux-system
66- labels:
77- cute.haus/flux-layer-placeholder: "true"
88-data:
99- note: Remove this placeholder when external-routes has real resources.
-43
k8s/helmfile.yaml
···11-# Low history; CRD-heavy charts (cert-manager, traefik, cnpg) push each
22-# revision Secret to ~1MB, and the default of 10 stalls the apiserver.
33-helmDefaults:
44- historyMax: 3
55-66-repositories:
77- - name: grafana
88- url: https://grafana.github.io/helm-charts
99- - name: prometheus-community
1010- url: https://prometheus-community.github.io/helm-charts
1111- - name: traefik
1212- url: https://traefik.github.io/charts
1313- - name: longhorn
1414- url: https://charts.longhorn.io
1515- - name: cnpg
1616- url: https://cloudnative-pg.github.io/charts
1717- - name: tailscale
1818- url: https://pkgs.tailscale.com/helmcharts
1919- - name: jetstack
2020- url: https://charts.jetstack.io
2121- - name: nfd
2222- url: https://kubernetes-sigs.github.io/node-feature-discovery/charts
2323- - name: intel
2424- url: https://intel.github.io/helm-charts/
2525-2626-releases:
2727- # ─── Cluster infra ──────────────────────────────────────────────────────
2828- # Ingress, storage, postgres operator, tailscale operator. These come
2929- # before any app release that depends on their CRDs / StorageClasses.
3030-3131- # ─── Apps ───────────────────────────────────────────────────────────────
3232-3333- # Ingress + Service + EndpointSlice for services that live OUTSIDE the
3434- # cluster (on tailnet IPs). e.g. plex/immich on jubilife, audiobookshelf
3535- # on eterna. See charts/external-routes/values.yaml for the route list.
3636- - name: external-routes
3737- namespace: default
3838- chart: ./charts/external-routes
3939- needs:
4040- - default/cluster-tls
4141- - tailscale/tailscale-operator
4242- values:
4343- - values/global.yaml
+5-8
k8s/values/README.md
···11# 📋 values/
2233-Helmfile-provided value overrides.
33+Shared non-secret Helm values.
4455```
66values/
77-└── secrets/ # vals refs (ref+sops://) into ../../secrets/
77+└── global.yaml # image pins and operational constants mirrored into Flux
88```
991010-Each file under `secrets/` is plain yaml containing only `vals` refs that
1111-point into [`../../secrets/`](../../secrets/) for the actual encrypted values.
1212-helmfile passes these as `values:` to a release; `vals` resolves the refs
1313-at render time (SOPS-decrypts → splices into the values stream → helm
1414-renders).
1010+Secret values are first-class SOPS-encrypted Kubernetes Secret manifests under
1111+[`../flux/secrets`](../flux/secrets). Do not put secret-like values here.
15121613The chart's own `values.yaml` (under `charts/<name>/values.yaml`) holds
1717-the in-tree, non-secret defaults. This dir holds only the secret overlay.
1414+the in-tree, non-secret defaults. This dir holds only shared non-secret values.
18151916See [`../charts/README.md`](../charts/README.md) for the full secret flow.