Compare commits

..

No commits in common. "09d087fb11c821c57bcc8f25bbe213b4211a3ee1" and "f0d2f23a0f078cc42a405382e025845d003112a3" have entirely different histories.

61 changed files with 165 additions and 1001 deletions

View File

@ -1,12 +0,0 @@
# Config for: ansible-lint
# Applied by: ansible-lint (run from repo root)
exclude_paths:
- .cache/
- manifests/
- argocd/
- values/
use_default_rules: true
skip_list:
- yaml[line-length]

View File

@ -1,6 +1,6 @@
--- ---
name: secrets-leak-scanner name: secrets-leak-scanner
description: Scans staged/diffed files in this homelab repo for plaintext secrets that should instead come from .env or be sealed via kubeseal. Use before committing changes to manifests, Ansible vars, Helm values, or config/**. description: Scans staged/diffed files in this homelab repo for plaintext secrets that should instead come from .env or be sealed via kubeseal. Use before committing changes to manifests, Ansible vars, or Helm values.
tools: Read, Grep, Glob, Bash tools: Read, Grep, Glob, Bash
model: sonnet model: sonnet
--- ---
@ -9,12 +9,6 @@ You scan changes in this repo for secrets that are about to be committed in
plaintext. You have read-only Bash access (`git diff`, `git status`, `grep`) plaintext. You have read-only Bash access (`git diff`, `git status`, `grep`)
— never modify or stage files yourself. — never modify or stage files yourself.
Scope explicitly includes `config/**` (e.g. `config/dashy/conf.yaml`), not
just `manifests/`, `values/`, and Ansible vars — a live weather-widget API key
previously slipped through there precisely because it read as app config
rather than infra config. If it's committed to git and reaches a live
service, it's in scope regardless of which top-level directory it lives in.
## What "should never be plaintext in git" looks like here ## What "should never be plaintext in git" looks like here
Cross-reference `.env.example` for the full list of secret-shaped variable Cross-reference `.env.example` for the full list of secret-shaped variable

View File

@ -38,11 +38,5 @@ SWITCHBOT_SECRET=your_switchbot_secret_here
# Immich database credentials # Immich database credentials
IMMICH_POSTGRES_PASSWORD=your_password_here IMMICH_POSTGRES_PASSWORD=your_password_here
# Gitea database credentials (rotated off the plaintext value formerly in values/gitea.yaml)
GITEA_POSTGRES_PASSWORD=your_password_here
# Dashy weather widget API key (rotated off the plaintext value formerly in config/dashy/conf.yaml)
DASHY_WEATHER_API_KEY=your_api_key_here
PIA_USER=your_pia_username_here PIA_USER=your_pia_username_here
PIA_PASSWORD=your_pia_password_here PIA_PASSWORD=your_pia_password_here

View File

@ -1,44 +0,0 @@
# Config for: Gitea Actions CI
# Applied by: the self-hosted act_runner (ansible/roles/gitea-runner) on push
# Description: Read-only lint/validate pass -- no cluster access, no apply/deploy.
name: validate
on:
push:
branches:
- main
pull_request:
jobs:
lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Install validation tools
run: |
pip install yamllint ansible-lint
curl -L https://github.com/yannh/kubeconform/releases/latest/download/kubeconform-linux-amd64.tar.gz \
| tar xz kubeconform
sudo mv kubeconform /usr/local/bin/
- name: yamllint
run: yamllint -c .yamllint.yml .
- name: ansible-lint
run: |
ansible-galaxy collection install -r ansible/requirements.yml
ansible-lint ansible/
- name: ansible-playbook --syntax-check
run: |
for pb in ansible/playbooks/*.yaml; do
ansible-playbook --syntax-check -i ansible/inventory.yaml "$pb"
done
- name: kubeconform
run: |
kubeconform -summary -ignore-missing-schemas -kubernetes-version 1.32.0 \
-schema-location default \
-schema-location 'https://raw.githubusercontent.com/datreeio/CRDs-catalog/main/{{.Group}}/{{.ResourceKind}}_{{.ResourceAPIVersion}}.json' \
$(find manifests -name "*.yaml" -not -name "*-sealed.yaml") argocd/apps/*.yaml

7
.gitignore vendored
View File

@ -2,10 +2,3 @@
.DS_Store .DS_Store
tmp/ tmp/
old.debian-data old.debian-data
*.retry
.vault_pass*
kubeconfig*
*.kubeconfig
*.swp
*.swo
*~

View File

@ -1,25 +0,0 @@
# Config for: yamllint
# Applied by: yamllint -c .yamllint.yml .
extends: default
rules:
line-length: disable
document-start: disable
new-line-at-end-of-file: disable
comments-indentation: disable
commas:
max-spaces-after: -1
octal-values:
forbid-implicit-octal: true
forbid-explicit-octal: true
truthy:
allowed-values: ["true", "false", "on"]
comments:
min-spaces-from-content: 1
braces:
max-spaces-inside: 1
brackets:
max-spaces-inside: 1
ignore: |
router/

View File

@ -107,28 +107,6 @@ Never commit plaintext secrets. Two patterns coexist, both listed in
`ansible/group_vars/all/vault.yaml` holds Ansible-side secrets (e.g. `ansible/group_vars/all/vault.yaml` holds Ansible-side secrets (e.g.
`vault_k3s_node_token`) referenced from `host_vars`. `vault_k3s_node_token`) referenced from `host_vars`.
## Validation commands
No cluster access is required for any of these — run them before reporting a
change done, and show the actual output, not just a claim it passed.
```bash
yamllint -c .yamllint.yml .
ansible-lint ansible/
for pb in ansible/playbooks/*.yaml; do
ansible-playbook --syntax-check -i ansible/inventory.yaml "$pb"
done
kubeconform -summary -ignore-missing-schemas -kubernetes-version 1.32.0 \
-schema-location default \
-schema-location 'https://raw.githubusercontent.com/datreeio/CRDs-catalog/main/{{.Group}}/{{.ResourceKind}}_{{.ResourceAPIVersion}}.json' \
$(find manifests -name "*.yaml" -not -name "*-sealed.yaml") argocd/apps/*.yaml
```
`ansible-lint` needs the collections in `ansible/requirements.yml` installed
first (`ansible-galaxy collection install -r ansible/requirements.yml`),
otherwise it reports spurious `unknown-module` errors for `community.general`/
`community.docker` modules that are actually fine.
## Key gotchas (see README.md "Gotchas" for the full list) ## Key gotchas (see README.md "Gotchas" for the full list)
- Pi-hole has no wildcard DNS — add every new `home.arpa` hostname to both - Pi-hole has no wildcard DNS — add every new `home.arpa` hostname to both

View File

@ -11,7 +11,6 @@ in Kubernetes manifests plus Helm values.
| `minisforum` | `192.168.7.77` | K3s server, Traefik entrypoint, primary app node | | `minisforum` | `192.168.7.77` | K3s server, Traefik entrypoint, primary app node |
| `debian` / `nik-debian` | `192.168.7.183` | K3s agent, NFS storage, secondary Pi-hole | | `debian` / `nik-debian` | `192.168.7.183` | K3s agent, NFS storage, secondary Pi-hole |
| `mac-mini` | `192.168.7.96` | Standalone services such as Watch Party and Ollama | | `mac-mini` | `192.168.7.96` | Standalone services such as Watch Party and Ollama |
| `gpu-node` / `nik-gpu` | `192.168.7.98` | K3s agent with NVIDIA GPU passthrough, spot-tainted; runs Ollama directly on the host |
The cluster uses Traefik instead of the bundled K3s ingress controller. Internal The cluster uses Traefik instead of the bundled K3s ingress controller. Internal
services are published under `home.arpa` with certificates from an internal CA. services are published under `home.arpa` with certificates from an internal CA.
@ -75,7 +74,7 @@ Install workstation tools:
```bash ```bash
pip install ansible pip install ansible
ansible-galaxy collection install -r ansible/requirements.yml ansible-galaxy collection install community.general ansible.posix
``` ```
Also install `kubectl`, `helm`, and `kubeseal`. The inventory expects SSH Also install `kubectl`, `helm`, and `kubeseal`. The inventory expects SSH
@ -88,7 +87,6 @@ ansible-playbook -i ansible/inventory.yaml ansible/playbooks/bootstrap-minisforu
ansible-playbook -i ansible/inventory.yaml ansible/playbooks/setup-k3s.yaml -K ansible-playbook -i ansible/inventory.yaml ansible/playbooks/setup-k3s.yaml -K
ansible-playbook -i ansible/inventory.yaml ansible/playbooks/setup-nfs-debian.yaml -K ansible-playbook -i ansible/inventory.yaml ansible/playbooks/setup-nfs-debian.yaml -K
ansible-playbook -i ansible/inventory.yaml ansible/playbooks/join-debian-agent.yaml -K ansible-playbook -i ansible/inventory.yaml ansible/playbooks/join-debian-agent.yaml -K
ansible-playbook -i ansible/inventory.yaml ansible/playbooks/setup-gpu-node.yaml -K
``` ```
Install Argo CD once, then hand control to the app-of-apps: Install Argo CD once, then hand control to the app-of-apps:
@ -104,39 +102,7 @@ helm upgrade --install argocd argo/argo-cd \
kubectl apply -f manifests/argocd/app-of-apps.yaml kubectl apply -f manifests/argocd/app-of-apps.yaml
``` ```
The app-of-apps now brings up cert-manager's `ClusterIssuer`s (via After that, normal changes should flow through Git and Argo CD.
`cert-manager-config`) and Authentik's ingress/proxy-outpost/middleware (via
`authentik-config`) automatically — these used to require untracked manual
`kubectl apply` steps that weren't documented anywhere. **Authentik's Helm
chart itself is still a one-time manual install**, since its Argo CD
Application (`argocd/apps/authentik.yaml`) is deliberately left on manual sync
with a `targetRevision` placeholder pending a chart-version decision:
```bash
helm repo add authentik https://charts.goauthentik.io
helm repo update
helm upgrade --install authentik authentik/authentik \
-f values/authentik.yaml -n authentik --create-namespace
```
Populate secrets (see "Secrets" below) before Authentik, Gitea, Grafana, or
the other services that depend on them will come up healthy. After that,
normal changes should flow through Git and Argo CD.
### Cold-rebuild order
Rebuilding from nothing, the dependency order that actually matters is:
1. Ansible playbooks above (hosts, K3s, NFS).
2. `helm install argocd` + `kubectl apply -f manifests/argocd/app-of-apps.yaml`
— this alone now brings up cert-manager, sealed-secrets, and their
`ClusterIssuer`s/CA cert via sync-wave ordering.
3. Runtime secret scripts and Sealed Secret regeneration (see "Secrets") —
several Applications (Gitea, Grafana, home-services, Authentik) will sit
degraded/crash-looping until their secrets exist.
4. Manual Authentik Helm install (above) — every OAuth-gated service
(Grafana, Gitea, Argo CD SSO, Traefik dashboard) depends on it.
5. Everything else reconciles from Git on its own from here.
## Daily Operations ## Daily Operations
@ -213,14 +179,12 @@ state that must live on known disks:
| Location | Use | | Location | Use |
| --- | --- | | --- | --- |
| `/data/gitea` on `minisforum` | Gitea shared storage | | `/data/gitea` on `minisforum` | Gitea shared storage |
| `/data/prometheus` on `minisforum` | Prometheus (bound correctly) | | `/data/prometheus` on `minisforum` | Prometheus |
| `/data/grafana` on `minisforum` | Static PV defined for Grafana, but **not currently bound** — see gotcha below | | `/data/grafana` on `minisforum` | Grafana |
| `/data/loki` on `minisforum` | Static PV defined for Loki, but **not currently bound** — see gotcha below | | `/data/loki` on `minisforum` | Loki |
| `/mnt/storage` on `debian` | NFS media library | | `/mnt/storage` on `debian` | NFS media library and backups |
| `/home/nik/backups` on `debian` | NFS export for Gitea's backup CronJob (separate from `/mnt/storage`) |
The Debian NFS server exports both `/mnt/storage` and `/home/nik/backups` to The Debian NFS server exports `/mnt/storage` to `192.168.7.77`.
`192.168.7.77`.
## TLS and Trust ## TLS and Trust
@ -236,18 +200,8 @@ mobileconfig profile. The `ca-sync` CronJob updates those files from the
- Argo CD Applications mostly set `prune: false`; removing resources from Git may - Argo CD Applications mostly set `prune: false`; removing resources from Git may
require manual cleanup. require manual cleanup.
- Gitea uses a manual public `IngressRoute`; `values/gitea.yaml` has no - Gitea uses a manual public `IngressRoute`; the chart ingress is disabled in
`ingress:` key at all, so the chart's own ingress is off by chart default, `values/gitea.yaml`.
not an explicit setting.
- Grafana and Loki's static hostPath PVs (`grafana-pv`, `loki-pv` in
`manifests/monitoring/monitoring-pvs.yaml`) are currently unbound — their
Helm-managed PVCs got dynamically provisioned via the `local-path`
StorageClass instead (confirmed live via `kubectl get pv/pvc -n
monitoring`), unlike Prometheus which binds `prometheus-pv` correctly. Data
is not lost, just not on the disk the docs/manifest imply — needs a
deliberate decision (bind properly with a data migration, or drop the
orphaned static PVs and document reality) before relying on `/data/grafana`
or `/data/loki` for backups/DR.
- Gitea `ROOT_URL` changes can require deleting the generated inline config - Gitea `ROOT_URL` changes can require deleting the generated inline config
secret before reconciling. secret before reconciling.
- Pi-hole does not provide wildcard DNS here; add each new internal hostname to - Pi-hole does not provide wildcard DNS here; add each new internal hostname to

View File

@ -1,300 +0,0 @@
# Homelab Repo Audit & Refactor Plan
Living document. Produced by a full read-only audit of the repo (Ansible, Argo
CD, manifests, values, config, router). Nothing was applied, synced,
committed, or pushed as part of producing this. We work through the
"Migration Plan" stages one at a time, referencing this file; update the
Status column as stages land.
**Audit constraints honored**: `.env` was never opened; `router/backup-base.tar.gz`
was never extracted; no `kubectl apply` / `helm install` / Argo CD sync /
Ansible-against-real-hosts was run. All findings are grounded in file
citations, and the highest-severity ones were independently re-verified
(not just taken from sub-agent research output) before being recorded here.
## Stage Tracker
| Stage | Scope | Status |
|---|---|---|
| 1 | Safety fixes & secret hygiene | Files done, committed locally, **not pushed**. Actual secret rotation (new password/API key values + live DB password change) still needs the user — see note below. |
| 2 | Validation tooling & clean baseline | Done. `yamllint`/`ansible-lint`/`kubeconform` installed by user; `.yamllint.yml`/`.ansible-lint` added; baseline clean (0 yamllint issues, `kubeconform`: 117+20 resources valid, all playbooks pass `--syntax-check`). Remaining `ansible-lint` findings are either pre-existing role/var naming conventions (out of scope — would require repo-wide renames) or collections-not-installed noise that resolves once `ansible-galaxy collection install -r ansible/requirements.yml` is run. |
| 3 | Remove confirmed junk | Done, committed locally. `.DS_Store` confirmed never tracked (no action needed). |
| 4 | Ansible cleanup | Done, committed locally. |
| 5 | Argo CD bootstrap normalization | Files done, committed locally, **not pushed — highest-risk stage**. `authentik.yaml`'s chart version is a placeholder needing your input (`helm list -n authentik`); every new/changed Application here needs `kubectl diff` one at a time before/after enabling, not a blind batch push. |
| 6 | Values/DNS consolidation | Done, committed locally, **not pushed**. |
| 7 | Kubernetes correctness & security | Resource requests/limits + probes done (finding #16), committed locally, **not pushed**. `gitea-backup` RBAC narrowing (#11) done as part of Stage 5's commit. Grafana `runAsNonRoot`/`fsGroup` test (#25) and `:latest` image pinning not done — both need a live test window / registry inspection I didn't do unprompted. |
| 8 | Documentation & DR runbook | Done, committed locally. |
| 9 | CI & Claude Code guidance | Done, committed locally, **not pushed** (new CI automation surface). |
**Secret rotation still needs you** (Stage 1): I wired up the `existingSecret`/env-injection plumbing for the Gitea DB password and Dashy API key, but I don't generate or handle the actual new credential values — that's your call per this repo's credential-handling rule. See the session summary for exact steps.
### New findings from this session's live-cluster checks (not in the original audit)
- **Grafana and Loki's static PVs are orphaned.** `manifests/monitoring/monitoring-pvs.yaml` defines `grafana-pv`/`loki-pv` hostPath PVs, but live `kubectl get pv` shows both sitting `Available` (unbound) — their Helm-managed PVCs got dynamically provisioned via `local-path` instead, unlike `prometheus-pv` which binds correctly. Data isn't lost, just not where the README/manifest imply. Needs a decision: bind properly (data migration required) or drop the orphaned static PVs and document reality. Not yet actioned.
- **Finding #24 (GPU passthrough) is resolved as working, not broken.** Live check on node `nik-gpu`: `nvidia.com/gpu: "1"` allocatable, `nvidia` runtime handler registered in containerd, `nvidia-device-plugin` pod `Running`. The orphaned-looking `k3s-containerd-config.toml.j2` template is a non-issue in practice — no action needed.
- **Unknown #1 (Grafana PVC) resolved**: dynamically provisioned via `local-path`, not the static PV — see the orphaned-PV finding above, same root cause.
- Node hostnames in the live cluster are `nik-debian`/`nik-gpu`, not `debian`/`gpu-node` as in `ansible/inventory.yaml` — cosmetic (K3s registers by actual hostname), doesn't affect anything, just noted for anyone cross-referencing `kubectl get nodes` against the docs.
Open decisions needed from the user before/during the relevant stage:
- Finding #5 (`router/backup-base.tar.gz`): needs manual review outside this
workflow; may imply git-history scrub / router credential rotation.
- Finding #9 (K3s version skew): which version is canonical (`v1.32.2+k3s1`
vs `v1.32.4+k3s1`)?
- Finding #18: is `prune:true` on `home-services`/`otel-collector`/`tempo`
and `selfHeal:false` on both pihole Applications intentional policy?
- Finding #25: Grafana `runAsUser: 0` — worth testing `fsGroup`-only instead?
---
## Phase 1 — Current-State Architecture
```
Ansible (hosts) Argo CD (cluster)
───────────────── ──────────────────────────────────────────
minisforum (K3s server, .77) ──┐
debian (K3s agent, .183) ────┼─▶ K3s cluster ──▶ helm install argocd (manual, README.md:92-103)
gpu-node (K3s agent, .98) ──┘ │ │
mac-mini (standalone Docker) │ ▼
│ kubectl apply -f manifests/argocd/app-of-apps.yaml (manual, one-time)
│ │
│ ▼
│ Application: app-of-apps → watches argocd/apps/*.yaml
│ │
│ ┌───────────┴────────────────────────────┐
│ ▼ ▼
│ Helm-chart Applications Raw-manifest Applications
│ (chart + values/<name>.yaml) (path: manifests/<area>)
│ traefik, cert-manager*, gitea, core, media, home-services,
│ pihole, pihole-debian, monitoring, portfolio, homeassistant,
│ loki, tempo, otel-collector, *-secrets (sealed only)
│ sealed-secrets, argocd(self)
└── NOT reconciled by Argo CD at all:
manifests/cert-manager/ (ClusterIssuers)
manifests/authentik/ + values/authentik.yaml
4 files in manifests/network/ (non-sealed)
→ applied by hand, outside GitOps
```
Host-level services that never touch the K3s cluster: Home Assistant (Docker
Compose + systemd on `minisforum`, `ansible/roles/homeassistant/`), Watch
Party and Ollama (`mac-mini`), Ollama (`gpu-node`) — all Ansible-managed.
Bootstrap sequence as documented (`README.md:71-105`, `argocd/README.md:7-24`):
Ansible playbooks → manual `helm install argocd` → manual `kubectl apply` of
app-of-apps → everything else via Git. **This documented sequence is
incomplete** — it never mentions installing Authentik or the cert-manager
`ClusterIssuer`s, both required for the rest of the stack to actually work
(see Critical findings below).
## Executive Summary
The repo is coherently designed for a single-operator homelab: Ansible for
host bootstrap, Argo CD app-of-apps for cluster reconciliation, a clean
two-pattern secrets model (sealed-secrets for committed ciphertext, runtime
scripts for `.env`-sourced live secrets), and a genuinely useful pair of
existing Claude Code subagents (`infra-change-reviewer`, `secrets-leak-scanner`)
that already encode a lot of hard-won tribal knowledge. There's no
fashionable-but-wrong abstraction problem here — the biggest risks are **gaps
between what the documentation/GitOps model claims and what's actually wired
up**: two foundational subsystems (cert-manager's ClusterIssuers, and the
entire Authentik SSO stack) are invisible to Argo CD despite everything else
depending on them, a live database password and a live third-party API key
are committed in plaintext, and two high-value internal certificates are
silently issued by the wrong CA chain. None of this requires a restructure to
fix — it requires closing specific, identifiable gaps. Ansible is in good
shape except for a dual-`ansible.cfg` trap that breaks documented commands if
run from the wrong directory, and one likely-broken kubeconfig-fetch task in
the K3s bootstrap role. No CI or local validation tooling exists yet, which
is the main reason several of these gaps went unnoticed.
## Prioritized Findings
### Critical
| # | Finding | Evidence | Impact | Recommendation |
|---|---|---|---|---|
| 1 | `manifests/cert-manager/` (both `ClusterIssuer`s + the internal CA `Certificate`) is never referenced by any Argo CD `Application` — confirmed by grep across all of `argocd/apps/*.yaml`, zero `path:` hits | `argocd/apps/cert-manager.yaml` only installs the Helm chart/controller; no sibling Application points at `manifests/cert-manager/` | Every TLS certificate in the cluster depends on issuers that exist only because someone once ran `kubectl apply` by hand. A cold rebuild following the documented bootstrap steps will **not** restore them, and nothing detects/corrects drift on the live ones. | Add `argocd/apps/cert-manager-config.yaml` (path: `manifests/cert-manager`, sync-wave before consumers). See Stage 5. |
| 2 | `manifests/authentik/` (7 files: ingress, proxy outpost, middleware, 4 secret scripts) and `values/authentik.yaml` are never referenced by any Application, and Authentik's manual `helm upgrade --install` isn't even in the documented bootstrap steps | Confirmed by the same grep sweep; `values/authentik.yaml:1`'s header comment is the *only* place the install command lives | The SSO IdP gating Argo CD, Grafana, and Gitea OAuth is entirely outside GitOps and undocumented as a bootstrap step. Disaster recovery would silently fail to restore login for everything behind it. | Add `argocd/apps/authentik.yaml` (chart) + `argocd/apps/authentik-config.yaml` (path: `manifests/authentik`), mirroring the cert-manager two-Application pattern. |
| 3 | Plaintext PostgreSQL password committed | `values/gitea.yaml:48``password: gitea-db-password` under `postgresql.global.postgresql.auth.password`, no `existingSecret` used (contrast `values/gitea.yaml:11`, which correctly uses `existingSecret: gitea-admin-secret`) | A real credential sits in git history now, readable by anyone with repo access. Contradicts the repo's own stated no-plaintext-secrets rule and `.claude/agents/secrets-leak-scanner.md`'s invariant. | Rotate the DB password, convert to a `*-secret.sh` → sealed-secret pattern (mirror `manifests/media/immich-postgres-secret.sh`). Treat the current value as compromised since it's in history. |
| 4 | Live third-party API key committed in plaintext | `config/dashy/conf.yaml:63` and `:70` — a weather-widget API key used twice | Anyone with repo read access (or Dashy's own JS, which ships client-side) can extract and reuse the key. | Rotate the key at the provider; inject at apply-time from `.env` via `manifests/core/apply-dashy-config.sh` instead of hardcoding in `config/dashy/conf.yaml`. |
| 5 | `router/backup-base.tar.gz` is tracked in git (added in commit `e7718ce`, ~8.8KB compressed) | Confirmed tracked via `git ls-files`; **not opened**, per safety constraints | `router/uci-base.conf` itself (which was read) is a sanitized base config — WiFi disabled with default open SSID, no VPN configured, sentinel password field — which makes it likely the fuller `.tar.gz` backup is where any real WiFi PSK/VPN key/admin credential would actually live. Cannot confirm what's inside without extracting it. | **Action needed from user**: inspect this archive outside this workflow. If it contains live credentials, both git-history scrubbing and router credential rotation should be considered. |
| 6 | Two high-value internal certificates are issued by the wrong CA | `manifests/authentik/authentik-ingress.yaml:11-12` (`auth.home.arpa`) and `manifests/network/traefik-dashboard-ingress.yaml:65-66` (`traefik.home.arpa`) both set `issuerRef.name: internal-ca` — the root self-signed issuer — instead of `internal-ca-issuer`, the chained issuer every other internal `Certificate` uses (verified pattern in `manifests/homeassistant/homeassistant.yaml:31`, `manifests/home-services/certs.yaml`, `manifests/argocd/argocd.yaml:9`, media manifests) | Devices that installed the CA via the `ca-installer`/`ca-sync` flow (`manifests/README.md:36-42`) will **not** trust these two certs — browser TLS warnings on exactly the SSO login and the Traefik dashboard. Compounded by #1/#2: neither file is under GitOps, so nothing corrects this automatically. | Change `issuerRef.name` to `internal-ca-issuer` in both files. |
### High
| # | Finding | Evidence | Impact | Recommendation |
|---|---|---|---|---|
| 7 | Dual `ansible.cfg` breaks every documented playbook command if run from `ansible/` instead of repo root — empirically reproduced, not just theorized | `ansible.cfg` (root) vs `ansible/ansible.cfg`; confirmed via `ansible-config dump` that only one loads at a time, and reproduced two independent failures (path-doubling, then role-not-found) plus a silent loss of `host_key_checking=False` | A user who `cd`s into `ansible/` first (a very natural mistake) gets confusing, unrelated-looking errors, or worse, an interactive host-key prompt in what's meant to be scriptable. | Delete `ansible/ansible.cfg`; fold its two settings (`inject_facts_as_vars: False`, `deprecation_warnings: False`) into the root `ansible.cfg`. Single source of truth, run only from repo root (already the documented convention). |
| 8 | Likely-broken kubeconfig path in K3s bootstrap | `ansible/roles/k3s-server/tasks/main.yaml:50-62` — fetches kubeconfig to `~/.kube/config`, but the next task's `ansible.builtin.replace` targets `/tmp/k3s-minisforum.yaml`, a path nothing else in the role writes to | `replace` errors if the target doesn't exist — this task likely fails on a genuinely fresh `bootstrap-minisforum.yaml`/`setup-k3s.yaml` run, i.e. exactly the disaster-recovery path this needs to work for. **Static-analysis finding, not yet confirmed against a live re-run** — flagged as an unknown too. | Fix the path mismatch (verify what the fetch task actually names the local file and point `replace` at that same path); test on an actual rebuild before trusting this for DR. |
| 9 | K3s version defined in 3 places, already out of sync | `ansible/roles/k3s-server/defaults/main.yaml:6` and `k3s-agent/defaults/main.yaml:6` both `v1.32.2+k3s1`; `ansible/host_vars/gpu-node.yaml:35` is `v1.32.4+k3s1`. `ansible/README.md:58-59`'s upgrade doc only mentions the first two locations. | A fresh `debian`/`minisforum` (re)provision installs a different K3s version than what's apparently already running on `gpu-node` — real version-skew risk, and the documented upgrade procedure would miss the gpu-node override entirely. | Pick one canonical version, align all three, document all three locations. |
| 10 | 4 of 6 files in `manifests/network/` are excluded from Argo CD sync | `argocd/apps/network-secrets.yaml:14-15` sets `directory.include: '*-sealed.yaml'`, so `ddns-cronjob.yaml`, `glances-debian-ingress.yaml`, `traefik-dashboard-ingress.yaml`, `watch-party-ingress.yaml` are never synced | These back real, documented services (`README.md:52,63,64`) with zero drift protection or self-heal — same class of gap as #1/#2, smaller blast radius. | Add a proper `network.yaml` Application (or widen the include filter) covering these; keep `network-secrets.yaml` scoped to sealed secrets only. |
| 11 | `gitea-backup` CronJob has cluster-wide exec privileges it doesn't need | `manifests/gitea/gitea-backup.yaml:11` (`ClusterRole`) / `:23` (`ClusterRoleBinding`) grant `pods/exec` cluster-wide; the CronJob script only execs into the `gitea` namespace (lines 61-72) | The backup ServiceAccount can exec into any pod in any namespace — real privilege-escalation surface for a component whose only job is backing up one namespace. | Replace with a namespaced `Role`/`RoleBinding` scoped to `gitea`. |
| 12 | `homeassistant` Application/manifest namespace mismatch | `argocd/apps/homeassistant.yaml:14` targets/creates namespace `homeassistant`; every resource in `manifests/homeassistant/homeassistant.yaml` is hardcoded to `namespace: default` (lines 6,17,27,40) — verified directly | Argo CD creates and "manages" an empty, unused `homeassistant` namespace while the real resources live in `default`. Confusing, latent bug, no current functional harm. | Change `destination.namespace` to `default` and drop `CreateNamespace=true` (no runtime-affecting change — resources already live in `default`). |
| 13 | `gitea-backup` namespace has no `Namespace` object anywhere and isn't auto-created | `manifests/gitea/gitea-backup.yaml:8,29,39,100` all reference namespace `gitea-backup`; the owning Application (`gitea-secrets.yaml`) targets namespace `gitea` with `CreateNamespace=true` — a different namespace | Would fail to apply on any cluster where `gitea-backup` namespace wasn't created by hand out-of-band. | Add an explicit `Namespace: gitea-backup` object to the manifest, or fold the backup CronJob into the `gitea` namespace. |
| 14 | No CI or local validation tooling anywhere in the repo | Confirmed via recursive search: no `.github/`, `.gitea/workflows/`, pre-commit config, or yamllint/ansible-lint config at any level. `yamllint`, `ansible-lint`, `kubeconform` not installed locally either (only `helm`, `kubectl`, `kubeseal`, `jq`, `ansible-playbook` are present) | Given "no staging environment," every change is validated by pushing and watching Argo CD/production react. This is the root cause that let findings #3, #4, #20 go unnoticed. | See Stage 2. |
| 15 | Missing `requirements.yml` + undocumented collection dependency | No `requirements.yml` anywhere; `README.md:76-78` only tells a new operator to install `community.general`/`ansible.posix`, but `ansible/roles/glances/tasks/main.yaml:24` and `ansible/roles/watch-party/tasks/main.yaml:27` require `community.docker`, never mentioned | A fresh workstation following the documented bootstrap verbatim gets a "module not found" failure on `setup-glances-debian.yaml`/`deploy-watch-party.yaml`. | Add `ansible/requirements.yml` pinning all three collections; update `README.md`. |
| 16 | Missing resource requests/limits and probes on a long list of workloads | `manifests/core/{dashy,glances}.yaml`, `manifests/core/ca-installer/ca-installer.yaml`, `manifests/authentik/authentik-proxy-outpost.yaml`, `manifests/media/jellyfin.yaml`, main containers in `qbittorrent.yaml`/`jdownloader.yaml` (only their `gluetun` sidecars have probes), all 4 Deployments in `manifests/media/immich.yaml` | No protection against one workload starving others on a small, fixed-capacity cluster; no automatic restart on hang for several user-facing services. Contrast: `manifests/home-services/*.yaml` and `manifests/portfolio/portfolio.yaml` do this correctly already — good templates to copy from. | See Stage 7. |
### Medium
| # | Finding | Evidence | Recommendation |
|---|---|---|---|
| 17 | Sync-wave ordering doesn't cover cert-manager/sealed-secrets vs. their consumers | Only `gitea-secrets`/`gitea`/`loki`/`monitoring-secrets`/`monitoring`/`network-secrets` carry `sync-wave` annotations; `cert-manager` and `sealed-secrets` (whose CRDs everything else's `Certificate`/`SealedSecret` objects need) are unannotated, same wave-0 as their consumers | Add `sync-wave: "-2"` to `cert-manager`/`sealed-secrets`, `-1` to their config Applications, consistent with the existing pattern. Self-heals today via retries, so this is about shortening a fresh-bootstrap flaky window, not fixing active breakage. |
| 18 | Undocumented sync-policy exceptions | `home-services`, `otel-collector`, `tempo` use `prune: true` against the repo's stated `prune: false` norm (`argocd/README.md:37`); `pihole`/`pihole-debian` are the only two with `selfHeal: false` (plausibly intentional, given the documented external-IP-loss gotcha, but never stated as policy) | Add a one-line YAML comment on each explaining the exception, or normalize them if unintentional — needs user input on intent. |
| 19 | 3 Applications use `targetRevision: HEAD` instead of the documented `main` | `home-services.yaml:12`, `otel-collector.yaml:17`, `tempo.yaml:17` | Normalize to `main`. Zero behavioral difference today; purely consistency. |
| 20 | Pi-hole DNS entries have already drifted | `values/pihole.yaml:81,85` (`immich.home.arpa`, `gluetun.home.arpa`) missing from `values/pihole-debian.yaml` | Add the two missing entries. (Going forward this is now caught by the existing `PostToolUse` hook in `.claude/settings.json` — it just doesn't catch pre-existing drift.) |
| 21 | Orphaned DNS record | `nik4nao.home.arpa` exists in both pihole values files but no Certificate/Ingress/IngressRoute anywhere references it | Confirm it's unused, then remove. |
| 22 | `ansible/README.md` mislabels the `homeassistant` role "Legacy" | `ansible/README.md:54`; but `manifests/homeassistant/homeassistant.yaml`'s `Endpoints` (hardcoded to `192.168.7.77:8123`) has nothing else in the repo standing up a listener at that address — the "legacy" Ansible role/Docker Compose deployment is the **only** thing actually serving it | Re-word the doc entry; this is load-bearing, not dead — don't let a future cleanup pass delete it. |
| 23 | `docker` role logic duplicated wholesale inside `homeassistant` role | `ansible/roles/homeassistant/tasks/main.yaml:2-49` reimplements Docker CE install (Debian apt path) instead of reusing `ansible/roles/docker/tasks/main.yaml:6-43` (Ubuntu apt path) | Parameterize `docker` role for both distros, have `homeassistant` depend on it instead of duplicating. |
| 24 | GPU passthrough likely non-functional as wired | `ansible/roles/nvidia/templates/k3s-containerd-config.toml.j2` exists but is never referenced by any task in that role; `manifests/home-services/nvidia-device-plugin.yaml:5-9` defines a `RuntimeClass handler: nvidia` that depends on exactly that containerd config existing on `gpu-node` | **Unknown pending live verification** — needs a check on `gpu-node` before deciding whether to wire up the template or remove the RuntimeClass. |
| 25 | Grafana forced to run as root | `values/kube-prometheus-stack.yaml:49-52``runAsNonRoot: false`, `runAsUser: 0`, likely to support `initChownData: true` fixing hostPath ownership | Test whether `fsGroup` alone (without full root) suffices; needs a live test window with rollback ready. |
| 26 | Mixed `Ingress` vs Traefik `IngressRoute` usage with no stated policy | e.g. `manifests/media/qbittorrent.yaml` uses plain `Ingress` for the app (line 176) but `IngressRoute` for `gluetun-api` in the same file (line 216) | Not broken, but only the `IngressRoute` half gets native `Middleware` support without annotation workarounds — worth standardizing on one mechanism over time, not urgent. |
### Low (grouped)
- **Ansible hygiene**: zero `tags:` usage anywhere (no selective runs possible); FQCN usage inconsistent (`wireguard` and `homeassistant` roles are 100% bare module names, `ollama` is mixed — `ansible/roles/{wireguard,homeassistant,ollama}/tasks/main.yaml`); `nvidia/tasks/main.yaml:58-62` uses `changed_when: true` unconditionally, restarting Docker every run; `gitea-runner/tasks/main.yaml:89-93` unconditionally deletes `/run/docker.sock` every run despite the task name implying a conditional check.
- **Operational secret leaks (not committed, but printed)**: `ansible/roles/k3s-server/tasks/main.yaml:46-48` prints the real K3s join token via `debug: msg:` on every run; `ansible/roles/gitea-runner/tasks/main.yaml:55-79` writes the runner registration token into a world-readable (`0644`) systemd unit file; `ansible/roles/wireguard/tasks/main.yaml:169-184` prints generated WireGuard private keys to console. None are committed to git, but all are worth tightening.
- **Doc drift**: `README.md:203-204` claims Gitea ingress is "disabled in `values/gitea.yaml`" — that file has no `ingress:` key at all (it's disabled by chart default, not an explicit setting); `README.md:174-186` groups Gitea's backups under `/mnt/storage` when they actually live on a separate NFS export (`/home/nik/backups`, per `manifests/gitea/gitea-backup.yaml:92` and `ansible/roles/nfs-server/templates/exports.j2:7`); `ansible/README.md`'s role table and playbook list omit the `docker`/`nvidia` roles, `setup-gpu-node.yaml`, and `homeassistant.yaml`; root `README.md`'s host table omits `gpu-node` entirely as a 4th host; `config/dashy/conf.yaml:2` points at `manifests/apply-dashy-config.sh`, missing the real `core/` path segment.
- **`.gitignore` gaps** (nothing currently leaking, but no coverage): `*.retry` (a live risk — Ansible drops these on playbook failure and this repo's Ansible tree is actively used), `.vault_pass*`, kubeconfig-shaped filenames, editor swap files. `.DS_Store` files exist in the working tree at repo root and `config/` despite being gitignored — confirm they're actually untracked, not just ignored-going-forward.
## Duplicated / Ambiguous / Possibly Obsolete Resources
Per the "trace references before declaring obsolete" rule — **none of the
"unsynced" items above are actually dead**; they're all in active use, just
invisible to Argo CD. The only things that look genuinely obsolete or
redundant are:
- `nik4nao.home.arpa` DNS entry (both pihole values files) — no backing resource found anywhere.
- `ansible/ansible.cfg` — not obsolete exactly, but its only real-world effect today is negative (breaks documented commands); candidate for deletion/merge into the root cfg.
- `ansible/roles/nvidia/templates/k3s-containerd-config.toml.j2` — orphaned template, either finish wiring it up or remove it once GPU passthrough status is confirmed live.
- `.DS_Store` at repo root and in `config/` — should never have been committed; harmless but should be `git rm --cached`.
## Security Findings Summary (redacted)
- Two real credentials committed in plaintext: a database password (`values/gitea.yaml:48`) and a third-party API key (`config/dashy/conf.yaml:63,70`). Both should be treated as compromised and rotated — no value is printed anywhere in this document.
- One tracked binary (`router/backup-base.tar.gz`) whose contents were not inspected but is flagged as the most likely place real WiFi/VPN/router-admin credentials would live, given the sibling `uci-base.conf` is sanitized. Needs review outside this workflow; rotation and/or history-scrubbing may be warranted depending on what's inside.
- No plaintext secrets found in Ansible beyond the two operational-leak items (console-printed token, world-readable systemd unit) — the vault (`ansible/group_vars/all/vault.yaml`) is genuinely encrypted and used correctly.
- RBAC: one real over-scope (`gitea-backup`'s cluster-wide `pods/exec`), no wildcard verbs/resources found anywhere else.
- Existing guardrails already partially cover this class of problem going forward: `.claude/agents/secrets-leak-scanner.md` and the `PostToolUse` hook in `.claude/settings.json` — but the scanner's stated scope reads as Ansible/manifests/Helm-values-centric and doesn't explicitly call out `config/**`, which is exactly where the Dashy API key slipped through. Worth widening its scope statement rather than adding new tooling.
## What Should Be Preserved
- The Ansible → Argo CD boundary itself is clean — no Ansible task embeds Kubernetes YAML or runs `kubectl apply` against application manifests (verified by repo-wide grep). The one-time hand-off (`helm install argocd``kubectl apply -f app-of-apps.yaml`) is exactly right for this scale.
- The app-of-apps + one-Application-per-file pattern (20 flat files in `argocd/apps/`) is simple, greppable, and easy to reason about at this service count — an ApplicationSet or Kustomize layer would add indirection with no real benefit here.
- The two-pattern secrets model (sealed-secrets for committed ciphertext, runtime `.env`-sourced scripts for cluster-only secrets) is coherent and consistently documented across all three READMEs.
- Exact Helm chart version pinning on every chart-backed Application — zero floating versions found.
- No deprecated Kubernetes API versions anywhere — clean.
- Idempotent `curl | sh`-style installs are consistently guarded with `creates:` across `docker`, `nvidia`, `k3s-server`, `k3s-agent`, `ollama` roles.
- `.claude/agents/infra-change-reviewer.md` and `secrets-leak-scanner.md` plus the `settings.json` hooks already encode a lot of this audit's tribal knowledge (DNS-sync rule, sealed-secret hand-edit ban, cert-issuer split, GPU taint/toleration, Ansible dual-cfg risk) — build on these, don't replace them.
- Good in-repo examples worth using as the template when fixing the bad ones: `manifests/home-services/nvidia-device-plugin.yaml` (privilege drop), `manifests/core/ca-installer/ca-sync.yaml` (tightly-scoped namespaced RBAC), `manifests/home-services/{ai-gateway,ha-gateway}.yaml` + `manifests/portfolio/portfolio.yaml` (proper resources + probes).
## Unknowns (need live cluster info or user input — not determinable from the repo alone)
1. Whether Grafana's PVC actually binds to the static `grafana-pv` hostPath or silently gets dynamically provisioned via `local-path` instead (`values/kube-prometheus-stack.yaml` sets no `storageClassName`/`volumeName` for Grafana, unlike Prometheus/Loki which do). Needs `kubectl get pvc -n monitoring` / `kubectl get pv grafana-pv -o yaml`.
2. Whether GPU passthrough is actually functional today given the orphaned containerd template (finding #24). Needs a check on `gpu-node`.
3. What's actually inside `router/backup-base.tar.gz` (finding #5) — deliberately not inspected.
4. Whether the K3s bootstrap kubeconfig-path bug (finding #8) actually breaks a fresh run, or whether there's missing context from an untraced earlier step. Static reading only — recommend confirming before relying on it for DR.
5. Whether `selfHeal: false` on the two pihole Applications is deliberate policy (tied to the external-IP-loss gotcha) or an oversight.
6. What Authentik Helm chart version is actually running — it's outside Argo CD, so there's no `targetRevision` to read; needs `helm list -n authentik` (or wherever it's installed) on the live cluster.
7. Whether `values/gitea.yaml`'s committed password and Dashy's committed API key have ever been exposed beyond the operator (repo visibility/access history) — affects how urgent rotation is.
---
## Phase 4 — Target Architecture
Given this is a single cluster, single operator, ~20-service homelab with no
near-term multi-cluster plan, two credible options were considered.
### Option A (recommended): Keep the current three-way split, close the gaps
Keep `ansible/` / `argocd/apps/` / `manifests/` / `values/` exactly as they
are structurally — this layout already matches how the system is organized
conceptually, how the existing subagents and skills reference paths, and how
the current documentation is written. Fix the specific holes (findings
above) rather than moving files around.
```
homelab/
├── ansible.cfg # single config, root-only (ansible/ansible.cfg removed)
├── ansible/
│ ├── requirements.yml # NEW — pins community.general, ansible.posix, community.docker
│ ├── inventory.yaml
│ ├── group_vars/ · host_vars/
│ ├── playbooks/
│ └── roles/
├── argocd/
│ ├── apps/ # + cert-manager-config.yaml, authentik.yaml, authentik-config.yaml
│ └── values/
├── manifests/ # unchanged tree; every subdir now has a matching Application
├── values/ # unchanged
├── config/dashy/
├── router/
├── .yamllint.yml # NEW
├── .ansible-lint # NEW (config only, tool installed separately)
└── CLAUDE.md # + validation commands section
```
**Why not restructure further**: there's no duplication cost today that a
`clusters/`/`infrastructure/`/`apps/` split or Kustomize bases would actually
reduce — each service already has exactly one Application file and at most
one values file. Introducing Kustomize overlays or ApplicationSets would add
a templating/indirection layer this repo doesn't need at 1-cluster,
~20-service scale, and would itself become something to maintain and
explain.
### Option B (not recommended now): `clusters/` + `infrastructure/` + `apps/`, colocated values
Restructure to one directory per service (e.g.
`apps/gitea/{application.yaml,values.yaml}`), split `infrastructure/`
(cert-manager, sealed-secrets, traefik, pihole) from `apps/` (user-facing
services), under a `clusters/homelab/` root to leave room for a future
second cluster. This gives slightly tighter per-service ownership (one
directory instead of two: `argocd/apps/x.yaml` + `values/x.yaml`) but
requires moving **every** Application's `source.path`, which Argo CD treats
as a new resource identity — each move is a prune-risk event that must be
done one Application at a time with `prune: false` verified beforehand,
diffed, and rolled out carefully. That's a lot of migration risk for a
benefit (one fewer directory hop per service) that doesn't solve any problem
that exists today, and it presumes a multi-cluster future that isn't
planned.
**Recommendation: Option A.** Revisit Option B only if a second cluster is
actually added or the service count grows to where cross-referencing
`argocd/apps/` and `values/` by hand becomes genuinely painful — neither is
true today.
---
## Phase 5 — Incremental Migration Plan
Every stage is designed to leave the repo in a working, deployable state at
every commit boundary. Stages that touch what Argo CD currently manages are
explicitly marked **RUNTIME-AFFECTING** and require explicit go-ahead before
pushing, per the repo's own rule that `git push` is a production deploy.
| Stage | Scope | Key changes | Runtime impact | Rollback |
|---|---|---|---|---|
| **1. Safety fixes & secret hygiene** | Findings #3, #4, #6, plus `.gitignore` gaps | Rotate + re-secret the Gitea DB password and Dashy API key; fix the two `issuerRef` values; extend `.gitignore` (`*.retry`, `.vault_pass*`, kubeconfig patterns, swap files); `git rm --cached` the `.DS_Store` files | **RUNTIME-AFFECTING** for the password rotation (Gitea/Postgres restart) and cert reissuance (brief TLS transition on 2 hostnames); everything else is zero-impact | Revert commit; for password rotation, keep old secret retrievable until new one is confirmed working |
| **2. Validation tooling & clean baseline** | New root-level config | Add `.yamllint.yml`, `.ansible-lint`; install `yamllint`/`ansible-lint`/`kubeconform` locally (asking first, since that's installing software); run baseline, fix anything trivial it finds | None — local/offline only | Uninstall tools / delete config files |
| **3. Remove confirmed junk** | `.DS_Store`, orphaned `nik4nao.home.arpa` DNS entry, doc-path typo in `config/dashy/conf.yaml:2` | Delete/clean | Negligible (DNS entry removal is additive-safe to revert) | `git revert` |
| **4. Ansible cleanup** | Findings #7, #9, #15, plus low-severity items | Merge `ansible/ansible.cfg` into root; add `requirements.yml`; align `k3s_version` across 3 locations; fix `gitea-runner` docker.sock task, `nvidia` `changed_when`, K3s token debug print, systemd unit permissions; update both READMEs (gpu-node host, docker/nvidia roles, homeassistant status) | **RUNTIME-AFFECTING** only for the `k3s_version` alignment (changes what a future node provision installs) — needs sign-off on which version is canonical; rest is inert until playbooks rerun | Git revert; k3s_version change only takes effect on next actual provision, not immediately |
| **5. Argo CD bootstrap normalization** | Findings #1, #2, #10, #12, #13, #17, #19 | New `cert-manager-config.yaml` + `authentik.yaml`/`authentik-config.yaml` Applications (first-time adoption of already-live resources — `kubectl diff` before enabling automated sync on each); widen `network-secrets` coverage or add `network.yaml`; fix homeassistant namespace; add `gitea-backup` Namespace + narrow its RBAC to a Role; add missing sync-waves; normalize `targetRevision: HEAD``main` | **RUNTIME-AFFECTING, highest-risk stage** — bringing previously-unmanaged live resources under Argo CD for the first time. Roll out one Application at a time, diff first, watch sync status before moving to the next | Set the newly-added Application's sync policy back to manual, or delete the Application (resources stay, since `prune: false`) |
| **6. Values/DNS consolidation** | Finding #18, #20 | Reconcile `pihole.yaml`/`pihole-debian.yaml` drift (add missing entries); document or normalize the `prune:true`/`selfHeal:false` outliers (pending intent) | **RUNTIME-AFFECTING but low-risk** — adds DNS records only, additive | Revert values file |
| **7. Kubernetes correctness & security** | Findings #11, #16, #25, #26 | Add resource requests/limits + probes to flagged workloads (one service at a time); narrow `gitea-backup` RBAC; test Grafana `runAsNonRoot`/`fsGroup`-only; pin remaining `:latest` images to currently-running tags | **RUNTIME-AFFECTING** — limits set too low can OOMKill; needs `kubectl top` baselines first (gather live before proposing numbers); roll out one service at a time with rollback ready | Revert manifest, resync |
| **8. Documentation & DR runbook** | All doc-drift items | Fix host tables, role tables, playbook lists, ingress/storage claims; add an explicit cold-rebuild runbook reflecting the now-closed GitOps gaps | None | Revert |
| **9. CI & Claude Code guidance** | New `.gitea/workflows/validate.yaml` (yamllint/ansible-lint/kubeconform on push); `CLAUDE.md` validation-commands section; widen `secrets-leak-scanner.md`'s stated scope to include `config/**`; consider extending the `PreToolUse` hook to block Claude Code from reading `.env`/`router/backup-base.tar.gz` directly | New automation surface | None to the cluster; new CI pipeline needs approval since it's a new automation surface | Delete workflow file |
Every stage gets validated with whatever's actually available
(`ansible-playbook --syntax-check`, `yamllint`, `ansible-lint`,
`kubeconform`/`kubectl apply --dry-run=client` where a live context isn't
required) before being reported done — with the concrete diff and validation
output shown, not just a claim of success.
---
## Claude Code Project Support
- **`CLAUDE.md`**: keep it, add a short "Validation commands" section once Stage 2 lands (exact `yamllint`/`ansible-lint`/`ansible-playbook --syntax-check` invocations).
- **Guard against accidental `.env`/key/backup access**: partially covered already (`.gitignore`, sealed-secret `PreToolUse` hook). Extend that hook to also block Claude Code `Read` of `.env` and `router/backup-base.tar.gz` — small addition, Stage 9.
- **One validation skill/command**: rather than a new skill, widen `secrets-leak-scanner.md`'s stated file scope to explicitly include `config/**` (the actual gap that let the Dashy API key through) — cheaper and more targeted than adding new tooling.

View File

@ -5,5 +5,3 @@
inventory = ansible/inventory.yaml inventory = ansible/inventory.yaml
roles_path = ansible/roles roles_path = ansible/roles
host_key_checking = False host_key_checking = False
inject_facts_as_vars = False
deprecation_warnings = False

View File

@ -6,28 +6,16 @@ cluster.
## Inventory ## Inventory
`inventory.yaml` defines four groups: `inventory.yaml` defines three groups:
| Group | Host | Purpose | | Group | Host | Purpose |
| --- | --- | --- | | --- | --- | --- |
| `k3s_server` | `minisforum` | K3s server at `192.168.7.77` | | `k3s_server` | `minisforum` | K3s server at `192.168.7.77` |
| `k3s_agents` | `debian` | K3s agent and NFS storage at `192.168.7.183` | | `k3s_agents` | `debian` | K3s agent and NFS storage at `192.168.7.183` |
| `mac_mini` | `mac-mini` | Docker/Ollama host at `192.168.7.96` | | `mac_mini` | `mac-mini` | Docker/Ollama host at `192.168.7.96` |
| `gpu_workstation` | `gpu-node` | K3s agent with NVIDIA GPU passthrough at `192.168.7.98` (spot-tainted) |
All hosts use the `nik` user and the SSH key configured in `inventory.yaml`. All hosts use the `nik` user and the SSH key configured in `inventory.yaml`.
## Collections
Install the third-party collections this repo's roles depend on before
running any playbook:
```bash
ansible-galaxy collection install -r ansible/requirements.yml
```
(`community.general`, `ansible.posix`, `community.docker`.)
## Common Playbooks ## Common Playbooks
```bash ```bash
@ -35,7 +23,6 @@ ansible-playbook -i ansible/inventory.yaml ansible/playbooks/bootstrap-minisforu
ansible-playbook -i ansible/inventory.yaml ansible/playbooks/setup-k3s.yaml -K ansible-playbook -i ansible/inventory.yaml ansible/playbooks/setup-k3s.yaml -K
ansible-playbook -i ansible/inventory.yaml ansible/playbooks/setup-nfs-debian.yaml -K ansible-playbook -i ansible/inventory.yaml ansible/playbooks/setup-nfs-debian.yaml -K
ansible-playbook -i ansible/inventory.yaml ansible/playbooks/join-debian-agent.yaml -K ansible-playbook -i ansible/inventory.yaml ansible/playbooks/join-debian-agent.yaml -K
ansible-playbook -i ansible/inventory.yaml ansible/playbooks/setup-gpu-node.yaml -K
``` ```
Additional services: Additional services:
@ -48,7 +35,6 @@ ansible-playbook -i ansible/inventory.yaml ansible/playbooks/setup-glances-debia
ansible-playbook -i ansible/inventory.yaml ansible/playbooks/setup-ollama.yaml -K ansible-playbook -i ansible/inventory.yaml ansible/playbooks/setup-ollama.yaml -K
ansible-playbook -i ansible/inventory.yaml ansible/playbooks/deploy-watch-party.yaml ansible-playbook -i ansible/inventory.yaml ansible/playbooks/deploy-watch-party.yaml
ansible-playbook -i ansible/inventory.yaml ansible/playbooks/wireguard.yaml -K ansible-playbook -i ansible/inventory.yaml ansible/playbooks/wireguard.yaml -K
ansible-playbook -i ansible/inventory.yaml ansible/playbooks/homeassistant.yaml -K
``` ```
## Roles ## Roles
@ -56,24 +42,21 @@ ansible-playbook -i ansible/inventory.yaml ansible/playbooks/homeassistant.yaml
| Role | Responsibility | | Role | Responsibility |
| --- | --- | | --- | --- |
| `common` | Packages, user setup, firewall, base data directories | | `common` | Packages, user setup, firewall, base data directories |
| `docker` | Docker CE install (Debian and Ubuntu); depended on by `homeassistant` |
| `nvidia` | NVIDIA driver, CUDA toolkit, and containerd/Docker GPU runtime config |
| `k3s-server` | K3s server install, kubeconfig fetch, Helm install, primary node label | | `k3s-server` | K3s server install, kubeconfig fetch, Helm install, primary node label |
| `k3s-agent` | K3s agent join and storage/GPU node label | | `k3s-agent` | K3s agent join and storage node label |
| `nfs-server` | Export `/mnt/storage` from Debian to the K3s server | | `nfs-server` | Export `/mnt/storage` from Debian to the K3s server |
| `monitoring` | Host directories and ownership for Prometheus/Loki | | `monitoring` | Host directories and ownership for Prometheus/Loki |
| `gitea-runner` | Gitea Actions runner systemd service | | `gitea-runner` | Gitea Actions runner systemd service |
| `glances` | Host-level Glances service | | `glances` | Host-level Glances service |
| `ollama` | Ollama service on the Mac Mini and GPU node (branches on OS) | | `ollama` | Ollama service on the Mac Mini |
| `watch-party` | Watch Party Docker Compose deployment on the Mac Mini | | `watch-party` | Watch Party Docker Compose deployment on the Mac Mini |
| `wireguard` | WireGuard server configuration | | `wireguard` | WireGuard server configuration |
| `homeassistant` | Standalone Home Assistant deployment (Docker Compose + systemd on `minisforum`) — this is the **only** thing serving `ha.home.arpa`, not legacy/dead | | `homeassistant` | Legacy standalone Home Assistant deployment |
## Notes ## Notes
- K3s version is defined in three places and must be kept in sync: - K3s version is set in `roles/k3s-server/defaults/main.yaml` and
`roles/k3s-server/defaults/main.yaml`, `roles/k3s-agent/defaults/main.yaml`, `roles/k3s-agent/defaults/main.yaml`.
and the override in `host_vars/gpu-node.yaml`.
- `setup-gitea-runner.yaml` reads `GITEA_RUNNER_TOKEN` from the local - `setup-gitea-runner.yaml` reads `GITEA_RUNNER_TOKEN` from the local
environment. environment.
- The K3s role disables bundled Traefik because Traefik is managed by Argo CD. - The K3s role disables bundled Traefik because Traefik is managed by Argo CD.
@ -81,7 +64,3 @@ ansible-playbook -i ansible/inventory.yaml ansible/playbooks/homeassistant.yaml
mount that export directly. mount that export directly.
- Keep host automation idempotent where practical. These playbooks are meant to - Keep host automation idempotent where practical. These playbooks are meant to
be rerunnable during rebuilds. be rerunnable during rebuilds.
- To see the real K3s join token (needed once, to populate
`vault_k3s_node_token`), pass `-e k3s_show_token=true` to `setup-k3s.yaml`;
it's suppressed by default. Same pattern for WireGuard client configs via
`-e wireguard_show_client_configs=true` on `wireguard.yaml`.

4
ansible/ansible.cfg Normal file
View File

@ -0,0 +1,4 @@
[defaults]
inventory = inventory.yaml
inject_facts_as_vars = False
deprecation_warnings = False

View File

@ -32,8 +32,6 @@ k3s_server_url: "https://192.168.7.77:6443"
k3s_node_token: "{{ vault_k3s_node_token }}" k3s_node_token: "{{ vault_k3s_node_token }}"
# Check current cluster version with: k3s --version on minisforum # Check current cluster version with: k3s --version on minisforum
# Kept in sync with roles/k3s-server and roles/k3s-agent defaults — all three
# must match; see ansible/README.md "K3s version" note.
k3s_version: "v1.32.4+k3s1" k3s_version: "v1.32.4+k3s1"
k3s_node_labels: k3s_node_labels:

View File

@ -1,6 +1,6 @@
--- ---
- name: Deploy Home Assistant on Minisforum - name: Deploy Home Assistant on Minisforum
hosts: minisforum hosts: minisforum
become: true become: yes
roles: roles:
- homeassistant - homeassistant

View File

@ -1,6 +1,6 @@
# Config for: gpu-node workstation full setup # Config for: gpu-node workstation full setup
# Applied by: ansible-playbook -i ansible/inventory.yaml ansible/playbooks/setup-gpu-node.yaml # Applied by: ansible-playbook -i ansible/inventory.yaml ansible/playbooks/setup-gpu-node.yaml
- name: GPU node setup - name: gpu-node setup
hosts: gpu_workstation hosts: gpu_workstation
become: true become: true

View File

@ -1,6 +0,0 @@
# Config for: Ansible Galaxy collections
# Applied by: ansible-galaxy collection install -r ansible/requirements.yml
collections:
- name: community.general
- name: ansible.posix
- name: community.docker

View File

@ -1,4 +0,0 @@
---
# Part of role: docker
# Description: Default vars so this role doesn't depend on `common` having run first in the same play.
username: nik

View File

@ -1,26 +1,12 @@
--- ---
# Part of role: docker # Part of role: docker
# Called by: ansible/playbooks/setup-gpu-node.yaml, ansible/roles/homeassistant (meta dependency) # Called by: ansible/playbooks/setup-gpu-node.yaml
# Description: Installs Docker CE, adds user to docker group. Works on both Debian and Ubuntu. # Description: Installs Docker CE on Ubuntu, adds user to docker group.
- name: Install Docker prerequisites
ansible.builtin.apt:
name:
- ca-certificates
- curl
state: present
update_cache: true
- name: Create apt keyrings directory
ansible.builtin.file:
path: /usr/share/keyrings
state: directory
mode: "0755"
- name: Add Docker GPG key - name: Add Docker GPG key
ansible.builtin.shell: ansible.builtin.shell:
cmd: > cmd: >
curl -fsSL https://download.docker.com/linux/{{ ansible_facts['distribution'] | lower }}/gpg | curl -fsSL https://download.docker.com/linux/ubuntu/gpg |
gpg --dearmor -o /usr/share/keyrings/docker.gpg gpg --dearmor -o /usr/share/keyrings/docker.gpg
creates: /usr/share/keyrings/docker.gpg creates: /usr/share/keyrings/docker.gpg
@ -28,12 +14,12 @@
ansible.builtin.apt_repository: ansible.builtin.apt_repository:
repo: > repo: >
deb [arch=amd64 signed-by=/usr/share/keyrings/docker.gpg] deb [arch=amd64 signed-by=/usr/share/keyrings/docker.gpg]
https://download.docker.com/linux/{{ ansible_facts['distribution'] | lower }} https://download.docker.com/linux/ubuntu
{{ ansible_facts['distribution_release'] }} stable {{ ansible_facts['distribution_release'] }} stable
filename: docker filename: docker
state: present state: present
- name: Install Docker CE and Compose plugin - name: Install Docker CE
ansible.builtin.apt: ansible.builtin.apt:
name: name:
- docker-ce - docker-ce

View File

@ -74,7 +74,7 @@
[Install] [Install]
WantedBy=multi-user.target WantedBy=multi-user.target
mode: "0600" mode: "0644"
become: true become: true
notify: Restart act_runner notify: Restart act_runner
@ -86,18 +86,11 @@
become: false become: false
changed_when: false changed_when: false
- name: Check docker.sock type
ansible.builtin.stat:
path: /run/docker.sock
register: docker_sock_stat
become: true
- name: Remove docker.sock if it is a directory - name: Remove docker.sock if it is a directory
ansible.builtin.file: ansible.builtin.file:
path: /run/docker.sock path: /run/docker.sock
state: absent state: absent
become: true become: true
when: docker_sock_stat.stat.exists and docker_sock_stat.stat.isdir
- name: Enable and start Docker - name: Enable and start Docker
ansible.builtin.systemd: ansible.builtin.systemd:

View File

@ -1,4 +1,4 @@
--- ---
- name: Reload systemd - name: reload systemd
ansible.builtin.systemd: systemd:
daemon_reload: true daemon_reload: yes

View File

@ -1,3 +0,0 @@
---
dependencies:
- role: docker

View File

@ -1,11 +1,55 @@
--- ---
# Part of role: homeassistant - name: Install Docker prerequisites
# Called by: ansible/playbooks/homeassistant.yaml apt:
# Description: Deploys the standalone Home Assistant Docker Compose stack. Docker itself is name:
# installed by the `docker` role (meta dependency) instead of being duplicated here. - ca-certificates
- curl
state: present
update_cache: yes
- name: Create apt keyrings directory
file:
path: /etc/apt/keyrings
state: directory
mode: "0755"
- name: Download Docker GPG key
get_url:
url: https://download.docker.com/linux/debian/gpg
dest: /etc/apt/keyrings/docker.asc
mode: "0644"
- name: Add Docker apt repository
apt_repository:
repo: "deb [arch=amd64 signed-by=/etc/apt/keyrings/docker.asc] https://download.docker.com/linux/debian trixie stable"
state: present
filename: docker
- name: Install Docker CE and Compose plugin
apt:
name:
- docker-ce
- docker-ce-cli
- containerd.io
- docker-buildx-plugin
- docker-compose-plugin
state: present
update_cache: yes
- name: Enable and start Docker
systemd:
name: docker
enabled: yes
state: started
- name: Add nik to docker group
user:
name: nik
groups: docker
append: yes
- name: Create HA config directory - name: Create HA config directory
ansible.builtin.file: file:
path: /home/nik/homeassistant/config path: /home/nik/homeassistant/config
state: directory state: directory
owner: nik owner: nik
@ -13,7 +57,7 @@
mode: "0755" mode: "0755"
- name: Deploy docker-compose.yaml - name: Deploy docker-compose.yaml
ansible.builtin.template: template:
src: docker-compose.yaml.j2 src: docker-compose.yaml.j2
dest: /home/nik/homeassistant/docker-compose.yaml dest: /home/nik/homeassistant/docker-compose.yaml
owner: nik owner: nik
@ -21,24 +65,24 @@
mode: "0644" mode: "0644"
- name: Deploy systemd unit - name: Deploy systemd unit
ansible.builtin.template: template:
src: homeassistant.service.j2 src: homeassistant.service.j2
dest: /etc/systemd/system/homeassistant.service dest: /etc/systemd/system/homeassistant.service
mode: "0644" mode: "0644"
notify: Reload systemd notify: reload systemd
- name: Deploy base configuration.yaml - name: Deploy base configuration.yaml
ansible.builtin.template: template:
src: configuration.yaml.j2 src: configuration.yaml.j2
dest: /home/nik/homeassistant/config/configuration.yaml dest: /home/nik/homeassistant/config/configuration.yaml
owner: nik owner: nik
group: nik group: nik
mode: "0644" mode: "0644"
force: false force: no
- name: Enable and start homeassistant - name: Enable and start homeassistant
ansible.builtin.systemd: systemd:
name: homeassistant name: homeassistant
enabled: true enabled: yes
state: started state: started
daemon_reload: true daemon_reload: yes

View File

@ -3,6 +3,6 @@
# Called by: ansible/playbooks/join-debian-agent.yaml # Called by: ansible/playbooks/join-debian-agent.yaml
# Description: Default variables for the k3s-agent role including version, server URL, and join token. # Description: Default variables for the k3s-agent role including version, server URL, and join token.
k3s_version: v1.32.4+k3s1 k3s_version: v1.32.2+k3s1
k3s_server_url: https://192.168.7.77:6443 k3s_server_url: https://192.168.7.77:6443
k3s_node_token: "" k3s_node_token: ""

View File

@ -3,7 +3,7 @@
# Called by: ansible/playbooks/setup-k3s.yaml # Called by: ansible/playbooks/setup-k3s.yaml
# Description: Default variables for the k3s-server role including version, IP, and server configuration. # Description: Default variables for the k3s-server role including version, IP, and server configuration.
k3s_version: v1.32.4+k3s1 k3s_version: v1.32.2+k3s1
k3s_server_ip: 192.168.7.77 k3s_server_ip: 192.168.7.77
k3s_server_config: k3s_server_config:

View File

@ -46,7 +46,6 @@
- name: Print node token - name: Print node token
ansible.builtin.debug: ansible.builtin.debug:
msg: "K3s node token: {{ k3s_node_token }}" msg: "K3s node token: {{ k3s_node_token }}"
when: k3s_show_token | default(false)
- name: Fetch kubeconfig to workstation - name: Fetch kubeconfig to workstation
ansible.builtin.fetch: ansible.builtin.fetch:
@ -56,7 +55,7 @@
- name: Fix kubeconfig server address - name: Fix kubeconfig server address
ansible.builtin.replace: ansible.builtin.replace:
path: "{{ lookup('env', 'HOME') }}/.kube/config" path: /tmp/k3s-minisforum.yaml
regexp: 'https://127\.0\.0\.1:6443' regexp: 'https://127\.0\.0\.1:6443'
replace: "https://{{ k3s_server_ip }}:6443" replace: "https://{{ k3s_server_ip }}:6443"
delegate_to: localhost delegate_to: localhost
@ -68,6 +67,6 @@
creates: /usr/local/bin/helm creates: /usr/local/bin/helm
- name: Label server node as primary - name: Label server node as primary
ansible.builtin.command: ansible.builtin.shell:
cmd: k3s kubectl label node minisforum node-role=primary --overwrite cmd: k3s kubectl label node minisforum node-role=primary --overwrite
changed_when: false changed_when: false

View File

@ -2,7 +2,7 @@
# Part of role: nvidia # Part of role: nvidia
# Called by: ansible/playbooks/setup-gpu-node.yaml # Called by: ansible/playbooks/setup-gpu-node.yaml
# Description: Restarts Docker after nvidia-container-toolkit runtime configuration. # Description: Restarts Docker after nvidia-container-toolkit runtime configuration.
- name: Restart docker - name: restart docker
ansible.builtin.systemd: ansible.builtin.systemd:
name: docker name: docker
state: restarted state: restarted

View File

@ -55,19 +55,11 @@
export LD_LIBRARY_PATH=/usr/local/cuda/lib64:$LD_LIBRARY_PATH export LD_LIBRARY_PATH=/usr/local/cuda/lib64:$LD_LIBRARY_PATH
mode: "0644" mode: "0644"
- name: Check if Docker already has the NVIDIA runtime configured
ansible.builtin.command:
cmd: grep -q '"nvidia"' /etc/docker/daemon.json
register: nvidia_docker_runtime_check
changed_when: false
failed_when: false
- name: Configure Docker runtime for NVIDIA - name: Configure Docker runtime for NVIDIA
ansible.builtin.command: ansible.builtin.command:
cmd: nvidia-ctk runtime configure --runtime=docker cmd: nvidia-ctk runtime configure --runtime=docker
when: nvidia_docker_runtime_check.rc != 0
changed_when: true changed_when: true
notify: Restart docker notify: restart docker
- name: Reboot if driver was just installed - name: Reboot if driver was just installed
ansible.builtin.reboot: ansible.builtin.reboot:

View File

@ -3,13 +3,12 @@
# Called by: ansible/playbooks/setup-ollama.yaml # Called by: ansible/playbooks/setup-ollama.yaml
# ansible/playbooks/setup-gpu-node.yaml # ansible/playbooks/setup-gpu-node.yaml
# Description: Handlers for the ollama role. Restarts ollama on config changes. # Description: Handlers for the ollama role. Restarts ollama on config changes.
- name: Restart ollama - name: restart ollama
become: true become: true
ansible.builtin.command: launchctl kickstart -k system/com.ollama.ollama command: launchctl kickstart -k system/com.ollama.ollama
changed_when: true
when: ansible_facts['system'] == 'Darwin' when: ansible_facts['system'] == 'Darwin'
- name: Restart ollama linux - name: restart ollama linux
ansible.builtin.systemd: ansible.builtin.systemd:
name: ollama name: ollama
state: restarted state: restarted

View File

@ -13,19 +13,19 @@
when: ansible_facts['system'] == 'Darwin' when: ansible_facts['system'] == 'Darwin'
- name: Deploy ollama launchd plist - name: Deploy ollama launchd plist
ansible.builtin.template: template:
src: ollama.plist.j2 src: ollama.plist.j2
dest: /Library/LaunchDaemons/com.ollama.ollama.plist dest: /Library/LaunchDaemons/com.ollama.ollama.plist
owner: root owner: root
group: wheel group: wheel
mode: "0644" mode: "0644"
become: true become: true
notify: Restart ollama notify: restart ollama
when: ansible_facts['system'] == 'Darwin' when: ansible_facts['system'] == 'Darwin'
- name: Load ollama launchd service - name: Load ollama launchd service
become: true become: true
ansible.builtin.command: launchctl load -w /Library/LaunchDaemons/com.ollama.ollama.plist command: launchctl load -w /Library/LaunchDaemons/com.ollama.ollama.plist
args: args:
creates: /var/run/ollama.pid creates: /var/run/ollama.pid
ignore_errors: true ignore_errors: true
@ -54,7 +54,7 @@
group: root group: root
mode: "0644" mode: "0644"
become: true become: true
notify: Restart ollama linux notify: restart ollama linux
when: ansible_facts['system'] == 'Linux' when: ansible_facts['system'] == 'Linux'
- name: Enable and start ollama service - name: Enable and start ollama service
@ -68,7 +68,7 @@
# ── shared ───────────────────────────────────────────────────────────────────── # ── shared ─────────────────────────────────────────────────────────────────────
- name: Wait for ollama to be ready - name: Wait for ollama to be ready
ansible.builtin.uri: uri:
url: "http://localhost:{{ ollama_port }}" url: "http://localhost:{{ ollama_port }}"
status_code: 200 status_code: 200
register: result register: result
@ -77,13 +77,13 @@
delay: 3 delay: 3
- name: Check installed ollama models - name: Check installed ollama models
ansible.builtin.uri: uri:
url: "http://localhost:{{ ollama_port }}/api/tags" url: "http://localhost:{{ ollama_port }}/api/tags"
return_content: true return_content: true
register: ollama_tags register: ollama_tags
- name: Pull ollama models - name: Pull ollama models
ansible.builtin.command: > command: >
{{ '/opt/homebrew/bin/ollama' if ansible_facts['system'] == 'Darwin' else '/usr/local/bin/ollama' }} {{ '/opt/homebrew/bin/ollama' if ansible_facts['system'] == 'Darwin' else '/usr/local/bin/ollama' }}
pull {{ item }} pull {{ item }}
loop: "{{ ollama_models }}" loop: "{{ ollama_models }}"

View File

@ -8,8 +8,8 @@
repo: "{{ watch_party_repo }}" repo: "{{ watch_party_repo }}"
dest: "{{ watch_party_dir }}" dest: "{{ watch_party_dir }}"
version: main version: main
update: true update: yes
accept_hostkey: true accept_hostkey: yes
environment: environment:
GIT_SSL_NO_VERIFY: "true" GIT_SSL_NO_VERIFY: "true"

View File

@ -1,5 +1,5 @@
--- ---
- name: Restart wg0 - name: Restart wg0
ansible.builtin.systemd: systemd:
name: wg-quick@wg0 name: wg-quick@wg0
state: restarted state: restarted

View File

@ -1,6 +1,6 @@
--- ---
- name: Install WireGuard and tools - name: Install WireGuard and tools
ansible.builtin.apt: apt:
name: name:
- wireguard - wireguard
- wireguard-tools - wireguard-tools
@ -9,13 +9,13 @@
update_cache: true update_cache: true
- name: Allow WireGuard port through UFW - name: Allow WireGuard port through UFW
community.general.ufw: ufw:
rule: allow rule: allow
port: "51820" port: "51820"
proto: udp proto: udp
- name: Enable IP forwarding - name: Enable IP forwarding
ansible.posix.sysctl: sysctl:
name: net.ipv4.ip_forward name: net.ipv4.ip_forward
value: "1" value: "1"
sysctl_set: true sysctl_set: true
@ -23,7 +23,7 @@
reload: true reload: true
- name: Create WireGuard config directory - name: Create WireGuard config directory
ansible.builtin.file: file:
path: /etc/wireguard path: /etc/wireguard
state: directory state: directory
mode: "0700" mode: "0700"
@ -32,88 +32,88 @@
# --- Server keypair --- # --- Server keypair ---
- name: Check if server private key exists - name: Check if server private key exists
ansible.builtin.stat: stat:
path: /etc/wireguard/server.key path: /etc/wireguard/server.key
register: server_key_stat register: server_key_stat
- name: Generate server private key - name: Generate server private key
ansible.builtin.shell: wg genkey > /etc/wireguard/server.key shell: wg genkey > /etc/wireguard/server.key
when: not server_key_stat.stat.exists when: not server_key_stat.stat.exists
- name: Set permissions on server private key - name: Set permissions on server private key
ansible.builtin.file: file:
path: /etc/wireguard/server.key path: /etc/wireguard/server.key
mode: "0600" mode: "0600"
owner: root owner: root
group: root group: root
- name: Read server private key - name: Read server private key
ansible.builtin.slurp: slurp:
src: /etc/wireguard/server.key src: /etc/wireguard/server.key
register: server_private_key register: server_private_key
- name: Derive server public key - name: Derive server public key
ansible.builtin.shell: wg pubkey < /etc/wireguard/server.key shell: wg pubkey < /etc/wireguard/server.key
register: server_public_key register: server_public_key
changed_when: false changed_when: false
# --- Phone keypair --- # --- Phone keypair ---
- name: Check if phone private key exists - name: Check if phone private key exists
ansible.builtin.stat: stat:
path: /etc/wireguard/phone.key path: /etc/wireguard/phone.key
register: phone_key_stat register: phone_key_stat
- name: Generate phone private key - name: Generate phone private key
ansible.builtin.shell: wg genkey > /etc/wireguard/phone.key shell: wg genkey > /etc/wireguard/phone.key
when: not phone_key_stat.stat.exists when: not phone_key_stat.stat.exists
- name: Set permissions on phone private key - name: Set permissions on phone private key
ansible.builtin.file: file:
path: /etc/wireguard/phone.key path: /etc/wireguard/phone.key
mode: "0600" mode: "0600"
owner: root owner: root
group: root group: root
- name: Read phone private key - name: Read phone private key
ansible.builtin.slurp: slurp:
src: /etc/wireguard/phone.key src: /etc/wireguard/phone.key
register: phone_private_key register: phone_private_key
- name: Derive phone public key - name: Derive phone public key
ansible.builtin.shell: wg pubkey < /etc/wireguard/phone.key shell: wg pubkey < /etc/wireguard/phone.key
register: phone_public_key register: phone_public_key
changed_when: false changed_when: false
# --- Mac keypair --- # --- Mac keypair ---
- name: Check if mac private key exists - name: Check if mac private key exists
ansible.builtin.stat: stat:
path: /etc/wireguard/mac.key path: /etc/wireguard/mac.key
register: mac_key_stat register: mac_key_stat
- name: Generate mac private key - name: Generate mac private key
ansible.builtin.shell: wg genkey > /etc/wireguard/mac.key shell: wg genkey > /etc/wireguard/mac.key
when: not mac_key_stat.stat.exists when: not mac_key_stat.stat.exists
- name: Set permissions on mac private key - name: Set permissions on mac private key
ansible.builtin.file: file:
path: /etc/wireguard/mac.key path: /etc/wireguard/mac.key
mode: "0600" mode: "0600"
owner: root owner: root
group: root group: root
- name: Read mac private key - name: Read mac private key
ansible.builtin.slurp: slurp:
src: /etc/wireguard/mac.key src: /etc/wireguard/mac.key
register: mac_private_key register: mac_private_key
- name: Derive mac public key - name: Derive mac public key
ansible.builtin.shell: wg pubkey < /etc/wireguard/mac.key shell: wg pubkey < /etc/wireguard/mac.key
register: mac_public_key register: mac_public_key
changed_when: false changed_when: false
# --- Server config --- # --- Server config ---
- name: Write wg0.conf - name: Write wg0.conf
ansible.builtin.template: template:
src: wg0.conf.j2 src: wg0.conf.j2
dest: /etc/wireguard/wg0.conf dest: /etc/wireguard/wg0.conf
mode: "0600" mode: "0600"
@ -123,14 +123,14 @@
# --- Service --- # --- Service ---
- name: Enable and start wg-quick@wg0 - name: Enable and start wg-quick@wg0
ansible.builtin.systemd: systemd:
name: wg-quick@wg0 name: wg-quick@wg0
enabled: true enabled: true
state: started state: started
# --- Phone client config + QR --- # --- Phone client config + QR ---
- name: Write phone client config - name: Write phone client config
ansible.builtin.copy: copy:
dest: /etc/wireguard/phone-client.conf dest: /etc/wireguard/phone-client.conf
mode: "0600" mode: "0600"
owner: root owner: root
@ -149,7 +149,7 @@
# --- Mac client config --- # --- Mac client config ---
- name: Write mac client config - name: Write mac client config
ansible.builtin.copy: copy:
dest: /etc/wireguard/mac-client.conf dest: /etc/wireguard/mac-client.conf
mode: "0600" mode: "0600"
owner: root owner: root
@ -167,23 +167,19 @@
PersistentKeepalive = 25 PersistentKeepalive = 25
- name: Display mac client config - name: Display mac client config
ansible.builtin.shell: cat /etc/wireguard/mac-client.conf shell: cat /etc/wireguard/mac-client.conf
register: mac_conf register: mac_conf
changed_when: false changed_when: false
when: wireguard_show_client_configs | default(false)
- name: Show mac client config - name: Show mac client config
ansible.builtin.debug: debug:
msg: "{{ mac_conf.stdout_lines }}" msg: "{{ mac_conf.stdout_lines }}"
when: wireguard_show_client_configs | default(false)
- name: Generate QR code for phone - name: Generate QR code for phone
ansible.builtin.shell: qrencode -t ansiutf8 < /etc/wireguard/phone-client.conf shell: qrencode -t ansiutf8 < /etc/wireguard/phone-client.conf
register: phone_qr register: phone_qr
changed_when: false changed_when: false
when: wireguard_show_client_configs | default(false)
- name: Display phone QR code - name: Display phone QR code
ansible.builtin.debug: debug:
msg: "{{ phone_qr.stdout_lines }}" msg: "{{ phone_qr.stdout_lines }}"
when: wireguard_show_client_configs | default(false)

View File

@ -1,20 +0,0 @@
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
name: authentik-config
namespace: argocd
spec:
project: default
source:
repoURL: https://gitea.nik4nao.com/nik/homelab.git
targetRevision: main
path: manifests/authentik
directory:
include: '*.yaml'
destination:
server: https://kubernetes.default.svc
namespace: authentik
syncPolicy:
automated:
prune: false
selfHeal: true

View File

@ -1,33 +0,0 @@
# NEEDS YOUR INPUT before first sync: targetRevision below is a placeholder.
# Run `helm list -n authentik` on the live cluster and set this to the chart
# version actually deployed (installed by hand per the header comment in
# values/authentik.yaml) -- Authentik is the SSO IdP gating Argo CD/Grafana/
# Gitea logins, so adopting it into GitOps with the wrong version could
# trigger an unwanted live chart upgrade/downgrade on first sync. Sync is
# left manual (no `automated:` block) until you've confirmed this and diffed
# with `kubectl diff` / `argocd app diff`.
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
name: authentik
namespace: argocd
annotations:
argocd.argoproj.io/sync-wave: "-1"
spec:
project: default
sources:
- repoURL: https://charts.goauthentik.io
chart: authentik
targetRevision: "REPLACE_ME"
helm:
valueFiles:
- $values/values/authentik.yaml
- repoURL: https://gitea.nik4nao.com/nik/homelab.git
targetRevision: main
ref: values
destination:
server: https://kubernetes.default.svc
namespace: authentik
syncPolicy:
syncOptions:
- CreateNamespace=true

View File

@ -1,22 +0,0 @@
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
name: cert-manager-config
namespace: argocd
annotations:
argocd.argoproj.io/sync-wave: "-1"
spec:
project: default
source:
repoURL: https://gitea.nik4nao.com/nik/homelab.git
targetRevision: main
path: manifests/cert-manager
directory:
include: '*.yaml'
destination:
server: https://kubernetes.default.svc
namespace: cert-manager
syncPolicy:
automated:
prune: false
selfHeal: true

View File

@ -3,8 +3,6 @@ kind: Application
metadata: metadata:
name: cert-manager name: cert-manager
namespace: argocd namespace: argocd
annotations:
argocd.argoproj.io/sync-wave: "-2"
spec: spec:
project: default project: default
sources: sources:

View File

@ -9,14 +9,14 @@ spec:
project: default project: default
source: source:
repoURL: https://gitea.nik4nao.com/nik/homelab.git repoURL: https://gitea.nik4nao.com/nik/homelab.git
targetRevision: main targetRevision: HEAD
path: manifests/home-services path: manifests/home-services
destination: destination:
server: https://kubernetes.default.svc server: https://kubernetes.default.svc
namespace: home-services namespace: home-services
syncPolicy: syncPolicy:
automated: automated:
prune: false prune: true
selfHeal: true selfHeal: true
syncOptions: syncOptions:
- CreateNamespace=true - CreateNamespace=true

View File

@ -11,8 +11,10 @@ spec:
path: manifests/homeassistant path: manifests/homeassistant
destination: destination:
server: https://kubernetes.default.svc server: https://kubernetes.default.svc
namespace: default namespace: homeassistant
syncPolicy: syncPolicy:
automated: automated:
prune: false prune: false
selfHeal: true selfHeal: true
syncOptions:
- CreateNamespace=true

View File

@ -1,23 +0,0 @@
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
name: network
namespace: argocd
spec:
project: default
source:
repoURL: https://gitea.nik4nao.com/nik/homelab.git
targetRevision: main
path: manifests/network
directory:
exclude: '*-sealed.yaml'
include: '*.yaml'
destination:
server: https://kubernetes.default.svc
namespace: pihole
syncPolicy:
automated:
prune: false
selfHeal: true
syncOptions:
- CreateNamespace=true

View File

@ -14,14 +14,14 @@ spec:
valueFiles: valueFiles:
- $values/values/otel-collector.yaml - $values/values/otel-collector.yaml
- repoURL: https://gitea.nik4nao.com/nik/homelab.git - repoURL: https://gitea.nik4nao.com/nik/homelab.git
targetRevision: main targetRevision: HEAD
ref: values ref: values
destination: destination:
server: https://kubernetes.default.svc server: https://kubernetes.default.svc
namespace: monitoring namespace: monitoring
syncPolicy: syncPolicy:
automated: automated:
prune: false prune: true
selfHeal: true selfHeal: true
syncOptions: syncOptions:
- CreateNamespace=false - CreateNamespace=false

View File

@ -21,6 +21,6 @@ spec:
syncPolicy: syncPolicy:
automated: automated:
prune: false prune: false
selfHeal: true selfHeal: false
syncOptions: syncOptions:
- CreateNamespace=true - CreateNamespace=true

View File

@ -21,6 +21,6 @@ spec:
syncPolicy: syncPolicy:
automated: automated:
prune: false prune: false
selfHeal: true selfHeal: false
syncOptions: syncOptions:
- CreateNamespace=true - CreateNamespace=true

View File

@ -3,8 +3,6 @@ kind: Application
metadata: metadata:
name: sealed-secrets name: sealed-secrets
namespace: argocd namespace: argocd
annotations:
argocd.argoproj.io/sync-wave: "-2"
spec: spec:
project: default project: default
sources: sources:

View File

@ -14,14 +14,14 @@ spec:
valueFiles: valueFiles:
- $values/values/tempo.yaml - $values/values/tempo.yaml
- repoURL: https://gitea.nik4nao.com/nik/homelab.git - repoURL: https://gitea.nik4nao.com/nik/homelab.git
targetRevision: main targetRevision: HEAD
ref: values ref: values
destination: destination:
server: https://kubernetes.default.svc server: https://kubernetes.default.svc
namespace: monitoring namespace: monitoring
syncPolicy: syncPolicy:
automated: automated:
prune: false prune: true
selfHeal: true selfHeal: true
syncOptions: syncOptions:
- CreateNamespace=false - CreateNamespace=false

View File

@ -1,5 +1,5 @@
# Config for: Dashy dashboard # Config for: Dashy dashboard
# Applied by: manifests/core/apply-dashy-config.sh (renders secrets from .env, creates ConfigMap from this file) # Applied by: manifests/apply-dashy-config.sh (creates ConfigMap from this file)
pageInfo: pageInfo:
title: Good morning, Nik title: Good morning, Nik
@ -60,14 +60,14 @@ sections:
id: 1_928_iframe id: 1_928_iframe
- type: weather - type: weather
options: options:
apiKey: "${DASHY_WEATHER_API_KEY}" apiKey: 770d36389dd547e8e3daccb39debde64
city: Tokyo, JP city: Tokyo, JP
units: metric units: metric
hideDetails: false hideDetails: false
id: 2_928_weather id: 2_928_weather
- type: weather-forecast - type: weather-forecast
options: options:
apiKey: "${DASHY_WEATHER_API_KEY}" apiKey: 770d36389dd547e8e3daccb39debde64
city: Tokyo, JP city: Tokyo, JP
numDays: 5 numDays: 5
units: metric units: metric

View File

@ -9,7 +9,7 @@ metadata:
spec: spec:
secretName: authentik-tls secretName: authentik-tls
issuerRef: issuerRef:
name: internal-ca-issuer name: internal-ca
kind: ClusterIssuer kind: ClusterIssuer
dnsNames: dnsNames:
- auth.home.arpa - auth.home.arpa

View File

@ -41,23 +41,6 @@ spec:
name: http name: http
- containerPort: 9443 - containerPort: 9443
name: https name: https
readinessProbe:
tcpSocket:
port: 9000
initialDelaySeconds: 10
periodSeconds: 15
livenessProbe:
tcpSocket:
port: 9000
initialDelaySeconds: 20
periodSeconds: 30
resources:
requests:
cpu: 20m
memory: 64Mi
limits:
cpu: 200m
memory: 256Mi
--- ---
apiVersion: v1 apiVersion: v1
kind: Service kind: Service

View File

@ -1,16 +1,10 @@
#!/usr/bin/env bash #!/usr/bin/env bash
# Usage: bash manifests/core/apply-dashy-config.sh # Usage: bash manifests/core/apply-dashy-config.sh
# Description: Renders config/dashy/conf.yaml with secrets from .env, updates the Dashy ConfigMap, and restarts the deployment # Description: Updates the Dashy ConfigMap from config/dashy/conf.yaml and restarts the deployment
set -euo pipefail set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
source "$SCRIPT_DIR/../../.env"
RENDERED="$(mktemp)"
trap 'rm -f "$RENDERED"' EXIT
sed "s|\${DASHY_WEATHER_API_KEY}|${DASHY_WEATHER_API_KEY}|g" "$SCRIPT_DIR/../../config/dashy/conf.yaml" > "$RENDERED"
kubectl create configmap dashy-config \ kubectl create configmap dashy-config \
--from-file=conf.yml="$RENDERED" \ --from-file=conf.yml=config/dashy/conf.yaml \
--namespace dashy \ --namespace dashy \
--dry-run=client -o yaml | kubectl apply -f - --dry-run=client -o yaml | kubectl apply -f -

View File

@ -63,25 +63,6 @@ spec:
image: nginx:alpine image: nginx:alpine
ports: ports:
- containerPort: 80 - containerPort: 80
readinessProbe:
httpGet:
path: /
port: 80
initialDelaySeconds: 5
periodSeconds: 10
livenessProbe:
httpGet:
path: /
port: 80
initialDelaySeconds: 10
periodSeconds: 30
resources:
requests:
cpu: 10m
memory: 32Mi
limits:
cpu: 100m
memory: 64Mi
volumeMounts: volumeMounts:
- name: web-files - name: web-files
mountPath: /usr/share/nginx/html/index.html mountPath: /usr/share/nginx/html/index.html

View File

@ -43,25 +43,6 @@ spec:
image: lissy93/dashy:latest image: lissy93/dashy:latest
ports: ports:
- containerPort: 8080 - containerPort: 8080
readinessProbe:
httpGet:
path: /
port: 8080
initialDelaySeconds: 10
periodSeconds: 10
livenessProbe:
httpGet:
path: /
port: 8080
initialDelaySeconds: 20
periodSeconds: 30
resources:
requests:
cpu: 50m
memory: 128Mi
limits:
cpu: 200m
memory: 256Mi
volumeMounts: volumeMounts:
- name: config - name: config
mountPath: /app/user-data/conf.yml mountPath: /app/user-data/conf.yml

View File

@ -34,23 +34,6 @@ spec:
value: "-w" value: "-w"
securityContext: securityContext:
privileged: true privileged: true
readinessProbe:
tcpSocket:
port: 61208
initialDelaySeconds: 10
periodSeconds: 15
livenessProbe:
tcpSocket:
port: 61208
initialDelaySeconds: 20
periodSeconds: 30
resources:
requests:
cpu: 50m
memory: 128Mi
limits:
cpu: 300m
memory: 256Mi
volumeMounts: volumeMounts:
- name: host-proc - name: host-proc
mountPath: /proc mountPath: /proc

View File

@ -2,21 +2,15 @@
# Delete: kubectl delete -f manifests/gitea/gitea-backup.yaml # Delete: kubectl delete -f manifests/gitea/gitea-backup.yaml
# Description: CronJob that backs up Gitea to NFS every 7 days, with RBAC and PV/PVC. # Description: CronJob that backs up Gitea to NFS every 7 days, with RBAC and PV/PVC.
apiVersion: v1 apiVersion: v1
kind: Namespace
metadata:
name: gitea-backup
---
apiVersion: v1
kind: ServiceAccount kind: ServiceAccount
metadata: metadata:
name: gitea-backup name: gitea-backup
namespace: gitea-backup namespace: gitea-backup
--- ---
apiVersion: rbac.authorization.k8s.io/v1 apiVersion: rbac.authorization.k8s.io/v1
kind: Role kind: ClusterRole
metadata: metadata:
name: gitea-backup name: gitea-backup
namespace: gitea
rules: rules:
- apiGroups: [""] - apiGroups: [""]
resources: ["pods"] resources: ["pods"]
@ -26,16 +20,15 @@ rules:
verbs: ["create"] verbs: ["create"]
--- ---
apiVersion: rbac.authorization.k8s.io/v1 apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding kind: ClusterRoleBinding
metadata: metadata:
name: gitea-backup name: gitea-backup
namespace: gitea
subjects: subjects:
- kind: ServiceAccount - kind: ServiceAccount
name: gitea-backup name: gitea-backup
namespace: gitea-backup namespace: gitea-backup
roleRef: roleRef:
kind: Role kind: ClusterRole
name: gitea-backup name: gitea-backup
apiGroup: rbac.authorization.k8s.io apiGroup: rbac.authorization.k8s.io
--- ---

View File

@ -1,16 +0,0 @@
#!/usr/bin/env bash
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
source "$SCRIPT_DIR/../../.env"
kubectl create secret generic gitea-postgres-secret \
--namespace=gitea \
--from-literal=postgresql-password="${GITEA_POSTGRES_PASSWORD}" \
--dry-run=client -o yaml \
| kubeseal \
--controller-namespace=kube-system \
--controller-name=sealed-secrets-controller \
--format yaml \
> "$SCRIPT_DIR/gitea-postgres-sealed.yaml"
echo "Wrote $SCRIPT_DIR/gitea-postgres-sealed.yaml"

View File

@ -88,23 +88,6 @@ spec:
value: "--data-checksums" value: "--data-checksums"
- name: PGDATA - name: PGDATA
value: /var/lib/postgresql/data/pgdata value: /var/lib/postgresql/data/pgdata
readinessProbe:
tcpSocket:
port: 5432
initialDelaySeconds: 10
periodSeconds: 15
livenessProbe:
tcpSocket:
port: 5432
initialDelaySeconds: 20
periodSeconds: 30
resources:
requests:
cpu: 100m
memory: 512Mi
limits:
cpu: 1000m
memory: 1Gi
volumeMounts: volumeMounts:
- name: data - name: data
mountPath: /var/lib/postgresql/data mountPath: /var/lib/postgresql/data
@ -146,23 +129,6 @@ spec:
image: docker.io/redis:6.2-alpine image: docker.io/redis:6.2-alpine
ports: ports:
- containerPort: 6379 - containerPort: 6379
readinessProbe:
tcpSocket:
port: 6379
initialDelaySeconds: 5
periodSeconds: 10
livenessProbe:
tcpSocket:
port: 6379
initialDelaySeconds: 10
periodSeconds: 30
resources:
requests:
cpu: 10m
memory: 32Mi
limits:
cpu: 200m
memory: 128Mi
volumeMounts: volumeMounts:
- name: data - name: data
mountPath: /data mountPath: /data
@ -223,23 +189,6 @@ spec:
value: http://immich-machine-learning:3003 value: http://immich-machine-learning:3003
- name: TZ - name: TZ
value: Asia/Tokyo value: Asia/Tokyo
readinessProbe:
tcpSocket:
port: 2283
initialDelaySeconds: 15
periodSeconds: 15
livenessProbe:
tcpSocket:
port: 2283
initialDelaySeconds: 30
periodSeconds: 30
resources:
requests:
cpu: 200m
memory: 2Gi
limits:
cpu: 2000m
memory: 4Gi
volumeMounts: volumeMounts:
- name: library - name: library
mountPath: /usr/src/app/upload mountPath: /usr/src/app/upload
@ -295,23 +244,6 @@ spec:
image: ghcr.io/immich-app/immich-machine-learning:v2.7.5 image: ghcr.io/immich-app/immich-machine-learning:v2.7.5
ports: ports:
- containerPort: 3003 - containerPort: 3003
readinessProbe:
tcpSocket:
port: 3003
initialDelaySeconds: 15
periodSeconds: 15
livenessProbe:
tcpSocket:
port: 3003
initialDelaySeconds: 30
periodSeconds: 30
resources:
requests:
cpu: 200m
memory: 512Mi
limits:
cpu: 2000m
memory: 2Gi
volumeMounts: volumeMounts:
- name: cache - name: cache
mountPath: /cache mountPath: /cache

View File

@ -82,25 +82,6 @@ spec:
value: "1000" value: "1000"
- name: TZ - name: TZ
value: "Asia/Tokyo" value: "Asia/Tokyo"
readinessProbe:
httpGet:
path: /
port: 5800
initialDelaySeconds: 15
periodSeconds: 15
livenessProbe:
httpGet:
path: /
port: 5800
initialDelaySeconds: 30
periodSeconds: 30
resources:
requests:
cpu: 200m
memory: 512Mi
limits:
cpu: 1000m
memory: 1Gi
volumeMounts: volumeMounts:
- name: config - name: config
mountPath: /config mountPath: /config
@ -110,13 +91,6 @@ spec:
image: python:3.12-alpine image: python:3.12-alpine
ports: ports:
- containerPort: 9666 - containerPort: 9666
resources:
requests:
cpu: 5m
memory: 16Mi
limits:
cpu: 50m
memory: 64Mi
volumeMounts: volumeMounts:
- name: config - name: config
mountPath: /config mountPath: /config

View File

@ -93,25 +93,6 @@ spec:
value: https://jellyfin.home.arpa value: https://jellyfin.home.arpa
- name: LIBVA_DRIVER_NAME - name: LIBVA_DRIVER_NAME
value: radeonsi value: radeonsi
readinessProbe:
httpGet:
path: /health
port: 8096
initialDelaySeconds: 15
periodSeconds: 15
livenessProbe:
httpGet:
path: /health
port: 8096
initialDelaySeconds: 30
periodSeconds: 30
resources:
requests:
cpu: 200m
memory: 1Gi
limits:
cpu: 2000m
memory: 4Gi
volumeMounts: volumeMounts:
- name: config - name: config
mountPath: /config mountPath: /config

View File

@ -103,25 +103,6 @@ spec:
value: "Asia/Tokyo" value: "Asia/Tokyo"
- name: WEBUI_PORT - name: WEBUI_PORT
value: "8080" value: "8080"
readinessProbe:
httpGet:
path: /
port: 8080
initialDelaySeconds: 15
periodSeconds: 15
livenessProbe:
httpGet:
path: /
port: 8080
initialDelaySeconds: 30
periodSeconds: 30
resources:
requests:
cpu: 50m
memory: 128Mi
limits:
cpu: 500m
memory: 512Mi
volumeMounts: volumeMounts:
- name: config - name: config
mountPath: /config mountPath: /config
@ -131,13 +112,6 @@ spec:
image: python:3-alpine image: python:3-alpine
ports: ports:
- containerPort: 8888 - containerPort: 8888
resources:
requests:
cpu: 5m
memory: 16Mi
limits:
cpu: 50m
memory: 64Mi
command: command:
- python3 - python3
- -c - -c

View File

@ -63,7 +63,7 @@ metadata:
spec: spec:
secretName: traefik-dashboard-tls secretName: traefik-dashboard-tls
issuerRef: issuerRef:
name: internal-ca-issuer name: internal-ca
kind: ClusterIssuer kind: ClusterIssuer
dnsNames: dnsNames:
- traefik.home.arpa - traefik.home.arpa

View File

@ -45,12 +45,9 @@ postgresql:
global: global:
postgresql: postgresql:
auth: auth:
password: gitea-db-password
username: gitea username: gitea
database: gitea database: gitea
existingSecret: gitea-postgres-secret
secretKeys:
adminPasswordKey: postgresql-password
userPasswordKey: postgresql-password
service: service:
ssh: ssh:

View File

@ -37,6 +37,7 @@ dnsmasq:
- address=/gitea.home.arpa/192.168.7.77 - address=/gitea.home.arpa/192.168.7.77
- address=/pihole.home.arpa/192.168.7.77 - address=/pihole.home.arpa/192.168.7.77
- address=/home.arpa/192.168.7.77 - address=/home.arpa/192.168.7.77
- address=/nik4nao.home.arpa/192.168.7.183
- address=/dashy.home.arpa/192.168.7.77 - address=/dashy.home.arpa/192.168.7.77
- address=/jellyfin.home.arpa/192.168.7.77 - address=/jellyfin.home.arpa/192.168.7.77
- address=/qbittorrent.home.arpa/192.168.7.77 - address=/qbittorrent.home.arpa/192.168.7.77
@ -46,11 +47,9 @@ dnsmasq:
- address=/ca.home.arpa/192.168.7.77 - address=/ca.home.arpa/192.168.7.77
- address=/auth.home.arpa/192.168.7.77 - address=/auth.home.arpa/192.168.7.77
- address=/traefik.home.arpa/192.168.7.77 - address=/traefik.home.arpa/192.168.7.77
- address=/immich.home.arpa/192.168.7.77
- address=/gitea.nik4nao.com/192.168.7.77 - address=/gitea.nik4nao.com/192.168.7.77
- address=/ha.home.arpa/192.168.7.77 - address=/ha.home.arpa/192.168.7.77
- address=/argocd.home.arpa/192.168.7.77 - address=/argocd.home.arpa/192.168.7.77
- address=/gluetun.home.arpa/192.168.7.77
persistentVolumeClaim: persistentVolumeClaim:
enabled: true enabled: true

View File

@ -68,6 +68,7 @@ dnsmasq:
- address=/gitea.home.arpa/192.168.7.77 - address=/gitea.home.arpa/192.168.7.77
- address=/pihole.home.arpa/192.168.7.77 - address=/pihole.home.arpa/192.168.7.77
- address=/home.arpa/192.168.7.77 - address=/home.arpa/192.168.7.77
- address=/nik4nao.home.arpa/192.168.7.183
- address=/dashy.home.arpa/192.168.7.77 - address=/dashy.home.arpa/192.168.7.77
- address=/jellyfin.home.arpa/192.168.7.77 - address=/jellyfin.home.arpa/192.168.7.77
- address=/qbittorrent.home.arpa/192.168.7.77 - address=/qbittorrent.home.arpa/192.168.7.77