Secure, scriptable command-line VPN client for Cisco AnyConnect and other SSL VPNs, built on OpenConnect — for macOS & Linux.
╔═════════════════════════════════════════════════════════════════╗
║ ║
║ ██╗ ██╗ ██████╗ ███╗ ██╗ ██╗ ██╗ ██████╗ ║
║ ██║ ██║ ██╔══██╗ ████╗ ██║ ██║ ██║ ██╔══██╗ ║
║ ██║ ██║ ██████╔╝ ██╔██╗ ██║ ███████ ██║ ██║ ██████╔╝ ║
║ ╚██╗ ██╔╝ ██╔═══╝ ██║╚██╗██║ ██║ ██║ ██╔═══╝ ║
║ ╚████╔╝ ██║ ██║ ╚████║ ╚██████╔╝ ██║ ║
║ ╚═══╝ ╚═╝ ╚═╝ ╚═══╝ ╚═════╝ ╚═╝ ║
║ ║
║ F O R O P E N C O N N E C T ║
╚═════════════════════════════════════════════════════════════════╝
$ vpn-up start "Frankfurt VPN"
Starting the Frankfurt VPN on frankfurt.simplica.com using Cisco AnyConnect ...
Connecting with Two-Factor Authentication (2FA) from Duo (PUSH) ...
Connected to Frankfurt VPN
$ vpn-up status
VPN is running (PID: 88933)
Profile : Frankfurt VPN
Gateway : frankfurt.simplica.com
Since : 2026-06-12 22:49:14
Uptime : 08:47VPN Up is a secure, scriptable command-line VPN manager for OpenConnect. It connects macOS and Linux machines to Cisco AnyConnect, Palo Alto GlobalProtect, Pulse Secure, Juniper Network Connect, and ocserv gateways straight from the terminal — with named profiles, simultaneous tunnels, Duo 2FA, certificate pinning, secure secret storage, auto-reconnect, and shell completion.
It's built for developers, consultants, DevOps engineers, and remote workers who'd rather drive their VPN from the command line than a vendor GUI.
- Connect to Cisco AnyConnect-compatible VPNs without the official GUI client
- Manage multiple OpenConnect VPN profiles, connect by name, and run compatible profiles side by side (
vpn-up start "Work VPN") - Use Duo 2FA from the command line — push, phone, SMS, or one-time passcode
- Store VPN secrets in the macOS Keychain, Linux Secret Service, or an encrypted OpenSSL vault — never in plaintext config files
- Run VPN connections as launchd/systemd login services with auto-reconnect
- Script VPN startup into your development, automation, and remote-support workflows
VPN Up is useful if you:
- Use OpenConnect instead of vendor VPN clients
- Connect to Cisco AnyConnect, GlobalProtect, Pulse Secure, Juniper, or ocserv VPNs
- Want a terminal-first VPN workflow on macOS or Linux
- Juggle multiple client VPN profiles on one machine, including simultaneous tunnels when their routes can coexist
- Need Duo 2FA support from the command line
- Want secure password storage without plaintext config files
- Want VPN auto-reconnect at login via launchd or systemd
- Why use VPN Up?
- Who is this for?
- Key Features
- VPN Up vs. raw OpenConnect
- Common use cases
- Prerequisites & Installation
- Quick Start
- Usage
- Configuration
- Roadmap
- Related articles
- Contributing & License
- Four SSL VPN protocols via OpenConnect: Cisco AnyConnect (and ocserv), Juniper Network Connect, Palo Alto GlobalProtect, and Pulse Secure
- Multiple VPN profiles and tunnels — pick from an interactive menu or connect by name with zero prompts (
vpn-up start "Work VPN"), with full AuthGroup/realm support; compatible profiles can stay connected at the same time - Duo 2FA —
push,phone,sms, or one-time passcodes prompted at connect time; empty method lets the gateway auto-push - TOTP authenticator-app 2FA — store the seed once and
vpn-upgenerates the code at connect time (viaoathtool); fully non-interactive, so it works as an auto-reconnecting login service - SSO / external-browser login — for gateways that force a browser-based SAML/SSO flow (Okta, Azure AD, Ping Identity, often with an embedded Duo iframe), via OpenConnect's
--external-browser(needs openconnect ≥ 9.0). Because the login runs in your real browser, passkeys / FIDO2 / YubiKey-WebAuthn work automatically - Client-certificate authentication — an X.509 cert/key file or a PKCS#11 smartcard / YubiKey PIV, on its own or alongside a password/SSO; the key passphrase or PIN is kept out of the process table
- Background or foreground execution, per your config
- No plaintext passwords — secrets live in the macOS Keychain, Linux Secret Service, or an AES-256-CBC + PBKDF2 OpenSSL vault; legacy plaintext is migrated and scrubbed automatically, and secrets never appear on command lines or in child-process environments
- Fail-closed server identity —
pin-sha256certificate pinning with a one-command pin fetch, or strict system trust-store validation - The sudo password is never stored — interactive prompt by default, or a sudoers rule scoped to the one
openconnectbinary - Isolated user data — config, profiles, secrets, and logs live in
~/.config/vpn-upwith600/700permissions, untouched by updates or reinstalls
- Profile-aware lifecycle —
statusshows every running tunnel with profile, gateway, and uptime;stopandlogs -ftarget a specific connection - Login service with auto-reconnect — launchd (macOS) / systemd (Linux) supervision; reconnects when the tunnel drops
- Lifecycle hooks — run your own scripts on connect/disconnect (mount shares, switch proxies)
- Desktop notifications on connect/disconnect (Notification Center /
notify-send)
- Guided setup — a first-run wizard plus
add-profile/remove-profile, which handle XML, secrets, pins, and services in one step doctordiagnostics — environment, dependencies, secret backend, and config at a glance- Bash/zsh tab completion for commands and profile names
- Hardened & tested — 100+ tests on macOS + Ubuntu in CI, shellcheck-clean, secret scanning, modern Bash (≥ 4), no
eval
VPN Up is not a replacement for OpenConnect — it's a safer, more convenient wrapper around it for people who want profile management, secure secrets, 2FA prompts, certificate pinning, logs, completion, and service management instead of long ad-hoc command lines.
| Feature | Raw openconnect |
VPN Up |
|---|---|---|
| Multiple named profiles | manual | ✅ |
| Secure secret storage (Keychain/keyring/vault) | — | ✅ |
| Duo 2FA prompts (push/phone/sms/passcode) | manual | ✅ |
| Certificate-pinning helper | manual | ✅ |
| Auto-reconnect login service (launchd/systemd) | manual | ✅ |
Profile-aware status / logs / stop |
— | ✅ |
| Shell completion | — | ✅ |
| Connect/disconnect hooks & notifications | — | ✅ |
It works as a command-line alternative to vendor clients such as the Cisco AnyConnect Secure Mobility Client, GlobalProtect, Pulse Secure, or Juniper Network Connect whenever your gateway speaks an OpenConnect-compatible protocol.
- Connect to a Cisco AnyConnect VPN from the macOS or Linux terminal
- Use OpenConnect with Duo Push authentication
- Manage multiple client VPN profiles on one machine
- Start a VPN automatically at login on macOS (launchd) or Linux (systemd)
- Run OpenConnect with certificate pinning for a known gateway
- Store VPN credentials securely outside plaintext files
- Script VPN startup before running developer tools or remote-support tasks
brew tap sorinipate/vpn-up
brew install vpn-upInstalls the vpn-up command with all dependencies (modern Bash, openconnect, xmlstarlet) and bash completion. Your data stays in ~/.config/vpn-up either way.
macOS ships with Bash 3.2. Install modern Bash and the dependencies:
brew install bash openconnect xmlstarletEnsure Homebrew Bash is first in your PATH:
echo 'export PATH="/opt/homebrew/bin:$PATH"' >> ~/.zprofile
exec $SHELL -lThen clone and run:
git clone https://github.com/sorinipate/vpn-up-for-openconnect.git
cd vpn-up-for-openconnect
chmod +x vpn-up.command# Debian / Ubuntu
sudo apt install bash openconnect xmlstarlet openssl
# RHEL / CentOS
sudo yum install bash openconnect xmlstarlet openssl
# Fedora
sudo dnf install bash openconnect xmlstarlet opensslOptional (recommended): secret-tool for Secret Service keyring storage.
vpn-up setup # one-time configuration wizard
vpn-up add-profile # guided profile creation (+ password, + certificate pin)
vpn-up start # connect (interactive menu)Manual installs: use
./vpn-up.commandinstead ofvpn-upthroughout.
vpn-up start # interactive profile menu
vpn-up start "Frankfurt VPN" # connect directly (scriptable)
vpn-up list # list configured profiles
vpn-up add-profile # guided profile creation (+ secret, + pin)
vpn-up remove-profile "Old VPN" # remove profile + secret + logs + service
vpn-up status # all running profiles, gateways, uptime
vpn-up logs -f # follow the connection log
vpn-up stop # stop all VPNs (or: stop "Frankfurt VPN")
vpn-up doctor # environment, secret backend, privilege boundary
vpn-up install-helper # install the root-owned privileged helperEach profile keeps its own log and PID/state files under ~/.config/vpn-up, so status, stop, and logs are profile-aware. You can connect multiple different profiles at once; starting the same profile twice is refused. Whether two tunnels coexist cleanly still depends on the routes and DNS settings pushed by their gateways — split-tunnel or non-overlapping routes are the safest fit.
For gateways that prompt for a time-based one-time code (Google Authenticator, Authy, a hardware TOTP token), store the base32 seed once and vpn-up generates the current code at connect time:
vpn-up set-secret "Work VPN" token_secret # paste the base32 seed…then set <tokenMode>totp</tokenMode> on the profile (the add-profile wizard offers this as a 2FA choice). Requires oathtool (brew install oath-toolkit / apt install oathtool; shown by vpn-up doctor).
- The seed stays in your keychain — it's never passed to openconnect's argv or written to disk; only the short-lived code is sent (on stdin), the same path as a Duo passcode.
- By default the code is a 6-digit SHA1 code on a 30-second step, matching every common authenticator app. For a gateway that needs something else, the wizard's advanced TOTP options (or the
<totpAlgorithm>/<totpDigits>/<totpStepSeconds>tags directly) support SHA1/SHA256/SHA512, a custom digit count, and a custom time step — everythingoathtool --totpitself supports. - Because no interaction is needed, a TOTP profile is the one 2FA method that can run as a login service with auto-reconnect (Duo passcode and SSO can't).
- Security note: storing the TOTP seed beside the password in the same keychain is effectively "1.5-factor" — it's opt-in.
Some gateways don't accept a username/password on the command line at all — they force a real browser window for a SAML/SSO login (Okta, Azure AD, Ping Identity), often wrapping an embedded Duo iframe. vpn-up supports this via OpenConnect's external-browser mode.
Mark a profile as SSO — either answer yes to the "Use SSO / browser-based login?" prompt in vpn-up add-profile, or set <authMode>sso</authMode> in the profile XML:
<VPN>
<name>Work SSO</name>
<protocol>anyconnect</protocol> <!-- anyconnect or gp; not nc -->
<host>vpn.example.com</host>
<authMode>sso</authMode>
</VPN>Then connect normally:
vpn-up start "Work SSO"OpenConnect opens your browser to the identity provider; you complete the login (including Duo) there, and the tunnel comes up. Notes:
-
Requires openconnect ≥ 9.0 (when
--external-browserlanded).vpn-up doctorreports your version and whether SSO is available. -
Runs in the foreground —
BACKGROUNDis ignored, and an SSO profile cannot run as a login service (service installrefuses it) because it needs an interactive desktop session. No password is stored or piped for SSO profiles. -
Linux + sudo caveat: openconnect runs as root, so a root-spawned browser may not reach your desktop session. If the browser doesn't appear, point
vpn-upat a session-aware opener:# ~/bin/vpn-up-browser (chmod +x) #!/bin/sh exec sudo -u "$SUDO_USER" xdg-open "$@"
export VPN_UP_EXTERNAL_BROWSER="$HOME/bin/vpn-up-browser"
VPN_UP_EXTERNAL_BROWSERoverrides the opener command (default:openon macOS,xdg-openon Linux, or the bundledopenconnect-external-browserhelper if installed).
vpn-up service install "Frankfurt VPN" # connect at login, reconnect on drop
vpn-up service status
vpn-up service uninstall "Frankfurt VPN"Uses a launchd user agent on macOS and a systemd user unit on Linux; the service manager supervises openconnect in the foreground and relaunches it if the tunnel drops (30s throttle). Requirements:
- the passwordless sudoers rule — there is no TTY to type a sudo password into
- the profile's password stored in the secrets backend
- a non-interactive 2FA method (
push,phone,sms— notpasscode)
⚠️ The login service is not recommended right now. It cannot work without the passwordless sudoers rule, and that rule currently grants effective root to your account. Until the privileged helper lands (see SECURITY.md), prefer starting the VPN manually and typing your sudo password. If you do run the service, do it only on a single-user machine you trust.
Drop executable scripts into ~/.config/vpn-up/hooks/connected.d/ or ~/.config/vpn-up/hooks/disconnected.d/ and they run (in name order) when a tunnel comes up or goes down. Hooks receive VPN_EVENT, VPN_NAME, and VPN_HOST in their environment (never the password). Because hooks are executable code, a hook is skipped unless it is owned by you and not group/world-writable — same rule as the config file. Hook failures are reported but never block the VPN.
mkdir -p ~/.config/vpn-up/hooks/connected.d
cat > ~/.config/vpn-up/hooks/connected.d/10-hello <<'EOF'
#!/bin/sh
echo "$(date): $VPN_NAME up ($VPN_HOST)" >> "$HOME/vpn-history.log"
EOF
chmod 700 ~/.config/vpn-up/hooks/connected.d/10-helloHomebrew installs completion automatically. For manual installs:
# bash (~/.bashrc)
source /path/to/vpn-up-for-openconnect/completions/vpn-up.bash
# zsh (~/.zshrc)
autoload -U +X bashcompinit && bashcompinit
source /path/to/vpn-up-for-openconnect/completions/vpn-up.bashCompletes commands and profile names (spaces handled).
vpn-up set-secret "Frankfurt VPN" password
vpn-up delete-secret "Frankfurt VPN" passwordPasswords are stored in the OS keychain/keyring (or an encrypted vault as fallback) — never in files, never in the process table, never exported to child processes.
openconnect needs root. By default you get the normal sudo prompt, which avoids the ambient passwordless-root problem below and is the safer default. Never store your sudo password anywhere.
Prompt mode is a compatibility mode, not a hardened boundary: a user-writable openconnect is a poor sudo target even with a password prompt, because a process running as you can replace the binary and wait for your next legitimate sudo openconnect. Closing that requires a root-owned installation — see SECURITY.md.
Earlier versions of this README described the rule below as safe because it is "scoped to the one binary." That was wrong, and is retracted. sudoers matches the command, not its arguments: when a rule names a command with no arguments, the user may run it with whatever arguments they choose. Several
openconnectflags execute another program as root —--script,--script-tun,--csd-wrapper, and--config/--xmlconfig, which can name those from a file. So the rule below is equivalent to passwordless root for your account, usable by anything running as your user, whether or not it goes throughvpn-up.Filtering inside
vpn-upcannot fix this — the rule permitsopenconnectdirectly, bypassingvpn-upentirely. The boundary has to move into sudoers, and it now can:
vpn-up install-helper # root-owned helper; connects ask for your password
vpn-up install-helper --passwordless # also authorize unattended connects
vpn-up doctor # what this machine's boundary actually looks likeThis installs a root-owned vpn-up-helper, which builds the openconnect command line itself from a closed set of validated options, and a separate vpn-up-admin that approves endpoints and is never passwordless. Sudoers then names only the helper — a binary that is safe for any arguments — instead of openconnect, which is not.
It also retires the legacy rule below on every run, with or without --passwordless: leaving the old arbitrary-root grant in place next to a hardened boundary is worse than either alone, because it looks fixed.
Requirements and limits, honestly:
| Needs | a C toolchain — Xcode command line tools on macOS, cc on Linux (the helper is compiled from this checkout, as you, and installed as root) |
| Linux | works with a distro-packaged openconnect |
| macOS | works with a MacPorts openconnect (root-owned /opt/local); verified directly against a real install, dylib closure included — see design section 17.1 |
| Homebrew | refused on every platform — the prefix is owned by the installing user (see the caveat below) |
| Assumes | that no process running as your user tampers with the build while you install; read "What installation itself assumes" before using --passwordless |
Without --passwordless you still get the closed schema, the approved-endpoint binding and the closure check — a password per connect is the only cost, and a login service is the only thing that needs more.
Kept documented because machines still carry it and because install-helper is not available everywhere yet. It is equivalent to passwordless root for your account — a deliberate trade-off, appropriate on a single-user machine you trust, not on a shared or managed one. vpn-up doctor reports it, and install-helper removes it.
# Verify the real path first — it varies by platform and by Homebrew version:
command -v openconnect
# macOS (Homebrew, Apple Silicon — usually /opt/homebrew/bin/openconnect):
echo "$USER ALL=(root) NOPASSWD: /opt/homebrew/bin/openconnect" | sudo tee /etc/sudoers.d/vpn-up
# Linux:
echo "$USER ALL=(root) NOPASSWD: /usr/sbin/openconnect" | sudo tee /etc/sudoers.d/vpn-up
sudo chmod 440 /etc/sudoers.d/vpn-up
sudo visudo -cf /etc/sudoers.d/vpn-up # validateHomebrew prefix caveat (macOS). Permitting a binary through sudoers only means anything if neither the binary nor the directories above it can be replaced by the unprivileged user. Homebrew's prefix —
/opt/homebrewon Apple Silicon,/usr/localon Intel — is owned by the installing user, so a rule pointing into it can be defeated by swapping the binary, no arguments needed.Worse,
openconnect's compiled-in defaultvpnc-scriptis inside that prefix ($HOMEBREW_PREFIX/etc/vpnc/vpnc-script) and is executed as root on every connect. Runopenconnect --helpand look under "VPN configuration script" for the default it prints, thenls -lthat path. If you own it,sudo openconnectruns code you can modify as root, with no special arguments at all.For a passwordless rule on macOS, point it at a root-owned
openconnectwhose default script is also root-owned — or keep thesudoprompt.
Do not add broad commands like
killto this rule. Stopping the VPN still asks for your sudo password; that's intentional.
vpn-up pin vpn.example.com # print the pin
vpn-up pin --save "Frankfurt VPN" # write it into the profilePrints the gateway's pin-sha256:... value for <serverCertificate>, or saves it into the profile directly with --save. If no pin is configured, the gateway's certificate must validate against the system trust store or the connection is refused (fail closed). Legacy SHA1 pins still work but print a deprecation warning. Verify any pin out-of-band with your VPN administrator before trusting it.
vpn-up doctorNeed an openconnect flag vpn-up doesn't model (--no-dtls, --os=win, --csd-wrapper, an HTTP/SOCKS proxy, MTU, --reconnect-timeout, …)? Set <extraArgs> on the profile (or answer the optional "Extra openconnect arguments" prompt in add-profile); the value is appended verbatim to the openconnect command, just before the gateway host:
<extraArgs>--no-dtls --reconnect-timeout 30</extraArgs>- Tokenized with
xargs, so quotes are respected ("--csd-wrapper=/path with space"stays one argument) — and nevereval'd. - Avoid flags
vpn-upalready manages —--protocol,--user,--passwd-on-stdin,--background,--servercert,--authgroup,--pid-file,--external-browser,--token-mode/--token-secret. Duplicating one prints a warning (it may conflict) but is still passed. ⚠️ Some flags execute a program as root. openconnect runs undersudo, and--script/-s,--script-tun/-S,--csd-wrapper,--csd-user,--config,--xmlconfig/-x, and--external-browserall give it something to run — as root. (--csd-user=rootruns the gateway-supplied CSD binary as root;--external-browsernames the SSO opener openconnect launches.)vpn-upprints a loud warning and still passes them, but combined with a passwordless sudoers rule they form a root-execution path. Point them only at a program on a path that only root can write.
Split-tunnel routing: to send only some subnets through the VPN (and the rest direct), set <extraArgs>--script "vpn-slice 10.0.0.0/8 host.corp"</extraArgs> — vpn-slice is a drop-in vpnc-script replacement. Full recipe (include/exclude syntax, hooks alternative, caveats): Split-tunnel routing.
Authenticate with an X.509 client certificate — a file or a PKCS#11 smartcard / YubiKey PIV — on its own (cert-only) or alongside a password/SSO. Set <clientCertificate> (and <clientKey> if the key is separate); each is a file path or a pkcs11: URI:
<clientCertificate>/etc/vpn/me.pem</clientCertificate>
<!-- or a smartcard / YubiKey PIV: -->
<clientCertificate>pkcs11:manufacturer=piv_II;id=%01;type=cert</clientCertificate>- The cert path/URI is not a secret (it stays in the XML); a key passphrase or PKCS#11 PIN is — it never goes on the command line. An encrypted key/token prompts interactively, or store a PKCS#11 PIN for unattended use:
vpn-up set-secret "Work" key_password(fed via a transientpin-sourcefile, never argv). - For a login service, use a PKCS#11 token with a stored PIN, or an unencrypted
0600key file (a passphrase-protected file key can't run unattended — there's no TTY to prompt on). vpn-up doctorreports PKCS#11 (p11-kit) availability. Full guide: docs/client-certificate-auth.
Reach the gateway through a proxy by setting <proxy> on the profile (or answer the optional proxy prompt in add-profile). It's passed to openconnect's --proxy:
<proxy>http://proxy.corp:8080</proxy>
<!-- or SOCKS: -->
<proxy>socks5://127.0.0.1:1080</proxy>- The proxy URL is an identifier, not a secret — it lives in the profile XML.
⚠️ Avoid embeddinguser:pass@in the URL: like any argument, it would be visible in the process table. Prefer a proxy that doesn't require inline credentials.
All user state (config, profiles, secrets vault, logs, PID files) lives in ~/.config/vpn-up (override with VPN_UP_HOME or XDG_CONFIG_HOME), so updating or deleting the program directory never touches your data. Legacy files from the old in-repo config/ directory are migrated automatically on first run.
readonly QUIET=TRUE # openconnect output verbosity
readonly BACKGROUND=TRUE
readonly SHOW_BANNER=TRUE # ASCII banner on start (independent of QUIET)
readonly NOTIFICATIONS=TRUE # desktop notification on connect/disconnect
readonly ENCRYPTION_ENABLED=TRUE
⚠️ ENCRYPTION_KEYis intentionally ignored. OS keychain/keyring is always preferred. The OpenSSL vault prompts for a passphrase when needed.
Seeded from config/vpn-up.command.profiles.default on first run, or created via vpn-up add-profile:
<VPNs>
<VPN>
<name>Frankfurt VPN</name>
<protocol>anyconnect</protocol>
<host>frankfurt.example.com</host>
<authGroup>SimplicaEmployees</authGroup>
<user>your.username</user>
<password></password>
<duo2FAMethod>push</duo2FAMethod>
<serverCertificate>pin-sha256:BASE64_HASH</serverCertificate>
<authMode>password</authMode>
<tokenMode>totp</tokenMode>
<extraArgs>--no-dtls</extraArgs>
<clientCertificate>/etc/vpn/me.pem</clientCertificate>
<clientKey></clientKey>
<proxy>http://proxy.corp:8080</proxy>
<totpAlgorithm>SHA1</totpAlgorithm>
<totpDigits>6</totpDigits>
<totpStepSeconds>30</totpStepSeconds>
</VPN>
</VPNs>Supported tag aliases: username/user, group/authGroup, duoMethod/duo2FAMethod. The <password> field is deprecated: plaintext values are migrated to the secrets backend and blanked in the XML automatically on first use — prefer vpn-up set-secret. A duo2FAMethod of passcode prompts for the one-time code at connect time. <authMode> is password (default) or sso for browser-based SAML/SSO login. <tokenMode> is empty (default) or totp for authenticator-app 2FA. <extraArgs> passes extra openconnect flags verbatim — see Advanced: extra openconnect arguments. <clientCertificate>/<clientKey> enable client-certificate authentication — a file path or a PKCS#11 URI; a key passphrase/PIN goes in the secrets backend (key_password), never the XML. <proxy> routes the connection through an HTTP/SOCKS proxy. <totpAlgorithm>/<totpDigits>/<totpStepSeconds> are all optional and only apply to tokenMode=totp; left empty (or absent), they default to SHA1/6/30 — oathtool's own implicit defaults.
Under consideration (open an issue if you need one of these):
- Yubikey OATH (HOTP) token support (TOTP authenticator codes are already supported — see TOTP authenticator-app 2FA — as is a Yubikey PIV client certificate via client-certificate authentication)
Explicitly out of scope: Windows support, GUI, and RSA SecurID — importing a SecurID token requires passing the token secret to openconnect on the command line (--token-secret), which would break VPN Up's rule of never putting secrets in argv (the same reason TOTP is fed on stdin rather than via --token-secret).
Known behavior: some AnyConnect gateways emit an initial Unexpected 404 result from server — this is benign if the connection proceeds successfully. Simultaneous full-tunnel VPNs can also fight over the default route or DNS. VPN Up tracks and controls each OpenConnect process independently, but OpenConnect and the VPN gateways still own route/DNS installation.
Background and design notes:
- A Safer OpenConnect Workflow for Cisco AnyConnect VPNs on macOS and Linux — Architegrity
- A Safer OpenConnect Workflow for Cisco AnyConnect VPNs on macOS and Linux — Dev.to
- A Safer OpenConnect Workflow for Cisco AnyConnect VPNs on macOS and Linux — Medium
Contributions are welcome — see CONTRIBUTING.md for the workflow (PRs only, shellcheck + test suite must pass on macOS and Ubuntu).
- 📖 Documentation site: sorinipate.github.io/vpn-up-for-openconnect
- 📐 Design docs: PRD.md (what & why), ARCHITECTURE.md (how), and PRIVILEGED-HELPER-DESIGN.md (draft: the planned root trust boundary)
- 🔐 Security issues: please report privately via SECURITY.md, not public issues
- 📄 Full release history: CHANGELOG.md
- ⚖️ Licensed under the MIT License — © Sorin-Doru Ipate
- ☕ If it saves you time, you can buy me a coffee
Thanks to all contributors and users who helped test and refine this project.