Scan gateway
The Scan Gateway lets you register and scan private infrastructure without exposing any host to the internet. You run one per network (VPC, data center, cluster); it holds a single outbound mutual-TLS tunnel to RingZero and does two things:
- Relays registration — gateway-mode sidecars in the network talk only to the gateway; their attestation, heartbeats, and challenge ride the tunnel out to RingZero.
- Runs scans from inside — the AI pentest agents’ probe traffic egresses from the gateway, inside your network, so it can reach private hosts that RingZero could never reach directly.
Read Connectivity & network topologies first if you haven’t — it explains when to use gateway mode and how the trust model works.
Requirements
Section titled “Requirements”- Outbound 443 to
gateway.ringzero.techandapi.ringzero.tech. No inbound from the internet. - A private network scope (RFC1918 / CGNAT / IPv6 ULA CIDRs) the gateway will serve — public ranges are rejected.
- An Admin or Owner to issue the enrollment token and approve the gateway’s scope.
Deploy
Section titled “Deploy”-
Issue an enrollment token. In the app, Settings → Gateways → Enroll gateway. You get a single-use, short-lived token (
rz_gw_…) and choose the network scope this gateway will serve. -
Get the gateway.
Terminal window docker pull ghcr.io/ringzero/gateway:1Multi-arch (
linux/amd64,linux/arm64), distroless, non-root UID65532.Terminal window curl -fsSLo ringzero-gateway \https://get.ringzero.tech/gateway/1.0.3/ringzero-gateway-linux-amd64curl -fsSLo ringzero-gateway.sha256 \https://get.ringzero.tech/gateway/1.0.3/ringzero-gateway-linux-amd64.sha256sha256sum -c ringzero-gateway.sha256chmod +x ringzero-gateway && sudo mv ringzero-gateway /usr/local/bin/ -
Write the config (
/etc/ringzero/gateway.yaml):/etc/ringzero/gateway.yaml tunnel_url: https://gateway.ringzero.tech/v1/gateway/tunnelapi_url: https://api.ringzero.tech/v1enrollment_token_file: /run/secrets/ringzero-gateway-token# Private CIDRs this gateway may serve. Admin-approved in the app.# Public ranges are rejected — public hosts use direct mode.network_scope:- 10.0.0.0/8- fc00::/7name: acme-prod-vpc# Internal listener that gateway-mode sidecars dial (mTLS). Bind to the# internal interface only — never 0.0.0.0 on an internet-facing host.sidecar_ingest:port: 7300bind: 10.0.0.5health:port: 7402bind: 127.0.0.1metrics: trueLint it before first run:
Terminal window ringzero-gateway validate --config /etc/ringzero/gateway.yaml --check-network--check-networkresolves DNS, probes outbound 443, and rejects any publicly-routable CIDR innetwork_scope. -
Provide the enrollment token securely — mount it as a file (
0400, owned by the gateway user):Terminal window sudo install -m 0400 -o ringzero /dev/stdin /run/secrets/ringzero-gateway-token <<< "$RINGZERO_ENROLLMENT_TOKEN" -
Run it.
Terminal window docker run -d --name ringzero-gateway \-v /etc/ringzero/gateway.yaml:/etc/ringzero/gateway.yaml:ro \-v /run/secrets/ringzero-gateway-token:/run/secrets/ringzero-gateway-token:ro \--health-cmd "ringzero-gateway status" \ghcr.io/ringzero/gateway:1Terminal window ringzero-gateway --config /etc/ringzero/gateway.yaml -
Approve it. The gateway appears in Settings → Gateways as
pending. An Admin/Owner reviews the requested network scope and approves — it moves toactiveand starts serving hosts. This approval step is the guardrail on which private ranges a gateway is trusted to reach.
Point sidecars at the gateway
Section titled “Point sidecars at the gateway”Hosts in the gateway’s network run their sidecar in gateway mode — no inbound port, no internet egress, they only reach the gateway:
mode: gatewaygateway_endpoint: 10.0.0.5:7300registration_token_file: /run/secrets/ringzero-tokenlabels: env: production service: acme-webEverything else about the sidecar — the registration token, heartbeats, host lifecycle — works exactly as in direct mode, just carried over the gateway instead of a direct connection.
Kubernetes
Section titled “Kubernetes”Run the gateway as a StatefulSet in the cluster; sidecars reach it by Service DNS. Because sidecars dial out to the gateway, this needs no ingress and works with ClusterIP-only services — the case where direct mode struggles.
apiVersion: apps/v1kind: StatefulSetmetadata: name: ringzero-gatewayspec: serviceName: ringzero-gateway replicas: 1 # one identity per StatefulSet — see HA below selector: matchLabels: { app: ringzero-gateway } template: metadata: labels: { app: ringzero-gateway } spec: containers: - name: ringzero-gateway image: ghcr.io/ringzero/gateway:1 ports: - containerPort: 7300 # sidecar-ingest (cluster-internal) volumeMounts: - name: ringzero-gateway-config mountPath: /etc/ringzero readOnly: true - name: ringzero-state # persistent gateway credential mountPath: /var/lib/ringzero resources: requests: { cpu: 50m, memory: 64Mi } limits: { memory: 256Mi } # sized by scan concurrency — tune volumes: - name: ringzero-gateway-config secret: secretName: ringzero-gateway # carries gateway.yaml + token volumeClaimTemplates: - metadata: { name: ringzero-state } spec: accessModes: [ReadWriteOnce] resources: { requests: { storage: 64Mi } }---apiVersion: v1kind: Servicemetadata: name: ringzero-gatewayspec: selector: { app: ringzero-gateway } ports: - port: 7300 targetPort: 7300A StatefulSet (not a Deployment) because the gateway derives a persistent
mTLS credential at enrollment and must keep it across restarts — the
volumeClaimTemplate gives it a stable /var/lib/ringzero. The bundled
gateway.yaml in the Secret should point enrollment_token_file at
/etc/ringzero/token (the Secret carries both keys, mounted together), matching
the sidecar bundle convention.
Gateway-mode sidecars then set gateway_endpoint: ringzero-gateway.default.svc:7300.
A Service round-robins fine here — sidecars dial out, so a long-lived
sidecar→gateway connection simply pins to whichever gateway pod it reaches
(unlike the direct-mode inbound challenge, which can’t be load-balanced).
High availability
Section titled “High availability”A gateway isn’t fully stateless — it holds a persistent enrolled identity plus
a soft-state (tunnel-synced) authorized-target table — but it is
restart-tolerant and horizontally redundant. Run several separately-enrolled
gateways per network: each needs its own enrollment token and its own
persistent state (a second StatefulSet, or replicas: N where each pod gets a
distinct token — a single-use token can’t be shared across pods). Any active
gateway whose network_scope covers a host’s address can register and scan it;
RingZero load-balances scan routing across healthy gateways and fails over
automatically. Removing a gateway (Settings → Gateways → Revoke, or scale
its StatefulSet down) simply shifts its hosts to the others.
Observability
Section titled “Observability”GET :7402/healthz (bound to localhost) reports tunnel and enrollment state:
{ "status": "ok", "gateway": "gw_1a2b", "enrolled": true, "tunnel": "connected", "activeStreams": 3 }GET :7402/metrics exposes Prometheus metrics prefixed ringzero_gateway_
(bind health.bind to the pod/host network — not the default 127.0.0.1 — if
Prometheus scrapes it from another pod):
| Metric | Meaning |
|---|---|
ringzero_gateway_tunnel_up | 1 when the outbound tunnel is connected |
ringzero_gateway_scan_streams_active | In-flight scan probe streams |
ringzero_gateway_probe_bytes_total | Bytes proxied to targets |
ringzero_gateway_egress_refused_total{reason} | Probes refused (out_of_scope, host_inactive) — the key abuse-signal metric |
ringzero_gateway_build_info{version} | Running version |
Alert on ringzero_gateway_tunnel_up == 0 (the gateway can’t reach RingZero)
and on any sustained egress_refused_total (something is trying to scan
outside scope).
| Command | What it does |
|---|---|
ringzero-gateway (default: run) | Enroll on first start, then hold the tunnel. |
ringzero-gateway validate | Lint the config; --check-network adds DNS + outbound 443 probes and rejects publicly-routable CIDRs. Never prints the token. |
ringzero-gateway status | Print enrollment and tunnel state. |
ringzero-gateway version | Version, commit, architecture. |
Egress allowlist
Section titled “Egress allowlist”| Host | When |
|---|---|
gateway.ringzero.tech:443 | Runtime — the tunnel |
api.ringzero.tech:443 | Runtime — enrollment and status |
get.ringzero.tech:443 | Install-time — binaries + checksums |
ghcr.io:443, pkg-containers.githubusercontent.com:443 | Install-time — image pulls |
Note the inversion from direct mode: there is no inbound allowlist and no
/v1/meta/ip-ranges to configure — nothing from
RingZero ever dials into your network.