build Install hardened systemd unit from repo

Keep hito.service in the repo as the source of truth. setup.sh install and deploy now install it when it changed, after systemd-analyze verify and with a timestamped backup of the old unit. The old point_unit only sed-edited a hand-made unit during install. The unit narrows ReadWritePaths to /srv/hito/shared, so the service cannot rewrite releases or the current symlink. It also adds capability, syscall, and namespace hardening and UMask=0077, and groups the environment lines to show EnvironmentFile precedence.

Commit
61a230fa75f569ce48cb0a29e95d37e2b1fca3cc
Author
Marius Peter <dev@marius-peter.com>
Author date
Committer
Marius Peter <dev@marius-peter.com>
Committer date
Changed files
deploy/hito.service
index 00000000..ac4cd2f7 000000..100644
@@ -0,0 +1,52 @@
1 Added: # Installed by deploy/setup.sh to /etc/systemd/system/hito.service.
2 Added: # Edit this file, not the installed copy: install and deploy overwrite it.
3 Added:
4 Added: [Unit]
5 Added: Description=hito — High Intensity Trainer Online
6 Added: After=network.target
7 Added:
8 Added: [Service]
9 Added: Type=simple
10 Added: User=git
11 Added: Group=git
12 Added: WorkingDirectory=/srv/hito/current
13 Added: ExecStart=/srv/hito/current/_build/default/bin/main.exe
14 Added: Restart=on-failure
15 Added: RestartSec=5
16 Added:
17 Added: # EnvironmentFile= values override Environment= values, whatever the order.
18 Added: Environment=HITO_DB=sqlite3:/srv/hito/shared/hito.sqlite
19 Added: Environment=HITO_PORT=8080
20 Added: # HITO_VERSION, rewritten by every deploy.
21 Added: EnvironmentFile=/srv/hito/shared/deploy.env
22 Added: # Host secrets (HITO_SECRET and so on). Keep it root:root 0600.
23 Added: EnvironmentFile=/etc/hito/hito.env
24 Added:
25 Added: # Hardening. Only the database directory is writable.
26 Added: UMask=0077
27 Added: NoNewPrivileges=yes
28 Added: ProtectSystem=strict
29 Added: ProtectHome=yes
30 Added: ReadWritePaths=/srv/hito/shared
31 Added: ReadOnlyPaths=/srv/git
32 Added: PrivateTmp=yes
33 Added: PrivateDevices=yes
34 Added: CapabilityBoundingSet=
35 Added: AmbientCapabilities=
36 Added: ProtectKernelTunables=yes
37 Added: ProtectKernelModules=yes
38 Added: ProtectKernelLogs=yes
39 Added: ProtectControlGroups=yes
40 Added: ProtectClock=yes
41 Added: ProtectHostname=yes
42 Added: RestrictSUIDSGID=yes
43 Added: RestrictNamespaces=yes
44 Added: RestrictRealtime=yes
45 Added: RestrictAddressFamilies=AF_INET AF_INET6 AF_UNIX
46 Added: LockPersonality=yes
47 Added: MemoryDenyWriteExecute=yes
48 Added: SystemCallArchitectures=native
49 Added: SystemCallFilter=@system-service
50 Added:
51 Added: [Install]
52 Added: WantedBy=multi-user.target
deploy/setup.sh
index d76f93c4..d8e7dd92 100755..100755
@@ -5,11 +5,12 @@
5 5 # sudo bash setup.sh install One-time host setup: take git ownership of
6 6 # /srv/hito, build the shared switch as git,
7 7 # install the hook, the scoped sudoers rule, and
8 Removed: # the systemd unit (User=git) pointed at current/.
9 Removed: # Idempotent. Creates no release; the first push
10 Removed: # (or 'deploy') does.
11 Removed: # sudo bash setup.sh deploy Build master as a fresh release on the shared
12 Removed: # switch and flip current. Health-checks and
8 Added: # the systemd unit from hito.service beside this
9 Added: # script. Idempotent. Creates no release; the
10 Added: # first push (or 'deploy') does.
11 Added: # sudo bash setup.sh deploy Reinstall hito.service if it changed, build
12 Added: # master as a fresh release on the shared
13 Added: # switch, and flip current. Health-checks and
13 14 # rolls back on failure. Use to go live without a
14 15 # push, or right after 'install'.
15 16 # sudo bash setup.sh reset Wipe /srv/hito to the clean layout, preserving
@@ -90,20 +91,22 @@
90 91 rm -f /usr/local/libexec/hito-deploy
91 92 }
92 93
93 Removed: point_unit() {
94 Removed: cp -a "${UNIT}" "${UNIT}.bak.$(date +%s)"
95 Removed: sed -i \
96 Removed: -e 's#^ExecStart=.*#ExecStart=/srv/hito/current/_build/default/bin/main.exe#' \
97 Removed: -e 's#^WorkingDirectory=.*#WorkingDirectory=/srv/hito/current#' \
98 Removed: -e 's#^Environment=HITO_DB=.*#Environment=HITO_DB=sqlite3:/srv/hito/shared/hito.sqlite#' \
99 Removed: -e "s#^User=.*#User=${SVC_USER}#" \
100 Removed: -e "s#^Group=.*#Group=${SVC_GROUP}#" \
101 Removed: "${UNIT}"
102 Removed: if ! grep -q '^EnvironmentFile=/srv/hito/shared/deploy.env$' "${UNIT}"; then
103 Removed: sed -i '/^Environment=HITO_DB=/a EnvironmentFile=/srv/hito/shared/deploy.env' "${UNIT}"
94 Added: # Install the unit shipped beside this script. The repo copy is the source of
95 Added: # truth. A changed installed copy is backed up, then replaced.
96 Added: install_unit() {
97 Added: local src="${HERE}/hito.service"
98 Added: if cmp -s "${src}" "${UNIT}"; then
99 Added: echo " unit unchanged"
100 Added: return 0
104 101 fi
102 Added: systemd-analyze verify "${src}"
103 Added: if [[ -f "${UNIT}" ]]; then
104 Added: cp -a "${UNIT}" "${UNIT}.bak.$(date +%s)"
105 Added: fi
106 Added: install -o root -g root -m 0644 "${src}" "${UNIT}"
105 107 systemctl daemon-reload
106 Removed: echo " unit runs as ${SVC_USER} and points at current/ (backup kept alongside)"
108 Added: systemctl enable hito >/dev/null 2>&1
109 Added: echo " installed ${UNIT} (previous copy kept alongside)"
107 110 }
108 111
109 112 health_ok() {
@@ -207,7 +210,7 @@
207 210 echo "== shared opam switch =="; ensure_switch
208 211 echo "== post-receive hook =="; install_hook
209 212 echo "== scoped sudoers =="; install_sudoers
210 Removed: echo "== systemd unit =="; point_unit
213 Added: echo "== systemd unit =="; install_unit
211 214 echo
212 215 echo "Done. Run 'sudo bash setup.sh deploy' to build the first release,"
213 216 echo "or push master to trigger the hook."
@@ -218,6 +221,7 @@
218 221 echo "FATAL: no shared switch; run 'install' first" >&2
219 222 exit 1
220 223 fi
224 Added: echo "== systemd unit =="; install_unit
221 225 echo "== deploy master =="; build_and_flip
222 226 }
223 227