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) |
