Pluggable VPN architecture
VPN/mesh management is optional. LAN-only deployments use vpn.provider: "null".
Interface
D interface VpnProvider (source/lindir/vpn/provider.d):
| Method | Purpose |
|---|---|
enrollHost(hostname, tags) |
Create a mesh auth / preauth key or WireGuard peer; return status |
syncGroupAcls(group → members) |
Merge IdM groups into control-plane ACL / policy / sidecar map |
removeHost(hostname) |
Delete matching device/node/peer from the mesh |
Backends
| Provider config | Implementation | Status |
|---|---|---|
null / none |
NullVpnProvider |
Done — no-op |
tailscale |
TailscaleVpnProvider |
Live API when apiKey + tailnet set; otherwise dry-run |
headscale |
HeadscaleVpnProvider |
Live API when apiBase + apiKey + tailnet (user) set; otherwise dry-run |
wireguard |
WireGuardVpnProvider |
Local peer registry + config files; optional wg set |
Config — Tailscale
"vpn": {
"provider": "tailscale",
"apiBase": "https://api.tailscale.com",
"apiKey": "tskey-api-…",
"tailnet": "example.ts.net"
}
Create an API access token in the Tailscale admin console with scopes to manage auth keys, devices, and the policy file (ACL). Tags used at enroll (default tag:lindir) must be defined in the ACL tagOwners and allowed for that token / OAuth client.
Hosts already on the mesh do not need a new auth key. Pass skipVpn: true (CLI --skip-vpn, Admin UI checkbox). IdM enroll succeeds even if VPN key creation fails (vpn response may start with error:).
Client join
lindir-client enroll consumes structured vpnDetail from the enroll response:
vpnDetail.kind |
Client action |
|---|---|
auth-key |
tailscale up --authkey=… (and --login-server=… for Headscale) |
wireguard |
Write wg-quick config (mode 600) and bring the interface up |
Auth keys and WireGuard private keys are never written to audit detail (only kind / address).
Config — Headscale
"vpn": {
"provider": "headscale",
"apiBase": "https://headscale.example.com",
"apiKey": "…",
"tailnet": "lindir"
}
| Field | Meaning for Headscale |
|---|---|
apiBase |
Headscale base URL ( /api/v1 appended if missing) |
apiKey |
From headscale apikeys create |
tailnet |
Headscale user name (or numeric user id). Created via API if missing |
Policy sync requires Headscale policy.mode: database. File-based policy is read-only via the API.
Config — WireGuard
"vpn": {
"provider": "wireguard",
"wireguard": {
"endpoint": "vpn.example.com:51820",
"iface": "wg0",
"serverAddress": "10.66.0.1/24",
"peerNetwork": "10.66.0.0/24",
"dns": "10.66.0.1",
"configDir": "/var/lib/lindir/wireguard",
"serverPrivateKeyPath": "",
"applyLive": false
}
}
| Field | Meaning |
|---|---|
endpoint |
Written into client configs as Endpoint |
iface |
Interface name for generated wg0.conf and optional wg set |
serverAddress / peerNetwork |
Server address and IP pool for peers |
configDir |
Server key, wg0.conf, clients/<host>.conf, groups.json |
applyLive |
If true, run wg set after enroll/remove (needs root + wireguard-tools) |
WireGuard has no cloud ACL API: syncGroupAcls writes groups.json and refreshes the server config. Bring the interface up yourself, e.g. wg-quick up /var/lib/lindir/wireguard/wg0.conf.
Live behavior
Tailscale
| Call | API |
|---|---|
| Host enroll | POST /api/v2/tailnet/{tailnet}/keys — one-shot preauthorized auth key; response vpn field is auth-key:tskey-auth-… |
| Sync ACLs | GET then POST /api/v2/tailnet/{tailnet}/acl with If-Match ETag; merges group:<name> → member emails (falls back to username) |
| Remove host | GET …/devices, match hostname, DELETE /api/v2/device/{id} |
Headscale
| Call | API |
|---|---|
| Host enroll | Resolve/create user → POST /api/v1/preauthkey (user name); returns auth-key:… + vpnDetail.loginServer |
| Sync ACLs | GET /api/v1/policy → merge groups → PUT /api/v1/policy ({"policy":"…"}); seeds a default policy if the DB has none |
| Remove host | GET /api/v1/node, match hostname/givenName/name, DELETE /api/v1/node/{id} |
WireGuard
| Call | Behavior |
|---|---|
| Host enroll | Generate X25519 keypair, allocate IP, store peer, write client + server configs; returns wg-peer:… plus vpnDetail.wireguardConfig (full client conf, one-shot) |
| Sync ACLs | Write groups.json from IdM groups; regenerate server config |
| Remove host | Revoke peer, regenerate config, optional wg set … remove / wg syncconf |
applyLive: true brings the interface up with wg-quick when needed and syncs peers with wg syncconf. Generated server conf includes PostUp FORWARD accepts for hub-and-spoke; set net.ipv4.ip_forward=1 on the host (or via the container runtime with --sysctl / privileged).
API
- Host enroll (
POST /v1/hosts/enroll) callsvpn.enrollHost(optionaltagsarray on the body); response includesvpn+ optionalvpnDetail. POST /v1/vpn/sync-aclspushes group ACL sync.DELETE /v1/hosts/{id}and MDM retire callvpn.removeHost.
