I am hardening my systemd service file for openconnect(8). In my setup, I am using vpn-slice to setup routes (I pass arguments such that it doesn't write to any files) and use various files to define connection parameters and credentials. My target system is Debian Buster (systemd version 241).
This is what I came up with:
[Unit]
Description=Openconnect VPN to %i
After=network-online.target
Wants=network-online.target
[Service]
Type=simple
EnvironmentFile=-/etc/openconnect/_default.conf
EnvironmentFile=/etc/openconnect/%i.conf
StandardInput=file:/etc/openconnect/%i.pwd
# StandardOutput is changed to `inherit` when setting StandardInput.
StandardOutput=journal
ExecStart=/usr/sbin/openconnect --non-inter --setuid=nobody --passwd-on-stdin --user "${OC_USER}" --script "${OC_SCRIPT}" ${OC_OPTIONS} "${OC_SERVER}"
Restart=always
RestartSec=1s
## security
CapabilityBoundingSet=CAP_NET_ADMIN CAP_NET_BIND_SERVICE
# required to downgrade to `nobody`
CapabilityBoundingSet=CAP_SETGID CAP_SETUID
NoNewPrivileges=true
RestrictAddressFamilies=AF_INET AF_INET6 AF_NETLINK
LockPersonality=true
MemoryDenyWriteExecute=true
#PrivateDevices=yes
#DeviceAllow=/dev/net/tun rwm
#DeviceAllow=char-misc rwm
#ReadWritePaths=/dev/net/
#PrivateUsers=true
PrivateTmp=true
ProtectSystem=strict
ProtectControlGroups=true
ProtectHome=true
ProtectHostname=true
ProtectKernelLogs=true
ProtectKernelModules=true
ProtectKernelTunables=true
ReadWritePaths=/proc/sys/net/ipv4/route/flush
RestrictNamespaces=true
RestrictRealtime=true
SystemCallArchitectures=native
SystemCallFilter=@system-service
SystemCallFilter=~@privileged @resources
SystemCallFilter=@setuid
RemoveIPC=true
[Install]
WantedBy=multi-user.target
Basically I see two issues with my current unit definiton:
- I couldn't get the
PrivateDevices
running. When activating theDeviceAllow
andReadWritePaths
above, the unit fails early:
openconnect@abc.service: Failed to set up mount namespacing: No such file or directory
openconnect@abc.service: Failed at step NAMESPACE spawning /usr/sbin/openconnect: No such file or directory
When I leave out the ReadWritePaths
, the vpn-slice
script fails instead:
Traceback (most recent call last):
File "/usr/bin/vpn-slice", line 11, in <module>
load_entry_point('vpn-slice==0.15', 'console_scripts', 'vpn-slice')()
File "/usr/lib/python3/dist-packages/vpn_slice/__main__.py", line 592, in main
do_pre_init(env, args)
File "/usr/lib/python3/dist-packages/vpn_slice/__main__.py", line 112, in do_pre_init
providers.prep.create_tunnel()
File "/usr/lib/python3/dist-packages/vpn_slice/linux.py", line 106, in create_tunnel
os.makedirs(os.path.dirname(node), exist_ok=True)
File "/usr/lib/python3.7/os.py", line 221, in makedirs
mkdir(name, mode)
OSError: [Errno 30] Read-only file system: '/dev/net'
- Right now I am passing
--setuid=nobody
toopenconnect
. However, when I activatePrivateUsers=true
openconnect fails:
Failed to bind local tun device (TUNSETIFF): Operation not permitted
To configure local networking, openconnect must be running as root
So, I was wondering if this is the best approach in the first place and experimented with running the entire unit as User=nobody
, but couldn't get this working either (I tried setting AmbientCapabilities=
).
Since the --setuid
requires several privileges I am also wondering if it could be more secure in the end to just let it run as root
formally while having all the limitations above.
Any advice for a more secure setup is highly appreciated.