Commit Graph
4354 Commits
Author SHA1 Message Date
k.renner 1cef24475c Merge pull request 'docs: document SOPS age-key and Ansible Vault password backup procedure' (#228) from fix/issue-210 into master 2026-07-14 19:08:01 +00:00
security-bot e3286565e4 chore(image-automation): bump gitea.renner.uno/k.renner/shelly-manager:build.1784054477
Updates managed by image-automation-controller.
2026-07-14 18:42:59 +00:00
security-bot a551909f40 chore(image-automation): bump gitea.renner.uno/k.renner/shelly-manager:build.1784053385
Updates managed by image-automation-controller.
2026-07-14 18:24:42 +00:00
k.renner 37e364c58f Merge pull request 'fix(gatus): trim public status page to stop internal topology disclosure (Fixes #206)' (#243) from fix/issue-206 into master 2026-07-14 18:24:04 +00:00
claude-botandClaude Opus 4.8 db04f6f1c3 fix(gatus): trim public status page to stop internal topology disclosure (#206)
status.renner.uno / status.k.renner.uno is a public, unauthenticated gatus
page. Its endpoints named the internal service estate (Authentik, Gitea,
Immich, Grafana, HA, Paperless, Proxmox, PBS, UDM DNS, Mosquitto) and gatus
exposed each check's target hostname/URL/port — and on failure the error
text (e.g. mosquitto.home-assistant.svc.cluster.local, 10.2.40.1) — via the
public results and /api/v1/endpoints/statuses API (all hide-* default false).

Per owner decision keep the page public (not Authentik-gated) but scrub the
disclosure:
- rename every endpoint to an opaque label (Web Service N / Infrastructure N),
  removing product/vendor/hostname identifiers from the public `name`
- add a per-endpoint ui block with hide-hostname/hide-url/hide-port/
  hide-conditions/hide-errors: true so gatus scrubs targets server-side

All checks keep running unchanged (real targets stay in `url`, visible only
to git/cluster operators). Public page now shows group + opaque label +
up/down + response time only. Maintainer opaque->real map kept as a comment.

Fixes #206

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-14 20:21:56 +02:00
k.renner f9c5d53ec7 Merge pull request 'fix(ansible): restrict woodpecker-ci SSH key to pod network, drop dead ci-new key (Fixes #184)' (#242) from fix/issue-184 into master 2026-07-14 18:18:25 +00:00
claude-botandClaude Opus 4.8 94c6d5d71e fix(ansible): restrict woodpecker-ci SSH key to the pod network, drop dead ci-new key
The CI automation keys were deployed to root's (and ladmin's) authorized_keys
on every host with no key_options, giving any holder of a CI key unrestricted
root fleet-wide (issue #184).

- Restrict woodpecker-ci@homelab with from="10.244.0.0/16,fd10:244::/48" — a
  network-of-origin restriction to the cluster pod CIDRs, so the key only
  authenticates from within the CI's network segment. Determined by inspecting
  Cilium: routing-mode=tunnel with enable-ipv4-masquerade=true but
  ipv4-native-routing-cidr=10.0.0.0/8 (and v6 fd00::/8), which covers every
  10.2.x / fd.. LAN SSH target, so pod egress to the fleet is NOT SNAT'd to the
  node IP — pods present their real POD IP, BGP-advertised to the UDM
  (cilium-bgp.yaml). The stable boundary is therefore the pod subnet
  (talos cluster.yaml podSubnets: 10.244.0.0/16 + fd10:244::/48), which
  survives node DHCP-lease churn and pod rescheduling across k8s-1/2/3 — unlike
  pinning any node's current (DHCP) IP. Value lives in group_vars/all/vars.yml
  (ci_ssh_from_cidrs) as the single source of truth.
- Remove ansible-runner@ci-new entirely from the play, the common role, and
  common_ssh_public_keys — dead key, no such host exists anywhere in the repo.
- Keep krenner@fortress (human) unrestricted.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-14 20:13:40 +02:00
k.renner bab4a62ced Merge pull request 'fix(velero): remove dead bootstrap-restore Job (Fixes #200)' (#241) from fix/issue-200 into master 2026-07-14 18:10:35 +00:00
security-bot 09aca0404c chore(image-automation): bump gitea.renner.uno/k.renner/shelly-manager:build.1784052517
Updates managed by image-automation-controller.
2026-07-14 18:10:25 +00:00
claude-botandClaude Opus 4.8 952e61507f fix(velero): remove dead bootstrap-restore Job (Fixes #200)
The velero-bootstrap-restore Job (restore-job.yaml + its RBAC) was never
wired into any Flux Kustomization — no `path` field in
kubernetes/clusters/homelab/ referenced it, and the parent
infrastructure/kustomization.yaml only aggregates `velero/`, not
`velero-bootstrap-restore/`. It never ran, creating false confidence that
app-data auto-restore-on-bootstrap existed.

Per owner decision on #200: delete it and rely on the documented manual
restore procedure in kubernetes/RUNBOOK-cold-rebuild.md. The real seed
path is the seed-* Kustomizations (rsync from legacy host) plus the
per-app restore steps in the runbook.

Also updates two now-dangling doc comments in
post-crds/velero-schedules.yaml that referenced the deleted Job's
SCHEDULES mechanism, pointing them at the runbook instead.

kubectl kustomize on infrastructure/ and infrastructure/post-crds/ both
build clean after removal.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-14 20:07:21 +02:00
k.renner ab018cc253 Merge pull request 'fix(bootstrap): break Flux<->Gitea circular bootstrap (Fixes #187)' (#226) from fix/issue-187-flux-gitea-bootstrap into master 2026-07-14 17:54:39 +00:00
claude-botandClaude Opus 4.8 2aa0989a8a fix(bootstrap): wire GitHub break-glass source to break Flux<->Gitea loop
Flux pulls manifests from the in-cluster Gitea, which is itself Flux-managed
(Ceph data, in-cluster DNS+Traefik), so a cold rebuild has no entry point.
Give it one:

- bootstrap.sh: FLUX_SOURCE=github selects 'flux bootstrap github' against the
  mirror (github.com/konairius/Homelab, branch master) instead of
  'flux bootstrap gitea'; defaults to gitea so steady-state is unchanged.
- kubernetes/RUNBOOK-cold-rebuild.md: single tofu->talos->flux->openbao->apps
  runbook, with the break-glass github bootstrap and the manual OpenBao
  snapshot-restore+unseal step wired in.

The GitHub mirror is already kept current by Gitea's own native push-mirror
(server-side, sync_on_commit=true) — no setup and no new credential required;
the runbook just references it. OpenBao restore stays manual (unseal key out
of CI, in pass).

Refs #187

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-14 19:53:28 +02:00
security-bot 6f87b2640a chore(image-automation): bump gitea.renner.uno/k.renner/shelly-manager:build.1784051271
Updates managed by image-automation-controller.
2026-07-14 17:49:51 +00:00
k.renner 8ab5282b82 Merge pull request 'fix(opentofu): remove retired Gitea/Woodpecker LXC state drift (Fixes #199)' (#232) from fix/issue-199-tofu-state-drift into master
ci/woodpecker/push/deploy-infrastructure Pipeline failed
2026-07-14 17:41:09 +00:00
k.renner 22320f0f7c Merge pull request 'fix(openbao): add crowdsec to ClusterSecretStore namespaceSelector' (#240) from fix/openbao-css-crowdsec into master 2026-07-14 17:11:00 +00:00
security-bot c03df26203 chore(image-automation): bump gitea.renner.uno/k.renner/shelly-manager:build.1784048946
Updates managed by image-automation-controller.
2026-07-14 17:10:43 +00:00
claude-botandClaude Opus 4.8 82918c86a7 fix(openbao): add crowdsec to ClusterSecretStore namespaceSelector
PR #231 scoped the openbao ClusterSecretStore to the 12 namespaces that
had ExternalSecrets at the time. PR #236 then added a new consumer in the
crowdsec namespace (the bouncer key), which the selector refused
(SecretSyncedError). Add crowdsec to the allowed namespaces.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-14 19:10:37 +02:00
k.renner 98b1b1a153 Merge pull request 'fix(crowdsec): move bouncer key from cleartext Secret to OpenBao ExternalSecret' (#236) from fix/issue-212 into master 2026-07-14 17:04:47 +00:00
k.renner b1dec3e764 Merge pull request 'fix(ci): sandbox update-audit agent against prompt-injection exfil' (#238) from fix/issue-197 into master 2026-07-14 15:27:21 +00:00
k.renner 0a53419cd6 Merge pull request 'fix(openbao): harden secrets backend (prune-guard, versioned policy, store scoping, egress) — Fixes #202' (#231) from fix/issue-202-openbao-hardening into master 2026-07-14 15:25:36 +00:00
security-bot 55e444a8c4 chore(image-automation): bump gitea.renner.uno/k.renner/shelly-manager:build.1784042235
Updates managed by image-automation-controller.
2026-07-14 15:18:50 +00:00
k.renner 1e0af78a47 Merge pull request 'fix(traefik): add security-headers + rate-limit Middlewares as entrypoint defaults' (#229) from fix/issue-205 into master 2026-07-14 15:13:56 +00:00
k.renner 50f4cd0959 Merge pull request 'decommission test-browser (KasmWeb): remove app per owner directive' (#218) from fix/issue-195 into master 2026-07-14 15:11:08 +00:00
k.renner fa99b9081a Merge pull request 'fix(velero): add apps-daily schedule covering Gitea/Woodpecker PVCs' (#223) from fix/issue-186 into master 2026-07-14 15:08:23 +00:00
security-bot daf4c232b8 chore(image-automation): bump quay.io/ceph/ceph:v19.2.5
Updates managed by image-automation-controller.
2026-07-14 14:14:10 +00:00
k.renner 209be02796 Merge pull request 'Fixes #208 (partial): remove confirmed-dead stale artifacts' (#221) from fix/issue-208-stale-artifacts into master 2026-07-14 11:05:46 +00:00
claude-bot ab3c4581d5 fix(velero): add apps-daily schedule covering Gitea/Woodpecker PVCs
The apps namespace (Gitea, Woodpecker, gatus) had no Velero schedule at
all, so the in-cluster Gitea (data-gitea-0, the Flux GitOps source of
truth) and Woodpecker (data-woodpecker-server-0) PVCs migrated from the
legacy LXCs were unprotected. The only PBS coverage (opentofu/backup_jobs.tf
VMID 114/231) targets the original LXCs, not the live cluster PVCs.

Add an apps-daily Schedule (06:45 local, next free stagger slot) mirroring
the existing PVC-only pattern; no CNPG cluster runs in this namespace so no
labelSelector exclusion is needed. Also register it in the bootstrap-restore
Job's SCHEDULES list so a fresh-cluster bootstrap restores it like the
others.

Fixes #186

Co-Authored-By: Claude Bot <claude-bot@renner.uno>
2026-07-14 13:04:28 +02:00
k.renner 53d08c3e1c Merge pull request 'fix(network-policies): default-deny CNP for external-secrets namespace' (#237) from fix/issue-194 into master 2026-07-14 11:03:33 +00:00
k.renner 331351a4b0 Merge pull request 'fix(velero): add home-assistant-daily Schedule for 9 uncovered PVCs' (#217) from fix/issue-192 into master 2026-07-14 11:00:25 +00:00
k.renner 0dc687143a Merge pull request 'fix(image-automation): bound the netbird-ingress semver range' (#222) from fix/issue-204 into master 2026-07-14 10:58:26 +00:00
k.renner 2198c61d88 Merge pull request 'feat(k8s): PodDisruptionBudgets for single-replica stateful workloads' (#234) from fix/issue-201 into master 2026-07-14 10:56:10 +00:00
security-bot 7a12ae1198 chore(image-automation): bump gitea.renner.uno/k.renner/shelly-manager:build.1784026451
Updates managed by image-automation-controller.
2026-07-14 10:55:52 +00:00
k.renner 5681da8ccd Merge pull request 'fix(ci): pin base image digests, verify tool checksums, pin claude-code version' (#233) from fix/issue-209 into master
ci/woodpecker/push/build-ci-images Pipeline was successful
2026-07-14 10:54:49 +00:00
k.renner 35b96f8a49 Merge pull request 'fix(certificates): remove unverified authentik cert fetch into system CA trust' (#230) from fix/issue-185 into master 2026-07-14 10:53:26 +00:00
k.renner e94a92e9bb Merge pull request 'chore(ansible): delete decommissioned legacy roles (immich, grafana-observability, keepalived, postgres-simple-ha)' (#225) from fix/issue-198 into master 2026-07-14 10:51:21 +00:00
k.renner ecf4c7f492 Merge pull request 'fix(ansible): guard archived destructive plays with confirm_destroy' (#219) from fix/issue-207 into master 2026-07-14 10:51:08 +00:00
k.renner 3e84d07d40 Merge pull request 'fix(ci): gate plan/deploy pipelines to master, close secret-exfil path' (#215) from fix/issue-196 into master 2026-07-14 10:50:58 +00:00
k.renner e435415ca1 Merge pull request 'feat(shelly-manager): SA + RBAC for ESPHome firmware pipeline' (#214) from feat/shelly-manager-firmware-rbac into master
Reviewed-on: gitea_admin/homelab#214
2026-07-14 10:50:57 +00:00
k.renner 22369b59f3 Merge pull request 'fix(opentofu): remove tracked tfplan file and add to .gitignore' (#220) from fix/issue-213 into master
ci/woodpecker/push/deploy-infrastructure Pipeline failed
2026-07-14 10:47:24 +00:00
claude-bot 82292ee479 security(cilium): stop committing CA + Hubble private keys; generate in-cluster
The Talos bootstrap inline manifest (patches/cilium-cni.yaml) and its
standalone copy (cilium.yaml) embedded Secret/cilium-ca (RSA ca.key) and
Secret/hubble-server-certs (RSA tls.key) as plaintext base64 — rendered
Helm output from hubble.tls.auto.method=helm. Anyone with repo/history
read access could mint certs under the Cilium CA and impersonate/decrypt
the Hubble mTLS channel.

Switch to hubble.tls.auto.method=cronJob: replace the two committed-key
Secrets with the certgen ServiceAccount/Role/RoleBinding/Job/CronJob.
The Job generates (and the CronJob renews) Secret cilium-ca and issues
Secret/hubble-server-certs in-cluster at install. No private key material
is committed. Verified with `helm template cilium/cilium --version 1.19.2`:
the only delta between the two methods is exactly these 7 resources; the
ConfigMap and cilium-agent DaemonSet are unchanged, and the agent's
hubble-tls volume is already optional:true so it starts before certgen
runs. Both files and the inline block scalar validated as YAML.

This is the code half of the fix. Rotating the already-leaked live CA +
Hubble certs and scrubbing them from git history is an operator task
tracked on the issue.

Refs #188
2026-07-14 12:30:46 +02:00
claude-bot f354cb2862 chore(ansible): delete decommissioned legacy roles (immich, grafana-observability, keepalived, postgres-simple-ha)
Owner confirmed on #225 that all five roles flagged in #198 are legacy and
were decommissioned some time ago; the earlier inventory-presence liveness
check was wrong (inventory has stale entries for services migrated into the
k8s cluster). So instead of sanitizing committed secrets, remove the dead
code that held them.

Deleted role directories (all owner-confirmed legacy):
- ansible/roles/immich            -> now runs in-cluster (kubernetes/apps/immich, secrets via OpenBao ESO)
- ansible/roles/grafana-observability -> dashboards now live in kubernetes/apps/monitoring/dashboards
- ansible/roles/postgres-simple-ha
- ansible/roles/keepalived-ntp
- ansible/roles/keepalived-vip

Deleted plays that exclusively orchestrate the deleted legacy roles:
- ansible/plays/install-immich.yml
- ansible/plays/configure-grafana-observability.yml
- ansible/plays/backup-immich-db.yml            (one-time migration dump from legacy LXC 225)
- ansible/plays/configure-immich-db-password.yml (loads the deleted immich role)

Edited (kept, live):
- ansible/plays/update.yml -> dropped the `import_playbook: install-immich.yml`
  line so the CI update path (.woodpecker/update-all.yml -> plays/update.yml)
  keeps working.

NOT touched (ambiguous — reference companion roles the owner did NOT list as
legacy; flagged on #225 for owner decision, no dangling roles deleted here):
- install-ntp-ha.yml        (also uses chrony-ntp-server)
- install-proxmox-dns-ha.yml (also uses proxmox-dns, which is still live)
- install-authentik-ha.yml   (also uses authentik-stateless)
These three plays now reference deleted keepalived-*/postgres-simple-ha roles;
their companion roles + inventory groups need an owner call before removal.

Refs #198, #225. Coordinates with #208 (decommission sweep) for immich.
2026-07-14 12:26:11 +02:00
claude-bot e643533141 fix(ci): sandbox update-audit agent against prompt-injection exfil
Closes gitea issue #197. The update-audit agent fetches untrusted
third-party release pages via WebFetch/WebSearch and its Write output
is posted verbatim to a public Gitea issue, so injected content in a
fetched page could try to redirect the agent's behavior. Harden the
tool surface (verified empirically against the pinned Claude Code CLI):

- Drop WebSearch: it cannot be domain-scoped like WebFetch, and the
  prompt already gives explicit source URLs or a well-known registry
  for everything it needs to look up.
- Add ci/agents/update-audit/settings.json (loaded via --settings) that
  denies Read on /proc, /root, /home, /etc, and the repo's own .git/ —
  closing the credential-file and env-var-dump read paths a
  prompt-injected instruction could otherwise try. Verified this deny
  mechanism actually blocks reads, unlike Write path-scoping, which
  was tested and found NOT enforced under --permission-mode acceptEdits
  (documented in the pipeline comment; a full WebFetch domain allowlist
  was also considered and rejected, since Helm chart lookups legitimately
  hit whichever domain each HelmRelease's own trusted source URL points
  at).
- prompt.md now tells the agent fetched content is untrusted data, not
  instructions, and to never echo it verbatim into the report.

Not fixed here: GITEA_TOKEN is still the broad registry/release-writing
token (no write:issue-only token exists yet) — that requires an
operator to mint one; tracked via a comment on #197 and a code comment
at the secret binding.
2026-07-14 12:19:48 +02:00
claude-bot 2c349f728b scope down: drop woodpecker-ci and openbao egress, already covered elsewhere
PR #235 (fix/issue-193-woodpecker-ci-hardening) already adds
kubernetes/infrastructure/network-policies/woodpecker-ci-ns.yaml, with a
deliberately different (and better-informed) egress design: default-allow,
because a past attempt at egress enforcement in this namespace broke
crowdsec (66dbe9f) — history this branch didn't have context on.

PR #231 (fix/issue-202-openbao-hardening) already adds an egress block to
openbao-ns.yaml as part of a broader OpenBao hardening pass, including a
"world" allowance (OIDC) this branch's version omitted.

Dropping both here to avoid shipping a conflicting/duplicate policy;
external-secrets-ns.yaml (this issue's third gap, not covered by any other
open PR) is the only change left on this branch.

Co-Authored-By: Claude Bot <claude-bot@renner.uno>
2026-07-14 12:14:40 +02:00
claude-bot 880bc9c9ac fix(crowdsec): move bouncer key from cleartext Secret to OpenBao ExternalSecret
bouncer-key.yaml committed the CrowdSec<->ingress-nginx bouncer key in
cleartext across two namespaces, outside the ESO/OpenBao pattern used
elsewhere in the cluster. Replace both Secrets with ExternalSecrets
pulling the same value from a single OpenBao KV path
(infrastructure/crowdsec-bouncer-key), matching the existing
apps/infrastructure ExternalSecret convention.

Requires the owner to populate OpenBao before merge:
  bao kv put -mount=homelab infrastructure/crowdsec-bouncer-key key=<value>

Fixes #212
2026-07-14 12:12:33 +02:00
claude-bot 8311ed12d3 fix(network-policies): default-deny CNPs for external-secrets, woodpecker-ci; openbao egress
Closes the gaps from #194: external-secrets and woodpecker-ci had no
CiliumNetworkPolicy at all (Cilium defaults to allow-all absent one), and
openbao-ns.yaml had an ingress-only CNP with no egress block, which leaves
egress from openbao pods completely unenforced rather than merely permissive.

- external-secrets-ns.yaml: default-deny-ingress; egress scoped to the
  namespace, DNS, and the openbao KV API on :8200 (the only
  ClusterSecretStore in this cluster) plus host/remote-node for
  kube-apiserver (webhook admission, Secret watch/create, leader election).
- woodpecker-ci-ns.yaml: this namespace hosts only ephemeral, sometimes
  privileged, pipeline-defined step pods (no static workloads) — the goal
  is blocking lateral movement into unrelated namespaces while still
  allowing the git clone (east-west via k8s_gateway -> traefik, since
  gitea.renner.uno now has an in-cluster Ingress) and the broad internet
  egress every CI system legitimately needs (image pulls, package
  managers).
- openbao-ns.yaml: added an egress block (DNS, intra-namespace, host/
  remote-node for the k8s auth method + kubelet) so egress enforcement is
  actually turned on, not just missing.

music-assistant is deliberately NOT covered here: its only workload runs
hostNetwork: true, so a namespace-scoped CiliumNetworkPolicy would select
no enforceable pod-network endpoint and be a no-op (same documented
limitation as the rook-ceph Ceph daemons). Left for a comment on #194
rather than shipping a policy that looks protective but isn't.

Co-Authored-By: Claude Bot <claude-bot@renner.uno>
2026-07-14 12:11:33 +02:00
claude-bot 2feec4b193 fix(ci): pin base image digests, verify tool checksums, pin claude-code version
ci/woodpecker/push/build-ci-images Pipeline was successful
Addresses #209. All four ci/images/*/Containerfile FROM lines were
tag-pinned only (alpine:3.21, node:22-alpine), letting the base silently
change between builds. Pin each to its current manifest-list digest.

ci/images/k8s/Containerfile downloaded talosctl, kubectl, and velero via
curl with no integrity check, and installed flux via `curl | bash`
(itself an unpinned, unverified fetch-and-execute). Replace the flux
install with a pinned-version tarball download verified against its
published sha256 checksum, same as talosctl/kubectl/velero now are.
kubectl's version was also resolved dynamically from dl.k8s.io's
stable.txt at build time instead of being pinned; add KUBECTL_VERSION.

ci/images/tofu/Containerfile's tofu.zip download gets the same
sha256 verification.

ci/images/claude/Containerfile's CLAUDE_CODE_VERSION defaulted to
`latest` and was never overridden by build-ci-images.yml, so the image
holding repo/issue-write tokens floated to whatever npm resolved at
build time. Pin it to the current release (2.1.209).

Checksums and digests verified against the respective registries/release
APIs at fix time (2026-07-14).

Co-Authored-By: Claude Bot <claude-bot@renner.uno>
2026-07-14 12:10:41 +02:00
claude-botandClaude Opus 4.8 2a76fd3689 feat(k8s): add PodDisruptionBudgets for single-replica stateful workloads
Adds minAvailable:1 PDBs for the data-bearing single-replica StatefulSets
that had no eviction protection: gitea, woodpecker-server, and paperless's
protonmail-bridge + syncthing sidecars. On a 1-replica workload this makes
the eviction API refuse *voluntary* evictions, so a node drain blocks and
surfaces the pod instead of yanking a live SQLite DB / RWO-PVC pod mid-write.
Involuntary disruptions (node crash, OOM) are unaffected.

Deliberately excluded, with rationale:
- CNPG clusters im-pg and gf-pg (instances:1): CloudNativePG intentionally
  omits a primary PDB for single-instance clusters (no failover target), and
  handles graceful primary shutdown itself. A manual maxUnavailable:0 would
  only wedge node drains with no HA benefit.
- Ceph mons: the Rook operator self-manages quorum-preserving mon/OSD PDBs
  (disruptionManagement.managePodBudgets default). A hand-written PDB would
  be redundant or fight the operator on each reconcile.
- Stateless single-replica Deployments (redis/tika/gotenberg caches, etc.):
  a blocking PDB gives no recoverability benefit, only blocks drains.

Refs #201

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-14 12:10:11 +02:00
claude-bot e3e8f57838 fix(certificates): remove unverified authentik cert fetch into system CA trust
The certificates role fetched https://authentik.renner.uno with
validate_certs: false and fed whatever it got back into
update-ca-certificates on every Ansible-managed host — an on-path/DNS
attacker during that fetch could plant a rogue CA cluster-wide.

authentik.renner.uno also turned out to be a stale hostname: it isn't
routed by the current Traefik/cert-manager ingress. The live service
(auth.renner.uno) already gets a publicly-trusted Let's Encrypt cert via
cert-manager, which the OS default CA bundle already trusts. Nothing else
in the repo depends on the installed authentik.crt, so removing the task
(and its now-unused handler) closes the hole with no functional loss.

Fixes #185

Co-Authored-By: Claude Bot <claude-bot@renner.uno>
2026-07-14 12:08:00 +02:00
claude-bot 0adf39c8a2 docs(kubernetes): decouple SOPS age-key backup from Phase 4 gate, document vault-password backup
Issue #210: neither the SOPS age key nor ANSIBLE_VAULT_PASSWORD has a
documented offline backup location. The age-key archive was previously
bundled into the Phase 4 point-of-no-return checklist, blocking it on the
reconcile/unseal soak for no reason — backing it up to pass is safe to do
immediately since it only adds a copy. Also documents a pass location and
verification procedure for the Ansible Vault password, which gates PBS and
other Ansible-Vault-encrypted credentials and had no backup plan at all.

Co-Authored-By: Claude Bot <claude-bot@renner.uno>
2026-07-14 12:07:50 +02:00
claude-bot a33a186bf8 fix(traefik): add security-headers + rate-limit Middlewares as entrypoint defaults
Closes #205. The websecure entrypoint had zero response hardening and no
rate limiting — CrowdSec is detection-only since the 2026-06-27 IPv6
mass-ban outage, and no headers/rate-limit Middleware existed anywhere in
the cluster, so logins sat behind app-level throttling only.

Adds two Middlewares wired as entrypoint defaults via the same
additionalArguments mechanism the (still-detached) crowdsec-bouncer
Middleware documents for its own re-enable:

- security-headers: HSTS, nosniff, xss-filter, referrer-policy,
  permissions-policy, and a conservative frame-ancestors 'self' CSP
  (deliberately not a full default-src policy — apps here span Paperless,
  Immich, Grafana, Gitea, Home Assistant, etc. with very different
  script/style/font sources).
- rate-limit: generous defaults (100 req/s, burst 200) as a baseline
  abuse/brute-force backstop rather than traffic shaping, keyed off the
  real client IP via sourceCriterion.ipStrategy.excludedIPs (same trusted
  pod CIDRs as forwardedHeaders/crowdsec-bouncer) so it doesn't collapse
  every external client onto one shared budget — the same client-IP-
  visibility gap that caused the CrowdSec outage this fix is otherwise
  independent of.

CrowdSec enforcement itself stays disabled; that re-enable is separate,
documented, unaddressed work.
2026-07-14 12:07:44 +02:00