Refactor Debian/Ubuntu package

Move files for packaging outside the docs directory into its own
packaging directory. Replace the existing postinstall and postremove
scripts with Debian maintainerscripts to behave more like a typical
Debian package:

* Start and enable the headscale systemd service by default
* Does not print informational messages
* No longer stop and disable the service on updates

This package also performs migrations for all changes done in previous
package versions on upgrade:

* Set login shell to /usr/sbin/nologin
* Set home directory to /var/lib/headscale
* Migrate to system UID/GID

The package is lintian-clean with a few exceptions that are documented
as excludes and it passes puipars (both tested on Debian 12).

The following scenarious were tested on Ubuntu 22.04, Ubuntu 24.04,
Debian 11, Debian 12:

* Install
* Install same version again
* Install -> Remove -> Install
* Install -> Purge -> Install
* Purge
* Update from 0.22.0
* Update from 0.26.0

See: #2278
See: #2133
Fixes: #2311
This commit is contained in:
Florian Preinstorfer 2025-05-16 17:59:57 +02:00 committed by nblock
parent d2879b2b36
commit 4a941a2cb4
11 changed files with 189 additions and 119 deletions

5
packaging/README.md Normal file
View file

@ -0,0 +1,5 @@
# Packaging
We use [nFPM](https://nfpm.goreleaser.com/) for making `.deb` packages.
This folder contains files we need to package with these releases.

87
packaging/deb/postinst Normal file
View file

@ -0,0 +1,87 @@
#!/bin/sh
# postinst script for headscale.
set -e
# Summary of how this script can be called:
# * <postinst> 'configure' <most-recently-configured-version>
# * <old-postinst> 'abort-upgrade' <new version>
# * <conflictor's-postinst> 'abort-remove' 'in-favour' <package>
# <new-version>
# * <postinst> 'abort-remove'
# * <deconfigured's-postinst> 'abort-deconfigure' 'in-favour'
# <failed-install-package> <version> 'removing'
# <conflicting-package> <version>
# for details, see https://www.debian.org/doc/debian-policy/ or
# the debian-policy package.
HEADSCALE_USER="headscale"
HEADSCALE_GROUP="headscale"
HEADSCALE_HOME_DIR="/var/lib/headscale"
HEADSCALE_SHELL="/usr/sbin/nologin"
HEADSCALE_SERVICE="headscale.service"
case "$1" in
configure)
groupadd --force --system "$HEADSCALE_GROUP"
if ! id -u "$HEADSCALE_USER" >/dev/null 2>&1; then
useradd --system --shell "$HEADSCALE_SHELL" \
--gid "$HEADSCALE_GROUP" --home-dir "$HEADSCALE_HOME_DIR" \
--comment "headscale default user" "$HEADSCALE_USER"
fi
if dpkg --compare-versions "$2" lt-nl "0.27"; then
# < 0.24.0-beta.1 used /home/headscale as home and /bin/sh as shell.
# The directory /home/headscale was not created by the package or
# useradd but the service always used /var/lib/headscale which was
# always shipped by the package as empty directory. Previous versions
# of the package did not update the user account properties.
usermod --home "$HEADSCALE_HOME_DIR" --shell "$HEADSCALE_SHELL" \
"$HEADSCALE_USER" >/dev/null
fi
if dpkg --compare-versions "$2" lt-nl "0.27" \
&& [ $(id --user "$HEADSCALE_USER") -ge 1000 ] \
&& [ $(id --group "$HEADSCALE_GROUP") -ge 1000 ]; then
# < 0.26.0-beta.1 created a regular user/group to run headscale.
# Previous versions of the package did not migrate to system uid/gid.
# Assume that the *default* uid/gid range is in use and only run this
# migration when the current uid/gid is allocated in the user range.
# Create a temporary system user/group to guarantee the allocation of a
# uid/gid in the system range. Assign this new uid/gid to the existing
# user and group and remove the temporary user/group afterwards.
tmp_name="headscaletmp"
useradd --system --no-log-init --no-create-home --shell "$HEADSCALE_SHELL" "$tmp_name"
tmp_uid="$(id --user "$tmp_name")"
tmp_gid="$(id --group "$tmp_name")"
usermod --non-unique --uid "$tmp_uid" --gid "$tmp_gid" "$HEADSCALE_USER"
groupmod --non-unique --gid "$tmp_gid" "$HEADSCALE_USER"
userdel --force "$tmp_name"
fi
# Enable service and keep track of its state
if deb-systemd-helper --quiet was-enabled "$HEADSCALE_SERVICE"; then
deb-systemd-helper enable "$HEADSCALE_SERVICE" >/dev/null || true
else
deb-systemd-helper update-state "$HEADSCALE_SERVICE" >/dev/null || true
fi
# Bounce service
if [ -d /run/systemd/system ]; then
systemctl --system daemon-reload >/dev/null || true
if [ -n "$2" ]; then
deb-systemd-invoke restart "$HEADSCALE_SERVICE" >/dev/null || true
else
deb-systemd-invoke start "$HEADSCALE_SERVICE" >/dev/null || true
fi
fi
;;
abort-upgrade|abort-remove|abort-deconfigure)
;;
*)
echo "postinst called with unknown argument '$1'" >&2
exit 1
;;
esac

42
packaging/deb/postrm Normal file
View file

@ -0,0 +1,42 @@
#!/bin/sh
# postrm script for headscale.
set -e
# Summary of how this script can be called:
# * <postrm> 'remove'
# * <postrm> 'purge'
# * <old-postrm> 'upgrade' <new-version>
# * <new-postrm> 'failed-upgrade' <old-version>
# * <new-postrm> 'abort-install'
# * <new-postrm> 'abort-install' <old-version>
# * <new-postrm> 'abort-upgrade' <old-version>
# * <disappearer's-postrm> 'disappear' <overwriter>
# <overwriter-version>
# for details, see https://www.debian.org/doc/debian-policy/ or
# the debian-policy package.
case "$1" in
remove)
if [ -d /run/systemd/system ]; then
systemctl --system daemon-reload >/dev/null || true
fi
;;
purge)
userdel headscale
rm -rf /var/lib/headscale
if [ -x "/usr/bin/deb-systemd-helper" ]; then
deb-systemd-helper purge headscale.service >/dev/null || true
fi
;;
upgrade|failed-upgrade|abort-install|abort-upgrade|disappear)
;;
*)
echo "postrm called with unknown argument '$1'" >&2
exit 1
;;
esac

34
packaging/deb/prerm Normal file
View file

@ -0,0 +1,34 @@
#!/bin/sh
# prerm script for headscale.
set -e
# Summary of how this script can be called:
# * <prerm> 'remove'
# * <old-prerm> 'upgrade' <new-version>
# * <new-prerm> 'failed-upgrade' <old-version>
# * <conflictor's-prerm> 'remove' 'in-favour' <package> <new-version>
# * <deconfigured's-prerm> 'deconfigure' 'in-favour'
# <package-being-installed> <version> 'removing'
# <conflicting-package> <version>
# for details, see https://www.debian.org/doc/debian-policy/ or
# the debian-policy package.
case "$1" in
remove)
if [ -d /run/systemd/system ]; then
deb-systemd-invoke stop headscale.service >/dev/null || true
fi
;;
upgrade|deconfigure)
;;
failed-upgrade)
;;
*)
echo "prerm called with unknown argument '$1'" >&2
exit 1
;;
esac

View file

@ -0,0 +1,52 @@
[Unit]
After=syslog.target
After=network.target
Description=headscale coordination server for Tailscale
X-Restart-Triggers=/etc/headscale/config.yaml
[Service]
Type=simple
User=headscale
Group=headscale
ExecStart=/usr/bin/headscale serve
ExecReload=/usr/bin/kill -HUP $MAINPID
Restart=always
RestartSec=5
WorkingDirectory=/var/lib/headscale
ReadWritePaths=/var/lib/headscale /var/run
AmbientCapabilities=CAP_NET_BIND_SERVICE CAP_CHOWN
CapabilityBoundingSet=CAP_NET_BIND_SERVICE CAP_CHOWN
LockPersonality=true
NoNewPrivileges=true
PrivateDevices=true
PrivateMounts=true
PrivateTmp=true
ProcSubset=pid
ProtectClock=true
ProtectControlGroups=true
ProtectHome=true
ProtectHostname=true
ProtectKernelLogs=true
ProtectKernelModules=true
ProtectKernelTunables=true
ProtectProc=invisible
ProtectSystem=strict
RemoveIPC=true
RestrictAddressFamilies=AF_INET AF_INET6 AF_UNIX
RestrictNamespaces=true
RestrictRealtime=true
RestrictSUIDSGID=true
RuntimeDirectory=headscale
RuntimeDirectoryMode=0750
StateDirectory=headscale
StateDirectoryMode=0750
SystemCallArchitectures=native
SystemCallFilter=@chown
SystemCallFilter=@system-service
SystemCallFilter=~@privileged
UMask=0077
[Install]
WantedBy=multi-user.target