By default, OpenVPN runs as the root user. This page seeks to describe how to instead run as an unprivileged user, "openvpn", instead. This is more secure than the built-in directives(--user and --group) because the openvpn process is never started with root permissions. Additionally, reconnects(including those which push fresh routes and configuration changes) which normally break after privileges are dropped via --user are handled without issue.
Configuration
Init Script
The init script is modifed to invoke the openvpn command via su instead of calling it directly(as root). It is recommended to copy the sample init script to a new one(/etc/rc.d/init.d/openvpn-su)before making these changes. Otherwise, package updates will wipe them out.
First, we must tell the init script which user to run as; insert the following near the top of the init script:
OPENVPN_USER="openvpn"
Next, remove the following line:
$openvpn --daemon --writepid $piddir/$bn.pid --config $c --cd $work $script_security
....and replace it with:
if [ -z "$OPENVPN_USER" ] then $openvpn --daemon --writepid $piddir/$bn.pid --config $c --cd $work $script_security else su $OPENVPN_USER -s /bin/sh --command="$openvpn --daemon --writepid $piddir/$bn.pid --cd $work --config $c $script_security" fi
Optional: If you would like, you could move the OPENVPN_USER variable definition into a sysconfig file, and source that instead of defining it directly. This is more in line with typical init script behavior, where a different user may be desirable. The usage of the if block in the init script is meant to accommodate the possibility of the variable being undefined(in which case, openvpn will be executed as root).
Wrapper for ip
Because openvpn will be running unprivileged, it can't execute the ip command directly. Create a wrapper script, /usr/local/sbin/unpriv-ip (remember to chmod this to 755):
#!/bin/sh sudo /sbin/ip $*
Next, grant sudo access to the openvpn user so it can use the wrapper script. Use visudo to edit your sudoers list, and insert the first line where convenient(at the end works well). NOTE: If you have previously specified "Defaults requiretty" in your sudoers(a useful additional security measure), you will need the second line as well.
openvpn ALL=(ALL) NOPASSWD: /sbin/ip Defaults:openvpn !requiretty
Secure Wrapper
Your wrapper script can be created to filter input parameters to only those legitimately used by OpenVPN:
#!/bin/bash # This script wraps `ip` to allow it to be run as root by the `openvpn` user. # You can/should extend this script to also filter IP addresses and device names. # List of allowed commands created by searching openvpn source for iproute_path # src/openvpn/lladdr.c # :31 link set addr %s dev %s # # src/openvpn/networking_iproute2.c # :68 link set dev %s up/down # :83 link set dev %s up mtu %d # # :99 addr add dev %s %s/%d # :116 -6 addr add %s/%d dev %s # :170 addr add dev %s local %s peer %s # # :134 addr del dev %s %s/%d # :152 -6 addr del %s/%d dev %s # :188 addr del dev %s local %s peer %s # # :206 route add %s/%d (metric %d) (dev %s) (via %s) # :237 -6 route add %s/%d dev %s # :266 route del %s/%d (metric %d) # :287 -6 route del %s/%d dev %s (via %s) (metric %d) DEBUG= debug_echo () { if [ ! -z "$DEBUG" ]; then echo "$1" >&2 fi } CMD_IP=`which ip` CMD_SUDO=`which sudo` ORIGINAL_ARGS=$* if (("x$1" == "x-6")); then debug_echo "Using IPv6" USING_IPv6=1 shift else USING_IPv6=0 fi case "$1" in "link") debug_echo "Allowed first arg: $1" case "$2 $3" in "set addr"|"set dev") debug_echo "Allowed second/third argument: $2 $3" ;; *) echo "Unrecognized second/third argument: $2 $3" exit 1 ;; esac ;; "addr") debug_echo "Allowed first arg: $1" case "$2 $3" in "addr add"|"addr del") debug_echo "Allowed second/third argument: $2 $3" ;; *) echo "Unrecognized second/third argument: $2 $3" exit 1 ;; esac ;; "route") debug_echo "Allowed first arg: $1" case "$2" in "add"|"del") debug_echo "Allowed second/third argument: $2 $3" ;; *) echo "Unrecognized second/third argument: $2 $3" exit 1 ;; esac ;; *) echo "Unrecognized first argument: $1" exit 1 ;; esac echo "$CMD_IP $ORIGINAL_ARGS" $CMD_IP $ORIGINAL_ARGS
Change the wrapper script, /usr/local/sbin/unpriv-ip
#!/bin/sh sudo /usr/local/sbin/unpriv-ip-filter $*
Grant sudo access to the openvpn user so it can use the wrapper wrapper script, but not the wrapper script or ip command directly.
openvpn ALL=(ALL) NOPASSWD: /usr/local/sbin/unpriv-ip-filter
TUN/TAP Device
Because openvpn will be running as an unprivileged user, a static tun/tap device is needed. The init script already supports running a shell script before executing openvpn, so create one to handle this task(/etc/openvpn/openvpn-startup):
#!/bin/sh openvpn --rmtun --dev tun0 openvpn --mktun --dev tun0 --dev-type tun --user openvpn --group openvpn
User
If you are using openvpn from a binary distribution(such as that provided by EPEL), there should already be an openvpn user created, but it will need to be modified slightly. If it does not exist, create it.
[root@hostname ~]# mkdir /var/lib/openvpn [root@hostname ~]# chown openvpn:openvpn /var/lib/openvpn [root@hostname ~]# usermod -d /var/lib/openvpn -s /sbin/nologin openvpn
Some other directories will need to be set up so that the openvpn user can write to them.
[root@hostname ~]# mkdir /var/log/openvpn [root@hostname ~]# chown openvpn:openvpn /var/run/openvpn /var/log/openvpn /etc/openvpn -R [root@hostname ~]# chmod u+w /var/run/openvpn /var/log/openvpn -R
Config Changes
Lastly, you need to modify your openvpn config files to take advantage of all of these changes. Add the following directives to your openvpn configuration file(/etc/openvpn/openvpn.conf):
log /var/log/openvpn/openvpn iproute /usr/local/sbin/unpriv-ip dev tun0 persist-tun
Usage
Now, give it a whirl!
[root@hostname ~]# service openvpn-su restart Shutting down openvpn: [ OK ] Starting openvpn: Sun Dec 4 03:42:19 2011 TUN/TAP device tun0 opened Sun Dec 4 03:42:19 2011 Persist state set to: ON [ OK ] [root@hostname ~]# ps -ef |grep openvpn openvpn 25557 1 0 03:42 ? 00:00:00 /usr/sbin/openvpn --daemon --wri root 25560 25499 0 03:42 pts/0 00:00:00 grep openvpn [root@hostname ~]#
Troubleshooting
Init Script
The init script changes above only apply to the default OpenVPN init scripts, not those provided by Debian/Ubuntu? and derivatives. These do not have support for the .sh auto-execute which this technique relies upon. You can try copying the default init script from the source distribution into /etc/rc.d/init.d/openvpn-su and then patching as above, but the author has not tested this methodology. Information and suggestions are welcomed.
Logs
Since openvpn is no longer being executed as root, it is unable to write to the syslog. Thus you must use /var/log/openvpn/ and the --log directive. If no files are being created inside this directory, check that the permissions on the directory are correct(it should be owned by the openvpn user, and have a mask of 0755 / drwxr-xr-x).
Sudo
Permissions
You should also look at permissions/ownership for your keydir and /etc/openvpn/. The openvpn user should be able to read these, but not write to them, and no user but openvpn should be able to read your keys.
SELinux
In case you have SELinux enabled (e.g. you're using RHEL), you will need to set up additional user policies to allow the scripts run at startup. Create the following files:
# /tmp/openvpn_unpriv_hack.te module openvpn_unpriv_hack 1.0; require { type openvpn_t; type sudo_exec_t; class file { read open execute getattr execute_no_trans }; class process setrlimit; class capability sys_resource; } #============= openvpn_t ============== allow openvpn_t sudo_exec_t:file { read open execute getattr execute_no_trans}; allow openvpn_t self:process setrlimit; allow openvpn_t self:capability sys_resource;
then compile and install the security modules:
$ checkmodule -M -m -o /tmp/openvpn_unpriv_hack.mod /tmp/openvpn_unpriv_hack.te $ semodule_package -o /tmp/openvpn_unpriv_hack.pp -m /tmp/openvpn_unpriv_hack.mod $ semodule -i /tmp/openvpn_upriv_hack.pp
and check if they have loaded correctly:
$ semodule -l | grep openvpn openvpn 1.9.1 openvpn_unpriv_hack 1.0
Run OpenVPN within unprivileged podman container
I was able to run openvpn from within unprivileged podman container.
- Install podman (https://podman.io/getting-started/installation)
- Create unprivileged user
useradd -m openvpn # Automatically start-up systemd user instances loginctl enable-linger openvpn
- Create directories where configuration, certificates and entrypoint script will be stored
mkdir -p /opt/openvpn/server/{ssl,status,ccd}
- Make systemd-networkd to create tun0 which will be required by openvpn in later step
cat > /etc/systemd/network/21_openvpn.tun0.netdev<<EOF [NetDev] Name=tun0 Kind=tun [Tun] User=openvpn Group=openvpn EOF cat > /etc/systemd/network/22_openvpn.tun0.network<<EOF [Match] Name=tun0 [Network] Address=10.254.254.1/24 #KeepConfiguration=yes #BindCarrier=yes #CriticalConnection=yes ConfigureWithoutCarrier=yes IgnoreCarrierLoss=yes IPForward=yes [Link] MTUBytes=1389 EOF systemctl restart systemd-networkd
- Use easy-rsa to create your CA authority and all required certificates, at the end of this step you should create ca.crt, ta.key, dh.pem, crl.pem, your_server.key, your_server.crt - copy everything to /opt/openvpn/server/ssl
- Create /opt/openvpn/server/server.conf - at least following keys should match (below is not a complete conf file, only key options are mentioned)
dev tun0 ca /server/ssl/ca.crt cert /server/ssl/your_server.crt key /server/ssl/easy-rsa/pki/private/your_server.key crl-verify /server/ssl/crl.pem dh /server/ssl/dh.pem tls-auth /server/ssl/ta.key 0 server 10.254.254.0 255.255.255.0 client-config-dir /server/ccd status /server/status/openvpn-status.log log-append /server/status/openvpn.log explicit-exit-notify 1 ccd-exclusive # Below was manually calculated, since openvpn is not allowed to update tun device link-mtu 1442 ifconfig-noexec
- Ensure /opt/openvpn is owned by and can be read only by openvpn:openvpn
- Create systemd openvpn.service file (as root)
cat > /etc/systemd/system/openvpn.service<<EOF [Unit] Description=OpenVPN in Podman container After=syslog.target network-online.target Wants=network-online.target [Service] User=openvpn Group=openvpn DeviceAllow=/dev/null rw DeviceAllow=/dev/net/tun rw DeviceAllow=/dev/fuse rw WorkingDirectory=/opt/openvpn ExecStartPre=/usr/bin/bash -c 'if [ -n "$(podman ps | grep openvpn | head -n 1)" ]; then podman stop -t 0 -i openvpn; fi' ExecStartPre=/usr/bin/bash -c 'if [ -n "$(podman ps -a | grep openvpn | head -n 1)" ]; then podman rm -i openvpn; fi' ExecStart=/usr/bin/podman run --rm --name openvpn -v /opt/openvpn/server:/server -v /run/systemd/resolve/resolv.conf:/etc/resolv.conf --network="host" -p 37898:37898 --device /dev/net/tun --device /dev/null archlinux:latest /usr/bin/bash /server/entrypoint.sh ExecStop=/usr/bin/podman stop -t 0 openvpn ProtectSystem=true RestartSec=5s Restart=on-failure TimeoutSec=5s [Install] WantedBy=multi-user.target EOF
- Create /opt/openvpn/server/entrypoint.sh
cat > /opt/openvpn/server/entrypoint.sh<<EOF #!/bin/bash pacman -Sy --noconfirm openvpn net-tools nano openvpn --cd /server --config /server/openvpn.conf EOF chmod ugo+x /opt/openvpn/server/entrypoint.sh
- Start the service (as root) - this will result in podman container running as openvpn user
systemctl start openvpn.service
Note: above should work with recent versions of Openvpn (post-November 2020, when OpenVpn? switched to netlink)