Refactor ssh&users
This commit is contained in:
@@ -1,96 +1,141 @@
|
||||
{ self, ... }: {
|
||||
flake.homeManagerModules.sshOutboundRbw = {
|
||||
config,
|
||||
lib,
|
||||
pkgs,
|
||||
...
|
||||
}:
|
||||
let
|
||||
cfg = config.chiasson.ssh.outbound.rbw;
|
||||
inventory = self.lib.sshInventory;
|
||||
selectedHostNames =
|
||||
if cfg.hosts == [ "all" ] then
|
||||
builtins.attrNames inventory.activeHosts
|
||||
else
|
||||
cfg.hosts;
|
||||
missing = builtins.filter (name: !(builtins.hasAttr name inventory.hosts)) selectedHostNames;
|
||||
selectedHosts = builtins.listToAttrs (
|
||||
builtins.map (name: {
|
||||
inherit name;
|
||||
value = inventory.hosts.${name};
|
||||
}) selectedHostNames
|
||||
);
|
||||
sshConfigTemplate = inventory.mkSshConfigTemplate {
|
||||
selectedHosts = selectedHosts;
|
||||
user = cfg.user;
|
||||
};
|
||||
in
|
||||
{
|
||||
options.chiasson.ssh.outbound.rbw = {
|
||||
enable = lib.mkEnableOption "Generated `~/.ssh/config` + rbw agent socket.";
|
||||
user = lib.mkOption {
|
||||
type = lib.types.str;
|
||||
default = config.home.username;
|
||||
description = "`User` in generated `Host` blocks.";
|
||||
};
|
||||
hosts = lib.mkOption {
|
||||
type = lib.types.listOf lib.types.str;
|
||||
default = [ "all" ];
|
||||
description = "Inventory hosts to emit (or `[ \"all\" ]`).";
|
||||
};
|
||||
manageSshConfig = lib.mkOption {
|
||||
type = lib.types.bool;
|
||||
default = true;
|
||||
description = "Write `~/.ssh/config` from the template.";
|
||||
};
|
||||
};
|
||||
|
||||
config = lib.mkIf cfg.enable (lib.mkMerge [
|
||||
{
|
||||
assertions = [
|
||||
{
|
||||
assertion = missing == [ ];
|
||||
message = "ssh.outbound.rbw: unknown host keys: ${builtins.concatStringsSep ", " missing}";
|
||||
}
|
||||
];
|
||||
}
|
||||
{
|
||||
home.packages = with pkgs; [ rbw pinentry-gtk2 ];
|
||||
home.sessionVariables.SSH_AUTH_SOCK = self.lib.rbwSshSocket.sessionVariable;
|
||||
home.file = inventory.mkIdentityFiles selectedHosts;
|
||||
|
||||
programs.ssh.enable = lib.mkIf cfg.manageSshConfig false;
|
||||
home.activation.rbwSshConfig = lib.mkIf cfg.manageSshConfig (lib.hm.dag.entryAfter [ "writeBoundary" ] ''
|
||||
mkdir -p "$HOME/.ssh"
|
||||
chmod 700 "$HOME/.ssh"
|
||||
RBW_SSH_SOCK="${self.lib.rbwSshSocket.activationPath}"
|
||||
cat > "$HOME/.ssh/config" <<'EOF'
|
||||
${sshConfigTemplate}
|
||||
EOF
|
||||
sed -i "s|__RBW_SSH_SOCK__|$RBW_SSH_SOCK|g" "$HOME/.ssh/config"
|
||||
chmod 600 "$HOME/.ssh/config"
|
||||
'');
|
||||
|
||||
systemd.user.services.rbw-agent-bootstrap = {
|
||||
Unit = {
|
||||
Description = "Bootstrap rbw SSH agent";
|
||||
PartOf = [ "graphical-session.target" ];
|
||||
After = [ "graphical-session.target" ];
|
||||
};
|
||||
Service = {
|
||||
Type = "oneshot";
|
||||
ExecStart = "${pkgs.bash}/bin/bash -lc '${pkgs.rbw}/bin/rbw unlocked >/dev/null 2>&1 || true'";
|
||||
RemainAfterExit = true;
|
||||
};
|
||||
Install = {
|
||||
WantedBy = [ "graphical-session.target" ];
|
||||
};
|
||||
flake.homeManagerModules.sshOutboundRbw =
|
||||
{ config, lib, pkgs, ... }:
|
||||
let
|
||||
cfg = config.chiasson.ssh.outbound.rbw;
|
||||
inventory = self.lib.sshInventory;
|
||||
# Resolve which hosts to emit blocks for, and which user identity to
|
||||
# use as the IdentityFile filter against the rbw agent.
|
||||
selectedHostNames =
|
||||
if cfg.hosts == [ "all" ] then
|
||||
builtins.attrNames inventory.activeHosts
|
||||
else
|
||||
cfg.hosts;
|
||||
missingHosts = builtins.filter (name: !(builtins.hasAttr name inventory.hosts)) selectedHostNames;
|
||||
# All identities this HM user may authenticate AS: cfg.user ∪
|
||||
# extraIdentities. `cfg.user` can be null (e.g. user not in inventory
|
||||
# yet) — in that case the Host-block IdentityFile line is suppressed
|
||||
# and cross-account SSH is wired from extraIdentities alone.
|
||||
effectiveIdentities = lib.filter (n: n != null) ([ cfg.user ] ++ cfg.extraIdentities);
|
||||
defaultUserHasKey =
|
||||
cfg.user == null
|
||||
|| (inventory.users.${cfg.user}.publicKey or "") != "";
|
||||
# Each extra identity must have a pubkey in the inventory. Dropping
|
||||
# it silently would produce a dead Match block — fail the build.
|
||||
missingExtraKeys = lib.filter
|
||||
(n: (inventory.users.${n}.publicKey or "") == "")
|
||||
cfg.extraIdentities;
|
||||
in
|
||||
{
|
||||
options.chiasson.ssh.outbound.rbw = {
|
||||
enable = lib.mkEnableOption "Generated `~/.ssh/config` + rbw agent socket.";
|
||||
user = lib.mkOption {
|
||||
type = lib.types.nullOr lib.types.str;
|
||||
default = config.home.username;
|
||||
description = ''
|
||||
Inventory user whose public key is written to `~/.ssh/id_ed25519_<user>.pub`
|
||||
and used as `IdentityFile` to filter the rbw agent. Set `null`
|
||||
to disable the per-user .pub file / IdentityFile line (e.g.
|
||||
when the user isn't in the inventory yet).
|
||||
'';
|
||||
};
|
||||
hosts = lib.mkOption {
|
||||
type = lib.types.listOf lib.types.str;
|
||||
default = [ "all" ];
|
||||
description = "Inventory hosts to emit (or `[ \"all\" ]`).";
|
||||
};
|
||||
manageSshConfig = lib.mkOption {
|
||||
type = lib.types.bool;
|
||||
default = true;
|
||||
description = "Write `~/.ssh/config` (disable for manual management).";
|
||||
};
|
||||
# Cross-account SSH on the operator's laptop/desktop. Each entry
|
||||
# adds a `~/.ssh/id_ed25519_<account>.pub` filter file and a
|
||||
# `Match user <account>` block in `~/.ssh/config` so
|
||||
# `ssh <account>@<host>` picks the right key from the rbw agent.
|
||||
# `cfg.user` is already wired by the Host blocks — don't list it here.
|
||||
extraIdentities = lib.mkOption {
|
||||
type = lib.types.listOf lib.types.str;
|
||||
default = [ ];
|
||||
example = [ "server" "builder" ];
|
||||
description = ''
|
||||
Catalog accounts this HM user may authenticate AS, in addition
|
||||
to `cfg.user`. Each entry must have a publicKey pasted into
|
||||
`modules/lib/ssh-inventory.nix`. Typical for the operator's
|
||||
primary laptop: `[ "server" "builder" ]` so fleet/ops commands
|
||||
like `ssh server@r5500` or `ssh builder@nix-server` work
|
||||
without touching `/etc/passwd` on each host.
|
||||
'';
|
||||
};
|
||||
};
|
||||
|
||||
home.activation.rbwPinentryConfig = lib.hm.dag.entryAfter [ "writeBoundary" ] ''
|
||||
${pkgs.rbw}/bin/rbw config set pinentry "${pkgs.pinentry-gtk2}/bin/pinentry-gtk-2" >/dev/null 2>&1 || true
|
||||
'';
|
||||
}
|
||||
]);
|
||||
};
|
||||
config = lib.mkIf cfg.enable (lib.mkMerge [
|
||||
{
|
||||
assertions = [
|
||||
{
|
||||
assertion = missingHosts == [ ];
|
||||
message = "ssh.outbound.rbw: unknown host keys: ${builtins.concatStringsSep ", " missingHosts}";
|
||||
}
|
||||
{
|
||||
assertion = defaultUserHasKey;
|
||||
message = "ssh.outbound.rbw: no publicKey for inventory user `${cfg.user}` in `modules/lib/ssh-inventory.nix`.";
|
||||
}
|
||||
{
|
||||
assertion = missingExtraKeys == [ ];
|
||||
message = "ssh.outbound.rbw: extraIdentities has entries with no publicKey in `modules/lib/ssh-inventory.nix`: ${builtins.concatStringsSep ", " missingExtraKeys}";
|
||||
}
|
||||
# `enable=true` with neither a default user nor any extras gives the
|
||||
# agent no IdentityFile at all; with IdentitiesOnly yes the user
|
||||
# would silently be unable to authenticate as any catalog user.
|
||||
{
|
||||
assertion = effectiveIdentities != [ ];
|
||||
message = "ssh.outbound.rbw: enabled but no identities wired. Set `chiasson.ssh.outbound.rbw.user` (defaults to `home.username`) or list catalog accounts under `extraIdentities`.";
|
||||
}
|
||||
];
|
||||
}
|
||||
(lib.mkMerge [
|
||||
{
|
||||
home.packages = with pkgs; [ rbw pinentry-gtk2 ];
|
||||
home.sessionVariables.SSH_AUTH_SOCK = self.lib.rbwSshSocket.sessionVariable;
|
||||
# Write a `.pub` file per identity this HM user may act as
|
||||
# (cfg.user ∪ extraIdentities). OpenSSH reads each one via
|
||||
# IdentityFile/Match to filter agent keys — they never hold
|
||||
# private key material, so 0644 (HM's default) is fine.
|
||||
# OpenSSH's StrictModes only rejects group/other-writable files.
|
||||
home.file = lib.listToAttrs (map
|
||||
(n: {
|
||||
name = inventory.mkIdentityFileName n;
|
||||
value.text = "${inventory.users.${n}.publicKey}\n";
|
||||
})
|
||||
effectiveIdentities);
|
||||
programs.ssh.enable = lib.mkIf cfg.manageSshConfig false;
|
||||
}
|
||||
(lib.mkIf cfg.manageSshConfig {
|
||||
home.file.".ssh/config".text = inventory.mkSshConfigTemplate {
|
||||
selectedHosts = builtins.listToAttrs (map (n: { name = n; value = inventory.hosts.${n}; }) selectedHostNames);
|
||||
user = cfg.user;
|
||||
inherit (cfg) extraIdentities;
|
||||
};
|
||||
})
|
||||
{
|
||||
systemd.user.services.rbw-agent-bootstrap = {
|
||||
Unit = {
|
||||
Description = "Bootstrap rbw SSH agent";
|
||||
PartOf = [ "graphical-session.target" ];
|
||||
After = [ "graphical-session.target" ];
|
||||
};
|
||||
Service = {
|
||||
Type = "oneshot";
|
||||
ExecStart = "${pkgs.bash}/bin/bash -lc '${pkgs.rbw}/bin/rbw unlocked >/dev/null 2>&1 || true'";
|
||||
RemainAfterExit = true;
|
||||
};
|
||||
Install.WantedBy = [ "graphical-session.target" ];
|
||||
};
|
||||
home.activation.rbwPinentryConfig = lib.hm.dag.entryAfter [ "writeBoundary" ] ''
|
||||
${pkgs.rbw}/bin/rbw config set pinentry "${pkgs.pinentry-gtk2}/bin/pinentry-gtk-2" >/dev/null 2>&1 || true
|
||||
'';
|
||||
}
|
||||
])
|
||||
]);
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,35 +1,51 @@
|
||||
{ self, ... }: {
|
||||
flake.nixosModules.sshInbound = {
|
||||
config,
|
||||
lib,
|
||||
...
|
||||
}:
|
||||
let
|
||||
cfg = config.chiasson.ssh.inbound;
|
||||
inventory = self.lib.sshInventory;
|
||||
in
|
||||
{
|
||||
options.chiasson.ssh.inbound = {
|
||||
enable = lib.mkEnableOption "Apply SSH inventory public keys to `authorized_keys`.";
|
||||
userAuthorizedHosts = lib.mkOption {
|
||||
type = lib.types.attrsOf (lib.types.either (lib.types.enum [ "all" ]) (lib.types.listOf lib.types.str));
|
||||
default = { };
|
||||
example = {
|
||||
olivier = "all";
|
||||
admin = [ "14900k" "t2mbp" ];
|
||||
flake.nixosModules.sshInbound =
|
||||
{ config, lib, ... }:
|
||||
let
|
||||
cfg = config.chiasson.ssh.inbound;
|
||||
inventory = self.lib.sshInventory;
|
||||
# Every inbound-enabled user must have a publicKey pasted into the
|
||||
# inventory — silently dropping the key would lock the user out.
|
||||
missingKeys = lib.filter
|
||||
(name:
|
||||
let line = inventory.mkAuthorizedKeyLine name (cfg.userAuthorizedHosts.${name} or "all");
|
||||
in line == null)
|
||||
(lib.attrNames cfg.userAuthorizedHosts);
|
||||
in
|
||||
{
|
||||
options.chiasson.ssh.inbound = {
|
||||
enable = lib.mkEnableOption "Write user-specific SSH inventory public keys into `authorized_keys`.";
|
||||
userAuthorizedHosts = lib.mkOption {
|
||||
type = lib.types.attrsOf (lib.types.either (lib.types.enum [ "all" ]) (lib.types.listOf lib.types.str));
|
||||
default = { };
|
||||
example = {
|
||||
olivier = "all";
|
||||
server = [ "192.168.2.25" "192.168.2.15" ];
|
||||
};
|
||||
description = ''
|
||||
Catalog users whose inventory public key is installed in
|
||||
`authorized_keys`. The value is either `"all"` (no `from=`
|
||||
restriction) or a list of source identifiers appended to
|
||||
sshd's `from=` constraint. List entries should be IPs or CIDRs;
|
||||
hostnames require reverse-DNS to resolve cleanly on the target.
|
||||
'';
|
||||
};
|
||||
description = ''
|
||||
Catalog users that receive the SSH inventory public keys in `authorized_keys`.
|
||||
'';
|
||||
};
|
||||
};
|
||||
|
||||
config = lib.mkIf cfg.enable {
|
||||
users.users = lib.mapAttrs
|
||||
(_user: _selection: {
|
||||
openssh.authorizedKeys.keys = inventory.authorizedKeys;
|
||||
config = lib.mkMerge [
|
||||
{
|
||||
assertions = [{
|
||||
assertion = cfg.enable -> missingKeys == [ ];
|
||||
message = "chiasson.ssh.inbound: users enabled but missing public key in `modules/lib/ssh-inventory.nix`: ${lib.concatStringsSep ", " missingKeys}";
|
||||
}];
|
||||
}
|
||||
(lib.mkIf cfg.enable {
|
||||
users.users = lib.mapAttrs
|
||||
(name: selection: {
|
||||
openssh.authorizedKeys.keys = [ (inventory.mkAuthorizedKeyLine name selection) ];
|
||||
})
|
||||
cfg.userAuthorizedHosts;
|
||||
})
|
||||
cfg.userAuthorizedHosts;
|
||||
];
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user