Commit 40d308739f88
Changed files (6)
systems
carthage
common
users
lib/default.nix
@@ -109,6 +109,7 @@
self.nixosModules.govanityurl
self.nixosModules.gosmee
self.nixosModules.rsync-replica
+ self.nixosModules.praetorian
agenixInput.nixosModules.default
inputs.disko.nixosModules.disko
inputs.lanzaboote.nixosModules.lanzaboote
lib/functions.nix
@@ -331,6 +331,55 @@ let
inherit user group openFirewall;
};
+ /**
+ Render an authorized_keys list for a given host + account from the keyed SSH
+ registry (see globals.ssh.<user>).
+
+ Each registry entry is `{ key; access; }` where `access.<account>.<host>` (or
+ `access.<account>.default`) resolves to one of:
+ - "trusted" -> bare key (full, unrestricted login)
+ - { gated = "a"; } -> key prefixed with command="praetorian run a" + lockdown
+ - absent -> key omitted for that host/account
+ `access.<account>` may also be the string "trusted"/"absent" as a whole-account
+ shorthand for `.default`.
+
+ Every entry MUST declare a non-empty `access`; missing/invalid shapes throw at
+ eval time so an over-provisioned key can never silently grant access.
+
+ @param registry The keyed SSH registry (attrset label -> { key; access; })
+ @param host The current hostname
+ @param account The account being rendered ("vincent", "root", ...)
+ @return List of authorized_keys lines
+ */
+ authorizedKeysFor =
+ registry: host: account:
+ let
+ gateOpts = "no-pty,no-agent-forwarding,no-port-forwarding,no-X11-forwarding,no-user-rc";
+ resolveLeaf =
+ label: leaf: key:
+ if leaf == null || leaf == "absent" then
+ null
+ else if leaf == "trusted" then
+ key
+ else if builtins.isAttrs leaf && leaf ? gated then
+ ''command="praetorian run ${leaf.gated}",${gateOpts} ${key}''
+ else
+ throw ''ssh key '${label}': invalid access leaf for ${account}@${host} (expected "trusted", { gated = "alias"; }, or "absent")'';
+ resolveOne =
+ label: entry:
+ let
+ access =
+ if entry ? access && entry.access != { } then
+ entry.access
+ else
+ throw "ssh key '${label}': missing non-empty 'access'";
+ ua = access.${account} or null;
+ leaf = if builtins.isString ua then ua else (if ua == null then null else (ua.${host} or ua.default or null));
+ in
+ if ua == null then null else resolveLeaf label leaf entry.key;
+ in
+ builtins.filter (x: x != null) (lib.attrValues (lib.mapAttrs resolveOne registry));
+
/**
Create a Samba share configuration with common defaults.
@@ -389,5 +438,6 @@ in
sshConfigs
mkServiceDefaults
mkSambaShare
+ authorizedKeysFor
;
}
systems/carthage/extra.nix
@@ -123,6 +123,53 @@ in
../common/services/openssh.nix
];
+ # ── Praetorian SSH command gating ───────────────────────────────────
+ # Gates command-restricted keys (see globals.ssh.vincent) to git over SSH.
+ # authorized_keys entries use command="praetorian run <alias>"; these aliases
+ # define what each is allowed to run. Repos live under /home/vincent/git.
+ services.praetorian = {
+ enable = true;
+ aliases = {
+ # aomi TPM key: full git push + pull on any repo under ~/git.
+ aomi-git.allow = {
+ "git-receive-pack" = {
+ arg = {
+ pos = 1;
+ glob = "/home/vincent/git/*";
+ };
+ num_args = 1;
+ };
+ "git-upload-pack" = {
+ arg = {
+ pos = 1;
+ glob = "/home/vincent/git/*";
+ };
+ num_args = 1;
+ };
+ };
+ # Termux phones: pull-only on any repo under ~/git (src/home, passage, ...).
+ git-pull.allow = {
+ "git-upload-pack" = {
+ arg = {
+ pos = 1;
+ glob = "/home/vincent/git/*";
+ };
+ num_args = 1;
+ };
+ };
+ # Passage sync keys: pull-only on the passage store repo.
+ passage-pull.allow = {
+ "git-upload-pack" = {
+ arg = {
+ pos = 1;
+ glob = "/home/vincent/git/passage.git";
+ };
+ num_args = 1;
+ };
+ };
+ };
+ };
+
# ── Fail2ban ────────────────────────────────────────────────────────
services.fail2ban = {
enable = true;
systems/common/users/root.nix
@@ -1,9 +1,13 @@
{
globals,
+ hostname,
+ libx,
...
}:
{
users.users.root = {
- openssh.authorizedKeys.keys = globals.ssh.vincent ++ [ globals.machines.shikoku.ssh.vincent ];
+ openssh.authorizedKeys.keys =
+ libx.authorizedKeysFor globals.ssh.vincent hostname "root"
+ ++ [ globals.machines.shikoku.ssh.vincent ];
};
}
systems/common/users/vincent.nix
@@ -57,7 +57,7 @@ in
initialPassword = "changeMe";
# FIXME set this up better
- openssh.authorizedKeys.keys = globals.ssh.vincent;
+ openssh.authorizedKeys.keys = libx.authorizedKeysFor globals.ssh.vincent hostname "vincent";
# 🤔
packages = [ pkgs.home-manager ];
globals.nix
@@ -1,28 +1,113 @@
_: {
ssh = {
- vincent = [
+ # Keyed SSH registry. Each entry is `{ key; access; }` where
+ # `access.<account>.<host|default>` resolves to one of:
+ # "trusted" -> full unrestricted login
+ # { gated = "a"; } -> command="praetorian run a" restricted (see carthage)
+ # absent -> no access on that host/account
+ # Rendered via libx.authorizedKeysFor. Every entry MUST declare a non-empty
+ # `access` (eval-time throw otherwise) so a key can never be silently
+ # over-provisioned. Greppable unrestricted surface: `grep '"trusted"'`.
+ vincent = {
# Yubikeys (PIV - legacy, keep during transition)
- "ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBFT5Rx+4Wuvd8lMBkcHxb4oHdRhm/OTg+p5tvPzoIN9enSmgRw5Inm/SlS8ZzV87G1NESTgzDRi6hREvqDlKvxs="
- "ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBGHMa4rHuBbQQYv+8jvlkFCD2VYRGA4+5fnZAhLx8iDirzfEPqHB60UJWcDeixnJCUlpJjzFbS4crNOXhfCTCTE="
- "ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBBFzxC16VqwTgWDQfw2YCiOw2JzpH3z9XgHtKoHhBdHi2i9m9XUc7fIUeEIIf7P8ARRNd8q5bjvl8JY7LtPkNCU="
- # FIDO2 resident keys (homelab)
- "sk-ssh-ed25519@openssh.com AAAAGnNrLXNzaC1lZDI1NTE5QG9wZW5zc2guY29tAAAAIODTc5Exm59skgJdu6/rA3CpX4k4P1CFBqCFtelWGGmEAAAAC3NzaDpob21lbGFi homelab-servers"
- "sk-ssh-ed25519@openssh.com AAAAGnNrLXNzaC1lZDI1NTE5QG9wZW5zc2guY29tAAAAIAGh5p44LvQrWjAMyC/5LjUnViqFl3ddVfiFnoiLgJb7AAAAEnNzaDpjcml0aWNhbC1pbmZyYQ== infra-touch-required"
- # FIDO2 resident keys (okinawa)
- "sk-ssh-ed25519@openssh.com AAAAGnNrLXNzaC1lZDI1NTE5QG9wZW5zc2guY29tAAAAIEefW7gStvkrO98v6UUawwa3yOu896Ei8USE/Sh2DjaUAAAABHNzaDo= vincent@okinawa"
- # Passage sync keys (non-SK, for automated git pull)
- "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIBqhe3iS2058Ro8jN0b5Sr1tb+fEyqwgEmEC7vCM0za4 vincent@kyushu-passage"
- "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAII/CgISZ5XVyg8eYH7b56EWo4UGplOzzZKdtMKtkZqxc vincent@okinawa-passage"
- # Host keys (trusted machines)
+ # TODO: phase these out once FIDO2/TPM coverage is complete.
+ yubikey-piv-1 = {
+ key = "ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBFT5Rx+4Wuvd8lMBkcHxb4oHdRhm/OTg+p5tvPzoIN9enSmgRw5Inm/SlS8ZzV87G1NESTgzDRi6hREvqDlKvxs=";
+ access = {
+ vincent.default = "trusted";
+ root.default = "trusted";
+ };
+ };
+ yubikey-piv-2 = {
+ key = "ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBGHMa4rHuBbQQYv+8jvlkFCD2VYRGA4+5fnZAhLx8iDirzfEPqHB60UJWcDeixnJCUlpJjzFbS4crNOXhfCTCTE=";
+ access = {
+ vincent.default = "trusted";
+ root.default = "trusted";
+ };
+ };
+ yubikey-piv-3 = {
+ key = "ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBBFzxC16VqwTgWDQfw2YCiOw2JzpH3z9XgHtKoHhBdHi2i9m9XUc7fIUeEIIf7P8ARRNd8q5bjvl8JY7LtPkNCU=";
+ access = {
+ vincent.default = "trusted";
+ root.default = "trusted";
+ };
+ };
+ # FIDO2 resident keys (touch is the gate -> unrestricted)
+ homelab-servers = {
+ key = "sk-ssh-ed25519@openssh.com AAAAGnNrLXNzaC1lZDI1NTE5QG9wZW5zc2guY29tAAAAIODTc5Exm59skgJdu6/rA3CpX4k4P1CFBqCFtelWGGmEAAAAC3NzaDpob21lbGFi homelab-servers";
+ access = {
+ vincent.default = "trusted";
+ root.default = "trusted";
+ };
+ };
+ critical-infra = {
+ key = "sk-ssh-ed25519@openssh.com AAAAGnNrLXNzaC1lZDI1NTE5QG9wZW5zc2guY29tAAAAIAGh5p44LvQrWjAMyC/5LjUnViqFl3ddVfiFnoiLgJb7AAAAEnNzaDpjcml0aWNhbC1pbmZyYQ== infra-touch-required";
+ access = {
+ vincent.default = "trusted";
+ root.default = "trusted";
+ };
+ };
+ okinawa-fido = {
+ key = "sk-ssh-ed25519@openssh.com AAAAGnNrLXNzaC1lZDI1NTE5QG9wZW5zc2guY29tAAAAIEefW7gStvkrO98v6UUawwa3yOu896Ei8USE/Sh2DjaUAAAABHNzaDo= vincent@okinawa";
+ access = {
+ vincent.default = "trusted";
+ root.default = "trusted";
+ };
+ };
+ # Passage sync keys (non-SK, automated git pull only)
+ # TODO: replace with the per-machine TPM approach.
+ kyushu-passage = {
+ key = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIBqhe3iS2058Ro8jN0b5Sr1tb+fEyqwgEmEC7vCM0za4 vincent@kyushu-passage";
+ access.vincent.carthage = {
+ gated = "passage-pull";
+ };
+ };
+ okinawa-passage = {
+ key = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAII/CgISZ5XVyg8eYH7b56EWo4UGplOzzZKdtMKtkZqxc vincent@okinawa-passage";
+ access.vincent.carthage = {
+ gated = "passage-pull";
+ };
+ };
# aomi user key: TPM-sealed ecdsa via ssh-tpm-agent (machine-bound, no touch)
- "ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBGEL1A++UUsFnGmnrIR8S6A8IUFkR5WyxDPol2bs02TY0O+sWOPJFJZIAKqcWVSUIkxRoktUBMRe0mMZyiHa3jw= vdemeest@aomi-tpm"
- "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIE3iD9Eaf5xglTyP+kIO9t8qQpF2H42rQ2AuUibs2hn7 vincent@aion"
- "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIGThdcaPfIaB7d+K5uODqEusLKGI5ZCye0aNOCaMoInO Kyushu's ssh key"
- # Boox (osaka) Termux
- "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAICa0SyAspL7PBPudCjb7oCBG17WRmYnDQF7/BYkFwqDi oksaka-termux"
- # Light Phone (suzu) Termux
- "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIK0Q1oXzMMJG03n2VoxKKOGruyGBy8V8yanqgeeUYm+N suzu-termux"
- ];
+ # Gated to git push+pull on carthage only.
+ aomi-tpm = {
+ key = "ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBGEL1A++UUsFnGmnrIR8S6A8IUFkR5WyxDPol2bs02TY0O+sWOPJFJZIAKqcWVSUIkxRoktUBMRe0mMZyiHa3jw= vdemeest@aomi-tpm";
+ access.vincent.carthage = {
+ gated = "aomi-git";
+ };
+ };
+ # aion host key: has automation (restic backup to aix).
+ # TODO: scope to aix + gate restic instead of full trust everywhere.
+ aion-hostkey = {
+ key = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIE3iD9Eaf5xglTyP+kIO9t8qQpF2H42rQ2AuUibs2hn7 vincent@aion";
+ access = {
+ vincent.default = "trusted";
+ root.default = "trusted";
+ };
+ };
+ # kyushu host key.
+ # TODO: work on removing this key entirely.
+ kyushu-hostkey = {
+ key = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIGThdcaPfIaB7d+K5uODqEusLKGI5ZCye0aNOCaMoInO Kyushu's ssh key";
+ access = {
+ vincent.default = "trusted";
+ root.default = "trusted";
+ };
+ };
+ # Termux phones: git pull only (src/home, passage, ... under ~/git).
+ osaka-termux = {
+ key = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAICa0SyAspL7PBPudCjb7oCBG17WRmYnDQF7/BYkFwqDi oksaka-termux";
+ access.vincent.carthage = {
+ gated = "git-pull";
+ };
+ };
+ suzu-termux = {
+ key = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIK0Q1oXzMMJG03n2VoxKKOGruyGBy8V8yanqgeeUYm+N suzu-termux";
+ access.vincent.carthage = {
+ gated = "git-pull";
+ };
+ };
+ };
};
syncthingFolders = {
sync = {