Skip to content

Security: JamilleJung/wireguard-gui

Security

SECURITY.md

πŸ›‘οΈ Security Policy

πŸ“§ Reporting a vulnerability

Please report security issues privately - do not open a public issue for anything exploitable.

Please include the version (or commit), your distro + package manager, repro steps, and impact. Do not include real private keys or production configs. Coordinated disclosure is appreciated - you'll get an acknowledgement as soon as possible and be kept updated on a fix.

πŸ“‹ Supported versions

This is an early project; only the latest release (and main) receive fixes. If you are running an older version, please upgrade before reporting.

πŸ” Threat model

wireguard-gui manages WireGuard tunnels under /etc/wireguard, which requires root. The design goal is to keep the part that runs as root as small and auditable as possible, and to keep everything else unprivileged.

In scope (things we actively defend against):

  • A bug or hijacked environment in the unprivileged UI escalating to root.
  • A user who can run the helper (via the passwordless grant) escalating beyond "manage WireGuard tunnels" to arbitrary root code execution.
  • The privileged helper being tricked into touching files outside /etc/wireguard (path traversal) or running attacker-chosen commands.
  • A malicious or malformed .conf/QR import corrupting existing tunnels or smuggling code that would run as root.
  • Another local user reading your private keys through the helper.
  • Truncated/lost configs from interrupted writes.

Out of scope (cannot be defended against here):

  • An attacker who is already root, or who can already run code as your user (they can read your keys directly; pinning a binary path doesn't help).
  • The security of WireGuard itself, the kernel module, or wg/wg-quick.
  • Physical access / a compromised display server / your clipboard history.

πŸ”’ The privilege boundary

The GUI runs as your normal user. The only thing that runs as root is one small Rust helper binary, wg-helper (src/bin/wg-helper.rs in source), invoked as sudo -n wg-helper <verb> [name] (sudoers mode) or pkexec wg-helper ... (polkit / fallback). The GUI binary itself never runs as root.

Authorisation is scoped to exactly that one helper path:

  • the sudoers drop-in grants passwordless execution of only /usr/local/lib/wireguard-gui/wg-helper for the installing user;
  • the polkit rule allows pkexec of only that program, and only for a user who is both in an active local session and a member of the wireguard group. install.sh --polkit (and the .deb) create the group and add the intended user. This is deliberate: the helper can read configs that contain private keys, so a passwordless grant must not be handed to every logged-in user. Until a user is in the group the rule denies and pkexec falls back to asking for the admin password (fail closed).

Because the grant is bound to the absolute helper path, pointing the app at a different program (e.g. via $WG_HELPER) cannot silently gain root - it would fall outside the sudoers/polkit grant and prompt or fail. In release builds the helper-path override is additionally refused unless WG_ALLOW_UNSAFE_HELPER=1 is set and the target is an absolute, root-owned, non-world-writable file.

πŸ›‘οΈ Helper hardening

The helper itself:

  • exports a fixed PATH (/usr/sbin:/usr/bin:/sbin:/bin) so a hijacked caller PATH can't redirect the wg/wg-quick/logger it runs as root;
  • validates every tunnel name against ^[A-Za-z0-9][A-Za-z0-9_.-]{0,14}$ and rejects ., .., /, \, so "$WG_DIR/<name>.conf" can never escape /etc/wireguard;
  • rejects PostUp / PreUp / PostDown / PreDown script hooks in any config it saves (see below) - these are blocked at the privilege boundary, not just warned about in the UI;
  • no sh -c - all subprocess calls use argv arrays directly;
  • timeouts - every external call has a Duration bound;
  • writes configs atomically (temp file with O_EXCL + fsync + rename, mode 600) and keeps a timestamped 0600 backup before any overwrite, rename, or delete;
  • validates the saved config shape in the helper before save/rename, in addition to the unprivileged frontend validation (second check inside the privileged boundary);
  • logs every mutating action (with the invoking user) to the journal (logger -t wireguard-gui), with private/preshared keys redacted.

🚫 Script hooks are blocked

WireGuard configs may contain PostUp / PreUp / PostDown / PreDown directives, which wg-quick runs as root when the tunnel is activated. Left unchecked, a config saved through the helper could therefore run arbitrary root commands - turning the narrow "manage tunnels" grant into full root.

To keep the privilege boundary meaningful, the helper refuses to save or rename any config that contains those directives (the editor surfaces the error). If you genuinely need a hook, edit the file under /etc/wireguard directly as root - outside this constrained helper.

πŸ”Œ Kill switch scope

The helper can add/remove tunnel-scoped firewall rules for an active wg-quick tunnel, preferring nftables (inet filter) with an iptables/ip6tables fallback. The rules allow loopback, the tunnel interface, the tunnel's fwmark (WireGuard's own encrypted egress) and - when $SSH_CONNECTION is set - established SSH return traffic, and reject everything else. On the iptables fallback it fails closed if the host has IPv6 but ip6tables is missing, rather than silently leaving IPv6 unprotected.

The kill switch is not persistent: it installs no daemon and the rules live only as long as the helper-managed tunnel does. They are torn down on deactivate/delete/rename. If a tunnel is stopped by some other path (a manual wg-quick down, a service restart, a reboot of just the unit), the rules can linger until you next use the app - re-activating or toggling the kill switch off clears them. Do not rely on it as a permanent system firewall.

πŸ”‘ Private keys and QR codes

  • A tunnel .conf contains the interface private key. Files are written 0600; backups are 0600 in /etc/wireguard/.backup. The directory itself is mode 0700 and root-owned.
  • The editor displays the config including the private key in clear text by design (same as any WireGuard tool). The app does not log it.
  • Show QR renders the full config - including the private key - as a QR code. Anyone who photographs your screen gets the key. Only display it when it is safe to do so.
  • Export writes every tunnel's .conf into a .zip; that archive contains private keys. It is created 0600 and refuses to follow a symlink at the destination. Store it somewhere safe and delete it when done.
  • Copying the running config copies the full [Interface] block, including the private key, to your clipboard. Clipboard managers may keep history; clear it when done.

βœ… Supply chain and verifying a download

  • Prebuilt release artifacts ship with a SHA256SUMS file that is signed with minisign (SHA256SUMS.minisig; public key minisign.pub). Signing is fail-closed: the release aborts rather than publish unsigned artifacts.
  • All GitHub Actions are pinned to commit SHAs (including first-party actions/*); cargo-deb and linuxdeploy are pinned to exact versions, and linuxdeploy is SHA-256-verified before it runs.
  • The release token is least-privilege: read-only everywhere except the one job that publishes.
  • CI runs cargo audit against the RUSTSEC advisory DB, and Dependabot watches the crate and Action pins.
  • Verify a download with:
sha256sum -c SHA256SUMS --ignore-missing
minisign -Vm SHA256SUMS -P RWSrokrj4nWGDhUf409+6yXuqPfF7WQuGtSk/PdsnTWKwfOpb3Hv4DxG

When in doubt, build from source - cargo build --release is reproducible on any supported distro with the Rust toolchain and Slint dev libraries.

🧾 Dependency advisories (accepted, documented)

cargo audit runs in CI against the RUSTSEC database. A few advisories are flagged on transitive build-time / proc-macro / optional dependencies - none of which are compiled into the runtime binary:

  • ansi_term and atty come from a build-time codegen step inside the ksni tray crate (dbus-codegen β†’ clap v2);
  • bincode and paste come from the Slint UI framework and its image-codec compiler step.

These are unmaintained / soundness-lint advisories, not exploitable vulnerabilities, and they live in upstream crates this project does not control. Eliminating them today would require replacing the entire UI framework, or pulling a much heavier runtime dependency (tokio, a C appindicator, …) into the app to drop a build-time warning - a net increase in real attack surface. They are therefore listed as documented --ignore entries in the audit gate, which still fails on any real or new vulnerability. Dependabot watches the crate and Action pins, so the ignores can be removed as soon as a clean upstream fix is available.

To see what a shipped binary actually contains (the audit gate scans the full lockfile, including non-shipping deps), every release binary embeds an SBOM via cargo auditable build - run cargo audit bin <binary> to audit exactly what was compiled in.

πŸ“ Notes

  • The privileged surface is the wg-helper binary only. The GUI binary runs unprivileged and never escalates.
  • Treat the sudoers/polkit grant as "this local user may control WireGuard without a password" - equivalent to the trust you'd place in wg-quick, minus script hooks (which the helper blocks).
  • Config files contain private keys in clear text (same as upstream WireGuard tools). They are stored 0600, root-owned, in /etc/wireguard (mode 0700).

There aren't any published security advisories