diff --git a/SECURITY.md b/SECURITY.md
index dfb559ef..c91892de 100644
--- a/SECURITY.md
+++ b/SECURITY.md
@@ -47,7 +47,7 @@ is a very different thing from an overlooked one. Full detail in
| Alertmanager webhook URL and the MokerLink SNMP community, in a local Claude Code session transcript | Found 2026-08-20 by a value-level sweep of the host. Redacted in place; mode 600, never committed or synced. The webhook topic was rotated — on the public ntfy instance the topic name *is* the credential, there is nothing to revoke — and delivery re-verified end to end. The switch community deliberately was not: rotating it means the `neo` residual above all over again. **That reasoning is spent, and this value now retires with the hardware.** The overwrite it was afraid of was done on 2026-09-12 and persisted, and the MokerLink is being replaced by a switch commissioned with SNMP v2c off entirely — so what retires this community is not a fresh community but the absence of one. The thing that must not be carried across is any value the MokerLink held, passphrases included. **Retired 2026-11-05**, when the CRS326 replaced it with no v2c community at all and v3 passphrases generated fresh ([#444](https://github.com/Gerrrt/HomeLab/issues/444)); the switch section above has the proof. |
| Pre-purge objects still served by GitHub after the history rewrite | The 2026-08-19 rewrite (`021d2b6`) removed both secrets above from every *reachable* commit, but GitHub still serves the orphaned objects by SHA. Verified 2026-08-26: `647d90a`, `21afcad`, `efb2632` and `ee3d443` all still resolve through the API, and the tree at `21afcad` still lists `certificates/Gandalf.Gondor.Lab/ca-key.pem` and `cert-key.pem`. Garbage collection requested from GitHub Support on 2026-08-26 — **pending**; this is the [purge runbook](docs/runbooks/purge-git-history.md)'s *Afterwards* step, and it is the last one outstanding. The repository has no forks and a network count of 0, so nothing else is perpetuating them. Both credentials were rotated *before* the rewrite, so this changes nothing about their status: the old keys and the old community remain superseded and must still be treated as public. Re-check with `gh api repos/Gerrrt/HomeLab/commits/647d90a --jq .sha` — a `404` means GitHub has collected them. |
| Alertmanager published on `0.0.0.0`, letting anyone who could reach it silence an alert | Fixed 2026-08-30 — 9093 now binds to `127.0.0.1` ([#70](https://github.com/Gerrrt/HomeLab/issues/70), [ADR-0012](docs/adr/0012-publish-only-ports-with-an-off-host-consumer.md)). This was the sharpest of the three because a silence switches off monitoring and the record of it lives in the system being switched off. Nothing off-host ever used the port: silences are reached through Grafana, which proxies Alertmanager over the compose network behind a login, so closing it cost no capability. |
-| Prometheus and Loki published on `0.0.0.0` with no authentication | Fixed — [#182](https://github.com/Gerrrt/HomeLab/issues/182), [ADR-0067](docs/adr/0067-authenticate-the-ingest-ports-with-a-token-per-client.md). Until then anything that could route to `10.0.99.20:9090` or `:3100` could read every metric and log line, inject metrics through the remote-write receiver, and delete log ranges through Loki's delete API, with firewall default-deny as the whole control. Prometheus and Loki now publish on `127.0.0.1` only. The host's address on both ports is an ingest proxy that serves a push to one token per agent (`oracle`, `trinity`, `Saruman`), a query to one reader token (Glance, Home Assistant, the deploy script's arrival check), and the admin and delete APIs to nobody. `IngestAuthNotEnforced` pages if the published address ever answers a token-less request with anything but the proxy's 401. The tokens no longer cross the wire in cleartext: both ports serve TLS under the estate CA, and every client verifies it ([#764](https://github.com/Gerrrt/HomeLab/issues/764), [ADR-0086](docs/adr/0086-serve-the-ingest-ports-over-tls-under-the-estate-ca.md)). **One thing remains, deliberately:** loopback is unauthenticated, because a local user on the monitoring host already holds the SOPS key. The firewall still narrows who can try: Winterfell (99) itself and `Saruman`'s one pass from `10.0.30.110`. |
+| Prometheus and Loki published on `0.0.0.0` with no authentication | Fixed — [#182](https://github.com/Gerrrt/HomeLab/issues/182), [ADR-0067](docs/adr/0067-authenticate-the-ingest-ports-with-a-token-per-client.md). Until then anything that could route to `10.0.99.20:9090` or `:3100` could read every metric and log line, inject metrics through the remote-write receiver, and delete log ranges through Loki's delete API, with firewall default-deny as the whole control. Prometheus and Loki now publish on `127.0.0.1` only. The host's address on both ports is an ingest proxy that serves a push to one token per agent (`oracle`, `trinity`, `Saruman`), a query to one reader token (Glance, Home Assistant, the deploy script's arrival check), and the admin and delete APIs to nobody. `IngestAuthNotEnforced` pages if the published address ever answers a token-less request with anything but the proxy's 401. The tokens no longer cross the wire in cleartext: both ports serve TLS under the estate CA, and every client verifies it ([#764](https://github.com/Gerrrt/HomeLab/issues/764), [ADR-0086](docs/adr/0086-serve-the-ingest-ports-over-tls-under-the-estate-ca.md)). **One thing remains, deliberately:** loopback is unauthenticated, because a local user on the monitoring host already holds the SOPS key. The firewall still narrows who can try: Winterfell (99) itself and `Saruman`'s one pass from `10.0.30.20`. |
| The lab's Prometheus and Loki published to VLAN 30 with no authentication | **Open until the rollout lands.** The fix is authored ([#834](https://github.com/Gerrrt/HomeLab/issues/834)), and none of the four hosts converges, so it is live only once `stacks/lab/README.md`'s ordered rollout has run and its three `curl`s return 401. `alexander` published both to the segment that exists to hold attackers (#436, 2026-09). Anything on it could `POST /-/quit`, forge series and delete log ranges in the lab's evidence store. Once rolled out, they are on `127.0.0.1`, behind `stacks/lab/Caddyfile`: one token each for `odin`, `phoenix` and `fenrir`, and a reader token, all the lab's own and never the estate's. **Two residuals.** The tokens cross VLAN 30 as plain HTTP, where ARP spoofing is expected (#764's question). And nothing pages if the proxy is bypassed, because the lab has no Alertmanager (ADR-0020, [#858](https://github.com/Gerrrt/HomeLab/issues/858)). |
| The monitoring host's disk and swap are unencrypted | **Accepted residual, not a fix in progress** — see below. |
| Loki's log volume has no size ceiling | **Accepted residual, not a fix in progress** — see below. Prometheus has a byte ceiling since [#184](https://github.com/Gerrrt/HomeLab/issues/184); Loki has no size-based retention to set, only time. `HostDiskWillFillIn24h` is the control. |
diff --git a/docs/adr/0007-defensive-estate-and-offensive-range.md b/docs/adr/0007-defensive-estate-and-offensive-range.md
index f3cf3a55..7cb3b825 100644
--- a/docs/adr/0007-defensive-estate-and-offensive-range.md
+++ b/docs/adr/0007-defensive-estate-and-offensive-range.md
@@ -89,6 +89,13 @@
> filesystems are crosses, read by the hypervisor through the guest agent and
> sent over `Saruman`'s existing pass. No path is opened, and nothing else a
> guest produces crosses. The text here is left as written, per ADR-0001.
+>
+> **`Saruman`'s address.** `Saruman` moved from `10.0.30.110` to
+> **`10.0.30.20`** on 2026-11-07, as
+> [`build-the-playground.md`](../runbooks/build-the-playground.md) §3 has it
+> ([#421](https://github.com/Gerrrt/HomeLab/issues/421)). Each rule, export,
+> alias and command below that names `.110` now names `.20`, and the decision
+> is unchanged. The text below is left as written, per ADR-0001.
## Context
diff --git a/docs/adr/0016-open-casabonita-inward-and-keep-it-terminal-outward.md b/docs/adr/0016-open-casabonita-inward-and-keep-it-terminal-outward.md
index 029863b8..0855ea4a 100644
--- a/docs/adr/0016-open-casabonita-inward-and-keep-it-terminal-outward.md
+++ b/docs/adr/0016-open-casabonita-inward-and-keep-it-terminal-outward.md
@@ -54,6 +54,13 @@
> ImaginationLAN of the shape ADR-0053 gives `golem`
> ([ADR-0072](0072-put-the-iso-store-on-smaug-over-nfs-to-saruman-alone.md)).
> Same kind of pass, host- and port-scoped, and the table is not edited.
+>
+> **`Saruman`'s address.** `Saruman` moved from `10.0.30.110` to
+> **`10.0.30.20`** on 2026-11-07, as
+> [`build-the-playground.md`](../runbooks/build-the-playground.md) §3 has it
+> ([#421](https://github.com/Gerrrt/HomeLab/issues/421)). Each rule, export,
+> alias and command below that names `.110` now names `.20`, and the decision
+> is unchanged. The text below is left as written, per ADR-0001.
## Context
diff --git a/docs/adr/0017-buy-ifrit-for-iops-and-keep-the-range-disposable.md b/docs/adr/0017-buy-ifrit-for-iops-and-keep-the-range-disposable.md
index 2a5bf2c6..6673d39b 100644
--- a/docs/adr/0017-buy-ifrit-for-iops-and-keep-the-range-disposable.md
+++ b/docs/adr/0017-buy-ifrit-for-iops-and-keep-the-range-disposable.md
@@ -39,6 +39,13 @@
> ([#451](https://github.com/Gerrrt/HomeLab/issues/451)). The collector still
> runs from the range when there is one. The text here is left as written, per
> ADR-0001.
+>
+> **`Saruman`'s address.** `Saruman` moved from `10.0.30.110` to
+> **`10.0.30.20`** on 2026-11-07, as
+> [`build-the-playground.md`](../runbooks/build-the-playground.md) §3 has it
+> ([#421](https://github.com/Gerrrt/HomeLab/issues/421)). Each rule, export,
+> alias and command below that names `.110` now names `.20`, and the decision
+> is unchanged. The text below is left as written, per ADR-0001.
## Context
diff --git a/docs/adr/0020-run-the-lab-stack-in-a-guest-with-its-own-prometheus.md b/docs/adr/0020-run-the-lab-stack-in-a-guest-with-its-own-prometheus.md
index e3bcb61b..3f102eed 100644
--- a/docs/adr/0020-run-the-lab-stack-in-a-guest-with-its-own-prometheus.md
+++ b/docs/adr/0020-run-the-lab-stack-in-a-guest-with-its-own-prometheus.md
@@ -2,6 +2,14 @@
**Status:** Accepted · 2026-09
+> [!NOTE]
+> **`Saruman`'s address.** `Saruman` moved from `10.0.30.110` to
+> **`10.0.30.20`** on 2026-11-07, as
+> [`build-the-playground.md`](../runbooks/build-the-playground.md) §3 has it
+> ([#421](https://github.com/Gerrrt/HomeLab/issues/421)). Each rule, export,
+> alias and command below that names `.110` now names `.20`, and the decision
+> is unchanged. The text below is left as written, per ADR-0001.
+
## Context
[ADR-0007](0007-defensive-estate-and-offensive-range.md) gave `Saruman` the
diff --git a/docs/adr/0028-let-guest-liveness-cross-but-not-guest-telemetry.md b/docs/adr/0028-let-guest-liveness-cross-but-not-guest-telemetry.md
index c5c09f33..c51e505e 100644
--- a/docs/adr/0028-let-guest-liveness-cross-but-not-guest-telemetry.md
+++ b/docs/adr/0028-let-guest-liveness-cross-but-not-guest-telemetry.md
@@ -22,6 +22,13 @@
> `qm guest exec`. This closes the half the first consequence below leaves
> open, for those services only. Everything else in "What services it runs,
> and their health" stays in the lab.
+>
+> **`Saruman`'s address.** `Saruman` moved from `10.0.30.110` to
+> **`10.0.30.20`** on 2026-11-07, as
+> [`build-the-playground.md`](../runbooks/build-the-playground.md) §3 has it
+> ([#421](https://github.com/Gerrrt/HomeLab/issues/421)). Each rule, export,
+> alias and command below that names `.110` now names `.20`, and the decision
+> is unchanged. The text below is left as written, per ADR-0001.
## Context
diff --git a/docs/adr/0039-decline-proxmox-clustering-while-ifrit-is-the-range.md b/docs/adr/0039-decline-proxmox-clustering-while-ifrit-is-the-range.md
index b522f495..b0c70979 100644
--- a/docs/adr/0039-decline-proxmox-clustering-while-ifrit-is-the-range.md
+++ b/docs/adr/0039-decline-proxmox-clustering-while-ifrit-is-the-range.md
@@ -25,6 +25,13 @@
> converted. The argument here is unchanged: the mirror is on `Saruman`'s own
> bridge, so what removes Zeek's view is moving the domain guests off it, not
> a cluster.
+>
+> **`Saruman`'s address.** `Saruman` moved from `10.0.30.110` to
+> **`10.0.30.20`** on 2026-11-07, as
+> [`build-the-playground.md`](../runbooks/build-the-playground.md) §3 has it
+> ([#421](https://github.com/Gerrrt/HomeLab/issues/421)). Each rule, export,
+> alias and command below that names `.110` now names `.20`, and the decision
+> is unchanged. The text below is left as written, per ADR-0001.
## Context
diff --git a/docs/adr/0049-shut-down-on-the-ups-from-a-nut-server-on-the-firewall.md b/docs/adr/0049-shut-down-on-the-ups-from-a-nut-server-on-the-firewall.md
index 0c66aec8..e7d6d1a2 100644
--- a/docs/adr/0049-shut-down-on-the-ups-from-a-nut-server-on-the-firewall.md
+++ b/docs/adr/0049-shut-down-on-the-ups-from-a-nut-server-on-the-firewall.md
@@ -47,6 +47,13 @@ power source [#413](https://github.com/Gerrrt/HomeLab/issues/413) owed
> the intent of reusing it, and it is now true on the card as well as here.
> A user of NUT's own remains the way to separate them. The decision is
> unchanged.
+>
+> **`Saruman`'s address.** `Saruman` moved from `10.0.30.110` to
+> **`10.0.30.20`** on 2026-11-07, as
+> [`build-the-playground.md`](../runbooks/build-the-playground.md) §3 has it
+> ([#421](https://github.com/Gerrrt/HomeLab/issues/421)). Each rule, export,
+> alias and command below that names `.110` now names `.20`, and the decision
+> is unchanged. The text below is left as written, per ADR-0001.
## Context
diff --git a/docs/adr/0068-mirror-the-lab-bridge-to-zeek-with-tc-not-open-vswitch.md b/docs/adr/0068-mirror-the-lab-bridge-to-zeek-with-tc-not-open-vswitch.md
index 690adffd..061526f8 100644
--- a/docs/adr/0068-mirror-the-lab-bridge-to-zeek-with-tc-not-open-vswitch.md
+++ b/docs/adr/0068-mirror-the-lab-bridge-to-zeek-with-tc-not-open-vswitch.md
@@ -12,6 +12,13 @@
> [ADR-0069](0069-vendor-the-ja4-scripts-into-the-sensor-stack-rather-than-build-an-image.md)
> decides. [`stacks/sensor/README.md`](../../stacks/sensor/README.md) has the
> query that reads them. The text below is left as written, per ADR-0001.
+>
+> **`Saruman`'s address.** `Saruman` moved from `10.0.30.110` to
+> **`10.0.30.20`** on 2026-11-07, as
+> [`build-the-playground.md`](../runbooks/build-the-playground.md) §3 has it
+> ([#421](https://github.com/Gerrrt/HomeLab/issues/421)). Each rule, export,
+> alias and command below that names `.110` now names `.20`, and the decision
+> is unchanged. The text below is left as written, per ADR-0001.
## Context
diff --git a/docs/adr/0072-put-the-iso-store-on-smaug-over-nfs-to-saruman-alone.md b/docs/adr/0072-put-the-iso-store-on-smaug-over-nfs-to-saruman-alone.md
index 6dae441f..5820c6e8 100644
--- a/docs/adr/0072-put-the-iso-store-on-smaug-over-nfs-to-saruman-alone.md
+++ b/docs/adr/0072-put-the-iso-store-on-smaug-over-nfs-to-saruman-alone.md
@@ -17,6 +17,13 @@ on ImaginationLAN rather than Hicks or Winterfell; decides
> §2b). `phoenix` holds read-only `PVEAuditor` on the store, so it cannot
> write to it either. What this costs is up to a day between a replacement
> and the page. The text below is left as written, per ADR-0001.
+>
+> **`Saruman`'s address.** `Saruman` moved from `10.0.30.110` to
+> **`10.0.30.20`** on 2026-11-07, as
+> [`build-the-playground.md`](../runbooks/build-the-playground.md) §3 has it
+> ([#421](https://github.com/Gerrrt/HomeLab/issues/421)). Each rule, export,
+> alias and command below that names `.110` now names `.20`, and the decision
+> is unchanged. The text below is left as written, per ADR-0001.
## Context
diff --git a/docs/adr/0079-tag-on-demand-guests-and-leave-them-out-of-the-stopped-guest-alert.md b/docs/adr/0079-tag-on-demand-guests-and-leave-them-out-of-the-stopped-guest-alert.md
index 67ba29bc..60baa108 100644
--- a/docs/adr/0079-tag-on-demand-guests-and-leave-them-out-of-the-stopped-guest-alert.md
+++ b/docs/adr/0079-tag-on-demand-guests-and-leave-them-out-of-the-stopped-guest-alert.md
@@ -2,6 +2,14 @@
**Status:** Accepted · 2026-10
+> [!NOTE]
+> **`Saruman`'s address.** `Saruman` moved from `10.0.30.110` to
+> **`10.0.30.20`** on 2026-11-07, as
+> [`build-the-playground.md`](../runbooks/build-the-playground.md) §3 has it
+> ([#421](https://github.com/Gerrrt/HomeLab/issues/421)). Each rule, export,
+> alias and command below that names `.110` now names `.20`, and the decision
+> is unchanged. The text below is left as written, per ADR-0001.
+
## Context
`HypervisorGuestStopped` (`stacks/observability/prometheus/rules/host.rules.yaml`)
diff --git a/docs/adr/0084-read-management-certificate-expiry-from-inside-each-segment.md b/docs/adr/0084-read-management-certificate-expiry-from-inside-each-segment.md
index a248f3f3..6857dca2 100644
--- a/docs/adr/0084-read-management-certificate-expiry-from-inside-each-segment.md
+++ b/docs/adr/0084-read-management-certificate-expiry-from-inside-each-segment.md
@@ -2,6 +2,14 @@
**Status:** Accepted · 2026-10
+> [!NOTE]
+> **`Saruman`'s address.** `Saruman` moved from `10.0.30.110` to
+> **`10.0.30.20`** on 2026-11-07, as
+> [`build-the-playground.md`](../runbooks/build-the-playground.md) §3 has it
+> ([#421](https://github.com/Gerrrt/HomeLab/issues/421)). Each rule, export,
+> alias and command below that names `.110` now names `.20`, and the decision
+> is unchanged. The text below is left as written, per ADR-0001.
+
## Context
Four management consoles serve self-signed certificates that nothing watched
diff --git a/docs/adr/0106-name-the-range-host-gandalf-and-leave-ifrit-to-its-disabled-amt.md b/docs/adr/0106-name-the-range-host-gandalf-and-leave-ifrit-to-its-disabled-amt.md
index 91352885..0b19fce1 100644
--- a/docs/adr/0106-name-the-range-host-gandalf-and-leave-ifrit-to-its-disabled-amt.md
+++ b/docs/adr/0106-name-the-range-host-gandalf-and-leave-ifrit-to-its-disabled-amt.md
@@ -4,6 +4,14 @@
[ADR-0014](0014-put-ifrit-on-imaginationlan-and-give-the-targets-no-route.md)
and [ADR-0017](0017-buy-ifrit-for-iops-and-keep-the-range-disposable.md)
+> [!NOTE]
+> **`Saruman`'s address.** `Saruman` moved from `10.0.30.110` to
+> **`10.0.30.20`** on 2026-11-07, as
+> [`build-the-playground.md`](../runbooks/build-the-playground.md) §3 has it
+> ([#421](https://github.com/Gerrrt/HomeLab/issues/421)). Each rule, export,
+> alias and command below that names `.110` now names `.20`, and the decision
+> is unchanged. The text below is left as written, per ADR-0001.
+
## Context
[ADR-0014](0014-put-ifrit-on-imaginationlan-and-give-the-targets-no-route.md)
diff --git a/docs/architecture.md b/docs/architecture.md
index cb1e855e..003818b6 100644
--- a/docs/architecture.md
+++ b/docs/architecture.md
@@ -185,7 +185,7 @@ hole from the monitoring VLAN into the monitored one.
| Host | VLAN | Stack | Contents |
| --- | --- | --- | --- |
| `prometheus` (10.0.99.20) | 🔴 99 | [`stacks/observability`](../stacks/observability) | Prometheus, Alertmanager, Loki, Grafana, snmp-exporter, blackbox-exporter, speedtest-tracker, docker-socket-proxy, Alloy |
-| `Saruman` (10.0.30.110) | 🟢 30 | *(none — and none intended)* | Proxmox VE 9, hosting twenty-one guests: `alexander`, built 2026-09-05 ([#262](https://github.com/Gerrrt/HomeLab/issues/262)); `phoenix`, built 2026-09-20 ([#436](https://github.com/Gerrrt/HomeLab/issues/436)); `odin`, built 2026-09-27 for the security tooling (ADR-0030); `fenrir`, built 2026-09-30 as the Zeek sensor ([#437](https://github.com/Gerrrt/HomeLab/issues/437)); `golem`, built 2026-10-03 as the backup server ([#485](https://github.com/Gerrrt/HomeLab/issues/485)); `eden`, built 2026-10-07 as the BloodHound guest ([#451](https://github.com/Gerrrt/HomeLab/issues/451)), off between sessions; `garuda`, built 2026-10-10 as the analyst workstation ([#921](https://github.com/Gerrrt/HomeLab/issues/921)); and ADR-0029's six-machine domain, built by hand 2026-09-24 to 2026-09-25 ([#414](https://github.com/Gerrrt/HomeLab/issues/414)), of which `carbuncle` and `siren` run per session; and all eight of [ADR-0090](adr/0090-test-the-dotfiles-os-layers-on-on-demand-saruman-guests.md)'s on-demand dotfiles test VMs, `dot-debian`, `dot-fedora` and `dot-windows`, built 2026-10-08, and `dot-opensuse`, `dot-arch`, `dot-alpine`, `dot-gentoo` and `dot-nixos`, built 2026-10-09 ([#920](https://github.com/Gerrrt/HomeLab/issues/920)). Alloy agent (native package). It runs no compose stack by decision, not by omission: Docker would rewrite the iptables its own firewall relies on ([ADR-0014](adr/0014-put-ifrit-on-imaginationlan-and-give-the-targets-no-route.md)), which is why the agent here is the native package and why `stacks/lab` runs in a guest |
+| `Saruman` (10.0.30.20) | 🟢 30 | *(none — and none intended)* | Proxmox VE 9, hosting twenty-one guests: `alexander`, built 2026-09-05 ([#262](https://github.com/Gerrrt/HomeLab/issues/262)); `phoenix`, built 2026-09-20 ([#436](https://github.com/Gerrrt/HomeLab/issues/436)); `odin`, built 2026-09-27 for the security tooling (ADR-0030); `fenrir`, built 2026-09-30 as the Zeek sensor ([#437](https://github.com/Gerrrt/HomeLab/issues/437)); `golem`, built 2026-10-03 as the backup server ([#485](https://github.com/Gerrrt/HomeLab/issues/485)); `eden`, built 2026-10-07 as the BloodHound guest ([#451](https://github.com/Gerrrt/HomeLab/issues/451)), off between sessions; `garuda`, built 2026-10-10 as the analyst workstation ([#921](https://github.com/Gerrrt/HomeLab/issues/921)); and ADR-0029's six-machine domain, built by hand 2026-09-24 to 2026-09-25 ([#414](https://github.com/Gerrrt/HomeLab/issues/414)), of which `carbuncle` and `siren` run per session; and all eight of [ADR-0090](adr/0090-test-the-dotfiles-os-layers-on-on-demand-saruman-guests.md)'s on-demand dotfiles test VMs, `dot-debian`, `dot-fedora` and `dot-windows`, built 2026-10-08, and `dot-opensuse`, `dot-arch`, `dot-alpine`, `dot-gentoo` and `dot-nixos`, built 2026-10-09 ([#920](https://github.com/Gerrrt/HomeLab/issues/920)). Alloy agent (native package). It runs no compose stack by decision, not by omission: Docker would rewrite the iptables its own firewall relies on ([ADR-0014](adr/0014-put-ifrit-on-imaginationlan-and-give-the-targets-no-route.md)), which is why the agent here is the native package and why `stacks/lab` runs in a guest |
| `gandalf` (10.0.30.30) | 🟢 30 | *(none — and none intended)* | Proxmox VE 9, the range host, installed 2026-11-07 ([#421](https://github.com/Gerrrt/HomeLab/issues/421)). No guests yet. Guest disks go on the ZFS pool `range`, targets go on the portless `vmbr1`, and it is off between sessions ([ADR-0017](adr/0017-buy-ifrit-for-iops-and-keep-the-range-disposable.md), [ADR-0106](adr/0106-name-the-range-host-gandalf-and-leave-ifrit-to-its-disabled-amt.md)) |
| `alexander` (10.0.30.40) | 🟢 30 | [`stacks/lab`](../stacks/lab) | Prometheus, Loki, Grafana, Alloy and its Docker socket proxy, and Caddy as the ingest proxy in front of the first two ([#834](https://github.com/Gerrrt/HomeLab/issues/834)) — the lab's own observability, which never remote-writes to VLAN 99 ([ADR-0007](adr/0007-defensive-estate-and-offensive-range.md), [ADR-0020](adr/0020-run-the-lab-stack-in-a-guest-with-its-own-prometheus.md)). A guest on `Saruman`, not the hypervisor; Alloy agent (Docker) |
| `odin` (10.0.30.60) | 🟢 30 | [`stacks/soc`](../stacks/soc) | Wazuh (indexer, manager, dashboard), Velociraptor and Alloy: the security half of ADR-0007, placed by [ADR-0030](adr/0030-give-the-security-tooling-its-own-guest-and-its-own-stack.md) on a second guest on `Saruman` because `alexander`'s 8 GiB cannot hold both. Built 2026-09-27 by [`build-the-soc-guest.md`](runbooks/build-the-soc-guest.md), the stack authored and CI-validated ahead of the guest the way `stacks/lab` was ahead of `alexander` ([#266](https://github.com/Gerrrt/HomeLab/issues/266), [#267](https://github.com/Gerrrt/HomeLab/issues/267)); the six domain machines enrol as agents by GPO, which is the day those two issues close. Its Alloy pushes to `alexander`, never to VLAN 99, and the indexer's health is the one series that crosses into the lab's Prometheus. Alloy agent (Docker) |
diff --git a/docs/changelog.md b/docs/changelog.md
index b4452750..f2496529 100644
--- a/docs/changelog.md
+++ b/docs/changelog.md
@@ -19,6 +19,29 @@ docstring gives: it is a record, not a claim about now.
## 2026-11-07
+- **`Saruman` moved from `10.0.30.110` to `10.0.30.20`**
+ ([#421](https://github.com/Gerrrt/HomeLab/issues/421),
+ [`build-the-playground.md`](runbooks/build-the-playground.md) §3). The
+ hypervisor no longer sits inside ImaginationLAN's `.100–.200` DHCP pool, the
+ pool the attack VM will lease from. The `.20` rules and export were added
+ first and the `.110` ones removed only after every path had been checked
+ over `.20`: the Alloy push, NUT, the ISO store's NFS mount, `8006` from
+ Hicks (and refused from `morpheus`), and `phoenix`'s API call with TLS
+ verified. The same 10 guests stayed running throughout. The new pfSense
+ rules kept their copy descriptions, which end in `(.20)`.
+
+ §3 had listed two things that name the address. A grep of the repository
+ found six more on the hosts: the NUT pass, the host firewall's
+ `local_network` alias, the API certificate's names, `phoenix.env`, the
+ Mac's `known_hosts` and its SSH config. If the alias had stayed at `.110`,
+ the next DHCP client to lease `.110` would have joined Proxmox's
+ `management` set. The cutover also turned up a trap: `ifreload` starts
+ with `#!/usr/bin/env python3`, and root's dotfiles put mise's Python first,
+ so a plain `ifreload -a` failed with `ModuleNotFoundError` and changed
+ nothing. Under `env -i` with the system PATH it worked. Boot was never at
+ risk, because `networking.service` runs with systemd's PATH. §3 now
+ records all of this, and the twelve ADRs that quote `.110` carry a note.
+
- **`DiskIoErrors` fired on a healthy disk, and its ATA pattern now needs the
kernel's prefix.** `oracle` sent it at 22:33 and 23:28 UTC, each time
clearing 15 minutes later. The disk was fine: no kernel I/O error, SMART
diff --git a/docs/diagrams/current/network.png b/docs/diagrams/current/network.png
index 2bd1d701..4b77ac61 100644
Binary files a/docs/diagrams/current/network.png and b/docs/diagrams/current/network.png differ
diff --git a/docs/diagrams/current/network.svg b/docs/diagrams/current/network.svg
index 107c6052..8fcb3c30 100644
--- a/docs/diagrams/current/network.svg
+++ b/docs/diagrams/current/network.svg
@@ -495,7 +495,7 @@
Saruman
-.110
+.20
Proxmox VE · hypervisor
Alloy → prometheus 9090/3100
@@ -658,7 +658,7 @@
gandalf
-Proxmox VE · range host, no guests yet · Saruman → .20
+Proxmox VE · range host, no guests yet
.30
diff --git a/docs/hardware.md b/docs/hardware.md
index a0dcfe2b..b2633ec5 100644
--- a/docs/hardware.md
+++ b/docs/hardware.md
@@ -320,7 +320,7 @@ version component in either table.
| `Saruman` | `shiva` — HPE iLO 4, firmware 2.82 | `10.0.30.10` | iLO Advanced licensed. Dedicated network port. DHCP with a reservation. Hardened 2026-09-09 per [ADR-0033](adr/0033-keep-the-ilo-on-the-lab-segment.md): IPMI-over-LAN, SSH and iLO Federation off; HTTPS, the remote console and SNMP stay on; the account's credential is shared with nothing else; the security log was read for a baseline. Polled over SNMPv3 authPriv (SHA, AES, user `prometheus`) since 2026-09-20 per [ADR-0036](adr/0036-poll-the-ilo-and-the-ups-card-over-snmpv3-and-keep-the-firewall-on-bsnmpd.md), with *SNMPv1 Request* off — it answers no community at all |
The BMC and the host it manages carry different names and different addresses:
-`shiva` is the iLO, `Saruman` is the hypervisor at `10.0.30.110`. Earlier
+`shiva` is the iLO, `Saruman` is the hypervisor at `10.0.30.20`. Earlier
revisions of this repository treated `shiva` as the hypervisor itself.
## Accessories
diff --git a/docs/network.md b/docs/network.md
index 48716a86..5c9ef7f8 100644
--- a/docs/network.md
+++ b/docs/network.md
@@ -324,7 +324,7 @@ though not the only one — ImaginationLAN has two host-scoped passes to
[#764](https://github.com/Gerrrt/HomeLab/issues/764). So the firewall is no
longer the only thing between a Hicks workstation and the metric and log
stores.
- `10.0.30.110` keeps its explicit pass for `Saruman`'s Alloy agent, which now
+ `10.0.30.20` keeps its explicit pass for `Saruman`'s Alloy agent, which now
presents `Saruman`'s own token.
- **ImaginationLAN is reached entire**, on every protocol and port, by
decision: [ADR-0031](adr/0031-narrow-hicks-to-a-named-list-on-winterfell-and-leave-the-lab-open.md)
@@ -421,7 +421,7 @@ Televisions and consoles. Internet only.
share through it. With 4533 and 13378, seven exist from Hicks and
Winterfell. **Two more are from ImaginationLAN, and both exist, which
makes nine.** `Allow NFS from Saruman to smaug`,
- `10.0.30.110 → 10.0.40.30:2049`, lets the hypervisor mount `erebor/iso` as
+ `10.0.30.20 → 10.0.40.30:2049`, lets the hypervisor mount `erebor/iso` as
its ISO store for Packer
([ADR-0072](adr/0072-put-the-iso-store-on-smaug-over-nfs-to-saruman-alone.md),
[#446](https://github.com/Gerrrt/HomeLab/issues/446)). It was created on
@@ -486,7 +486,7 @@ Where things get broken on purpose.
| --- | --- | --- | --- | --- | --- | --- |
| morpheus | `10.0.30.1` | `02:26:26:xx:xx:xx` | HP ProDesk 600 G4 Mini | FreeBSD 16.0 | Rack U15 | Firewall |
| shiva | `10.0.30.10` | `94:57:a5:xx:xx:xx` | HPE iLO 4 (DL360 Gen9 BMC)[^Shiva] | iLO 2.82 | Rack U3 | Out-of-band management |
-| Saruman | `10.0.30.110` | `14:02:ec:xx:xx:xx` | HPE ProLiant DL360 Gen9[^Shiva] | Proxmox VE 9 | Rack U3 | Hypervisor |
+| Saruman | `10.0.30.20` | `14:02:ec:xx:xx:xx` | HPE ProLiant DL360 Gen9[^Shiva] | Proxmox VE 9 | Rack U3 | Hypervisor |
| gandalf | `10.0.30.30` | `90:2e:16:xx:xx:xx` | Lenovo ThinkCentre M80q | Proxmox VE 9 | Rack U18 | Range hypervisor, off between sessions. Its Intel AMT, the name `ifrit`, is disabled ([ADR-0106](adr/0106-name-the-range-host-gandalf-and-leave-ifrit-to-its-disabled-amt.md)) |
| alexander | `10.0.30.40` | `bc:24:11:xx:xx:xx` | KVM guest on `Saruman` | Ubuntu 26.04 LTS | Rack U3 | Lab observability |
| phoenix | `10.0.30.70` | `bc:24:11:xx:xx:xx` | KVM guest on `Saruman` | Ubuntu 26.04 LTS | Rack U3 | Deployment host |
@@ -680,7 +680,7 @@ Where things get broken on purpose.
address set out of the node's own subnet, so `10.0.30.0/24` reached the API,
SSH, VNC, the SPICE proxy and the migration range through a rule nobody
wrote, underneath a `DROP` policy that looked closed. The `local_network`
- alias is pinned to `10.0.30.110` so that the four rules above are the only
+ alias is pinned to `10.0.30.20` so that the four rules above are the only
way in. Hicks reaches `8006`, `8007` and `22`; `phoenix` reaches `8006`;
nothing else on this segment reaches the hypervisor at all.
- `fenrir` is at `10.0.30.90`, the next free decade, built 2026-09-30 as the
@@ -695,23 +695,22 @@ Where things get broken on purpose.
path `fenrir` needs is intra-segment — its Alloy pushes to `alexander` — so
it adds no firewall rule.
- `Saruman` runs an Alloy agent and is the one host on this segment with a path
- into Winterfell: a single pass, `10.0.30.110 → 10.0.99.20` on 9090 and 3100
+ into Winterfell: a single pass, `10.0.30.20 → 10.0.99.20` on 9090 and 3100
TCP, unlogged and above the ADR-0014 tripwire. The hypervisor's own telemetry
only; guests get no such rule (ADR-0007, as amended by #88). Past the rule,
the ingest proxy wants `Saruman`'s agent token (#182), so the pass lets the
host try and the token is what gets it served. The push is TLS under the
estate CA (#764), so the token is not readable at the firewall that forwards
it.
-- **`Saruman` is the one fixed address that sits inside a DHCP pool.** Every
- other static in the estate lives below `.100`; this one is at `.110`, and the
- ImaginationLAN pool runs `.100–.200`. Until 2026-08-30 there was no
- reservation for it either, so Kea could have leased the same address to
- another device. There is one now, and it holds *because* the server is Kea:
- `reservations-in-subnet` is true and `reservations-out-of-pool` is unset, so
- reservations are consulted on every allocation. Under ISC dhcpd, whose
- binaries are still on the box, the same reservation would not reliably
- protect an in-pool address. Moving `Saruman` below `.100` is the fix that
- does not depend on that.
+- **`Saruman` is at `.20`, below the pool, like every other static.** Until
+ 2026-11-07 it was at `.110`, the one fixed address in the estate inside a
+ DHCP pool (ImaginationLAN's runs `.100–.200`). Only Kea's
+ reservation kept that address from being leased to another device, and only
+ because Kea consults reservations on every allocation. ISC dhcpd, whose
+ binaries are still on the box, would not have. The move, in
+ [`build-the-playground.md`](runbooks/build-the-playground.md) §3
+ ([#421](https://github.com/Gerrrt/HomeLab/issues/421)), took that dependency
+ away before the attack VM starts taking leases from the same pool.
- A second hypervisor, `gandalf`, carries the attack tooling and the
deliberately-vulnerable targets. It is installed (Proxmox VE 9,
2026-11-07) and has no guests yet. It sits on this segment single-homed, on
@@ -728,8 +727,7 @@ Where things get broken on purpose.
anywhere, which is what makes a `172.30.30.x` source in a firewall block a
leak reporting itself. The attack VM takes a lease from the pool like any
other guest. The build is
- [`build-the-playground.md`](runbooks/build-the-playground.md), and `Saruman`
- moves to `10.0.30.20` as part of it.
+ [`build-the-playground.md`](runbooks/build-the-playground.md).
- **The WireGuard peers live on `172.31.0.0/24`, and it is routed rather than
translated**
diff --git a/docs/roadmap.md b/docs/roadmap.md
index 8b3e9124..2b6180e2 100644
--- a/docs/roadmap.md
+++ b/docs/roadmap.md
@@ -283,7 +283,8 @@ Gated on the domain, on a household observation, or on something to publish.
`gandalf`.** Gated on #414 and the SOC: an attack VM pointed at an
uninstrumented estate teaches nothing. Both are met. The host is installed
as of 2026-11-07: Proxmox VE 9, the `range` pool and both bridges, the
- runbook's §1 and §2. Next is §3, moving `Saruman` to `.20`, then §4–§9.
+ runbook's §1 and §2. §3 ran the same night: `Saruman` is at `.20`. Next is
+ §4, closing `gandalf`'s management plane, then §5–§9.
The name is
[ADR-0106](adr/0106-name-the-range-host-gandalf-and-leave-ifrit-to-its-disabled-amt.md)'s.
→ [runbook](runbooks/build-the-playground.md)
diff --git a/docs/runbooks/add-monitored-device.md b/docs/runbooks/add-monitored-device.md
index 744b775f..5592ce8a 100644
--- a/docs/runbooks/add-monitored-device.md
+++ b/docs/runbooks/add-monitored-device.md
@@ -183,7 +183,7 @@ on the **ImaginationLAN** interface:
| Action | Source | Destination | Ports | Log |
| --- | --- | --- | --- | --- |
-| pass, TCP | `10.0.30.110` | `10.0.99.20` | 9090, 3100 | **off** |
+| pass, TCP | `10.0.30.20` | `10.0.99.20` | 9090, 3100 | **off** |
Place it **above** the ADR-0014 tripwire (`vlan30 net → Internal_Segments`,
pass + log). Below it, every push from `Saruman` is a logged pass on the
diff --git a/docs/runbooks/build-the-jumpbox.md b/docs/runbooks/build-the-jumpbox.md
index 5c6e5875..15bd2f53 100644
--- a/docs/runbooks/build-the-jumpbox.md
+++ b/docs/runbooks/build-the-jumpbox.md
@@ -254,7 +254,7 @@ IN ACCEPT -source 10.0.30.70 -p tcp -dport 8006 -log nolog
install -d -m 700 ~/.config/proxmox
umask 077
cat > ~/.config/proxmox/phoenix.env <<'EOT'
-PROXMOX_URL=https://10.0.30.110:8006/api2/json
+PROXMOX_URL=https://10.0.30.20:8006/api2/json
PROXMOX_TOKEN_ID=phoenix@pve!builder
PROXMOX_TOKEN_SECRET=
EOT
diff --git a/docs/runbooks/build-the-lab-domain.md b/docs/runbooks/build-the-lab-domain.md
index 9ffe7790..e328f905 100644
--- a/docs/runbooks/build-the-lab-domain.md
+++ b/docs/runbooks/build-the-lab-domain.md
@@ -673,7 +673,7 @@ ImaginationLAN*, mapping each guest's MAC to its address.
> anyway. The reservation is there so the address is recorded where a reader
> looks for it, and so the protection does not depend on the pool never moving —
> the reasoning [`build-the-playground.md`](build-the-playground.md) already
-> writes out, and the mistake `10.0.30.110` is still an apology for.
+> writes out, and the mistake `10.0.30.110` was an apology for until `Saruman` moved to `.20` on 2026-11-07.
>
> Leave the scope's DNS setting alone. Reserved clients receive the interface
> address like everyone else; the DC addresses are set inside Windows, which is
diff --git a/docs/runbooks/build-the-lab-templates.md b/docs/runbooks/build-the-lab-templates.md
index a4f2568f..241906e4 100644
--- a/docs/runbooks/build-the-lab-templates.md
+++ b/docs/runbooks/build-the-lab-templates.md
@@ -124,7 +124,7 @@ pveum acl list | grep phoenix
**Then make `phoenix` verify the API's certificate.** Packer and
`scripts/packer-smoke.sh` both check TLS, because the token travels in a header,
-and a guest on VLAN 30 answering for `10.0.30.110` would collect it otherwise.
+and a guest on VLAN 30 answering for `10.0.30.20` would collect it otherwise.
The attack VM shares that segment.
**First, check what `Saruman` actually serves.** Its API certificate should
@@ -136,11 +136,11 @@ and naming only `10.0.0.208`, the host's address before it moved. On
`Saruman`:
```bash
-echo | openssl s_client -connect 10.0.30.110:8006 2>/dev/null | openssl x509 -noout -issuer -ext subjectAltName
+echo | openssl s_client -connect 10.0.30.20:8006 2>/dev/null | openssl x509 -noout -issuer -ext subjectAltName
```
The issuer must be `PVE Cluster Manager CA`, and the names must include
-`10.0.30.110`. If either is wrong, keep a copy of any custom certificate,
+`10.0.30.20`. If either is wrong, keep a copy of any custom certificate,
regenerate the node's own from `/etc/hosts` (check the host's line there
first, because this reads it), and stop serving the custom one. Restarting
`pveproxy` drops open web-UI sessions and touches no guest:
@@ -285,7 +285,7 @@ as trustworthy as that list, so the list is written here, once, with care.
`phoenix` cannot:
```bash
- make install-agent-collectors AGENT=root@10.0.30.110 ARGS='--only iso-store-state'
+ make install-agent-collectors AGENT=root@10.0.30.20 ARGS='--only iso-store-state'
```
The installer starts one run straight away. That run reads every ISO, so
diff --git a/docs/runbooks/build-the-nas.md b/docs/runbooks/build-the-nas.md
index 6acf6f1a..4a83612d 100644
--- a/docs/runbooks/build-the-nas.md
+++ b/docs/runbooks/build-the-nas.md
@@ -250,7 +250,7 @@ first row here on ImaginationLAN. `golem`'s `2049` pass is ImaginationLAN's othe
| Winterfell (99) | `tcp` `10.0.99.20` → `10.0.40.30` port `22` | `Allow SSH to smaug` | **above** *Block access to CasaBonita* |
| Hicks (50) | `tcp` `vlan50 net` → `10.0.40.30` port `13378` | `Allow 13378 to smaug` | **above** *Block access to CasaBonita* — §6.5 |
| Hicks (50) | `tcp` `vlan50 net` → `10.0.40.30` port `445` | `Allow SMB to smaug` | **above** *Block access to CasaBonita* — §5 |
-| ImaginationLAN (30) | `tcp` `10.0.30.110` → `10.0.40.30` port `2049` | `Allow NFS from Saruman to smaug` | **above** *Block access to CasaBonita* on `igc0.30` — §5b |
+| ImaginationLAN (30) | `tcp` `10.0.30.20` → `10.0.40.30` port `2049` | `Allow NFS from Saruman to smaug` | **above** *Block access to CasaBonita* on `igc0.30` — §5b |
**Four, where [ADR-0016](../adr/0016-open-casabonita-inward-and-keep-it-terminal-outward.md)
wrote three.** The Hicks pass is one rule per port rather than one rule
@@ -679,7 +679,9 @@ there. Have the TrueNAS UI and console shell, the pfSense UI, a shell on
> rather than the UI: `erebor/iso`, lz4, atime off, dedup off, POSIX ACLs,
> a 500 GiB quota, no snapshot task. Record size reads `1M`.
> `pippin` is uid `3003`, gid `3002`. The share admits `10.0.30.110` alone,
-> with maproot `pippin`/`pippin`.
+> with maproot `pippin`/`pippin`. (`Saruman` moved to `10.0.30.20` on
+> 2026-11-07, and the share and the rule moved with it:
+> [`build-the-playground.md`](build-the-playground.md) §3.)
>
> `Allow NFS from Saruman to smaug` was read from `morpheus` with
> `pfctl -sr -vv`. On `igc0.30` it printed directly above *Block access to
@@ -709,8 +711,8 @@ there. Have the TrueNAS UI and console shell, the pfSense UI, a shell on
| | |
| --- | --- |
| Dataset | `erebor/iso`: not snapshotted, not backed up, 500 GiB quota |
-| Export | NFSv4, to `10.0.30.110` alone, maproot `pippin` |
-| Rule | ImaginationLAN (30): `10.0.30.110 → 10.0.40.30:2049/tcp`, `Allow NFS from Saruman to smaug` |
+| Export | NFSv4, to `10.0.30.20` alone, maproot `pippin` |
+| Rule | ImaginationLAN (30): `10.0.30.20 → 10.0.40.30:2049/tcp`, `Allow NFS from Saruman to smaug` |
| On `Saruman` | `fstab` mount at `/mnt/smaug-iso`, Proxmox `dir` storage `smaug-iso`, content `iso`, `is_mountpoint` |
1. **The dataset.** It exists (above). Finish it from the console shell, and
@@ -744,7 +746,7 @@ there. Have the TrueNAS UI and console shell, the pfSense UI, a shell on
| --- | --- |
| Path | `/mnt/erebor/iso` |
| Description | `ISO store for Saruman (ADR-0072)` |
- | Authorized hosts | `10.0.30.110` |
+ | Authorized hosts | `10.0.30.20` |
| Maproot User / Group | `pippin` / `pippin` |
| Mapall | empty |
@@ -753,7 +755,7 @@ there. Have the TrueNAS UI and console shell, the pfSense UI, a shell on
and a squash to `nobody` refuses the write.
5. **The rule**, in the pfSense UI on **ImaginationLAN (30)**: pass, `tcp`,
- source `10.0.30.110`, destination `10.0.40.30` port `2049`, description
+ source `10.0.30.20`, destination `10.0.40.30` port `2049`, description
exactly **`Allow NFS from Saruman to smaug`**. Place it **directly above**
the first block on that interface whose destination covers
`10.0.40.0/24`. `build-the-backup-guest.md` §4 finds that block and names
diff --git a/docs/runbooks/build-the-playground.md b/docs/runbooks/build-the-playground.md
index bd8d6248..4a3406a6 100644
--- a/docs/runbooks/build-the-playground.md
+++ b/docs/runbooks/build-the-playground.md
@@ -43,7 +43,7 @@ decision and on this runbook.
The machine is ready too. `gandalf`, a refurbished ThinkCentre M80q, was
bought on 2026-10-05 and has its 32 GB and its SATA range disk
([ADR-0096](../adr/0096-fit-ifrits-range-disk-as-a-sata-ssd-in-the-tinys-bay.md))
-fitted. **§1 and §2 were done on 2026-11-07**
+fitted. **§1, §2 and §3 were done on 2026-11-07**
([#421](https://github.com/Gerrrt/HomeLab/issues/421)), and the parts as
fitted are in [`hardware.md`](../hardware.md). The node is named `gandalf`,
and `ifrit` names its disabled Intel AMT
@@ -231,50 +231,130 @@ protection does not depend on the pool never moving.
## 3. Move `Saruman` to `10.0.30.20`
-ADR-0014 requires this "at the same time", and the reason is in
-[`network.md`](../network.md): `10.0.30.110` is the one fixed address in the
-estate that sits *inside* a DHCP pool. It holds today only because the server is
-Kea and consults reservations on every allocation; under the ISC dhcpd binaries
-still on the box it would not.
-
-This is the riskiest step in the build. Nothing scrapes `Saruman` by address —
-its Alloy agent pushes — so the blast radius is one firewall rule and the
-documents:
-
-1. Change the static on `Saruman` and its Kea reservation to `10.0.30.20`.
-2. Edit the pass rule on the **ImaginationLAN** interface from
- `10.0.30.110 → 10.0.99.20` to `10.0.30.20 → 10.0.99.20`, ports 9090 and
- 3100, **logging off**, still above the ADR-0014 tripwire. It is documented in
- [`add-monitored-device.md`](add-monitored-device.md); logging it makes the
- agent's every push a hit on the tripwire.
-3. Confirm the agent is still arriving before you move on — a broken rule looks
- exactly like a host that is down. The hypervisor should still be in this
- list:
+> **Ran on 2026-11-07** ([#421](https://github.com/Gerrrt/HomeLab/issues/421)).
+> `Saruman` is at `10.0.30.20`, and every path below was checked over the
+> new address. The steps are kept as the record of how it was done, and they
+> have the shape to follow if the hypervisor's address ever moves again.
+
+ADR-0014 requires this "at the same time". At `.110`, `Saruman` was the one
+fixed address in the estate inside a DHCP pool, and the attack VM leases from
+that pool. Kea's reservation kept the address safe only because Kea consults
+reservations on every allocation.
+
+The runbook first listed two things that name the address: the Alloy pass
+and the ISO store's pass and export. **There were six.** The NUT pass, the
+host firewall's `local_network` alias, the API certificate, `phoenix`'s
+endpoint and the Mac's `known_hosts` all named it as well. The alias is the
+one with a security cost. Left at `.110`, it would put whatever leased
+`.110` next into Proxmox's `management` set, as §4 explains, and that could
+be the attack VM.
+
+**Add the `.20` paths before you move the address, and remove the `.110`
+ones only after it moves.** That way nothing breaks at the cutover.
+
+1. **Before you start.** Check that `.20` is free: from `morpheus`, it should
+ not answer a ping and should have no ARP entry. Then, on `Saruman`, copy
+ `/etc/network/interfaces`, `/etc/hosts` and
+ `/etc/pve/firewall/cluster.fw` to `/root/`. Record a baseline:
+ - the `host` list in Loki (step 6);
+ - `ssh admin@10.0.99.1 'upsc -c mjolnir@localhost'`;
+ - `pvesm status --storage smaug-iso`;
+ - the running guests in `qm list`.
+2. **The ImaginationLAN rules on `morpheus`.** Copy each of these rules
+ with source `10.0.30.20`, and place each copy directly under its original:
+ - the Alloy passes to `10.0.99.20`, on 9090 and 3100, with **logging
+ off**. They are documented in
+ [`add-monitored-device.md`](add-monitored-device.md). If you log them,
+ every push the agent makes counts as a tripwire hit;
+ - `Allow NUT from Saruman` to `10.0.30.1:3493`;
+ - `Allow NFS from Saruman to smaug` to `10.0.40.30:2049`.
+
+ Every copy must sit above its block rule. `pfctl` matches the first rule
+ that fits, so check the order with
+ `pfctl -sr | grep -n "on igc0.30"`. On `smaug`, add `10.0.30.20` to the
+ `erebor/iso` share's authorized hosts. On `morpheus`, change `Saruman`'s
+ Kea reservation to `10.0.30.20`.
+3. **Change the address.** Edit `/etc/hosts`,
+ `address` under `vmbr0` in `/etc/network/interfaces`, and
+ `local_network` in `/etc/pve/firewall/cluster.fw`. `/etc/pve` is the
+ cluster filesystem, so write the file with `cp`, not `sed -i`. Then:
+
+ ```bash
+ env -i PATH=/usr/sbin:/usr/bin:/sbin:/bin /usr/sbin/ifreload -a
+ ```
+
+ **The `env -i` is needed.** `ifreload` starts with
+ `#!/usr/bin/env python3`, and root's dotfiles put mise's Python first on
+ the PATH. That Python has neither Debian's `python3-systemd` nor
+ ifupdown2's library path, so a plain `ifreload -a` dies with
+ `ModuleNotFoundError` and changes nothing. Boot is not affected, because
+ `networking.service` runs with systemd's default PATH.
+ `ifquery vmbr0`, under the same `env -i`, reads the new stanza back
+ without applying it.
+
+ **Any SSH session, and any Proxmox web Shell, connected to `.110` drops
+ at this point.** Restarting `pveproxy` also drops the web Shell. Use the
+ iLO console, or reconnect to `.20`. The host key is unchanged, and the
+ client says so: it reports the key as already known under another
+ address.
+4. **Reissue the API certificate** from the new `/etc/hosts`:
+ `pvecm updatecerts --force && systemctl restart pveproxy`. `phoenix`
+ verifies TLS, so the certificate's names must include `10.0.30.20`:
+
+ ```bash
+ echo | openssl s_client -connect 10.0.30.20:8006 2>/dev/null | openssl x509 -noout -issuer -ext subjectAltName
+ ```
+
+ Then check three more things:
+ - `qm list` shows the same running guests as before;
+ - after a minute, `tc filter show dev vmbr0 egress` shows the Zeek
+ mirror again;
+ - `pve-firewall localnet` reports `local_network: 10.0.30.20`.
+5. **Change the clients.** In `~/.config/proxmox/phoenix.env` on
+ `phoenix`, change `PROXMOX_URL` and `PROXMOX_VE_ENDPOINT`, then check
+ the API over the new endpoint. Rerunning
+ [`provision-lab-guests.md`](provision-lab-guests.md) §1 will not do it,
+ because on a configured `phoenix` that block stops at
+ `already present`. This one only reads the file, prints only the status
+ code, and does not pass `-k`:
+
+ ```bash
+ ( set -a; . ~/.config/proxmox/phoenix.env; set +a
+ printf 'header = "Authorization: PVEAPIToken=%s"\n' "${PROXMOX_VE_API_TOKEN}" \
+ | curl -K - -fsS -o /dev/null -w 'API %{http_code}\n' "${PROXMOX_VE_ENDPOINT}api2/json/version" )
+ ```
+
+ It must print `API 200`. A certificate that doesn't name `10.0.30.20`
+ fails here instead. On the Mac, run
+ `ssh-keygen -R 10.0.30.110` and update the host's entry in the SSH
+ config. Nothing on VLAN 99 can SSH to `Saruman`, so `prometheus` has no
+ `known_hosts` entry to update.
+6. **Check every path over `.20`.** This list should still include the
+ hypervisor:
```bash
curl -s http://localhost:3100/loki/api/v1/label/host/values | jq -r '.data[]'
```
- `host` is `constants.hostname` as the agent sees it, so read the value out of
- the list rather than asserting one — see
+ `host` is `constants.hostname` as the agent sees it, so read the value
+ out of the list rather than assert one. See
[`ship-firewall-logs.md`](ship-firewall-logs.md) for why that label means
- something different on relayed logs.
- **If the ISO store exists** ([ADR-0072](../adr/0072-put-the-iso-store-on-smaug-over-nfs-to-saruman-alone.md),
- [`build-the-nas.md`](build-the-nas.md) §5b), it is a second rule and an
- export, and both name `.110`:
- - Edit `Allow NFS from Saruman to smaug` on ImaginationLAN to source
- `10.0.30.20`. Its position does not change.
- - On `smaug`, change the `erebor/iso` NFS share's authorized host to
- `10.0.30.20`.
- - On `Saruman`, `umount /mnt/smaug-iso && mount /mnt/smaug-iso`, then
- `pvesm status --storage smaug-iso` must say `active`.
-4. Update the address where the documents carry it: `hardware.md`,
- `network.md`, `architecture.md`, `add-monitored-device.md`,
- `replace-the-smart-storage-battery.md`, and the ADRs that quote the rule —
- those get a note rather than an edit, per
- [ADR-0001](../adr/0001-record-architecture-decisions.md). `roadmap.md` also
- carries `.110`, in a Done entry, and that one is **left alone**: it records
- what was true when that work landed.
+ something different on relayed logs. Also check:
+ - `pfctl -ss` shows states from `.20` to `10.0.99.20`;
+ - `upsc -c mjolnir@localhost` lists `10.0.30.20`;
+ - after `umount /mnt/smaug-iso && mount /mnt/smaug-iso`, `smaug-iso` is
+ `active`;
+ - `8006` answers from Hicks and is refused from `morpheus`.
+
+ A broken rule looks exactly like a host that is down.
+7. **Remove the `.110` paths.** Delete the four `.110` rules and remove
+ `.110` from the share, then run step 6 again. The copies were left with
+ `(.20)` at the end of their descriptions.
+8. **The documents.** Wherever a current-state document names the address,
+ update it. ADRs that quote it get a note rather than an edit, per
+ [ADR-0001](../adr/0001-record-architecture-decisions.md). Dated records
+ are left alone: the changelog, `roadmap.md`'s Done entries and dated log
+ rows.
---
@@ -336,7 +416,7 @@ than narrowed here; ADR-0017's Consequences record that it admits nothing.
**This is the step that makes the difference between ADR-0014 being true and
merely being written down.** Proxmox detects a "local network" from the node's
own address, puts that CIDR in a `management` IP set, and gives the set five
-RETURN rules in the host input chain. `Saruman` is `10.0.30.110/24`, so the
+RETURN rules in the host input chain. `Saruman` is `10.0.30.20/24`, so the
detected value was **the entire lab segment** — and with `policy_in: DROP` in
place, every address on VLAN 30 still reached the GUI, SSH, the VNC range, the
SPICE proxy and the migration range. ADR-0014's three rules were redundant
@@ -357,7 +437,7 @@ enable: 1
policy_in: ACCEPT
[ALIASES]
-local_network 10.0.30.110
+local_network 10.0.30.20
```
Use the node's own address, and check that `localnet` now reports it as user
@@ -422,7 +502,7 @@ segment and is not Hicks, so it is the right prober; a second probe to a host
that should answer keeps a broken `nc` from reading as a closed port:
```bash
-ssh admin@10.0.99.1 'nc -z -w 3 10.0.30.110 8006; echo "8006 exit=$?"; nc -z -w 3 10.0.30.40 3100; echo "control exit=$?"'
+ssh admin@10.0.99.1 'nc -z -w 3 10.0.30.20 8006; echo "8006 exit=$?"; nc -z -w 3 10.0.30.40 3100; echo "control exit=$?"'
```
Expect `8006 exit=1` and `control exit=0`. Then confirm the hypervisor's own
@@ -444,7 +524,7 @@ skips this section — page nothing on their own. Install the collector that
reads it ([#576](https://github.com/Gerrrt/HomeLab/issues/576)):
```bash
-make install-agent-collectors AGENT=root@10.0.30.110 ARGS='--only pve-firewall-state'
+make install-agent-collectors AGENT=root@10.0.30.20 ARGS='--only pve-firewall-state'
```
and read it back on `prometheus`:
diff --git a/docs/runbooks/build-the-sensor-guest.md b/docs/runbooks/build-the-sensor-guest.md
index b50dd119..c181682c 100644
--- a/docs/runbooks/build-the-sensor-guest.md
+++ b/docs/runbooks/build-the-sensor-guest.md
@@ -93,7 +93,7 @@ ls /sys/class/net/vmbr1/brif
ifquery --check -a
```
-- `vmbr0` must still show `10.0.30.110/24`.
+- `vmbr0` must still show `10.0.30.20/24`.
- `vmbr1` must show **no address at all**, not even an `fe80::`.
- `addr_gen_mode` must read `1`.
- `brif` stays empty until §2's VM starts.
@@ -306,18 +306,18 @@ Both ship through the agent-collector installer. **Run it from a checkout on
Hicks**, which can SSH to `Saruman` as root.
Run from the checkout on `Saruman` itself, the installer SSHes to
-`root@10.0.30.110` from `10.0.30.110`. That login is from VLAN 30, so it fires
+`root@10.0.30.20` from `10.0.30.20`. That login is from VLAN 30, so it fires
the critical `SshLoginFromUnexpectedSubnet`, which clears ten minutes later.
That is what happened on the first build. Also, `Saruman`'s own host key is not
-in root's `known_hosts` there: compare `ssh-keyscan -t ed25519 10.0.30.110`
+in root's `known_hosts` there: compare `ssh-keyscan -t ed25519 10.0.30.20`
against `/etc/ssh/ssh_host_ed25519_key.pub` before adding it.
```bash
-make install-agent-collectors AGENT=root@10.0.30.110 ARGS='--only zeek-mirror'
+make install-agent-collectors AGENT=root@10.0.30.20 ARGS='--only zeek-mirror'
```
```bash
-make install-agent-collectors AGENT=root@10.0.30.110 ARGS='--only zeek-mirror-state'
+make install-agent-collectors AGENT=root@10.0.30.20 ARGS='--only zeek-mirror-state'
```
The first installs `scripts/zeek-mirror.sh` as `/usr/local/bin/homelab-zeek-mirror`
diff --git a/docs/runbooks/fit-the-saruman-ssds.md b/docs/runbooks/fit-the-saruman-ssds.md
index 84f85fae..73ce6067 100644
--- a/docs/runbooks/fit-the-saruman-ssds.md
+++ b/docs/runbooks/fit-the-saruman-ssds.md
@@ -101,7 +101,7 @@ both arrays — not assumed from either.**
> still the *Before* column of every table here.
`shiva` is the iLO, not the hypervisor. The host behind it is `Saruman` at
-`10.0.30.110`; see [`../hardware.md`](../hardware.md). Both are on VLAN 30.
+`10.0.30.20`; see [`../hardware.md`](../hardware.md). Both are on VLAN 30.
## Where each step runs, and why it matters
@@ -112,7 +112,7 @@ it is.
| Steps | Host | Who runs them |
| --- | --- | --- |
| 0, 1, 10, 11 | `prometheus`, `10.0.99.20`, VLAN 99 | the monitoring host — Prometheus and Alertmanager are here |
-| 2–9 | `Saruman`, `10.0.30.110`, VLAN 30 | **you, from the Mac.** Nothing on VLAN 99 can reach this host |
+| 2–9 | `Saruman`, `10.0.30.20`, VLAN 30 | **you, from the Mac.** Nothing on VLAN 99 can reach this host |
Run the `Saruman` half inside `tmux`. A dropped SSH session during step 9's
disk move leaves a half-mirrored disk, and that is a worse afternoon than a
@@ -679,7 +679,7 @@ what [`build-the-lab-guest.md`](build-the-lab-guest.md) already assumes.
> read instead by `scripts/collect-thin-pool-state.sh`, from `lvs` every ten
> minutes on the host, into `ThinPoolNearlyFull` and `ThinPoolWillFillIn24h`
> ([#538](https://github.com/Gerrrt/HomeLab/issues/538)). Installing it is
-> `make install-agent-collectors AGENT=root@10.0.30.110
+> `make install-agent-collectors AGENT=root@10.0.30.20
> ARGS='--only thin-pool-state'`, and the check is
> `homelab_thin_pool_data_percent{host="Saruman"}` — one series per pool.
diff --git a/docs/runbooks/move-into-the-32u-rack.md b/docs/runbooks/move-into-the-32u-rack.md
index be37b820..c3c06927 100644
--- a/docs/runbooks/move-into-the-32u-rack.md
+++ b/docs/runbooks/move-into-the-32u-rack.md
@@ -331,7 +331,7 @@ hours, so it will not fire inside a normal window.
| Order | Host | How | You should see |
| --- | --- | --- | --- |
-| 1 | `Saruman` and its guests | The Proxmox UI at `https://10.0.30.110:8006` from the Mac on Hicks (`10.0.30.20` once [`build-the-playground.md`](build-the-playground.md) §3 has run), *node → Shutdown*. It stops its guests first | The iLO at `10.0.30.10` shows the server off |
+| 1 | `Saruman` and its guests | The Proxmox UI at `https://10.0.30.20:8006` from the Mac on Hicks, *node → Shutdown*. It stops its guests first | The iLO at `10.0.30.10` shows the server off |
| 2 | `smaug` | The TrueNAS UI, *Power → Shut Down* | The tower's power light goes out. `erebor` is exported on the way down |
| 3 | `trinity` | `ssh rabbit@10.0.99.40 sudo systemctl poweroff` | The ProDesk's light goes out. **AdGuard goes with it**: from here, the house cannot resolve outside names |
| 4 | `morpheus` | The pfSense console on the KVM, option *Halt system* | The house loses internet, DNS and DHCP |
diff --git a/docs/runbooks/provision-lab-guests.md b/docs/runbooks/provision-lab-guests.md
index 3cf5a7a8..d4de7552 100644
--- a/docs/runbooks/provision-lab-guests.md
+++ b/docs/runbooks/provision-lab-guests.md
@@ -86,7 +86,7 @@ if grep -qE '^(PROXMOX_VE_|TF_VAR_state_passphrase)' "$E"; then
echo "already present"
elif cp -p "$E" "$E.bak-445"; then
( set -a; . "$E"; set +a; umask 077
- printf 'PROXMOX_VE_ENDPOINT=https://10.0.30.110:8006/\nPROXMOX_VE_API_TOKEN=%s=%s\nTF_VAR_state_passphrase=%s\n' \
+ printf 'PROXMOX_VE_ENDPOINT=https://10.0.30.20:8006/\nPROXMOX_VE_API_TOKEN=%s=%s\nTF_VAR_state_passphrase=%s\n' \
"$PROXMOX_TOKEN_ID" "$PROXMOX_TOKEN_SECRET" "$(openssl rand -base64 32)" >> "$E" )
chmod 600 "$E"; grep -o '^[A-Za-z_]*=' "$E"
( set -a; . "$E"; set +a; echo "passphrase length ${#TF_VAR_state_passphrase}"
diff --git a/docs/runbooks/replace-the-smart-storage-battery.md b/docs/runbooks/replace-the-smart-storage-battery.md
index cf4606f8..b36a7dc5 100644
--- a/docs/runbooks/replace-the-smart-storage-battery.md
+++ b/docs/runbooks/replace-the-smart-storage-battery.md
@@ -28,7 +28,7 @@ lid comes off — not after.**
> end.
`shiva` is the iLO, not the hypervisor. The host behind it is `Saruman` at
-`10.0.30.110`; see [`../hardware.md`](../hardware.md). The pack is internal to
+`10.0.30.20`; see [`../hardware.md`](../hardware.md). The pack is internal to
the ProLiant DL360 Gen9 and is not hot-swappable, so fitting it means taking
`Saruman` down.
diff --git a/docs/runbooks/schedule-maintenance.md b/docs/runbooks/schedule-maintenance.md
index 12e4ca8f..1ddbd549 100644
--- a/docs/runbooks/schedule-maintenance.md
+++ b/docs/runbooks/schedule-maintenance.md
@@ -242,7 +242,7 @@ The estate's `GuestServiceUnhealthy` warns after fifteen minutes
warns when an agent stops running the check, and `GuestServiceStateStale` when
the collector stops writing. It requires `qm`, so it installs on the hypervisor
only, from the Mac, because VLAN 99 cannot reach VLAN 30 on port 22:
-`make install-agent-collectors AGENT=root@10.0.30.110 ARGS='--only guest-service-state'`.
+`make install-agent-collectors AGENT=root@10.0.30.20 ARGS='--only guest-service-state'`.
**`drift-check` is the collector that belongs to another repository.**
`Gerrrt/Lemmiwinks/.claude/tools/drift-check` reads the wiki's machine-checkable
diff --git a/docs/runbooks/shut-down-on-the-ups.md b/docs/runbooks/shut-down-on-the-ups.md
index 9bc9d5d2..963561e0 100644
--- a/docs/runbooks/shut-down-on-the-ups.md
+++ b/docs/runbooks/shut-down-on-the-ups.md
@@ -405,7 +405,7 @@ the existing *Allow internet* rule.
| Action | Pass |
| Interface | ImaginationLAN |
| Protocol | TCP |
-| Source | Single host or alias, `10.0.30.110` |
+| Source | Single host or alias, `10.0.30.20` |
| Destination | Single host or alias, `10.0.30.1` |
| Destination port range | From `3493` to `3493` |
| Description | `Allow NUT from Saruman` |
@@ -553,7 +553,7 @@ waits on its logged-in monitors in step 5:
ssh admin@10.0.99.1 'upsc -c mjolnir@localhost'
```
-It should list `10.0.30.110` and `::1`. The firewall's own monitor connects
+It should list `10.0.30.20` and `::1`. The firewall's own monitor connects
over IPv6 loopback.
### 3.6 Read the guest shutdown policy
@@ -728,7 +728,7 @@ there this loop would read `---` before anything halted.
```bash
while :; do
- nc -z -G 2 -w 2 10.0.30.110 22 >/dev/null 2>&1 && r=up || r='---'
+ nc -z -G 2 -w 2 10.0.30.20 22 >/dev/null 2>&1 && r=up || r='---'
printf '%s saruman=%s\n' "$(date -u +%H:%M:%S)" "$r"
sleep 2
done | tee ~/ups-fsd-saruman.log
diff --git a/docs/security.md b/docs/security.md
index 8333e1a1..7cb5f143 100644
--- a/docs/security.md
+++ b/docs/security.md
@@ -355,7 +355,7 @@ televisions' `bilbo` and the workstations' credential are revoked apart. The
same pass puts an SMB login prompt in front of everything on Hicks, the
corporate laptop included, as `443` already puts the TrueNAS one.
**One more is from ImaginationLAN, and it exists.**
-`10.0.30.110 → 10.0.40.30:2049`, `Allow NFS from Saruman to smaug`, lets the
+`10.0.30.20 → 10.0.40.30:2049`, `Allow NFS from Saruman to smaug`, lets the
hypervisor mount `erebor/iso`, the ISO store Packer builds templates from
([ADR-0072](adr/0072-put-the-iso-store-on-smaug-over-nfs-to-saruman-alone.md)).
It was created on 2026-10-01. `Saruman` mounts the share through it, and
@@ -514,7 +514,7 @@ and write the metric and log stores, and delete logs, without a credential.
That is exactly the failure ADR-0002 predicted when it recorded that "a
compromised workstation reaches Winterfell". The firewall narrowed who
"anything" was: since 2026-09-02 Hicks reaches `10.0.99.20` on `3000` only,
-and `10.0.30.110` on ImaginationLAN has one explicit pass for `Saruman`'s
+and `10.0.30.20` on ImaginationLAN has one explicit pass for `Saruman`'s
agent. But a control that depends on one un-reviewed rule ordering is not
authentication.
diff --git a/packer/variables.pkr.hcl b/packer/variables.pkr.hcl
index a7c4638c..5102bdff 100644
--- a/packer/variables.pkr.hcl
+++ b/packer/variables.pkr.hcl
@@ -28,7 +28,7 @@ variable "proxmox_token_secret" {
# Verified. Saruman's API certificate is signed by the cluster's own CA
# (/etc/pve/pve-root-ca.pem), which build-the-lab-templates.md §2 installs in
# phoenix's trust store. Skipping verification would hand the token to any
-# guest on VLAN 30 that answered for 10.0.30.110, and the attack VM shares
+# guest on VLAN 30 that answered for 10.0.30.20, and the attack VM shares
# that segment. Set true only to debug, never to build.
variable "proxmox_insecure_skip_tls_verify" {
type = bool
diff --git a/scripts/collect-iso-store-state.sh b/scripts/collect-iso-store-state.sh
index d31de79e..00999166 100755
--- a/scripts/collect-iso-store-state.sh
+++ b/scripts/collect-iso-store-state.sh
@@ -3,7 +3,7 @@
# Whether the ISOs on `smaug-iso` are the ones this repository expects (#440,
# ADR-0072).
#
-# THE GAP. The ISO store is an NFSv4 export of `erebor/iso` to `10.0.30.110`
+# THE GAP. The ISO store is an NFSv4 export of `erebor/iso` to `10.0.30.20`
# alone, and NFS with sec=sys authenticates by address. Anything on VLAN 30
# that took `Saruman`'s address could replace an installer, and every template
# Packer built from it afterwards would carry the change. ADR-0072 named the
@@ -45,7 +45,7 @@ set -uo pipefail
# placed on the share, from `sha256sum` on `Saruman`, cross-checked against
# the vendor's published hash where one exists (build-the-lab-templates.md
# §2b). Changing it means re-running `make install-agent-collectors
-# AGENT=root@10.0.30.110 ARGS='--only iso-store-state'`, because the host has
+# AGENT=root@10.0.30.20 ARGS='--only iso-store-state'`, because the host has
# no checkout of this repository.
EXPECTED="$(cat <<'LIST'
# Ubuntu 26.04.1 live server. releases.ubuntu.com SHA256SUMS, with a good
diff --git a/scripts/collect-pve-firewall-state.sh b/scripts/collect-pve-firewall-state.sh
index 478945c5..83fed5bf 100755
--- a/scripts/collect-pve-firewall-state.sh
+++ b/scripts/collect-pve-firewall-state.sh
@@ -9,7 +9,7 @@
# and the ways it goes off again are ordinary: a `pve-firewall stop` while
# debugging a guest that cannot reach the API, an upgrade that rewrites
# /etc/pve/firewall, or a rebuild that skips §4 the way the first build did.
-# The check that would notice, `nc -z 10.0.30.110 8006` from off the segment, is
+# The check that would notice, `nc -z 10.0.30.20 8006` from off the segment, is
# something a person does once.
#
# WHY RULE COUNTS AS WELL AS THE SWITCH. "Enabled with zero rules" and
@@ -134,7 +134,7 @@ enable: 1
policy_in: ACCEPT
[ALIASES]
-local_network 10.0.30.110"
+local_network 10.0.30.20"
check "a disabled rule is not counted" count_rules 1 \
"[RULES]
|IN ACCEPT -source 10.0.50.0/24 -p tcp -dport 8006
@@ -158,7 +158,7 @@ enable: 1
policy_in: ACCEPT
[ALIASES]
-local_network 10.0.30.110"
+local_network 10.0.30.20"
check "REJECT refuses too" parse_policy_drop 1 \
"[OPTIONS]
policy_in: REJECT"
diff --git a/scripts/zeek-mirror.sh b/scripts/zeek-mirror.sh
index 54c1c4cf..f45d8d35 100755
--- a/scripts/zeek-mirror.sh
+++ b/scripts/zeek-mirror.sh
@@ -6,7 +6,7 @@
# WHY tc AND NOT OPEN VSWITCH. #437 was filed saying port mirroring needs OVS.
# It does not: a Linux bridge port is a netdev, and a `clsact` qdisc with a
# `matchall` filter and a `mirred` action copies whatever enters it to another
-# netdev. Converting `vmbr0` would have meant moving 10.0.30.110 — the
+# netdev. Converting `vmbr0` would have meant moving the hypervisor's address — the
# management plane ADR-0014's host firewall guards — onto an OVSIntPort, and
# putting a bridge that speaks VLAN tags under the one segment built to hold an
# attacker. ADR-0068 records the comparison.
diff --git a/stacks/media/README.md b/stacks/media/README.md
index 3b470c83..3060d86c 100644
--- a/stacks/media/README.md
+++ b/stacks/media/README.md
@@ -97,7 +97,7 @@ every television would have to trust, and a second thing to be down.
| Prometheus, on `22` | The fourth rule — `10.0.99.20 → 10.0.40.30:22`, inert until [`build-the-nas.md`] §6.2 switches SSH on for the backup pull, as `frodo` with one key and read access to `erebor/apps` ([ADR-0045]) |
| A Hicks phone, on `4533` | `50 → 10.0.40.30:4533`, `Allow 4533 to smaug`, for Navidrome — **created 2026-09-22**, ahead of the service and of the 13378 pass, so it is the fifth that exists; 445 is the sixth, and 13378 the seventh (§6.6) |
| A Hicks workstation, on `445` | `50 → 10.0.40.30:445`, `Allow SMB to smaug`, to mount the `media` share as `samwise` — **created 2026-09-23** by [`build-the-nas.md`] §5's *Workstations* steps, and mounted from a Hicks workstation ([ADR-0051]) |
-| `Saruman`, on `2049` | `10.0.30.110 → 10.0.40.30:2049`, `Allow NFS from Saruman to smaug`, to mount `erebor/iso` as the ISO store. Not the media stack's, and listed because it is a way into this host. **Created 2026-10-01** by [`build-the-nas.md`] §5b, and mounted by `Saruman` ([ADR-0072]) |
+| `Saruman`, on `2049` | `10.0.30.20 → 10.0.40.30:2049`, `Allow NFS from Saruman to smaug`, to mount `erebor/iso` as the ISO store. Not the media stack's, and listed because it is a way into this host. **Created 2026-10-01** by [`build-the-nas.md`] §5b, and mounted by `Saruman` ([ADR-0072]) |
| Everything else on the estate | Not at all — default deny |
[ADR-0012] asks for a named off-host consumer before a port is published, and
@@ -119,7 +119,7 @@ for a boundary it is not.
The NFS export on `2049` is not one of those three media ports, and it
differs in kind. A device on CasaBonita can open the port, but the export
-itself admits `10.0.30.110` alone, so the share answers only `Saruman`. The
+itself admits `10.0.30.20` alone, so the share answers only `Saruman`. The
host scope is enforced twice, by the firewall rule and by the export, and
only the export applies on the segment.
diff --git a/stacks/sensitive/glance/pages/homelab.yml b/stacks/sensitive/glance/pages/homelab.yml
index f16d4a82..e465e99b 100644
--- a/stacks/sensitive/glance/pages/homelab.yml
+++ b/stacks/sensitive/glance/pages/homelab.yml
@@ -131,7 +131,7 @@
color: 349 89 72
links:
- title: Proxmox · Saruman
- url: https://10.0.30.110:8006
+ url: https://10.0.30.20:8006
icon: /assets/icons/proxmox.svg
- title: TrueNAS · smaug
url: https://10.0.40.30