legacy-of-lunatic-kingdom: make the restic account sftp-only
sftp previously worked through useradd's empty-shell default, and the keys could open a shell.

Assisted-by: GLM-5.3 via pi
change ymrutsxrrsytqnrxpmvunyuzwmyqnxpq
commit 09dcb6aac3cf893646673fe42ef53153f7ba7cbc
author Alpha Chen <alpha@kejadlen.dev>
date
parent rvtvsxku
diff --git a/docs/legacy-of-lunatic-kingdom.md b/docs/legacy-of-lunatic-kingdom.md
index e88b262..b04fe99 100644
--- a/docs/legacy-of-lunatic-kingdom.md
+++ b/docs/legacy-of-lunatic-kingdom.md
@@ -55,7 +55,7 @@ can scrape caddy's metrics endpoint.
 |---|---|---|
 | caddy | Quadlet container | [caddy.yml][caddy] |
 | syncthing | Debian package, `syncthing@syncthing.service` | [syncthing.yml][syncthing] |
-| restic repository | sftp directory under the `restic` user | [main.yml][main] |
+| restic repository | sftp-only `restic` user | [main.yml][main] |
 | node-exporter | Debian package, with apt and systemd collectors | [main.yml][main] |
 | journal-upload | ships journald to VictoriaLogs on lotus-land-story | [journal-upload.yml][journal] |
 | unattended-upgrades | Debian package, weekly apply with security-only origins | [unattended-upgrades.yml][upgrades] |
diff --git a/legacy-of-lunatic-kingdom/main.yml b/legacy-of-lunatic-kingdom/main.yml
index b2587c2..e46d513 100644
--- a/legacy-of-lunatic-kingdom/main.yml
+++ b/legacy-of-lunatic-kingdom/main.yml
@@ -109,6 +109,21 @@
   become: true
   tasks:
 
+    # Applied before the account changes below so sftp never briefly depends
+    # on the user's shell; internal-sftp serves absolute paths, so the
+    # repository URL stays as it is.
+    - name: Restrict the restic user to sftp
+      ansible.builtin.blockinfile:
+        path: /etc/ssh/sshd_config
+        block: |
+          Match User restic
+              ForceCommand internal-sftp
+              AllowTcpForwarding no
+              X11Forwarding no
+        validate: /usr/sbin/sshd -t -f %s
+        mode: "0644"
+      notify: Restart sshd
+
     - name: Create Restic user
       ansible.builtin.user:
         name: restic
@@ -117,12 +132,17 @@
         # which is how Debian creates system users. '*' is not locked but
         # can never match a password.
         password: "*"
+        # internal-sftp needs no shell.
+        shell: /usr/sbin/nologin
         state: present
 
     - name: Add SSH key for restic user
       ansible.posix.authorized_key:
         user: restic
         key: "{{ item }}"
+        # ForceCommand confines; restrict also kills forwarding and pty at
+        # the auth layer.
+        key_options: restrict
       loop:
         - ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAILrlVay1GlpCuhYSUup/K/gRYmltycViPBAdZ8t9OKtu # lotus-land-story
         - ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIPgX0tA28NO3djYaV++JxggR0t47xou+R0HQVwVjzj8s # 1password
@@ -135,6 +155,12 @@
         group: restic
         mode: "0755"
 
+  handlers:
+    - name: Restart sshd
+      ansible.builtin.systemd:
+        name: sshd
+        state: restarted
+
 - name: Keep dhcpcd out of resolv.conf
   # Trixie's ifupdown uses dhcpcd for the dhcp method, and it rewrites
   # /etc/resolv.conf on every lease event, stomping the file tailscaled owns