-
Notifications
You must be signed in to change notification settings - Fork 0
feat(baseline): named sudo users (key-only NOPASSWD accounts) #23
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
2 commits
Select commit
Hold shift + click to select a range
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,127 @@ | ||
| --- | ||
| # Additional named sudo users — each its OWN 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 (login by key only). Distinct from | ||
| # ssh_admin_extra_pubkeys, which only adds keys to the SHARED admin account. Runs | ||
| # after the admin block and before the firewall / DevSec hardening, so a listed | ||
| # operator can log in immediately once hardening applies. | ||
| # | ||
| # These accounts are NOT lockout-critical — the play always connects as the admin | ||
| # user — so, unlike the admin path, this block needs no getent-probe or lockout | ||
| # assert: a plain `when: not ansible_check_mode` guard on the key install is enough | ||
| # to keep `make check` on a fresh host (where the account doesn't exist yet, so | ||
| # authorized_key would hard-fail) from blowing up. | ||
|
|
||
| # Refuse two operator-input shapes that would otherwise misconfigure an account | ||
| # SILENTLY (the role's ethos: fail loud, never a silent sudo lockout) — checked here, | ||
| # before any account is touched: | ||
| # * An entry with no `keys` (or keys: []): the tasks below would still create the | ||
| # account, join it to `sudo`, write its NOPASSWD drop-in, and lock its password — | ||
| # a dormant account with passwordless root and no way to log in (ssh_hardening | ||
| # disables password auth). A `key:`/`keys:` typo hits exactly this. | ||
| # * Two names that sanitize to the SAME sudoers.d filename (e.g. `deploy.bot` and | ||
| # `deploy_bot`, or a name colliding with the admin's own drop-in — this block runs | ||
| # AFTER the admin block): the copy loop would silently overwrite, leaving a user | ||
| # with `sudo` + a locked password but NO NOPASSWD grant. `visudo -cf` validates | ||
| # content, not a clobbered file, so it cannot catch this. | ||
| - name: Assert each named sudo user is well-formed and has a unique sudoers.d filename | ||
| ansible.builtin.assert: | ||
| that: | ||
| - unnamed_entries | length == 0 | ||
| # `keys` as a scalar string (a common `- "ssh-..."` mistake) is truthy, so it | ||
| # slips past the keyless check below, but subelements() then mis-iterates it AFTER | ||
| # accounts are already changed. Reject it up front to keep this a pre-flight gate. | ||
| - stringy_keys_entries | 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 with a missing/empty `name`: {{ unnamed_entries | length }}. | ||
| Entries whose `keys` is a string, not a list: {{ stringy_keys_entries }}. | ||
| 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: | ||
| # `keys` must NOT be read with map(attribute='keys'): it collides with the dict | ||
| # .keys() METHOD, so an entry that omits `keys` (a `key:` typo) yields the bound | ||
| # method (truthy) and silently escapes the keyless check. `map('list')` yields each | ||
| # entry's real key NAMES instead, so `superset(['keys'])` tells us which entries | ||
| # actually carry a `keys` key; attribute access is then safe only on those. | ||
| names: "{{ baseline_sudo_users | map(attribute='name', default='') | list }}" | ||
| key_names_per_entry: "{{ baseline_sudo_users | map('list') | list }}" | ||
| with_keys: >- | ||
| {{ baseline_sudo_users | zip(key_names_per_entry) | ||
| | selectattr('1', 'superset', ['keys']) | map(attribute='0') | list }} | ||
| # `default=''` on the name deref so a missing `name` reports cleanly here rather | ||
| # than raising an undefined-attribute error while building fail_msg. | ||
| unnamed_entries: >- | ||
| {{ (baseline_sudo_users | rejectattr('name', 'defined') | list) | ||
| + (baseline_sudo_users | selectattr('name', 'defined') | ||
| | rejectattr('name', 'truthy') | list) }} | ||
| stringy_keys_entries: >- | ||
| {{ with_keys | selectattr('keys', 'string') | ||
| | map(attribute='name', default='') | list }} | ||
| # Keyless = entries with no `keys` key at all (absent), plus entries whose `keys` | ||
| # is present but empty/null (falsy). | ||
| keyless_named_users: >- | ||
| {{ (names | zip(key_names_per_entry) | ||
| | rejectattr('1', 'superset', ['keys']) | map(attribute='0') | list) | ||
| + (with_keys | selectattr('keys', 'falsy') | ||
| | map(attribute='name', default='') | list) }} | ||
| # ssh_admin_user_effective is resolved by the admin block in main.yml; on the | ||
| # molecule `tasks_from: sudo_users` path that block never runs, so default it out. | ||
| sudoers_dropin_names: >- | ||
| {{ (names | map('regex_replace', '[^A-Za-z0-9_-]', '_') | list) | ||
| + ([ssh_admin_user_effective | regex_replace('[^A-Za-z0-9_-]', '_')] | ||
| if ssh_admin_user_effective is defined else []) }} | ||
|
|
||
| - name: Create the additional named sudo users | ||
| ansible.builtin.user: | ||
| name: "{{ item.name }}" | ||
| groups: [sudo] | ||
| append: true | ||
| shell: /bin/bash | ||
| create_home: true | ||
| loop: "{{ baseline_sudo_users }}" | ||
| loop_control: | ||
| label: "{{ item.name }}" | ||
|
|
||
| # Same sudoers.d filename hazard as the admin drop-in: sudo's #includedir SKIPS any | ||
| # file whose name contains a '.' or ends in '~' — which, with the password locked | ||
| # below, is a silent sudo lockout. Sanitize the username into the filename (the rule | ||
| # inside still names the real user); `visudo -cf` validates content, not the name. | ||
| - name: Grant each named sudo user passwordless sudo | ||
| ansible.builtin.copy: | ||
| dest: "/etc/sudoers.d/{{ item.name | regex_replace('[^A-Za-z0-9_-]', '_') }}" | ||
| owner: root | ||
| group: root | ||
| mode: "0440" | ||
| content: "{{ item.name }} ALL=(ALL) NOPASSWD:ALL\n" | ||
| validate: "visudo -cf %s" | ||
| loop: "{{ baseline_sudo_users }}" | ||
| loop_control: | ||
| label: "{{ item.name }}" | ||
|
|
||
| - name: Lock each named sudo user's password (key-only login; NOPASSWD sudo) | ||
| ansible.builtin.user: | ||
| name: "{{ item.name }}" | ||
| password_lock: true | ||
| loop: "{{ baseline_sudo_users }}" | ||
| loop_control: | ||
| label: "{{ item.name }}" | ||
|
|
||
| # subelements flattens each (user, key) pair. The assert above already rejects a | ||
| # keyless entry, so skip_missing=true here is just defence-in-depth (it would emit | ||
| # no pairs rather than error). authorized_key writes into the account's ~/.ssh, so | ||
| # the user must exist first — under --check the create above is a no-op, so skip the | ||
| # install in check mode (see the not-lockout-critical note in the block header above). | ||
| - name: Install each named sudo user's authorized keys | ||
| ansible.posix.authorized_key: | ||
| user: "{{ item.0.name }}" | ||
| key: "{{ item.1 }}" | ||
| state: present | ||
| loop: "{{ baseline_sudo_users | subelements('keys', skip_missing=true) }}" | ||
| loop_control: | ||
| label: "{{ item.0.name }}" | ||
| when: not ansible_check_mode | ||
|
thiras marked this conversation as resolved.
|
||
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.