Manual setup
This page walks through a from-scratch sidecar deployment, and is home to the operational reference: systemd, hardening, upgrades, deregistration, the CLI, and troubleshooting. If you’d rather start from a pre-filled config, use the automated setup — the app generates a ready-to-mount bundle for you.
Before you start
Section titled “Before you start”You need a registration token (rz_reg_…) from an org admin — issued in
Settings → Hosts → Registration tokens. See
Sidecar authentication & configuration
for token semantics and the full config reference.
Install and run
Section titled “Install and run”-
Get the sidecar.
Terminal window docker pull ghcr.io/ringzero/sidecar:1Tags:
1(latest stable major — recommended),1.6,1.6.3(exact pin). Images are multi-arch (linux/amd64,linux/arm64), distroless, and run as non-root UID65532.Terminal window curl -fsSLo ringzero-sidecar \https://get.ringzero.tech/sidecar/1.6.3/ringzero-sidecar-linux-amd64curl -fsSLo ringzero-sidecar.sha256 \https://get.ringzero.tech/sidecar/1.6.3/ringzero-sidecar-linux-amd64.sha256sha256sum -c ringzero-sidecar.sha256chmod +x ringzero-sidecar && sudo mv ringzero-sidecar /usr/local/bin/Builds are available for
linux-amd64andlinux-arm64. Always verify the checksum. -
Write the config. Minimal
/etc/ringzero/sidecar.yaml:/etc/ringzero/sidecar.yaml api_url: https://api.ringzero.tech/v1registration_token_file: /run/secrets/ringzero-tokenlabels:env: productionservice: acme-webSanity-check it before first run:
Terminal window ringzero-sidecar validate --config /etc/ringzero/sidecar.yaml -
Provide the token securely. Put the token in the file the config points at, readable only by the sidecar’s user (create one if it doesn’t exist:
sudo useradd --system --no-create-home ringzero):Terminal window sudo install -m 0400 -o ringzero /dev/stdin /run/secrets/ringzero-token <<< "$RINGZERO_REGISTRATION_TOKEN"In Compose/Kubernetes, mount it as a secret instead — see the deployment guides.
-
Run it.
Terminal window docker run -d --name ringzero-sidecar \-v /etc/ringzero/sidecar.yaml:/etc/ringzero/sidecar.yaml:ro \-v /run/secrets/ringzero-token:/run/secrets/ringzero-token:ro \-p 7301:7301 \--health-cmd "ringzero-sidecar healthcheck" \ghcr.io/ringzero/sidecar:1Port 7301 is the challenge listener — it must be reachable from RingZero whenever the sidecar (re)registers, i.e. at every start.
Terminal window ringzero-sidecar --config /etc/ringzero/sidecar.yamlFor a production daemon, run it under systemd — unit below.
-
Verify. A successful first run logs the full registration handshake:
{"level":"info","msg":"attestation sent","hostname":"web-1.acme.internal","addresses":["203.0.113.24"]}{"level":"info","msg":"challenge received","port":7301}{"level":"info","msg":"challenge satisfied"}{"level":"info","msg":"host registered","host":"hst_2d8e","status":"active"}{"level":"info","msg":"heartbeat loop started","interval":"60s"}Then confirm from both sides:
-
In the app: Settings → Hosts shows the host as
activewith a fresh Last heartbeat. -
On the host (binary installs):
Terminal window curl -s localhost:7302/healthz{ "status": "ok", "host": "hst_2d8e", "registered": true, "lastHeartbeat": "2026-07-22T09:14:32Z" }For containers, the image is distroless (no shell, no curl) — use the health command instead:
Terminal window docker inspect --format '{{.State.Health.Status}}' ringzero-sidecar
What
activemeans and what happens next: host lifecycle. -
Bare metal / VMs (systemd)
Section titled “Bare metal / VMs (systemd)”The unit is stack-independent — it serves JVM, Go, Rust, Node.js, or anything else on the host. One sidecar per host covers every service on it.
[Unit]Description=RingZero sidecarAfter=network-online.targetWants=network-online.target
[Service]User=ringzeroExecStart=/usr/local/bin/ringzero-sidecar --config /etc/ringzero/sidecar.yamlRestart=on-failureStateDirectory=ringzeroNoNewPrivileges=trueProtectSystem=strictReadOnlyPaths=/etc/ringzero /run/secrets/ringzero-token
[Install]WantedBy=multi-user.targetStateDirectory=ringzero gives the sidecar its writable
/var/lib/ringzero (where the host credential
lives) while ProtectSystem=strict locks down everything else.
sudo systemctl daemon-reloadsudo systemctl enable --now ringzero-sidecarsudo systemctl status ringzero-sidecarContainer hardening
Section titled “Container hardening”What the image already is: distroless, statically linked, no shell or package
manager, non-root UID 65532, both ports unprivileged. What you should add in
Kubernetes:
securityContext: readOnlyRootFilesystem: true allowPrivilegeEscalation: false capabilities: drop: [ALL] seccompProfile: type: RuntimeDefaultvolumeMounts: - name: ringzero-state mountPath: /var/lib/ringzero # the sidecar's only writable path# volumes:# - name: ringzero-state# emptyDir: { sizeLimit: 1Mi }The sidecar writes exactly one thing — the host credential under
/var/lib/ringzero — so readOnlyRootFilesystem: true plus a tiny emptyDir
there is a complete sandbox. (An emptyDir credential dies with the pod;
that’s fine — the next start re-registers idempotently.)
Upgrading the sidecar
Section titled “Upgrading the sidecar”There is no self-update — a security agent that rewrites itself would be a supply-chain liability, and upgrades flowing through your deploy pipeline is the same deploy-access-is-proof model as registration itself. To upgrade: pull the new tag (or download + checksum the new binary) and restart. Registration is idempotent, so the host record survives with no scope gap beyond the restart (7301 reachability applies, as at any start).
Tag strategy: track 1 to pick up patch/minor releases on every redeploy;
pin 1.6.3 in change-controlled environments and bump deliberately.
Version skew policy: the control plane supports the current and previous
major version. When a sidecar version enters its 90-day sunset window, the
heartbeat response says so — the sidecar logs warn version_deprecated and
Settings → Hosts badges the host. After sunset, heartbeats are rejected
with version_unsupported and the host expires until upgraded.
Deregistration and uninstall
Section titled “Deregistration and uninstall”Stopping the sidecar removes the host from scan scope automatically within
~3 minutes (3 missed heartbeats → expired). To leave scope immediately:
ringzero-sidecar deregisterThis authenticates with the host credential and moves the host to expired
right away (audit event: host_deregistered). It works while the daemon runs
or after it has stopped. For ephemeral infrastructure — CI runners, preview
environments — set deregister_on_shutdown: true so a clean SIGTERM does this
for you.
Full uninstall: deregister, stop the process/container, then delete
/etc/ringzero/, /var/lib/ringzero/, and the token file. (A host that was
not deregistered and later restarts simply
reactivates — nothing breaks either way.)
CLI reference
Section titled “CLI reference”| Command | What it does |
|---|---|
ringzero-sidecar (default: run) | Start the daemon: register, then heartbeat. |
ringzero-sidecar validate | Lint the config: unknown keys, malformed durations, token file existence/permissions/rz_reg_ shape (never prints the token). --check-network additionally verifies DNS and outbound 443 to the API (through the configured proxy) without registering. Non-zero exit on any finding — CI-friendly. |
ringzero-sidecar deregister | Move this host to expired immediately using the host credential. |
ringzero-sidecar healthcheck | Probe the local /healthz; exit 0 for ok/degraded, non-zero otherwise. Exists because the image has no curl — use as Docker HEALTHCHECK or K8s exec probe. |
ringzero-sidecar version | Print version, commit, and architecture — the same string heartbeats report. |
Troubleshooting
Section titled “Troubleshooting”Symptom (log error code) | Cause | Fix |
|---|---|---|
challenge_unreachable | RingZero couldn’t connect to port 7301 at the attested address | Open inbound 7301 (allowlistable to the published ranges); check NAT — or set challenge.advertise_address |
challenge_wrong_responder | The challenge reached a different sidecar — a K8s Service is load-balancing 7301 across replicas | Switch to the DaemonSet per-node pattern |
challenge_port_in_use | Second sidecar in the same network namespace | One sidecar per registration unit — stop the extra instance |
token_expired | Registration token past its expiry | Issue a fresh token in Settings → Hosts → Registration tokens, update the secret, restart |
token_revoked | Token was revoked by an admin | Same as above — and check with your admin why |
host_revoked | The host was revoked; sidecar exits non-zero | Admin decision — re-registering needs a fresh token; see registration lifecycle |
reactivation_failed | Host expired and auto-reactivation couldn’t complete the challenge | Open 7301 and restart the sidecar (token not needed — it reuses the host credential) |
clock_skew | Host clock off by more than 60 s (attestations are time-signed) | Enable NTP/chrony and re-run |
attestation_rejected | Host identity failed validation (e.g. loopback-only addresses) | Ensure a routable address; containers need the pod/host network, or advertise_address |
version_unsupported | Sidecar older than the supported N/N-1 majors | Upgrade and restart |
| Heartbeat TLS/certificate errors | TLS-intercepting proxy in the path | The sidecar pins end-to-end TLS — bypass the interception for api.ringzero.tech |
Host flips to expired in the app | Heartbeats stopped — sidecar down, egress blocked | Check process/container status, outbound 443, and /metrics heartbeat counters |
Raise verbosity with RINGZERO_LOG_LEVEL=debug while diagnosing; drop back to
info afterwards.