The belief is that a here-document with four lines in it is a complete non-interactive login, and it holds right up until the gateway offers two groups, prints a banner, or asks for a token after the password. Then the same script types the password into a menu that expected a number. The command-line client on Linux is scriptable, but only if you feed it what it is actually waiting for rather than what the tutorial assumed.

The fixed here-document is the myth

Cisco's Linux client installs a small program at /opt/cisco/anyconnect/bin/vpn, and Secure Client 5 still ships it. With the vpnagentd service running, the -s flag reads answers from standard input in the order the client asks for them. That order is not fixed across servers.

A group menu adds one line. A login banner adds another. A token prompt after the password breaks a four-line script outright, because the fifth line was never going to be the token. The failure is quiet: the client receives the wrong string, the connection attempt ends, and the script reports nothing useful.

Run the client by hand once against the same gateway and version, and write down the prompts in the order they appear. That transcript is the specification your script has to satisfy.

What the client is waiting for

In the common single-group case the sequence is username, password, and sometimes a banner confirmation. The first line of a here-document only becomes a menu index when the server actually presents a menu, which is why the same script works at one site and fails at the next.

/opt/cisco/anyconnect/bin/vpn -s connect vpn.example.net <<EOF
0
alice
$VPN_PASSWORD
y
EOF

Read that as four answers to four possible questions, not as a fixed protocol. The zero is a menu selection, not a password, and the trailing y accepts a banner that many gateways never send.

Use expect when the order is not predictable

When the prompt order varies, stop counting lines and wait for the prompt instead. An expect script spawns the connect command, waits for the username prompt, sends, waits for the password prompt, sends, and then waits for the connected state. Each wait has its own timeout.

Treat a timeout as a failure and exit. A script that keeps sending input after an unexpected prompt will happily type the password into a shell, a menu, or whatever else happens to be reading. That is how a login script becomes an incident.

Expect also gives you a transcript you can log with the secret removed, which is worth more than the exit code when a gateway changes its prompts after an upgrade.

The secret has to live somewhere

Every version of this problem shares one weakness: the password must exist in a form the client can read. Passing it as an argument exposes it in the process table. Echoing it through a pipeline often puts it in shell history. Both are readable by other local users on a shared jump host.

install -m 600 /dev/null ~/.vpn-secret
umask 077; VPN_PASSWORD=$(cat ~/.vpn-secret) /opt/cisco/anyconnect/bin/vpn -s connect vpn.example.net

A file with mode 600 owned by the account is the minimum, and it is still a plaintext secret on disk. secret-tool lookup reads from the Linux keyring, and on macOS the same job is done by security find-generic-password -w. Where the organization supports certificate or SAML authentication, prefer it, because a stored password plus MFA still cannot complete a login without a human.

Confirm the tunnel, then own the teardown

Do not trust the absence of error text. Ask the client for its state and check the interface independently:

/opt/cisco/anyconnect/bin/vpn state
ip -brief address

The state should read Connected, and a tunnel interface should hold an address. Then request one resource the tunnel is supposed to reach, because a connected state with no route is a real outcome and an easy one to miss.

Disconnect with vpn disconnect, and let the script that started the tunnel's own the teardown. A failed job that leaves a session open consumes a licence seat and looks like an authentication problem to the next person who tries.

Order of work: confirm vpnagentd, capture the real prompt order, choose here-document or expect, keep the secret out of argv and history, verify state and one resource, then disconnect in the same script.

Pennadoù liammet