···603603Note that you should add a newline at the end if setting a non-empty message
604604since the knot won't do this for you.
605605606606+## Secure Mode
607607+608608+Secure Mode isolates each `git` subprocess to the repository it is
609609+operating on, using two mechanisms:
610610+611611+- **Linux Landlock** restricts the filesystem paths the subprocess
612612+ can access -- it can only read/write its own repository and the
613613+ system directories it needs to run.
614614+- **UID isolation** runs each subprocess as a virtual UID assigned
615615+ to the repository owner, so that repositories belonging to
616616+ different owners are isolated from each other at the OS level
617617+ even if Landlock were somehow bypassed.
618618+619619+Secure Mode requires:
620620+621621+- Linux kernel >= 5.19 (Landlock V2). This is the minimum needed
622622+ for `git push` to work, because receive-pack's quarantine
623623+ migration uses cross-directory rename which requires the
624624+ Landlock `REFER` access right (added in V2). Kernels 5.13-5.18
625625+ support Landlock V1 and clones will work, but pushes will fail
626626+ with cross-device link errors. On kernels without any Landlock
627627+ support (< 5.13), the sandbox call is a no-op: UID isolation
628628+ still applies but no filesystem restriction is enforced.
629629+- `CAP_SETUID`, `CAP_SETGID`, and `CAP_CHOWN` available to the
630630+ knot process. The NixOS module grants these automatically; for
631631+ manual setups see the `setcap` step below.
632632+633633+### NixOS
634634+635635+Add `server.secureMode = true;` to your knot module configuration:
636636+637637+```nix
638638+services.tangled.knot = {
639639+ server.secureMode = true;
640640+ # ... other options
641641+};
642642+```
643643+644644+The NixOS module handles everything else automatically:
645645+646646+- Grants the required capabilities to the knot service via
647647+ `AmbientCapabilities` in the systemd unit.
648648+- Installs a capability-bearing wrapper at
649649+ `/run/wrappers/bin/knot` via `security.wrappers`, so that
650650+ SSH-invoked git operations (pushes) also run under the correct
651651+ UID without requiring the service to run as root.
652652+- Runs `knot migrate-isolation` at service start to chown
653653+ existing repositories to their virtual UIDs.
654654+655655+### Manual setup
656656+657657+**Step 1.** Grant the required capabilities to the knot binary.
658658+This allows the knot process to switch to virtual UIDs at runtime
659659+without running as root. You will need to repeat this step
660660+whenever the binary is updated.
661661+662662+```
663663+sudo setcap cap_setuid,cap_setgid,cap_chown+eip /usr/local/bin/knot
664664+```
665665+666666+**Step 2.** Run the migration tool to assign virtual UIDs to all
667667+existing repositories and set their filesystem permissions. This
668668+must be run as root:
669669+670670+```
671671+sudo knot migrate-isolation \
672672+ --git-dir /home/git \
673673+ --db /home/git/knotserver.db \
674674+ --internal-api 127.0.0.1:5444
675675+```
676676+677677+You can re-run this at any time with `--force` to reapply
678678+permissions (e.g. after a manual repair or after updating the
679679+binary).
680680+681681+**Step 2a.** Ensure the home directory is traversable by
682682+non-group users. Git subprocesses run as virtual UIDs that are
683683+not in the git group, and they need to resolve
684684+`$HOME/.config/git/config` to load the global config:
685685+686686+```
687687+sudo chmod o+x /home/git
688688+```
689689+690690+This adds only the execute bit, not read -- the virtual UIDs can
691691+traverse to known paths but cannot list directory contents.
692692+693693+**Step 3.** Enable Secure Mode in your environment file:
694694+695695+```
696696+KNOT_SERVER_SECURE_MODE=true
697697+```
698698+699699+Or pass it as a flag:
700700+701701+```
702702+knot server --secure-mode
703703+```
704704+705705+**Step 4.** Regenerate the `AuthorizedKeysCommand` with the
706706+`-secure-mode` flag. This causes `knot keys` to emit guard
707707+command lines that include `-secure-mode`, so SSH pushes also
708708+get UID isolation:
709709+710710+```
711711+sudo tee /etc/ssh/sshd_config.d/authorized_keys_command.conf <<EOF
712712+Match User git
713713+ AuthorizedKeysCommand /usr/local/bin/knot keys \
714714+ -o authorized-keys -secure-mode
715715+ AuthorizedKeysCommandUser nobody
716716+EOF
717717+```
718718+719719+Reload `sshd` after making this change.
720720+721721+> **Note:** the server will refuse to start in Secure Mode if any
722722+> repositories have not yet been isolation-migrated. Re-run
723723+> `migrate-isolation` if you see this error.
724724+606725## Troubleshooting
607726608727If you run your own knot, you may run into some of these