You add auth-user-pass to a profile, reconnect, and the client asks for a username and password. It looks like MFA is on. Then you type a code from the wrong account, or a code that expired ten minutes ago, and the tunnel still comes up. That is the symptom: the client changed, the server did not. A one-time password is only a factor when a verifier on the server decides whether it is valid for the identity claiming it.

The prompt proves the client asked, nothing more

An .ovpn file is a client-side document. It can tell the client to request credentials, cache them or stop caching them. It cannot enroll a user, install a PAM module, or make the server require a second factor it does not currently check.

Installing an authenticator app on a phone does not create that relationship either. The Google Authenticator PAM project is an OTP verification component that can sit behind OpenVPN; it is not a way to bolt Google account login onto an imported profile.

Enrollment is the part a profile cannot do

TOTP needs enrollment material shared between the authenticator and its verifier. The six digits on the phone are output; the seed behind them is the secret, and it belongs to one identity.

So the operator needs an enrollment process that binds the right seed to the right VPN identity, plus a verified replacement path for a lost phone. Letting users upload a file named after themselves turns enrollment into an account-takeover opportunity, and encrypting the upload does not change that.

A PAM-based design does not have to create a separate Unix account per VPN user. The module documentation supports alternate secret paths and a dedicated file-access account, which is a filesystem identity rather than the login identity being verified.

The common mistake is a fixed secret path ending in one username, copied from a single-user example. That is a one-identity demonstration, not a per-user lookup, and using it for every request gives nobody an independently enrolled authenticator. Write the identity mapping down, protect the seed files, and keep the password, certificate and account checks in the same authentication stack.

Read the PAM stack for what is actually required

A line such as account required pam_permit.so does not implement account eligibility and does not add a factor. It permits that PAM account-management stage, which is a statement about a stage rather than about your users.

The same applies to permissive enrollment options. Accepting users with no enrolled secret may be a deliberate rollout choice, but it is not proof that MFA is mandatory, and it should be written down as a temporary exception with an end date.

Match the client's interaction to the server's design

auth-user-pass

That directive requests username and password input. It does not configure PAM, create an enrollment record, or tell the server that the password field contains a TOTP value. The server integration and the client have to agree on an interaction the deployment actually supports: a challenge, a documented combined credential format, or a product-specific flow.

Do not append a code to the password just because the client shows one field. That works only where the server's plugin expects exactly that format, and it fails confusingly everywhere else.

Keep seeds and QR material out of shared profiles and routine diagnostic logs. A code submitted over the authenticated login channel is not the same object as the seed provisioned during controlled enrollment, and mixing them in a support ticket is how secrets escape.

Reconnects, tokens and the test that settles it

Plan for what happens after the first successful login. A previously accepted code may be expired or rejected as a replay at the next authentication. auth-nocache controls credential retention and does not install MFA, and reneg-sec 0 is not a definition of TOTP support no matter how often it appears in forum threads.

Some deployments issue authentication tokens after the initial check so that reconnects do not re-prompt. That is a design decision with its own validity window and revocation story, not a default you inherit.

Then test the admission policy with the client people actually use. An enrolled user with the right password, certificate and current code should connect. A wrong code, a user with no enrollment, and a forbidden account should each fail, and the reason should be visible in the server log. Until you have seen those rejections, you have a prompt, not a policy.

Artiglyn mynjee