Namespaces
One Open-Source software to easily to the job: https://github.com/slingamn/namespaced-openvpn
It can be even used with systemd (source):
Before=systemd-user-sessions.service After=network-online.target Wants=network-online.target Documentation=https://github.com/slingamn/namespaced-openvpn [Service] Type=notify WorkingDirectory=/etc/openvpn ExecStart=/usr/local/sbin/namespaced-openvpn --namespace %i --daemon nsovpn-%i --status /run/namespaced-openvpn/%i.status 10 --cd /etc/openvpn --script-security 2 --config /etc/openvpn/%i.conf --auth-user-pass /etc/openvpn/auth.txt --writepid /run/namespaced-openvpn/%i.pid PIDFile=/run/namespaced-openvpn/%i.pid KillMode=process ExecReload=/bin/kill -HUP $MAINPID RestartSec=5s Restart=on-failure [Install] WantedBy=multi-user.target [Service] RuntimeDirectory=namespaced-openvpn
i.e. systemctl start nsopenvpn@configfile
Another systemd service can then easily use it by using NetworkNamespacePath=configname
inside the [Service] section.
Also with Require=nsopenvpn@configfile
it can be assured that the VPN is started before.
It will create for the process a sandboxed network environment. It is quite secure (of IP leaks) however it is also the most restrictive way. It is not possible to access local network services. To do so one would have to either add additional (possible complicated) routes/bridges or create a socat device.
A system user using a shell is also not automatic protected this way.
IPTables
If security is not that important, one can use iptables to do a similar job.
With that local communication is still possible. Also it is easy to really put the whole user inside a VPN (without requiring a special command for starting a shell inside the VPN).
#!/bin/bash
# Mark all traffic with the number 42 of a specific user when its not towards 127.0.0.1
# REPLACE THEUSERTORESTRICT
iptables -t mangle -nvL OUTPUT | grep -q 0x2a ||
iptables -t mangle -A OUTPUT ! -d 127.0.0.1 -m owner --uid-owner THEUSERTORESTRICT -j MARK --set-mark 42
# Create a new routing table if not exists
grep -q '^42 vpn$' /etc/iproute2/rt_tables ||
echo '42 vpn' >>/etc/iproute2/rt_tables
# Create a gateway with low priority which routes nowhere (to disconnect when the VPN interface is removed)
ip route add default via 127.0.0.1 metric 10000 table vpn || true
# Get the subnet of tun0 Interface, and replace the last octet with a .1 as this is commonly the gateway. Might need to be replaced
subnet=`ip -o -f inet addr show tun0 | awk '/scope global/ {print $4}' | perl -ne 's/(?<=\d.)\d{1,3}(?=\/)/0/g; print;'`
ip3=${subnet%.*}
gatewayip=$ip3.1
# Add the VPN as gateway for traffic in the routing table
ip route show table vpn | grep -q "metric 11" ||
ip route add default via $gatewayip dev tun0 metric 11 table vpn
# ip route show table vpn
# All traffic marked with the number 42 should go through the VPN routing table
ip rule | grep -q 0x2a ||
ip rule add fwmark 42 lookup vpn prio 42
# Masquerade/SNAT on the VPN interface
iptables -t nat -nvL POSTROUTING | grep -q tun0 ||
iptables -t nat -A POSTROUTING -o tun0 -j MASQUERADE
The script was written with the help of: https://unix.stackexchange.com/a/589605/388631
It is not 100% optimal. It guesses the gateway IP because OpenVPN does not transmit the Gateway IP into ENV when using the OpenVPN option pull-filter ignore redirect-gateway
Also a disconnect of tun0 is protected in a very primitive way.
I use it like this inside a custom OpenVPN systemd script (having the code from above in a file called restrictscript.sh; also don’t forget to option from above inside the vpns configurationfile):
/usr/sbin/openvpn --script-security 2 --config /etc/openvpn/myvpn.conf --auth-user-pass /etc/openvpn/auth.txt --writepid /run/namespaced-openvpn/usenetvpn.pid --route-up /etc/openvpn/restrictscript.sh
It is easily testable by suing into the user and then running curl ifconfig.me