feat(baseline): named sudo users (key-only NOPASSWD accounts) - #23
Conversation
Add `baseline_sudo_users`: a list of `{name, keys:[...]}` entries, each
becoming a distinct login account (own username, home, key) created
key-only exactly like the admin — member of `sudo`, a NOPASSWD sudoers.d
drop-in, and a locked password. This grants teammates their OWN accounts
rather than sharing keys on the admin via `ssh_admin_extra_pubkeys`.
The block lives in its own `tasks/sudo_users.yml` (included after the
admin keys, before the firewall/DevSec hardening) so molecule can exercise
this container-safe subset on its own; the rest of baseline is host-only.
Mirrors the admin path's hard-won reasoning: the sudoers.d `.`/`~`
filename sanitize, `visudo -cf` validation, and a check-mode guard on the
key install. A fail-loud precondition assert refuses two silent-lockout
input shapes: an entry with no keys, and names that sanitize to a
colliding drop-in filename (including vs the admin's own).
Molecule coverage (two users incl. a dotted name + multi-key, non-vacuous
password-lock assertion) plus README, defaults, and group_vars example.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
Warning Review limit reached
Next review available in: 44 minutes Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available. How can I continue?After more reviews become available, a review can be triggered using the To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews. How do review limits work?CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability. For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window. Please refer docs for additional details. Review details⚙️ Run configurationConfiguration used: Organization UI Review profile: CHILL Plan: Pro Plus Run ID: 📒 Files selected for processing (1)
📝 WalkthroughWalkthroughThe baseline role adds ChangesNamed sudo user accounts
Estimated code review effort: 3 (Moderate) | ~25 minutes Sequence Diagram(s)sequenceDiagram
participant Baseline as baseline role
participant Users as sudo_users.yml
participant System as Linux accounts and sudoers
participant SSH as authorized_keys
Baseline->>Users: include named-user tasks
Users->>System: create sudo users
Users->>System: install and validate NOPASSWD rules
Users->>System: lock local passwords
Users->>SSH: install configured public keys
System-->>Baseline: expose account and sudoers state
SSH-->>Baseline: expose authorized key state
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Code Review
This pull request introduces the baseline_sudo_users feature to the baseline Ansible role, allowing the creation of distinct, key-only named sudo accounts with passwordless sudo access. The changes include task definitions, documentation updates, and Molecule test coverage. The review feedback highlights potential failures during dry-runs (--check mode) on fresh hosts. Specifically, the reviewer suggests probing existing users with getent to conditionally run the password locking and authorized keys installation tasks in check mode, which prevents execution failures while still enabling configuration drift reporting for existing users.
Important
The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.
There was a problem hiding this comment.
🧹 Nitpick comments (1)
ansible/roles/baseline/tasks/sudo_users.yml (1)
27-50: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueConsider validating
nameis non-empty in the assert.The assert catches keyless entries and filename collisions, but an entry with a missing or empty
namewould pass this check and fail later at theusermodule step. While the failure would be loud (not silent), it slightly undermines the assert's stated design goal of catching all misconfiguration "before any account is touched." Adding anamecheck would make the fail message more informative and consistent with the assert's purpose.♻️ Optional: add name validation to the assert
that: + - named_users_without_name | length == 0 - keyless_named_users | length == 0 - sudoers_dropin_names | length == (sudoers_dropin_names | unique | length) fail_msg: >- baseline_sudo_users is misconfigured (validated before any account is created). + Entries lacking a non-empty `name`: {{ named_users_without_name }}. Entries lacking a non-empty `keys` list: {{ keyless_named_users }}. Sanitized /etc/sudoers.d/ filenames must be unique (including vs the admin drop-in), else one grant silently overwrites another -> silent sudo lockout; got {{ sudoers_dropin_names }}. vars: + named_users_without_name: >- + {{ (baseline_sudo_users | rejectattr('name', 'defined') + | map(attribute='name') | list) + + (baseline_sudo_users | selectattr('name', 'defined') + | selectattr('name', 'falsy') | map(attribute='name') | list) }} keyless_named_users: >-🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@ansible/roles/baseline/tasks/sudo_users.yml` around lines 27 - 50, Update the “Assert each named sudo user” task to reject entries whose name is missing or empty before account creation. Add a name-validation condition and include the affected entries in fail_msg, alongside keyless_named_users and sudoers_dropin_names, while preserving the existing key and filename uniqueness checks.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Nitpick comments:
In `@ansible/roles/baseline/tasks/sudo_users.yml`:
- Around line 27-50: Update the “Assert each named sudo user” task to reject
entries whose name is missing or empty before account creation. Add a
name-validation condition and include the affected entries in fail_msg,
alongside keyless_named_users and sudoers_dropin_names, while preserving the
existing key and filename uniqueness checks.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro Plus
Run ID: 1e4e0e50-f1c0-4e65-8915-8f3d48692409
📒 Files selected for processing (7)
ansible/inventory/group_vars/all.ymlansible/molecule/default/converge.ymlansible/molecule/default/verify.ymlansible/roles/baseline/README.mdansible/roles/baseline/defaults/main.ymlansible/roles/baseline/tasks/main.ymlansible/roles/baseline/tasks/sudo_users.yml
There was a problem hiding this comment.
Pull request overview
Adds support in the baseline Ansible role for provisioning multiple distinct, key-only sudo-enabled operator accounts (each with its own username/home/authorized_keys) instead of sharing keys on the single admin account.
Changes:
- Introduces
baseline_sudo_usersand a dedicatedsudo_users.ymltask file to create per-operator accounts, sudoers drop-ins, password locks, and authorized keys. - Wires the new task block into
roles/baseline/tasks/main.yml(runs after admin key setup and before firewall/hardening). - Extends Molecule coverage plus docs/defaults/inventory examples for the new variable.
Reviewed changes
Copilot reviewed 7 out of 7 changed files in this pull request and generated 1 comment.
Show a summary per file
| File | Description |
|---|---|
| ansible/roles/baseline/tasks/sudo_users.yml | New task block to assert input, create named sudo users, write sudoers drop-ins, lock passwords, and install authorized keys. |
| ansible/roles/baseline/tasks/main.yml | Includes the new sudo_users.yml block when baseline_sudo_users is non-empty, positioned before firewall/hardening. |
| ansible/roles/baseline/README.md | Documents baseline_sudo_users and clarifies distinction from ssh_admin_extra_pubkeys. |
| ansible/roles/baseline/defaults/main.yml | Adds baseline_sudo_users: [] with documentation of expected shape/behavior. |
| ansible/molecule/default/converge.yml | Configures baseline_sudo_users test data and runs tasks_from: sudo_users for container-safe Molecule coverage. |
| ansible/molecule/default/verify.yml | Verifies the two named users exist, are in sudo, have correct sudoers drop-ins, keys installed, and passwords locked. |
| ansible/inventory/group_vars/all.yml | Adds a commented example showing how to configure baseline_sudo_users. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Harden the baseline_sudo_users precondition assert (PR review): also reject
a scalar-string `keys` (truthy, so it slipped past the keyless check and
would mis-iterate in subelements AFTER accounts are already changed) and a
missing/empty `name`, so all malformed input fails before any account is
touched.
Read `keys` via map('list') + the `superset` test rather than
map(attribute='keys'): `keys` collides with the dict .keys() method, so
attribute access on an entry that omits `keys` (a `key:` typo) returns the
bound method (truthy) and silently escaped the keyless check. Builtin-only —
community.general (json_query) stays intentionally absent per galaxy.yml.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
What & why
Adds
baseline_sudo_usersto thebaselinerole: a list of{name, keys:[...]}entries, each becoming a distinct login account (own username, home, key) with sudo. Today the role supports exactly one admin account plus extra authorized keys on that same account (ssh_admin_extra_pubkeys); this grants teammates their own accounts instead of sharing keys on the admin.Each account is created key-only, exactly like the admin: member of
sudo, a NOPASSWD sudoers.d drop-in, and a locked password (login by key only).Implementation
roles/baseline/tasks/sudo_users.yml, wired viainclude_tasksfrommain.ymlafter the admin keys and before the firewall/DevSec hardening (so a listed operator can log in the moment hardening lands). Kept in its own file so molecule can exercise this container-safe block on its own — the rest ofbaselineis real-host-only.#includedirfilename hazard (./~silently skipped → silent lockout) → sameregex_replace('[^A-Za-z0-9_-]', '_')sanitize; the rule inside still names the real user.visudo -cfcontent validation.authorized_keycheck-mode hard-fail → guarded withwhen: not ansible_check_mode(these accounts aren't lockout-critical — the play always connects as the admin — so no getent-probe needed).keys, and names that sanitize to a colliding drop-in filename (including vs the admin's own drop-in).Docs & config
defaults/main.yml:baseline_sudo_users: []with a comment distinguishing it fromssh_admin_extra_pubkeys.README.md: table row + a sentence under the admin step.inventory/group_vars/all.yml: commented example (nothing enabled).Test coverage (molecule, runs in CI)
Two named users driven through converge → idempotence → verify:
molecule-operator(simple name, one key) andalice.smith(a.in the name — verifies the drop-in lands at the sanitized/etc/sudoers.d/alice_smith; two keys exercise thesubelementsfan-out).sudo, drop-ins are0440containingNOPASSWD:ALL, keys installed, and passwords locked.alice.smithis seeded with a*disabled-password sentinel sopassword_lockmust convert it to!*— a non-vacuous lock assertion.Validation
make lint-ansible→ pass (production profile)make molecule→ pass, idempotence successful, new assertions ranmake security(KICS) →HIGH: 0; no new findings from these changes🤖 Generated with Claude Code
Summary by CodeRabbit