Problem
The dashboard access policy is a source-IP ACL, but a container NATs the source
address, so the app is making a security decision about a client it can no
longer see. That is why the container config has to widen allowed_cidrs to the
RFC1918 ranges in scripts/generate-container-config.sh just to make
docker run -p 40114:40114 work out of the box.
The deeper issue is that we have two half-controls rather than one:
/internal/ui/ is gated by access_policy (wired in dashboard.RegisterRoutes)
/internal/status*, /internal/health, /internal/metrics and /version are
not gated at all
Verified behaviour today, container with the loopback-only default, request from
a non-allowed bridge address:
/internal/ui/ -> 403
/internal/status -> 200
/internal/status/models -> 200
/internal/status/endpoints -> 200
The JSON is exactly what the dashboard renders. So the CIDR gate protects the
HTML, not the data, and the Docker widening mostly buys a nicer view of
already-readable data.
Proposal
Make gate_internal_api real so there is a single control over all of
/internal/*, then adopt the pattern most comparable products use: let the
operator decide exposure at publish time rather than having the app guess
subnets.
- Wire
access_policy through the rest of /internal/* behind
gate_internal_api (it is inert today and logs a startup warning if set).
- Once that lands, default the container back to loopback-only and document
-p 127.0.0.1:40114:40114 for host-only access, -p 40114:40114 for
deliberate LAN exposure.
- Delete the CIDR widening and its
sed block from
scripts/generate-container-config.sh, and drop the Docker special case from
docs/content/configuration/dashboard.md.
Why not just set allowed_cidrs: ["0.0.0.0/0"] in the container
It looks equivalent today, but it is a trap: once gate_internal_api governs all
of /internal/*, an image shipping 0.0.0.0/0 would expose every internal API
to anyone who can reach the port. It also removes the one real protection the
current ranges give, which is rejecting public source IPs when someone publishes
the port on a cloud host.
Acceptance criteria
gate_internal_api: true applies access_policy to all /internal/* routes
and /version, with the same 403 body and Warn log line as the dashboard gate
- Default stays
false, so existing deployments are unaffected
- Container config no longer needs widened CIDRs for the dashboard to work
- Docs state one access model rather than a UI rule and a separate JSON caveat
Notes
Deferred from #213 deliberately, to avoid changing the security model immediately
before the v0.0.29 release.
Problem
The dashboard access policy is a source-IP ACL, but a container NATs the source
address, so the app is making a security decision about a client it can no
longer see. That is why the container config has to widen
allowed_cidrsto theRFC1918 ranges in
scripts/generate-container-config.shjust to makedocker run -p 40114:40114work out of the box.The deeper issue is that we have two half-controls rather than one:
/internal/ui/is gated byaccess_policy(wired indashboard.RegisterRoutes)/internal/status*,/internal/health,/internal/metricsand/versionarenot gated at all
Verified behaviour today, container with the loopback-only default, request from
a non-allowed bridge address:
The JSON is exactly what the dashboard renders. So the CIDR gate protects the
HTML, not the data, and the Docker widening mostly buys a nicer view of
already-readable data.
Proposal
Make
gate_internal_apireal so there is a single control over all of/internal/*, then adopt the pattern most comparable products use: let theoperator decide exposure at publish time rather than having the app guess
subnets.
access_policythrough the rest of/internal/*behindgate_internal_api(it is inert today and logs a startup warning if set).-p 127.0.0.1:40114:40114for host-only access,-p 40114:40114fordeliberate LAN exposure.
sedblock fromscripts/generate-container-config.sh, and drop the Docker special case fromdocs/content/configuration/dashboard.md.Why not just set
allowed_cidrs: ["0.0.0.0/0"]in the containerIt looks equivalent today, but it is a trap: once
gate_internal_apigoverns allof
/internal/*, an image shipping0.0.0.0/0would expose every internal APIto anyone who can reach the port. It also removes the one real protection the
current ranges give, which is rejecting public source IPs when someone publishes
the port on a cloud host.
Acceptance criteria
gate_internal_api: trueappliesaccess_policyto all/internal/*routesand
/version, with the same 403 body andWarnlog line as the dashboard gatefalse, so existing deployments are unaffectedNotes
Deferred from #213 deliberately, to avoid changing the security model immediately
before the v0.0.29 release.