Skip to content

Remote Access (WireGuard)

How a Raspberry Pi node and a developer workstation reach each other, and reach the cloud stack, without exposing anything to the public internet.

Why WireGuard: no third-party coordination service between a grower's node and their data, and no account tier to outgrow.

Topology

A hub-and-spoke network. One always-on VM acts as the hub; every other machine is a spoke that dials in to it. Spokes reach each other through the hub.

flowchart TB
    subgraph VPN["WireGuard network (private subnet)"]
        HUB["🛡️ Hub<br/>cloud VM"]
        PI["🍓 Raspberry Pi node"]
        DEV["💻 Developer workstation"]
    end
    PI <-->|encrypted UDP| HUB
    DEV <-->|encrypted UDP| HUB
    HUB --- CLOUD["Cloud stack<br/>API · auth · account · dashboard"]

Only the hub has a public address. It exposes the WireGuard port and the public landing site; everything else on it is bound to the VPN. The Pi needs no port forwarding, no static IP and no inbound firewall rule, which is what makes a node deployable on a domestic connection behind NAT.

What is reachable, and what is not

Surface Reachable from Notes
Landing page Public internet Static site, ports 80/443
Cloud API and admin dashboard VPN only Public routes are deliberately not served
Pi local dashboard and API VPN and the node's local network Never port-forwarded
SSH to the Pi VPN (or the local network) Key-based

Keep the node private

The node's dashboard and API are meant for a trusted local network or the VPN. Never port-forward them.

Enrolling a node

WireGuard has no auth key and no signup step. Enrolment is a one-time exchange of public keys, done deliberately on both ends.

On the Pi (Linux):

1. Install on the Pi

sudo apt update
sudo apt install -y wireguard

2. Generate the node's key pair

wg genkey | sudo tee /etc/wireguard/privatekey | wg pubkey | sudo tee /etc/wireguard/publickey
sudo chmod 600 /etc/wireguard/privatekey

The private key never leaves the Pi. Send the public key to whoever administers the hub.

3. Write the node's config

/etc/wireguard/wg0.conf:

[Interface]
PrivateKey = <this node's private key>
Address    = <the address the hub assigned you>/24

[Peer]
PublicKey           = <the hub's public key>
Endpoint            = <hub host>:<hub port>
AllowedIPs          = <VPN subnet>/24
PersistentKeepalive = 25

PersistentKeepalive = 25 is not optional for a node

Without it, the node's NAT mapping expires as soon as it goes quiet, and the hub can no longer initiate a handshake inward. During a real outage this is the difference between being able to probe a node and being able only to observe that its last handshake is stale. Set it on every node.

4. Bring it up

sudo systemctl enable --now wg-quick@wg0
sudo wg show                     # confirm a recent handshake
ping -c3 <hub VPN address>

The hub side must add the node as a peer with the same public key and address before the handshake succeeds. That is an administrative step, not a self-service one, and it is intentional.

sudo wg show wg0 latest-handshakes    # seconds since each peer was last heard from
sudo wg show wg0 transfer             # bytes in/out per peer

A latest-handshake older than roughly 180 seconds means the peer is effectively offline. It is a quick manual check. The fleet's online/offline status in the cloud comes from the node's own sync requests, not from WireGuard.

A stale handshake is not the same as a dead node

A node that loses its uplink keeps collecting and buffering measurements locally, and flushes them when the link returns. Loss of connectivity costs latency, not data. See Architecture Overview.

Troubleshooting

Symptom Likely cause
No handshake at all Wrong public key on one side, or the hub's UDP port is not reachable
Handshake succeeds, no traffic passes AllowedIPs too narrow, or an address collision on the subnet
Works, then goes quiet after a few minutes Missing PersistentKeepalive on the node
Works on Wi-Fi, not after reboot wg-quick@wg0 not enabled, so the tunnel never comes up unattended
Node unreachable but its own logs look healthy Check the node's uplink first. The VPN cannot fix a link that is down beneath it