Blame
| ebaa90 | Samuli Seppänen | 2025-02-27 12:35:52 | 1 | 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. |
| 2 | ||||
| 3 | # Configuration |
|||
| 4 | ## Init Script |
|||
| 5 | 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. |
|||
| 6 | ||||
| 7 | First, we must tell the init script which user to run as; insert the following near the top of the init script: |
|||
| 8 | ``` |
|||
| 9 | OPENVPN_USER="openvpn" |
|||
| 10 | ``` |
|||
| 11 | Next, remove the following line: |
|||
| 12 | ``` |
|||
| 13 | $openvpn --daemon --writepid $piddir/$bn.pid --config $c --cd $work $script_security |
|||
| 14 | ``` |
|||
| 15 | ....and replace it with: |
|||
| 16 | ``` |
|||
| 17 | if [ -z "$OPENVPN_USER" ] |
|||
| 18 | then |
|||
| 19 | $openvpn --daemon --writepid $piddir/$bn.pid --config $c --cd $work $script_security |
|||
| 20 | else |
|||
| 21 | su $OPENVPN_USER -s /bin/sh --command="$openvpn --daemon --writepid $piddir/$bn.pid --cd $work --config $c $script_security" |
|||
| 22 | fi |
|||
| 23 | ``` |
|||
| 24 | ||||
| 25 | 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). |
|||
| 26 | ||||
| 27 | ## Wrapper for *ip* |
|||
| 28 | 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): |
|||
| 29 | ``` |
|||
| 30 | #!/bin/sh |
|||
| 31 | sudo /sbin/ip $* |
|||
| 32 | ``` |
|||
| 33 | ||||
| 34 | 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. |
|||
| 35 | ||||
| 36 | ``` |
|||
| 37 | openvpn ALL=(ALL) NOPASSWD: /sbin/ip |
|||
| 38 | Defaults:openvpn !requiretty |
|||
| 39 | ``` |
|||
| 40 | ||||
| 41 | ### Secure Wrapper |
|||
| 42 | Your wrapper script can be created to filter input parameters to only those legitimately used by OpenVPN: |
|||
| 43 | ``` |
|||
| 44 | #!/bin/bash |
|||
| 45 | ||||
| 46 | # This script wraps `ip` to allow it to be run as root by the `openvpn` user. |
|||
| 47 | # You can/should extend this script to also filter IP addresses and device names. |
|||
| 48 | ||||
| 49 | # List of allowed commands created by searching openvpn source for iproute_path |
|||
| 50 | # src/openvpn/lladdr.c |
|||
| 51 | # :31 link set addr %s dev %s |
|||
| 52 | # |
|||
| 53 | # src/openvpn/networking_iproute2.c |
|||
| 54 | # :68 link set dev %s up/down |
|||
| 55 | # :83 link set dev %s up mtu %d |
|||
| 56 | # |
|||
| 57 | # :99 addr add dev %s %s/%d |
|||
| 58 | # :116 -6 addr add %s/%d dev %s |
|||
| 59 | # :170 addr add dev %s local %s peer %s |
|||
| 60 | # |
|||
| 61 | # :134 addr del dev %s %s/%d |
|||
| 62 | # :152 -6 addr del %s/%d dev %s |
|||
| 63 | # :188 addr del dev %s local %s peer %s |
|||
| 64 | # |
|||
| 65 | # :206 route add %s/%d (metric %d) (dev %s) (via %s) |
|||
| 66 | # :237 -6 route add %s/%d dev %s |
|||
| 67 | # :266 route del %s/%d (metric %d) |
|||
| 68 | # :287 -6 route del %s/%d dev %s (via %s) (metric %d) |
|||
| 69 | ||||
| 70 | DEBUG= |
|||
| 71 | debug_echo () { |
|||
| 72 | if [ ! -z "$DEBUG" ]; then |
|||
| 73 | echo "$1" >&2 |
|||
| 74 | fi |
|||
| 75 | } |
|||
| 76 | ||||
| 77 | CMD_IP=`which ip` |
|||
| 78 | CMD_SUDO=`which sudo` |
|||
| 79 | ||||
| 80 | ORIGINAL_ARGS=$* |
|||
| 81 | ||||
| 82 | if (("x$1" == "x-6")); then |
|||
| 83 | debug_echo "Using IPv6" |
|||
| 84 | USING_IPv6=1 |
|||
| 85 | shift |
|||
| 86 | else |
|||
| 87 | USING_IPv6=0 |
|||
| 88 | fi |
|||
| 89 | ||||
| 90 | case "$1" in |
|||
| 91 | "link") |
|||
| 92 | debug_echo "Allowed first arg: $1" |
|||
| 93 | case "$2 $3" in |
|||
| 94 | "set addr"|"set dev") |
|||
| 95 | debug_echo "Allowed second/third argument: $2 $3" |
|||
| 96 | ;; |
|||
| 97 | *) |
|||
| 98 | echo "Unrecognized second/third argument: $2 $3" |
|||
| 99 | exit 1 |
|||
| 100 | ;; |
|||
| 101 | esac |
|||
| 102 | ;; |
|||
| 103 | ||||
| 104 | "addr") |
|||
| 105 | debug_echo "Allowed first arg: $1" |
|||
| 106 | case "$2 $3" in |
|||
| 107 | "addr add"|"addr del") |
|||
| 108 | debug_echo "Allowed second/third argument: $2 $3" |
|||
| 109 | ;; |
|||
| 110 | *) |
|||
| 111 | echo "Unrecognized second/third argument: $2 $3" |
|||
| 112 | exit 1 |
|||
| 113 | ;; |
|||
| 114 | esac |
|||
| 115 | ;; |
|||
| 116 | ||||
| 117 | "route") |
|||
| 118 | debug_echo "Allowed first arg: $1" |
|||
| 119 | case "$2" in |
|||
| 120 | "add"|"del") |
|||
| 121 | debug_echo "Allowed second/third argument: $2 $3" |
|||
| 122 | ;; |
|||
| 123 | *) |
|||
| 124 | echo "Unrecognized second/third argument: $2 $3" |
|||
| 125 | exit 1 |
|||
| 126 | ;; |
|||
| 127 | esac |
|||
| 128 | ;; |
|||
| 129 | ||||
| 130 | *) |
|||
| 131 | echo "Unrecognized first argument: $1" |
|||
| 132 | exit 1 |
|||
| 133 | ;; |
|||
| 134 | esac |
|||
| 135 | ||||
| 136 | echo "$CMD_IP $ORIGINAL_ARGS" |
|||
| 137 | $CMD_IP $ORIGINAL_ARGS |
|||
| 138 | ``` |
|||
| 139 | ||||
| 140 | Change the wrapper script, **/usr/local/sbin/unpriv-ip** |
|||
| 141 | ``` |
|||
| 142 | #!/bin/sh |
|||
| 143 | sudo /usr/local/sbin/unpriv-ip-filter $* |
|||
| 144 | ``` |
|||
| 145 | ||||
| 146 | Grant sudo access to the openvpn user so it can use the wrapper wrapper script, but not the wrapper script or *ip* command directly. |
|||
| 147 | ||||
| 148 | ``` |
|||
| 149 | openvpn ALL=(ALL) NOPASSWD: /usr/local/sbin/unpriv-ip-filter |
|||
| 150 | ``` |
|||
| 151 | ||||
| 152 | ## TUN/TAP Device |
|||
| 153 | 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**): |
|||
| 154 | ||||
| 155 | ``` |
|||
| 156 | #!/bin/sh |
|||
| 157 | openvpn --rmtun --dev tun0 |
|||
| 158 | openvpn --mktun --dev tun0 --dev-type tun --user openvpn --group openvpn |
|||
| 159 | ``` |
|||
| 160 | ||||
| 161 | ## User |
|||
| 162 | 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. |
|||
| 163 | ||||
| 164 | ``` |
|||
| 165 | [root@hostname ~]# mkdir /var/lib/openvpn |
|||
| 166 | [root@hostname ~]# chown openvpn:openvpn /var/lib/openvpn |
|||
| 167 | [root@hostname ~]# usermod -d /var/lib/openvpn -s /sbin/nologin openvpn |
|||
| 168 | ``` |
|||
| 169 | ||||
| 170 | Some other directories will need to be set up so that the openvpn user can write to them. |
|||
| 171 | ||||
| 172 | ``` |
|||
| 173 | [root@hostname ~]# mkdir /var/log/openvpn |
|||
| 174 | [root@hostname ~]# chown openvpn:openvpn /var/run/openvpn /var/log/openvpn /etc/openvpn -R |
|||
| 175 | [root@hostname ~]# chmod u+w /var/run/openvpn /var/log/openvpn -R |
|||
| 176 | ``` |
|||
| 177 | ||||
| 178 | ## Config Changes |
|||
| 179 | 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**): |
|||
| 180 | ||||
| 181 | ``` |
|||
| 182 | log /var/log/openvpn/openvpn |
|||
| 183 | iproute /usr/local/sbin/unpriv-ip |
|||
| 184 | dev tun0 |
|||
| 185 | persist-tun |
|||
| 186 | ``` |
|||
| 187 | ||||
| 188 | # Usage |
|||
| 189 | Now, give it a whirl! |
|||
| 190 | ||||
| 191 | ``` |
|||
| 192 | [root@hostname ~]# service openvpn-su restart |
|||
| 193 | Shutting down openvpn: [ OK ] |
|||
| 194 | Starting openvpn: Sun Dec 4 03:42:19 2011 TUN/TAP device tun0 opened |
|||
| 195 | Sun Dec 4 03:42:19 2011 Persist state set to: ON |
|||
| 196 | [ OK ] |
|||
| 197 | [root@hostname ~]# ps -ef |grep openvpn |
|||
| 198 | openvpn 25557 1 0 03:42 ? 00:00:00 /usr/sbin/openvpn --daemon --wri |
|||
| 199 | root 25560 25499 0 03:42 pts/0 00:00:00 grep openvpn |
|||
| 200 | [root@hostname ~]# |
|||
| 201 | ``` |
|||
| 202 | ||||
| 203 | ## Troubleshooting |
|||
| 204 | ||||
| 205 | ### Init Script |
|||
| 206 | 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. |
|||
| 207 | ||||
| 208 | ### Logs |
|||
| 209 | 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). |
|||
| 210 | ||||
| 211 | ### Sudo |
|||
| 212 | ||||
| 213 | ||||
| 214 | ### Permissions |
|||
| 215 | ||||
| 216 | 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. |
|||
| 217 | ||||
| 218 | ### SELinux |
|||
| 219 | ||||
| 220 | 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. |
|||
| 221 | Create the following files: |
|||
| 222 | ``` |
|||
| 223 | # /tmp/openvpn_unpriv_hack.te |
|||
| 224 | ||||
| 225 | module openvpn_unpriv_hack 1.0; |
|||
| 226 | ||||
| 227 | require { |
|||
| 228 | type openvpn_t; |
|||
| 229 | type sudo_exec_t; |
|||
| 230 | class file { read open execute getattr execute_no_trans }; |
|||
| 231 | class process setrlimit; |
|||
| 232 | class capability sys_resource; |
|||
| 233 | } |
|||
| 234 | ||||
| 235 | #============= openvpn_t ============== |
|||
| 236 | allow openvpn_t sudo_exec_t:file { read open execute getattr execute_no_trans}; |
|||
| 237 | allow openvpn_t self:process setrlimit; |
|||
| 238 | allow openvpn_t self:capability sys_resource; |
|||
| 239 | ``` |
|||
| 240 | then compile and install the security modules: |
|||
| 241 | ``` |
|||
| 242 | $ checkmodule -M -m -o /tmp/openvpn_unpriv_hack.mod /tmp/openvpn_unpriv_hack.te |
|||
| 243 | $ semodule_package -o /tmp/openvpn_unpriv_hack.pp -m /tmp/openvpn_unpriv_hack.mod |
|||
| 244 | $ semodule -i /tmp/openvpn_upriv_hack.pp |
|||
| 245 | ``` |
|||
| 246 | and check if they have loaded correctly: |
|||
| 247 | ``` |
|||
| 248 | $ semodule -l | grep openvpn |
|||
| 249 | openvpn 1.9.1 |
|||
| 250 | openvpn_unpriv_hack 1.0 |
|||
| 251 | ``` |
|||
| 252 | ||||
| 253 | # Run OpenVPN within unprivileged podman container |
|||
| 254 | ||||
| 255 | I was able to run openvpn from within unprivileged podman container. |
|||
| 256 | ||||
| 257 | 1. Install podman (https://podman.io/getting-started/installation) |
|||
| 258 | ||||
| 259 | 2. Create unprivileged user |
|||
| 260 | ||||
| 261 | ``` |
|||
| 262 | useradd -m openvpn |
|||
| 263 | # Automatically start-up systemd user instances |
|||
| 264 | loginctl enable-linger openvpn |
|||
| 265 | ``` |
|||
| 266 | ||||
| 267 | 3. Create directories where configuration, certificates and entrypoint script will be stored |
|||
| 268 | ||||
| 269 | ``` |
|||
| 270 | mkdir -p /opt/openvpn/server/{ssl,status,ccd} |
|||
| 271 | ``` |
|||
| 272 | ||||
| 273 | 4. Make systemd-networkd to create tun0 which will be required by openvpn in later step |
|||
| 274 | ||||
| 275 | ``` |
|||
| 276 | cat > /etc/systemd/network/21_openvpn.tun0.netdev<<EOF |
|||
| 277 | [NetDev] |
|||
| 278 | Name=tun0 |
|||
| 279 | Kind=tun |
|||
| 280 | ||||
| 281 | [Tun] |
|||
| 282 | User=openvpn |
|||
| 283 | Group=openvpn |
|||
| 284 | EOF |
|||
| 285 | ||||
| 286 | cat > /etc/systemd/network/22_openvpn.tun0.network<<EOF |
|||
| 287 | [Match] |
|||
| 288 | Name=tun0 |
|||
| 289 | ||||
| 290 | [Network] |
|||
| 291 | Address=10.254.254.1/24 |
|||
| 292 | ||||
| 293 | #KeepConfigurationyes |
|||
| 294 | #BindCarrieryes |
|||
| 295 | #CriticalConnectionyes |
|||
| 296 | ||||
| 297 | ConfigureWithoutCarrier=yes |
|||
| 298 | IgnoreCarrierLoss=yes |
|||
| 299 | IPForward=yes |
|||
| 300 | ||||
| 301 | [Link] |
|||
| 302 | MTUBytes=1389 |
|||
| 303 | EOF |
|||
| 304 | ||||
| 305 | systemctl restart systemd-networkd |
|||
| 306 | ``` |
|||
| 307 | ||||
| 308 | 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 |
|||
| 309 | ||||
| 310 | 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) |
|||
| 311 | ||||
| 312 | ``` |
|||
| 313 | dev tun0 |
|||
| 314 | ca /server/ssl/ca.crt |
|||
| 315 | cert /server/ssl/your_server.crt |
|||
| 316 | key /server/ssl/easy-rsa/pki/private/your_server.key |
|||
| 317 | crl-verify /server/ssl/crl.pem |
|||
| 318 | dh /server/ssl/dh.pem |
|||
| 319 | tls-auth /server/ssl/ta.key 0 |
|||
| 320 | server 10.254.254.0 255.255.255.0 |
|||
| 321 | client-config-dir /server/ccd |
|||
| 322 | status /server/status/openvpn-status.log |
|||
| 323 | log-append /server/status/openvpn.log |
|||
| 324 | explicit-exit-notify 1 |
|||
| 325 | ccd-exclusive |
|||
| 326 | # Below was manually calculated, since openvpn is not allowed to update tun device |
|||
| 327 | link-mtu 1442 |
|||
| 328 | ifconfig-noexec |
|||
| 329 | ``` |
|||
| 330 | ||||
| 331 | 7. Ensure /opt/openvpn is owned by and can be read only by openvpn:openvpn |
|||
| 332 | ||||
| 333 | 8. Create systemd openvpn.service file (as root) |
|||
| 334 | ||||
| 335 | ``` |
|||
| 336 | cat > /etc/systemd/system/openvpn.service<<EOF |
|||
| 337 | [Unit] |
|||
| 338 | Description=OpenVPN in Podman container |
|||
| 339 | After=syslog.target network-online.target |
|||
| 340 | Wants=network-online.target |
|||
| 341 | ||||
| 342 | [Service] |
|||
| 343 | User=openvpn |
|||
| 344 | Group=openvpn |
|||
| 345 | ||||
| 346 | DeviceAllow=/dev/null rw |
|||
| 347 | DeviceAllow=/dev/net/tun rw |
|||
| 348 | DeviceAllow=/dev/fuse rw |
|||
| 349 | ||||
| 350 | WorkingDirectory=/opt/openvpn |
|||
| 351 | ||||
| 352 | ExecStartPre=/usr/bin/bash -c 'if [ -n "$(podman ps | grep openvpn | head -n 1)" ]; then podman stop -t 0 -i openvpn; fi' |
|||
| 353 | ExecStartPre=/usr/bin/bash -c 'if [ -n "$(podman ps -a | grep openvpn | head -n 1)" ]; then podman rm -i openvpn; fi' |
|||
| 354 | 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 |
|||
| 355 | ||||
| 356 | ExecStop=/usr/bin/podman stop -t 0 openvpn |
|||
| 357 | ProtectSystem=true |
|||
| 358 | RestartSec=5s |
|||
| 359 | Restart=on-failure |
|||
| 360 | TimeoutSec=5s |
|||
| 361 | ||||
| 362 | [Install] |
|||
| 363 | WantedBy=multi-user.target |
|||
| 364 | EOF |
|||
| 365 | ``` |
|||
| 366 | ||||
| 367 | 9. Create /opt/openvpn/server/entrypoint.sh |
|||
| 368 | ||||
| 369 | ``` |
|||
| 370 | cat > /opt/openvpn/server/entrypoint.sh<<EOF |
|||
| 371 | #!/bin/bash |
|||
| 372 | ||||
| 373 | pacman -Sy --noconfirm openvpn net-tools nano |
|||
| 374 | openvpn --cd /server --config /server/openvpn.conf |
|||
| 375 | ||||
| 376 | EOF |
|||
| 377 | ||||
| 378 | chmod ugo+x /opt/openvpn/server/entrypoint.sh |
|||
| 379 | ``` |
|||
| 380 | ||||
| 381 | 10. Start the service (as root) - this will result in podman container running as openvpn user |
|||
| 382 | ||||
| 383 | ``` |
|||
| 384 | systemctl start openvpn.service |
|||
| 385 | ``` |
|||
| 386 | ||||
| 387 | ||||
| 388 | Note: above should work with recent versions of Openvpn (post-November 2020, when OpenVpn switched to netlink) |
