# Systemd login services This is a continuation exercise to highlight advanced possibilities of `systemd`. Imagine we want to give each user a personal website folder in their `~/` directory but *only* active when they are logged into the machine. The site would be accessed by going to `http://localhost/$USERNAME` so for me it would be `http://192.168.0.38/waldek`. This website could give them stats, or just a hello message. In order to to this we'll need a basic Debian machine with a webserver installed. I would go for `nginx` but you can do `apache2` if you prefer. ## The webserver ``` ➜ ~ sudo apt install nginx Reading package lists... Done Building dependency tree Reading state information... Done nginx is already the newest version (1.14.2-2+deb10u4). 0 upgraded, 0 newly installed, 0 to remove and 0 not upgraded. ➜ ~ ``` This webserver only needs to offer these personal websites so we can go ahead and deactivate the default site. ``` ➜ ~ ls /etc/nginx/sites-enabled/ -l total 0 lrwxrwxrwx 1 root root 34 Aug 29 21:38 default -> /etc/nginx/sites-available/default ➜ ~ sudo rm /etc/nginx/sites-enabled/default ➜ ~ ls /etc/nginx/sites-enabled/ -l total 0 ➜ ~ ``` We'll create a configuration file from scratch to host our user websites. In `/etc/nginx/sites-available` you should create a `user-site.conf` file and put the following content in there. It's a *super* basic configuration but it works! ``` server { listen 80 default_server; location ~ ^/(.+?)(/.*)?$ { alias /home/$1/site$2; index index.html index.htm; autoindex on; } } ``` Now we symlink that file to the `/etc/nginx/sites-enabled` directory and we reload our webserver. The advantage of reloading instead of restarting is that the webserver won't have any downtime. On serious production servers this can be a handy feature. ``` ➜ ~ sudo ln -s /etc/nginx/sites-available/user_site.conf /etc/nginx/sites-enabled ➜ ~ ls /etc/nginx/sites-enabled/ -l total 0 lrwxrwxrwx 1 root root 41 Aug 29 21:43 user_site.conf -> /etc/nginx/sites-available/user_site.conf ➜ ~ sudo systemctl reload nginx.service ``` We can test our website by using `wget` and showing the output on `STDOUT` with the following command. ``` ➜ ~ wget -O - localhost/waldek --2021-08-29 21:46:45-- http://localhost/waldek Resolving localhost (localhost)... ::1, 127.0.0.1 Connecting to localhost (localhost)|::1|:80... connected. HTTP request sent, awaiting response... 404 Not Found 2021-08-29 21:46:45 ERROR 404: Not Found. ➜ ~ ``` The 404 error is because we don't *have* a personal website in our home folder so let's quickly create one and test again with `wget`. This time around I'll add the `-q` argument to reduce the verbosity. ``` ➜ ~ mkdir ~/site && echo "hello world" > ~/site/index.html ➜ ~ wget -q -O - localhost/waldek hello world ➜ ~ ``` ## More users Let's add a second user to our machine to test out our system. We'll also need to add the site directory and test it all out. ``` ➜ ~ sudo adduser alice ➜ ~ sudo su alice alice@squid:/home/waldek$ cd && mkdir site && echo "this is Alice her website" > site/index.html alice@squid:~$ cat site/index.html this is Alice her website alice@squid:~$ exit exit ➜ ~ wget -q -O - localhost/alice this is Alice her website ➜ ~ ``` It's working nicely as expected but all these users will have their website running permanently and we want them only available when they are logged in. How would we go about that? Let's have a dive into our running services. ``` ➜ ~ sudo systemctl --type=service --no-pager UNIT LOAD ACTIVE SUB DESCRIPTION apparmor.service loaded active exited Load AppArmor profiles console-setup.service loaded active exited Set console font and keymap cron.service loaded active running Regular background program processing daemon dbus.service loaded active running D-Bus System Message Bus getty@tty1.service loaded active running Getty on tty1 ifup@enp1s0.service loaded active exited ifup for enp1s0 ifupdown-pre.service loaded active exited Helper to synchronize boot up for ifupdown keyboard-setup.service loaded active exited Set the console keyboard layout kmod-static-nodes.service loaded active exited Create list of required static device nodes for the current kernel networking.service loaded active exited Raise network interfaces nginx.service loaded active running A high performance web server and a reverse proxy server ntopng.service loaded active running ntopng - High-Speed Web-based Traffic Analysis and Flow Collection Tool redis-server.service loaded active running Advanced key-value store rsyslog.service loaded active running System Logging Service serial-getty@ttyS0.service loaded active running Serial Getty on ttyS0 ssh.service loaded active running OpenBSD Secure Shell server systemd-journal-flush.service loaded active exited Flush Journal to Persistent Storage systemd-journald.service loaded active running Journal Service systemd-logind.service loaded active running Login Service systemd-modules-load.service loaded active exited Load Kernel Modules systemd-random-seed.service loaded active exited Load/Save Random Seed systemd-remount-fs.service loaded active exited Remount Root and Kernel File Systems systemd-sysctl.service loaded active exited Apply Kernel Variables systemd-sysusers.service loaded active exited Create System Users systemd-timesyncd.service loaded active running Network Time Synchronization systemd-tmpfiles-setup-dev.service loaded active exited Create Static Device Nodes in /dev systemd-tmpfiles-setup.service loaded active exited Create Volatile Files and Directories systemd-udev-trigger.service loaded active exited udev Coldplug all Devices systemd-udevd.service loaded active running udev Kernel Device Manager systemd-update-utmp.service loaded active exited Update UTMP about System Boot/Shutdown systemd-user-sessions.service loaded active exited Permit User Sessions user-runtime-dir@1000.service loaded active exited User Runtime Directory /run/user/1000 user@1000.service loaded active running User Manager for UID 1000 LOAD = Reflects whether the unit definition was properly loaded. ACTIVE = The high-level unit activation state, i.e. generalization of SUB. SUB = The low-level unit activation state, values depend on unit type. 33 loaded units listed. Pass --all to see loaded but inactive units, too. To show all installed unit files use 'systemctl list-unit-files'. ➜ ~ ``` We're currently the only user logged in on this system. This is verifiable with a few commands. `waldek` is the only one logged in but over a few different connections, some running tmux, some not. ``` ➜ ~ who waldek pts/0 2021-08-29 19:54 (192.168.0.33) waldek pts/1 2021-08-29 21:40 (tmux(1554).%4) waldek pts/2 2021-08-29 20:37 (tmux(1554).%0) waldek pts/3 2021-08-29 20:38 (tmux(1554).%1) waldek pts/4 2021-08-29 20:50 (tmux(1554).%2) waldek pts/5 2021-08-29 20:51 (tmux(1554).%3) waldek pts/6 2021-08-29 22:01 (192.168.0.33) ➜ ~ ``` The first user account created on most Linux machine is the UID 1000. We can use the `id` command to find out which UID is assigned to a specific user or vice a versa. ``` ➜ ~ id -u -n 1000 waldek ➜ ~ id -u alice 1002 ``` Now going back to the list of running service for our UID or just 'user' we get the following. ``` ➜ ~ sudo systemctl --type=service --no-pager | grep 1000 user-runtime-dir@1000.service loaded active exited User Runtime Directory /run/user/1000 user@1000.service loaded active running User Manager for UID 1000 ➜ ~ sudo systemctl --type=service --no-pager | grep user systemd-sysusers.service loaded active exited Create System Users systemd-user-sessions.service loaded active exited Permit User Sessions user-runtime-dir@1000.service loaded active exited User Runtime Directory /run/user/1000 user@1000.service loaded active running User Manager for UID 1000 ➜ ~ ``` We've already made an `@.service` ourselves so we *know* it's a template that is instantiated multiple times. Let's log in as `alice` and see what we get. ``` ➜ ~ who waldek pts/0 2021-08-29 19:54 (192.168.0.33) waldek pts/1 2021-08-29 21:40 (tmux(1554).%4) waldek pts/2 2021-08-29 20:37 (tmux(1554).%0) waldek pts/3 2021-08-29 20:38 (tmux(1554).%1) waldek pts/4 2021-08-29 20:50 (tmux(1554).%2) waldek pts/5 2021-08-29 20:51 (tmux(1554).%3) waldek pts/6 2021-08-29 22:01 (192.168.0.33) alice pts/7 2021-08-29 22:12 (192.168.0.33) ➜ ~ sudo systemctl --type=service --no-pager | grep user systemd-sysusers.service loaded active exited Create System Users systemd-user-sessions.service loaded active exited Permit User Sessions user-runtime-dir@1000.service loaded active exited User Runtime Directory /run/user/1000 user-runtime-dir@1002.service loaded active exited User Runtime Directory /run/user/1002 user@1000.service loaded active running User Manager for UID 1000 user@1002.service loaded active running User Manager for UID 1002 ➜ ~ ``` `who` tells us alice is definitely logged in and the list of running services shows a new **instance** of the `user@.service` running. We can peak at the configuration file to learn more about this service. ``` ➜ ~ sudo systemctl cat user@.service # /usr/lib/systemd/system/user@.service # SPDX-License-Identifier: LGPL-2.1+ # # This file is part of systemd. # # systemd is free software; you can redistribute it and/or modify it # under the terms of the GNU Lesser General Public License as published by # the Free Software Foundation; either version 2.1 of the License, or # (at your option) any later version. [Unit] Description=User Manager for UID %i Documentation=man:user@.service(5) After=systemd-user-sessions.service user-runtime-dir@%i.service dbus.service Requires=user-runtime-dir@%i.service IgnoreOnIsolate=yes [Service] User=%i PAMName=systemd-user Type=notify ExecStart=-/lib/systemd/systemd --user Slice=user-%i.slice KillMode=mixed Delegate=pids memory TasksMax=infinity TimeoutStopSec=120s ➜ ~ ``` This service is run whenever a user logs in and remains running until they log out. If they log in multiple times, the service is not disturbed so it's a good point to attach our *actions* to. But what will we do? We'll need to write a custom service that starts and stops each individual website. A script is probably the way to go but I can think of a different way as well. We can try that afterwards. ## The script The script needs to do two things. * start the website * stop the website But it also needs to be modular because we'll want to reuse it for all users on the machine. We know that we can pass information from the service to the script, like we did for the USB stick. Starting and stopping is a bit out of place here, because we won't stop `nginx` itself. We just need to remove the website or put a 'user is offline...' message in the `index.html` file. One script that does multiple things screams **functions** and **case**! A quick and dirty script can be seen below. ``` ➜ ~ cat site-manager.sh #!/bin/bash function start(){ mkdir -p /home/$1/site && echo "created online site dir for $1" chown $1:$1 /home/$1/site/* && echo "chowned all site files to $1:$1" echo "$1 logged in..." > /home/$1/site/index.html && echo "online site installed for $1" } function stop(){ mkdir -p /home/$1/.offline-site && echo "created offline site dir for $1" chown $1:$1 /home/$1/site/* && echo "chowned all site files to $1:$1" mv /home/$1/site/* /home/$1/.offline-site/ && echo "moved online site to offline site for $1" echo "$1 not logged in..." > /home/$1/site/index.html && echo "offline site installed for $1" } USER="$(id -u -n $2)" case $1 in start) start $USER ;; stop) stop $USER ;; *) echo -n "NOP" ;; esac ➜ ~ ``` If we try out this script, as `sudo` because we need to modify files owned by other users, we get the following. It's a proof of concept but more than enough to continue with the service files. ``` ➜ ~ sudo ./site-manager.sh start 1002 created online site dir for alice chowned all site files to alice:alice online site installed for alice ➜ ~ wget -q -O - localhost/alice alice logged in... ➜ ~ sudo ./site-manager.sh stop 1002 created offline site dir for alice chowned all site files to alice:alice moved online site to offline site for alice offline site installed for alice ➜ ~ wget -q -O - localhost/alice alice not logged in... ➜ ~ ``` ## The service Now that we have a functional script, we can write a service file for it. As we'll be using it for more than one user, we'll do a template. You can name it whatever you want but I went for the following. ``` ➜ ~ sudo systemctl cat user-website@.service # /etc/systemd/system/user-website@.service [Unit] Description=User %i website service PartOf=user@%i.service After=systemd-user-sessions.service dbus.service [Service] Type=oneshot RemainAfterExit=true ExecStart=/home/waldek/site-manager.sh start %i ExecStop=/home/waldek/site-manager.sh stop %i ➜ ~ ``` There are a couple of new thing in this service file so let's break them down. This time we specify the `Type` of service. We can read the `man systemd.service` for more information but the gist of it is this. ``` • Behavior of oneshot is similar to simple; however, the service manager will consider the unit started after the main process exits. It will then start follow-up units. RemainAfterExit= is particularly useful for this type of service. Type=oneshot is the implied default if neither Type= nor ExecStart= are specified. ``` The combination of `Type=oneshot` and `RemainAfterExit=true` make it so that the service is started and remains active even after the script has finished. In order for the service to *stop* when the user logs out, we need the `PartOf` line. ``` PartOf= Configures dependencies similar to Requires=, but limited to stopping and restarting of units. When systemd stops or restarts the units listed here, the action is propagated to this unit. Note that this is a one-way dependency — changes to this unit do not affect the listed units. When PartOf=b.service is used on a.service, this dependency will show as ConsistsOf=a.service in property listing of b.service. ConsistsOf= dependency cannot be specified directly. ``` We can test out this service, for our alice user, as follows. ``` ➜ ~ sudo systemctl start user-website@1002.service ➜ ~ sudo journalctl --unit user-website@1002.service -- Logs begin at Sun 2021-08-29 22:58:12 CEST, end at Sun 2021-08-29 23:00:53 CEST. -- Aug 29 23:00:25 squid systemd[1]: Starting User 1002 website service... Aug 29 23:00:25 squid site-manager.sh[507]: created online site dir for alice Aug 29 23:00:25 squid site-manager.sh[507]: chowned all site files to alice:alice Aug 29 23:00:25 squid site-manager.sh[507]: online site installed for alice Aug 29 23:00:25 squid systemd[1]: Started User 1002 website service. ➜ ~ sudo systemctl stop user-website@1002.service ➜ ~ sudo journalctl --unit user-website@1002.service -- Logs begin at Sun 2021-08-29 22:58:12 CEST, end at Sun 2021-08-29 23:01:09 CEST. -- Aug 29 23:00:25 squid systemd[1]: Starting User 1002 website service... Aug 29 23:00:25 squid site-manager.sh[507]: created online site dir for alice Aug 29 23:00:25 squid site-manager.sh[507]: chowned all site files to alice:alice Aug 29 23:00:25 squid site-manager.sh[507]: online site installed for alice Aug 29 23:00:25 squid systemd[1]: Started User 1002 website service. Aug 29 23:01:06 squid systemd[1]: Stopping User 1002 website service... Aug 29 23:01:06 squid site-manager.sh[529]: created offline site dir for alice Aug 29 23:01:06 squid site-manager.sh[529]: chowned all site files to alice:alice Aug 29 23:01:06 squid site-manager.sh[529]: moved online site to offline site for alice Aug 29 23:01:06 squid site-manager.sh[529]: offline site installed for alice Aug 29 23:01:06 squid systemd[1]: user-website@1002.service: Succeeded. Aug 29 23:01:06 squid systemd[1]: Stopped User 1002 website service. ➜ ~ ``` Now the service works as expected but does it actually trigger when users log in? Let's investigate! When alice is logged in over SSH we get the following active services. ``` ➜ ~ sudo systemctl --type=service --no-pager | grep user systemd-sysusers.service loaded active exited Create System Users systemd-user-sessions.service loaded active exited Permit User Sessions user-runtime-dir@1000.service loaded active exited User Runtime Directory /run/user/1000 user-runtime-dir@1002.service loaded active exited User Runtime Directory /run/user/1002 user@1000.service loaded active running User Manager for UID 1000 user@1002.service loaded active running User Manager for UID 1002 ➜ ~ ``` It does not seem to works, let's try starting the service to see if it *actually* lists itself. ``` ➜ ~ sudo systemctl --type=service --no-pager | grep user systemd-sysusers.service loaded active exited Create System Users systemd-user-sessions.service loaded active exited Permit User Sessions user-runtime-dir@1000.service loaded active exited User Runtime Directory /run/user/1000 user-runtime-dir@1002.service loaded active exited User Runtime Directory /run/user/1002 user-website@1002.service loaded active exited User 1002 website service user@1000.service loaded active running User Manager for UID 1000 user@1002.service loaded active running User Manager for UID 1002 ➜ ~ ``` It does list itself as active, thanks to the `RemainAfterExit` setting but how do we link our template service to the `user@.service` template? ## Overriding service files Our `PartOf` setting links the shutdown of our service. We can try this by logging alice back out. The `user-website@1002.service` will stop but if we log back in it won't be started again! ``` ➜ ~ sudo systemctl --type=service --no-pager | grep user systemd-sysusers.service loaded active exited Create System Users systemd-user-sessions.service loaded active exited Permit User Sessions user-runtime-dir@1000.service loaded active exited User Runtime Directory /run/user/1000 user@1000.service loaded active running User Manager for UID 1000 ➜ ~ ``` In order to have the service start we need to attach it to the `user@.service file`. Up until now we made our changes into the actual file but we can be smarter that that! The service files supplied by Debian are very good and if they would make changes to them it would destroy *our* changes after an update. To mitigate this we've been user `service.d/` directories and systemd has the same on an individual service level. You can create the structure by hand but the *easiest* way is to use the built in `edit` subcommand to `systemctl`. By invoking `sudo systemctl edit user@.service` your favorite text editor will open up with a blank file. Here we'll add our additions to. ``` [Unit] Requires=user-website@%i.service ``` We simply state that `user@.service` requires `user-website@.service` to start as well. If we now inspect the service file of `user@.service` we see it sources it's configuration form **two** places. One is the default `/usr/lib/systemd/system/user@.service`, which can be modified by upstream changes, the other one is in our classic `/etc/systemd/system/user@.service.d/override.conf` path. ``` ➜ ~ sudo systemctl cat user@.service # /usr/lib/systemd/system/user@.service # SPDX-License-Identifier: LGPL-2.1+ # # This file is part of systemd. # # systemd is free software; you can redistribute it and/or modify it # under the terms of the GNU Lesser General Public License as published by # the Free Software Foundation; either version 2.1 of the License, or # (at your option) any later version. [Unit] Description=User Manager for UID %i Documentation=man:user@.service(5) After=systemd-user-sessions.service user-runtime-dir@%i.service dbus.service Requires=user-runtime-dir@%i.service IgnoreOnIsolate=yes [Service] User=%i PAMName=systemd-user Type=notify ExecStart=-/lib/systemd/systemd --user Slice=user-%i.slice KillMode=mixed Delegate=pids memory TasksMax=infinity TimeoutStopSec=120s # /etc/systemd/system/user@.service.d/override.conf [Unit] Requires=user-website@%i.service ➜ ~ ``` ## An alternative approach You can achieve similar results in multiple ways. Without much scripting you can try to chain the following concepts together. * `nginx` sites-available * symbolic links to `nginx` sites-enabled * `nginx` location, include and wildcards * `systemctl reload`