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)