Skip to content

feat(baseline)!: unify sudo accounts into one primitive; single operator list - #30

Merged
thiras merged 2 commits into
mainfrom
issue-27-unify-sudo-accounts
Jul 12, 2026
Merged

thiras merged 2 commits into
mainfrom
issue-27-unify-sudo-accounts

Conversation

@thiras

@thiras thiras commented Jul 12, 2026 •

Copy link
Copy Markdown
Contributor

Summary

Implements #27 (unify the two duplicate sudo-account paths into one primitive) and, per follow-up, collapses the ssh_admin_* config family into a single operator list.

What changed

  • One primitive — roles/baseline/tasks/sudo_account.yml renders a single key-only NOPASSWD account (create → sanitized /etc/sudoers.d drop-in → password lock → getent probe → key install). Both the admin head and every named operator route through it.
  • One config — removed ssh_admin_user, ssh_admin_pubkey, ssh_admin_pubkey_autodetect, ssh_admin_extra_pubkeys, ssh_admin_passwordless_sudo. Admins now come from a single baseline_sudo_users list, with two knobs: baseline_sudo_autodetect_runner (default true) and baseline_sudo_passwordless (default true, per-entry override).
  • Runner as head — the control-machine runner ($USER + ~/.ssh key) is auto-detected and prepended as the lockout-critical head, so the operator running make deploy is provisioned without being committed. A same-name explicit entry overrides the auto-head.
  • Lockout guard — one unconditional assert over the merged set requires ≥1 non-root operator with a usable (non-blank) key before ssh_hardening runs; the pre-flight rejects malformed entries (missing name, stringy/empty/blank keys, colliding sudoers.d filenames).

Breaking change

Inventories using ssh_admin_* must migrate to baseline_sudo_users (+ the two knobs). All in-repo references (group_vars, both READMEs, galaxy example, root quickstart) are updated.

Review

A multi-agent PR review caught a silent-lockout bug: a blank/whitespace key member (keys: [""]) slipped all validation into a password-locked, keyless account. Fixed via a pre-flight blank_key_entries rejection and by strengthening the lockout guard to require usable (non-blank) key material. Stale-doc and comment findings also addressed.

Verification

  • make lint-ansible — clean (production profile).
  • make molecule default scenario — converge + idempotence (changed=0) + verify green.
  • Runner-head + lockout logic exercised across a 9-case matrix (blank/whitespace/empty/keyless/root-only/override/autodetect-off).

Follow-up (not in this PR)

A committed regression test for the main.yml resolution/guard (molecule only covers sudo_users.yml) — recommend extracting the resolution into a tasks_from-able file with a dedicated scenario.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Added centralized configuration for multiple sudo operator accounts and SSH keys.
    • Added optional automatic inclusion of the deployment runner as an operator.
    • Added configurable passwordless sudo behavior globally or per operator.
    • Added safeguards against creating inaccessible or keyless administrative setups.
  • Documentation

    • Updated setup guides, examples, defaults, and migration guidance for the new operator configuration.

…tor list

Collapse the baseline role's two near-duplicate key-only NOPASSWD
sudo-account paths (the admin account in main.yml and named users in
sudo_users.yml) into one primitive, and remove the ssh_admin_* config
family in favour of a single operator list.

- New tasks/sudo_account.yml renders one account (create -> sanitized
  /etc/sudoers.d drop-in -> password lock -> getent probe -> keys); the
  admin head and every named operator both route through it, so the
  sanitize/lock/check-mode logic lives in one place.
- tasks/sudo_users.yml validates the whole set (well-formed, keyed,
  unique sudoers.d filenames, non-blank key members) and loops the
  primitive; still standalone-includable for molecule (tasks_from).
- main.yml auto-detects the runner ($USER + ~/.ssh) as the lockout-
  critical head of baseline_sudo_users; a same-name explicit entry
  overrides it. The unconditional lockout guard now requires >=1
  non-root operator with a usable (non-blank) key before hardening.

BREAKING CHANGE: removes ssh_admin_user, ssh_admin_pubkey,
ssh_admin_pubkey_autodetect, ssh_admin_extra_pubkeys and
ssh_admin_passwordless_sudo. Configure admins via baseline_sudo_users
plus baseline_sudo_autodetect_runner / baseline_sudo_passwordless.

Verified: ansible-lint (production profile), molecule default (converge
+ idempotence + verify), and a runner-head/lockout logic matrix.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings July 12, 2026 15:28
@coderabbitai

coderabbitai Bot commented Jul 12, 2026 •

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@thiras, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 47 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

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 configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 6652806b-dd89-4f7f-9165-921329d11455

📥 Commits

Reviewing files that changed from the base of the PR and between 91bc380 and 1c29c37.

📒 Files selected for processing (3)
  • ansible/.ansible-lint
  • ansible/inventory/hosts.yml.example
  • ansible/roles/baseline/tasks/main.yml
📝 Walkthrough

Walkthrough

The PR replaces legacy single-admin SSH variables with a unified baseline_sudo_users model. It optionally prepends an autodetected runner, validates the merged operator set, guards against SSH lockout, and provisions each account through shared tasks.

Changes

Sudo operator model

Layer / File(s) Summary
Operator configuration contract
README.md, ansible/README.md, ansible/galaxy/README.md, ansible/inventory/group_vars/all.yml, ansible/roles/baseline/{README.md,defaults/main.yml}, ansible/molecule/default/converge.yml
Documentation, defaults, inventory, examples, and fixture comments now describe baseline_sudo_users, optional runner autodetection, and passwordless settings.
Operator resolution and lockout guard
ansible/roles/baseline/tasks/main.yml
The role merges an optional autodetected runner with configured operators, asserts that at least one non-root operator has keys, reports the resolved set, and invokes shared provisioning.
Shared account validation and provisioning
ansible/roles/baseline/tasks/sudo_users.yml, ansible/roles/baseline/tasks/sudo_account.yml
Validation covers operator names, key values, and sanitized sudoers filenames; each operator is provisioned with user creation, sudoers configuration, password handling, and authorized keys.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Runner as Control-machine runner
  participant Baseline as baseline tasks/main.yml
  participant Operators as sudo_users.yml
  participant Account as sudo_account.yml
  participant Host as Target host

  Runner->>Baseline: Provide runner identity and SSH key
  Baseline->>Baseline: Merge runner head with baseline_sudo_users
  Baseline->>Baseline: Validate keyed non-root operator exists
  Baseline->>Operators: Provision baseline_operators
  Operators->>Account: Include one task per operator
  Account->>Host: Create account and configure sudo
  Account->>Host: Install authorized keys
Loading

Possibly related issues

Possibly related PRs

  • decdn/devops#23 — Directly overlaps with the baseline_sudo_users transition and operator-list provisioning workflow.
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly captures the main change: consolidating sudo accounts into one primitive and a single operator list.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch issue-27-unify-sudo-accounts

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request refactors the baseline Ansible role to consolidate admin and named sudo users into a single operator list, utilizing a shared primitive task for account provisioning. However, a critical issue was identified in both tasks/main.yml and tasks/sudo_users.yml where Jinja2 filters attempt to access the 'keys' attribute of dictionaries. In Jinja2, this resolves to the dictionary's built-in '.keys()' method rather than the key's value, which can cause playbook crashes or bypass the lockout guard. The reviewer provides robust Jinja2 loop suggestions to safely extract and validate these keys.

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.

Comment thread ansible/roles/baseline/tasks/main.yml
Comment thread ansible/roles/baseline/tasks/sudo_users.yml

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR refactors the baseline Ansible role to unify previously split “admin user” and “named sudo users” provisioning into a single operator mechanism, with the runner identity auto-detected as the default lockout-critical head. It also migrates documentation/examples away from the ssh_admin_* variable family to the new baseline_sudo_* configuration.

Changes:

  • Introduces a shared sudo_account.yml task primitive and routes all sudo-operator provisioning through it.
  • Replaces the ssh_admin_* configuration family with a single baseline_sudo_users list plus baseline_sudo_autodetect_runner / baseline_sudo_passwordless.
  • Updates READMEs, inventory examples, and molecule coverage text to reflect the new operator model.

Reviewed changes

Copilot reviewed 10 out of 10 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
README.md Quickstart text updated to reference baseline_sudo_users runner autodetection.
ansible/roles/baseline/tasks/sudo_users.yml Validates the merged operator set and provisions operators via the shared primitive.
ansible/roles/baseline/tasks/sudo_account.yml New reusable task primitive for creating key-only sudo operators (sudoers drop-in, password lock, key install, check-mode safety).
ansible/roles/baseline/tasks/main.yml Resolves runner head + operator set and enforces an unconditional lockout guard before hardening.
ansible/roles/baseline/README.md Role documentation migrated to the unified operator list and new knobs.
ansible/roles/baseline/defaults/main.yml Defaults migrated from ssh_admin_* to baseline_sudo_*.
ansible/README.md Deployment docs migrated to the unified operator model and lockout-guard description.
ansible/molecule/default/converge.yml Updates molecule comments to match the new multi-key loop wording.
ansible/inventory/group_vars/all.yml Inventory documentation migrated to baseline_sudo_* and single operator list.
ansible/galaxy/README.md Collection usage example updated to baseline_sudo_users + disabling runner autodetect for explicit admin provisioning.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread ansible/roles/baseline/tasks/main.yml
Comment thread ansible/README.md

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 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.

Inline comments:
In `@ansible/roles/baseline/tasks/main.yml`:
- Around line 25-39: The autodetection candidates in the baseline_runner_pubkey
task use literal `~` paths that `first_found` cannot resolve. Update the
`candidates` values used by `found` to expand the controller’s home directory
before calling `query('ansible.builtin.first_found', ...)`, while preserving the
existing autodetection condition and key lookup behavior.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 67f5ab18-fcd9-437b-8102-46b46f1e3ebd

📥 Commits

Reviewing files that changed from the base of the PR and between 9013a7c and 91bc380.

📒 Files selected for processing (10)
  • README.md
  • ansible/README.md
  • ansible/galaxy/README.md
  • ansible/inventory/group_vars/all.yml
  • ansible/molecule/default/converge.yml
  • ansible/roles/baseline/README.md
  • ansible/roles/baseline/defaults/main.yml
  • ansible/roles/baseline/tasks/main.yml
  • ansible/roles/baseline/tasks/sudo_account.yml
  • ansible/roles/baseline/tasks/sudo_users.yml

Comment thread ansible/roles/baseline/tasks/main.yml
…itly

Address PR #30 review:
- hosts.yml.example + .ansible-lint still referenced the removed ssh_admin_*
  variables (missed by an earlier *.yml-scoped grep). Point them at
  baseline_sudo_users / baseline_sudo_passwordless.
- Expand ~ with the expanduser filter before first_found in the runner
  autodetect, so the lockout-critical key lookup does not depend on
  first_found's version-varying tilde handling (verified working on the
  pinned ansible-core; hardened for robustness).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@thiras
thiras merged commit 589b9b1 into main Jul 12, 2026
9 checks passed
@thiras
thiras deleted the issue-27-unify-sudo-accounts branch July 12, 2026 15:53
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants