Skip to content

Components

What each component is, what it owns, and how it authenticates. For the diagrams, see How It Works.

Edge agent (edge/gateway)

  • One Go binary, edge-agent (symlinked as ea), cross-compiled for OpenBSD amd64 and arm64. It runs as the edge_agent rc.d service. The subcommands are serve (daemon), setup (enrol), reenroll, status and show.
  • Identity lives in identity.json in the agent data directory: gateway_id, api_token, the nexus address and the realtime address.
  • Talks to nexus with Connect RPC on GatewayDeviceService, using Authorization: Bearer <api_token>. The calls are GetDesiredState, GetConfig, RequestRelay, RequestCertificate, UploadLogs, ReportCommandReply and WhoAmI.
  • Holds one WebSocket to realtime at wss://<realtime>/gateway/<id>?token=<api_token>. Heartbeats, state reports, config reports, log batches, agent-update phases and command results go up this socket. Pushes come down it: apply_config, and commands such as confirm_config, update_agent, restart_agent, reboot, set_hostname, ping/traceroute, plus the shell frames.
  • Renders pf.conf, hostname.if, and the DHCP, DNS, IPsec and WireGuard files from the JSON config. Each apply is protected by a 60 s rollback guard (Config Lifecycle).
  • Keeps a relay link: RequestRelay, then WireGuard over a three-rung ladder (Relay Link). Serves published services through an in-agent TLS proxy with an access-code gate (Published Services).
  • Self-updates from the OTA bucket through a presigned URL. An out-of-process watchdog restores the previous binary if the new one does not confirm (OTA).

Nexus (platform/nexus)

  • A Go HTTP/1.1 server. Every Connect RPC service is mounted on one ServeMux. It runs as the Scaleway serverless container nexus-<env> (port 8080, health /healthz) and scales to zero.
  • Runs its migrations against the environment database at start-up, and seeds the role table into authz (SeedRoles).
  • Services by caller:
Package Service Caller Auth
gateway.v1 GatewayEnrollmentService agent, first boot bootstrap token in the request
gateway.v1 GatewayDeviceService agent Bearer <api_token> (stored as SHA-256)
gateway.v1 GatewayInternalService realtime DO, terraform (RegisterRelayToken) X-Internal-Key
relay.v1 RelayEnrollmentService relayd, first boot bootstrap token registered by terraform
relay.v1 RelayService (Heartbeat, RequestCertificate, ReportAttach, ReportDetach) relayd Bearer <relay api_token>
portal.v1 PortalSessionService, PortalOrgService, PortalGatewayService, PortalAdminService, PortalUsersService portal Zitadel JWT (issuer + audience = project id), then a per-handler authz check
portal.v1 ReleasePublisherService edge/gateway CI X-Release-Key
  • Pushes to a device with one call, RealtimePushService.Push on the realtime Worker (X-Internal-Key). An offline device returns offline, and the change waits for the device's next GetConfig / GetDesiredState.
  • When NEXUS_RELAY_ZONE_SUFFIX is set, nexus also runs the relay allocator, writes the per-gateway DNS records in Cloudflare, and brokers Let's Encrypt certificates for gateways and relays (ACME dns-01). Gateways and relays never hold a DNS credential.
  • Presigns OTA downloads from the environment bucket with a read-only key.
  • Calls the Zitadel Management API with NEXUS_ZITADEL_SERVICE_TOKEN, to invite users and look them up.
  • Allows CORS for a fixed list of portal origins in internal/app/app.go: the three environment portals and localhost:5173.

Authz (platform/authz)

  • A Go Connect RPC service (authz.v1.AuthzService: IsAllowed, AssignRole, RevokeRole, SeedRoles, ListRoles, ListUserPermissions, ListRoleHolders), run as the container authz-<env>.
  • Owns acl(subject_id, role_id, target, target_id), roles and role_actions in the same database as nexus. Its tables do not overlap with nexus's.
  • Callers authenticate with Authorization: Bearer <AUTHZ_API_KEY>, a random key that terraform shares between nexus, authz and the Monsieur Wifi containers.
  • Nexus defines the roles and targets (portal:org, portal:device, portal:segment, portal:system). See Authz model.

Realtime (platform/realtime)

  • A Cloudflare Worker (digilan-realtime-dev, -stg, -production) with one SQLite-backed Durable Object, GatewayRoom, per device (idFromName(device_id)). It is deployed by wrangler from its own CI, not by terraform. The custom domains in wrangler.toml create its DNS.
  • /gateway/<id> upgrades a WebSocket in one of three roles:
    • gateway: token checked with ValidateGatewayToken.
    • viewer: read-only, single-use ticket checked with ValidateViewerTicket.
    • operator: root console, ticket checked with ValidateShellTicket, frames relayed both ways.
  • Gateway frames are broadcast to viewers and dispatched to nexus as typed GatewayInternalService calls (Heartbeat, StateReport, ConfigReport, ReportAgentUpdate, UploadInternalLogs, DeviceOffline, …).
  • /gateway.v1.RealtimePushService/Push is nexus's entry point, checked against the INTERNAL_API_KEY Worker secret.

Portal (platform/portal)

  • SvelteKit 2 + Svelte 5, built with @sveltejs/adapter-cloudflare and deployed to the Pages project digilan-portal-<env>.
  • Signs in with oidc-client-ts (authorization code + PKCE) and sends the access token on every Connect RPC call.
  • Environment values are baked in at build time: VITE_ZITADEL_ISSUER, VITE_ZITADEL_CLIENT_ID, VITE_ZITADEL_PROJECT_ID, VITE_NEXUS_URL, VITE_REALTIME_WS_URL, VITE_MRWIFI_URL, VITE_RADIUS_URL and VITE_ENV.

Relays (edge/relay)

  • relayd on a Debian Scaleway instance (DEV1-S by default). Terraform creates it, and it installs itself through cloud-init from the GitLab package registry, pinned to one version through relayd_package_url. A relay is replaced, never updated in place.
  • Listens on TCP 443 for SNI forwarding and rung-3 WebSocket tunnels, and on UDP 51820 and UDP 443 for WireGuard (nftables redirects UDP 443 to 51820).
  • Enrols once with a bootstrap token. It then heartbeats every 30 s and receives the whole set of peers and routes on each heartbeat. See Relay Architecture.

Zitadel

  • One instance at auth.digilan-edge.com, shared by all environments. It runs as a Scaleway serverless container with its own RDB instance.
  • Each environment has its own Zitadel project, digilan-<env>, and a portal app in it, both created by scripts/zitadel-apps.sh. Nexus checks that the token audience equals that project id.
  • Self-registration and external-IdP registration are off. A second factor (TOTP, WebAuthn or passkey) is available but not required on the live instance.

Postgres

  • One RDB instance, digilan-shared (PostgreSQL 17), created in environments/shared. Each environment gets its own database on it: digilan-dev, digilan-stg and digilan (prod) by default.
  • Nexus and authz share the environment database. Monsieur Wifi (mrwifi-<env>) and radius-ctrl (radius-<env>) get databases of their own on the same instance where they are deployed.

OTA storage

  • One private bucket per environment, digilan-agent-releases-<env>. Nexus holds a read-only key (digilan-ota-reader-<env>) and CI holds a write-only key (digilan-ota-publisher-<env>).
  • The ed25519 signing key is shared by every environment (environments/shared/ota-signing.tf), so a gateway moved between environments still trusts the releases.

radius-ctrl and Monsieur Wifi

Both run on dev only. The stg and prod terraform roots do not pass their image variables.

  • radius-ctrl has three parts: a .NET API (radius-api-<env>), a FreeRADIUS VM (UDP 1812/1813) and a SPA on Pages (digilan-radius-<env>). It authenticates admins against the same Zitadel project. See Radius.
  • Monsieur Wifi has two parts: mrwifi-go-<env> (a Connect API that validates Zitadel tokens and calls authz) and mrwifi-php-<env> (the legacy Laravel app). See Monsieur Wifi.

Environments

Service dev stg prod
nexus dev.nexus.digilan-edge.com stg.nexus.digilan-edge.com prod.nexus.digilan-edge.com
authz dev.authz.digilan-edge.com stg.authz.digilan-edge.com authz.digilan-edge.com
realtime dev.realtime.digilan-edge.com stg.realtime.digilan-edge.com realtime.digilan-edge.com
portal dev.portal.digilan-edge.com stg.portal.digilan-edge.com portal.digilan-edge.com
relays relay1..3.digilan-edge.com none prod-relay1..2.digilan-edge.com
per-gateway names <gw>.r.digilan-edge.com none under prod's relay_zone_suffix
agent releases published by CI not published published by CI
radius SPA dev.radius.digilan-edge.com none none

The defaults are in infra/terraform/environments/<env>/variables.tf. Shared by every environment: the container registry (rg.fr-par.scw.cloud/digilan), Zitadel, the shared RDB instance, transactional email, and the OTA signing key.