The board boots from the common image, gets a DHCP lease, and a provisioning script runs before anyone has touched the device. Somewhere in that script it has to obtain a WireGuard identity that belongs to this board and no other, get that identity authorized, and write a profile that the gateway will accept. The image can carry the client software, a reviewed template and the gateway's public key. What it must never carry is one private key shared by the whole fleet, because that turns a single stolen board into a fleet-wide incident. The order below keeps the identity local and the authorization explicit.

What the image carries, and what it does not

Ship the package, the template with placeholders, the gateway's public key and the enrollment client. The template is a shape, not a secret: it should be readable by the provisioning user and contain no key material.

The private key is generated on the device at first boot and never leaves it. Only the public half travels, inside an enrollment request that is authorized by something other than the device's own claim about who it is. That distinction is what stops a stolen board from becoming a fleet-wide problem. It is also the part most first-boot scripts get wrong.

Step 1: generate the key locally

Run the generation in a protected directory after the underlying network connection is approved and working. The subshell keeps the umask change local to this operation, and set -C refuses to clobber an existing file:

(
    umask 077
    set -C
    wg genkey > /etc/wireguard/device.key &&
        wg pubkey < /etc/wireguard/device.key > /etc/wireguard/device.pub
)

The WireGuard quick start documents the key-generation relationship these two commands rely on. If the generation is interrupted, leave the device unenrolled and let the workflow reconcile the files. Do not delete an existing identity just to make the script run cleanly from the top.

Persist the pending key across retries. Regenerating an identity after every temporary network failure is how a fleet ends up with orphaned peers nobody can match to a device.

Step 2: authorize a device record

Assume the inventory calls this board device-042 and assigns it 10.90.0.42/32. The enrollment service has to verify that this requester is entitled to that record. A serial number typed into a form is not proof, because serial numbers are printed on the outside of the box.

A per-device bootstrap credential, an operator approval, or a hardware-backed attestation can establish the claim. Protect the request's integrity and the response's authenticity, and do not replace one shared VPN secret with one shared factory credential.

The registered record binds the device, its public key, its assigned address and its permitted management role. WireGuard itself defines no enrollment API, so that service is something you build and operate deliberately.

Step 3: write two matching configurations

The device needs a profile that routes the management prefixes into the tunnel, and the gateway needs a peer entry that only permits this device's own source address. With a gateway at 10.90.0.1 and a management network of 10.60.0.0/16, the device side looks like this:

[Interface]
PrivateKey = <device-042 private key>
Address = 10.90.0.42/32

[Peer]
PublicKey = <trusted gateway public key>
Endpoint = vpn.example.com:51820
AllowedIPs = 10.90.0.1/32, 10.60.0.0/16
PersistentKeepalive = 25

The bracketed values are placeholders, not keys. The keepalive matters when the board sits behind NAT and has to stay reachable; it is not required everywhere, and adding it by habit keeps a radio awake for no reason.

On the gateway, the peer entry is narrow by design:

[Peer]
PublicKey = <device-042 public key>
AllowedIPs = 10.90.0.42/32

Do not copy the device's destination list onto the gateway peer. On the device that list says what to route into the tunnel; on the gateway it says which source addresses this peer may use, and those are different statements.

Forwarding, firewall rules and the return path to the management network still have to permit the approved access. A registered peer is not permission for every application behind it.

Step 4: commit enrollment as a lifecycle state

Apply the gateway record and the device profile through whoever owns each configuration, then mark enrollment complete only when the intended identity and address are accepted. Retries should reconcile that same record rather than allocate a second peer or a second address.

Keep a recovery path for interrupted provisioning, including the case where the gateway accepted the key and the device never wrote its profile.

When a board is replaced or retired, remove its public key and its authorization together. A new key is a deliberate re-enrollment event, not a side effect of a reimage. Order of work: generate locally, authorize against inventory, write both sides, commit the state, then test one management request through the tunnel.

Artiglyn mynjee