Zero Trust Architecture With WireGuard
The NIST Zero Trust Architecture publication (aka NIST SP 800-207) describes what “zero trust” means in the context of securing computer networks and information systems, and what a standard zero-trust architecture looks like. For many organizations, moving to a zero-trust architecture is not something you do overnight — it’s a gradual process of improving a system here, refining a policy there, gathering more data from this group of devices, and so on.
In this article, we’ll cover the basics of zero trust, and show you how you can start to migrate to a zero-trust architecture using WireGuard — even for systems and applications that don’t natively fit the paradigm:
Tenets of Zero Trust
What does zero trust mean? NIST SP 800-207 lays out the tenets of zero trust (in section 2.1). Here we’ll summerize each tenet with a short, pithy label, to make it easy to keep each principle in mind:
Everything is in-scope
(1) All data sources and computing services and considered resources.
Inventory everything: devices, systems, data sources, applications — internal and external. Include all the SaaS (Software as a Service), PaaS (Platform as a Service), IaaS (Infrastructure as a Service), and anything-else-as-a-service you use; as well as any externally-owned or operated devices that can access your internal resources.
Network is untrusted
(2) All communication is secured regardless of network location.
In other words, assume breach. Your adversaries are in your network and on your user devices (if not now, they will be at some point), so you need to design your internal infrastructure to prevent them from moving around and getting access to more systems and data.
Least-privileged access
(3) Access to individual enterprise resources is granted on a per-session basis.
Access to systems and data needs to be limited and granular: session-based, task-oriented, and time-bound.
Policy-based access control
(4) Access to resources is determined by dynamic policy — including the observable state of client identity, application/service, and the requesting asset — and may include other behavioral and environmental attributes.
Rather than static access grants enumerating each user and resource, access should be defined by higher-level policies that reflect business needs; and these policies should be evaluated dynamically using the most up-to-date attributes of the user and resource, as well as the observed behavior of the user, the device she’s using, the network environment as a whole, and the current threat landscape.
No device is trusted
(5) The enterprise monitors and measures the integrity and security posture of all owned and associated assets.
All devices (internal and external) need to be monitored for potential security risks — and access restricted based on risk.
Continuous access evaluation
(6) All resource authentication and authorization are dynamic and strictly enforced before access is allowed.
Authentication and authorization must be continually re-evaluated, to check not only for changes to the accessing user and her authorization, but also to account for incoming data on the behavior of the user, her device, the network environment, emerging threats, etc. Access must be cut off when something changes such that the user or her device no longer meets the policy requirements to access a resource.
Monitor and log everything
(7) The enterprise collects as much information as possible about the current state of assets, network infrastructure and communications, and uses it to improve its security posture.
Collect all the data you can about the user, her device, your network, and the global threat environment — and feed it all back into your access control.
Zero Trust Components
The core components of a zero trust architecture are pretty simple:
-
A Subject (ie user) uses
-
A System (ie local user device) to connect through
-
A Policy Enforcement Point (PEP), which checks with
-
A Policy Decision Point (PDP), composed of
-
A Policy Administrator (PA), which communicates about access decisions (and manages implementation details like issuing credentials or managing session state) between the PEP and
-
A Policy Engine (PE), which evaluates access policies to make the access decision; and if access is granted, the PEP allows the connection to continue on to
-
-
A Resource (ie system, application, or collection of data), the object of the connection
A digram of an individual connection would look like this:
While the policies used by these zero-trust components should apply organization-wide, there may be many enforcement points (PEPs) spread out all over an organization’s network; and it might also have multiple decision points (PDPs) to handle access checks for different types of technologies or to handle checks at different physical locations:
Policy Enforcement For Web Apps
Policy enforcement for applications that use HTTP is generally much easier with a zero-trust architecture than applications that use other protocols. There are many off-the-shelf tools (nginx, Apache, HAProxy, etc) that can terminate TLS connections, decode the HTTP request, and allow you to plug in your zero-trust policy checks, before proxying it on to the appropriate application server if the checks pass.
While some web apps may need their authentication and authorization systems re-written to enable this, more and more web apps are being built from the ground up with pluggable systems that allow their authentication and authorization to be controlled through an HTTP proxy (or can customized to do so with a small amount of effort).
Policy Enforcement for Other Apps
However, many applications or systems that users need to access are not available over HTTP. Common examples include file shares, printing, voice-over-IP (VoIP), video conferencing, remote desktop (RDP), database access, and terminal access (SSH).
Zero-trust policy enforcement for applications like this can be more difficult, as there generally isn’t off-the-shelf software that can proxy these applications natively to insert custom authentication and authorization into their access flow. In lieu of implementing a native proxy for each of these applications, what you can instead set up zero-trust policy enforcement via a virtual networking tool like WireGuard, paired with a traditional firewall like iptables or nftables.
WireGuard’s mandatory mutual authentication and cryptokey routing enables you to securely bind a private IP address to a device. This in turn allows you to enforce zero-trust policies for the device with an off-the-shelf firewall, using the WireGuard IP address to identify the device.
How to Get Started
Start with one application and the devices that use it. If the system to which users would normally connect to access the application can host WireGuard and an appropriate firewall, use it as the PEP (policy enforcement point). Otherwise set up another host as the PEP, install WireGuard and a firewall on it, and block off all inbound network connections to the application except through the PEP.
Select an available private subnet for the WireGuard network (we’ll use 10.0.0.0/24
in our example). Reserve one address for the PEP itself (10.0.0.1
), and one address for the application (10.0.0.2
). Designate an address on the WireGuard subnet for each device that needs to access the application. In our example, we’ll use 10.0.0.11
for Alice’s laptop, 10.0.0.12
for her phone, and 10.0.0.13
for Bob’s workstation.
To access the application, users will use the WireGuard network’s address for the application (10.0.0.2
); and to identify each users’ device, our zero-trust infrastructure will use the WireGuard network’s address for the device (eg 10.0.0.11
for Alice’s laptop).
WireGuard Configuration
The WireGuard configuration for our PEP will look like this:
# local settings for the PEP [Interface] PrivateKey = AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEE= Address = 10.0.0.1/24 ListenPort = 51820 # remote settings for Alice's laptop [Peer] PublicKey = fE/wdxzl0klVp/IR8UcaoGUMjqaWi3jAd7KzHKFS6Ds= AllowedIPs = 10.0.0.11 # remote settings for Alice's phone [Peer] PublicKey = jUd41n3XYa3yXBzyBvWqlLhYgRef5RiBD7jwo70U+Rw= AllowedIPs = 10.0.0.12 # remote settings for Bob's workstation [Peer] PublicKey = MMsBeTT9v/7XNWB8a/jSMn9O8olPVNduUwvUPJ6eB14= AllowedIPs = 10.0.0.13
And the WireGuard configuration for each device that needs to access the application will look like this (if the PEP’s public IP address is 198.51.100.10
):
# local settings for Alice's laptop [Interface] PrivateKey = ABBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBFA= Address = 10.0.0.11 # remote settings for the PEP [Peer] PublicKey = /TOE4TKtAqVsePRVR+5AA43HkAK5DSntkOCO7nYq5xU= Endpoint = 198.51.100.10:51820 AllowedIPs = 10.0.0.0/24
(See the WireGuard Hub and Spoke Configuration guide for more details on each WireGuard configuration setting.)
Firewall Configuration
Configure the firewall on the PEP to:
-
Translate the virtual address for the application to the real IP address of the application (from the perspective of the PEP itself)
-
Block inbound connections to the application except from the WireGuard addresses of authorized devices
-
Log all access to the application
If the PEP is on the same host as the application itself, a basic ruleset for a nftables firewall would look like this (if the application is for example an SMB fileshare):
#!/usr/sbin/nft -f flush ruleset define pub_iface = "eth0" define wg_iface = "wg0" define wg_port = 51820 define pep = 10.0.0.1 define fileshare_virt = 10.0.0.2 define fileshare_real = $pep define fileshare_tcp = { 139, 445 } define fileshare_udp = { 137, 138 } define alices_laptop = 10.0.0.11 define alices_phone = 10.0.0.12 define bobs_workstation = 10.0.0.13 table inet nat { chain prerouting { type nat hook prerouting priority -100; policy accept; # translate application's virtual address to real address ip daddr $fileshare_virt dnat ip to $fileshare_real } } table inet filter { # set of devices authorized to access SMB fileshare set device-access-to-fileshare { typeof ip saddr elements = { $alices_laptop, $alices_phone, $bobs_workstation } } chain device-access-policies { # log zero-trust network access log level debug prefix "device-access-policies: " # enforce zero-trust access policies ip saddr @device-access-to-fileshare ip daddr $fileshare_real tcp dport $fileshare_tcp accept ip saddr @device-access-to-fileshare ip daddr $fileshare_real udp dport $fileshare_udp accept reject with icmpx type admin-prohibited } chain input { type filter hook input priority 0; policy drop; # accept all loopback packets iif "lo" accept # accept all icmp/icmpv6 packets meta l4proto { icmp, ipv6-icmp } accept # accept all packets that are part of an already-established connection ct state vmap { invalid : drop, established : accept, related : accept } # drop new connections over rate limit ct state new limit rate over 1/second burst 10 packets drop # accept all DHCPv6 packets received at a link-local address ip6 daddr fe80::/64 udp dport dhcpv6-client accept # accept all WireGuard packets received on a public interface iifname $pub_iface udp dport $wg_port accept # filter packets inbound from WireGuard network through device access policies chain iifname $wg_iface goto device-access-policies # reject with polite "port unreachable" icmp response reject } chain forward { type filter hook forward priority 0; policy drop; reject with icmpx type host-unreachable } }
This firewall configuration would allow Alice access the SMB fileshare using its virtual WireGuard IP address of 10.0.0.2
(eg smbclient //10.0.0.2/share1
) from her laptop and phone when her WireGuard connection is up, and Bob from his workstation when his WireGuard connection is up.
(See the WireGuard With Nftables guide for more details how to configure nftables firewalls with WireGuard.)
PEP on a Different Host
If the PEP is on a different host than the application, change the forward
chain in the above nftables rulset to jump to the device-access-policies
chain for inbound access from the WireGuard network, and to allow it to forward returning packets for established connections:
chain forward { type filter hook forward priority 0; policy drop; # forward all packets that are part of an already-established connection ct state vmap { invalid : drop, established : accept, related : accept } # drop new connections over rate limit ct state new limit rate over 1/second burst 10 packets drop # filter packets inbound from WireGuard network through device access policies chain iifname $wg_iface goto device-access-policies # reject with polite "host unreachable" icmp response reject with icmpx type host-unreachable }
Also change the definition for the application’s real IP address in the ruleset; in the above example, replace the $fileshare_real
variable with the actual IP address of the application’s host (from the perspective of the PEP):
define fileshare_real = 192.168.1.123
Also, enable packet forwarding on the PEP host (eg sysctl -w net.ipv4.conf.all.forwarding=1
); and either adjust the network between the PEP host and the application host to be able to route packets from the WireGuard network, or apply packet masquerading to the PEP’s nftables config by adding the following postrouting
chain to its nat
table:
table inet nat { chain prerouting { type nat hook prerouting priority -100; policy accept; # translate application's virtual address to real address ip daddr $fileshare_virt dnat ip to $fileshare_real } chain postrouting { type nat hook postrouting priority 100; policy accept; # masquerade WireGuard network addresses on LAN iifname $wg_iface oifname $pub_iface masquerade } }
PEP Remote Access
Make sure you also lock down remote access to the PEP itself. The only port that should be remotely accessible is the WireGuard listen port (51820
in our example). Treat any needed remote access to the PEP as an application protected by the PEP itself: each device that needs access to the PEP must use the WireGuard network, where its access will be limited and logged by the firewall.
For example, to allow Alice to administer the PEP over SSH from her laptop, update the device-access-policies
chain from the above example to the following:
# set of devices authorized to access SMB fileshare set device-access-to-fileshare { typeof ip saddr elements = { $alices_laptop, $alices_phone, $bobs_workstation } } # set of devices authorized to access PEP itself for administration set device-access-to-pep-admin { typeof ip saddr elements = { $alices_laptop } } chain device-access-policies { # log zero-trust network access log level debug prefix "device-access-policies: " # enforce zero-trust access policies ip saddr @device-access-to-fileshare ip daddr $fileshare_real tcp dport $fileshare_tcp accept ip saddr @device-access-to-fileshare ip daddr $fileshare_real udp dport $fileshare_udp accept ip saddr @device-access-to-pep-admin ip daddr $pep tcp dport ssh accept reject with icmpx type admin-prohibited }
With this policy in place, and when the WireGuard interface on her laptop is up, Alice will be able to connect to the PEP over SSH using the PEP’s direct WireGuard address (eg via ssh 10.0.0.1
).
Basic Management
The firewall rules we set up above represent the first iteration of our zero-trust access-control policies. Primitive though they be, they are powerful already: they assert that only Alice and Bob are authorized to access our application, and are authenticated through the possession of the WireGuard private keys registered for their devices.
Updating Policies
Later on you can connect these rules to a dynamic policy engine to strengthen them further; but initially you can just maintain a static ruleset in your configuration management system, and re-deploy them to the PEP when they change. Nftables is especially convenient for this — you can easily extract bits and pieces from your main ruleset definitions into external files that can be managed and updated separately.
For example, instead of enumerating the devices authorized to access each application in our main nftables.conf
file, we could simply define empty sets for those devices in our main file, and use an include
statement at the end of the main file to include additional files from a different directory (/etc/nftables.conf.d/
in this case):
#!/usr/sbin/nft -f flush ruleset # ... table inet filter { # set of devices authorized to access SMB fileshare set device-access-to-fileshare { typeof ip saddr; } # set of devices authorized to access PEP itself for administration set device-access-to-pep-admin { typeof ip saddr; } chain device-access-policies { # log zero-trust network access log level debug prefix "device-access-policies: " # enforce zero-trust access policies ip saddr @device-access-to-fileshare ip daddr $fileshare_real tcp dport $fileshare_tcp accept ip saddr @device-access-to-fileshare ip daddr $fileshare_real udp dport $fileshare_udp accept ip saddr @device-access-to-pep-admin ip daddr $pep tcp dport ssh accept reject with icmpx type admin-prohibited } # ... } include "/etc/nftables.conf.d/*.*"
Within the included directory, we could create a file like the following to populate the device-access-to-fileshare
set, containing the WireGuard IP addresses of the devices authorized to access the fileshare:
# /etc/nftables.conf.d/device-access-to-fileshare.conf table inet filter { set device-access-to-fileshare { typeof ip saddr; elements = { 10.0.0.11, 10.0.0.12, 10.0.0.13, }; }; }
And we could create a second file like the following to populate the device-access-to-pep-admin
set, containing the WireGuard IP addresses of the devices authorized to administer the PEP via SSH:
# /etc/nftables.conf.d/device-access-to-pep-admin.conf table inet filter { set device-access-to-pep-admin { typeof ip saddr; elements = { 10.0.0.11, }; }; }
You can store these smaller included files as-is in your configuration management system (SCM), or build them out on-the-fly from the policy rules and inventory list in your configuration management database. With this first iteration, we’re effectively using our SCM as our zero-trust policy engine (PE), and our CI/CD (Continuous Integration / Continuous Deploment) pipelines as the policy administrator (PA).
Tip
|
Use the
(With most Linux distributions, this is what “restarting” the nftables service actually does behind the scenes.) Alternatively, you can add IP addresses to existing nftables sets on-the-fly with the
Or remove IP address with the
|
Adding User Devices
To add a brand new user or device to the application, you just need to generate a new public-key pair and WireGuard address for the new user’s device; add the device’s public key and IP address to the WireGuard config on the PEP:
# local settings for the PEP [Interface] PrivateKey = AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEE= Address = 10.0.0.1/24 ListenPort = 51820 # ... # remote settings for Cate's workstation [Peer] PublicKey = kAYKIv6gpBDqueaVNOsY8ddKmIC2dnnXJQ0iKzWxfBI= AllowedIPs = 10.0.0.14
Tip
|
Use the
Alternatively, you can add and remove peers on-the-fly to an active WireGuard interface with the
Or to remove that same peer from the
|
Monitoring Logs
Any log
statements you have in your nftables ruleset will be logged to the kernel message facility. If you are already piping your kernel logs into your SIEM (Security Information and Event Management) system, and you log at the suggested points in the example nftables config above, you’ll have a record of every new connection attempted (rejected or accepted) within the WireGuard network.
Otherwise, you can have the host’s logging daemon push these logs into a custom log file or stream. For example, if you’re using rsyslogd, you can add the following line to your rsyslogd config to send messages containing the custom log prefix device-access-policies:
(which we used in our example nftables config) to the /var/log/device-access-policies.log
file:
# /etc/rsyslog.d/20-nftables.conf
:msg,contains,"device-access-policies:" /var/log/device-access-policies.log
Log entries will look like this, for example when Alice starts a new SMB connection from her laptop to the fileshare:
Aug 22 23:52:41 pep kernel: [98200.220647] device-access-policies: IN=wg0 OUT=eth0 MAC= SRC=10.0.0.11 DST=192.168.1.123 LEN=60 TOS=0x00 PREC=0x00 TTL=63 ID=41412 DF PROTO=TCP SPT=39122 DPT=445 WINDOW=64860 RES=0x00 SYN URGP=0
The Next Steps
Once you have a basic PEP (Policy Enforcement Point) with WireGuard and a firewall like nftables in place, you have a solid foundation for your zero-trust architecture on top of which you can layer stronger and more sophisticated policies, monitoring, and analysis. So the first thing you’ll want to do is apply this same architecture to any applications or systems that are currently not well secured.
Dynamic Policies
The next major improvement to make is to upgrade your PDP (Policy Decision Point) to make your PE (Policy Engine) more dynamic. If you don’t have some automation in place to automatically update each PEP’s firewall configuration when an administrator grants or revokes a user’s access to an application, do that first.
Then you may want to add some automation to temporarily block users or devices based on suspicious behavior. This could be something as simple as a custom script that periodically compiles a blocklist of suspicious IP addresses from your threat intelligence feeds and network activity logs, saves it in your SCM, and pushes it out to your PEPs via CI/CD pipeline.
However, you may want to eventually deploy a more specialized tool for defining and implementing complex policies, like OPA, Permify, Aserto, or a variety of other commercial products.
In an ideal world, you would have a single PE (replicated across multiple environments as necessary) that you can use uniformly for all your PEPs, and that is able to integrate all of the following into its access decisions:
-
Your identity management system
-
Your asset inventory
-
Your data access policies
-
Your network and system activity logs
-
External threat intelligence feeds
-
Your asset management and monitoring systems
-
Any other security analysis tools (SIEM, XDR, etc) you use
More and Better Data
Once you have a PDP that can make use of more sophisticated sources of data, you can improve the quantity and variety of data you feed into it. You may want to fine-tune the firewall logging on your PEPs to provide the PDP with additional information, such as the public IP addresses used by the remote devices connecting to the PEPs (check out some of our other articles about monitoring WireGuard usage for more suggestions about this).
You should also make sure you have an endpoint management agent running on all devices allowed to connect to your PEPs. Such agents will be able to provide your PE with intelligence on the device’s patch level, the integrity of its system components, and any unusual or suspicious behavior its user has engaged in recently.
This will allow your PE to block access to applications from users who are otherwise authorized to use the application — but authorized only when you have high confidence that the user is actually who she says she is, and not an adversary who has compromised the user’s device. Adding WireGuard multi-factor authentication can also further strengthen your confidence in the authenticity of the device user’s identity.