Architecture

How the PingWAF control plane and data plane fit together, what the gRPC protocol carries, and how policy reaches the edge.

8 min read Edit this page on GitHub

PingWAF splits into two processes that share one binary. The control plane owns all state; the data plane carries traffic and enforces policy. The data plane has no database of its own — and must not be given one.

Control plane

pingwaf server exposes three surfaces:

SurfaceDefault portPurpose
REST API + dashboard9080 (HTTPS)Sites, rules, logs, configuration versions
gRPC ControlPlane9090Agent registration, heartbeats, rule sync, log and metric ingestion
PostgreSQL5432The only durable store; Elasticsearch is optional for request logs

The dashboard is compiled into the binary with rust-embed, so there is no separate frontend deployment to keep in sync with the API.

Data plane

pingwaf agent accepts traffic on :80 and :443, evaluates WAF rules, terminates TLS and proxies to origin pools. It keeps a disk-backed rule cache under cache_dir and rebuilds its in-memory provider whenever the cached configuration hash changes — roughly every five seconds. Requests always read the current provider through ArcSwap, so a policy change never requires a restart.

The gRPC contract

The single source of truth is pingwaf-proto/proto/control_plane.proto. Seven RPCs make up the service:

RPCKindPurpose
RegisterAgentunaryJoin the fleet; receives agent_id, an agent_token, the heartbeat interval and initial site configs
Heartbeatbidirectional streamLiveness, stats and certificate status up; commands down
SyncRulesserver streamNew rule bundles pushed after any configuration change
ShipLogsclient streamBatched request and attack logs
ShipCertEventsclient streamACME issuance and renewal events
ShipMetricsclient streamTraffic and cache metrics
GetSiteConfigunaryPull the full configuration for one site

The same service also answers visitor login, passkey and session checks used by the challenge flow. Those calls are authenticated with the agent token and fail closed: if the control plane is unreachable, the request is not admitted.

How a change reaches the edge

  1. A REST write (dashboard or API) stores the new configuration in PostgreSQL.
  2. The control plane records a configuration snapshot for the affected scope.
  3. notify_config_changed pushes a fresh SiteConfig to every agent serving that site over the existing stream.
  4. The agent persists the bundle to its rule cache, the cache hash changes, and the provider is swapped in memory.

Failure behaviour

fail_open decides what an agent does when the control plane cannot be reached:

  • true (default) — keep serving with the last synchronised policy.
  • false — return 503 for hosts that have no synchronised rules.

The site-level failover policy takes precedence over the global default, and the choice is recorded in configuration so it survives restarts.

Run modes

ModeCommandRole
Control planepingwaf serverAPI, gRPC, dashboard, database. Does not proxy traffic
Data planepingwaf agentConnects to a remote control plane and proxies on :80/:443
All-in-onepingwaf all-in-oneBoth in one process; the agent talks to the local server over loopback

PINGWAF_MODE selects the same mode when the subcommand is omitted.

Configuration versions

Every change is versioned per scope. Retention keeps at most 50 versions per scope, 30 days of history, and snapshots up to 2 MiB. A rollback takes an advisory lock, writes a pre-rollback snapshot, restores the previous state, then records the result as a new version — so a rollback is itself auditable and reversible.

Rollbacks can be triggered from the console or with pingwaf config rollback <id> --yes. The CLI path records the version but does not push to agents immediately; the next configuration change or agent reconnect applies it.