From d15cdb5ba20412135731321b70294ead0971db9d Mon Sep 17 00:00:00 2001 From: Garrett Allen <98648590+Gerrrt@users.noreply.github.com> Date: Sat, 14 Nov 2026 00:53:12 +0000 Subject: [PATCH 1/3] fix(analyst): close garuda's SSH to keys, drop GitHub write creds; bump its agent pin MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit An audit of garuda on 2026-11-13 found Kali's stock sshd taking passwords on every address, cloud-init's passwordless sudo for analyst, and an owner-scoped gh login on the box, all on the segment the lab domain's six share. - stacks/analyst/ssh/10-garuda.conf: key-only, analyst from Hicks and phoenix alone, no forwarding. build-the-analyst-workstation.md §4 installs it and removes the passwordless sudo; §0 and §9 record and check both. - §7 reads the dotfiles release tags without a GitHub login; garuda holds no GitHub write credential. - docs/architecture.md no longer describes the cgroup fence ADR-0093 replaced. - stacks/soc/linux-agents.yaml: wazuh-agent 4.14.8 -> 4.14.10, the manager's, hash checked against Wazuh's signed apt index. §12 gains the upgrade step. - Every Alloy runs with --disable-reporting, as Grafana and Loki already do. Co-Authored-By: Claude Opus 5.5 --- docs/architecture.md | 2 +- docs/changelog.md | 26 +++++++ .../runbooks/build-the-analyst-workstation.md | 67 +++++++++++++++++-- docs/security.md | 1 + stacks/analyst/README.md | 8 +++ stacks/analyst/compose.yaml | 2 + stacks/analyst/ssh/10-garuda.conf | 29 ++++++++ stacks/bloodhound/compose.yaml | 2 + stacks/lab/compose.yaml | 2 + stacks/observability/compose.yaml | 2 + stacks/sensor/compose.yaml | 2 + stacks/soc/compose.yaml | 2 + stacks/soc/linux-agents.yaml | 15 +++-- 13 files changed, 147 insertions(+), 13 deletions(-) create mode 100644 stacks/analyst/ssh/10-garuda.conf diff --git a/docs/architecture.md b/docs/architecture.md index 9856f4d3..7e6c06d3 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -192,7 +192,7 @@ hole from the monitoring VLAN into the monitored one. | `fenrir` (10.0.30.90) | 🟢 30 | [`stacks/sensor`](../stacks/sensor) | Zeek on a `tc` mirror of `Saruman`'s lab bridge, so that the domain's east-west traffic, which crosses no router and so never reaches Suricata on `morpheus`, is observed at all ([#437](https://github.com/Gerrrt/HomeLab/issues/437), [ADR-0068](adr/0068-mirror-the-lab-bridge-to-zeek-with-tc-not-open-vswitch.md)). A guest on `Saruman`, VMID 190, with a second NIC alone on the port-less `vmbr1` that receives only the mirror's copies. Its logs go to `alexander`, never to VLAN 99; whether the mirror is delivering is answered on the hypervisor by `homelab_zeek_mirror_active`. Built 2026-09-30 by [`build-the-sensor-guest.md`](runbooks/build-the-sensor-guest.md), and proved by rebooting `Saruman` with the mirror's timer disabled: the gauge fell to 0 and `ZeekMirrorInactive` fired. Alloy agent (Docker) | | `eden` (10.0.30.41) | 🟢 30 | [`stacks/bloodhound`](../stacks/bloodhound) | Off between sessions: BloodHound CE over one Postgres that holds its graph too, for attack-path analysis of `ad.matrix.elysium` — which path to Domain Admin exists, so that `odin`, `fenrir` and Sysmon can be asked whether they saw it ([#451](https://github.com/Gerrrt/HomeLab/issues/451), [ADR-0081](adr/0081-run-bloodhound-ce-on-a-saruman-guest.md)). A guest on `Saruman`, VMID 141, tagged `on-demand` so that its being off is not an alert (ADR-0079), and in no backup job: the graph is one collection run from rebuilt. Its UI is TLS on a leaf from the estate's CA, reached from Hicks; the collector's upload is intra-segment, so it adds no firewall rule. The graph is a map of the lab's weaknesses and never leaves the segment; its Alloy pushes the guest's own telemetry to `alexander`. Built 2026-10-07 by [`build-the-bloodhound-guest.md`](runbooks/build-the-bloodhound-guest.md), a full clone of template 901. Alloy agent (Docker) | | `diabolos` (10.0.30.61) | 🟢 30 | [`stacks/scratch`](../stacks/scratch) | **Not built yet**, and torn down between investigations: a DISPOSABLE copy of `stacks/soc` (Wazuh and Velociraptor on the same digests, soc's config mounted unchanged) for detonations and one-off questions, so that noise never spends `odin`'s shard budget or enters its record ([#438](https://github.com/Gerrrt/HomeLab/issues/438), [ADR-0071](adr/0071-run-disposable-investigations-on-a-guest-that-is-destroyed.md)). A guest on `Saruman`, VMID 161, created with the Proxmox tag `disposable`; the hypervisor's guest-state collector reports the tag and the guest's age, and `DisposableGuestOutlived` fires on the estate when it has existed for a fortnight, running or stopped. No Alloy, no scrape, no backup and no committed secrets: its age key and `secrets/scratch.sops.yaml` are made on the guest and destroyed with it. Built and destroyed by [`run-a-scratch-investigation.md`](runbooks/run-a-scratch-investigation.md) | -| `garuda` (10.0.30.62) | 🟢 30 | [`stacks/analyst`](../stacks/analyst) | Defense's analyst workstation: Kali Purple **without its SOC**, a seat for working the alerts `odin` raises, with CyberChef, Wireshark, and the [`dotfiles-Defense`](https://github.com/dotgibson/dotfiles-Defense) layer on `dotfiles-Debian` ([#921](https://github.com/Gerrrt/HomeLab/issues/921), [ADR-0091](adr/0091-put-a-kali-purple-analyst-workstation-on-saruman.md)). Purple's SIEM and sensor daemons stay off, because Wazuh, Zeek and Suricata already run; Defense's own `siemup` lab comes up for an exercise only. A guest on `Saruman`, VMID 162, cloned by `tofu/` from template 910 into the `analyst` pool, **always on** and not tagged `on-demand`, so a stopped `garuda` is an alert (ADR-0079), and in `golem`'s backup job, because its case notes are in no repository. Standalone, not a domain member: whoever takes the domain must not also take the notes. Its stack is the collector alone, pushing to `alexander`. It is enrolled in `odin`'s Wazuh and Velociraptor from Debian packages staged on `odin` ([ADR-0092](adr/0092-enrol-garuda-in-odins-soc-with-debian-packages-staged-on-odin.md)); OpenVAS runs on demand and scans the domain's six only, fenced by nftables on the scanner's cgroup ([ADR-0093](adr/0093-run-openvas-on-garuda-on-demand-scoped-to-the-domain-by-nftables.md), [`scan-the-lab-with-openvas.md`](runbooks/scan-the-lab-with-openvas.md)). Built 2026-10-10 by [`build-the-analyst-workstation.md`](runbooks/build-the-analyst-workstation.md), a full clone of template 910. Alloy agent (Docker) | +| `garuda` (10.0.30.62) | 🟢 30 | [`stacks/analyst`](../stacks/analyst) | Defense's analyst workstation: Kali Purple **without its SOC**, a seat for working the alerts `odin` raises, with CyberChef, Wireshark, and the [`dotfiles-Defense`](https://github.com/dotgibson/dotfiles-Defense) layer on `dotfiles-Debian` ([#921](https://github.com/Gerrrt/HomeLab/issues/921), [ADR-0091](adr/0091-put-a-kali-purple-analyst-workstation-on-saruman.md)). Purple's SIEM and sensor daemons stay off, because Wazuh, Zeek and Suricata already run; Defense's own `siemup` lab comes up for an exercise only. A guest on `Saruman`, VMID 162, cloned by `tofu/` from template 910 into the `analyst` pool, **always on** and not tagged `on-demand`, so a stopped `garuda` is an alert (ADR-0079), and in `golem`'s backup job, because its case notes are in no repository. Standalone, not a domain member: whoever takes the domain must not also take the notes. SSH is key-only from Hicks and `phoenix`, `sudo` asks, and it holds no GitHub write credential. Its stack is the collector alone, pushing to `alexander`. It is enrolled in `odin`'s Wazuh and Velociraptor from Debian packages staged on `odin` ([ADR-0092](adr/0092-enrol-garuda-in-odins-soc-with-debian-packages-staged-on-odin.md)); OpenVAS runs on demand and scans the domain's six only, fenced at layer 2 by an nftables `netdev` egress hook on `scan0`, the scanner's own macvlan in its own network namespace ([ADR-0093](adr/0093-run-openvas-on-garuda-on-demand-scoped-to-the-domain-by-nftables.md), [`scan-the-lab-with-openvas.md`](runbooks/scan-the-lab-with-openvas.md)). Built 2026-10-10 by [`build-the-analyst-workstation.md`](runbooks/build-the-analyst-workstation.md), a full clone of template 910. Alloy agent (Docker) | | `dot-debian` (10.0.30.91) | 🟢 30 | *(none — a test machine)* | Built 2026-10-08. Debian 13 (trixie), cloned from `tpl-debian-13` (903), where a release of the [dotgibson dotfiles](https://github.com/dotgibson/dotfiles-core)' Debian layer is bootstrapped on a real install. A guest on `Saruman`, VMID 191, tagged `on-demand` (ADR-0079) and off between runs, rolled back to its `clean` snapshot before each one ([#920](https://github.com/Gerrrt/HomeLab/issues/920), [ADR-0090](adr/0090-test-the-dotfiles-os-layers-on-on-demand-saruman-guests.md), [`test-the-dotfiles-layers.md`](runbooks/test-the-dotfiles-layers.md)). Standalone, not a domain member. No scrape, no backup: it is rebuilt from its template, and runs no Alloy | | `dot-fedora` (10.0.30.92) | 🟢 30 | *(none — a test machine)* | Built 2026-10-08. Fedora Server 44, cloned from `tpl-fedora-server` (904), for the dotfiles' Fedora layer. A guest on `Saruman`, VMID 192, tagged `on-demand` (ADR-0079) and off between runs, rolled back to its `clean` snapshot before each one ([#920](https://github.com/Gerrrt/HomeLab/issues/920), [ADR-0090](adr/0090-test-the-dotfiles-os-layers-on-on-demand-saruman-guests.md), [`test-the-dotfiles-layers.md`](runbooks/test-the-dotfiles-layers.md)). Standalone, not a domain member. No scrape, no backup: it is rebuilt from its template, and runs no Alloy | | `dot-opensuse` (10.0.30.93) | 🟢 30 | *(none — a test machine)* | Built 2026-10-09. openSUSE Tumbleweed, cloned from `tpl-opensuse-tw` (905), for the dotfiles' openSUSE layer. A guest on `Saruman`, VMID 193, tagged `on-demand` (ADR-0079) and off between runs, rolled back to its `clean` snapshot before each one ([#920](https://github.com/Gerrrt/HomeLab/issues/920), [ADR-0090](adr/0090-test-the-dotfiles-os-layers-on-on-demand-saruman-guests.md), [`test-the-dotfiles-layers.md`](runbooks/test-the-dotfiles-layers.md)). Standalone, not a domain member. No scrape, no backup: it is rebuilt from its template, and runs no Alloy | diff --git a/docs/changelog.md b/docs/changelog.md index 173089c2..c7089306 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -17,6 +17,32 @@ roadmap as it read that day, and the *Done* entries keep the shape they had there. `check_docs.py` does not check this file, for the reason its module docstring gives: it is a record, not a claim about now. +## 2026-11-13 + +- **An audit of `garuda` found SSH open to the lab domain, and owner GitHub + credentials on it.** A read-only audit against this repository found the role + intact: no Purple SOC daemons, GVM stopped, the fence files identical to + `stacks/analyst/gvm/`, not a domain member, Wazuh and Velociraptor connected. + It also found Kali's stock `sshd` taking passwords on every address, with + cloud-init's passwordless `sudo` for `analyst`, on the segment the domain's + six share. And it found a `gh` login with the `repo` and `admin:public_key` + scopes, beside an SSH key for GitHub with no passphrase. This change writes + the fix into the build: + - `stacks/analyst/ssh/10-garuda.conf`, installed by + `build-the-analyst-workstation.md` §4, which makes SSH key-only from Hicks + and `phoenix` with forwarding off; + - the same step removes the passwordless `sudo`; + - §7 reads the dotfiles release tags without a GitHub login, and `garuda` + holds no GitHub write credential. + + The box itself changes when §4 is run on it, which this entry does not + record. Three smaller items rode along: + - `docs/architecture.md` still described the cgroup fence ADR-0093 replaced; + - the Linux agent pin in `stacks/soc/linux-agents.yaml` moves from 4.14.8 to + the manager's 4.14.10, with the hash checked against Wazuh's signed index; + - every Alloy runs with `--disable-reporting`, as Grafana and Loki already do. + `garuda`'s attempts to report were failing at the resolver anyway. + ## 2026-11-12 - **#1473 is deployed on `alexander` and verified.** This completes the #1430 diff --git a/docs/runbooks/build-the-analyst-workstation.md b/docs/runbooks/build-the-analyst-workstation.md index 25df3262..e3191aea 100644 --- a/docs/runbooks/build-the-analyst-workstation.md +++ b/docs/runbooks/build-the-analyst-workstation.md @@ -51,6 +51,8 @@ Enrolling `garuda` in Wazuh and Velociraptor is §12, phase 2. OpenVAS, phase | Domain | **Not joined** | The domain is the target and is reverted. A member would fall with it, notes and all (ADR-0091) | | SIEM and sensors | **Off** | Wazuh, Zeek and Suricata already run. Their binaries stay, for offline work on a pcap; their daemons do not | | Backup | **Yes**, `golem`'s nightly job | Its case notes are not in git | +| SSH and `sudo` | **Keys only, from Hicks and `phoenix`; `sudo` asks** (§4) | The domain's six share the segment, and no firewall stands between them and `garuda` | +| GitHub | **Read-only:** clones over https, no `gh` login, no GitHub key | The estate converges from `Gerrrt/HomeLab` ([ADR-0021](../adr/0021-converge-on-a-timer-instead-of-deploying-over-ssh.md)). Whoever takes `garuda` must not also be able to write to it | | Login user | `analyst` | Not `operator`: Debian ships a system group of that name, which broke cloud-init on the dotfiles VMs | ## 1. Build template 910, from `phoenix` @@ -159,6 +161,42 @@ manager, not in this repository or in OpenTofu's state: ssh -t analyst@10.0.30.62 sudo passwd analyst ``` +**Then close SSH to keys, and make sudo ask.** Kali's stock `sshd` takes +passwords on every address, and cloud-init gives `analyst` `sudo` with no +password. On a segment shared with the domain's six, which exist to be +compromised, those two together are a guessed password away from root on the +box that holds the case notes. So once the console password exists, SSH stops +accepting it, from anywhere but Hicks and `phoenix`, and `sudo` starts asking +for it. [`stacks/analyst/ssh/10-garuda.conf`](../../stacks/analyst/ssh/10-garuda.conf) +is the drop-in. From `phoenix`'s checkout: + +```bash +scp stacks/analyst/ssh/10-garuda.conf analyst@10.0.30.62: +ssh -t analyst@10.0.30.62 ' + sudo install -m 0644 -o root -g root 10-garuda.conf /etc/ssh/sshd_config.d/ && rm 10-garuda.conf && + sudo sshd -t && sudo systemctl reload ssh' +``` + +**Before going on, open a second session** and check it from there, so a +mistake cannot lock out the one you are in: + +```bash +ssh analyst@10.0.30.62 true && echo key-ok +ssh -o PubkeyAuthentication=no analyst@10.0.30.62 true # must say "Permission denied (publickey)" +``` + +Then take away the passwordless `sudo`. `analyst` keeps `sudo` through the +`sudo` group, which asks for the password. Check that it does before removing +the grant, so a wrong password is found while the grant still stands: + +```bash +ssh -t analyst@10.0.30.62 'sudo -k && sudo -v && echo sudo-password-ok' +ssh -t analyst@10.0.30.62 'sudo rm /etc/sudoers.d/90-cloud-init-users && sudo -k && sudo -n true 2>&1 | grep -q "password is required" && echo nopasswd-gone' +``` + +Every later step that uses `sudo` asks for the password, so an `ssh` that +runs `sudo` takes `-t`. + ## 5. Kali Purple, without its SOC On `garuda`: @@ -291,8 +329,9 @@ GitHub. Each bootstrap links from wherever it is cloned, so moving a clone means re-running its bootstrap: ```bash -deb=$(gh api repos/dotgibson/dotfiles-Debian/releases/latest --jq .tag_name) -def=$(gh api repos/dotgibson/dotfiles-Defense/releases/latest --jq .tag_name) +latest() { curl -fsSL "https://api.github.com/repos/dotgibson/$1/releases/latest" | jq -r .tag_name; } +deb=$(latest dotfiles-Debian) +def=$(latest dotfiles-Defense) echo "Debian $deb, Defense $def" # into §11 mkdir -p ~/code/dotgibson @@ -306,7 +345,10 @@ exec zsh core doctor; echo "doctor exit $?" ``` -Without `gh` on the guest, read the two tags from the releases pages. +The release API needs no login, and `jq` is §5's. **No GitHub credential on +`garuda`:** no `gh auth login`, and no key added to the account. Every clone is +over https and read-only (§0). A change found here is made from Hicks or +`phoenix`, which can push. A good run is: @@ -366,6 +408,14 @@ Each line is a pass or a stop: - `docker ps --format '{{.Names}}'` prints exactly `analyst-alloy` and `analyst-docker-socket-proxy`; - `ss -lntup` shows no Suricata, Zeek or Elastic listener. +- **Access** (§4, §7). On `garuda`: + - `sudo sshd -T | grep -iE '^(passwordauthentication|permitrootlogin|allowusers) '` + prints `passwordauthentication no`, `permitrootlogin no`, and `allowusers` + for `analyst@10.0.50.0/24` and `analyst@10.0.30.70`; + - `sudo -k; sudo -n true` says a password is required; + - `gh auth status` says it is not logged in, or `gh` is absent; + - no key in `~/.ssh` is on the GitHub account, and any private key there has + a passphrase: `ssh-keygen -y -P '' -f ` fails. - **dotfiles.** §7's two bootstrap exits and the `core doctor` exit. - **Desktop.** From `Hicks`, `garuda`'s console in Proxmox shows the Purple XFCE session. Wireshark opens a capture, and CyberChef opens. @@ -623,9 +673,9 @@ the Wazuh agent and the Velociraptor client, as Debian packages staged on ```bash d=$(mktemp -d) && cd "$d" - curl -fsSO http://10.0.30.60:8448/wazuh-agent_4.14.8_amd64.deb + curl -fsSO http://10.0.30.60:8448/wazuh-agent_4.14.10_amd64.deb curl -fsSO http://10.0.30.60:8448/velociraptor-client_0.77.3_amd64.deb - echo "6ed202e211fd8c544d1906c9b94dcd4b503d1829eef7d0a82ce4bcf4a1688967 wazuh-agent_4.14.8_amd64.deb" | sha256sum -c - + echo "873cfec3720a7d7a344afc2c646cf5895a04caa3e4dd7b5124987264f711db94 wazuh-agent_4.14.10_amd64.deb" | sha256sum -c - sha256sum velociraptor-client_0.77.3_amd64.deb # must equal staging's hash ``` @@ -637,7 +687,7 @@ the Wazuh agent and the Velociraptor client, as Debian packages staged on ```bash sudo WAZUH_MANAGER=10.0.30.60 WAZUH_AGENT_NAME=garuda WAZUH_AGENT_GROUP=default \ - dpkg -i wazuh-agent_4.14.8_amd64.deb + dpkg -i wazuh-agent_4.14.10_amd64.deb sudo sh -c 'umask 027; cat > /var/ossec/etc/authd.pass && chgrp wazuh /var/ossec/etc/authd.pass' # paste, Enter, Ctrl-D sudo systemctl enable --now wazuh-agent && sudo systemctl restart wazuh-agent ``` @@ -646,6 +696,11 @@ the Wazuh agent and the Velociraptor client, as Debian packages staged on `sudo rm /var/ossec/etc/authd.pass`. The agent keeps its own key in `client.keys`. A re-enrolment writes the file again. + **To upgrade an enrolled agent** after the pin moves, stage again on + `odin`, fetch and check the new `.deb` as in step 2, and + `sudo dpkg -i` it with no variables. The package keeps `ossec.conf` and + `client.keys`, so the agent comes back as itself, with no password. + 4. **The Velociraptor client.** The package carries `odin`'s server URL, CA and nonce, and installs and starts its own service: diff --git a/docs/security.md b/docs/security.md index 91674877..abee889e 100644 --- a/docs/security.md +++ b/docs/security.md @@ -21,6 +21,7 @@ What this network is actually built to survive: | Someone on the lab segment reading the domain's metrics | **Scoped, unauthenticated, and accepted.** `windows_exporter` listens on `9182` on all six of ADR-0029's guests, on the segment that exists to hold attackers. Each guest's Windows Firewall admits `10.0.30.40` (`alexander`) alone ([`build-the-lab-domain.md`](runbooks/build-the-lab-domain.md) §7). That rule is the difference between an endpoint a `/24` sweep finds and one you have to go looking for, and it is a residual, not a control: the exporter has no authentication, so whoever owns `alexander` reads every gauge. It has no write API, so what leaks is the shape of the host (services, disks, uptime, the licence clock), the same class as `node_exporter` on the NAS below | | Someone on the lab segment fetching a Velociraptor client package | **Scoped by address, and accepted.** `odin`'s `caddy` service (container `soc-agent-msi`) serves the two agent MSIs `ansible/` installs, and the two Debian packages `garuda` installs, on `8448` ([#1068](https://github.com/Gerrrt/HomeLab/issues/1068), [ADR-0092](adr/0092-enrol-garuda-in-odins-soc-with-debian-packages-staged-on-odin.md)). Its Caddyfile answers the six domain addresses, `.50`–`.55`, and `garuda`'s `.62`, and refuses everyone else, Hicks's route from VLAN 50 included. The Velociraptor MSI and the Velociraptor `.deb` each carry the client config: the server URL, its CA, and the enrolment nonce. Whoever holds either can enrol a rogue client, which is why #864 took the MSI out of SYSVOL. The residual: an address on this segment can be spoofed, and any of the seven can fetch either package. What the nonce buys is noise in Velociraptor's client list, the same class as the Wazuh enrolment password. Rotating it means a new server config, a rebuilt MSI with the role's pin moved, and the staged `.deb` deleted so that `scripts/stage-agent-msis.sh` builds it again. The two Wazuh packages carry no secret | | An OpenVAS plugin on `garuda` reaching a non-target | **Fenced at layer 2** ([ADR-0093](adr/0093-run-openvas-on-garuda-on-demand-scoped-to-the-domain-by-nftables.md)). OpenVAS scans the domain's six only. While a scan runs, the scanner runs in a network namespace whose one route to the segment is a macvlan, `scan0` at `10.0.30.63`, behind an nftables `netdev` egress hook that drops every frame — IP, ARP (`send_arp_request`) and raw (`send_frame`) — to anything but the six. So the AF_PACKET paths that bypass an IP filter are caught here. Proved: a GVM scan of an out-of-scope host raised the egress drop counter with 0 open ports, and an `arping` from the namespace to an out-of-scope host was dropped while an in-scope host answered. `gvmd`, `notus`, `redis` and `mosquitto` stay on the host; only MQTT crosses to the namespace, on a veth that routes nowhere else | +| Someone on the lab segment logging in to `garuda` | **Closed at `sshd`.** `garuda` shares VLAN 30 with the domain's six, and nothing between them filters. Its `sshd` takes keys only, admits `analyst` from Hicks's `10.0.50.0/24` and `phoenix`'s `10.0.30.70` alone, and forwards nothing; `sudo` asks for the console password ([`stacks/analyst/ssh/10-garuda.conf`](../stacks/analyst/ssh/10-garuda.conf), [`build-the-analyst-workstation.md`](runbooks/build-the-analyst-workstation.md) §4). It holds no GitHub write credential, so taking it does not reach the repository the estate converges from. The audit of 2026-11-13 found it taking passwords from the whole segment, with passwordless `sudo` and an owner-scoped GitHub token on it | | Someone on the lab segment reaching the SOC's web UIs | **Scoped by address, and accepted.** The Wazuh dashboard (`443`) and the Velociraptor GUI (`8889`) on `odin` are published by the stack's `caddy` service (container `soc-agent-msi`), not by their own containers, and its Caddyfile answers Hicks's `10.0.50.0/24` and refuses everyone else ([#1139](https://github.com/Gerrrt/HomeLab/issues/1139)). Until then both answered all of VLAN 30, the six domain guests included, because traffic between VLAN 30 hosts never crosses pfSense — so no firewall rule could have narrowed them. WireGuard peers are not in the list. The residuals: Hicks is a whole /24 with no per-host source yet ([ADR-0031](adr/0031-narrow-hicks-to-a-named-list-on-winterfell-and-leave-the-lab-open.md)), and an address on VLAN 30 can be spoofed, so this narrows who reaches the login pages and does not replace them. And the hop from Caddy to Velociraptor skips certificate verification: the GUI's certificate is signed by the CA inside the gitignored `server.config.yaml` on `odin`, and the hop is container to container on the `soc` bridge. The dashboard hop is verified against the Wazuh generator's root | | Someone on the lab segment reaching the lab's Grafana or BloodHound, or a detonation reaching `diabolos`'s UIs | **Scoped by address, as the SOC's are.** `alexander`'s Grafana (`3000`) is published by the lab stack's `caddy`, and `eden`'s BloodHound (`8443`) by the bloodhound stack's own `caddy`; each answers Hicks's `10.0.50.0/24` and refuses the rest of VLAN 30, the six domain guests included. `diabolos`, not yet built, has soc's fence for its dashboard and GUI. Until then Grafana and BloodHound answered all of VLAN 30, where BloodHound's graph is the domain's attack-path map ([#1428](https://github.com/Gerrrt/HomeLab/issues/1428)). The residuals are the SOC row's: Hicks is a whole /24, and an address on VLAN 30 can be spoofed for a one-way packet but not for the TLS session the UIs need | | A range target with a path out | It has none — `gandalf`'s targets sit on a bridge with no physical port, on `172.30.30.0/24`, which the firewall does not route and on which nothing has a default route at all ([ADR-0014](adr/0014-put-ifrit-on-imaginationlan-and-give-the-targets-no-route.md), [ADR-0017](adr/0017-buy-ifrit-for-iops-and-keep-the-range-disposable.md)) | diff --git a/stacks/analyst/README.md b/stacks/analyst/README.md index aa4178f6..fec8c557 100644 --- a/stacks/analyst/README.md +++ b/stacks/analyst/README.md @@ -38,6 +38,14 @@ lab domain's six at layer 2 ([ADR-0093]): [`scan-the-lab-with-openvas.md`](../../docs/runbooks/scan-the-lab-with-openvas.md) installs both, proves the fence and runs a scan. +## SSH: `ssh/` + +`ssh/10-garuda.conf` is installed as `/etc/ssh/sshd_config.d/10-garuda.conf` by +[`build-the-analyst-workstation.md`] §4. It makes SSH key-only, admits +`analyst` from Hicks and `phoenix` alone, and turns forwarding off. The domain's +six share VLAN 30 with `garuda`, and nothing between them filters. The same +step takes away cloud-init's passwordless `sudo`. + ## What is not here Kali Purple's SOC. Its SIEM and sensors stay off, because Wazuh on `odin`, diff --git a/stacks/analyst/compose.yaml b/stacks/analyst/compose.yaml index 4dcc4029..d2eb1073 100644 --- a/stacks/analyst/compose.yaml +++ b/stacks/analyst/compose.yaml @@ -152,6 +152,8 @@ services: command: - run - --server.http.listen-addr=0.0.0.0:12345 + # No usage reports to Grafana Labs, as Grafana and Loki send none. + - --disable-reporting - --storage.path=/var/lib/alloy/data - /etc/alloy volumes: diff --git a/stacks/analyst/ssh/10-garuda.conf b/stacks/analyst/ssh/10-garuda.conf new file mode 100644 index 00000000..4dc441bd --- /dev/null +++ b/stacks/analyst/ssh/10-garuda.conf @@ -0,0 +1,29 @@ +# sshd drop-in for garuda, installed as /etc/ssh/sshd_config.d/10-garuda.conf +# by build-the-analyst-workstation.md §4. +# +# WHY. garuda sits on VLAN 30 beside the lab domain's six, which exist to be +# compromised, and traffic inside the segment crosses no firewall. Kali's stock +# sshd takes passwords on every address, so a domain host that fell could guess +# its way in. This box holds the case notes ADR-0091 keeps off the domain for +# exactly that reason. +# +# sshd takes the first value it reads, and sshd_config includes this directory +# before its own settings, so these win. 10- sorts ahead of Kali's +# 20-systemd-userdb.conf. + +# Keys only. analyst's console password (§4) is for the desktop's login screen +# and for sudo, not for SSH. +PasswordAuthentication no +KbdInteractiveAuthentication no +PermitRootLogin no + +# Hicks (10.0.50.0/24, the trusted workstations) and phoenix (10.0.30.70, the +# deployment host: install-agent-collectors.sh). Nothing else on the segment +# needs a shell here. +AllowUsers analyst@10.0.50.0/24 analyst@10.0.30.70 + +# A seat for reading alerts, not a jump host. gsad's UI (127.0.0.1:9392) is +# used from garuda's own desktop (ADR-0093), so nothing needs a tunnel. +AllowTcpForwarding no +AllowAgentForwarding no +X11Forwarding no diff --git a/stacks/bloodhound/compose.yaml b/stacks/bloodhound/compose.yaml index 94db499a..9d42f56f 100644 --- a/stacks/bloodhound/compose.yaml +++ b/stacks/bloodhound/compose.yaml @@ -341,6 +341,8 @@ services: command: - run - --server.http.listen-addr=0.0.0.0:12345 + # No usage reports to Grafana Labs, as Grafana and Loki send none. + - --disable-reporting - --storage.path=/var/lib/alloy/data - /etc/alloy volumes: diff --git a/stacks/lab/compose.yaml b/stacks/lab/compose.yaml index 735d2851..fdf8b27b 100644 --- a/stacks/lab/compose.yaml +++ b/stacks/lab/compose.yaml @@ -484,6 +484,8 @@ services: command: - run - --server.http.listen-addr=0.0.0.0:12345 + # No usage reports to Grafana Labs, as Grafana and Loki send none. + - --disable-reporting - --storage.path=/var/lib/alloy/data - /etc/alloy volumes: diff --git a/stacks/observability/compose.yaml b/stacks/observability/compose.yaml index 48e00e78..897671eb 100644 --- a/stacks/observability/compose.yaml +++ b/stacks/observability/compose.yaml @@ -1209,6 +1209,8 @@ services: command: - run - --server.http.listen-addr=0.0.0.0:12345 + # No usage reports to Grafana Labs, as Grafana and Loki send none. + - --disable-reporting - --storage.path=/var/lib/alloy/data # The directory, so all three files load. Mounting one file here is how a # host would silently lose its Docker or syslog collection. diff --git a/stacks/sensor/compose.yaml b/stacks/sensor/compose.yaml index b9414422..6c87ef00 100644 --- a/stacks/sensor/compose.yaml +++ b/stacks/sensor/compose.yaml @@ -243,6 +243,8 @@ services: command: - run - --server.http.listen-addr=0.0.0.0:12345 + # No usage reports to Grafana Labs, as Grafana and Loki send none. + - --disable-reporting - --storage.path=/var/lib/alloy/data - /etc/alloy volumes: diff --git a/stacks/soc/compose.yaml b/stacks/soc/compose.yaml index b7310653..00d39163 100644 --- a/stacks/soc/compose.yaml +++ b/stacks/soc/compose.yaml @@ -702,6 +702,8 @@ services: command: - run - --server.http.listen-addr=0.0.0.0:12345 + # No usage reports to Grafana Labs, as Grafana and Loki send none. + - --disable-reporting - --storage.path=/var/lib/alloy/data - /etc/alloy # No depends_on, unlike the lab's Alloy. This is the guest's own collector diff --git a/stacks/soc/linux-agents.yaml b/stacks/soc/linux-agents.yaml index e9fa9df4..d511e8e0 100644 --- a/stacks/soc/linux-agents.yaml +++ b/stacks/soc/linux-agents.yaml @@ -4,16 +4,19 @@ # package from Wazuh's apt pool and refuses one that does not match. # # The version is the manager's (wazuh/wazuh-manager in compose.yaml): an agent -# may not be newer than its manager, and garuda starts level with it. The six -# Windows agents keep their own pin in ansible/roles/soc_agents/defaults/main.yml. +# may not be newer than its manager, and garuda is kept level with it. It was +# built at 4.14.8 and left there when the manager went to 4.14.10. This pin +# was brought level on 2026-11-13, and garuda takes it by +# build-the-analyst-workstation.md §12's upgrade. The six Windows agents keep +# their own pin in ansible/roles/soc_agents/defaults/main.yml. # -# The hash was taken on 2026-10-10 from the package, and agreed with the -# SHA256 that Wazuh's apt index lists for wazuh-agent 4.14.8-1. That index is +# The hash was taken on 2026-11-13 from the package, and agreed with the +# SHA256 that Wazuh's apt index lists for wazuh-agent 4.14.10-1. That index is # covered by dists/stable/InRelease, which carries a good signature from the # Wazuh Signing Key, 0DCF CA55 47B1 9D2A 6099 5060 96B3 EE5F 2911 1145. A bump # takes the new hash the same way, never from the download alone. # # The Velociraptor client .deb has no pin here: it is built on odin, by the # server, with its config inside, and staging records its hash. -wazuh_agent_deb_version: "4.14.8" -wazuh_agent_deb_sha256: "6ed202e211fd8c544d1906c9b94dcd4b503d1829eef7d0a82ce4bcf4a1688967" +wazuh_agent_deb_version: "4.14.10" +wazuh_agent_deb_sha256: "873cfec3720a7d7a344afc2c646cf5895a04caa3e4dd7b5124987264f711db94" From 6cdaf7c49f2880f08c7df6779310b2569e85e4b8 Mon Sep 17 00:00:00 2001 From: Garrett Allen <98648590+Gerrrt@users.noreply.github.com> Date: Sat, 14 Nov 2026 05:48:20 +0000 Subject: [PATCH 2/3] fix(analyst): address review on garuda's hardening MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - sshd: DisableForwarding yes in place of the three per-kind options, which left Unix-socket forwarding at its default yes; §9 checks it. - Alloy: --disable-reporting on both of deploy-agent.sh's paths (Docker and the native package's CUSTOM_ARGS) and in check_syslog_senders.sh's throwaway Alloy, so every Alloy the repo starts opts out. - Runbook §7: the steps that revoke and remove the gh token and GitHub SSH key from a guest built before the no-credential rule. Refs #921 Co-Authored-By: Claude Opus 5.5 --- docs/changelog.md | 6 +++-- .../runbooks/build-the-analyst-workstation.md | 24 ++++++++++++++++--- scripts/check_syslog_senders.sh | 2 +- scripts/deploy-agent.sh | 3 ++- stacks/analyst/ssh/10-garuda.conf | 6 ++--- 5 files changed, 31 insertions(+), 10 deletions(-) diff --git a/docs/changelog.md b/docs/changelog.md index b4516180..a05d197b 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -33,14 +33,16 @@ docstring gives: it is a record, not a claim about now. and `phoenix` with forwarding off; - the same step removes the passwordless `sudo`; - §7 reads the dotfiles release tags without a GitHub login, and `garuda` - holds no GitHub write credential. + holds no GitHub write credential. It also has the steps that revoke and + remove the two the audit found. The box itself changes when §4 is run on it, which this entry does not record. Three smaller items rode along: - `docs/architecture.md` still described the cgroup fence ADR-0093 replaced; - the Linux agent pin in `stacks/soc/linux-agents.yaml` moves from 4.14.8 to the manager's 4.14.10, with the hash checked against Wazuh's signed index; - - every Alloy runs with `--disable-reporting`, as Grafana and Loki already do. + - every Alloy runs with `--disable-reporting`, as Grafana and Loki already do: + the six stacks' Compose files, and both paths in `scripts/deploy-agent.sh`. `garuda`'s attempts to report were failing at the resolver anyway. - **`ragnarok`'s outbound scope is a filter on the VM, as ADR-0014 decided.** diff --git a/docs/runbooks/build-the-analyst-workstation.md b/docs/runbooks/build-the-analyst-workstation.md index e3191aea..57ccf054 100644 --- a/docs/runbooks/build-the-analyst-workstation.md +++ b/docs/runbooks/build-the-analyst-workstation.md @@ -350,6 +350,22 @@ The release API needs no login, and `jq` is §5's. **No GitHub credential on over https and read-only (§0). A change found here is made from Hicks or `phoenix`, which can push. +**On a guest built before this rule**, take the credentials off before §9's +check. The audit of 2026-11-13 found a `gh` login and a GitHub SSH key on +`garuda`. Revoke them first, from a trusted host: on github.com, *Settings → +SSH and GPG keys* deletes the key, and *Settings → Applications → Authorized +OAuth Apps → GitHub CLI → Revoke* ends the token (and `gh` on every other +machine, which then logs in again). Then remove the local copies on `garuda`: + +```bash +gh auth logout +git credential-cache exit +rm -f ~/.ssh/id_ed25519 ~/.ssh/id_ed25519.pub +``` + +`git credential-cache exit` drops an https login git may be holding in memory; +the clones keep pulling without one. + A good run is: - both bootstraps exit 0; @@ -409,9 +425,11 @@ Each line is a pass or a stop: `analyst-docker-socket-proxy`; - `ss -lntup` shows no Suricata, Zeek or Elastic listener. - **Access** (§4, §7). On `garuda`: - - `sudo sshd -T | grep -iE '^(passwordauthentication|permitrootlogin|allowusers) '` - prints `passwordauthentication no`, `permitrootlogin no`, and `allowusers` - for `analyst@10.0.50.0/24` and `analyst@10.0.30.70`; + - `sudo sshd -T | grep -iE '^(passwordauthentication|permitrootlogin|allowusers|disableforwarding) '` + prints `passwordauthentication no`, `permitrootlogin no`, + `disableforwarding yes`, and `allowusers` for `analyst@10.0.50.0/24` and + `analyst@10.0.30.70`. `sshd -T` still lists the separate forwarding + options as `yes`; `DisableForwarding` overrides them; - `sudo -k; sudo -n true` says a password is required; - `gh auth status` says it is not logged in, or `gh` is absent; - no key in `~/.ssh` is on the GitHub account, and any private key there has diff --git a/scripts/check_syslog_senders.sh b/scripts/check_syslog_senders.sh index 6d0f3067..deb9106d 100755 --- a/scripts/check_syslog_senders.sh +++ b/scripts/check_syslog_senders.sh @@ -171,7 +171,7 @@ docker run -d --name "${TAG}-alloy" --label homelab.logs=off \ --network "${NET}" --ip "${ALLOY_IP}" \ -e LOKI_URL="http://${LOKI_IP}:3100/loki/api/v1/push" \ -v "${WORK}/alloy:/etc/alloy:ro" -v "${WORK}/tls:/etc/syslog-tls:ro" "${ALLOY_IMAGE}" \ - run --server.http.listen-addr=0.0.0.0:12345 --storage.path=/tmp/alloy /etc/alloy >/dev/null + run --server.http.listen-addr=0.0.0.0:12345 --disable-reporting --storage.path=/tmp/alloy /etc/alloy >/dev/null wait_for() { # url seconds local i diff --git a/scripts/deploy-agent.sh b/scripts/deploy-agent.sh index 32752432..4989c42f 100755 --- a/scripts/deploy-agent.sh +++ b/scripts/deploy-agent.sh @@ -458,6 +458,7 @@ docker) -p 127.0.0.1:12345:12345 \ "$IMAGE" run \ --server.http.listen-addr=0.0.0.0:12345 \ + --disable-reporting \ --storage.path=/var/lib/alloy/data \ /etc/alloy >/dev/null @@ -527,7 +528,7 @@ native) ## Written by scripts/deploy-agent.sh from the HomeLab repository. Edits here ## are overwritten on the next deploy; change the repository instead. CONFIG_FILE="/etc/alloy" -CUSTOM_ARGS="--server.http.listen-addr=127.0.0.1:12345" +CUSTOM_ARGS="--server.http.listen-addr=127.0.0.1:12345 --disable-reporting" RESTART_ON_UPGRADE=true ALLOY_HOSTNAME="${HOSTNAME_LABEL}" LOKI_URL="${LOKI_URL}" diff --git a/stacks/analyst/ssh/10-garuda.conf b/stacks/analyst/ssh/10-garuda.conf index 4dc441bd..77b02c14 100644 --- a/stacks/analyst/ssh/10-garuda.conf +++ b/stacks/analyst/ssh/10-garuda.conf @@ -24,6 +24,6 @@ AllowUsers analyst@10.0.50.0/24 analyst@10.0.30.70 # A seat for reading alerts, not a jump host. gsad's UI (127.0.0.1:9392) is # used from garuda's own desktop (ADR-0093), so nothing needs a tunnel. -AllowTcpForwarding no -AllowAgentForwarding no -X11Forwarding no +# DisableForwarding covers TCP, Unix-socket, agent and X11 forwarding at once; +# AllowTcpForwarding alone leaves AllowStreamLocalForwarding at its default yes. +DisableForwarding yes From a7d75414bf63e524f9e9a694f338c06affd8b381 Mon Sep 17 00:00:00 2001 From: Garrett Allen <98648590+Gerrrt@users.noreply.github.com> Date: Sat, 14 Nov 2026 22:08:41 +0000 Subject: [PATCH 3/3] docs(analyst): say garuda's SSH hardening is authored, not yet live MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit security.md's row and architecture.md's line stated the hardening as the running state. It is live only once build-the-analyst-workstation.md §4 and §7 have run on garuda, so both now say so, as fenrir's row does. Refs #921 Co-Authored-By: Claude Opus 5.5 --- docs/architecture.md | 2 +- docs/security.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/architecture.md b/docs/architecture.md index 7fc73244..e3ef0781 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -195,7 +195,7 @@ hole from the monitoring VLAN into the monitored one. | `fenrir` (10.0.30.90) | 🟢 30 | [`stacks/sensor`](../stacks/sensor) | Zeek on a `tc` mirror of `Saruman`'s lab bridge, so that the domain's east-west traffic, which crosses no router and so never reaches Suricata on `morpheus`, is observed at all ([#437](https://github.com/Gerrrt/HomeLab/issues/437), [ADR-0068](adr/0068-mirror-the-lab-bridge-to-zeek-with-tc-not-open-vswitch.md)). A guest on `Saruman`, VMID 190, with a second NIC alone on the port-less `vmbr1` that receives only the mirror's copies. Its logs go to `alexander`, never to VLAN 99; whether the mirror is delivering is answered on the hypervisor by `homelab_zeek_mirror_active`. Built 2026-09-30 by [`build-the-sensor-guest.md`](runbooks/build-the-sensor-guest.md), and proved by rebooting `Saruman` with the mirror's timer disabled: the gauge fell to 0 and `ZeekMirrorInactive` fired. Alloy agent (Docker) | | `eden` (10.0.30.41) | 🟢 30 | [`stacks/bloodhound`](../stacks/bloodhound) | Off between sessions: BloodHound CE over one Postgres that holds its graph too, for attack-path analysis of `ad.matrix.elysium` — which path to Domain Admin exists, so that `odin`, `fenrir` and Sysmon can be asked whether they saw it ([#451](https://github.com/Gerrrt/HomeLab/issues/451), [ADR-0081](adr/0081-run-bloodhound-ce-on-a-saruman-guest.md)). A guest on `Saruman`, VMID 141, tagged `on-demand` so that its being off is not an alert (ADR-0079), and in no backup job: the graph is one collection run from rebuilt. Its UI is TLS on a leaf from the estate's CA, reached from Hicks; the collector's upload is intra-segment, so it adds no firewall rule. The graph is a map of the lab's weaknesses and never leaves the segment; its Alloy pushes the guest's own telemetry to `alexander`. Built 2026-10-07 by [`build-the-bloodhound-guest.md`](runbooks/build-the-bloodhound-guest.md), a full clone of template 901. Alloy agent (Docker) | | `diabolos` (10.0.30.61) | 🟢 30 | [`stacks/scratch`](../stacks/scratch) | **Not built yet**, and torn down between investigations: a DISPOSABLE copy of `stacks/soc` (Wazuh and Velociraptor on the same digests, soc's config mounted unchanged) for detonations and one-off questions, so that noise never spends `odin`'s shard budget or enters its record ([#438](https://github.com/Gerrrt/HomeLab/issues/438), [ADR-0071](adr/0071-run-disposable-investigations-on-a-guest-that-is-destroyed.md)). A guest on `Saruman`, VMID 161, created with the Proxmox tag `disposable`; the hypervisor's guest-state collector reports the tag and the guest's age, and `DisposableGuestOutlived` fires on the estate when it has existed for a fortnight, running or stopped. No Alloy, no scrape, no backup and no committed secrets: its age key and `secrets/scratch.sops.yaml` are made on the guest and destroyed with it. Built and destroyed by [`run-a-scratch-investigation.md`](runbooks/run-a-scratch-investigation.md) | -| `garuda` (10.0.30.62) | 🟢 30 | [`stacks/analyst`](../stacks/analyst) | Defense's analyst workstation: Kali Purple **without its SOC**, a seat for working the alerts `odin` raises, with CyberChef, Wireshark, and the [`dotfiles-Defense`](https://github.com/dotgibson/dotfiles-Defense) layer on `dotfiles-Debian` ([#921](https://github.com/Gerrrt/HomeLab/issues/921), [ADR-0091](adr/0091-put-a-kali-purple-analyst-workstation-on-saruman.md)). Purple's SIEM and sensor daemons stay off, because Wazuh, Zeek and Suricata already run; Defense's own `siemup` lab comes up for an exercise only. A guest on `Saruman`, VMID 162, cloned by `tofu/` from template 910 into the `analyst` pool, **always on** and not tagged `on-demand`, so a stopped `garuda` is an alert (ADR-0079), and in `golem`'s backup job, because its case notes are in no repository. Standalone, not a domain member: whoever takes the domain must not also take the notes. SSH is key-only from Hicks and `phoenix`, `sudo` asks, and it holds no GitHub write credential. Its stack is the collector alone, pushing to `alexander`. It is enrolled in `odin`'s Wazuh and Velociraptor from Debian packages staged on `odin` ([ADR-0092](adr/0092-enrol-garuda-in-odins-soc-with-debian-packages-staged-on-odin.md)); OpenVAS runs on demand and scans the domain's six only, fenced at layer 2 by an nftables `netdev` egress hook on `scan0`, the scanner's own macvlan in its own network namespace ([ADR-0093](adr/0093-run-openvas-on-garuda-on-demand-scoped-to-the-domain-by-nftables.md), [`scan-the-lab-with-openvas.md`](runbooks/scan-the-lab-with-openvas.md)). Built 2026-10-10 by [`build-the-analyst-workstation.md`](runbooks/build-the-analyst-workstation.md), a full clone of template 910. Alloy agent (Docker) | +| `garuda` (10.0.30.62) | 🟢 30 | [`stacks/analyst`](../stacks/analyst) | Defense's analyst workstation: Kali Purple **without its SOC**, a seat for working the alerts `odin` raises, with CyberChef, Wireshark, and the [`dotfiles-Defense`](https://github.com/dotgibson/dotfiles-Defense) layer on `dotfiles-Debian` ([#921](https://github.com/Gerrrt/HomeLab/issues/921), [ADR-0091](adr/0091-put-a-kali-purple-analyst-workstation-on-saruman.md)). Purple's SIEM and sensor daemons stay off, because Wazuh, Zeek and Suricata already run; Defense's own `siemup` lab comes up for an exercise only. A guest on `Saruman`, VMID 162, cloned by `tofu/` from template 910 into the `analyst` pool, **always on** and not tagged `on-demand`, so a stopped `garuda` is an alert (ADR-0079), and in `golem`'s backup job, because its case notes are in no repository. Standalone, not a domain member: whoever takes the domain must not also take the notes. Its build makes SSH key-only from Hicks and `phoenix`, makes `sudo` ask, and keeps GitHub write credentials off it (§4 and §7); until those steps have run on it, the stock defaults stand. Its stack is the collector alone, pushing to `alexander`. It is enrolled in `odin`'s Wazuh and Velociraptor from Debian packages staged on `odin` ([ADR-0092](adr/0092-enrol-garuda-in-odins-soc-with-debian-packages-staged-on-odin.md)); OpenVAS runs on demand and scans the domain's six only, fenced at layer 2 by an nftables `netdev` egress hook on `scan0`, the scanner's own macvlan in its own network namespace ([ADR-0093](adr/0093-run-openvas-on-garuda-on-demand-scoped-to-the-domain-by-nftables.md), [`scan-the-lab-with-openvas.md`](runbooks/scan-the-lab-with-openvas.md)). Built 2026-10-10 by [`build-the-analyst-workstation.md`](runbooks/build-the-analyst-workstation.md), a full clone of template 910. Alloy agent (Docker) | | `dot-debian` (10.0.30.91) | 🟢 30 | *(none — a test machine)* | Built 2026-10-08. Debian 13 (trixie), cloned from `tpl-debian-13` (903), where a release of the [dotgibson dotfiles](https://github.com/dotgibson/dotfiles-core)' Debian layer is bootstrapped on a real install. A guest on `Saruman`, VMID 191, tagged `on-demand` (ADR-0079) and off between runs, rolled back to its `clean` snapshot before each one ([#920](https://github.com/Gerrrt/HomeLab/issues/920), [ADR-0090](adr/0090-test-the-dotfiles-os-layers-on-on-demand-saruman-guests.md), [`test-the-dotfiles-layers.md`](runbooks/test-the-dotfiles-layers.md)). Standalone, not a domain member. No scrape, no backup: it is rebuilt from its template, and runs no Alloy | | `dot-fedora` (10.0.30.92) | 🟢 30 | *(none — a test machine)* | Built 2026-10-08. Fedora Server 44, cloned from `tpl-fedora-server` (904), for the dotfiles' Fedora layer. A guest on `Saruman`, VMID 192, tagged `on-demand` (ADR-0079) and off between runs, rolled back to its `clean` snapshot before each one ([#920](https://github.com/Gerrrt/HomeLab/issues/920), [ADR-0090](adr/0090-test-the-dotfiles-os-layers-on-on-demand-saruman-guests.md), [`test-the-dotfiles-layers.md`](runbooks/test-the-dotfiles-layers.md)). Standalone, not a domain member. No scrape, no backup: it is rebuilt from its template, and runs no Alloy | | `dot-opensuse` (10.0.30.93) | 🟢 30 | *(none — a test machine)* | Built 2026-10-09. openSUSE Tumbleweed, cloned from `tpl-opensuse-tw` (905), for the dotfiles' openSUSE layer. A guest on `Saruman`, VMID 193, tagged `on-demand` (ADR-0079) and off between runs, rolled back to its `clean` snapshot before each one ([#920](https://github.com/Gerrrt/HomeLab/issues/920), [ADR-0090](adr/0090-test-the-dotfiles-os-layers-on-on-demand-saruman-guests.md), [`test-the-dotfiles-layers.md`](runbooks/test-the-dotfiles-layers.md)). Standalone, not a domain member. No scrape, no backup: it is rebuilt from its template, and runs no Alloy | diff --git a/docs/security.md b/docs/security.md index 7fa3f17e..0f619423 100644 --- a/docs/security.md +++ b/docs/security.md @@ -22,7 +22,7 @@ What this network is actually built to survive: | Someone on the lab segment reading the domain's metrics | **Scoped, unauthenticated, and accepted.** `windows_exporter` listens on `9182` on all six of ADR-0029's guests, on the segment that exists to hold attackers. Each guest's Windows Firewall admits `10.0.30.40` (`alexander`) alone ([`build-the-lab-domain.md`](runbooks/build-the-lab-domain.md) §7). That rule is the difference between an endpoint a `/24` sweep finds and one you have to go looking for, and it is a residual, not a control: the exporter has no authentication, so whoever owns `alexander` reads every gauge. It has no write API, so what leaks is the shape of the host (services, disks, uptime, the licence clock), the same class as `node_exporter` on the NAS below | | Someone on the lab segment fetching a Velociraptor client package | **Scoped by address, and accepted.** `odin`'s `caddy` service (container `soc-agent-msi`) serves the two agent MSIs `ansible/` installs, and the two Debian packages `garuda` installs, on `8448` ([#1068](https://github.com/Gerrrt/HomeLab/issues/1068), [ADR-0092](adr/0092-enrol-garuda-in-odins-soc-with-debian-packages-staged-on-odin.md)). Its Caddyfile answers the six domain addresses, `.50`–`.55`, and `garuda`'s `.62`, and refuses everyone else, Hicks's route from VLAN 50 included. The Velociraptor MSI and the Velociraptor `.deb` each carry the client config: the server URL, its CA, and the enrolment nonce. Whoever holds either can enrol a rogue client, which is why #864 took the MSI out of SYSVOL. The residual: an address on this segment can be spoofed, and any of the seven can fetch either package. What the nonce buys is noise in Velociraptor's client list, the same class as the Wazuh enrolment password. Rotating it means a new server config, a rebuilt MSI with the role's pin moved, and the staged `.deb` deleted so that `scripts/stage-agent-msis.sh` builds it again. The two Wazuh packages carry no secret | | An OpenVAS plugin on `garuda` reaching a non-target | **Fenced at layer 2** ([ADR-0093](adr/0093-run-openvas-on-garuda-on-demand-scoped-to-the-domain-by-nftables.md)). OpenVAS scans the domain's six only. While a scan runs, the scanner runs in a network namespace whose one route to the segment is a macvlan, `scan0` at `10.0.30.63`, behind an nftables `netdev` egress hook that drops every frame — IP, ARP (`send_arp_request`) and raw (`send_frame`) — to anything but the six. So the AF_PACKET paths that bypass an IP filter are caught here. Proved: a GVM scan of an out-of-scope host raised the egress drop counter with 0 open ports, and an `arping` from the namespace to an out-of-scope host was dropped while an in-scope host answered. `gvmd`, `notus`, `redis` and `mosquitto` stay on the host; only MQTT crosses to the namespace, on a veth that routes nowhere else | -| Someone on the lab segment logging in to `garuda` | **Closed at `sshd`.** `garuda` shares VLAN 30 with the domain's six, and nothing between them filters. Its `sshd` takes keys only, admits `analyst` from Hicks's `10.0.50.0/24` and `phoenix`'s `10.0.30.70` alone, and forwards nothing; `sudo` asks for the console password ([`stacks/analyst/ssh/10-garuda.conf`](../stacks/analyst/ssh/10-garuda.conf), [`build-the-analyst-workstation.md`](runbooks/build-the-analyst-workstation.md) §4). It holds no GitHub write credential, so taking it does not reach the repository the estate converges from. The audit of 2026-11-13 found it taking passwords from the whole segment, with passwordless `sudo` and an owner-scoped GitHub token on it | +| Someone on the lab segment logging in to `garuda` | **Authored, and live only once applied.** `garuda` shares VLAN 30 with the domain's six, and nothing between them filters. Its `sshd` takes keys only, admits `analyst` from Hicks's `10.0.50.0/24` and `phoenix`'s `10.0.30.70` alone, and forwards nothing; `sudo` asks for the console password ([`stacks/analyst/ssh/10-garuda.conf`](../stacks/analyst/ssh/10-garuda.conf), [`build-the-analyst-workstation.md`](runbooks/build-the-analyst-workstation.md) §4). It holds no GitHub write credential, so taking it does not reach the repository the estate converges from. The audit of 2026-11-13 found it taking passwords from the whole segment, with passwordless `sudo` and an owner-scoped GitHub token on it. Until §4's step and §7's credential removal have run on `garuda`, that is still the running state | | Someone on the lab segment reaching the SOC's web UIs | **Scoped by address, and accepted.** The Wazuh dashboard (`443`) and the Velociraptor GUI (`8889`) on `odin` are published by the stack's `caddy` service (container `soc-agent-msi`), not by their own containers, and its Caddyfile answers Hicks's `10.0.50.0/24` and refuses everyone else ([#1139](https://github.com/Gerrrt/HomeLab/issues/1139)). Until then both answered all of VLAN 30, the six domain guests included, because traffic between VLAN 30 hosts never crosses pfSense — so no firewall rule could have narrowed them. WireGuard peers are not in the list. The residuals: Hicks is a whole /24 with no per-host source yet ([ADR-0031](adr/0031-narrow-hicks-to-a-named-list-on-winterfell-and-leave-the-lab-open.md)), and an address on VLAN 30 can be spoofed, so this narrows who reaches the login pages and does not replace them. And the hop from Caddy to Velociraptor skips certificate verification: the GUI's certificate is signed by the CA inside the gitignored `server.config.yaml` on `odin`, and the hop is container to container on the `soc` bridge. The dashboard hop is verified against the Wazuh generator's root | | Someone on the lab segment reaching `eden`'s SSH | **Key-only, unfenced, and accepted.** `eden` holds the map of the weak domain's attack paths, and its `sshd` answers all of VLAN 30, the six domain guests included: the guest runs with `firewall=0` and no host firewall, and nothing in this repository fences `sshd` on any Linux lab guest. What bounds it is the key. `sshd` refuses passwords, and `garnet` takes two keys, the operator's and `phoenix`'s, which `tofu/` installs; `root`'s copies are command-restricted by cloud-init. `garnet`'s sudo is `NOPASSWD`, from the default user Proxmox's cloud-init makes on every Linux guest `tofu/` builds, and is accepted with it: on `eden` `garnet` is in the `docker` group, which is root already. A fence by source address, as the domain's six have for `phoenix`, would be its own decision for every Linux guest, not `eden`'s alone | | Someone on the lab segment reaching the lab's Grafana or BloodHound, or a detonation reaching `diabolos`'s UIs | **Scoped by address, as the SOC's are.** `alexander`'s Grafana (`3000`) is published by the lab stack's `caddy`, and `eden`'s BloodHound (`8443`) by the bloodhound stack's own `caddy`; each answers Hicks's `10.0.50.0/24` and refuses the rest of VLAN 30, the six domain guests included. `diabolos`, not yet built, has soc's fence for its dashboard and GUI. Until then Grafana and BloodHound answered all of VLAN 30, where BloodHound's graph is the domain's attack-path map ([#1428](https://github.com/Gerrrt/HomeLab/issues/1428)). The residuals are the SOC row's: Hicks is a whole /24, and an address on VLAN 30 can be spoofed for a one-way packet but not for the TLS session the UIs need |