Skip to content

Sidecar authentication & configuration

The sidecar authenticates with a registration token and reads its settings from a YAML config file, with environment variables as overrides. This page covers the sidecar’s credentials and configuration in depth — for the platform-wide access model (user roles, API key scopes), see Authentication & permissions.

Registration tokens (rz_reg_…) are issued by an Admin or Owner in Settings → Hosts → Registration tokens. They are deliberately single-purpose — a registration token can only register hosts; it cannot read scans, findings, hosts, or anything else. They are:

  • Per-org — a token registers hosts into exactly one organization.
  • Expiring — you choose a lifetime at creation (default 90 days). An expired token stops registering new hosts; already-registered hosts keep heartbeating (heartbeats authenticate with a per-host credential, below).
  • Revocable — revoking a token immediately blocks new registrations with it. Revoking a host immediately removes that host from scan scope.

Rotate by issuing a new token, rolling it out to your deploy pipeline, then revoking the old one — registrations made with the old token are unaffected.

During registration the sidecar derives a host credential, stored only on that host (under /var/lib/ringzero) and rotated automatically. It authenticates heartbeats, challenge responses, and deregistration for that one host and nothing else: even a fully compromised host can’t read your org’s data, register other hosts, or touch the API beyond its own lifecycle. You never handle host credentials directly — they live and die with the registration.

On every start the sidecar re-attests and answers a fresh challenge — registration is not a one-time event. The rules:

  • Idempotent per host. If the control plane matches an existing host (same org + host identity), the existing hst_… record is updated — addresses, labels, sidecar version — never duplicated. Restarting or upgrading the sidecar keeps the same host record with no gap in scan scope beyond the restart itself.
  • Labels are re-attested on registration. To change a host’s labels, edit the config (or RINGZERO_LABELS) and restart. Heartbeats don’t carry labels. Labels feed scan-time scoping.
  • Heartbeats retry, never slow down. Transient failures are retried with jittered backoff within the 60-second interval — the 3-missed-beats expiry math is the product’s freshness guarantee, so the sidecar never voluntarily stretches it. An API outage shorter than ~3 minutes costs nothing.
  • expired hosts self-heal. If heartbeats are rejected because the host expired (say the sidecar was down for ten minutes), it automatically re-runs attestation + challenge using its host credential — no registration token needed, since it’s provably the same host. Port 7301 must be reachable at that moment.
  • revoked is terminal. Revocation is an admin decision; the sidecar logs host_revoked and exits non-zero so your orchestrator’s restart loop surfaces it. Re-registering a revoked host requires a fresh registration token — deliberately.

Multiple sidecars on one host: a second instance in the same network namespace fails fast with challenge_port_in_use (bind conflict on 7301). A second instance with the same attested identity elsewhere just hits the idempotent-re-registration path — both heartbeat the same hst_…, harmless but pointless. One sidecar per registration unit.

The sidecar reads /etc/ringzero/sidecar.yaml by default (override with --config or RINGZERO_CONFIG). Full annotated reference:

/etc/ringzero/sidecar.yaml
# Connectivity mode. `direct` (default) = public/internet-reachable host;
# RingZero dials the inbound challenge below. `gateway` = private host; the
# sidecar connects OUT to an in-network Scan Gateway and everything (attest,
# heartbeat, challenge) rides that outbound connection — no inbound at all.
mode: direct
# Gateway mode only: the in-network Scan Gateway's sidecar-ingest address.
# gateway_endpoint: 10.0.0.5:7300
# RingZero API endpoint. Only change if RingZero support tells you to.
# (Not used directly in gateway mode — the gateway relays control traffic.)
api_url: https://api.ringzero.tech/v1
# The registration token — provide EXACTLY ONE of the following.
# Prefer registration_token_file (mounted secret) over the inline value.
registration_token_file: /run/secrets/ringzero-token
# registration_token: rz_reg_8c1d… # inline — avoid outside local testing
# Optional: labels attached to the host registration, visible in
# Settings → Hosts and used for scan scoping. Re-attested on restart.
labels:
env: production
service: acme-web
team: platform
# Deregister on clean shutdown (SIGTERM/SIGINT). Leave false so rolling
# restarts and upgrades don't flap the registration; set true for ephemeral
# hosts (CI runners, preview environments).
deregister_on_shutdown: false
heartbeat:
# Interval between heartbeats. The host expires after 3 missed beats,
# so with the default 60s a dead host leaves scan scope within ~3 minutes.
interval: 60s
challenge:
# DIRECT MODE ONLY. Port for the registration challenge listener, which
# RingZero must reach whenever the sidecar (re)registers. Inert in gateway
# mode (the challenge rides the outbound connection to the gateway instead).
port: 7301
# Interface to bind the listener on.
bind: 0.0.0.0
# Address (or host:port) the control plane should dial for the challenge
# when the host's own interfaces aren't externally reachable (NAT, K8s).
# advertise_address: 203.0.113.24
health:
# Local health endpoint (GET /healthz) and Prometheus metrics
# (GET /metrics). Bound to localhost by default — never expose publicly.
port: 7302
bind: 127.0.0.1
metrics: true
# Outbound proxy for control-plane traffic (attestation + heartbeats).
# Standard HTTPS_PROXY / NO_PROXY env vars are also honored; this key wins.
# proxy:
# url: http://proxy.internal:3128
log:
level: info # debug | info | warn | error
format: json # json | text

Every setting can be overridden by environment; env wins over file. Secrets support _FILE variants that read the value from a file — prefer these with Docker/Kubernetes secrets.

VariableOverridesNotes
RINGZERO_CONFIGconfig file pathDefault /etc/ringzero/sidecar.yaml
RINGZERO_MODEmodedirect (default) or gateway
RINGZERO_GATEWAY_ENDPOINTgateway_endpointGateway mode only — the Scan Gateway’s host:7300
RINGZERO_API_URLapi_url
RINGZERO_REGISTRATION_TOKENregistration_token
RINGZERO_REGISTRATION_TOKEN_FILEregistration_token_filePreferred for secrets
RINGZERO_LABELSlabelsComma-separated key=value pairs. Replaces the whole map — no merging with the file’s labels
RINGZERO_DEREGISTER_ON_SHUTDOWNderegister_on_shutdown
RINGZERO_HEARTBEAT_INTERVALheartbeat.intervale.g. 60s
RINGZERO_CHALLENGE_PORTchallenge.port
RINGZERO_CHALLENGE_BINDchallenge.bind
RINGZERO_CHALLENGE_ADVERTISE_ADDRESSchallenge.advertise_addresshost or host:port
RINGZERO_HEALTH_PORThealth.port
RINGZERO_HEALTH_BINDhealth.bind
RINGZERO_HEALTH_METRICShealth.metrics
RINGZERO_PROXY_URLproxy.urlBeats HTTPS_PROXY
RINGZERO_LOG_LEVELlog.level

Precedence, highest first: env var → config file → built-in default.

The mode key decides how RingZero reaches this host — and it’s the single most important choice for anything that isn’t a public cloud VM. See Connectivity & network topologies for the full comparison; in short:

  • direct (default) — public/internet-reachable hosts. Everything in the “Network requirements” table below applies, including the inbound challenge.
  • gateway — private hosts. Set gateway_endpoint to the in-network Scan Gateway; the sidecar makes one outbound connection to it and carries attestation, heartbeat, and challenge over that. No inbound 7301, and no internet egress from the host at all — it only needs to reach the gateway. The challenge.* and proxy.* keys are inert in gateway mode.

These apply to direct mode. In gateway mode the host needs only outbound reach to the gateway’s :7300 — see the Scan Gateway page.

DirectionWhatWhy
Outbound 443sidecar → api.ringzero.techAttestation and heartbeats (TLS)
Inbound 7301RingZero → sidecarChallenge — required whenever the sidecar (re)registers
Local 7302you → sidecar/healthz + /metrics — keep private (binds 127.0.0.1 by default)

When exactly is 7301 needed? At every registration — which means every sidecar start, and any automatic reactivation of an expired host. RingZero never initiates a challenge outside a registration the sidecar itself began, so between restarts the port may be firewalled. In practice, leave the rule in place: a restart with 7301 blocked fails with challenge_unreachable. (If opening any inbound port is a non-starter, that’s exactly what gateway mode is for.)

Egress allowlist (for locked-down networks):

HostWhen
api.ringzero.tech:443Runtime — attestation, heartbeats
get.ringzero.tech:443Install-time only — binaries + checksums
ghcr.io:443, pkg-containers.githubusercontent.com:443Install-time only — image pulls

Ingress: challenge connections originate from RingZero’s published source ranges — GET /v1/meta/ip-ranges — so you can allowlist 7301 to those ranges instead of the whole internet. Range changes are announced ≥30 days ahead.

Clock: the host clock must be within 60 seconds of true time (attestations are time-signed) — run NTP/chrony.

IPv6: the sidecar attests all routable v4 and v6 addresses; each is challenge-verified independently, and v6-only hosts are fully supported. advertise_address accepts either family.

Set proxy.url (or rely on HTTPS_PROXY/NO_PROXY) to route control-plane traffic through an egress proxy. The connection uses HTTP CONNECT tunneling — TLS stays end-to-end to api.ringzero.tech, so there is no proxy-CA trust configuration, and TLS-intercepting proxies will break the sidecar (you’ll see certificate errors on heartbeats). The inbound challenge is unaffected by any proxy.

For security reviewers: during registration the control plane delivers a single-use nonce (5-minute TTL) over the sidecar’s outbound TLS connection. The sidecar then serves that nonce, signed with its host credential, at GET /.well-known/ringzero-challenge on port 7301; the control plane dials each attested address, fetches, and verifies. Properties that matter:

  • The listener is read-only — it reflects no request input and executes nothing.
  • The nonce is single-use and expires in 5 minutes.
  • Outside an active registration window, the listener answers every request with an empty 204 — an internet scanner probing 7301 learns only that a RingZero sidecar exists.
  • The listener is deliberately plain HTTP: the payload is a signed, single-use nonce, so channel encryption would add certificate-provisioning complexity for no confidentiality gain.

In direct mode the challenge must land on the specific sidecar that’s registering — which is exactly what a Kubernetes Service can’t guarantee: a Service in front of a multi-replica Deployment round-robins port 7301 to any pod, so challenges reach the wrong sidecar nondeterministically (challenge_wrong_responder). Pick a pattern by how your workload runs:

Per-node (recommended for replicated workloads in direct mode). Run the sidecar as a DaemonSet with hostPort: 7301 and register each node. Every node gets its own hst_…, the challenge dials the node’s address directly, and replica counts stop mattering:

ringzero-daemonset.yaml
apiVersion: apps/v1
kind: DaemonSet
metadata:
name: ringzero-sidecar
spec:
selector:
matchLabels: { app: ringzero-sidecar }
template:
metadata:
labels: { app: ringzero-sidecar }
spec:
containers:
- name: ringzero-sidecar
image: ghcr.io/ringzero/sidecar:1
ports:
- containerPort: 7301
hostPort: 7301
volumeMounts:
- name: ringzero-config
mountPath: /etc/ringzero
readOnly: true
volumes:
- name: ringzero-config
secret:
secretName: ringzero-sidecar

Per-pod. Keep the sidecar in the workload’s pod (the manifests in the deployment guides) only when each pod has its own reachable addresshostNetwork: true, a per-pod LoadBalancer, or a routable pod network. Set challenge.advertise_address if the reachable address isn’t one of the pod’s own interfaces.

Registration unit rule of thumb: whatever answers the challenge at an attested address is what gets registered — a Compose machine, a node (DaemonSet), or a pod. Each gets its own hst_… record.

GET :7302/healthz — liveness and registration state:

{ "status": "ok", "host": "hst_2d8e", "registered": true, "lastHeartbeat": "2026-07-22T09:14:32Z" }

status is ok, or degraded when one or more consecutive heartbeats have failed (still HTTP 200 — the sidecar is alive and retrying). HTTP 500 only when unregistered or revoked. Wire it as your container health probe via ringzero-sidecar healthcheck (the image is distroless — there’s no curl inside).

GET :7302/metrics — Prometheus text format (on by default; the port is local-only). Metrics:

MetricType
ringzero_sidecar_registeredgauge (0/1)
ringzero_sidecar_heartbeats_totalcounter
ringzero_sidecar_heartbeat_failures_totalcounter
ringzero_sidecar_last_heartbeat_timestamp_secondsgauge
ringzero_sidecar_build_info{version="1.6.3"}gauge

Alert on time() - ringzero_sidecar_last_heartbeat_timestamp_seconds > 120 — that’s one beat away from leaving scan scope.

Logs are structured (log.format: json by default); every lifecycle event seen in the manual setup verification is a single line with a stable msg. Raise RINGZERO_LOG_LEVEL=debug while diagnosing; drop back to info afterwards.

  • Mount the token as a file (registration_token_file / Docker secret / Kubernetes Secret volume) rather than an inline env value where possible — env vars leak into docker inspect, crash dumps, and child processes.
  • Scope CI access so only the deploy pipeline can read the token.
  • Rotate tokens on a schedule and when people with access leave.