Least Privilege by Domain, Not by Subnet

Share
Least Privilege by Domain, Not by Subnet | OpenVPN Blog
15:25

 Here's how application-layer access works when the client is never given a route to the destination at all. 

Here is the problem with every access rule written against a subnet.

You want someone to reach one internal application. The application lives at 10.20.30.40. So you authorize 10.20.30.0/24, because that's the unit your firewall thinks in. The person now has a route to two hundred and fifty-four addresses, of which you intended them to reach exactly one.

Nothing has gone wrong. You wrote the rule correctly. The rule is just incapable of expressing what you meant, because subnets are a routing concept and least privilege is an application concept, and translating between them always rounds in the direction of too much access.

That gap is where lateral movement lives. It's also the thing that separates a well-configured VPN from actual Zero Trust — and closing it doesn't require a different platform, just a different unit of authorization.

Why "just write tighter rules" doesn't fix it

The obvious response is to authorize a host route instead — 10.20.30.40/32 rather than the whole /24. Better, and worth doing, but it has three problems that don't go away:

The client still has a route. It knows the destination address, it has a network path toward it, and the only thing preventing it reaching the neighbors is a rule you have to maintain correctly forever. defense by configuration rather than by architecture.

Addresses change and rules don't. The application moves, gets a second instance, moves into a container with a dynamic address. Anything behind a load balancer or an autoscaling group has an address that isn't a stable fact about it. Your /32 is now either broken or pointing at something else, and nobody finds out until someone can't work.

It doesn't scale to how people think. Nobody's access requirement is "10.20.30.40." It's "the wiki." Every translation from the second thing to the first is a chance to get it wrong, and every review of those rules requires someone who remembers the mapping.

What you actually want is to authorize wiki.internal.example.com and have the infrastructure work out the rest — and critically, to give the client no route to anything, including the thing it's allowed to reach.

Subnet Calculator Blog Image CTA

Authorizing the name instead of the network

That's what domain routing does in Access Server, and the mechanism is worth understanding because the security property falls directly out of it.

When a client asks for a hostname covered by an access rule, Access Server intercepts the DNS query and returns an address from its own internal pool rather than the real one. The client connects to that address. Access Server translates it, server-side, to the actual destination and proxies the connection.

The consequence is the whole point: the client never learns, or routes toward, the actual destination address. The address it was handed exists only inside Access Server's translation tables. There is no route to the real network on the client at all — not a permitted one, not a denied one, none.

So access rules stop being evaluated against subnets and start being evaluated against hostnames. And because the client was never on the destination network, there's no residual network path to sibling systems. Someone authorized for the wiki cannot reach the database on the same subnet, and not because a rule forbids it. Because they were never given a way there.

That's the difference between filtering and not granting. A firewall rule is a check that can be misconfigured, bypassed, or forgotten. An absent route is an absent route.

This is what an identity-aware proxy is, and it's the same architectural move commercial ZTNA platforms make. The difference in a self-hosted deployment is that the proxy is yours — both planes stay inside your trust domain, so neither the decision nor the traffic leaves your control. We walked through the mechanism in more depth in a separate post.

Designing the rules

Rules can be set at three levels, and the useful discipline is to be deliberate about which level each one belongs at:

Global — things everyone gets. Keep this list short and boring. DNS, time, an intranet homepage. Anything you'd be comfortable seeing in an audit finding.

Per group — where most policy should live. A group is the unit you can review meaningfully: "what can Finance reach" is a question someone can actually answer, and a rule set someone will actually maintain.

Per user — exceptions, and treat them as such. Individual rules are appropriate and sometimes unavoidable, but every one is a thing that will outlive the reason it was created. Give them an owner and a review date.

Rules can be written as exact domains, wildcards, or wildcard TLDs — which matters for design, because a wildcard is how you avoid maintaining a list of forty hostnames, and it's also how you accidentally grant more than you meant. Which is what the next rule type is for.

Each rule also carries a mode:

NAT — traffic exits via Access Server's own IP, so the destination sees Access Server rather than the client. Simple, and the destination network needs no changes.

Route — the client retains its VPN IP, so downstream systems and their logs can distinguish one user from another. Choose this when something past Access Server needs to make its own decisions or keep its own attribution.

And two rule types that do the unglamorous work:

Deny rules (Access Server 3.2.0 and later) for carving exceptions out of a broader permit — excluding internal-admin.example.com from a wildcard rule the rest of the group can reach. If you use wildcards, you will want these.

Bypass rules for domains that should skip the tunnel entirely while everything else goes through it. This is split tunnelling at per-domain precision rather than an all-or-nothing switch, and it's under-used — the honest reason to reach for it is performance, since not everything benefits from being proxied.

One prerequisite worth checking before you plan any of this: domain routing needs Access Server 3.1.0 or newer, and depends on the DNS proxy, which is on by default from 3.1.0 (sacli --key "dnsproxy.mode" if you need to check or change it). Deny rules need 3.2.0. Confirm your version before designing a rule set around features you don't have yet.

The migration trap

This is the step most likely to leave you believing you've done something you haven't.

If your existing configuration contains IP subnets representing the network Access Server sits on, those need removing and replacing with domain names. Not supplementing — replacing.

Leave them in place and every client still gets the old network route. You've added application-layer rules on top of network-layer access rather than instead of it, the subnet route wins, and the least-privilege property you were trying to buy simply isn't there. The configuration looks correct. The rules exist. And every client can still reach everything, because you never took the road away.

If you do one verification step, do this one: after configuring domain routing, check what routes a connected client actually has. Not what the rules say — what the client's routing table says.

What doesn't fit

Three honest limitations, because you'd rather know now.

Overlapping IP addresses. Domain routing can't resolve destinations that overlap with the subnet Access Server itself is deployed on. Destinations need to be on different subnets. In a network with a long history and some address reuse, this is a real constraint on where you can put things.

Resources without a name. The unit of authorization is a hostname, so anything you reach only by address needs a DNS name before it can be governed this way. Usually a small internal-DNS task, occasionally an argument with whoever owns a legacy appliance.

Applications that hard-code addresses. Some do, and they'll ignore the DNS answer entirely. These end up in the deliberate network-level remainder — which is fine, and expected, and better than pretending otherwise.

Client-to-client is the path people forget

One more, because it survives all the work above.

Authorising users to applications says nothing about whether users can reach each other. In many default configurations connected clients share an overlay and have a path between them, without any rule permitting it — it's simply what happens when nothing says otherwise.

That matters because it's a lateral movement path that bypasses your entire resource-level policy. An attacker who compromises one endpoint doesn't need to reach a server if they can reach every other connected laptop.

Disable client-to-client connectivity unless something genuinely needs it, and where cross-group traffic is required, permit it explicitly. Then go and check what your current deployment does, because a surprising number of otherwise careful policies have this wide open.

Verifying, rather than assuming

Domain routing is doing address translation on your behalf, which means you can inspect it rather than trust it. Three levels of check, cheapest first.

What rules does the server think it has?

  • sacli AccessControlRulesList

Start here. It's the fastest way to catch the gap between what you configured and what you meant to configure — including any surviving IP subnet rules from the migration trap above.

Is the DNS proxy answering the way you expect? The DNS proxy debug logs will show which queries matched a rule and what address was handed back. This is where you'll see a client that isn't getting the internal pool address because its query never matched.

Is the translation actually in place? On the Access Server host:

  • nft list chain ip as_nat AS_DOMAIN_RT

or, on an iptables-based system:

  • iptables -t nat -S AS0_DOMAIN_RT

Read the chain and confirm the mappings match the rules you think you wrote. Together these take about five minutes and they catch the class of mistake where the policy is right and the plumbing isn't — worth doing after every change to the rule set, not just at deployment.

What this buys you

Access authorized per application rather than per network. Clients with no route to anything they aren't entitled to, rather than routes plus rules denying them. Logs that name the application rather than the session. And a policy model expressed in the same terms your access requirements are already written in.

That's least privilege that means something — and on a self-hosted deployment, the proxy making those decisions and carrying that traffic is one you run.

Ready to see how OpenVPN can help protect your organization from attacks?

Try the self-hosted Access Server solution or the managed CloudConnexa service for free, no credit card required.

See Which One is Right for You

Frequently asked questions

How does ZTNA work?

A user authenticates against an identity provider; the platform evaluates that identity along with context — device posture, location, time, risk signals — against a policy for the specific resource requested; and if permitted, it establishes a path to that resource only. The user gets no network route to anything else, and the decision is re-evaluated during the session rather than trusted for its duration. NIST SP 800-207 splits this into a policy engine that decides, a policy administrator that establishes or tears down the connection, and an enforcement point guarding the path.

How do you enforce least privilege on a network?

Stop authorising networks. Subnet-based rules grant access to everything on the subnet, so translating "reach this application" into "reach this /24" always rounds toward too much. authorize the hostname instead, using an identity-aware proxy that gives the client no route to the destination at all. Then default to deny for anything no rule mentions, block client-to-client traffic unless required, and review rules on a schedule, since access policy decays as roles change.

What is an identity-aware proxy?

A component that sits between users and applications, authenticates each request against an identity, and forwards only what policy permits — so the client connects to the proxy rather than to the application, and never has a network path to the application itself. It's the core mechanism behind most ZTNA products. In a self-hosted deployment the proxy runs on your own infrastructure, meaning the access decisions and the traffic both stay inside your trust domain.

What is microsegmentation, and how does it relate to ZTNA?

Microsegmentation divides a network into small isolated zones so traffic between workloads must pass a policy check — mostly east-west traffic between servers. ZTNA governs user-to-resource access, mostly north-south. They approach the same goal from opposite ends, and a mature architecture uses both: ZTNA so a compromised user credential reaches little, microsegmentation so a compromised workload spreads no further. Domain-based access is a partial substitute for microsegmentation on the user-facing side, since clients never join the destination network in the first place.

What's the difference between NAC and ZTNA?

Network Access Control decides whether a device may join a network, typically at the switch port or wireless association, and is largely about the local network. ZTNA decides whether an authenticated identity may reach a specific resource, regardless of which network they're on. NAC gates admission; ZTNA gates authorization per resource. "Universal ZTNA" is the term for extending ZTNA's model to on-premises users so the office isn't treated as inherently trusted.

Can two users on the same VPN reach each other, and should they?

In many default configurations yes — clients sharing an overlay often have a path between them with no rule permitting it. Generally they shouldn't. Client-to-client connectivity is a lateral movement path that bypasses resource-level policy entirely, so an attacker on one endpoint can reach every other connected endpoint. Disable it by default and permit specific cross-group flows explicitly where something genuinely needs them.

Does application-layer access work for non-web applications?

Yes. Because the mechanism works at the DNS and network-translation layer rather than by inspecting HTTP, it isn't limited to web applications — SSH, RDP, database connections, and internal APIs all work the same way, provided the destination has a resolvable name and the client honours the DNS response. The exceptions are applications that hard-code IP addresses and ignore DNS entirely.

Do you still need network-level access for anything?

Usually, and that's the correct outcome rather than an unfinished migration. Administrators who genuinely need broad access, protocols that won't proxy, and devices that can't participate all remain better served by network-level access. The goal isn't eliminating it — it's making it the deliberate exception for a named few instead of the default for everyone.

Related posts from OpenVPN

Subscribe for Blog Updates