Skip to content

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.

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.

  1. Get the sidecar.

    Terminal window
    docker pull ghcr.io/ringzero/sidecar:1

    Tags: 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 UID 65532.

  2. Write the config. Minimal /etc/ringzero/sidecar.yaml:

    /etc/ringzero/sidecar.yaml
    api_url: https://api.ringzero.tech/v1
    registration_token_file: /run/secrets/ringzero-token
    labels:
    env: production
    service: acme-web

    Sanity-check it before first run:

    Terminal window
    ringzero-sidecar validate --config /etc/ringzero/sidecar.yaml
  3. 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.

  4. 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:1

    Port 7301 is the challenge listener — it must be reachable from RingZero whenever the sidecar (re)registers, i.e. at every start.

  5. 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 active with 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 active means and what happens next: host lifecycle.

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.

/etc/systemd/system/ringzero-sidecar.service
[Unit]
Description=RingZero sidecar
After=network-online.target
Wants=network-online.target
[Service]
User=ringzero
ExecStart=/usr/local/bin/ringzero-sidecar --config /etc/ringzero/sidecar.yaml
Restart=on-failure
StateDirectory=ringzero
NoNewPrivileges=true
ProtectSystem=strict
ReadOnlyPaths=/etc/ringzero /run/secrets/ringzero-token
[Install]
WantedBy=multi-user.target

StateDirectory=ringzero gives the sidecar its writable /var/lib/ringzero (where the host credential lives) while ProtectSystem=strict locks down everything else.

Terminal window
sudo systemctl daemon-reload
sudo systemctl enable --now ringzero-sidecar
sudo systemctl status ringzero-sidecar

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 for the sidecar container
securityContext:
readOnlyRootFilesystem: true
allowPrivilegeEscalation: false
capabilities:
drop: [ALL]
seccompProfile:
type: RuntimeDefault
volumeMounts:
- 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.)

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.

Stopping the sidecar removes the host from scan scope automatically within ~3 minutes (3 missed heartbeats → expired). To leave scope immediately:

Terminal window
ringzero-sidecar deregister

This 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.)

CommandWhat it does
ringzero-sidecar (default: run)Start the daemon: register, then heartbeat.
ringzero-sidecar validateLint 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 deregisterMove this host to expired immediately using the host credential.
ringzero-sidecar healthcheckProbe 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 versionPrint version, commit, and architecture — the same string heartbeats report.
Symptom (log error code)CauseFix
challenge_unreachableRingZero couldn’t connect to port 7301 at the attested addressOpen inbound 7301 (allowlistable to the published ranges); check NAT — or set challenge.advertise_address
challenge_wrong_responderThe challenge reached a different sidecar — a K8s Service is load-balancing 7301 across replicasSwitch to the DaemonSet per-node pattern
challenge_port_in_useSecond sidecar in the same network namespaceOne sidecar per registration unit — stop the extra instance
token_expiredRegistration token past its expiryIssue a fresh token in Settings → Hosts → Registration tokens, update the secret, restart
token_revokedToken was revoked by an adminSame as above — and check with your admin why
host_revokedThe host was revoked; sidecar exits non-zeroAdmin decision — re-registering needs a fresh token; see registration lifecycle
reactivation_failedHost expired and auto-reactivation couldn’t complete the challengeOpen 7301 and restart the sidecar (token not needed — it reuses the host credential)
clock_skewHost clock off by more than 60 s (attestations are time-signed)Enable NTP/chrony and re-run
attestation_rejectedHost identity failed validation (e.g. loopback-only addresses)Ensure a routable address; containers need the pod/host network, or advertise_address
version_unsupportedSidecar older than the supported N/N-1 majorsUpgrade and restart
Heartbeat TLS/certificate errorsTLS-intercepting proxy in the pathThe sidecar pins end-to-end TLS — bypass the interception for api.ringzero.tech
Host flips to expired in the appHeartbeats stopped — sidecar down, egress blockedCheck process/container status, outbound 443, and /metrics heartbeat counters

Raise verbosity with RINGZERO_LOG_LEVEL=debug while diagnosing; drop back to info afterwards.