Compare commits

...
Author SHA1 Message Date
Ruben Hensen d187f55e57 docs: tonight's decisions on the roadmap 2026-10-26 23:29:23 +01:00
Ruben Hensen 3fd585de5f docs: immich ML v2 is done (#39) 2026-10-26 23:25:11 +01:00
Ruben Hensen 0dc69d34f0 Merge pull request #50 from rubenhensen/proxy-protocol
Pass the visitor's address from node001 to the home cluster
2026-10-26 23:12:14 +01:00
Ruben Hensen 30540ea579 node001: send the visitor's address to the home cluster
The stream server on 443 now starts each connection with a PROXY header.
nginx sends it to every upstream or none, so Stalwart (8443) and the
local nginx for Gatus and ntfy (8444) get theirs through a hop on
8445/8446 that takes it off again; nothing changes for them.

Deploy after the Gateway accepts the header, or the tunneled hosts stop
answering until it does.
2026-10-26 23:11:38 +01:00
Ruben Hensen 53d74768ad gateway: take the visitor's address from a PROXY header
node001 passes TLS through without opening it, so every request from
the tunneled hosts arrives from 46.224.26.65. Wordfence locked that
address out after 20 failed logins on 2026-10-26, and with it everyone
who logs in to scoutingmbg.nl. With a PROXY header from node001, Envoy
puts the visitor's address in X-Forwarded-For instead.

The header is optional: s2 and ntfy2 point straight at home, and
node001's proxy on port 80 sends none either.
2026-10-26 23:11:38 +01:00
Ruben Hensen b6462fb3a9 Merge pull request #49 from rubenhensen/roadmap-garage
docs: Velero on Garage
2026-10-26 23:04:15 +01:00
3 changed files with 79 additions and 18 deletions

No files matched your search

+36 -15
View File
@@ -38,7 +38,8 @@ docs/longhorn-second-ssd.md:
2026-10-25). The snap is held, so microk8s gets no updates at all until 2026-10-25). The snap is held, so microk8s gets no updates at all until
someone refreshes it someone refreshes it
- [ ] Second SSD, docs/longhorn-second-ssd.md - [ ] Second SSD, docs/longhorn-second-ssd.md
- [ ] Optional: `MACHINE_LEARNING_MODEL_REVISION=v2` for immich ML - [x] `MACHINE_LEARNING_MODEL_REVISION=v2` for immich ML (PR #39,
2026-10-25; the models are cached on a volume)
## Next, in order ## Next, in order
@@ -82,7 +83,9 @@ friend.
certificate; the admin API stays on the LAN. The cluster side (DNS, certificate; the admin API stays on the LAN. The cluster side (DNS,
node001 tunnel, Certificate, HTTPRoute) goes in git. Needed: how much node001 tunnel, Certificate, HTTPRoute) goes in git. Needed: how much
space he wants, free space on the NAS space he wants, free space on the NAS
- [ ] Decide which copy of the immich library goes off-site (see 5) - [x] Which copy of the immich library goes off-site: Velero's, with the
rest of the bucket (decided 2026-10-26). It is most of the ~190 GB, so
ask him for room for it
- [ ] A restore test that runs on a schedule. `backup-testing/` predates - [ ] A restore test that runs on a schedule. `backup-testing/` predates
Envoy Gateway and still rewrites `*.hensen.io` and bypasses sol-auth. Envoy Gateway and still rewrites `*.hensen.io` and bypasses sol-auth.
Planned on 2026-10-25, not run: a second Velero on the real bucket is Planned on 2026-10-25, not run: a second Velero on the real bucket is
@@ -202,8 +205,8 @@ Findings from 2026-10-25:
maintenance job pods) was fixed that afternoon by maintenance job pods) was fixed that afternoon by
`velero/skip-emptydir-policy.yaml`. `velero/skip-emptydir-policy.yaml`.
- [ ] Whether the immich library stays in Velero. It stays for now - [x] Whether the immich library stays in Velero: it stays, since
(2026-10-25); decide again with the off-site copy (2) Velero's bucket is what goes off-site (2; decided 2026-10-26)
- [ ] CNPG backups through the barman cloud plugin - [ ] CNPG backups through the barman cloud plugin
- [x] Velero's target, MinIO on the NAS, is no longer maintained: no - [x] Velero's target, MinIO on the NAS, is no longer maintained: no
community images since October 2025, maintenance mode since December community images since October 2025, maintenance mode since December
@@ -298,10 +301,22 @@ Counted 2026-10-25 over the 73 running containers:
OIDC logins of Vault and FreshRSS reach authentik, all 24 OIDC logins of Vault and FreshRSS reach authentik, all 24
ExternalSecrets refresh, raft intact; Gitea no longer reaches the NAS ExternalSecrets refresh, raft intact; Gitea no longer reaches the NAS
or Vault, and a pod in `default` no longer reaches Vault or Gitea or Vault, and a pod in `default` no longer reaches Vault or Gitea
- [ ] Certificates couldn't renew in those 11 namespaces: Envoy may only
reach each app's own port, and cert-manager's HTTP-01 solver listens on
8089. ynab's renewal hung from 2026-10-26 17:35 UTC; a policy for the
solver pods, applied by hand in actual-budget that evening, let it
through within 20 seconds (now valid until 2027-01-24). PR #51 adds it
to all 11. Without it the next renewals hang too: gitea on 11-15,
bolletjestrui on 11-18, the rest from 12-03
- [ ] The infrastructure namespaces (cert-manager, cnpg-system, - [ ] The infrastructure namespaces (cert-manager, cnpg-system,
external-secrets, envoy-gateway-system, velero, mariadb-system, dns, external-secrets, envoy-gateway-system, velero, mariadb-system, dns,
default) still have none; `default` can reach the LAN and everything default) still have none; `default` can reach the LAN and everything
without a policy without a policy. Prepared on 2026-10-26, not yet merged: PR #53 for
all but Envoy (one commit per namespace; `default` denies everything),
PR #54 for envoy-gateway-system on its own, since a mistake there takes
every public host down. Merge them when someone is home to check: the
cert-manager, CNPG, ESO and MariaDB webhooks fail closed. Velero's
policy only lets it reach Garage, not MinIO's port 9000
- [x] Requests: PR #33, merged 2026-10-25 11:23 UTC, for everything that - [x] Requests: PR #33, merged 2026-10-25 11:23 UTC, for everything that
had none except the Vault server, from `kubectl top` every 5 minutes had none except the Vault server, from `kubectl top` every 5 minutes
from 03:13 to 08:40 UTC (highest memory plus 10%, CPU at p95, 10m from 03:13 to 08:40 UTC (highest memory plus 10%, CPU at p95, 10m
@@ -334,10 +349,14 @@ Counted 2026-10-25 over the 73 running containers:
`litespeed-cache` only caches pages on a LiteSpeed server, so it does `litespeed-cache` only caches pages on a LiteSpeed server, so it does
little here, `wordpress-importer` is usually for a one-off import, and little here, `wordpress-importer` is usually for a one-off import, and
`code-snippets` runs PHP from the dashboard `code-snippets` runs PHP from the dashboard
- [ ] Rate limiting per visitor in Envoy needs the client's address, which - [ ] Rate limiting per visitor in Envoy, on hold. Since PR #50
node001 loses: it passes TLS through, so every request arrives from (2026-10-26) node001 passes the visitor's address on with the PROXY
46.224.26.65. That needs the PROXY protocol from node001's nginx to Envoy protocol, and the apps see it: authentik logs it, Apache logs it for
(ClientTrafficPolicy `enableProxyProtocol`) WordPress, so Wordfence's lockout after 20 failed logins now hits one
visitor instead of everyone. Envoy Gateway's local rate limit can't
count per address (no `Distinct`); that takes its global rate limit,
with a Redis and a ratelimit service next to it. Not worth two more
components on the one node while the apps' own limits work
- [x] Probes for gitea (`/api/healthz`), freshrss (TCP, since - [x] Probes for gitea (`/api/healthz`), freshrss (TCP, since
mod_auth_openidc answers 401 to non-browsers) and serpbear (`/login`) mod_auth_openidc answers 401 to non-browsers) and serpbear (`/login`)
(2026-10-25). Liveness only after 5 failures 30s apart, to ride out the (2026-10-25). Liveness only after 5 failures 30s apart, to ride out the
@@ -375,7 +394,10 @@ every PR.
fail the run; tested with a planted Secret and an untagged image), plus fail the run; tested with a planted Secret and an untagged image), plus
PR annotations for containers without requests or probes. freshrss's PR annotations for containers without requests or probes. freshrss's
`alpine/git` init container had no tag; now `v2.52.0` `alpine/git` init container had no tag; now `v2.52.0`
- [ ] Automerge patch versions of low-risk apps now that CI runs - [ ] Automerge patch versions of low-risk apps now that CI runs: PR #52
(ntfy, gatus, freshrss, serpbear, actual-budget and CI's own tools;
three days after the release, only on weekday evenings and at the
weekend, when the cluster can be reached)
- [x] Renovate listed external-snapshotter twice (#15 and #24): its - [x] Renovate listed external-snapshotter twice (#15 and #24): its
`argocd` manager and a custom `github-tags` regex both matched. The `argocd` manager and a custom `github-tags` regex both matched. The
regex is gone; Renovate should close #15 by itself regex is gone; Renovate should close #15 by itself
@@ -432,11 +454,10 @@ step after that, if downtime starts to matter.
there and the volumes hold nothing (0 bytes in every backup). Drop both there and the volumes hold nothing (0 bytes in every backup). Drop both
mounts and the two PVCs, or make them ConfigMap files if custom config mounts and the two PVCs, or make them ConfigMap files if custom config
is wanted is wanted
- [ ] `kubectl delete namespace secrets`, empty since its app went - [x] The namespace `secrets` was already gone on 2026-10-26
- [ ] 41 Failed Velero backups: the dailies from 2026-09-26 to 10-21 and - [x] The Failed Velero backups (the dailies from 2026-09-26 to 10-21 and
the weeklies since 08-02. Velero doesn't restore from a Failed backup; the weeklies since 08-02; 40 by then, one had expired), deleted through
left alone they expire by 2027-01-16. Velero on 2026-10-26. Left: 13 Completed, 3 PartiallyFailed
`kubectl -n velero exec deploy/velero -c velero -- /velero backup delete $(kubectl -n velero get backups.velero.io -o json | jq -r '.items[] | select(.status.phase=="Failed") | .metadata.name' | xargs) --confirm`
- [ ] The mariadb-operator Backup `mbgwp-pre-migration-backup`, a dump - [ ] The mariadb-operator Backup `mbgwp-pre-migration-backup`, a dump
from 2026-02-22 on its own 10 GiB volume (which has a Longhorn backup from 2026-02-22 on its own 10 GiB volume (which has a Longhorn backup
from 2026-10-22). The PVC has no owner, so it goes separately: from 2026-10-22). The PVC has no owner, so it goes separately:
+23
View File
@@ -0,0 +1,23 @@
# node001 passes TLS for the tunneled hosts through without opening it, so
# without this every request arrives from 46.224.26.65. Wordfence locks that
# address out after 20 failed logins (2026-10-26), and with it everyone who
# logs in to scoutingmbg.nl. node001 puts a PROXY header with the visitor's
# address in front of each connection; Envoy takes the address from there
# and adds it to X-Forwarded-For.
#
# Optional, because s2 and ntfy2 point straight at home and their clients
# send no header, and neither does node001's proxy on port 80. The price:
# whoever connects to the home address directly can send a header of their
# own and claim any address.
apiVersion: gateway.envoyproxy.io/v1alpha1
kind: ClientTrafficPolicy
metadata:
name: proxy-protocol
namespace: envoy-gateway-system
spec:
targetRefs:
- group: gateway.networking.k8s.io
kind: Gateway
name: homelab
proxyProtocol:
optional: true
+20 -3
View File
@@ -173,12 +173,18 @@ in
# - mail.rubenhensen.nl (and anything else) → local stalwart on 8443 # - mail.rubenhensen.nl (and anything else) → local stalwart on 8443
# - tunneled hosts → home cluster ingress on 443 # - tunneled hosts → home cluster ingress on 443
# - s1 / ntfy1.rubenhensen.nl → local nginx on 8444 (Gatus, ntfy) # - s1 / ntfy1.rubenhensen.nl → local nginx on 8444 (Gatus, ntfy)
#
# Each connection starts with a PROXY header that carries the visitor's
# address, so the home cluster sees who is asking instead of this box
# (gateway/proxy-protocol.yaml in k8scd). nginx sends the header to
# every upstream or none, so the local ones go through a hop on
# 8445/8446 that takes it off again.
streamConfig = '' streamConfig = ''
map $ssl_preread_server_name $tunnel_upstream { map $ssl_preread_server_name $tunnel_upstream {
${sniMapEntries} ${sniMapEntries}
s1.rubenhensen.nl 127.0.0.1:8444; s1.rubenhensen.nl 127.0.0.1:8446;
ntfy1.rubenhensen.nl 127.0.0.1:8444; ntfy1.rubenhensen.nl 127.0.0.1:8446;
default 127.0.0.1:8443; default 127.0.0.1:8445;
} }
server { server {
@@ -186,6 +192,17 @@ ${sniMapEntries}
listen [::]:443; listen [::]:443;
proxy_pass $tunnel_upstream; proxy_pass $tunnel_upstream;
ssl_preread on; ssl_preread on;
proxy_protocol on;
}
server {
listen 127.0.0.1:8445 proxy_protocol;
proxy_pass 127.0.0.1:8443;
}
server {
listen 127.0.0.1:8446 proxy_protocol;
proxy_pass 127.0.0.1:8444;
} }
''; '';