OpenConnect ties two separate exchanges together with one short-lived value: the web login and the VPN tunnel. The first exchange is ordinary HTTPS: credentials, a SAML round trip, or a token go to the gateway, and the reply carries an authentication cookie. The second exchange presents that cookie to the same gateway as proof that the web phase already happened, and only then does the tunnel come up.

Nothing about the tunnel is negotiated during the login. That is the part worth understanding, because most scripting mistakes assume the cookie is a password to be stored, or a key that protects traffic, when it is really a bearer ticket for a specific gateway session.

What the client and gateway exchange, in order

The authentication phase looks like a normal protected website. OpenConnect sends the user's credentials, or follows the gateway's redirect to an identity provider, and the gateway responds with a document that embeds the auth cookie. In the AnyConnect protocol that value travels in a cookie header on the following requests, usually under a name such as webvpn, and the manual treats it as the credential that carries the session forward.

The second phase is a CONNECT request to the gateway's VPN endpoint with that cookie attached. The gateway checks it, assigns an address, and the client starts CSTP over TLS, with DTLS as an optional faster data path once the session exists. Only at this point are tunnel parameters, routes, DNS settings and the session's traffic keys established. The cookie got the client through the door; it did not encrypt anything.

That sequencing explains why openconnect --authenticate exists. Run it, and it performs the web phase, prints a small block of shell variables such as COOKIE, HOST, CONNECT_URL and FINGERPRINT, then exits before any tunnel is built. A wrapper script can then start the real connection with those values, which is exactly the split a headless or two-container deployment needs.

The cookie is a session bearer, not a key

Treat the value the way you would treat a session ID in a web application. Whoever holds it during its lifetime can ask the gateway for the tunnel that session is entitled to, so its secrecy matters. But copying it does not reveal traffic, because the tunnel's confidentiality comes from the TLS handshake and the derived keys, not from the cookie string.

It is also not a permanent grant. The gateway and the identity provider decide how long the session lives, and a browser that still shows a signed-in page says nothing about whether the tunnel session is still valid. Reconnecting after expiry means repeating the web phase and producing a fresh cookie, which is the point of reauthentication rather than a nuisance to engineer around.

Do not treat a cookie as a universal substitute for other credentials. A SAML assertion, a browser password, and a cookie issued by a different service are different objects with different audiences, and pasting one into the other's field usually fails at the gateway or, worse, succeeds against the wrong trust boundary.

Passing the cookie without leaving copies

The safest mechanical handoff on Linux is standard input. --cookie-on-stdin reads the value from stdin, so the cookie never appears in the process argument list, where any local user can read it with ps. A pipeline that authenticates in one process and feeds the tunnel in another keeps the value in a pipe rather than in a file or an environment variable.

Arguments and environment variables are weaker for different reasons. An argument is visible in the process table for the lifetime of the client and often lands in shell history. An environment variable is inherited by child processes, and a systemd unit that sets it in the unit file stores the secret in a readable configuration. If you must use a file, restrict it to the service account and remove it when the session ends.

Keep the privilege boundary deliberate as well. The process that talks to the identity provider, and possibly opens a browser, should run as the user who owns that login. The process that configures routes, DNS and the tunnel device may need more privilege. Running the whole browser as root to make its cookie available to a root-owned tunnel is the wrong trade, and modern OpenConnect setups can hand the value across that boundary explicitly.

Logging deserves the same suspicion. Verbose client output, helper scripts and gateway diagnostics can all echo request material, and not every logging level redacts the cookie. Capture the smallest error record you need, keep it out of shared channels, and check a redacted sample before attaching it to a ticket.

Verification and expiry in the second phase

The second phase still has to verify the server. The FINGERPRINT that --authenticate prints is a certificate pin in pin-sha256: form, and passing it to the tunnel invocation with --servercert keeps the check anchored to the gateway you authenticated against. Turning certificate checking off to make a script simpler removes the only proof that the cookie is being handed to the intended host.

When the gateway rejects the cookie, the fix is a new authentication round, not a longer cache. Store the cookie only for the lifetime of the connection, delete temporary files on exit, and use the identity provider's or gateway's session-removal process when a session is exposed or a device is lost. A cached cookie is not an exemption from MFA.

Before automating this, verify four things: the tunnel runs as the right account, the cookie arrives on stdin, the certificate fingerprint is pinned, and the session is discarded when the tunnel closes. If any of those is missing, the automation has widened the trust in the cookie rather than reproduced the manual flow.

Winndannde jowitiiɗe heen