Imported from stephane-klein/homelab.sklein.xyz (
AGENTS.md). Install upstream withnpx skills add stephane-klein/homelab.sklein.xyz. Copyright stays with the author.
Agent Instructions
Language Policy
- All project content must be in English: source code, comments, commit messages, and documentation.
- Human conversations in OpenCode remain in French.
Safety Rules
- Never run any
destroy-*script orhelmfile destroycommand without explicit user confirmation in the same conversation turn. Always ask first. - If you must run
helmfile destroy, always use--selector name=<release>to target only one release. - When in doubt about a command's destructiveness, ask before executing.
Architecture Overview
This project provisions Fedora CoreOS bare-metal servers, runs a two-node k3s cluster across them, and manages the Netbird VPN mesh that connects everything.
Netbird management with OpenTofu
Netbird provides the WireGuard-based VPN mesh for all devices (servers, laptops, phones) in the homelab. The network configuration is managed declaratively with OpenTofu via the terraform-provider-netbird.
Key choices:
- OpenTofu over Terraform — fully open-source (Apache 2.0), community-driven under Linux Foundation governance.
- Local state backend —
terraform.tfstateat the project root (gitignored). - Provider version pinned —
~> 0.0.9inversions.tf. - Manual workflow —
tofu init && tofu applyis run manually.
Resource structure
homelab-serversgroup — the two NUC servers.user-devicesgroup — personal devices (fp5 phone, t14s laptop).- Policies enforce unidirectional access: user devices reach servers, servers cannot initiate connections to user devices.
- Netbird SSH proxy (
ssh_enabled,netbird-sshprotocol) avoids manual SSH key distribution — user identities are managed in Netbird. - Setup keys are created by
tofu applyand extracted viatofu output -raw.
k3s cluster
k3s is the single runtime platform for all services. It is
installed post-OS via the official get.k3s.io script, driven by
scripts/deploy-k3s.sh.
- nuc-i7-gen11 — control-plane (server), k3s single-server mode with the
default kine/SQLite datastore
(
/var/lib/rancher/k3s/server/db/state.db), not embedded etcd - Secrets encryption at rest —
secrets-encryption: trueis set in the k3s config. The AES-CBC key lives in the datastore bootstrap data and incred/encryption-config.jsonon the control-plane. Control-plane backups (./scripts/backup-k3s-control-plane.sh→backups/) plus the cluster token (homelab/k3s_token) are how the cluster can be restored (./scripts/restore-k3s-control-plane.sh). - nuc-i3-gen5 — worker (agent)
- Netbird-local networking — k3s binds on the Netbird VPN IP (
wt0) interface. systemd units haveAfter=netbird.serviceso the cluster only starts after the VPN is up. - Built-in components disabled — embedded Traefik and ServiceLB are turned off; two standalone Traefik instances deployed via Helm serve as ingress controllers (internal on Netbird VPN, public on IPv6). See Internal (Netbird VPN) Ingress and Public Internet Ingress sections in README.md.
- Two ingress controllers — one internal on the Netbird VPN IP
(
traefikingressClass, private CA) and one public on the static IPv6 address::1000(traefik-publicingressClass, Let's Encrypt via Cloudflare DNS-01, external-dns for automatic AAAA records). Default opt-in: services useingressClassName: traefik(internal); public exposure requires explicitingressClassName: traefik-public. - Deployment pattern — Helmfile is preferred over raw
helm upgrade --installwhenever possible (see why). New workloads should use Helmfile. Existinghelm upgrade --installscripts are candidates for migration. See the Helmfile section below.
Public Internet Ingress
The public-facing Traefik (traefik-public) binds on the static IPv6
address 2001:861:8b91:6620::1000 via hostNetwork. Services opt-in with
ingressClassName: traefik-public. TLS is automated via Let's Encrypt
(DNS-01, Cloudflare API token in .secret), and DNS records are managed
by external-dns (watches Ingress resources, creates AAAA records in
Cloudflare for *.ipv6.ingress.homelab.public.stephane-klein.info).
Grafana
Grafana is a dashboard visualization tool deployed via
Helmfile (helmfile/helmfile.yaml.gotmpl) at https://grafana.sklein.internal
(protected by Authelia — all *.sklein.internal subdomains are behind
ForwardAuth). Scripts interact with the Grafana API bypassing Authelia via
kubectl port-forward -n grafana svc/grafana, since the internal k8s service
does not require authentication. Dashboards are provisioned from JSON files in
grafana/dashboards/ via ConfigMaps labeled grafana_dashboard: "1".
Secret detection with gitleaks
Gitleaks prevents committing secrets accidentally. It runs as:
- Git pre-commit hook (
git-hooks/pre-commit) — checks staged files ongit commit. mise run gitleaks-check-push— scan the whole project beforejj git push.jj publish— local alias that updates themainbookmark, runs gitleaks, then pushes.
Configuration:
.gitleaks.toml— extends default rules with lower entropy threshold (2.0 vs 3.5) forgeneric-api-key, and allowlists for known safe paths (.secret,certs/,*.tfstate*,README.md).- Gitleaks is installed via
miseand pinned to version8.30.1.
Setup (one-time, after clone):
mise install
mise run setup-git-hooks
mise run setup-jj-alias
Application layout
Each application lives in its own self-contained directory under apps/<app>/
(helmfile, values, deploy/destroy scripts, mise tasks, README). See
docs/agents/helmfile-apps.md for the full
pattern. Legacy services not yet migrated still use
helmfile/helmfile.yaml.gotmpl with values in helmfile/values/. All services
deploy as k3s workloads via Helm. The scripts/ directory contains only
executable scripts.
Helmfile
Helmfile is for Helm what docker-compose.yml
is for Docker — a declarative way to define, version, and apply Helm releases.
It is the preferred deployment method over raw helm upgrade --install.
New applications define their own apps/<app>/helmfile.yaml (single release)
with values.yaml alongside. Deploy/destroy run as
helmfile -f apps/<app>/helmfile.yaml apply|destroy. See
docs/agents/helmfile-apps.md.
Authelia
Authelia provides SSO authentication before
Traefik via a ForwardAuth middleware. It runs as a k3s workload in the
authelia namespace, deployed via scripts/deploy-authelia.sh. Configuration
lives in config/authelia/. Access control rules use a wildcard
(*.sklein.internal, one_factor) so any new subdomain is automatically
protected.
Provisioning workflow
mise run setup-secret— populate.secretfrom Gopasstofu init && tofu apply— apply Netbird configurationtofu output -raw setup_key_nuc_i3_gen5 >> .secret— extract setup keys./nuc-*/create-custom-iso.sh— build Fedora CoreOS ISO./scripts/deploy-k3s.sh— install k3s (server + agent) over SSH./scripts/deploy-traefik.sh— deploy internal Traefik + cert-manager + private CA./scripts/deploy-traefik-public.sh— deploy public Traefik on IPv6./scripts/deploy-cert-manager-issuer-public.sh— deploy Let's Encrypt ClusterIssuer (DNS-01 via Cloudflare)./scripts/deploy-external-dns.sh— deploy external-dns for automatic Cloudflare DNS records
Supplementary Documentation
docs/agents/— operational snapshots of subsystems (loaded on demand by the agent)docs/agents/helmfile-apps.md— per-application Helmfile deployment patterndocs/decisions/— architecture decision recordsdocs/runbooks/— operational runbooks (maintenance, recovery, one-shot procedures).opencode/skills/new-decision/— skill for creating new decision records