Refactor ssh&users
This commit is contained in:
@@ -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
@@ -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;
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user