GrooveSeek

Semantic search over a Markdown knowledge base, served over MCP.

View the Project on GitHub alphabet-h/grooveseek

21. Take the socket you were given

Context and problem

GrooveSeek authenticates nobody, and says so in the refusal it prints when a non-loopback bind is asked for without --i-know (non_loopback_bind_refusal in grooveseek/src/transport/mod.rs): anything that can reach the port can read the entire knowledge base. Reachability is therefore the whole of the access control — and until v1.10.0, reachability meant a TCP port. A loopback port is open to every account on the host, so several daemons on one machine, one per body of documents and each meant for one caller, were separated by nothing stronger than a convention about which port belonged to whom.

A socket in the file system moves that question somewhere the kernel already answers it. Its owner and its mode are checked at connect(2), before the first byte arrives, by the same code that guards every other file. What it needs is somebody to create it with the right owner and mode before the daemon starts.

The question this record answers is not whether to serve over AF_UNIX. It is who creates the socket, and what serve does when it cannot have the one it was promised.

Decision drivers

Options considered

  1. Take the descriptor a .socket unit already bound, on explicit opt-in, and refuse to start when it cannot be taken. Taken.

  2. Detect socket activation from the environment — use LISTEN_FDS wherever it happens to be set. Rejected. It makes one command line mean two different things, and the variable that decides which is inherited: a shell started from an activated service, a supervisor passing its own descriptors on, or a parent that set the variables for somebody else all reach serve the same way. The check sd_listen_fds(3) puts first exists precisely because the environment travels further than the descriptors do (the LISTEN_PID arms of check_listen_env in grooveseek/src/transport/systemd_fd.rs). Auto-detection would make surviving that check the ordinary path rather than the opted-into one.

  3. Bind a filesystem path of groove’s own (--unix-socket <path>). Rejected. It would work, and it would add a second creator of sockets: the owner and the mode would then be settled by whichever of groove, the unit file and the surrounding umask got there first, differently per deployment. Creating the socket belongs to the service manager in this design, and leaving it there keeps one answer to “who decided who can reach this”.

  4. Put systemd-socket-proxyd between the unit and groove, the arrangement the systemd manual’s own namespace example uses. Rejected on three properties of the proxy, each from a primary source:

    • it has no per-connection timeout and no SO_KEEPALIVE (https://github.com/systemd/systemd/issues/23320), while MCP over Streamable HTTP holds connections open — half-dead ones would sit in its --connections-max= budget;
    • it forwards no credentials: systemd-socket-proxyd(8) says it “will not forward SCM_RIGHTS, SCM_CREDENTIALS, SCM_SECURITY, SO_PEERCRED, SO_PEERPIDFD, SO_PEERSEC, SO_PEERGROUPS and similar”;
    • one proxy serves one socket (https://github.com/systemd/systemd/issues/15599), so each added deployment adds a unit.

    A fourth concern has no document behind it and is recorded here as the estimate it is: the proxy accepts a connection before it has one to the upstream, so a caller’s liveness check would be answered “connected” and then left waiting, rather than failing. This was not measured, and it did not have to be — the three above were enough on their own.

    Either way it puts a second process in front of each daemon, where taking the descriptor directly puts none.

  5. Check the connecting uid with SO_PEERCRED. Rejected — and option 4 is what makes it available to reject, since not going through the proxy is exactly what leaves the credentials reachable. axum’s Connected trait is not sealed (axum-0.8.9/src/extract/connect_info.rs:80-83), axum carries a UnixListener implementation of it as a compile test (axum-0.8.9/src/serve/mod.rs:503-513), and IncomingStream::io() (axum-0.8.9/src/serve/mod.rs:436-439) hands back the UnixStream whose peer_cred() answers the question.

    It is still not taken. The socket’s mode has the kernel ask the same question earlier, and asking it again in the application splits one condition across separate places — the socket’s mode and the application’s own code — which can drift apart. And Connected::connect_info returns Self rather than an io::Result (axum-0.8.9/src/extract/connect_info.rs:82), so a peer_cred() that fails has nowhere to say so: the default would be either fail-open or a refusal with no stated cause.

    The condition that would reopen it: one socket that has to be opened to more than one principal — a monitoring agent beside the intended caller, or a read-only user from elsewhere. The mode would then be widened to a group, and telling the uids apart would have to happen here.

Decision

groove serve accepts on the socket it was handed, and creates none.

Consequences

References