NetworkManager VPN plugin for Pulse Secure / OpenConnect with browser-based SSO/SAML authentication.
This plugin provides full NetworkManager integration for Pulse Secure VPNs that require browser-based SAML/SSO authentication. It appears in system tray apps (gnome-shell, KDE Plasma, nm-applet) as a standard VPN connection type.
Key features:
- CEF (Chromium Embedded Framework) browser with WebAuthn/FIDO2 support (hardware keys like YubiKey)
- Custom D-Bus VPN service with automatic reconnection after suspend/resume and network changes
- DTLS/ESP mode for better VPN performance
- Browser extension support (e.g., Bitwarden password manager)
- User-agent switching for Okta compatibility
- KDE Plasma integration plugin
- NixOS module with declarative configuration
- Selenium WebDriver as an alternative auth engine
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β User Space (runs as user) β
β ββββββββββββββββ β
β β NM Frontend β gnome-shell / KDE Plasma / nm-applet β
β ββββββββ¬ββββββββ β
β β β
β ββββββββΌβββββββββββββββββββββββ ββββββββββββββββββββββββββββββ β
β β pulse-sso-auth-dialog βββββΆβ pulse-browser-auth (CEF) β β
β β (NM auth-dialog protocol) β β Chromium browser window β β
β β Reads stdin, outputs cookie β β Monitors for DSID cookie β β
β βββββββββββββββββββββββββββββββ β WebAuthn/FIDO2, extensions β β
β ββββββββββββββββββββββββββββββ β
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ€
β Root Space (runs as root) β
β ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ β
β β nm-pulse-sso-service.py (custom D-Bus VPN plugin) β β
β β - Implements org.freedesktop.NetworkManager.VPN.Plugin β β
β β - Spawns openconnect with -C <cookie> --protocol=pulse β β
β β - Auth failure retry (up to 3 attempts, re-launches browser) β β
β β - Reconnection via SIGUSR2 (SSL) or SIGTERM (DTLS) β β
β β β β
β β ββββββββββββββββββββββββββββββββββββββ β β
β β β nm-pulse-sso-helper β β β
β β β Called by openconnect via --script β β β
β β β Configures TUN, routes, DNS β β β
β β β Reports IP config back via D-Bus β β β
β β ββββββββββββββββββββββββββββββββββββββ β β
β ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ β
β β
β Recovery layer (systemd + NM dispatcher): β
β - vpn-reconnect.sh: post-resume.target service β
β - nm-dispatcher.sh: interface change handler β
β - vpnc hooks: default route fixup, Docker route narrowing β
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
C++ application using Chromium Embedded Framework. Navigates to the VPN URL, opens a browser window for SAML authentication, and monitors cookies. Outputs DSID=<value> on stdout when the authentication cookie is set.
Features:
- User-agent switching: starts with a Windows UA to bypass Okta's Linux blocking, then switches to Linux UA after the SAML page loads
- Browser extension loading via
--extension <path>(comma-separated for multiple) - WebAuthn/FIDO2 support for hardware security keys
- Popup blocking (single browser window)
- Profile/cache persisted at
~/.cache/pulse-browser-auth - 300-second default authentication timeout
Python script following NetworkManager's auth-dialog stdin/stdout protocol. Reads VPN settings from NM (DATA_KEY/DATA_VAL pairs), launches the CEF browser, and returns the DSID cookie and server certificate fingerprint to NetworkManager. No GUI of its own -- the CEF browser window is the user interface.
If an existing cookie is available and valid, it skips the browser entirely.
Custom Python D-Bus service implementing org.freedesktop.NetworkManager.VPN.Plugin. This is not the standard nm-openconnect-service -- it is a purpose-built service for Pulse SSO.
Responsibilities:
- Receives
Connect()/ConnectInteractive()calls from NetworkManager with credentials - Spawns
openconnect --protocol=pulse -C <cookie>with the helper script - Reads runtime config from
/etc/nm-pulse-sso/config(DTLS mode, TCP keepalive) - Handles openconnect exit codes: auth failures (exit 2) trigger browser re-launch via
systemd-run; other failures restart openconnect with the existing cookie - Auth failure retry: up to 3 consecutive failures before giving up
- Reconnection retry: up to 10 attempts with 3-second intervals
- Uses multiple
systemd-runlaunch strategies to propagate graphical session environment (Wayland/X11)
Called by openconnect via --script. Reads openconnect environment variables (TUNDEV, INTERNAL_IP4_ADDRESS, DNS servers, split-tunnel routes, etc.), invokes vpnc-script for standard network setup, and reports IP configuration back to the D-Bus service.
DNS handling: prepends the local gateway IP to the DNS list and sets VPN DNS priority to 100 (fallback), so local DNS resolves quickly while VPN DNS provides access to internal domains.
- vpn-reconnect.sh -- systemd service on
post-resume.target. Waits for network connectivity, fixes the VPN server route to the physical interface, sends SIGUSR2 (non-DTLS) or SIGTERM (DTLS) to openconnect - nm-dispatcher.sh -- NM dispatcher (
90-vpn-reconnect) for connectivity-change and interface-down events - vpnc hooks -- default route fixup and Docker route narrowing on connect/reconnect
- service-restart.sh -- kills old service process on NixOS rebuild
Qt/C++ plugin that registers the VPN type in KDE Plasma's network applet, allowing users to create and manage Pulse SSO VPN connections from KDE system settings.
diagnose-nm-pulse-vpn [minutes] collects logs, network state, routing tables, DNS config, process info, and connectivity tests. Output is saved to /tmp/vpn-diagnose-<timestamp>.log. Defaults to 15 minutes of log lookback.
The plugin does not include an nm-connection-editor UI component, so connections should be created via nmcli or the NixOS module's declarative configuration. The KDE Plasma plugin does provide a settings UI.
CLI (nmcli):
nmcli connection add type vpn con-name "Pulse VPN" \
vpn-type openconnect-pulse-sso \
vpn.data "gateway=https://vpn.example.com/saml,protocol=pulse"GUI: Click on the VPN in your system tray or network settings.
CLI:
nmcli connection up "Pulse VPN"A CEF browser window opens for SAML/SSO authentication. If extensions are configured (e.g., Bitwarden), they are loaded in the browser. Once authentication completes and the DSID cookie is set, the browser closes automatically and the VPN connects.
Use pulse-browser-setup to launch the CEF browser pointed at the Chrome Web Store for installing extensions and configuring settings. Settings persist in ~/.cache/pulse-browser-auth.
Add this flake as an input and use the NixOS module:
{
inputs.nm-openconnect-pulse-sso.url = "github:erahhal/nm-openconnect-pulse-sso";
outputs = { self, nixpkgs, nm-openconnect-pulse-sso, ... }: {
nixosConfigurations.myhost = nixpkgs.lib.nixosSystem {
modules = [
nm-openconnect-pulse-sso.nixosModules.default
{
services.nm-pulse-sso = {
enable = true;
gateway = "https://vpn.example.com/saml";
};
}
];
};
};
}services.nm-pulse-sso = {
enable = true;
gateway = "https://vpn.example.com/saml"; # Required: VPN gateway URL
vpnName = "Pulse VPN"; # Connection name (default: "Pulse VPN")
enableDtls = true; # DTLS/ESP for better performance (default: true)
enableRecovery = true; # Auto-reconnect scripts (default: true)
enableTcpKeepalive = false; # TCP keepalive on TLS channel (default: false)
tcpKeepaliveInterval = 120; # Keepalive interval in seconds (default: 120)
enableSelenium = false; # Use Selenium instead of CEF (default: false)
extensions = []; # Browser extension packages (default: [])
pinExtensions = true; # Pin extensions to toolbar (default: true)
};Extensions are unpacked Chrome extension directories loaded into the CEF browser:
services.nm-pulse-sso = {
enable = true;
gateway = "https://vpn.example.com/saml";
extensions = [
(pkgs.callPackage ./extensions/bitwarden.nix {})
];
};The module automatically:
- Installs the NM plugin, openconnect, and CEF browser
- Creates the VPN connection profile declaratively
- Sets up D-Bus policy for the VPN service
- Installs recovery scripts (vpn-reconnect systemd service, NM dispatcher, vpnc hooks)
- Applies KDE Plasma overlay for desktop integration
- Sets
rpfilterto "loose" when recovery is enabled (required for reliable reconnection) - Optionally patches openconnect for TCP keepalive
- Installs
diagnose-nm-pulse-vpnandpulse-browser-setupsystem-wide
- CEF mode (default): ensure
pulse-browser-authis available and~/.cache/pulse-browser-authis writable - Selenium mode: ensure
chromedriverandchromiumare in PATH - Check
journalctl -u NetworkManagerfor auth-dialog launch errors - On Wayland, the service uses
systemd-runwith environment propagation; verify WAYLAND_DISPLAY is set
- Default timeout is 300 seconds
- Check
journalctl -u NetworkManagerfor CEF/browser errors - User-agent switching may need adjustment if the identity provider blocks requests
- Run
diagnose-nm-pulse-vpnto collect diagnostic info - Check DNS configuration and routes in the diagnostic output
- If using Docker, the vpnc hooks automatically narrow routes to avoid conflicts
- With
enableRecovery = true(default), the module installs recovery scripts automatically - DTLS mode uses full restart (SIGTERM); non-DTLS mode uses graceful reconnect (SIGUSR2)
- If the cookie has expired, a browser window opens for re-authentication (up to 3 attempts)
rpfilteris automatically set to "loose" mode- Check
journalctl -u vpn-reconnectfor post-resume logs
diagnose-nm-pulse-vpn # Collect last 15 minutes of logs
diagnose-nm-pulse-vpn 30 # Collect last 30 minutes of logsOutput is saved to /tmp/vpn-diagnose-<timestamp>.log.