Skip to content

Config Lifecycle

Storage

A gateway's configuration is one JSON document. Every save inserts a new row in gateway_configs. Rows are never edited in place, apart from their status:

gateway_configs (
    id           BIGSERIAL PRIMARY KEY,
    device_id    VARCHAR(64) NOT NULL REFERENCES devices(id),
    version      BIGINT NOT NULL,
    config_data  JSONB NOT NULL,
    config_hash  VARCHAR(64) NOT NULL,       -- sha256 of config_data
    created_by   VARCHAR(128),               -- Zitadel sub
    created_at   TIMESTAMPTZ NOT NULL DEFAULT now(),
    status       VARCHAR(16) NOT NULL,
    applied_at   TIMESTAMPTZ,
    comment      VARCHAR,
    UNIQUE (device_id, version)
)
  • devices.desired_config_version is the version the platform wants on the device.
  • devices.config_version is the version that was last confirmed.

SaveGatewayConfig refuses a config in any of these cases:

  • it is oversized
  • it still contains template placeholders
  • it has an unusable FQDN alias
  • the caller's base_version is stale, meaning someone else saved in between

Status transitions

stateDiagram-v2
    [*] --> draft : SaveGatewayConfig
    draft --> pending : ApplyGatewayConfig
    failed --> pending : ApplyGatewayConfig
    pending --> awaiting_confirm : agent reports applied
    pending --> failed : agent reports an error
    awaiting_confirm --> applied : ConfirmGatewayConfig
    awaiting_confirm --> rolled_back : no confirm within 60 s
Status Meaning
draft Saved on nexus, not pushed.
pending desired_config_version is set and the apply_config push was sent. It stays pending while the device is offline, and the device picks it up on its next GetConfig.
awaiting_confirm The agent applied it and armed its rollback guard.
applied Confirmed. applied_at is set and devices.config_version is moved to this version.
failed The agent could not apply it. It can be applied again.
rolled_back No confirmation reached the agent in time, so it restored the previous state.

ConfirmGatewayConfig refuses a version whose status is failed or rolled_back, because that version is not running on the device.

The sequence across portal, nexus, realtime and agent is drawn in How It Works.

Confirmation

The portal starts a 60 s countdown when the push succeeds. The operator confirms explicitly; the portal never confirms on its own. Confirmation sends a confirm_config command to the agent.

The push can be lost, for example when the new config cut the socket it would arrive on. To cover that, the agent polls GetDesiredState while its guard is armed. A config_status of applied counts as a confirmation.

Do not bypass the rollback

The rollback guard is what keeps a bad rule from locking the operator out of a remote box. Do not disable it, and do not make confirmation automatic.

Rollback on the gateway

The guard is edge/gateway/internal/rollback, created with a 60 s timeout in cmd/agent/serve.go.

  1. Before applying, it snapshots the connectivity-critical state to disk (mode 0600). That covers pf.conf, /etc/mygate, the IPv4 default route, the generated iked.conf, dhcpd.conf and unbound.conf, and the list of virtual interfaces. If the snapshot cannot be written, the config is not applied.
  2. It starts the timer.
  3. On confirm_config for that version, or on a confirmed status seen by polling, it stops the timer and drops the snapshot.
  4. On timeout, it restores the snapshot and removes any virtual interface the change created. It re-syncs static routes from the restored config, then sends a config_report with status: rolled_back.

The snapshot carries a deadline, so an agent that dies inside the window finishes the rollback when it comes back (internal/rollback/resume.go).

Rolling back from the portal

RollbackConfig(version) changes nothing on the device. It copies an older version's config_data into a new draft with the next version number, which the operator then applies and confirms as usual. History stays append-only.

Payload shape

The agent decodes the blob into DeviceConfig (edge/gateway/internal/config/device.go). Top-level keys:

Key Contents
hostname Device hostname
interfaces Physical, VLAN, bridge, trunk, PPPoE, WireGuard, IPsec, cellular; addressing and per-interface DHCP
firewall rules (structured, or raw pf for system rules)
nat rules, see below
aliases host, network, port, group, fqdn
routes Static routes (destination, gateway, optional interface)
services, schedules Objects referenced by rules
dns_filter, detection, syslog, flow_export Filtering, Suricata, log forwarding, flow export
dynamic_routing OSPF
ip_forwarding Absent means on
published_services name, scheme, address, port, require_code, label, disabled

NAT rules

There is one rule type. Source and destination translation are each optional, and the pf construct depends on which ones are present (internal/pf/generate.go):

{
  "id": "nat-…", "enabled": true, "name": "…", "description": "…", "log": false,
  "match": {
    "in_interface": "",        // set for destination NAT (match in on …)
    "out_interface": "egress", // set for source NAT (match out on …)
    "protocol": "tcp",
    "source": "lan_net", "source_port": "",
    "destination": "", "destination_port": ""
  },
  "translated_source":      { "address": "egress", "port": "", "randomize_port": false,
                              "method": "", "sticky": false },   // nat-to
  "translated_destination": { "address": "10.0.1.50", "port": "8443",
                              "method": "", "sticky": false },   // rdr-to
  "bidirectional": false                                         // binat-to
}
Rule shape pf
translated_source only match out on <out_interface> … nat-to <addr> [port] [method] [sticky-address]
translated_destination only match in on <in_interface> … rdr-to <addr> [port] [method] [sticky-address]
bidirectional match on <interface> from <internal> binat-to <addr>
  • address: "egress" means the interface's own address.
  • method is one of round-robin, random, source-hash or bitmask.
  • A translated address that is not on the interface is not added as an alias automatically. The operator adds the alias.