Refactor ssh&users

This commit is contained in:
2026-07-15 10:02:04 -03:00
parent e68321ac1e
commit 645e1f0f31
11 changed files with 580 additions and 474 deletions
+4 -3
View File
@@ -1,6 +1,7 @@
{ ... }: {
flake.lib.rbwSshSocket =
let
# Bash/Fish init snippets — used by wisdom/shells/{bash,fish}.nix.
bashSnippet = ''
if [ -z "''${SSH_AUTH_SOCK:-}" ]; then
if [ -n "''${XDG_RUNTIME_DIR:-}" ]; then
@@ -21,10 +22,10 @@
end
end
'';
in
{
in {
inherit bashSnippet fishSnippet;
# Used by home-manager modules to expose SSH_AUTH_SOCK to GUI apps
# and HM-managed shells declaratively (no activation script needed).
sessionVariable = "$XDG_RUNTIME_DIR/rbw/ssh-agent-socket";
activationPath = "/run/user/$(id -u)/rbw/ssh-agent-socket";
};
}
+110 -85
View File
@@ -1,131 +1,156 @@
{ lib, ... }: {
flake.lib.sshInventory =
let
# Hosts: pure network endpoints. No public keys live here anymore —
# keys are per USER (see `users` below), one key per person.
hosts = {
"14900k" = {
hostName = "192.168.2.25";
aliases = [ "14900k" "nixdesk" ];
publicKey = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAILwUevBGnf+Y/sL1ZsB4bt0c50a89iqwPRoYUGP4UHsL 14900k";
};
ideapad = {
hostName = "192.168.2.229";
aliases = [ "ideapad" ];
publicKey = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIIQwaaI90xIMjZ46EcMyO8kBwGCxf7qVL75IYhw8Ssze ideapad";
};
t2mbp = {
hostName = "192.168.2.15";
aliases = [ "t2mbp" ];
publicKey = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIMhVWB9YXl/FuQvufle4VWUas/QM8qCKoRd5a83Tt3S6 t2mbp";
};
uConsole = {
"uConsole" = {
hostName = "192.168.2.99";
aliases = [ "uConsole" "uconsole" ];
publicKey = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIAVPR0lRAcywPR7iTchM3+eO7NCdXAR6NPzYXxalr+dP uConsole";
};
# Scratchpad / recovery target — hostName isn't an IP, so `activeHosts`
# excludes it from navi deployments. Leave it in `hosts` for inventory
# visibility while recovery is in progress.
test = {
hostName = "test";
aliases = [ "test" ];
publicKey = null;
};
nix-server = {
"nix-server" = {
hostName = "192.168.2.238";
aliases = [ "nix-server" ];
publicKey = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIL3KDicMjtOFR6LfZrFzfAD1gdYUdwv6ZM4PSgtmIuzd nix-server";
};
r5500 = {
hostName = "192.168.2.100";
aliases = [ "r5500" ];
publicKey = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIK7iWCEtkYDLZFRF3w1gzyAok5VCAGUOwu4iWZdMjf3D r5500";
};
};
mkIdentityFileName = hostName: ".ssh/id_ed25519_${lib.strings.toLower hostName}.pub";
activeHosts = builtins.removeAttrs hosts (
builtins.filter (name: hosts.${name}.publicKey == null) (builtins.attrNames hosts)
);
# Per-user public keys. One keypair per person, generated once with
# `ssh-keygen -t ed25519 -C <comment>` (or `rbw gen ssh-key <user>`),
# private key stored in Bitwarden under the same name. Paste the
# `ssh-ed25519 AAAA… <comment>` line here.
users = {
olivier = {
publicKey = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAICpORzDw8kEOobjywl7bQaEp6Tez1c/ZP+7VBDsTcM8I olivier";
};
server = {
publicKey = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAICsksvNGOt/ebJtpRi9ocLBjwyl16VSaf+BsL2Hz5PRy server";
};
builder = {
publicKey = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIILBtfW1cq9mG6uAjAfpZuVa1EWDSTPSCThapDXXlLgV builder";
};
};
mkIdentityFiles = selectedHosts:
builtins.listToAttrs (
builtins.map
(hostName: {
name = mkIdentityFileName hostName;
value.text = "${selectedHosts.${hostName}.publicKey}\n";
})
(builtins.attrNames selectedHosts)
);
# Hosts SSH is actually safe to deploy/scan to: anything whose hostName
# parses as an IPv4 dotted-quad. Filters out scratchpad/non-routable
# entries (e.g. `hostName = "test"`).
activeHosts = lib.filterAttrs
(_: h: lib.match "[0-9]+\\.[0-9]+\\.[0-9]+\\.[0-9]+" h.hostName != null)
hosts;
# Gitea git-over-SSH listens on port 222. System SSH (nix deploy, server@…) uses port 22
# via the catalog `nix-server` Host block — never list nix-server or 192.168.2.238 here.
giteaSshBlock = identityAgent: ''
Host git.chiasson.cloud gitea
HostName 192.168.2.238
Port 222
User git
IdentityAgent ${identityAgent}
IdentitiesOnly no
Match host nix-server,192.168.2.238 user git
HostName 192.168.2.238
Port 222
User git
IdentityAgent ${identityAgent}
IdentitiesOnly no
'';
# .pub filename convention. OpenSSH 7.3+ accepts a `.pub` file as
# IdentityFile to filter `IdentityAgent` (rbw) keys without storing the
# private key on disk — keep this trick.
mkIdentityFileName = userName: ".ssh/id_ed25519_${lib.strings.toLower userName}.pub";
mkSshConfigTemplate =
{
selectedHosts ? activeHosts,
user ? null,
identityAgent ? "__RBW_SSH_SOCK__",
# Catalog accounts the operator may authenticate AS via
# `ssh <account>@<host>`. Each entry gets a `Match user <account>`
# block that overrides IdentityFile so OpenSSH asks the rbw agent
# for that account's private key. `cfg.user` is already covered
# by the Host blocks, so don't list it here.
extraIdentities ? [ ],
identityAgent ? "SSH_AUTH_SOCK",
}:
let
hostBlocks = builtins.map
(hostName:
let
entry = selectedHosts.${hostName};
hostPatterns = builtins.concatStringsSep " " (entry.aliases ++ [ entry.hostName ]);
userLine = if user == null then "" else " User ${user}\n";
portLine =
if hostName == "nix-server" then
" Port 22\n"
else
"";
in
''
Host ${hostPatterns}
HostName ${entry.hostName}
${userLine}${portLine} IdentityFile ~/${mkIdentityFileName hostName}
IdentityAgent ${identityAgent}
IdentitiesOnly yes
'')
(builtins.attrNames selectedHosts);
in
builtins.concatStringsSep "\n" (
[
(giteaSshBlock identityAgent)
]
++ hostBlocks
++ [
''
Host *
# Match blocks for cross-account SSH. Filter out cfg.user since
# the matching pub is already loaded via the Host block's
# IdentityFile line — re-emitting it would just duplicate.
matchBlocks = lib.concatStringsSep "\n" (map
(n: ''
Match user ${n}
IdentityFile ~/${mkIdentityFileName n}
IdentitiesOnly yes
IdentityAgent none
''
]
);
'')
(builtins.filter (n: n != user) extraIdentities));
# Per-host block. `IdentityFile` (when `user` is set) is a .pub file
# pointing at the agent's matching private key. With IdentitiesOnly
# yes, only agent keys matching that pub are tried — fixes the old
# "too many auth failures" symptom by design.
hostBlock = hostName:
let
e = selectedHosts.${hostName};
hostPatterns = lib.concatStringsSep " " (e.aliases ++ [ e.hostName ]);
idLine = if user == null then "" else " IdentityFile ~/${mkIdentityFileName user}\n";
in ''
Host ${hostPatterns}
HostName ${e.hostName}
${idLine} IdentityAgent ${identityAgent}
IdentitiesOnly yes
'';
# Gitea block: always emitted. `User git`, port 222. olivier's
# public key (passed via `IdentityFile`) is what gitea accepts
# because it gets pushed into gitea's authorized_keys through
# gitea's own UI for that account.
giteaBlock = ''
Host git.chiasson.cloud gitea
HostName 192.168.2.238
Port 222
User git
${if user == null then "" else " IdentityFile ~/${mkIdentityFileName user}\n"} IdentityAgent ${identityAgent}
IdentitiesOnly yes
'';
hostBlocks = lib.concatStringsSep "\n" (map hostBlock (lib.attrNames selectedHosts));
in
# Filter empty sections so an absent Match block (no extraIdentities)
# doesn't add leading whitespace to the rendered config.
lib.concatStringsSep "\n" (lib.filter (s: s != "") [
matchBlocks
giteaBlock
hostBlocks
''
Host *
IdentitiesOnly yes
IdentityAgent none
''
]);
# Per-user line for `users.users.<u>.openssh.authorizedKeys.keys`.
# `selection` is either "all" or a list of source identifiers for
# sshd's `from=` constraint (IP, CIDR, or hostname if reverse-DNS works).
# Returns null if inventory is missing the user's pubkey — caller is
# expected to assert and fail the build rather than silently lock out.
mkAuthorizedKeyLine = userName: selection:
let
entry = users.${userName} or null;
in
if entry == null then null
else if (entry.publicKey or "") == "" then null
else if selection == "all" then entry.publicKey
else ''from="${lib.concatStringsSep "," selection}" ${entry.publicKey}'';
in
{
inherit hosts activeHosts mkIdentityFiles mkSshConfigTemplate;
authorizedKeys = lib.unique (
builtins.map (entry: entry.publicKey) (builtins.attrValues activeHosts)
);
identityFiles = mkIdentityFiles activeHosts;
sshConfigTemplate = mkSshConfigTemplate { };
inherit hosts users activeHosts;
mkSshConfigTemplate = mkSshConfigTemplate;
mkAuthorizedKeyLine = mkAuthorizedKeyLine;
mkIdentityFileName = mkIdentityFileName;
};
}