|
| 1 | +--- |
| 2 | +title: "WireGuard Integration" |
| 3 | +description: "Secure node bootstrapping with WireGuard tunnels for cloud-init traffic." |
| 4 | +date: 2026-06-29T00:00:00+00:00 |
| 5 | +lastmod: 2026-06-29T00:00:00+00:00 |
| 6 | +draft: false |
| 7 | +weight: 500 |
| 8 | +toc: true |
| 9 | +categories: ["Cloud-Init", "WireGuard", "Security"] |
| 10 | +tags: ["Cloud-Init", "WireGuard", "VPN", "Security", "Networking"] |
| 11 | +--- |
| 12 | + |
| 13 | +## Overview |
| 14 | + |
| 15 | +OpenCHAMI's cloud-init server supports **WireGuard** to encrypt and isolate cloud-init traffic between compute nodes and the cloud-init server. This ensures that sensitive metadata, user data, and vendor data exchanged during node provisioning are transmitted over an encrypted tunnel. |
| 16 | + |
| 17 | +The cloud-init server embeds a WireGuard server that dynamically assigns VPN IPs to compute nodes, manages peer configurations, and can optionally restrict cloud-init data access to only those nodes connected via the WireGuard tunnel. |
| 18 | + |
| 19 | +## Architecture |
| 20 | + |
| 21 | +The WireGuard integration works in two phases: |
| 22 | + |
| 23 | +1. **Tunnel establishment** — At boot, the compute node calls the unprotected `/cloud-init/wg-init` endpoint to request a WireGuard IP and peer configuration. This happens before cloud-init itself runs. |
| 24 | +2. **Secure data fetch** — Once the tunnel is up, cloud-init fetches `/meta-data`, `/user-data`, `/vendor-data`, and group YAML files over the encrypted WireGuard link. |
| 25 | + |
| 26 | +```text |
| 27 | +┌──────────────┐ WireGuard Tunnel ┌──────────────────┐ |
| 28 | +│ Compute │ ◄────────────────────────► │ cloud-init │ |
| 29 | +│ Node │ wg0: 100.97.0.x/32 │ Server │ |
| 30 | +│ │ │ wg0: 100.97.0.1 │ |
| 31 | +└──────────────┘ └──────────────────┘ |
| 32 | + │ │ |
| 33 | + │ 1. POST /cloud-init/wg-init │ |
| 34 | + │ (before cloud-init starts) │ |
| 35 | + │ │ |
| 36 | + │ 2. GET /meta-data, /vendor-data │ |
| 37 | + │ (over WireGuard tunnel) │ |
| 38 | + └───────────────────────────────────────────┘ |
| 39 | +``` |
| 40 | + |
| 41 | +## Server-Side Configuration |
| 42 | + |
| 43 | +### Environment Variables |
| 44 | + |
| 45 | +| Variable | Description | Default | |
| 46 | +| --- | --- | --- | |
| 47 | +| `WIREGUARD_SERVER` | WireGuard server IP and network CIDR (e.g. `100.97.0.1/16`) | (none) | |
| 48 | +| `WIREGUARD_ONLY` | Only serve cloud-init data to clients in the WireGuard subnet | `false` | |
| 49 | +| `DEBUG` | Enable debug logging | `false` | |
| 50 | + |
| 51 | +### WIREGUARD_ONLY Behavior |
| 52 | + |
| 53 | +When `WIREGUARD_ONLY=true` is set, the cloud-init server restricts access to data-bearing endpoints (`/meta-data`, `/user-data`, `/vendor-data`, `/{group}.yaml`) to clients whose IP falls within the WireGuard subnet **or** whose request arrives on the server's WireGuard interface. |
| 54 | + |
| 55 | +{{< callout context="note" title="Important" icon="info-circle" >}} |
| 56 | +The `/cloud-init/wg-init` and `/cloud-init/phone-home/{id}` endpoints are **never** restricted by the WireGuard middleware, even when `WIREGUARD_ONLY=true`. This allows nodes to establish their WireGuard tunnel before fetching cloud-init data. |
| 57 | +{{< /callout >}} |
| 58 | + |
| 59 | +### Network Setup |
| 60 | + |
| 61 | +The cloud-init container needs access to both the internal cluster network and an external network for WireGuard traffic. Configure this using Podman quadlet overrides. |
| 62 | + |
| 63 | +{{< details "Example quadlet override (10-override.conf)" >}} |
| 64 | + |
| 65 | +```ini |
| 66 | +# /etc/containers/systemd/cloud-init-server.container.d/10-override.conf |
| 67 | +[Container] |
| 68 | +Image=ghcr.io/openchami/cloud-init:v1.4.2 |
| 69 | +Environment=WIREGUARD_SERVER=172.16.1.1/24 |
| 70 | +Environment=WIREGUARD_ONLY=true |
| 71 | +Environment=DEBUG=true |
| 72 | +AddCapability=NET_ADMIN |
| 73 | +AddDevice=/dev/net/tun |
| 74 | +PublishPort=58036:58036/udp |
| 75 | +Network=openchami-internal.network:alias=cloud-init-int |
| 76 | +Network=openchami-external.network:alias=cloud-init-wg |
| 77 | +``` |
| 78 | + |
| 79 | +{{< /details >}} |
| 80 | + |
| 81 | +### HAProxy Configuration |
| 82 | + |
| 83 | +If using HAProxy in front of the cloud-init server, ensure the backend routes traffic to the internal network alias: |
| 84 | + |
| 85 | +```haproxy |
| 86 | +backend cloud-init |
| 87 | + server cloud-init-server cloud-init-int:27777 |
| 88 | +``` |
| 89 | + |
| 90 | +## Client-Side Configuration |
| 91 | + |
| 92 | +Compute nodes require two pieces of setup to use WireGuard for cloud-init: |
| 93 | + |
| 94 | +1. A **systemd override** that runs a pre-exec script before cloud-init starts. |
| 95 | +2. A **shell script** that configures the WireGuard tunnel and requests a VPN IP from the server. |
| 96 | + |
| 97 | +### Systemd Override |
| 98 | + |
| 99 | +```ini |
| 100 | +# /etc/systemd/system/cloud-init.service.d/override.conf |
| 101 | +[Service] |
| 102 | +Environment=ochami_wg_ip=172.16.0.254 |
| 103 | +ExecStartPre=/usr/local/bin/ochami-ci-setup.sh |
| 104 | +ExecStopPost=/bin/bash -c "ip link delete wg0" |
| 105 | +``` |
| 106 | + |
| 107 | +The `ochami_wg_ip` variable should point to the cloud-init server's external IP address (the one accessible from the compute node before the tunnel is established). |
| 108 | + |
| 109 | +### WireGuard Setup Script |
| 110 | + |
| 111 | +The pre-exec script performs the following steps: |
| 112 | + |
| 113 | +1. Generate a WireGuard key pair on the compute node. |
| 114 | +2. Request a VPN IP from the cloud-init server via the `/cloud-init/wg-init` endpoint. |
| 115 | +3. Create and configure the `wg0` interface with the assigned IP and server peer. |
| 116 | +4. Add a route to the WireGuard server IP over the tunnel. |
| 117 | + |
| 118 | +{{< details "Example ochami-ci-setup.sh" >}} |
| 119 | + |
| 120 | +```bash |
| 121 | +#!/bin/sh |
| 122 | +set -e -o pipefail |
| 123 | + |
| 124 | +if [ -z "${ochami_wg_ip}" ]; then |
| 125 | + echo "ERROR: Failed to find the 'ochami_wg_ip' environment variable." |
| 126 | + if [ -f "/etc/cloud/cloud.cfg.d/ochami.cfg" ]; then |
| 127 | + echo "Removing ochami-specific cloud-config; cloud-init will use other defaults" |
| 128 | + rm /etc/cloud/cloud.cfg.d/ochami.cfg |
| 129 | + else |
| 130 | + echo "Not writing ochami-specific cloud-config; cloud-init will use other defaults" |
| 131 | + fi |
| 132 | + exit 0 |
| 133 | +fi |
| 134 | + |
| 135 | +echo "Loading WireGuard kernel module" |
| 136 | +modprobe wireguard |
| 137 | + |
| 138 | +echo "Generating WireGuard keys" |
| 139 | +wg genkey | tee /etc/wireguard/private.key | wg pubkey > /etc/wireguard/public.key |
| 140 | + |
| 141 | +echo "Requesting WireGuard tunnel configuration" |
| 142 | +PUBLIC_KEY=$(cat /etc/wireguard/public.key) |
| 143 | +PAYLOAD="{ \"public_key\": \"${PUBLIC_KEY}\" }" |
| 144 | +WG_PAYLOAD=$(curl -s -X POST -d "${PAYLOAD}" http://${ochami_wg_ip}:8081/cloud-init/wg-init) |
| 145 | + |
| 146 | +CLIENT_IP=$(echo $WG_PAYLOAD | jq -r '."client-vpn-ip"') |
| 147 | +SERVER_IP=$(echo $WG_PAYLOAD | jq -r '."server-ip"' | awk -F'/' '{print $1}') |
| 148 | +SERVER_PORT=$(echo $WG_PAYLOAD | jq -r '."server-port"') |
| 149 | +SERVER_KEY=$(echo $WG_PAYLOAD | jq -r '."server-public-key"') |
| 150 | + |
| 151 | +echo "Setting up WireGuard interface" |
| 152 | +ip link add dev wg0 type wireguard |
| 153 | +ip address add dev wg0 ${CLIENT_IP}/32 |
| 154 | +wg set wg0 private-key /etc/wireguard/private.key |
| 155 | +ip link set wg0 up |
| 156 | +wg set wg0 peer ${SERVER_KEY} allowed-ips ${SERVER_IP}/32 endpoint ${ochami_wg_ip}:${SERVER_PORT} |
| 157 | + |
| 158 | +echo "Adding route to WireGuard server" |
| 159 | +ip route replace "${SERVER_IP}/32" dev wg0 |
| 160 | + |
| 161 | +rm /etc/wireguard/private.key |
| 162 | +rm /etc/wireguard/public.key |
| 163 | +``` |
| 164 | + |
| 165 | +{{< /details >}} |
| 166 | + |
| 167 | +### Image Building |
| 168 | + |
| 169 | +Inject these files into the compute node image using the OpenCHAMI image builder. Add them under `copyfiles` in the image build configuration: |
| 170 | + |
| 171 | +```yaml |
| 172 | +copyfiles: |
| 173 | + - src: '/opt/workdir/images/files/cloud-init-override.conf' |
| 174 | + dest: '/etc/systemd/system/cloud-init.service.d/override.conf' |
| 175 | + - src: '/opt/workdir/images/files/ochami-ci-setup.sh' |
| 176 | + dest: '/usr/local/bin/ochami-ci-setup.sh' |
| 177 | +``` |
| 178 | +
|
| 179 | +The image must also include the `wireguard-tools` package: |
| 180 | + |
| 181 | +```yaml |
| 182 | +packages: |
| 183 | + - wireguard-tools |
| 184 | +``` |
| 185 | + |
| 186 | +### Boot Parameters |
| 187 | + |
| 188 | +Configure the node's boot parameters in BSS to point to the cloud-init server using the WireGuard subnet address: |
| 189 | + |
| 190 | +```yaml |
| 191 | +params: 'nomodeset ro root=live:http://172.16.0.254:7070/boot-images/... ip=dhcp ... cloud-init=enabled ds=nocloud-net;s=http://172.16.1.1:27777/cloud-init' |
| 192 | +``` |
| 193 | + |
| 194 | +The `ds=nocloud-net` URL should use the WireGuard server IP (e.g. `172.16.1.1:27777`), which is the address the compute node will reach once the tunnel is established. |
| 195 | + |
| 196 | +## How the Middleware Works |
| 197 | + |
| 198 | +The WireGuard middleware enforces access policy based on two criteria: |
| 199 | + |
| 200 | +- **Client IP in WireGuard subnet** — If the connecting client's IP falls within the configured WireGuard CIDR (e.g. `100.97.0.0/16`), the request is allowed. |
| 201 | +- **Request arrived on WireGuard interface** — If the connection was received on the server's `wg0` interface, the request is allowed regardless of client IP. |
| 202 | + |
| 203 | +Before the fix in PR [#113](https://github.com/OpenCHAMI/cloud-init/pull/113), the WireGuard middleware was applied globally to **all** routes when `WIREGUARD_ONLY=true`. This created a chicken-and-egg problem: compute nodes needed to call `/cloud-init/wg-init` to establish their WireGuard tunnel, but that endpoint was blocked by the middleware. |
| 204 | + |
| 205 | +The fix scoped the middleware to only the data-bearing routes (`/meta-data`, `/user-data`, `/vendor-data`, `/{group}.yaml`), leaving the tunnel setup endpoint (`/wg-init`) and phone-home endpoint (`/phone-home/{id}`) accessible without restriction. |
| 206 | + |
| 207 | +## Verification |
| 208 | + |
| 209 | +To verify the WireGuard tunnel is functioning correctly from a compute node: |
| 210 | + |
| 211 | +```bash |
| 212 | +# Check the WireGuard interface |
| 213 | +wg show |
| 214 | +
|
| 215 | +# Verify the tunnel is resolving the cloud-init server |
| 216 | +ping 100.97.0.1 |
| 217 | +``` |
| 218 | + |
| 219 | +On the cloud-init server, check that the WireGuard interface is active: |
| 220 | + |
| 221 | +```bash |
| 222 | +# List WireGuard peers |
| 223 | +wg show wg0 |
| 224 | +
|
| 225 | +# Check debug logs for connection metadata |
| 226 | +journalctl -u cloud-init-server |
| 227 | +``` |
| 228 | + |
| 229 | +If `WIREGUARD_ONLY=true` is set and a non-WireGuard client tries to access a protected endpoint, they will receive: |
| 230 | + |
| 231 | +```text |
| 232 | +Access denied: IP 10.89.2.63 not in WireGuard subnet or interface |
| 233 | +``` |
0 commit comments