Two-Factor Authentication with TunSafe

Two-factor authentication is an extra layer of security for your VPN Tunnels designed to ensure that you're the only person who can access your account, even if someone gets access your WireGuard configuration file.

When logging in to the VPN, you will be prompted for an access code. This access code can be generated from any of the existing authentication apps that support the TOTP standard such as Google Authenticator.

How to use this

This feature was added recently so please download TunSafe 1.5-rc2 or later. It's not yet available on mobiles.

Enable TOTP authentication for a Peer by adding this to the Server's config file:

RequireToken = totp-sha1:SECRET,digits=6,period=30,precision=15

When connecting to this Server, the Client looks like:

After a valid token is entered, the client logs in as usual. If an incorrect token is entered 10 times, the account is locked. To unlock the account, enter a valid code, wait 30 seconds, enter the next valid code, wait 30 seconds, enter the next valid code. If there are any failed attempts in between, the unlock procedure aborts. For rate limiting, there's also a token bucket scheme that refills by one attempt every 30 seconds. In the future there may be a way to configure those settings.


At TunSafe, we always try to find ways to keep our customers' WireGuard tunnels more secure. We don't like the fact that the WireGuard configuration file is all you need to connect to someone else's internal network. We think there should be some additional security measure in addition to this.

We're strong advocates of two-factor authentication and think that's an important feature of VPN deployments, especially in enterprise scenarios. We've been trying to find ways to add TOTP to a WireGuard tunnel. Having support for this means you can use one of the many authenticatior apps on your phone as an extra layer of security.

A straightforward way to add two-factor authentication on top of the existing WireGuard infrastructure is to first establish the tunnel as usual, and then having firewall rules that block all network packets. Then the peer could communicate with some internal host and exchange the two-factor information. This opens up the firewall and the peer can now communicate.

After a timeout, such as 1 hour, if no traffic has been exchanged, the firewall is re-enabled and a new two-factor exchange is required to open up the firewall again.

This sounds easy enough, however, it has one serious drawback. If an attacker connects with a config file during the time where the firewall is already open, then the attacker is able to access the network, WITHOUT providing any two-factor information. This is because the legitimate peer already opened up the firewall, and WireGuard has no way of differentiating the handshake of the attacker from the handshake of the legitimate peer. There's no state shared across two handshake messages, and nothing to associate the two-factor information with, so WireGuard has no idea that it's the attacker that's now establishing a tunnel.

To solve this, we've been adding some TunSafe extensions to the protocol. The first thing is a session ID. This is an ID that's unique for each connected peer. If someone else connects with the same config file, that session ID will be different. In the handshake response, the Server will provide the Client with a session ID. The Client proves that it knows the session ID by including a hash of it in all future handshake initiation packets, until the client is restarted.

When a Client connects to a server, and the Server has two-factor enabled for that client peer, then instead of setting up the WireGuard keypair as usual, the Server instead responds with a TunSafe specific TokenRequest message. When the Client sees this, it asks the user for the two-factor token. In the next handshake initiation packet, it will include the two-factor token in the TokenReply response, and also the session ID hash.

When the server sees the TokenReply, it will check if the supplied token is valid by using the standard TOTP algorithm. If valid, the handshake is accepted. There's no changes required to WireGuard's handshaking mechanism except an added field, so the security guarantees of WireGuard should still hold. In all future handshakes, the session ID hash will be included, and through this the server knows that this peer is legitimate.

For peers that don't have two-factor enabled, the handshake is completely unchanged. It's only clients that want to use two-factor authentication that need to be upgraded.

When using two-factor authentication, even if both peer private keys leak, there's no way whatsoever to connect to the tunnel without knowing the two-factor token.

Implementation details

In the terminology we use the words Client and Server. Client is the side that needs to provide a two-factor token, and Server is the side that verifies the two-factor token.

In the Handshake Initiation packet, right after the timestamp field, we insert a variable length blob. In the Handshake Response packet, in the field named empty, we also insert the same blob. WireGuard already encrypts and HMACs these fields using ChaCha20Poly1305. But since the length of them changes, it not a backwards compatible change when the fields are non-empty.

The variable length blob is limited to 1024 bytes big, to prevent UDP packets from growing too large. It's a sequence of TLV elements:

    0                   1                   2                   3
    0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
   |   Type (8)    |    Size (8)   |     Payload (Size bytes)    ...

We define the following extension types:

SessionIDAuth contains the ephemeral pubkey from the handshake message hashed with the session ID as key BLAKE2S(Epub, sessionID), this means that the Client can prove that it knows the session ID. If the Client doesn't provide a valid hash, the Server will refuse the login and instead send TokenRequest.

The TokenRequest contains a 32-byte symmetric ephemeral key. It also contains a byte saying what the most recent authentication failure reason was, and a flag saying what type of token we request, so the UI can display it nicely:

Unlike TokenRequest which is sent in the Handshake Response packet, the TokenReply is sent in the Handshake Initiation packet. The Handshake Initiation encryption uses only the server's long term public key. So to achieve better forward secrecy, we encrypt it an additional time using ChaCha20Poly1305 with the ephemeral key provided by TokenRequest.

TokenReplyPayload := AEAD(K, 0, TwoFactorToken, '')


This implementation is a TunSafe specific and experimental extension to the protocol. We would love to find a variant of this proposal, or another solution that can provide the same functionality across other WireGuard implementations. We think a standardized way of doing two-factor authentication would be hugely beneficial to the WireGuard community. We're available in #tunsafe on Freenode to discuss this proposal further.