Files
felhom.eu/documentation/audits/plex2shell-felhom-impact-2026-10-23
admin 8257ff1cbe
gates / gates (push) Successful in 12m37s
Plex2Shell impact on Felhom: verdict NOT REACHED; R-1241 (P1), R-1242 (P3) opened (register 186 -> 188)
Read-only audit of whether the hacked Plex pod on DooPlex (HM-040, 2026-10-17 -> 10-23) reached Felhom's
secrets. Two no-login doors existed (the Longhorn NFS export and the Longhorn API); neither was used:
nfsd accepted 0 new connections in the window (positive control), no off-schedule Longhorn backup or volume.
Evidence in documentation/audits/plex2shell-felhom-impact-2026-10-23/. The NFS export was narrowed from *
to 10.42.0.0/16 on the operator's yes (host change, not in git). 13-threat-model.md: attacker j and one
boundary row. STATUS: verdict, the P1, one do item.
2026-10-23 17:53:43 +02:00
..

Plex2Shell on DooPlex — did the attack reach Felhom's secrets? (2026-10-23)

Read-only investigation. Source of the incident facts: ~/incident-plex-2026-10-23/INCIDENT.md on DooPlex (HM-040, homelab session). Architecture read first: architecture/01-topology-and-trust.md, 04-control-plane-authorization.md §3.1, 05-hub-architecture.md §16.2, 13-threat-model.md, runbooks/secrets.md, runbooks/total-loss-of-dooplex.md, runbooks/target-selection.md (DooPlex = Tier 2). Window: 2026-10-17 04:00Z → 2026-10-23 08:04:34Z (the isolation NetworkPolicy incident-2026-10-23-plex-isolate, creationTimestamp in INCIDENT.md Phase 2a).

Verdict: NOT REACHED

Two open paths led from the hacked Plex pod to Felhom secrets. Each would have left a trace in a record that survived, and neither trace exists:

  1. NFS export (/mnt/5_hdd/backup/longhorn-pvc, *(rw,no_root_squash,no_subtree_check)): the NFS server accepted zero new TCP connections from 2026-10-10 00:00Z to 2026-10-23 13:05Z — node_nfsd_connections_total flat at 288, 3902 five-minute samples, no gap. Every NFS request needs a TCP connection (nfsd listens on TCP only: portlist, rpcinfo). Positive control: after this session's probe pod made three TCP connections to :2049 today the counter read 4 (one Longhorn client + three probes), in /proc/net/rpc/nfsd and in Prometheus alike. The only NFS client in the window was Longhorn's own manager (one long-lived NFSv4.2 connection). Read operations happened only in Longhorn's backup hours; bytes read per day stayed at 18–39 MiB.
  2. Longhorn manager API (no login, port 9500 — can create backups, backup targets, volumes): no backup was created outside the two recurring jobs' times (longhorn_backup_state series, 1-minute scrape); every scheduled night of the window wrote its backups to DooPlex's own NFS store (so the target was local each night); no volume existed in the window that is not explained; the backup target object is the one created 2025-11-29; DooPlex's network card sent no more than the Plex pod itself sent.

What would change this verdict: evidence of a container escape to the host (the incident session found none, INCIDENT.md „Anything OUTSIDE the Plex pod"), or a Longhorn target swap + backup + delete inside one minute (no record either way; Longhorn manager logs were lost, below).

Part 1 — the shared backup folder

what value evidence
export (before) /mnt/5_hdd/backup/longhorn-pvc *(rw,sync,no_subtree_check,no_root_squash) p1-nfs-exports-clients.txt
who can reach :2049 the host firewall accepts 192.168.0.0/24, 10.42.0.0/16 (pods), 10.43.0.0/16, 100.64.0.0/10 (tailnet) nft list ruleset, input chain
secure option no protection inside a pod: containerd 2.x sets net.ipv4.ip_unprivileged_port_start=0 in every pod; a uid-1000 probe with CapEff 0 bound port 700 and connected to :2049 p2-targeted-probes.txt (last line), sysctl read in the Plex pod netns = 0, host = 1024
filesystem the export is a sub-directory of /dev/sda1 (ext4, mounted at /mnt/5_hdd); root inode 2 has generation 0 findmnt, lsattr -vd /mnt/5_hdd
contents Longhorn backup store, 119 GB: nightly backups of every Longhorn volume, including felhom-system/hub-data (the hub database), NOT encrypted (spec.encrypted=false on all four felhom-system volumes) p1-backup-folder-listing.txt
v4 pseudo-root /, /mnt, /mnt/5_hdd, /mnt/5_hdd/backup appear read-only as path components only; no other directory under /mnt/5_hdd/backup is exported /proc/fs/nfsd/exports
clients now one: longhorn-manager-w4tf6 10.42.0.214, NFSv4.2 /proc/fs/nfsd/clients/*/info
NFS logging NFSv3 mounts are logged only at debug level; NFSv4 attaches are not logged at all (positive control: client 3 is attached and the journal has no attach line); detaches are logged (2 lines in boot -1, none in the window); illegal-port refusals are logged (none). An absent line proves nothing for v4. journalctl -t rpc.mountd, strings rpc.mountd

No subtree check matters beyond the folder. With no_subtree_check on a sub-directory export, the server checks only that a file handle belongs to the exported filesystem. A published technique (HvS-Consulting's nfs-security-tooling, 2024) builds the handle of the filesystem's root directory and walks the whole disk; with no_root_squash it does so as root. On /dev/sda1 that reaches the operator signing keys (/mnt/5_hdd/felhom.eu/ felhom-op-operational, felhom-rec-recovery, felhom_op_ed25519, no passphrase, R-924), the private security-review evidence, the workspace, the encrypted sec store (its age key is on /dev/sdb1, not reachable) and the other backup trees. Grade: documented technique, every precondition measured here, NOT exercised (doing it would be an attack on a Tier-2 machine).

The change (operator yes, 2026-10-23 ~15:18Z): the line became /mnt/5_hdd/backup/longhorn-pvc 10.42.0.0/16(rw,sync,no_subtree_check,no_root_squash); exportfs -ra rc 0. Read back: exportfs -v and /proc/fs/nfsd/exports show 10.42.0.0/16; Longhorn's backup target synced at 15:20:10Z with available: true; the Longhorn client is still attached. Backup of the old file: /etc/exports.bak-2026-10-23-plex2shell. Undo: sudo cp -p /etc/exports.bak-2026-10-23-plex2shell /etc/exports && sudo exportfs -ra. This closes the folder to the home network and the tailnet. It does NOT close it to a hacked pod — Longhorn connects from its pod address, so the pod network must stay allowed; limiting it to DooPlex itself would break Longhorn. Root squash is not possible either (the store is root-owned 0700 and Longhorn writes as root). p1-export-change.txt.

Part 2 — what the hacked Plex could reach without a login

Measured from probe pod default/cc-p2s-probe (uid 1000, CapEff 0, no service-account token, no NetworkPolicy in default — the Plex pod's position before the isolation). Created 15:19:46Z, removed 15:33:44Z (p2-probe-lifecycle.txt, before/after listings). 186 of 196 TCP Service ports answered (p2-service-sweep.txt).

target no-login result what a no-login caller gets
Longhorn manager API longhorn-backend:9500, UI longhorn-frontend:80 200 every volume (234 names), backup targets, backup volumes; actions snapshotCreate, snapshotBackup, attach, detach, pvcCreate, activate offered on hub-data
NFS on the node :2049, :111 open see Part 1
Felhom hub hub:8080 (bypasses the ingress geo filter) /healthz 200, /login 200, every data path 401/403/302 nothing (matches 13 §4: health, login, register, armed re-enroll)
kube-apiserver 10.43.0.1:443 / 192.168.0.180:6443 401 nothing (k3s config.yaml unchanged since 2026-08-05)
kubelet :10250 401; :10255 closed nothing
Prometheus, Grafana (anonymous Viewer), Alertmanager, node-exporter, kube-state-metrics 200 metrics and dashboards; Alertmanager also lets anyone create a silence. No secret values
Gitea 200 the public repos (R-925 ruling); registry /v2/ 401
Vaultwarden /alive, /api/config, /admin login page 200 nothing without the master password
ArgoCD, Headlamp, the filebrowsers 401 on the API nothing (Headlamp's own service account can read every Secret — behind OIDC)
felhom-system/umami-db 5432, other databases open password needed
LAN router admin (.1), a TP-Link (.254), a printer (.20), demo-felhom's Proxmox UI and SSH (.162), PBS UI on DooPlex :8007, Samba :445 logins needed; p2-lan-reach.txt

Part 3 — evidence of use in the window, per path

path best record that exists result
NFS nfsd counters in Prometheus (connections, ops by method, bytes, RPC errors) — p3-nfsd-*.json unused (verdict 1). RPC errors (auth/fmt/cInt) 0
Longhorn API longhorn_backup_state, longhorn_volume_state series; nfsd bytes written per night; Backup/Snapshot/BackupTarget/Volume creationTimestamp — p3-longhorn-* no sign of use: 106 backups in the window = 53 daily (10-23 02:00Z) + 53 weekly (10-18 03:00Z), all to the local URL; 0 backups first seen off-schedule; no unexplained volume. Longhorn CRs carry no managedFields, so who changed what is not recorded there (p3-longhorn-objects.txt, CORRECTION)
Longhorn manager/UI logs — no record: lost. DooPlex was rebooted at 13:20Z by the maintenance session (~/maintenance/2026-10-window); the pre-reboot container logs of longhorn-manager, longhorn-ui, the hub, the ingress, Gitea, Vaultwarden, Grafana and Prometheus are gone. The incident session's earlier greps (before the reboot) found 10.42.0.52 in none of longhorn-manager, CNPG, Gitea (from 10-21 20:59), authentik; that is their record, not re-verifiable
hub /data/access-log (R-1140), 10-18 10:25Z → now, 12 764 lines — p3-hub-access-log.txt no call from the Plex pod (10.42.0.52: 0; positive control: the probe's 10.42.0.107 is logged). Credentialed requests come only from the boxes' LAN addresses, the home WAN and the operator's sessions. Gap: 10-17 04:00Z → 10-18 10:25Z no record (the log started then) — but no hub route gives a secret without a credential
SSH to DooPlex journal (boot -1 survives) 0 sshd lines from 10.42/10.43 in the window (control: 61 lines from 192.168.x)
Samba per-client log files log.%m no file for 10.42.0.52 (positive control: log.10.42.0.107 appeared when the probe touched :445) — p3-samba-clients.txt
Alertmanager silences API (keeps expired ones) only two silences, both CC's, both outside the attack
DNS CoreDNS keeps no query log; only upstream-timeout lines name a domain, without the client names in p3-coredns-error-names.txt; hand.genddos.st looked up 10 times on 10-19 08:31–09:50Z (client unknown)
network bytes cAdvisor per pod (no peer label) — p3-network-bytes.txt the Plex pod sent ~1.5 TB (143 / 202 / 528 / 441 / 206 GiB per day, 10-18 → 10-22; before: 0.04 GiB/day) and received ≤ 7.9 GiB/day. DooPlex's NIC eno1 sent the same extra out. Outbound-heavy: the shape of a flood tool or of streaming, not of a download. It did not come from the NFS store (nfsd reads flat) or from other pods (bridge-to-pod bytes flat)

Part 4 — Felhom's secrets on DooPlex

Exposure = what a path could read, judged with Part 3. „Possible path" means the door existed; it does not mean it was used.

secret where it lives path that could read it (Part 2) evidence of use exposure
hub DB: Cloudflare API tokens, tunnel tokens, git tokens (unsealed, R-975) hub.db on Longhorn hub-data; unencrypted nightly copies in the NFS store NFS export (direct, no escape needed); Longhorn API backup to an outside target none (Part 3) none
hub DB: box API keys, controller keys, owner passphrases, PBS-DR tokens, break-glass passwords, Storage Box sub-account passwords (sealed, AES-GCM, key OFFSITE_SECRET_KEY) same same — but the key is a k8s Secret, not in the DB or its backups none none (sealed values useless without the key)
escrow blobs host_escrow.blob / identity_blob same same none none — sealed means: wrapped by each household's recovery code, which the hub never holds (03-host-agent.md, escrow retention: „the hub never decrypts either table")
hub tokens (api_tokens.token_hash), self-bind / claim codes same, hashed same none none (hashes)
k8s Secrets in felhom-system (report-api global key, offsite-secret-key, storagebox = main Storage Box/Hetzner login, resend-api, tenantsync, agent-poke, wg-endpoint-ssh, gitea-creds, umami, TLS keys) etcd/k3s datastore (/var/lib/rancher, sdb1); also the nightly GPG secrets export on sda1 (/mnt/5_hdd/backup/secrets/exports, symmetric, passphrase file on sdb1) k8s API anonymous: 401. NFS escape → the .gpg files only (key not on sda1) none none
hub session / signing material k8s Secret / hub DB as above none none
operator signing keys (operational + recovery + felhom_op_ed25519, no passphrase) /mnt/5_hdd/felhom.eu/ on sda1 NFS export → file-handle escape (documented, not exercised) none (no NFS connection at all) none — but the door is still open to any pod (row filed)
sec store cc-secrets/store/cc.sops.yaml on sda1, age key ~/.config/sops/age/keys.txt on sdb1 escape → ciphertext only none none
registry login, Gitea tokens ~/.docker/config.json, ~/.git-credentials (sdb1, 0600) none from the pod network (registry 401, Gitea API anonymous = public repos only) — none
Vaultwarden (operator's password manager, R-923) Longhorn volume; nightly unencrypted Longhorn copy in the NFS store (vault items stay encrypted by the master password) login page; NFS export → encrypted vault DB none for NFS; login logs lost none (no record of login attempts — logs lost, and the vault is encrypted at rest)
off-site ciphertext (ep0 copy, demo-felhom backups) sda1 escape → ciphertext only none none

Part 5 — plan

Verdict NOT REACHED → no rotation plan. The open doors are filed instead (rows below). Nothing was rotated.

Observations

  • A hacked pod on DooPlex can still read the hub's unencrypted nightly backups and, by the file-handle escape, the operator signing keys through the NFS export. → FILED: R-1241 (P1).
  • The Felhom demo NAS export /mnt/5_hdd/felhom-data → 192.168.0.162 is a sub-directory of the same disk with no_subtree_check: a compromised demo-felhom host could walk all of sda1 as uid 101000 (world-readable files only, e.g. the .gpg secrets exports and every repo). → FILED: R-1242 (P3).
  • The hub answers pods directly, without the ingress geo filter. → NOT-A-FINDING: the geo filter is not an authentication control; every no-token path refused (measured).
  • demo-felhom's Proxmox UI and SSH are reachable from any DooPlex pod (masqueraded as 192.168.0.180). → NOT-A-FINDING: same door as R-982 (root password SSH from the LAN), now with „a hacked DooPlex pod" as one more LAN attacker; added to 13 §3.
  • Pre-reboot pod logs were erased by the 13:20Z reboot. → NOT-A-FINDING for Felhom: the hub's own access log persisted (R-1140 did its job). Homelab item, below.

For the homelab session (no HM numbers minted; evidence paths in this folder)

  1. The NFS export is still open to every pod, no_root_squash, no_subtree_check, on the disk that holds the Felhom signing keys. secure does nothing in pods (containerd sets ip_unprivileged_port_start=0). Options: an egress NetworkPolicy so only longhorn-system reaches :2049/:111, and/or move the store to its own filesystem (LV or loop) so the subtree escape has nothing else to reach. Not managed by Ansible — the narrowed line persists.
  2. Longhorn manager API and UI need no login from any pod (create backups/targets/volumes, delete volumes).
  3. The old Plex volume was deleted through Longhorn's API at 11:24:15Z by nobody identified (INCIDENT.md EVIDENCE NOTE). Ask the operator whether it was them; if not, it is an unattributed write on an open API.
  4. The Plex pod sent ~1.5 TB out on 10-18 → 10-22 (not in INCIDENT.md); and CoreDNS saw hand.genddos.st lookups on 10-19. Possibly a flood tool riding the same compromise.
  5. The maintenance reboot at 13:20Z erased the window's pod logs (longhorn-manager, ingress, hub, Gitea, …): no log shipping, kubelet keeps one previous container log. Evidence rule R-320 applies to the homelab too.
  6. Prometheus, Alertmanager (silences!), Grafana (anonymous Viewer), kube-state-metrics, node-exporter, MinIO console, Headlamp (its service account can read every Secret and write RBAC — behind OIDC) all answer every pod.
  7. A user process python3 main.py -d -i (kisfenyo) listens on 0.0.0.0:8443 on DooPlex.