Skip to content

Unify user management into one primitive; provision the runner from its identity, not committed lists #27

Description

@thiras

Summary

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:

  1. create the user (groups: [sudo], append, /bin/bash, create_home)
  2. 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
  3. lock the password (key-only login)
  4. install authorized keys
  5. 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

  1. One account primitive. Factor the five steps into a single reusable unit
    (a sudo_account.yml include_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.
  2. 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).
  3. 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.
  • No secrets or private keys enter version control (repo hard rule feat(ansible): add deployment for deCDN node and internal anvil devnet #1).

Acceptance

  • A single primitive renders a key-only NOPASSWD sudo account; admin + named
    users both use it (no duplicated sanitize/lock logic).
  • Running make deploy provisions the runner as an admin with no entry
    committed to git.
  • Merged-set lockout + sudoers.d-filename-uniqueness asserts cover admin and
    named users together.
  • make check on a fresh host and make molecule both still pass.

Related: #25 (encrypted-at-rest inventory secrets).

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Type

    No type

    Fields

    Priority

    None yet

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions