systemd hardening
When installing Vikunja on a systemd-based system, the service may be configured to run as root. It is generally a good idea to run services with the least amount of privileges they need. This guide shows how to run Vikunja as its own dedicated user and how to optionally add extra security restrictions using systemd.
The unit shipped with the deb and rpm packages already applies the restrictions that are safe for a service running as root — blocking access to kernel tunables, modules, namespaces and the like. The sandboxing below goes further by restricting which parts of the filesystem Vikunja can see, which is only safe once it runs as its own user with its own data directory.
Create a dedicated user#
Create a system account for Vikunja that will not be able to log in interactively and will only be used to run the service. --user-group also creates the matching vikunja group that the unit below refers to:
sudo useradd --system --user-group --home-dir /var/lib/vikunja --shell /usr/sbin/nologin vikunja
Next, ensure Vikunja has its own directory for data, and that the new user owns it:
sudo mkdir -p /var/lib/vikunja
sudo chown -R vikunja:vikunja /var/lib/vikunja
The config file holds your database password and the secret used to sign tokens, so give the new user read access without making it readable by everyone:
sudo chown root:vikunja /etc/vikunja/config.yml
sudo chmod 640 /etc/vikunja/config.yml
If you are logging to files, create a log directory and update the paths in your config.yml under the log section.
sudo mkdir -p /var/log/vikunja
sudo chown -R vikunja:vikunja /var/log/vikunja
Set the data paths explicitly#
Do this before restarting, or Vikunja will come up with an empty database.
Vikunja resolves relative data paths against service.rootpath, which defaults to the service’s working directory. Changing WorkingDirectory therefore changes where a relative path points. The packaged config.yml has every option commented out, so the defaults apply: moving the working directory to /var/lib/vikunja moves the expected database from /opt/vikunja/vikunja.db to /var/lib/vikunja/vikunja.db. Vikunja finds nothing there, creates a new empty database, and your existing data is left behind in /opt/vikunja.
Pin the paths in /etc/vikunja/config.yml so they no longer depend on the working directory:
database:
path: "/var/lib/vikunja/vikunja.db"
files:
basepath: "/var/lib/vikunja/files"
Then stop the service and move your existing data across:
sudo systemctl stop vikunja.service
sudo mv /opt/vikunja/vikunja.db* /var/lib/vikunja/
sudo mv /opt/vikunja/files /var/lib/vikunja/
sudo chown -R vikunja:vikunja /var/lib/vikunja
If you use MySQL or PostgreSQL there is no database file to move, only files.
Modify the systemd service unit#
The package already provides a service unit (/lib/systemd/system/vikunja.service). Modify it using:
sudo systemctl edit vikunja.service
Apply the following changes:
- Run Vikunja as the dedicated vikunja user
- Use
/var/lib/vikunjaas the working directory - Explicitly specify the config file when starting the binary
- Add basic systemd sandboxing options to limit host access
[Service]
User=vikunja
Group=vikunja
WorkingDirectory=/var/lib/vikunja
ExecStart=/usr/local/bin/vikunja
Environment=VIKUNJA_CONFIG=/etc/vikunja/config.yml
Restart=always
RestartSec=3
LimitNOFILE=65536
NoNewPrivileges=true
ProtectHome=true
PrivateTmp=true
ProtectSystem=strict
ReadWritePaths=/var/lib/vikunja
Also include your configured files and log directory in ReadWritePaths and ensure the vikunja user has the correct permissions.
ProtectHome hides /home, /root and /run/user from the service. It is safe here because the vikunja user’s home is /var/lib/vikunja, which it does not cover — but do not add it to a service still running as root. Vikunja’s home is then /root, and libpq cannot check there for a client certificate, so PostgreSQL connections with sslmode=require or stricter fail with a permission error even when no certificate exists.
If you bind Vikunja to a port below 1024, the unprivileged user needs the capability to do so:
AmbientCapabilities=CAP_NET_BIND_SERVICE
CapabilityBoundingSet=CAP_NET_BIND_SERVICE
Reload systemd and restart Vikunja#
sudo systemctl daemon-reload
sudo systemctl restart vikunja.service
Verify the service#
Check that the service is running under the correct user:
ps -u vikunja -f
Additionally, check the logs for errors:
journalctl -u vikunja.service -e
Running CLI commands#
Run Vikunja’s CLI as the service user from now on:
sudo -u vikunja vikunja migrate
As root it writes root-owned files into the data directory — including SQLite’s write-ahead log — and the service will fail to start afterwards.