fix(security): bind Grafana to loopback instead of publishing admin/admin on all interfaces - #206
Merged
kuny0707 merged 1 commit intoAug 3, 2026
Conversation
barbatos2011
force-pushed
the
security/grafana-exposure
branch
2 times, most recently
from
August 3, 2026 09:02
28f417b to
03b6ba3
Compare
…admin password RenderMonitoringCompose emitted the Grafana service with an unconditional host port publication and no environment block at all - no GF_SECURITY_ADMIN_PASSWORD, no auth hardening - so the deployed container kept grafana-oss's built-in admin/admin while docker published it on 0.0.0.0. On a cloud VM with a permissive security group that is the public internet: full node metrics, the ability to lock the operator out, and - because a Grafana admin can add arbitrary datasources and query them through the backend proxy - a server-side request primitive into the host's internal network and cloud metadata endpoints. Grafana is now bound to 127.0.0.1 by default. A new monitoring.grafana.expose restores the wide bind, but is rejected unless monitoring.grafana.admin_password_env is also set, so the escape hatch cannot recreate the finding; the renderer independently falls back to loopback for an unvalidated intent. The password is rendered as a required compose variable reference, so the secret never enters the 0644 compose file and an unset or empty value fails at interpolation before any container is created. Verified against real Docker: the default resolves to host_ip 127.0.0.1, and expose without a password falls back to loopback. Breaking: Grafana is no longer reachable off-host by default. Use an SSH tunnel, a reverse proxy, or the new expose opt-in.
barbatos2011
force-pushed
the
security/grafana-exposure
branch
from
August 3, 2026 09:11
03b6ba3 to
b430dc3
Compare
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.
trond apply --monitordeployed a Grafana reachable from anywhere that could route to the host, with the image's built-in credentials. Found by an audit ofdevelopat0c654cd.The problem
RenderMonitoringComposeemitted the Grafana service with an unconditional- "{{.GrafanaPort}}:3000"and noenvironment:block at all — noGF_SECURITY_ADMIN_PASSWORD, no auth hardening — so the container kept grafana-oss's built-inadmin/adminwhile docker published it on0.0.0.0. Nothing else in the repo supplies a password;README.md:371documentsadmin/admin.On a cloud VM with a permissive security group that is the public internet. The payoff is not just metrics: a Grafana admin can create arbitrary datasources and query them through the backend proxy, which turns it into a server-side request primitive into the host's internal network and cloud metadata endpoints.
What changed
Grafana binds to
127.0.0.1by default.A new
monitoring.grafana.exposerestores the all-interfaces bind, but is rejected unlessmonitoring.grafana.admin_password_envis also set — so the escape hatch cannot recreate the finding. The renderer independently falls back to loopback for an unvalidated intent, so it fails closed rather than honouring a request it cannot verify.The password is named by env var, not embedded: it renders as a required compose variable reference, so the secret never enters the 0644 compose file, and an unset or empty value fails at interpolation before any container is created.
Verified against real Docker (
docker compose configon four rendered configurations): the default resolves tohost_ip: 127.0.0.1;expose: truewith a password drops the bind address and passes the secret through as a reference;expose: truewithout a password stays on loopback.Note the finding's premise that the Prometheus port already had loopback treatment turned out to be wrong — Prometheus is opt-in-published via a conditional, and there was no pre-existing bind-address idiom to follow.
Behaviour changes worth reviewing
ssh -L 3000:127.0.0.1:3000), a reverse proxy, or the newexposeopt-in. This mostly affects SSH/remote targets; for local targets trond already advertisedgrafana_urlashttp://127.0.0.1:<port>.expose: truewithoutadmin_password_env— unreachable for any intent written today, since both fields are new.GF_SECURITY_ADMIN_PASSWORDonly applies on first initialisation of thegrafana_datavolume. That is Grafana's behaviour, not ours; setting it for an already-running stack does not rotate an existing password.Testing
go test ./... -race -count=1,go vet ./...andgofmtclean on the branch. New tests parse the rendered compose as YAML rather than substring-matching, and assert a set password never appears in the rendered file.Unrelated bug noticed while tracing callers
deployMonitoringanddeployNetworkMonitoringbuild the Grafana datasource URL ashttp://prometheus:<host port>, which ishttp://prometheus:0under the default unexposed-Prometheus config. Pre-existing and untouched here, but worth its own fix.