Your CI job needs to reach something private โ a staging database in a VPC, an internal registry, a deploy target behind a firewall. The usual answers are bad: whitelist GitHub's entire runner IP range, or run a self-hosted runner just for network access.
Better idea: put the runner on your private network for the length of the job. ๐ ankurk91/wireguard-action
๐งญ What is WireGuard?
A modern VPN protocol, and a small one โ ~4,000 lines of code against OpenVPN's hundreds of thousands. It lives in the Linux kernel (mainline since 5.6), so it's fast. Its crypto isn't configurable, so there's nothing to downgrade. And config is just keys, SSH-style:
[Interface]
PrivateKey = <client private key>
Address = 10.0.0.2/32
[Peer]
PublicKey = <server public key>
AllowedIPs = 0.0.0.0/0
Endpoint = vpn.example.com:51820
That's the whole thing. Ephemeral, keyed, instant to bring up โ a great fit for CI.
๐ค Why another action?
I went looking and came away unhappy. Not naming names, but the same problems kept showing up: actions pinned to end-of-life node12/node16, docs that stop at a YAML snippet, huge bundled node_modules running as root, no cleanup step, and tunnel state dumped into job logs.
The big one: wg-quick up exits 0 even when your peer is unreachable. It creates the interface, adds the routes, reports success โ and carries nothing. Your job then fails three steps later with a timeout that looks unrelated, and you burn an afternoon.
โจ Features
-
๐ฉบ Verifies the handshake. After connecting, it sends real traffic and checks
wg showfor a completed handshake. Dead tunnel โ the step fails here, with a message telling you to checkEndpoint, keys, and UDP reachability. Skipped for split tunnels, where there's nothing meaningful to test. -
๐งผ Cleans up automatically. A
post-if: always()step brings the interface down and deletes the config file on success, failure, or cancellation. No disconnect step to remember โ and no private key left behind on a self-hosted runner. -
๐ฆ Zero dependencies. No
node_modules, no bundleddist/. A ten-linenode24shim over readable, linted bash. You can audit the whole thing in five minutes โ which matters for something runningsudoon your runner. -
๐ Opt-in diagnostics.
diagnostics: trueprintswg show, addresses, routes, and your public IP before and after. Off by default, because that describes your network and job logs reach more people than your secrets do. -
๐ Real docs. A README and a TROUBLESHOOTING.md written from failures I actually hit โ including the two that bite everyone: GitHub runners have no IPv6 (strip IPv6 from your config or the tunnel won't start), and
AllowedIPs = 0.0.0.0/0routes the runner's own connection to GitHub through your VPN (if your VPN blocks that, the job hangs).
๐ Getting started
1. Grab a client config from your WireGuard server โ the whole wg0.conf.
2. Add it as a repository secret called WIREGUARD_CONFIG, pasting the entire file.
โ ๏ธ Never commit the config or inline it โ it holds your private key.
3. Add one step:
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- name: Connect to WireGuard VPN
uses: ankurk91/wireguard-action@v1
with:
config: ${{ secrets.WIREGUARD_CONFIG }}
# ๐ Everything below is routed through the VPN.
- name: Talk to something private
run: curl -sf http://10.0.0.50:8080/health
That's it โ there is no disconnect step to add.
| Input | Required | Default | Description |
|---|---|---|---|
config |
โ | โ | Full config contents. Always from a secret. |
interface |
โ | wg0 |
Written to /etc/wireguard/<interface>.conf. |
diagnostics |
โ | false |
Print tunnel state and routes to the log. |
Requires an Ubuntu runner and an IPv4-only config.
๐งช Tested for real
Not "I ran it once." CI stands up a real linuxserver/wireguard server in Docker, in its own network namespace, and connects to it โ a genuine tunnel, no mocks. It then asserts the interface is up, the config is mode 600, both ends agree on the client's public key, ICMP crosses the tunnel, and the byte counters are non-zero.
That runs 8ร per commit: ubuntu-24.04 and ubuntu-26.04, on x86_64 and ARM, with diagnostics both on and off โ plus shellcheck and checkbashisms, and a monthly schedule so runner-image drift is caught by CI instead of by you on a Friday. It's also in use in real projects; the handshake check exists because of one. ๐ฌ The test workflow.
โก Performance & ๐ก๏ธ Security
It runs before everything else in your job, so it stays light: no apt-get update on the happy path (it installs from the image's existing indexes and only refreshes if that fails), --no-install-recommends, skips the install entirely if wg-quick is present, and waits for the dpkg lock instead of flaking when unattended-upgrades holds it. The handshake check is bounded to 5s.
On the security side: the config is chmod 600 (asserted in CI), the interface input is validated against wg-quick's own character class so it can't escape /etc/wireguard, the key is removed on every exit path, diagnostics are opt-in, and the supply chain is empty. Pin @v1 โ or a full SHA if your threat model asks for it.
๐ Links
- ๐งฉ The action ยท README ยท Troubleshooting ยท Issues
- ๐ WireGuard ยท whitepaper ยท
wg-quick(8) - ๐ GitHub runner images ยท โ๏ธ MIT
โญ Star it if it saves you an afternoon.
This article was originally published by DEV Community and written by Ankur K.
Read original article on DEV Community