fix(observability): bind Alertmanager to loopback (#70) - #181
Merged
Conversation
Prometheus, Alertmanager and Loki published on ${BIND_ADDR}, which shipped
as 0.0.0.0. None of the three authenticates: Prometheus carries a writable
remote-write receiver, Loki runs auth_enabled: false with a delete API, and
Alertmanager's silence API is how monitoring gets switched off — quietly,
using the system that would otherwise have recorded the act.
Default-deny between VLANs was the only control, and ADR-0002 already
records the weakness that leaves: "a compromised workstation reaches
Winterfell". One firewall rule stood between an ordinary desktop compromise
and write access to the metric and log stores.
Nothing off the monitoring host consumed any of the three. Scrape targets,
Grafana's datasources and the local Alloy all address services by compose
name on the internal network; morpheus arrives by syslog on 1514/udp, not
via Loki. The wide bind was buying reach nobody was using.
So publish only what has an off-host consumer. Grafana and the syslog
receiver keep ${BIND_ADDR}; the other three take a literal 127.0.0.1 beside
the Alloy debug UI that was already there. Hard-coded rather than made a
variable: render-config.sh regenerates .env from .env.example on every
`make up`, so a variable would advertise a knob the deploy path does not
provide, and widening one of these hands a whole VLAN write access to the
stores — that belongs in a reviewed diff.
Silences stay reachable off-host through Grafana, which proxies Alertmanager
over the compose network behind a login, so this costs no capability.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Gerrrt
marked this pull request as draft
August 30, 2026 23:37
…70) The premise of the previous commit was wrong. It claimed nothing off-host consumed Prometheus or Loki, on the strength of roadmap #88 saying only the monitoring host ran an agent. Checking the host rather than the document: `oracle` has been remote-writing to 10.0.99.20:9090 and pushing to :3100 for about eighteen hours — 378 distinct metric names, 42 log lines in ten minutes, and it is not a scrape target, so those published ports are its only path. Loopback-binding them would have cut both silently, since Alloy retries with backoff and buffers rather than failing loudly. So Prometheus and Loki go back to ${BIND_ADDR} and are recorded as an accepted residual instead: published, unauthenticated, with firewall default-deny the only control. Closing that needs authentication in front of the ingest ports and a credential on every agent, which is separate work. Alertmanager keeps the loopback bind, and it was always the sharpest of the three — a silence switches off monitoring and the record of it lives in the system being switched off. Nothing off-host ever used the port; Grafana proxies it over the compose network behind a login, so this costs no capability. Also drops the plan to narrow BIND_ADDR to the management IP. That rested on the host being a 2012 MacBook Pro and therefore having Wi-Fi; it has one interface, a USB ethernet adapter already on VLAN 99, so 0.0.0.0 and 10.0.99.20 are the same thing here. ADR-0012 is rewritten around the rule that actually survived — publish a port when something off-host uses it — and renamed accordingly. Roadmap #88 is corrected to reflect that oracle already has an agent. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Gerrrt
marked this pull request as ready for review
August 30, 2026 23:42
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What changed
Alertmanager (9093) now binds to
127.0.0.1instead of${BIND_ADDR}. Prometheus, Loki, Grafana and the Alloy syslog receiver are unchanged. Plus ADR-0012 and the docs that stated the old arrangement.Why
Closes #70. Anyone who could reach Alertmanager could create a silence, which switches off an alert using the system that would otherwise have recorded the act — the one write surface here whose abuse leaves no trace. Nothing off-host uses port 9093: silences are reached through Grafana, which proxies Alertmanager over the compose network behind a login. So closing it costs no capability.
The scope of this PR shrank, and why
It originally moved Prometheus and Loki to loopback too, on the strength of
docs/roadmap.mdsaying only the monitoring host ran an agent. That document was about a day out of date. Checking the host instead:oraclehas been remote-writing for ~18 hours (720 samples in 12h, 1113 in 24h), 378 distinct metric names, 42 log lines in the last 10 minutes. It is not a Prometheus scrape target — all 13 active targets are compose-internal or SNMP — so10.0.99.20:9090and:3100are its only path. Loopback-binding them would have cut metrics and logs silently, because Alloy retries with backoff and buffers rather than failing loudly.A second assumption also failed: narrowing
BIND_ADDRto the management IP looked like cheap defence in depth, on the reasoning that a 2012 MacBook Pro has Wi-Fi.ip -br addrshows one interface, a USB ethernet adapter already on10.0.99.20/24.0.0.0.0and the management IP are the same thing here, so that would have added a failure mode and bought nothing.docs/roadmap.md#88 is corrected here —oracleis done,Sarumanis what remains.Blast radius
Monitoring host (
prometheus, 10.0.99.20) only. Removes one port from VLAN reachability, adds none.externalURLbecomes on-host-only. It seeds a JSON field in webhook payloads; no receiver template renders it, so no notification text changes. Left pointing atMONITORING_HOSTbecause that is still the host you would tunnel to. Off-host, silences are in Grafana under Alerting.10.0.99.20:9093stops working. Nothing in the repo documents one — every runbook already drove Alertmanager fromlocalhoston the host or viadocker exec ... amtool, both unaffected.127.0.0.1is a host boundary, not a user one: any local account or host-network container on the monitoring host still reaches Alertmanager unauthenticated. Stated in the ADR rather than claimed away.oracleis untouched, which is the point of the revision.--web.enable-remote-write-receiver,--web.enable-lifecycleorauth_enabled: false.Left open, deliberately
Prometheus and Loki stay published and unauthenticated. Anything that can route to
10.0.99.20can read every metric and log line, inject metrics, and delete log ranges. Firewall default-deny is the only control, and ADR-0002 already records what that leaves: "a compromised workstation reaches Winterfell." Recorded as an accepted residual inSECURITY.mdbeside the SNMP and unencrypted-disk entries, not quietly carried. Closing it properly needs authentication in front of the ingest ports plus a credential on every agent — separate work, and a new secret to rotate.secrets/*.sops.yamlVerification
make validatepasses with nothing skipped.scripts/check_docs.pycompares the ports table againstcompose.yaml; confirmed the guard is live rather than vacuously passing by reverting a row and watching it fail.Resolved binds from
docker compose configagainst a seeded validation env:Not yet deployed. After
make upon the monitoring host:ss -ltnshows127.0.0.1:9093, and9090/3100/3000still on0.0.0.0. Then the checks that prove nothing quietly broke — Grafana -> Alerting -> Silences can create one,oraclestill appears incount by (host) (node_uname_info)and its log stream is still advancing,{host="morpheus"}still returns firewall logs, Prometheus targets allUP, andmake reloadsucceeds.make validatepassesdocs/, service README, ordocs/roadmap.md)