Skip to content

Gate all of /internal/* behind one access policy and drop the Docker CIDR widening #214

Description

@thushan

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.

  1. Wire access_policy through the rest of /internal/* behind
    gate_internal_api (it is inert today and logs a startup warning if set).
  2. 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.
  3. 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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions