Commit ebaa90

2025-02-27 12:35:52 Samuli Seppänen: Add UnprivilegedUser page
/dev/null .. Pages/UnprivilegedUser.md
@@ 0,0 1,388 @@
+ 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.
+
+ 1. Install podman (https://podman.io/getting-started/installation)
+
+ 2. Create unprivileged user
+
+ ```
+ useradd -m openvpn
+ # Automatically start-up systemd user instances
+ loginctl enable-linger openvpn
+ ```
+
+ 3. Create directories where configuration, certificates and entrypoint script will be stored
+
+ ```
+ mkdir -p /opt/openvpn/server/{ssl,status,ccd}
+ ```
+
+ 4. 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
+
+ #KeepConfigurationyes
+ #BindCarrieryes
+ #CriticalConnectionyes
+
+ ConfigureWithoutCarrier=yes
+ IgnoreCarrierLoss=yes
+ IPForward=yes
+
+ [Link]
+ MTUBytes=1389
+ EOF
+
+ systemctl restart systemd-networkd
+ ```
+
+ 5. 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
+
+ 6. 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
+ ```
+
+ 7. Ensure /opt/openvpn is owned by and can be read only by openvpn:openvpn
+
+ 8. 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
+ ```
+
+ 9. 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
+ ```
+
+ 10. 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)
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9