Pasting a tls-crypt-v2 profile into a ticket looks harmless until you count what the file contains. The client's control-channel key sits next to an encrypted copy of that same key, and a third object, the wrapping key, never leaves the accepting server. Those three answer different questions about custody, sharing and retirement. Separating them settles most policy arguments before they start.

What the profile actually contains

The client half of tls-crypt-v2 is a key plus a wrapped copy of it. The client uses the plain key for control-channel protection and hands the wrapped copy to the server during setup, so the server recovers the same key without storing a plaintext copy per client. The wrapping key makes that recovery possible and belongs only on the accepting side. Everything below follows from where those three objects live.

ObjectWhere it livesWhat can open itWhat a leak costs
Server wrapping keyaccepting servers onlythe unwrap step inside the serverevery wrapped copy issued to that group becomes readable
Wrapped client keyinside the client profilethe server wrapping keyit names a client and exposes its metadata, but not the key inside
Client control keyinside the client profile, usable as-isthe client that owns itsomeone can open handshakes as that client until the server refuses the key

Read the table by custody rather than by name and the consequences fall out. Losing the wrapping key is the expensive event, because nothing in a profile can reconstruct it and the honest recovery is reissuing the group. A leaked profile is cheaper but far more common, since profiles get pasted into tickets by people who believe the wrapped copy makes the file safe to share. The two failures need different responses, which is the reason for keeping the objects apart.

Why the wrapped copy is not the whole story

A profile carries the usable client material together with its wrapped counterpart, so protecting the copy for the server says nothing about protecting the file. The blob is safe to transmit and unsafe to publish. Metadata inside it is visible to whoever receives the blob, including anyone who later reads the ticket it was pasted into.

Treat a profile as a credential rather than a configuration sample. A leaked file gives its holder a working client identity until the server stops accepting that identity, and the wrapping does nothing about that window. Issue one profile per client so withdrawal stays a single-file decision, and back the wrapping key up apart from the server configuration it enables. Neither habit repairs a compromised profile, but both decide how quickly the next one is contained.

Where the verify hook sits in the handshake

The verify hook runs before the TLS stack is exposed, and its job is to inspect the metadata carried in the wrapped blob. It receives that metadata plus a small, documented environment, and it has less to work with than an authentication hook. Assuming it can see usernames, session state or plugin variables is the usual way this hook gets written badly. Read the hook's own manual section instead of borrowing expectations from a different hook.

Its exit status is the decision: a non-zero result refuses the client and a zero result lets the handshake continue. The policy therefore has to say what happens when metadata is missing, unparsable or unexpected. Write the refusal path first, then decide which values are acceptable.

tls-crypt-v2 /etc/openvpn/server/tcv2-server.key
tls-crypt-v2-verify /etc/openvpn/server/verify-client-metadata.sh

Those two lines are the policy surface on the server: one names the wrapping key, the other names the program that judges each client. Everything interesting lives in the script, which deserves version control and the same review as any other access rule.

Account, certificate and control key retire apart

Early control-channel acceptance is not authentication, because it happens before the TLS identity check and before any account authorization. Disabling an account, revoking a certificate and refusing a client control key are three actions with three mechanisms. A deployment that performs only one of them has a gap rather than a policy. Reconcile the three deliberately, and record which step exercises which authority.

Timing is the second distinction. A changed acceptance list applies to new handshakes, while sessions already established keep running until something terminates them. Counting entries in the new list as proof that old sessions ended is how a removed client stays connected for hours.

Unreconciled authorities leave two holes that are easy to find in practice. An account can be disabled while its control key still opens a handshake, and a certificate can be revoked while the metadata policy still admits the same client. Both are the same mistake in different places, because an operator treated one list as the whole authority. Lay the three lists side by side at removal time and the mistake becomes visible.

Issuing and withdrawing a profile

Issuing is the easy half: create the client key on the accepting side, hand the profile to exactly one owner, keep the wrapping material restricted, and run the same verify program on every accepting server. Cheap admission is a property of the whole group, not of the newest host. A second server with an older or laxer script silently becomes the weakest path in the deployment.

Withdrawal needs the three steps in a deliberate order. Deny the client in the verify hook so new handshakes fail, revoke its certificate if a PKI is in use, and disable the account so later authorization has nothing to authorize. Then terminate the sessions the old acceptance allowed, since they will not notice any of the three. Rehearse the sequence on a client you can afford to break.

Metadata in a profile is an input to that policy, not evidence that access control already exists. The OpenVPN 2.6 design document describes the objects and the boundary they draw, while enforcing them stays with the operator.

འབྲེལ་བའི་རྩོམ་ཡིག