Minimize potential attack vectors and limit the scope of any compromise of the cloudflared tunnel daemon.
You may already be aware that a Cloudflare Tunnel (formerly known as Argo Tunnel) is a service that securely exposes local web servers to the internet without directly exposing them to the public. They are particularly useful when one connects to the Internet without a publicly routable IP address, most often behind the CGNAT of a mobile or other broadband provider. A Cloudflare Tunnel creates a secure, encrypted link between your server and Cloudflare’s network, eliminating the need to open ports on your firewall, and offering a solution when port forwarding isn’t available. This allows you to protect your infrastructure and easily manage access while Cloudflare handles security, traffic optimization, and DDoS protection.
This is not a tutorial on how to install and configure a Cloudflare Tunnel. There are myriad tutorials in text and video form, that detail that process fairly adequately. Rather, this article presents a method of enhancing the local network security aspects of running the cloudflared ‘lightweight’ connector daemon on your infrastructure, and addresses the terrifying potential for corporate snooping and absolute-breach security events.
A network namespace is a feature in Linux that can improve the security of running the cloudflared daemon, through isolating the tunnel’s network traffic from the rest of the system. By running cloudflared inside a separate network namespace, you create a distinct environment with its own network interfaces, IP addresses, firewall rules and routing tables. Were the cloudflared process to become compromised, an attacker would only have access to resources specifically made accessible from within that namespace, not the broader system.
Virtual Ethernet (veth) interfaces are pairs of network interfaces used to connect different network namespaces in Linux. Each veth pair consists of two interfaces: one in each namespace. These interfaces act like a “bridge” between the namespaces, allowing network traffic to pass between them. When one namespace sends data through its veth interface, the other namespace can receive it through the corresponding veth interface. This enables communication between isolated network namespaces while keeping their network environments separate and secure.
The presented systemd unit file strengthens the security of cloudflared by isolating it within a network namespace and tightly controlling its network access. Here’s how:
- A network namespace (
isolated_ns
) is created, ensuring cloudflared runs in a completely isolated network environment. - A veth pair connects the isolated namespace to the host, enabling controlled communication while keeping it secure.
- iptables rules are applied to restrict traffic within the namespace, allowing only specific outbound connections (e.g., DNS, web server) and blocking all other local network access.
- On the host, IP forwarding and NAT are configured to route traffic from the isolated namespace to the internet while hiding the namespace’s IP, further securing the system.
- When stopped, the systemd unit file ensures all network resources and rules are cleaned up, maintaining the system’s security.
This approach minimizes potential attack vectors and limits the scope of any compromise of the isolated namespace.
For the example presented, we have cloudflared running on the same machine as a webserver, and tunnel it’s ports 80 and 443 to Cloudflare’s infrastructure for wider exposure. The systemd unit is configured to deny all local LAN access from cloudflared, save for the ports on the webserver, a DNS server, and the Gateway to the Internet.
To configure the systemd unit for a similar webserver-only cloudflared setup, one need only adjust the requisite parameters. This is how we have these filled-out for the example network:
Environment=LAN_IP_RANGE=192.168.0.0/24 # The IP address range of example LAN.
Environment=WEBSERVER_IP=192.168.0.101 # The IP address of example webserver (may be external to cloudflared host).
Environment=GATEWAY_IP=192.168.0.1 # The IP address of example router's gateway.
Environment=DNSSERVER_IP=192.168.0.1 # The IP address of example router's DNS server.
Environment=TUNNEL_TOKEN=eyJhIjoiZDI0MMzdYzAtMTJhMi00MjQ3LWJhNTUtNjNmOGY5OGMzYjRhIOFlqZsakxUazBZV2QiLCJ0IjoiODI2ZGN0TVRJM1lXVXhaVEV3TldObSJ9WMxYzYzMWI4ZWQ3M2VliwicyI6Ik1qUTBzBZUzAwTlRkNWZjMDI0OWZmMjY3ak00WlRBd # Example (munged) tunnel token.
To protect a cloudflared instance that is configured to tunnel other services (SSH, RDP, SMB, etc.), you need only adjust the setup and discharge of iptables rules that govern the server’s associated ports. In the following, where access to the webserver’s ports 80 and 443 is permitted, you would substitute your own services’ port numbers and attendant protocols (tcp/udp):
...
# Initialization:
...
# Allow outgoing traffic to the local host's Web server on port 80 and 443.
ExecStartPre=ip netns exec isolated_ns iptables -A OUTPUT -d $WEBSERVER_IP -p tcp --dport 80 -j ACCEPT
ExecStartPre=ip netns exec isolated_ns iptables -A OUTPUT -d $WEBSERVER_IP -p tcp --dport 443 -j ACCEPT
...
# De-initialization
...
# Remove the rule allowing outgoing traffic to the local host's Web server on port 80 and 443.
ExecStop=ip netns exec isolated_ns iptables -D OUTPUT -d $WEBSERVER_IP -p tcp --dport 80 -j ACCEPT
ExecStop=ip netns exec isolated_ns iptables -D OUTPUT -d $WEBSERVER_IP -p tcp --dport 443 -j ACCEPT
...
After installing the cloudflared connector daemon, and configuring the systemd unit file appropriately, backup the files /etc/systemd/system/cloudflared.service and /etc/systemd/system/cloudflared-update.service, and replace these with the new versions. The timer unit file /etc/systemd/system/cloudflared-update.timer requires no changes.
The security enhanced systemd unit file /etc/systemd/system/cloudflared.service reads as follows:
[Unit]
Description=cloudflared
After=network-online.target
Wants=network-online.target
# Copyright © 2025, Murray R. Van Luyn.
[Service]
TimeoutStartSec=0
Type=notify
Environment=LAN_IP_RANGE=192.168.0.0/24 # Insert the IP address range of your LAN.
Environment=WEBSERVER_IP=192.168.0.101 # Insert the IP address of your webserver (may be external to cloudflared host).
Environment=GATEWAY_IP=192.168.0.1 # Insert the IP address of your gateway.
Environment=DNSSERVER_IP=192.168.0.1 # Insert the IP address of your DNS server (may be external).
Environment=TUNNEL_TOKEN=eyJhIjoiZDI0MMzdYzAtMTJhMi00MjQ3LWJhNTUtNjNmOGY5OGMzYjRhIOFlqZsakxUazBZV2QiLCJ0IjoiODI2ZGN0TVRJM1lXVXhaVEV3TldObSJ9WMxYzYzMWI4ZWQ3M2VliwicyI6Ik1qUTBzBZUzAwTlRkNWZjMDI0OWZmMjY3ak00WlRBd # Insert your tunnel token (munged).
# Initialization:
# Create network isolated_ns.
ExecStartPre=ip netns add isolated_ns
# Create the veth pair (veth0 and veth1) in the root network isolated_ns.
ExecStartPre=ip link add veth_host type veth peer name veth_ns
# Move veth1 into the network namespace isolated_ns.
ExecStartPre=ip link set veth_ns netns isolated_ns
# Assign IPs to and bring up the virtual ethernet interfaces.
ExecStartPre=ip addr add 192.168.100.1/24 dev veth_host
ExecStartPre=ip link set veth_host up
ExecStartPre=ip netns exec isolated_ns ip addr add 192.168.100.2/24 dev veth_ns
ExecStartPre=ip netns exec isolated_ns ip link set veth_ns up
# Set the default route and DNS server within the isolated_ns.
ExecStartPre=ip netns exec isolated_ns ip route add default via 192.168.100.1
ExecStartPre=ip netns exec isolated_ns sh -c "echo 'nameserver $DNSSERVER_IP' > /etc/resolv.conf"
# Allow localhost access inside the isolated_ns.
ExecStartPre=ip netns exec isolated_ns ip link set lo up
ExecStartPre=ip netns exec isolated_ns ip route add 127.0.0.0/8 dev lo
# 1. Inside the isolated network namespace (isolated_ns).
# Allow outgoing traffic to the DNS server on port 53.
ExecStartPre=ip netns exec isolated_ns iptables -A OUTPUT -d $DNSSERVER_IP -p tcp --dport 53 -j ACCEPT
ExecStartPre=ip netns exec isolated_ns iptables -A OUTPUT -d $DNSSERVER_IP -p udp --dport 53 -j ACCEPT
# Allow outgoing traffic to the local host's Web server on port 80 and 443.
ExecStartPre=ip netns exec isolated_ns iptables -A OUTPUT -d $WEBSERVER_IP -p tcp --dport 80 -j ACCEPT
ExecStartPre=ip netns exec isolated_ns iptables -A OUTPUT -d $WEBSERVER_IP -p tcp --dport 443 -j ACCEPT
# Allow outgoing traffic to localhost (127.0.0.1).
ExecStartPre=ip netns exec isolated_ns iptables -A OUTPUT -d 127.0.0.1 -j ACCEPT
# Allow incoming traffic from localhost (127.0.0.1).
ExecStartPre=ip netns exec isolated_ns iptables -A INPUT -s 127.0.0.1 -j ACCEPT
# Allow return traffic for related and established connections (response to queries).
ExecStartPre=ip netns exec isolated_ns iptables -A INPUT -m state --state RELATED,ESTABLISHED -j ACCEPT
# Block outgoing traffic to the local LAN (192.168.0.0/24) except for the Gateway, Webserver and DNS server.
ExecStartPre=ip netns exec isolated_ns iptables -A OUTPUT -d $LAN_IP_RANGE -j REJECT
# Block incoming traffic from the local LAN (192.168.0.0/24) except for the Gateway, Webserver and DNS server.
ExecStartPre=ip netns exec isolated_ns iptables -A INPUT -s $LAN_IP_RANGE -j REJECT
# 2. On the Host (Outside the isolated_ns).
# Enable IP forwarding on the host.
ExecStartPre=sysctl -w net.ipv4.ip_forward=1
# Set up NAT for traffic from the isolated network (192.168.100.0/24) through the host (eth0).
ExecStartPre=iptables -t nat -A POSTROUTING -s 192.168.100.0/24 -o eth0 -j MASQUERADE
# Allow forwarding from the isolated_ns through the Gateway on ports 7844, 443 and 53.
ExecStartPre=iptables -A FORWARD -i veth_host -o eth0 -p tcp --dport 7844 -j ACCEPT
ExecStartPre=iptables -A FORWARD -i veth_host -o eth0 -p udp --dport 7844 -j ACCEPT
ExecStartPre=iptables -A FORWARD -i veth_host -o eth0 -p tcp --dport 443 -j ACCEPT
ExecStartPre=iptables -A FORWARD -i veth_host -o eth0 -p tcp --dport 53 -j ACCEPT
ExecStartPre=iptables -A FORWARD -i veth_host -o eth0 -p udp --dport 53 -j ACCEPT
# Allow return traffic from the internet to the isolated_ns on ports 7844, 443 and 53 (return traffic).
ExecStartPre=iptables -A FORWARD -i eth0 -o veth_host -m state --state RELATED,ESTABLISHED -j ACCEPT
# Drop all other incoming traffic to the isolated_ns.
ExecStartPre=iptables -A FORWARD -i eth0 -o veth_host -j REJECT
# Drop all other outgoing traffic from the isolated_ns.
ExecStartPre=iptables -A FORWARD -i veth_host -o eth0 -j REJECT
# Isolate isolated_ns to prevent it using IPv6 traffic in, out or forward.
ExecStartPre=ip netns exec isolated_ns ip6tables -P INPUT DROP
ExecStartPre=ip netns exec isolated_ns ip6tables -P OUTPUT DROP
ExecStartPre=ip netns exec isolated_ns ip6tables -P FORWARD DROP
# Run the cloudflared tunnel service in network namespace isolation.
ExecStart=ip netns exec isolated_ns cloudflared tunnel run --token $TUNNEL_TOKEN
# De-initialization
# 1. Inside the isolated_ns (Isolated Network isolated_ns).
# Remove the rule allowing outgoing traffic to the DNS server on port 53.
ExecStop=ip netns exec isolated_ns iptables -D OUTPUT -d $DNSSERVER_IP -p tcp --dport 53 -j ACCEPT
ExecStop=ip netns exec isolated_ns iptables -D OUTPUT -d $DNSSERVER_IP -p udp --dport 53 -j ACCEPT
# Remove the rule allowing outgoing traffic to the local host's Web server on port 80 and 443.
ExecStop=ip netns exec isolated_ns iptables -D OUTPUT -d $WEBSERVER_IP -p tcp --dport 80 -j ACCEPT
ExecStop=ip netns exec isolated_ns iptables -D OUTPUT -d $WEBSERVER_IP -p tcp --dport 443 -j ACCEPT
# Remove the rule allowing outgoing traffic to localhost (127.0.0.1).
ExecStop=ip netns exec isolated_ns iptables -D OUTPUT -d 127.0.0.1 -j ACCEPT
# Remove the rule allowing incoming traffic from localhost (127.0.0.1).
ExecStop=ip netns exec isolated_ns iptables -D INPUT -s 127.0.0.1 -j ACCEPT
# Remove the rule allowing return traffic for related and established connections.
ExecStop=ip netns exec isolated_ns iptables -D INPUT -m state --state RELATED,ESTABLISHED -j ACCEPT
# Remove the rule blocking outgoing traffic to the local LAN (192.168.0.0/24).
ExecStop=ip netns exec isolated_ns iptables -D OUTPUT -d $LAN_IP_RANGE -j REJECT
# Remove the rule blocking incoming traffic from the local LAN (192.168.0.0/24).
ExecStop=ip netns exec isolated_ns iptables -D INPUT -s $LAN_IP_RANGE -j REJECT
# 2. On the Host (Outside the isolated_ns).
# Disable IP forwarding on the host.
ExecStop=sysctl -w net.ipv4.ip_forward=0
# Remove the NAT rule for traffic from the isolated_ns through the host.
ExecStop=iptables -t nat -D POSTROUTING -s 192.168.100.0/24 -o eth0 -j MASQUERADE
# Remove forwarding from the isolated_ns to the Gateway on ports 7844, 443 and 53.
ExecStop=iptables -D FORWARD -i veth_host -o eth0 -p tcp --dport 7844 -j ACCEPT
ExecStop=iptables -D FORWARD -i veth_host -o eth0 -p udp --dport 7844 -j ACCEPT
ExecStop=iptables -D FORWARD -i veth_host -o eth0 -p tcp --dport 443 -j ACCEPT
ExecStop=iptables -D FORWARD -i veth_host -o eth0 -p tcp --dport 53 -j ACCEPT
ExecStop=iptables -D FORWARD -i veth_host -o eth0 -p udp --dport 53 -j ACCEPT
# Remove the rule allowing return traffic from the internet to the isolated_ns.
ExecStop=iptables -D FORWARD -i eth0 -o veth_host -m state --state RELATED,ESTABLISHED -j ACCEPT
# Remove the rule rejecting incoming traffic to the isolated_ns.
ExecStop=iptables -D FORWARD -i eth0 -o veth_host -j REJECT
# Remove the rule rejecting outgoing traffic from the isolated_ns.
ExecStop=iptables -D FORWARD -i veth_host -o eth0 -j REJECT
# Remove the default route and DNS configuration from the isolated_ns.
ExecStop=ip netns exec isolated_ns ip route del default via 192.168.100.1
ExecStop=ip netns exec isolated_ns sh -c "echo '' > /etc/resolv.conf"
# Delete the loopback interface route (optional cleanup).
ExecStop=ip netns exec isolated_ns ip route del 127.0.0.0/8 dev lo
# Bring down the veth interfaces.
ExecStop=ip link set veth_host down
ExecStop=ip netns exec isolated_ns ip link set veth_ns down
# Delete the veth pair.
ExecStop=ip link delete veth_host
# Delete the network namespace isolated_ns.
ExecStop=ip netns del isolated_ns
Restart=on-failure
RestartSec=5s
[Install]
WantedBy=multi-user.target
The security enhanced systemd update unit file /etc/systemd/system/cloudflared-update.service reads as follows:
[Unit]
Description=Update cloudflared
After=network-online.target
Wants=network-online.target
[Service]
ExecStart=/bin/bash -c '/usr/bin/ip netns exec isolated_ns /usr/bin/cloudflared update; code=$?; if [ $code -eq 11 ]; then systemctl restart cloudflared; exit 0; fi; exit $code'
The system unit presented is far from robust, and will leave artefacts from any previous, failed invocation. These will cause subsequent executions to fail repeatedly. To clean-up all remnants, and start the service reliably and cleanly, use the following sequence of commands:
sudo systemctl disable cloudflared.service
sudo systemctl stop cloudflared.service
sudo ip netns del isolated_ns # Just to be sure.
sudo systemctl enable cloudflared.service
sudo reboot
Here is a list of commands you might find useful, to assist in configuring, running and debugging the systemd unit:
sudo nano /etc/systemd/system/cloudflared.service
sudo nano /etc/systemd/system/cloudflared-update.service
sudo systemctl daemon-reload
sudo ip netns add isolated_ns
sudo ip netns del isolated_ns
sudo systemctl enable cloudflared.service
sudo systemctl disable cloudflared.service
sudo systemctl start cloudflared.service
sudo systemctl stop cloudflared.service
sudo systemctl status cloudflared.service
journalctl -u cloudflared.service -b | grep -i error
Warnings in the cloudflared.service log relating to ICMP can safely be ignored. I could find no way to provide the isolated cloudflared executable with raw socket creation capability (needed to ping), and the service works fine without it. If you can find some means to accomplish this, then I’m all ears.
