OpenConnect wrapper with Azure AD (SAML) SSO support for Cisco SSL-VPNs
Project description
🔐 openconnect-saml
OpenConnect wrapper with Azure AD / SAML authentication for Cisco AnyConnect VPNs
Maintained fork of vlaci/openconnect-sso with improvements from kowyo/openconnect-lite
✨ Features
| Feature | Description |
|---|---|
| 🖥️ GUI Mode | Embedded Qt6 WebEngine browser with auto-fill |
| 🌐 Chrome Browser | Playwright-based Chromium backend — headless or visible |
| 🤖 Headless Mode | No display needed — works on servers, containers, SSH |
| 🔑 Auto-Login | Username, password, and TOTP auto-injection |
| 🌍 2FAuth | Fetch TOTP from a remote 2FAuth instance |
| 🔒 Keyring | Credentials stored securely (with in-memory fallback) |
| 📱 MFA Support | TOTP, Microsoft Authenticator number matching |
| 🔐 FIDO2/YubiKey | Hardware security key support for WebAuthn challenges |
| 🛡️ Security | XXE protection, no credential logging, safe config permissions |
| 🔄 Auto-Reconnect | Automatic re-authentication and reconnection on VPN drops |
| ⚙️ Systemd Service | Install as a persistent system service with one command |
| 🌐 Proxy | SOCKS and HTTP proxy support |
| 📜 Certificates | Client certificate handling with auto-fallback |
| 🐳 Docker-ready | Headless mode for containerized deployments |
| 👤 Multi-Profile | Save and switch between named VPN configurations |
| 📊 Status TUI | Live connection status with traffic stats (rich optional) |
| ⌨️ Shell Completion | Tab completion for bash, zsh, and fish |
| 🔀 Split-Tunnel | Route only specific subnets through the VPN |
| 🔑 Bitwarden TOTP | Fetch TOTP from Bitwarden CLI (bw) |
| 🔔 Notifications | Desktop notifications for VPN events |
| 🧙 Setup Wizard | Interactive setup command for easy configuration |
📦 Installation
# Headless (no GUI dependencies)
pip install openconnect-saml
# With GUI browser (Qt6 WebEngine)
pip install "openconnect-saml[gui]"
# With Chrome/Chromium browser (Playwright)
pip install "openconnect-saml[chrome]"
playwright install chromium
# With FIDO2/YubiKey support
pip install "openconnect-saml[fido2]"
# With connection status TUI (rich)
pip install "openconnect-saml[tui]"
# All extras (GUI + Chrome + FIDO2 + TUI)
pip install "openconnect-saml[gui,chrome,fido2,tui]"
# Arch Linux (AUR)
yay -S openconnect-saml
# or
paru -S openconnect-saml
Requires: Python ≥ 3.10 and OpenConnect in PATH
🚀 Quick Start
# GUI mode (default)
openconnect-saml --server vpn.example.com --user user@domain.com
# Headless mode (servers, containers, SSH)
openconnect-saml --server vpn.example.com --user user@domain.com --headless
📖 Usage
GUI Mode
openconnect-saml --server vpn.example.com
openconnect-saml --server vpn.example.com/usergroup
openconnect-saml --profile /opt/cisco/anyconnect/profile
Headless Mode
No display server required — perfect for servers, CI/CD, and containers:
# Auto-authenticate with saved credentials
openconnect-saml --server vpn.example.com --headless --user user@example.com
# Output auth cookie for scripting
openconnect-saml --server vpn.example.com --headless --authenticate json
How it works:
- Auto: HTTP requests + form parsing → submits credentials without a browser
- Fallback: If auto-auth fails (CAPTCHA, unsupported MFA) → prints URL + starts local callback server → authenticate in any browser
Chrome/Chromium Browser
Use Playwright-based Chromium instead of Qt WebEngine. Both backends support YubiKey/Nitrokey/WebAuthn and Duo/Microsoft Authenticator since v0.8.2 (Qt-WebEngine ≥ 6.7 required for the Qt backend). Chrome remains the recommended fallback if you hit Qt-platform-specific quirks.
# Visible Chrome window
openconnect-saml --server vpn.example.com --browser chrome
# Saved profile with Chrome override
openconnect-saml connect work --browser chrome
# Headless Chrome (no display needed)
openconnect-saml --server vpn.example.com --browser headless
Minimal Profile GUI
For a small Cisco Secure Client-like launcher around saved profiles:
openconnect-saml gui
The GUI lists saved profiles, starts connect <profile> --browser chrome,
shows output, and can terminate the VPN process. It is intentionally minimal;
advanced options still live in the CLI.
Auto-Reconnect
Keep the VPN alive — automatically re-authenticates and reconnects on drops:
# Unlimited retries with backoff (30s, 60s, 120s, 300s)
openconnect-saml --server vpn.example.com --headless --reconnect
# Limit to 5 reconnection attempts
openconnect-saml --server vpn.example.com --headless --reconnect --max-retries 5
Systemd Service
Install as a persistent system service:
# Install and enable
sudo openconnect-saml service install --server vpn.example.com --user user@domain.com
# Manage
sudo openconnect-saml service start --server vpn.example.com
sudo openconnect-saml service stop --server vpn.example.com
openconnect-saml service status
openconnect-saml service logs --server vpn.example.com
# Remove
sudo openconnect-saml service uninstall --server vpn.example.com
FIDO2/YubiKey
Hardware security key support for WebAuthn challenges during SAML authentication:
# FIDO2 is detected automatically during auth flows
# When a WebAuthn challenge is encountered:
# → Terminal prompt: "Touch your security key..."
# → PIN prompt if required
pip install "openconnect-saml[fido2]"
2FAuth TOTP Provider
Fetch TOTP codes from a 2FAuth instance instead of storing secrets locally:
# Via CLI flags
openconnect-saml --server vpn.example.com --headless --user user@domain.com \
--totp-source 2fauth \
--2fauth-url https://2fauth.example.com \
--2fauth-token YOUR_PERSONAL_ACCESS_TOKEN \
--2fauth-account-id 42
# Or via config file (~/.config/openconnect-saml/config.toml)
[credentials]
username = "user@example.com"
totp_source = "2fauth"
[2fauth]
url = "https://2fauth.example.com"
token = "eyJ0eXAiOiJKV1QiLC..."
account_id = 42
Setup:
- Install 2FAuth and add your VPN TOTP account
- Create a Personal Access Token in 2FAuth (Settings → OAuth → Personal Access Tokens)
- Note the account ID (visible in the URL when editing the account, or via API)
- Configure openconnect-saml with
--totp-source 2fauthortotp_source = "2fauth"in config
⚠️ HTTPS is strongly recommended for the 2FAuth URL. HTTP connections will trigger a warning.
Bitwarden TOTP Provider
Fetch TOTP codes from your Bitwarden vault via the bw CLI:
# Via CLI flags
openconnect-saml --server vpn.example.com --headless --user user@domain.com \
--totp-source bitwarden \
--bw-item-id YOUR_VAULT_ITEM_UUID
# Or via config file (~/.config/openconnect-saml/config.toml)
[credentials]
username = "user@example.com"
totp_source = "bitwarden"
[bitwarden]
item_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
Setup:
- Install the Bitwarden CLI (
bw) - Log in:
bw loginand unlock:bw unlock→ exportBW_SESSION - Find your item ID:
bw list items --search "VPN"→ note theidfield - Configure:
--totp-source bitwarden --bw-item-id <uuid>
Split-Tunnel Routing
Route only specific subnets through the VPN (split-tunneling):
# Include specific routes
openconnect-saml connect work --route 10.0.0.0/8 --route 172.16.0.0/12
# Exclude routes (bypass VPN for these)
openconnect-saml connect work --no-route 192.168.0.0/16
# Combine both
openconnect-saml connect work --route 10.0.0.0/8 --no-route 10.0.99.0/24
Or configure per-profile:
[profiles.work]
server = "vpn.company.com"
routes = ["10.0.0.0/8", "172.16.0.0/12"]
no_routes = ["192.168.0.0/16"]
Desktop Notifications
Get notified about VPN events (connect, disconnect, reconnect, errors):
# Enable via CLI
openconnect-saml connect work --notify
# Or in config
notifications = true
Supports: Linux (notify-send), macOS (osascript), fallback (terminal bell).
Setup Wizard
Interactive configuration wizard for first-time setup:
openconnect-saml setup
Guides through: server URL, username, TOTP provider, browser mode, auto-reconnect, and notifications. Saves a named profile.
Multi-Profile
Save named VPN configurations and switch between them:
# Add profiles
openconnect-saml profiles add work --server vpn.company.com --user user@company.com
openconnect-saml profiles add lab --server lab-vpn.company.com --user admin
# List profiles
openconnect-saml profiles list
# Connect to a profile
openconnect-saml connect work
openconnect-saml connect lab
# Override server from profile
openconnect-saml connect work --server alt-vpn.company.com
# Remove a profile
openconnect-saml profiles remove lab
# Legacy mode still works (backwards-compatible)
openconnect-saml --server vpn.example.com
Config file format:
[profiles.work]
server = "vpn.company.com"
user_group = "employees"
name = "Work VPN"
[profiles.work.credentials]
username = "user@company.com"
totp_source = "2fauth"
[profiles.lab]
server = "lab-vpn.company.com"
name = "Lab VPN"
[profiles.lab.credentials]
username = "admin"
Connection Status
View live VPN connection status:
# One-shot status
openconnect-saml status
# Live-updating status (refreshes every 2s)
openconnect-saml status --watch
Install with rich for a formatted table display:
pip install "openconnect-saml[tui]"
Shell Completion
# Generate completion scripts
openconnect-saml completion bash
openconnect-saml completion zsh
openconnect-saml completion fish
# Auto-install to default locations
openconnect-saml completion install
Advanced Options
--headless # No browser, terminal-only authentication
--browser BACKEND # Browser backend: qt, chrome, headless
--totp-source SOURCE # TOTP provider: local, 2fauth, bitwarden, 1password, pass, or none
--no-totp # Skip the TOTP prompt entirely (alias for --totp-source none)
--2fauth-url URL # 2FAuth instance URL
--2fauth-token TOKEN # 2FAuth Personal Access Token
--2fauth-account-id ID # 2FAuth account ID for VPN TOTP
--bw-item-id UUID # Bitwarden vault item ID for TOTP
--reconnect # Auto-reconnect on VPN drops
--max-retries N # Max reconnection attempts (default: unlimited)
--route CIDR # Include route in VPN tunnel (repeatable)
--no-route CIDR # Exclude route from VPN tunnel (repeatable)
--notify # Enable desktop notifications
--no-sudo # Don't use sudo (for --script-tun)
--ssl-legacy # Enable legacy SSL renegotiation
--csd-wrapper PATH # CSD/hostscan wrapper script
--timeout SECONDS # HTTP timeout (default: 30)
--window-size WxH # Browser window size (default: 800x600)
--on-connect CMD # Run command after VPN connects
--on-disconnect CMD # Run command after VPN disconnects
--reset-credentials # Clear saved keyring entries
--authenticate FORMAT # Auth only, output cookie (json|shell)
🐳 Docker
FROM python:3.12-slim
RUN pip install openconnect-saml && \
apt-get update && apt-get install -y openconnect && rm -rf /var/lib/apt/lists/*
ENTRYPOINT ["openconnect-saml", "--headless"]
docker run -it --cap-add=NET_ADMIN --device=/dev/net/tun \
vpn-client --server vpn.example.com --user user@example.com
⚙️ Configuration
Config file: ~/.config/openconnect-saml/config.toml
[default_profile]
server = "vpn.example.com"
user_group = ""
name = "My VPN"
[credentials]
username = "user@example.com"
# Named profiles (optional)
[profiles.work]
server = "vpn.company.com"
user_group = "employees"
name = "Work VPN"
[profiles.work.credentials]
username = "user@company.com"
Auto-fill Rules
Custom rules per URL pattern:
[[auto_fill_rules."https://*"]]
selector = "input[type=email]"
fill = "username"
[[auto_fill_rules."https://*"]]
selector = "input[name=passwd]"
fill = "password"
[[auto_fill_rules."https://*"]]
selector = "input[id=idTxtBx_SAOTCC_OTC]"
fill = "totp"
TOTP Configuration
For automated TOTP, add your secret to the config:
[credentials]
username = "user@example.com"
# TOTP secret is stored in keyring on first use
# Or set via: openconnect-saml --user user@example.com (prompts for secret)
To clear stored credentials:
openconnect-saml --user user@example.com --reset-credentials
🔄 Migrating from openconnect-sso
# Replace openconnect-sso with openconnect-saml
pip uninstall openconnect-sso
pip install openconnect-saml
# Rename config directory
mv ~/.config/openconnect-sso ~/.config/openconnect-saml
# Same CLI, new name
openconnect-saml --server vpn.example.com
🛠️ Development
git clone https://github.com/mschabhuettl/openconnect-saml
cd openconnect-saml
pip install -e ".[dev]"
pytest -v
ruff check .
📎 Links
| Resource | URL |
|---|---|
| GitHub | mschabhuettl/openconnect-saml |
| PyPI | pypi.org/project/openconnect-saml |
| AUR | aur.archlinux.org/packages/openconnect-saml |
| Releases | GitHub Releases |
| Issues | Bug Tracker |
| Changelog | CHANGELOG.md |
| License | GPL-3.0 |
| Original | vlaci/openconnect-sso |
🙏 Credits
- László Vaskó (vlaci) — original openconnect-sso
- Kowyo — openconnect-lite modernization
- Community contributors for issues, PRs, and testing
📄 License
openconnect-saml — v0.8.0 feature additions
The following sections document the six new features added in v0.8.0.
These are supplements to the main README.md; integrate them into the
appropriate sections of the main README.
1Password TOTP
Delegate TOTP generation to 1Password's op CLI. You'll need to be signed
in (op signin with an exported session token, or biometric/system
integration on desktop OSes).
openconnect-saml connect work \
--totp-source 1password \
--1password-item "vpn-work-mfa" \
--1password-vault "Engineering"
Configuration equivalent:
[1password]
item = "vpn-work-mfa" # UUID, name, or share URL
vault = "Engineering" # optional — searches all vaults if omitted
account = "acme.1password.com" # optional, for multi-account setups
[profiles.work.credentials]
totp_source = "1password"
pass (password-store) TOTP
Uses the pass-otp extension.
Your password entry must contain a otpauth:// or totp:// URI.
openconnect-saml connect work \
--totp-source pass \
--pass-entry "vpn/work-totp"
Configuration:
[pass]
entry = "vpn/work-totp"
[profiles.work.credentials]
totp_source = "pass"
Requirements: the pass binary, the pass-otp extension, and an
unlocked GPG agent.
Kill-switch (Linux / iptables)
Blocks every outbound connection except to the VPN server, loopback, and
tun*/utun*/ppp* tunnels. Connection replies are allowed via
conntrack ESTABLISHED,RELATED. Optionally allowlist DNS resolvers and
RFC1918 LAN ranges.
One-shot (auto-clears on disconnect):
openconnect-saml connect work --kill-switch \
--ks-allow-dns 1.1.1.1 --ks-allow-dns 9.9.9.9 \
--ks-allow-lan
Standalone (persists until explicitly disabled):
sudo openconnect-saml killswitch enable -s vpn.example.com \
--ks-allow-dns 1.1.1.1
sudo openconnect-saml killswitch status
sudo openconnect-saml killswitch disable
Persistent configuration:
[kill_switch]
enabled = true
allow_lan = false
ipv6 = true
dns_servers = ["1.1.1.1", "9.9.9.9"]
Safety notes
- iptables only (Linux); other platforms return a clear
KillSwitchNotSupportederror. - The chain
OPENCONNECT_SAML_KILLSWITCHis the only thing installed; removal is idempotent viakillswitch disable. - If the CLI crashes and the chain is stuck,
iptables -F OPENCONNECT_SAML_KILLSWITCH && iptables -X OPENCONNECT_SAML_KILLSWITCHwill remove it. - The session-based
--kill-switchflag automatically tears down on disconnect. The persistent form (configured in[kill_switch]or viakillswitch enable) stays up until explicitly disabled.
Profile export / import
Share profiles across machines without secrets:
openconnect-saml profiles export work --file work.json
openconnect-saml profiles export > all-profiles.json
openconnect-saml profiles import work.json
openconnect-saml profiles import work.json --as office --force
cat work.json | openconnect-saml profiles import -
The export strips password, totp, totp_secret, and the 2fauth
token — usernames, server URLs, TOTP source, and split-tunnel routes
are preserved. New companion commands:
openconnect-saml profiles rename old-name new-name
openconnect-saml profiles show work --json # redacted view
NetworkManager (.nmconnection) export
For the Ubuntu / GNOME VPN UI you can export a profile straight into the
network-manager-openconnect plugin format:
# Single profile to a file
openconnect-saml profiles export work \
--format nmconnection -o work.nmconnection
# All profiles into a directory (one .nmconnection each)
openconnect-saml profiles export \
--format nmconnection -o ./nm-export/
# Install system-wide (requires root)
sudo cp work.nmconnection /etc/NetworkManager/system-connections/
sudo chmod 600 /etc/NetworkManager/system-connections/work.nmconnection
sudo nmcli connection reload
The generated file uses
org.freedesktop.NetworkManager.openconnect, sets the gateway, the
auth-group (usergroup=), and a stable UUID derived from the profile
name (re-exporting overwrites the same NM connection rather than
duplicating). Secrets are not written; SAML/SSO authentication still
happens at connect time via the openconnect plugin.
config subcommand
Inspect and validate the configuration file:
openconnect-saml config path
openconnect-saml config show # TOML, secrets redacted
openconnect-saml config show --json
openconnect-saml config validate # schema + semantic checks
openconnect-saml config edit # opens $EDITOR
validate catches: TOML syntax errors, missing server on a profile,
unresolvable active_profile, missing [2fauth]/[bitwarden] for
profiles that reference them, invalid CIDRs in routes/no_routes, and
overly-permissive file modes.
doctor command
One-shot system diagnostics. Exit code: 0 = all OK, 1 = at least one failure, 2 = at least one warning.
openconnect-saml doctor
openconnect-saml doctor -s vpn.example.com # also test DNS + TCP
Checks include: Python version ≥ 3.10, openconnect binary,
sudo/doas, /dev/net/tun, core deps (attrs, keyring, lxml, pyotp,
requests, structlog, toml), optional deps (PyQt6, playwright, fido2,
rich), keyring backend, config dir + permissions, credential env-var
presence, DNS resolution + TCP reachability of a provided server, and
whether the kill-switch is currently active.
Connection history
A lightweight audit log of VPN sessions, written to
$XDG_STATE_HOME/openconnect-saml/history.jsonl (owner-read 0o600,
rotated at 512 KiB). One JSON object per line.
openconnect-saml history show # human-readable, newest first
openconnect-saml history show -n 20 # limit to 20 entries
openconnect-saml history show --json
openconnect-saml history clear
openconnect-saml history path
Events logged: connected, disconnected (with duration),
reconnecting (with attempt number and backoff delay), error.
Privacy / security
- Only metadata: timestamp, event type, server URL, profile name, username, duration, free-text message.
- Never logs passwords, tokens, TOTP codes, or session cookies.
- Enabled by default. Disable per session with
--no-historyor globally withconnection_history = falsein config.
Exit codes (reference)
| Code | Meaning |
|---|---|
| 0 | Success |
| 1 | Generic failure |
| 2 | Platform not supported / browser terminated |
| 3 | Authentication response missing expected fields |
| 4 | HTTP error during authentication |
| 17 | No AnyConnect profile found |
| 18 | No AnyConnect profile selected |
| 19 | Invalid arguments |
| 20 | No superuser tool / not running as Administrator |
| 21 | 2FAuth TOTP config missing |
| 22 | Bitwarden TOTP config missing |
| 23 | 1Password TOTP config missing |
| 24 | pass TOTP config missing |
| 130 | Interrupted (Ctrl-C) |
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file openconnect_saml-0.8.3.tar.gz.
File metadata
- Download URL: openconnect_saml-0.8.3.tar.gz
- Upload date:
- Size: 126.3 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
4d74e01e8b97f35d76e2a298c3b8a7060119ea3cc0e4cf31a4af3ed5c21df1a1
|
|
| MD5 |
82fb9a2280ad6276935b75cc8c0da132
|
|
| BLAKE2b-256 |
84e88a5497fd63b8b3b38b3812dc23bb51019ce55560c9d9b6c0a6069b9c3463
|
Provenance
The following attestation bundles were made for openconnect_saml-0.8.3.tar.gz:
Publisher:
publish.yml on mschabhuettl/openconnect-saml
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
openconnect_saml-0.8.3.tar.gz -
Subject digest:
4d74e01e8b97f35d76e2a298c3b8a7060119ea3cc0e4cf31a4af3ed5c21df1a1 - Sigstore transparency entry: 1405504219
- Sigstore integration time:
-
Permalink:
mschabhuettl/openconnect-saml@d8b48a7d541492521604f52997ccd652ec568c43 -
Branch / Tag:
refs/tags/v0.8.3 - Owner: https://github.com/mschabhuettl
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@d8b48a7d541492521604f52997ccd652ec568c43 -
Trigger Event:
push
-
Statement type:
File details
Details for the file openconnect_saml-0.8.3-py3-none-any.whl.
File metadata
- Download URL: openconnect_saml-0.8.3-py3-none-any.whl
- Upload date:
- Size: 98.1 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
1fdd9238d782632957a6f8e32da278e7b01290e52fb732de40ca5f95670cb4a9
|
|
| MD5 |
5e0b71be36cb1336eda02ef4bd59557d
|
|
| BLAKE2b-256 |
9988558850930137585fc02e65b20aade77dfba820edd7794cbfd0cfd4962d36
|
Provenance
The following attestation bundles were made for openconnect_saml-0.8.3-py3-none-any.whl:
Publisher:
publish.yml on mschabhuettl/openconnect-saml
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
openconnect_saml-0.8.3-py3-none-any.whl -
Subject digest:
1fdd9238d782632957a6f8e32da278e7b01290e52fb732de40ca5f95670cb4a9 - Sigstore transparency entry: 1405504253
- Sigstore integration time:
-
Permalink:
mschabhuettl/openconnect-saml@d8b48a7d541492521604f52997ccd652ec568c43 -
Branch / Tag:
refs/tags/v0.8.3 - Owner: https://github.com/mschabhuettl
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@d8b48a7d541492521604f52997ccd652ec568c43 -
Trigger Event:
push
-
Statement type: