Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Deployment

rypsok runs in one of two modes, with the same configuration file and the same egress policy.

Local mode: a child process over stdio

The host starts rypsok serve --transport stdio and speaks MCP on its standard input and output; nothing listens on the network. This is the mode for a host on the same machine, and the default.

Network mode: Streamable HTTP

rypsok serve --transport http listens on [service.http].bind, loopback by default, and serves MCP on /mcp. The 2026-07-28 revision is served statelessly; the handshake-based revisions keep their sessions in memory until legacy_revisions = false, which refuses them.

A stateless request carries the standard headers of the revision beside the metadata in its body; without them the answer is 400. Its Accept header lists both application/json and text/event-stream (the protocol requires a client to list both; with one the answer is 406):

curl -sS http://127.0.0.1:8737/mcp \
  -H 'Content-Type: application/json' -H 'Accept: application/json, text/event-stream' \
  -H 'MCP-Protocol-Version: 2026-07-28' -H 'Mcp-Method: tools/call' -H 'Mcp-Name: engine_status' \
  -d '{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"engine_status","arguments":{},
       "_meta":{"io.modelcontextprotocol/protocolVersion":"2026-07-28",
                "io.modelcontextprotocol/clientInfo":{"name":"probe","version":"0"},
                "io.modelcontextprotocol/clientCapabilities":{}}}}'
  • Off loopback, callers are required. Each caller has an identifier and a token read from the environment or a file, presented as Authorization: Bearer <token>; rate limits apply per caller.
  • TLS is terminated in front of rypsok, by a reverse proxy or a tunnel. List the name the proxy forwards in allowed_hosts; a bind on 0.0.0.0 or :: requires that list.
  • Host and Origin are validated; a foreign Host is refused, and any Origin is refused unless listed.
  • Connections, request bodies and idle time are bounded (LIM-H1 to LIM-H3). A connection that sends nothing is closed after the idle time too. Every connection holds a file descriptor: set the descriptor limit (LimitNOFILE in the unit, --ulimit nofile for a container) to at least max_connections + 64. Below that, accept fails when the descriptors run out; the listener then waits 100 ms between attempts and logs accept_failed at most once per 10 s.

A systemd unit for network mode, with the egress restrictions of the integration guide:

[Service]
ExecStart=/usr/local/bin/rypsok serve --transport http --config /etc/rypsok/rypsok.toml
LoadCredential=rypsok-mimlys:/etc/credentials/rypsok-mimlys   # token_file = "/run/credentials/rypsok.service/rypsok-mimlys"
DynamicUser=yes
NoNewPrivileges=yes
ProtectSystem=strict
ProtectHome=yes
PrivateTmp=yes
PrivateDevices=yes
ProtectKernelTunables=yes
ProtectControlGroups=yes
RestrictAddressFamilies=AF_INET AF_INET6
RestrictNamespaces=yes
LockPersonality=yes
MemoryDenyWriteExecute=yes
SystemCallFilter=@system-service
SystemCallArchitectures=native
CapabilityBoundingSet=
AmbientCapabilities=
UMask=077
MemoryMax=256M
MemorySwapMax=0
TasksMax=64
LimitNOFILE=128   # max_connections (default 64) + 64
IPAddressDeny=any
IPAddressAllow=127.0.0.0/8 ::1 <the engines' addresses>

Every line below ProtectSystem confines the process to what rypsok needs: a network socket, memory, no devices, no namespaces, no new privileges and no executable memory. MemoryMax, MemorySwapMax=0 and TasksMax turn a resource bomb that got past rypsok’s own bounds into a restart instead of a host-wide problem. Without MemorySwapMax=0 a host with swap slows down instead of restarting the process: the audit measured 200 MiB of the process moved to swap with no kill, because MemoryMax alone lets the kernel swap out before it kills. The IPAddressAllow line names the proxy, the engines and the sites the operator admits: replace <the engines' addresses> with the addresses (or networks) of the engines you configured and of any site you allow, and keep the line in step with [egress].

Does the configuration fit MemoryMax?

rypsok check prints the worst-case memory of your settings and warns when it is above the 256 MiB of this unit. The estimate is

13 MiB
+ limits.fetches_in_flight × fetch.max_bytes
+ limits.parsing_in_flight × (5.2 × fetch.max_bytes + 11 MiB)
+ service.http.max_sessions × 60 KiB   (only while legacy_revisions is on)
+ cache.max_entries × cache.max_entry_bytes   (only when the cache is on)

The constants are measured, once for each Linux build, because the allocator decides how much a process holds after a parse. The figures above are those of the musl build (the release binaries and the image); a glibc build, which cargo install makes on Linux, holds more: its constants are 14 MiB for the process at rest and 10.6 × fetch.max_bytes + 21 MiB for a parse. Both are fitted on the delivered concurrency (four fetches, two parses) at a 5 MiB and a 10 MiB page, so that the estimate, even without the sessions, is above every measured peak (020x); a session takes 58.5 KiB. All are rows LIM-M1 to LIM-M8 of the limits table. The result is an estimate, not a guarantee.

rypsok check uses the constants of the build it runs as and says which build the figure is for. With the defaults it says 122 MiB on musl and 197 MiB on glibc. Four 5 MiB pages at once, from four hosts, measured 98 to 107 MiB without sessions on the musl release build and 171 to 187 MiB on a glibc build (tools/measure-memory --defaults, which prints the estimate beside its measurement). With fetch.max_bytes at its ceiling of 10 MiB a glibc build peaked at 269 to 302 MiB, above the 256 MiB of the unit, and check warns; a musl build peaked at 173 to 175 MiB. On a platform other than Linux check prints the figure with the note that it was not measured there and gives no warning.

On musl the ceilings fetch.max_bytes (10 MiB) and service.http.max_sessions (2048) each fit the unit with the other at its default (194 and 227 MiB); both at once (299 MiB) do not, and check warns. On glibc the first already does not (323 MiB). In a container, --memory takes the place of MemoryMax in the same comparison, and --memory-swap set to the same value takes the place of MemorySwapMax=0 (memory and swap together, so no swap).

Binaries

Each release lists archives for Linux x86_64 and aarch64 (both static), macOS arm64 and Windows x86_64, two container images (below), and a file SHA256SUMS that names all six. Verify a download before running it, in two steps:

  1. Integrity. On Linux, sha256sum --check --ignore-missing SHA256SUMS in the directory that holds the file; elsewhere, compute the file’s SHA-256 and compare it with its line in SHA256SUMS. This shows the file is intact. It does not show who made it: anyone who can replace the file can replace SHA256SUMS beside it.
  2. Origin. gh attestation verify <file> --repo nabbisen/rypsok. Each release file carries a provenance attestation that GitHub signs when this repository’s release workflow builds it from a tag. The command checks the signature and that the file is the one the workflow built. It needs the GitHub CLI and a network.

Container

Each release holds an image for Linux x86_64 and one for Linux aarch64: rypsok-X.Y.Z-image-amd64.tar.gz and rypsok-X.Y.Z-image-arm64.tar.gz. There is no registry; the image is a file of the release, and it contains the very binary of the release’s Linux archive of the same architecture. Verify the file as described under Binaries, then load it. The image is distroless, runs as a non-root user and reads /etc/rypsok/rypsok.toml:

docker load < rypsok-X.Y.Z-image-amd64.tar.gz
docker run --rm -p 127.0.0.1:8737:8737 \
  --read-only --cap-drop ALL --security-opt no-new-privileges \
  --memory 256m --memory-swap 256m --pids-limit 64 --ulimit nofile=128:128 \
  -v "$PWD/rypsok.toml:/etc/rypsok/rypsok.toml:ro" \
  -e RYPSOK_TOKEN_MIMLYS=… rypsok:X.Y.Z-amd64

(rypsok:X.Y.Z-arm64 on aarch64.) To make an image yourself, build the static binary for x86_64-unknown-linux-musl (or aarch64-unknown-linux-musl), copy it to dist/linux-amd64/rypsok (or dist/linux-arm64/rypsok) and run docker build -t rypsok .; the Dockerfile compiles nothing and only copies that file onto the pinned base image. An image made so is your own build, not the release’s.

The image is static and runs as a non-root user; the flags take away what it does not need: a writable root, every capability, privilege escalation, unbounded memory (and swap) and processes.

Inside the container the bind must be 0.0.0.0:8737, so callers are required; publish the port on loopback or behind the proxy only. Restrict the container’s egress at the network level as the integration guide describes.

Which certificates the fetches trust

Every outbound HTTPS connection, to the engines and to the pages web_fetch reads, is checked against the operating system’s certificate store. rypsok bundles no root certificates of its own. The code that does this is the platform verifier of the TLS library (reqwest with the rustls-no-provider feature, which uses rustls-platform-verifier; no webpki-roots).

  • Linux and other Unix systems (not macOS): the roots are read from the system locations, usually /etc/ssl/certs. When SSL_CERT_FILE or SSL_CERT_DIR is set (SSL_CERT_DIR is a colon-separated list), roots are read only from there and the system locations are not read. A private certificate authority is therefore added either to the system store (update-ca-certificates, update-ca-trust) or through these variables, and in the second case the file must hold the public roots as well, for example a copy of the system bundle with the private root appended.
  • macOS and Windows: the system’s own verifier and store; the variables do nothing. Add a private authority to the system store.
  • A minimal container needs a CA bundle. The documented distroless image has one (/etc/ssl/certs/ca-certificates.crt). An image of your own that starts from scratch has none, and rypsok then does not start: it exits with status 1 and says “the HTTP client could not be built”. The same happens when SSL_CERT_FILE names a file that does not exist. A broken trust configuration is thus found at start, not at the first fetch.

Nothing in the configuration file changes this. A certificate that fails verification is, like any failed connection, a fetch that ends unavailable with the reason network; the response does not say why, so check the store first when every HTTPS fetch fails.

Tokens

Generate a token of at least 32 bytes (openssl rand -base64 32), give it to rypsok by token_env or token_file and to the caller out of band. Rotate by adding the new caller, switching the client, then removing the old one; each change is a restart.

State

Nothing persists across a restart: circuit states, counters, the robots.txt cache and the strict-mode set start empty.