Deployment

One configuration flag on the engine, one subscriber process per engine, and one 64 byte key file. Nothing is installed in the serving path.

How you get the binary

There is no public download, and this page is not a self-serve install. The tap is closed source and binary vended: release credentials are issued as part of an engagement rather than handed out from a download page. Read this to know what deploying it would involve and what your security review would be reviewing. It is written to be useful before you have the artifact, not only after.

What arrives is one tarball per release, infertap-<tag>-x86_64-linux.tar.gz: the binary, the systemd unit files whose headers are themselves the install instructions, and an example environment file. A deployment needs no checkout of any repository of ours. Beside it travel a CycloneDX SBOM and SHA256SUMS. A signed container image is published the same way.

Every artifact carries a cosign signature made by a key that never leaves KMS and answers only to tag builds. You verify origin first and integrity second, against a public key delivered once and out of band, never beside the artifact it verifies. That verification runs offline, with the public transparency log deliberately opted out of, which is what lets an air gapped site verify a release without reaching anything of ours at install time.

Engine contract

The engine must publish KV cache events. In vLLM that is one flag, and it forks nothing:

vllm serve MODEL \
  --kv-events-config '{"enable_kv_cache_events": true, "endpoint": "tcp://*:5557"}'

The endpoint address is the common first mistake. Binding tcp://127.0.0.1:5557 publishes to loopback only. If the subscriber runs in a different network namespace, which it does under a container runtime or an orchestrator, it connects successfully and receives nothing forever. The publisher reports no error, because from its side nothing is wrong.

Hash algorithm

The default is fine. --prefix-caching-hash-algo sha256_cbor is more reproducible across environments and vLLM's own prefix caching documentation recommends it for that reason. It is not required here: fleet wide block identity is derived from block contents rather than from the engine's hash, so it does not depend on the engine's algorithm or seed.

systemd

The shipped unit is packaging/systemd/infertap.service, and every hardening choice in it exists to enforce a claim on the trust page:

SettingEffect
DynamicUser, empty capability setRuns unprivileged with no capabilities to drop later
Read only filesystem, one writable path/var/lib/infertap holds the record stream and is the only thing it can write
LoadCredential plus --key-fileThe key is never on argv and never in the environment
MemoryMax=1G, TasksMax=16, LimitNOFILE=256The bounds are enforced a second time at the operating system layer
Restart=on-failureA restart re resubscribes. A publisher sequence reset is not treated as a drop, and an abrupt stop leaves at most one truncated final line

Container

packaging/docker/Dockerfile builds a minimal Debian image with libzmq5 as the only added library, running as a non root user. There is no EXPOSE, because there is no listener. Run it with the least privilege the runtime offers:

docker run --read-only --cap-drop=ALL --security-opt no-new-privileges \
  -v /etc/infertap/key:/run/secrets/key:ro \
  -v /var/lib/infertap:/var/lib/infertap \
  infertap --endpoints i0=tcp://engine:5557 \
           --out-dir /var/lib/infertap --key-file /run/secrets/key

Continuous integration builds the image and runs --show-payload inside it on every push, so the published image is checked against the same output the trust page describes.

Under an orchestrator

There is no manifest in the repository yet. The shape below is guidance rather than a shipped artifact, and it follows from three properties of the process rather than from a preference.

Run it as a native sidecar

One subscriber per engine process, in the engine's own pod. Kubernetes native sidecars, meaning an init container with restartPolicy: Always, are the right shape: the subscriber starts before the engine and is torn down after it, so the window where the engine publishes to nobody is as small as the platform allows. A plain second container is racier in both directions.

Sharing the pod also solves the endpoint problem above. The engine can publish to tcp://127.0.0.1:5557 and stay off the pod network entirely, because the subscriber is in the same network namespace.

Liveness only, no readiness

The subscriber serves no traffic, so it has nothing to be ready for. A readiness probe on it can only do harm: it gates the pod on a process that is not in the request path. A liveness probe is appropriate, and it should check that the process is alive rather than that it is receiving events. A quiet engine is a legitimate state, and a probe that treats silence as failure will restart the subscriber precisely when there is nothing to record.

Sizing the volume

An emptyDir is the natural home for the record directory in a pod, and it must be sized against the retention cap rather than against the expected rate. Peak disk is --max-disk-bytes plus one active segment, and orphans left by a crash sit outside the cap because the process will not delete data it never got to ship. Size the volume to the cap plus a margin, and set the cap rather than leaving it at the 16 GiB default if the volume is smaller.

An emptyDir is lost when the pod is. Records that have not shipped when a node drains are gone, which is bounded by the shipping interval rather than by the retention cap. That is the tradeoff for keeping the subscriber in the engine's pod, and it is usually the right one, because a subscriber outside the pod cannot use loopback and cannot be lifecycle bound to the engine.

A full sizeLimit evicts the whole pod, engine included. If the volume carries a sizeLimit and the record directory fills past it, the kubelet does not fail a write. It evicts the pod, and your engine restarts because a monitoring sidecar wrote too much. Keep --max-disk-bytes comfortably under sizeLimit: the gap has to hold one active segment plus any orphans from earlier crashes, so give it a couple of segments of headroom, not a few kilobytes.

Prefer medium: Memory for the volume. It removes disk contention with model loading and checkpoints, and a tap should not be doing disk I/O on a GPU node anyway. One rule comes with it: a memory backed volume counts against the pod's memory limit, so the limit must cover sizeLimit plus everything else in the pod with headroom, or filling the volume becomes an out of memory kill instead of an eviction.

Set the cap explicitly in pods rather than inheriting the default. 16 GiB is sized for a host disk that also survives long shipping outages. In a pod, where the volume competes with memory or triggers eviction, a few GiB with a working shipper is the right shape, and a shipping outage longer than that buys is better handled by fixing the shipper than by holding gigabytes of records inside an engine pod.

Provisioning the key

Records are pseudonymized before they leave the host, under a key you generate and hold. Every instance in a fleet must be provisioned from the same key file, because identities derived under different keys cannot be compared and every cross instance figure would read zero.

openssl rand -hex 64 > /etc/infertap/key
chmod 400 /etc/infertap/key

The file is exactly 64 bytes, hex or base64: a 32 byte key followed by a 32 byte nonce. Any other length is refused at startup, and so are two identical halves, which is what generating 32 bytes and pasting it twice produces. Both are fail closed rather than warnings, because a key file that is subtly wrong produces records that look correct and compare against nothing.

Distribute it the way you already distribute secrets to inference nodes. The process needs no other credential, and rotating it is covered under Operations.

First run

Before pointing it at an engine, look at what it emits:

infertap --show-payload

It prints every record the process can emit, pseudonymized under a labeled test key, with no engine required, and ends with the egress audit count. That output is byte compared against the live path by test, so what it shows is what ships. The record format section of the contract is the complete field enumeration behind it.