A script that starts a vendor VPN client hangs until it times out, and the log shows the client asking for a verification code that nobody typed. The password was supplied, so the obvious conclusion is that the password is wrong. It is not. The code prompt is a separate conversation on a separate channel, and your script never opened it.

Work through the five checks below in order. Stop at the first one that fails, because every later step assumes the earlier ones hold.

Check 1: does the client read standard input at all?

Test before you write any automation: pipe a dummy secret into the client and watch what happens. If it prints its prompt and then waits, it is not reading the pipe.

Many clients open the terminal device directly, or refuse to take a secret from a non-terminal stream, so that a stray pipe cannot leak a password. OpenVPN is the documented exception, because --auth-user-pass takes a file path. Vendor clients that ask for a one-time code usually have no equivalent flag, and their prompt is waiting on a terminal your script never provided.

Check 2: does the child have a terminal?

A pty allocates a terminal for the child, which turns prompts into readable output you can answer in order.

import pexpect

child = pexpect.spawn("openconnect vpn.example.net", encoding="utf-8", timeout=30)
child.expect("GROUP:")
child.sendline("staff")
child.expect("USERNAME:")
child.sendline("alice")
child.expect("PASSWORD:")
child.sendline(PASSWORD)
child.expect("Connected as", timeout=60)

The prompt strings above are illustrative; copy the exact text your client prints, including case and punctuation. Every expectation carries a timeout, and a closed child raises an exception instead of returning a status, so wrap the sequence and treat end-of-file as a failure. Note what this block does not do: the child dies with the script, because a pty is not a detach mechanism and disown is a shell builtin that cannot reach a process your script spawned.

Check 3: is there a supported non-interactive input?

Screen-scraping breaks on client upgrades, so look for a documented flag first.

Check the help output for a token or one-time-code option, and check whether the service accepts the code appended to the password field. Where the client reads a credentials file, hand it the file instead of a prompt:

openvpn --config office.ovpn --auth-user-pass <(printf "%s\n%s\n" "$USER_NAME" "$PASSWORD")

Process substitution gives the client a readable path on systems that provide /dev/fd. The secret is still a secret from the client's point of view, so keep it out of logs, shell history and process listings. If the client rejects /dev/fd or opens the credential path more than once, write a real file with restrictive permissions and delete it in the same script.

Check 4: will the tunnel outlive the script?

By default the tunnel dies with its parent, which is the opposite of what a bootstrap script needs.

disown -h only helps when you launched the client as a shell job, because it edits that shell's job table and cannot reach a process your script spawned through a pty. A service manager is the better home for anything that must reconnect unattended, because it restarts the client and records why it died. Either way, expect a fresh code after every reconnect: a one-time token is consumed by the first successful authentication.

Watch what sudo does to the sequence. A sudo password prompt is not the VPN's prompt, and answering it with the VPN password is the most common way an otherwise correct expectation order still fails.

Check 5: should a person's code be in a script?

This is the check people skip. A token belongs to a person, while a tunnel that must come up on a schedule belongs to the machine.

If a job has to reconnect unattended, ask whether a machine certificate or a service account can replace the interactive login. Automating someone's one-time code turns a second factor into a stored secret, which is a policy change rather than a scripting convenience. Where the service supports both, pick the credential that matches how the job actually runs. Write the answer down, because the next person will ask why the script holds a token.

What a working script looks like

The script prints the step it reached, never the secret, and exits non-zero on any failed expectation.

Test it by breaking it on purpose: wrong password, wrong group, expired token. Each failure should stop at a named step instead of hanging. Then run it once against the real service and check the tunnel from outside the script, not from inside it.

ဆက်စပ်ဆောင်းပါးများ