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 asea), cross-compiled for OpenBSD amd64 and arm64. It runs as theedge_agentrc.d service. The subcommands areserve(daemon),setup(enrol),reenroll,statusandshow. - Identity lives in
identity.jsonin the agent data directory:gateway_id,api_token, the nexus address and the realtime address. - Talks to nexus with Connect RPC on
GatewayDeviceService, usingAuthorization: Bearer <api_token>. The calls areGetDesiredState,GetConfig,RequestRelay,RequestCertificate,UploadLogs,ReportCommandReplyandWhoAmI. - 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 asconfirm_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 containernexus-<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.Pushon the realtime Worker (X-Internal-Key). An offline device returnsoffline, and the change waits for the device's nextGetConfig/GetDesiredState. - When
NEXUS_RELAY_ZONE_SUFFIXis 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 andlocalhost:5173.
Authz (platform/authz)¶
- A Go Connect RPC service (
authz.v1.AuthzService:IsAllowed,AssignRole,RevokeRole,SeedRoles,ListRoles,ListUserPermissions,ListRoleHolders), run as the containerauthz-<env>. - Owns
acl(subject_id, role_id, target, target_id),rolesandrole_actionsin 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 inwrangler.tomlcreate its DNS. /gateway/<id>upgrades a WebSocket in one of three roles:gateway: token checked withValidateGatewayToken.viewer: read-only, single-use ticket checked withValidateViewerTicket.operator: root console, ticket checked withValidateShellTicket, frames relayed both ways.
- Gateway frames are broadcast to viewers and dispatched to nexus as typed
GatewayInternalServicecalls (Heartbeat,StateReport,ConfigReport,ReportAgentUpdate,UploadInternalLogs,DeviceOffline, …). /gateway.v1.RealtimePushService/Pushis nexus's entry point, checked against theINTERNAL_API_KEYWorker secret.
Portal (platform/portal)¶
- SvelteKit 2 + Svelte 5, built with
@sveltejs/adapter-cloudflareand deployed to the Pages projectdigilan-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_URLandVITE_ENV.
Relays (edge/relay)¶
relaydon a Debian Scaleway instance (DEV1-Sby default). Terraform creates it, and it installs itself through cloud-init from the GitLab package registry, pinned to one version throughrelayd_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 byscripts/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 inenvironments/shared. Each environment gets its own database on it:digilan-dev,digilan-stganddigilan(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) andmrwifi-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.