Skip to content

Stop trusting loopback alone; security dependencies - #1

Merged
inkly merged 6 commits into
mainfrom
chore/security-deps
Sep 22, 2026
Merged

inkly merged 6 commits into
mainfrom
chore/security-deps

Conversation

@inkly

@inkly inkly commented Sep 22, 2026

Copy link
Copy Markdown
Collaborator
  • The JWT skipper trusted loopback alone, and also a Host: unix header, which any client sets and the gateway forwards: anyone who reached the gateway had the whole bus API without a token. Now a request is internal when it arrives on the unix socket (by its local address, not a header; the socket is 0600) or from loopback with the boot's secret, as in every other service.
  • The websocket exemption covers the subscription routes only; any other GET with an Upgrade header needs a token.
  • CI and releases build with the latest Go 1.26 patch; jwt, websocket, kin-openapi and Common v0.4.26.
  • The unused npm SDK scaffolding is gone.

Needs the services built from their security-deps branches, which send the secret. Known residual: the subscription routes still need no token.

setup-go with go-version-file: go.mod installs exactly the go directive (1.26.0), so releases were built without the stdlib security fixes shipped since. Ask for the latest 1.26 patch instead.
Common v0.4.26 requires go 1.26.8, so the go directive moves from 1.26.0 to 1.26.8. It replaces mholt/archiver/v3 with mholt/archives, which drops archiver/v3, nwaples/rardecode v1 and xi2/xz from the build (archiver and rardecode have unfixed advisories) and brings ulikunitz/xz to v0.5.15. It also brings golang-jwt/jwt/v4 v4.5.2 and gorilla/websocket v1.5.3. Nothing here used the JWT middleware or AddFile that Common removed.
v0.115.0 is affected by several openapi3filter advisories, two of them reachable from the request validator: a nil-pointer panic on a content parameter without a schema (GO-2026-6112, fixed in v0.144.0) and unbounded decompression of request bodies (GHSA-wq9g-9vfc-cfq9, fixed in v0.131.0). The deepmap/oapi-codegen v1.12.4 echo middleware and the generated code still build against it, so codegen/ is not regenerated.
…ternal secret

The JWT skipper let a request through when its real IP was loopback or its Host header was "unix". Loopback is any local process or container, or a same-host reverse proxy that does not set X-Forwarded-For. The Host header is set by the client, so any TCP client, including one on the LAN going through the gateway, could send Host: unix and reach the whole bus API without a token.

A request now skips the JWT only when it comes from loopback with the per-boot internal secret (external.IsInternalRequest, as the other services do since 2026-09-13), or when it really arrived on the unix socket listener, which is read from the connection's local address, not from a header. App-management publishes there without the secret. The socket was only root-reachable because of the systemd umask (0755, and connect needs write permission); it is now chmod 0600 explicitly.

The websocket GET skip stays: browsers cannot set headers on a WebSocket and the dashboard subscribes without a token. The access-log filter keeps its loopback test, it only decides logging.

The new test drives the whole router: loopback without or with a wrong secret, a LAN address with the secret, a forwarded LAN request and Host: unix over TCP all get 401; loopback (v4 and v6) with the secret, the unix socket and a websocket GET get through.
The JWT skipper let through every GET that carried "Upgrade: websocket", on any path. A client without a token could read /event_type, /action_type, /ysk and every other GET route just by adding that header.

The exemption now also needs the matched route to be one of the four subscribe routes: GET /event/{source_id}, GET /action/{source_id}, GET /socket.io and GET /socket.io/. Echo routes before it runs middleware, so the skipper reads the registered route pattern from c.Path() instead of parsing the URL. The header is compared case-insensitively.

Socket.IO polling (GET without the upgrade, and POST) stays behind the token, as before. The dashboard's socket.io-client 2.x opens the websocket transport directly and only falls back to polling in a browser with no WebSocket. user-service's local-storage listener dials GET /event/local-storage with x/net/websocket, which sends the header, so it still matches.

The subscriptions themselves remain open without a token: the dashboard subscribes without one.

Tests: the upgrade header on /event_type, /action_type and /ysk gets 401, as does a subscribe route without the header and a socket.io POST; the four subscribe routes get past the auth layer; a user JWT, validated against a JWKS served from a stand-in user-service, gets 200 from the LAN and a tampered one gets 401. The README no longer says loopback peers skip the token.
package.json and tsconfig.json generated a typescript-axios client from the OpenAPI spec with openapi-generator. No workflow, goreleaser config or Go code runs them and nothing is published from them; the dashboard has no generated message bus client, it talks to socket.io and the websocket routes directly. Their pinned generator and TypeScript versions only drew dependency alerts.
@inkly
inkly merged commit 948a4b6 into main Sep 22, 2026
1 check passed
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.

1 participant