You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
The baseline role has two parallel, near-duplicate user-management paths. Unify
them into a single mechanism, and make the operator running the playbook a
first-class member of that mechanism (detected from the runner's identity)
rather than something that has to be enumerated and committed to version control.
Current state
1. Admin account — roles/baseline/tasks/main.yml. Already "deploy as
yourself": ssh_admin_user defaults to the control-machine $USER and ssh_admin_pubkey autodetects from ~/.ssh (id_ed25519 > ecdsa > rsa).
No identity committed to git in the common case.
2. Named sudo users — roles/baseline/tasks/sudo_users.yml, driven by the baseline_sudo_users list. Each entry is a hardcoded {name, keys} that must
be enumerated in inventory/group_vars and committed to version control.
The two paths do the same five steps, copy-pasted:
create the user (groups: [sudo], append, /bin/bash, create_home)
NOPASSWD /etc/sudoers.d/<name> drop-in — with the identical
filename-sanitize hazard (regex_replace('[^A-Za-z0-9_-]', '_'), the ./~#includedir-skip footgun) duplicated in both files
lock the password (key-only login)
install authorized keys
the same lockout-avoidance reasoning, re-derived in both places
Divergences today: the admin path has an unconditional lockout-guard assert and a getent existence probe for check-mode; the named-users path re-implements a
different set of pre-flight asserts (sudoers_dropin_names uniqueness, keyless /
stringy-keys detection) and a when: not ansible_check_mode guard. Neither's
safety logic is shared, so a fix to one (e.g. a new sudoers.d hazard) has to be
remembered in the other.
Problems
Duplication / drift risk — one conceptual operation ("a key-only NOPASSWD
sudo account") implemented twice, with subtly different guards. The sudoers.d
filename-collision-vs-admin check in sudo_users.yml exists only because the
two paths are separate and can clobber each other's drop-ins.
Identities committed to git — baseline_sudo_users requires usernames and
full public keys in tracked files. Public keys aren't secrets, but committing
the operator roster is churn (every team change is a repo change) and is
exactly the enumeration the admin path already avoids by detecting $USER + ~/.ssh. Adjacent to Support encrypted-at-rest inventory secrets (ansible-vault / SOPS) #25 (encrypting secret inventory), but distinct: this
is about not committing the identity at all.
Proposal
One account primitive. Factor the five steps into a single reusable unit
(a sudo_account.ymlinclude_tasks taking {name, keys}, or a small custom
action) that owns the sanitize/lockout/check-mode logic once. Both the admin
and every named user route through it.
One operator list. The admin becomes just the auto-detected head of that
list. Conceptually: resolved_operators = [runner-detected] + committed extras,
fed through the same primitive, with the uniqueness/lockout asserts applied to
the merged set (subsuming today's admin-vs-named collision check).
Runner detection as the default. Keep $USER + ~/.ssh autodetection as
the zero-config path so the person running make deploy is provisioned without
appearing in any tracked file. baseline_sudo_users stays available for
explicitly adding other operators, but is no longer required to onboard the
runner.
Constraints / must-preserve
The unconditional lockout guard (non-root admin user + ≥1 key resolved
before ssh_hardening runs) must still gate the whole merged set — hardening
must never be reachable keyless.
Keep the check-mode / fresh-host behavior (make check must not hard-fail
when accounts don't exist yet).
Keep the sudoers.d ./~ filename sanitization and the
keyless/stringy-keys pre-flight rejections.
Molecule must still be able to exercise the named-user block standalone
(tasks_from), per the note in sudo_users.yml.
Summary
The
baselinerole has two parallel, near-duplicate user-management paths. Unifythem into a single mechanism, and make the operator running the playbook a
first-class member of that mechanism (detected from the runner's identity)
rather than something that has to be enumerated and committed to version control.
Current state
1. Admin account —
roles/baseline/tasks/main.yml. Already "deploy asyourself":
ssh_admin_userdefaults to the control-machine$USERandssh_admin_pubkeyautodetects from~/.ssh(id_ed25519>ecdsa>rsa).No identity committed to git in the common case.
2. Named sudo users —
roles/baseline/tasks/sudo_users.yml, driven by thebaseline_sudo_userslist. Each entry is a hardcoded{name, keys}that mustbe enumerated in inventory/group_vars and committed to version control.
The two paths do the same five steps, copy-pasted:
groups: [sudo],append,/bin/bash,create_home)/etc/sudoers.d/<name>drop-in — with the identicalfilename-sanitize hazard (
regex_replace('[^A-Za-z0-9_-]', '_'), the./~#includedir-skip footgun) duplicated in both filesDivergences today: the admin path has an unconditional lockout-guard assert and a
getentexistence probe for check-mode; the named-users path re-implements adifferent set of pre-flight asserts (
sudoers_dropin_namesuniqueness, keyless /stringy-keys detection) and a
when: not ansible_check_modeguard. Neither'ssafety logic is shared, so a fix to one (e.g. a new sudoers.d hazard) has to be
remembered in the other.
Problems
sudo account") implemented twice, with subtly different guards. The sudoers.d
filename-collision-vs-admin check in
sudo_users.ymlexists only because thetwo paths are separate and can clobber each other's drop-ins.
baseline_sudo_usersrequires usernames andfull public keys in tracked files. Public keys aren't secrets, but committing
the operator roster is churn (every team change is a repo change) and is
exactly the enumeration the admin path already avoids by detecting
$USER+~/.ssh. Adjacent to Support encrypted-at-rest inventory secrets (ansible-vault / SOPS) #25 (encrypting secret inventory), but distinct: thisis about not committing the identity at all.
Proposal
(a
sudo_account.ymlinclude_taskstaking{name, keys}, or a small customaction) that owns the sanitize/lockout/check-mode logic once. Both the admin
and every named user route through it.
list. Conceptually:
resolved_operators = [runner-detected] + committed extras,fed through the same primitive, with the uniqueness/lockout asserts applied to
the merged set (subsuming today's admin-vs-named collision check).
$USER+~/.sshautodetection asthe zero-config path so the person running
make deployis provisioned withoutappearing in any tracked file.
baseline_sudo_usersstays available forexplicitly adding other operators, but is no longer required to onboard the
runner.
Constraints / must-preserve
before
ssh_hardeningruns) must still gate the whole merged set — hardeningmust never be reachable keyless.
make checkmust not hard-failwhen accounts don't exist yet).
./~filename sanitization and thekeyless/stringy-
keyspre-flight rejections.(
tasks_from), per the note insudo_users.yml.Acceptance
users both use it (no duplicated sanitize/lock logic).
make deployprovisions the runner as an admin with no entrycommitted to git.
named users together.
make checkon a fresh host andmake moleculeboth still pass.Related: #25 (encrypted-at-rest inventory secrets).