Please report security issues privately - do not open a public issue for anything exploitable.
- Preferred: GitHub Private vulnerability reporting (Security β Report a vulnerability).
- Or email: izeystudio@gmail.com
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.
This is an early project; only the latest release (and main) receive fixes.
If you are running an older version, please upgrade before reporting.
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 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-helperfor the installing user; - the polkit rule allows
pkexecof only that program, and only for a user who is both in an active local session and a member of thewireguardgroup.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 andpkexecfalls 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.
The helper itself:
- exports a fixed
PATH(/usr/sbin:/usr/bin:/sbin:/bin) so a hijacked callerPATHcan't redirect thewg/wg-quick/loggerit 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/PreDownscript 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
Durationbound; - writes configs atomically (temp file with
O_EXCL+fsync+rename, mode600) 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.
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.
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.
- A tunnel
.confcontains the interface private key. Files are written0600; backups are0600in/etc/wireguard/.backup. The directory itself is mode0700and 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
.confinto a.zip; that archive contains private keys. It is created0600and 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.
- Prebuilt release artifacts ship with a
SHA256SUMSfile that is signed with minisign (SHA256SUMS.minisig; public keyminisign.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-debandlinuxdeployare pinned to exact versions, andlinuxdeployis SHA-256-verified before it runs. - The release token is least-privilege: read-only everywhere except the one job that publishes.
- CI runs
cargo auditagainst 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/PdsnTWKwfOpb3Hv4DxGWhen in doubt, build from source - cargo build --release is reproducible
on any supported distro with the Rust toolchain and Slint dev libraries.
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_termandattycome from a build-time codegen step inside theksnitray crate (dbus-codegenβclap v2);bincodeandpastecome 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.
- The privileged surface is the
wg-helperbinary only. The GUI binary runs unprivileged and never escalates. - Treat the
sudoers/polkitgrant as "this local user may control WireGuard without a password" - equivalent to the trust you'd place inwg-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(mode0700).