One public front terminates TLS and proxies over wg0 to origin nodes that expose no port at all. This is the exact order that brought the first pair live, including the two traps met on the way.
The model — one public front, private origins
Two kinds of sklet nodes share one private network and one WireGuard mesh
(ADR-006):
Node
Public ports
Holds
Runs
front
80 / 443, control API, the origins' control API relay
DNS records, ACME state, the mesh peer registry
meshauthority + the gateway
origin
none: no Floating IP at all (PUBLIC_IP=false)
the application and its data
the workload spaces
The front terminates TLS and proxies every public host to a service on an
origin over wg0, using routes of the form <space>/<service>. It stores no
application data and no application secret, so a compromised or saturated
front is destroyed and rebuilt, and the origin was never reachable from the
internet in the first place.
meshauthority lives on the front because an authority that hands out mesh
addresses must not depend on a machine it addresses. It is the control plane
only: once peers are configured, WireGuard carries traffic peer-to-peer.
The front is also the only public address of the whole mesh (ADR-019). An
origin has no Floating IP: it leaves through the shared network's router for
image pulls and telemetry, and is reached, for deploys, image watches, logs
and exec, through the front's control API relay. The front peeks the TLS
server name of every connection on its control port; <node-id>.nodes.front.<zone>
is spliced, still encrypted, to that peer's control API on its private
address. TLS is never terminated there, so the origin's pinned certificate
and token stay end to end.
This page is the exact path that put the first pair live on OVH on
2026-09-11: mesh-front-v1 (front) and sklet-auth-v1 (origin for the
identity provider), driven from infra/instance/.
0. The network comes first
The private network is its own stack, mesh-net-v1: network, subnet and the
router whose external gateway SNATs every Floating-IP-less node's egress. No
node owns it, so the front stays disposable even with prod attached:
cd infra/instancepnpm exec cdktf deploy mesh-net-v1 --auto-approve # env/mesh-net-v1.env: PRIVATE_CIDR=10.73.0.0/24
Every node below joins it with SHARED_NETWORK_NAME=mesh-net-v1-net and
SHARED_SUBNET_NAME=10.73.0.0/24.
1. Declare the two stacks
Each machine is a SklpHostStack id in infra/instance/main.ts, opted in by
the presence of env/<id>.env. The committed .env.example files document
every variable; the real .env is gitignored.
Front (env/mesh-front-v1.env):
PUBLIC_HTTP=trueSHARED_NETWORK_NAME=mesh-net-v1-netSHARED_SUBNET_NAME=10.73.0.0/24PRIVATE_CIDR=10.73.0.0/24PRIVATE_IP=10.73.0.10FLAVOR_NAME=d2-2DATA_VOLUME_GB=10 # ACME state + meshauthority's peer registryDNS_SUBDOMAIN=frontMESH_AUTHORITY_URL=https://10.73.0.10:4300 # itself; pin its certificate fingerprint on every node (see The mesh)MESH_TOKEN=<openssl rand -hex 32>MESH_PEER_CIDR=10.73.0.0/24CORE_CIDR=<skalpai core egress ip>/32NODE_RELAY=true # *.nodes.front.<zone> -> this Floating IP, agent relays
Origin (env/sklet-auth-v1.env): joins the front's network instead of
creating one, same token, no public HTTP.
PUBLIC_HTTP=falsePUBLIC_IP=false # no Floating IP; subnet-only security groupADVERTISE_VIA=nodes.front.<zone> # the front's node_relay_domain outputSHARED_NETWORK_NAME=mesh-net-v1-netSHARED_SUBNET_NAME=10.73.0.0/24PRIVATE_CIDR=10.73.0.0/24PRIVATE_IP=10.73.0.20FLAVOR_NAME=d2-2DATA_VOLUME_GB=20DNS_SUBDOMAIN=auth # private name only; app domains live on the frontMESH_AUTHORITY_URL=https://10.73.0.10:4300MESH_TOKEN=<same as the front>MESH_PEER_CIDR=10.73.0.0/24
The mesh only routes between peers on the same OpenStack network. Two stacks
each on their own dedicated network cannot reach one another over their
private IP at all, whatever the security group says.
2. Provision the front first
cd infra/instancepnpm exec cdktf synthpnpm exec cdktf deploy mesh-front-v1 --auto-approve
cloud-init installs sklet from the published dist, applies
cap_net_bind_service,cap_net_admin as root, and starts
sklet-agent.service in the user's lingered session. Wait for the control
API, then register a deploy context without ever printing the token:
until curl -sk -m 5 https://<front-ip>:6464/ -o /dev/null; do sleep 15; donesklp deploy login \ --server https://<front-ip>:6464 \ --token "$(ssh -i ssh-keys/mesh-front-v1-key architect@<front-ip> cat ~/.config/sklp/control/token)" \ --ca-fingerprint <fingerprint printed in the agent journal> \ --context mesh-front --space meshauthority
At this point the agent journal shows mesh join failed (continuing without it): expected, the authority is not running yet.
3. Deploy meshauthority on the front
The manifest is .sklp/meshauthority/deploy.yaml. Its two variables have no
project_id, so the host environment resolves them:
The manifest binds ports: ["0.0.0.0:4300:4300"]. The bind address is
honoured since #1198; before that every host port silently bound loopback and
peers got connection refused. The security group already limits 4300 to
MESH_PEER_CIDR.
4. Bring the front into its own mesh
The agent joins the mesh at boot, before any space is launched. On the node
that hosts meshauthority that first join can therefore never succeed. Since
#1200 the reconciler persists the peer list on every successful registration
(~/.config/sklp/mesh/peers.json), and the next restart brings wg0 up from
that cache. So, once /health answers:
ssh architect@<front-ip> 'systemctl --user restart sklet-agent.service; sleep 20; ip -br addr show wg0'# wg0 UNKNOWN 10.90.0.2/24
If the node runs a sklet older than #1200, seed the cache by hand from the
registry (GET /v1/peers with the bearer token) before the restart.
A reconfigured ports: mapping does not rebind a host port the agent already
opened; a change of bind address needs an agent restart (issue 1fe1bd38).
Boot order matters. The agent joins the mesh once, at startup. A node that
boots while meshauthority is not yet answering, the front itself and any
origin provisioned before this step, logs mesh join failed (continuing without it); its reconciler then registers on its own cadence and writes
peers.json, but wg0 only comes up at the next agent restart. Deploy the
front and meshauthority first, then the origins, and restart the agent once
on every node that booted too early:
systemctl --user restart sklet-agent.service && sleep 20 && ip -br addr show wg0
An empty wg0 on a fresh origin is this, not a network fault (issue 911201b4).
A dual-homed node misses its first join for a different reason. When the
mesh arrives on a second NIC — a node whose primary NIC is on another network,
like the cluster's — meshauthority answers fine, but the interface itself is
not routable yet: OpenStack attaches it after the instance is up and netplan
applies it around the moment the agent starts. The join leaves too early and
fails with no route to host, which reads like a firewall problem and is not
one. Same remedy, one restart, and the same rule applies — the agent joins
once, so nothing recovers on its own:
systemctl --user restart sklet-agent.service && sleep 20 && ip -br addr show wg0
Expect this on every dual-homed node you provision, not occasionally: it has
reproduced on every one so far (issue b50be31e). The node looks healthy in the
meantime — it logs node ready and serves its control API — while the front
resolves it and then times out relaying to it.
5. Provision the origin
The origin has no public address, so every command reaches it through the
front: SSH jumps over it, the control API goes through its relay.
pnpm exec cdktf deploy sklet-auth-v1 --auto-approvealias ossh='ssh -i ssh-keys/sklet-auth-v1-key -J architect@<front-ip> architect@10.73.0.20'until ossh 'curl -sk -m 5 https://127.0.0.1:6464/ -o /dev/null'; do sleep 15; doneossh 'ip -br addr show wg0; cat ~/.config/sklp/mesh/node-id; curl -s ifconfig.me'# wg0 UNKNOWN 10.90.0.3/24 — joined at first boot, the authority was up# <node-id># <the router's SNAT address> — egress works without a Floating IPsklp deploy login --server https://<node-id>.nodes.front.<zone>:6464 \ --token "$(ossh cat ~/.config/sklp/control/token)" \ --ca-fingerprint <fingerprint> --context sklet-auth --space <app space>
The login URL is the one the agent wrote itself into
~/.config/sklp/node-config.yaml from --advertise-via. Register the node in
the Skalpai app with that same server_url: core, image watches and the CI
spawner dial it and land on the origin through the front, certificate pinned.
If the last line printed nothing, the router does not SNAT ports without a
Floating IP on this cloud; check openstack router show <front>-router for
enable_snat.
Check the tunnel both ways: ping 10.90.0.3 from the front, ping 10.90.0.2
from the origin.
6. Route a public host to the origin
Routes are declared in the front's own manifest. A deploy file must declare
at least one service, so on the front they live next to meshauthority:
The front resolves urbangate-production to the origin's mesh IP through the
peer registry and sends Host: <service>.<space>.internal upstream. Point the
DNS record at the front's Floating IP, then re-apply the manifest to the
mesh-front context: traefik obtains the certificate and the route goes live.
7. Deploy the application on the origin
The origin is an ordinary sklet node: sklp deploy -context sklet-auth <stack>
by hand, or the git path (push → core → node) once the repository's
deploy-root.yaml names the node and the Skalpai project holds its secrets.
Register the node in the app with its server_url, token and
ca_fingerprint, exactly as for any other node.
Swapping sklet on a live node
The published installer is the update path; a manual swap drops the file
capabilities, and without them cross-space DNS and wg0 silently die:
Front env, cdktf deploy the front, sklp deploy login.
Deploy meshauthority (host env resolves its variables), check /health
on the private IP.
Restart the front's agent once: wg0 comes up from the peer cache.
Origin env on the front's network, cdktf deploy, it joins at first boot.
Public routes as <space>/<service> in the front's manifest, DNS to the
front, re-apply.
Deploy the workload on the origin by hand or through git reconcile.
Moving a public node behind the front
A node that already serves traffic on its own Floating IP (prod) joins the
model in place, without recreating the VM. Each step is reversible until the
last one; public traffic never stops.
Second NIC on the mesh network: MESH_NETWORK_NAME=mesh-net-v1-net,
MESH_PORT_IP=10.73.0.5, MESH_PEER_CIDR=10.73.0.0/24, then
cdktf deploy. The port is attached hot. On an already-booted node write
the netplan file by hand (DHCP, use-routes: false); a fresh node gets it
from cloud-init.
Mesh flags in the agent unit (--mesh-authority-url,
--mesh-endpoint <mesh ip>:51820, SKLP_MESH_TOKEN) and one agent
restart: wg0 comes up, the peer announces its spaces and services.
Routes on the front: declare every public host as
<space>/<service> in the front's manifest, apply, and compare each host
with curl --resolve <host>:443:<front ip> against the node itself before
touching DNS.
--advertise-via nodes.front.<zone> on the node, restart, then
PATCH /api/stacks/:id/gateway/nodes/:nodeId with the relay server_url
and the node's fingerprint. No delete: routes and deployment records stay.
DNS of every zone to the front's IP, then one front agent restart:
traefik only retries a failed ACME order on restart.
Resync image watches: an empty push on each repository the node
serves, so the watches take the relay address. Watches created by hand
with no repository stay on the old address.
PUBLIC_IP=false + PUBLIC_HTTP=false, cdktf deploy: the Floating
IP is detached (kept in the account when it was FLOATING_IP), 80/443
close, SSH goes through the front. Egress continues through the node's
network router, check it with curl ifconfig.me from the node.