GitOps-managed Kubernetes homelab: Talos Linux on four
Turing RK1 (arm64) nodes plus an x86 nas node, with Flux
reconciling everything in this repository into the cluster.
Nothing is applied by hand. Changes take effect by being merged into main;
Flux pulls it every minute.
| Area | Component |
|---|---|
| OS / nodes | Talos Linux, configured with topf |
| GitOps | Flux CD, secrets encrypted with sops + age |
| Networking | Cilium (kube-proxy replacement, BGP, L2 announcements, Gateway API) |
| Storage | Longhorn, 3 replicas, backups to S3 |
| Certificates | cert-manager |
| DNS | external-dns |
| Auth | Authelia + lldap (OIDC), gateway ExternalAuth for apps without OIDC |
| Policy | Kyverno (required resource requests) |
| Observability | kube-prometheus-stack, KRR for resource sizing |
| Scheduling | descheduler (rebalances pods by requests) |
| Path | Contents |
|---|---|
fluxcd/homelab/ |
One Flux Kustomization per unit; the dependency graph |
infrastructure/sources/ |
HelmRepository definitions |
infrastructure/base/<x>/ |
Infrastructure components (HelmReleases or manifests) |
infrastructure/configs/<x>/ |
CRs that need their component's CRDs first |
infrastructure/vars/ |
sops-encrypted cluster-secrets backing every ${VAR} |
apps/<name>/ |
One directory per application |
talos/ |
topf config (topf.yaml, patches, schematics) and mise tasks |
Tooling is pinned with mise:
mise install # repo-wide tools
cd talos && mise install # talosctl, topf, tpi, cilium-cliReproduce CI locally (no sops key needed):
.github/scripts/render.sh /tmp/rendered
.github/scripts/helm-template.sh
python3 .github/scripts/checks.py vars | secrets | disclosures | versionsmise //talos:upgrade [--dry-run] # Talos upgrade, node by node, waits for Longhorn health
mise //talos:upgrade-k8s [--dry-run] # Kubernetes upgrade via talosctl upgrade-k8s
mise //talos:schematic-ids # image factory schematic IDs
mise run install-cilium # bootstrap Cilium on a freshly reset clusterImmich logs in only through Authelia (LDAP group immich, admins from
cluster-admin). If SSO is broken, re-enable password login temporarily. The
settings come from the config file, so immich-admin enable-password-login
has no effect; flip it there and push, then give the admin a password (OIDC
users have none):
sops apps/immich/secrets.yaml # immich.json: "passwordLogin": {"enabled": true}
kubectl -n immich rollout restart deploy/immich-server
kubectl -n immich exec -it deploy/immich-server -c server -- immich-admin reset-admin-passwordSet it back to false once SSO works again.
Versions are set in talos/topf.yaml (talosVersion, kubernetesVersion);
bump them there, then run the matching task.
The private key lives in ~/.config/sops/age/keys.txt locally and in the
flux-system/sops-age Secret (key sops.agekey) in the cluster. The Secret is
created by hand, not from git. Flux decrypts with whatever is in that Secret,
so it must hold the new key before the re-encrypted files reach main,
and the old one until they do. An age identity file may hold several keys.
- Keep the old key as
~/.config/sops/age/keys.txt.old, generate the new one withage-keygen -o ~/.config/sops/age/keys.txt, put its public key in.sops.yaml,talos/.sops.yamlandfnox.toml, and re-encrypt every file withsops updatekeys -y <file>followed bysops rotate -i <file>(fresh data key). Commit on a branch, don't merge yet. - Give the cluster both keys (
replace, notapply, so nolast-applied-configurationannotation keeps a second copy of the keys):cat ~/.config/sops/age/keys.txt.old ~/.config/sops/age/keys.txt \ | kubectl -n flux-system create secret generic sops-age \ --from-file=sops.agekey=/dev/stdin --dry-run=client -o yaml \ | kubectl replace -f -
- Merge the branch, then
flux reconcile kustomization flux-system --with-sourceand wait untilflux get kustomizations -Ais all Ready on the new revision. HelmReleases fed by avaluesFromSecret need aflux reconcile helmrelease -n <ns> <name>. - Drop the old key from the cluster:
kubectl -n flux-system create secret generic sops-age \ --from-file=sops.agekey=$HOME/.config/sops/age/keys.txt \ --dry-run=client -o yaml \ | kubectl replace -f - - Check which public keys the cluster holds:
kubectl -n flux-system get secret sops-age -o jsonpath='{.data.sops\.agekey}' \ | base64 -d | age-keygen -y
Rotating the key doesn't protect values already in git history: anything the old key could decrypt has to be rotated at its source (API tokens, S3 keys, OIDC client secrets, …).
This repository is public. CI and a pre-push hook enforce that no secrets,
private IP addresses, MAC addresses or the cluster's domain are committed;
such values live in sops-encrypted files and are referenced as ${VAR}.
See CLAUDE.md for detailed conventions.