VPN Gateways¶
Manage an organization's site-to-site VPN gateways, their peers (remote sites) and exposures (services reachable through the tunnel). All commands are org-scoped and need org-admin rights; no project context is required.
Personal WireGuard client
bnerd vpn up / bnerd vpn down / bnerd vpn status manage your own WireGuard connection, and bnerd vpn devices lists and revokes enrolled devices — see VPN client. Those are open to any org member. This page is about the gateways the organization runs.
Gateways¶
bnerd vpn gateways list
bnerd vpn gateways get <gateway-id>
bnerd vpn gateways create <name> [--protocol ipsec|wireguard] [--listen-port <port>] \
[--gateway-config <json|@file>] [--region <region>]
bnerd vpn gateways delete <gateway-id>
bnerd vpn regions
list shows the phase (declared, ready, degraded, error), tunnel health, endpoint IP, region and peer/exposure counts. get adds the operator conditions, every peer with health and byte counters, every exposure, and — for wireguard gateways — the listen port and the gateway's own WireGuard public key (once reconciled).
| Flag | Default | Description |
|---|---|---|
--protocol | ipsec | ipsec or wireguard |
--listen-port | 51820 | WireGuard UDP listen port. --protocol wireguard only; when left at the default the CLI sends no wireguard config block and b'nerd applies the default port itself. |
--gateway-config | Gateway config, inline JSON or @file: ipsec.ike.{version,proposals,lifetime,dpd.{delay,timeout}}, serviceCIDR for ipsec, wireguard.listenPort for wireguard. Unknown keys are rejected with 422 validation_failed. | |
--region | region to create the gateway in (e.g. de-muc1); default: the only region — see bnerd vpn regions. Immutable after create. |
bnerd vpn gateways create hq-site --gateway-config '{"serviceCIDR":"10.42.0.0/24","ipsec":{"ike":{"version":2,"proposals":["aes256-sha512-ecp384"],"lifetime":"86400s"}}}'
bnerd vpn gateways create wg-site --protocol wireguard --listen-port 51821
bnerd vpn gateways create muc-site --region de-muc1
Regions
bnerd vpn regions lists the registered VPN regions a gateway create's --region may pick from (REGION, DEFAULT columns). One region marked DEFAULT means --region can be omitted; several regions with none marked default mean it is required — an unknown region, or a required-but-missing one, comes back as 422 validation_failed naming the valid regions, and the CLI exits with code 5. A gateway created before regions existed shows - (no region bound yet).
Deletion protection
A gateway flagged deletion_protected cannot be deleted; the API answers 422 deletion_protected and the CLI exits with code 5. Deletes do not prompt for confirmation.
Peers¶
bnerd vpn peers list <gateway-id>
bnerd vpn peers create <gateway-id> <name> [--psk <value|->] [--psk-file <path>] \
[--esp-proposals <a,b>] [--peer-address <ip>] [--peer-id <id>] \
[--remote-subnets <a,b>] \
[--wireguard-public-key <key>] [--subnets <a,b>] [--endpoint <host:port>] \
[--keepalive <seconds>] [--preshared-key <value|->] [--preshared-key-file <path>] \
[--peer-config <json|@file>] [--authorization <json|@file>]
bnerd vpn peers update <gateway-id> <peer-id> [--name <name>] [same flags as create]
bnerd vpn peers delete <gateway-id> <peer-id>
--esp-proposals/--peer-address/--peer-id/--remote-subnets are for ipsec gateways; --wireguard-public-key/--subnets/--endpoint/--keepalive/ --preshared-key[-file] are for wireguard gateways. --wireguard-private-key[-file] does not exist — hq never holds a peer's private key, only its public key and (for ipsec) the pre-shared key.
Peer secrets (--psk, --preshared-key) are write-only: the API never returns them and the CLI never prints them. Prefer stdin or a file so the secret stays out of your shell history:
# ipsec peer, from a secret manager via stdin
pass show vpn/office-psk | bnerd vpn peers create gw-1 office \
--esp-proposals aes256-sha256-modp2048 --peer-address 198.51.100.7 --peer-id office \
--remote-subnets 10.20.2.0/24 --psk - --authorization '{"exposures":["gitlab"]}'
# wireguard peer
bnerd vpn peers create gw-1 site-a --wireguard-public-key <base64-key> \
--subnets 10.20.2.0/24,10.20.3.0/24 --endpoint 198.51.100.7:51820 \
--keepalive 25 --preshared-key -
# rotate the PSK later — only the given fields are sent
pass show vpn/office-psk-new | bnerd vpn peers update gw-1 p-1 --psk -
--peer-config is an alternative to the typed flags: it is passed through as the peer's config verbatim, except a JSON object with neither a top-level ipsec nor wireguard key is treated as the ipsec block (so the pre-slice-1 unwrapped form still works). --peer-config cannot be combined with either flag group.
bnerd vpn peers create gw-1 office --psk-file ./office.psk \
--peer-config '{"ipsec":{"esp":{"proposals":["aes256-sha256-modp2048"]},"peer":{"address":"198.51.100.7","id":"office"},"remoteSubnets":["10.20.2.0/24"]}}'
--psk/--psk-file are mutually exclusive (same for --preshared-key/-file). update with no flags is rejected ("nothing to update").
update replaces the whole config block, not just the flags you pass
Only --name, a secret, --authorization and whether any config flag was passed are independent on update. As soon as one ipsec or wireguard config flag is given, the CLI resends the entire ipsec/wireguard block from the flags you passed on that invocation — --keepalive 30 alone sends a config with only persistentKeepalive set, which hq rejects (422) because publicKey is missing. Pass the full set of config flags (or the full --peer-config JSON) every time you touch a peer's config.
Exposures¶
bnerd vpn exposures list <gateway-id>
bnerd vpn exposures create <gateway-id> <name> --direction outbound|inbound [source flags] [--ports 443,8443] [--virtual-ip <ip>]
bnerd vpn exposures update <gateway-id> <exposure-id> [--name] [source flags] [--virtual-ip]
bnerd vpn exposures delete <gateway-id> <exposure-id>
Three source shapes are supported through typed flags; --source <json|@file> passes the raw object instead and overrides them.
Outbound exposures to your own cluster are not available yet
The --cluster/--service/--namespace shape needs the named cluster to be linked to the VPN cluster by b'nerd. No customer cluster is linked at this release, so the API refuses the request with 422 validation_failed and the code cluster_not_linked (the CLI exits with 5). Outbound exposures work today for platform apps (--platform-app, managed Keycloak); inbound exposures (--host) are unaffected. --ports is only accepted together with a full
source shape (typed flags or --source) — passing --ports on its own on update fails with "--direction is required when passing source flags".
| Shape | Flags | Sent as |
|---|---|---|
| Outbound, org service | --cluster <kube-cluster-id> --service <name> --namespace <ns> | {kube_cluster_id, service, namespace, ports} |
| Outbound, platform app | --platform-app <app-id> | {target_kind:"platform_service", platform_app_id, ports} |
| Inbound, remote host | --host <host-or-address> | {host, ports} |
bnerd vpn exposures create gw-1 gitlab --direction outbound --cluster c-1 --service gitlab --namespace apps --ports 443,8443
bnerd vpn exposures create gw-1 keycloak --direction outbound --platform-app app-7 --ports 443
bnerd vpn exposures create gw-1 erp --direction inbound --host erp.corp.example --ports 8080
Ports: at most 15, each in 1..65535. Combining shapes, --host on an outbound exposure, or cluster/app flags on an inbound one is rejected before any request.
Output and exit codes¶
All commands honour -o table|json|yaml. Errors map to the standard exit codes: 2 not found, 3 not org-admin, 5 validation (not_yet_supported, validation_failed, deletion_protected) — see Scripting & Automation.