Skip to content
Networking6 min read

Client VPN connected, authenticated, and reaching nothing

A Client VPN endpoint has its own route table and its own authorization rules, and a destination has to be in both before a connected user can reach it. Add one without the other and the tunnel is up, the client has an address, and every packet goes nowhere — with a different silence depending on which one you forgot.

· Venerable Networks

The VPN client says Connected. The user has an address from the client CIDR. DNS works. And the one thing they connected to reach — a database in a peered VPC, a file share on-premises — times out, while the VPC the endpoint sits in is reachable fine.

Client VPN has two independent lists that decide what a connected user can reach, and the endpoint is happy to be fully connected with a destination in neither.

Two lists, both required

A Client VPN endpoint is a managed OpenVPN server with its own network interfaces in the subnets you associate. Two things govern where a connected client's traffic can go:

The Client VPN route table. Not a VPC route table — the endpoint's own. It determines which destination networks the endpoint will forward a client's traffic toward, and through which associated subnet. Associating a subnet adds a route for that VPC's CIDR automatically. Anything else — a peered VPC, on-premises, the internet — you add yourself.

Authorization rules. A separate list that says which clients (all, or an Active Directory or SAML group) are permitted to send traffic to which destination CIDR. A route with no matching authorization rule is a road with a locked gate.

The documentation for setting up an endpoint states the pairing in one sentence that is easy to read as one step:

You can also turn on access to additional networks, such as AWS services, peered VPCs, on-premises networks, or the internet. For each additional network, you must add a route to the Client VPN endpoint route table and then configure an authorization rule to give clients access.

Two actions per destination, in two different places in the console, and the first one succeeding tells you nothing about the second.

Why the VPC itself usually works

When you associate a subnet, the endpoint adds a route for the whole VPC CIDR. Most setup guides then have you add an authorization rule for the VPC CIDR too. So the VPC — both lists — works on the first try, and the mental model that forms is "associate a subnet, add a rule, done".

The peered VPC, the on-premises range, and 0.0.0.0/0 for full tunnel are the destinations that got only one of the two, because the guide stopped before them.

The two silences

Which list you missed changes what the failure looks like, and the difference is diagnostic.

Route present, authorization rule missing. The endpoint knows how to get there and refuses to send the traffic. The client sees a timeout. Nothing on the far side sees anything, because the packet never left the endpoint. Flow logs on the associated subnet show nothing for that destination.

Authorization rule present, route missing. The client is allowed to go there and the endpoint has no path. In full-tunnel mode the client sends everything up the tunnel and the endpoint drops what it cannot route. In split-tunnel mode the client's own routing table is built from the endpoint's route table — AWS's split-tunnel documentation says "all of the routes in the Client VPN endpoint's route table are added to the client's route table when the VPN connection is established" — so a missing route means the client never sends that destination up the tunnel at all. It goes out the user's local internet connection, which for a 10.x address means it dies on their home router. The timeout looks identical. The packet took a completely different path to not arriving.

There is a third variant of this one that produces a ticket from a user who did everything right. The routes are pushed at connection time. If you add the missing route while users are connected, their clients do not have it until they reconnect — and the same page notes that in split-tunnel mode, modifying the endpoint's route table resets every client connection anyway. A user who was connected before the fix and is still connected after it is, from their client's point of view, still missing the route.

Checking both lists is two calls:

aws ec2 describe-client-vpn-routes --client-vpn-endpoint-id "$CVPN" \
  --query 'Routes[].[DestinationCidr,TargetSubnet,Status.Code,Origin]' --output table
 
aws ec2 describe-client-vpn-authorization-rules --client-vpn-endpoint-id "$CVPN" \
  --query 'AuthorizationRules[].[DestinationCidr,GroupId,AccessAll,Status.Code]' --output table

A destination has to appear in both outputs, with active status in both. The common shape of the failure is a CIDR in exactly one of the two tables.

The three other things, which are not Client VPN's

Once both lists are right, three ordinary VPC controls still apply, and AWS's access troubleshooting article lists all five together. The two that are specific to Client VPN are above. The remaining three are the same as for any traffic entering from the associated subnet — with one detail that trips people up:

When you associate a subnet with your Client VPN endpoint, Client VPN network interfaces are created in that subnet. Traffic that is sent to the VPC from the Client VPN endpoint is sent through a Client VPN network interface. Then, source network address translation (SNAT) is applied. This means that the source IP address from the client CIDR range is translated to the Client VPN network interface IP address.

So the target never sees the client CIDR. It sees an address from the associated subnet. A security group rule that permits the client CIDR — the obvious thing to write — permits nothing that actually arrives. The rule has to permit the associated subnet's CIDR, or reference the security group attached to the endpoint. The same applies to network ACLs on the target subnet, and to any route the target's subnet needs for the return trip, which goes to the associated subnet rather than to the client range.

Five things, then, for a destination outside the endpoint's own VPC:

ControlWhereWhat it has to say
Client VPN routethe endpoint's route tabledestination CIDR → an associated subnet
Authorization rulethe endpoint's rulesdestination CIDR, for the right group
Associated subnet's VPC route tablethe VPCa route to the destination (peering, TGW, VGW)
Target security groupthe targetinbound from the associated subnet, not the client CIDR
Target network ACLthe target's subnetinbound from the associated subnet, outbound back to it

The first two are the ones people have never seen before. The last three are the ones they get wrong because of the SNAT.

What to do with it

Treat route and rule as one change. Every destination added to a Client VPN endpoint is a route and an authorization rule, in the same change ticket, in the same Terraform module. An aws_ec2_client_vpn_route without a matching aws_ec2_client_vpn_authorization_rule for the same CIDR should fail review on sight.

Write target rules for the associated subnet. The client CIDR is the address the user sees on their own machine, and it is not the address their traffic carries past the endpoint. Security groups and network ACLs on anything the VPN reaches need to name the associated subnets, or the endpoint's security group.

Test from the client, to the destination, and read both tables when it fails. The tunnel being up proves authentication and the endpoint's own health. It proves nothing about any destination. Two describe calls settle whether the destination is in both lists before anyone looks at the far side.

Audit for the mismatch. The interesting state is a CIDR in one list and not the other:

comm -3 \
  <(aws ec2 describe-client-vpn-routes --client-vpn-endpoint-id "$CVPN" \
      --query 'Routes[].DestinationCidr' --output text | tr '\t' '\n' | sort -u) \
  <(aws ec2 describe-client-vpn-authorization-rules --client-vpn-endpoint-id "$CVPN" \
      --query 'AuthorizationRules[].DestinationCidr' --output text | tr '\t' '\n' | sort -u)

Any output is a destination that is half-configured. Left column: routable, not authorised. Right column: authorised, not routable.

The shape is the same as security group referencing across a Transit Gateway — two flags that both have to be on, where setting one produces a healthy-looking configuration that does nothing. The difference here is that Client VPN shows you a connected user, which is a more convincing kind of nothing.