Technology Sep 08, 2026 · 5 min read

๐Ÿ” A WireGuard GitHub Action That Actually Tells You When the Tunnel Is Dead

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...

DE
DEV Community
by Ankur K
๐Ÿ” A WireGuard GitHub Action That Actually Tells You When the Tunnel Is Dead

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 show for a completed handshake. Dead tunnel โ†’ the step fails here, with a message telling you to check Endpoint, 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 bundled dist/. A ten-line node24 shim over readable, linted bash. You can audit the whole thing in five minutes โ€” which matters for something running sudo on your runner.
  • ๐Ÿ” Opt-in diagnostics. diagnostics: true prints wg 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/0 routes 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

โญ Star it if it saves you an afternoon.

DE
Source

This article was originally published by DEV Community and written by Ankur K.

Read original article on DEV Community
Back to Discover

Reading List