Skip to content

fix(observability): bind Alertmanager to loopback (#70) - #181

Merged
Gerrrt merged 2 commits into
mainfrom
gerrrt/monitoring-bind-addr-security-d401aa
Aug 30, 2026
Merged

fix(observability): bind Alertmanager to loopback (#70)#181
Gerrrt merged 2 commits into
mainfrom
gerrrt/monitoring-bind-addr-security-d401aa

Conversation

@Gerrrt

@Gerrrt Gerrrt commented Aug 30, 2026

Copy link
Copy Markdown
Owner

What changed

Alertmanager (9093) now binds to 127.0.0.1 instead 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.md saying only the monitoring host ran an agent. That document was about a day out of date. Checking the host instead:

$ curl -sG localhost:9090/api/v1/query --data-urlencode 'query=count by (host) (node_uname_info)'
oracle, prometheus

$ curl -s 'localhost:3100/loki/api/v1/label/host/values'
morpheus, oracle, prometheus

oracle has 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 — so 10.0.99.20:9090 and :3100 are 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_ADDR to the management IP looked like cheap defence in depth, on the reasoning that a 2012 MacBook Pro has Wi-Fi. ip -br addr shows one interface, a USB ethernet adapter already on 10.0.99.20/24. 0.0.0.0 and 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 — oracle is done, Saruman is what remains.

Blast radius

Monitoring host (prometheus, 10.0.99.20) only. Removes one port from VLAN reachability, adds none.

  • Alertmanager's externalURL becomes on-host-only. It seeds a JSON field in webhook payloads; no receiver template renders it, so no notification text changes. Left pointing at MONITORING_HOST because that is still the host you would tunnel to. Off-host, silences are in Grafana under Alerting.
  • A browser bookmark to 10.0.99.20:9093 stops working. Nothing in the repo documents one — every runbook already drove Alertmanager from localhost on the host or via docker exec ... amtool, both unaffected.
  • 127.0.0.1 is 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.
  • oracle is untouched, which is the point of the revision.
  • No flags changed. Reachability was the problem, not --web.enable-remote-write-receiver, --web.enable-lifecycle or auth_enabled: false.

Left open, deliberately

Prometheus and Loki stay published and unauthenticated. Anything that can route to 10.0.99.20 can 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 in SECURITY.md beside 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.

  • No change to network segmentation or firewall rules
  • No new port published to a VLAN that could not already reach the service
  • No credential added outside secrets/*.sops.yaml

Verification

make validate passes with nothing skipped. scripts/check_docs.py compares the ports table against compose.yaml; confirmed the guard is live rather than vacuously passing by reverting a row and watching it fail.

Resolved binds from docker compose config against a seeded validation env:

alertmanager     127.0.0.1 : 9093 -> 9093/tcp
alloy            127.0.0.1 : 12345 -> 12345/tcp
alloy              0.0.0.0 : 1514 -> 1514/udp
grafana            0.0.0.0 : 3000 -> 3000/tcp
loki               0.0.0.0 : 3100 -> 3100/tcp
prometheus         0.0.0.0 : 9090 -> 9090/tcp

Not yet deployed. After make up on the monitoring host: ss -ltn shows 127.0.0.1:9093, and 9090/3100/3000 still on 0.0.0.0. Then the checks that prove nothing quietly broke — Grafana -> Alerting -> Silences can create one, oracle still appears in count by (host) (node_uname_info) and its log stream is still advancing, {host="morpheus"} still returns firewall logs, Prometheus targets all UP, and make reload succeeds.

  • make validate passes
  • Deployed to the lab and confirmed working
  • Docs updated (docs/, service README, or docs/roadmap.md)

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
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 Gerrrt changed the title fix(observability): bind the unauthenticated ports to loopback (#70) fix(observability): bind Alertmanager to loopback (#70) Aug 30, 2026
@Gerrrt
Gerrrt marked this pull request as ready for review August 30, 2026 23:42
@Gerrrt
Gerrrt merged commit 7a7044d into main Aug 30, 2026
3 checks passed
@Gerrrt
Gerrrt deleted the gerrrt/monitoring-bind-addr-security-d401aa branch August 30, 2026 23:47
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Prometheus, Loki and Alertmanager bind 0.0.0.0 with no authentication

1 participant