1. Define ownership of group state
The fictional service needs a trainee account with primary group appops and initial supplementary membership in legacyops. The play creates the groups first and declares that distinction through the user module. An exercise then adds reporting with append: true, retaining legacyops. A run with append: false leaves only reporting among supplementary groups; appops remains the primary group. Choosing the parameter requires knowing who owns the complete set. If another team manages a legitimate membership, an incomplete authoritative list can remove it. If the approved intention is to withdraw old access, a conservative addition can retain it. Before changing membership, compare current state, desired state and ownership of each entitlement.
2. Interpret prediction and convergence
The lab first requests creation of predicted_only in check mode. Each node reports a predicted change, but getent confirms the group does not exist afterward. By contrast, accounts.yml runs normally and changes accounts, keys, directories and the sudo rule. The second execution of that same desired state finishes with zero changes on both nodes. This demonstrates idempotency in the observed context, not freedom from future drift or universal correctness of the design. Post-play checks inspect the primary group, supplementary groups, owner, group and 0750 mode of the handover directory. When check mode encounters dependencies among resources that do not yet exist, prediction can be partial or fail; an actual rehearsal on a disposable target remains necessary.
3. Manage keys as a complete set
The account receives two public keys through one authorized_key call with exclusive: true. The deliberately incorrect exercise submits one key at a time in a loop while keeping exclusive on every iteration. At the end, only the final key permits a fresh connection. There is no automatic union across iterations: each call declares the exclusive set for its own request. The correct play submits both keys together, separated by a newline, and both regain access. During a production rotation, confirm the authorized set and test the new key before removing the old one according to the approved window. Do not confuse the server’s public authorized-key file with the controller’s private key.
4. Observe the scope of revocation
The trainee password is locked, but both keys still authenticate under the exercised OpenSSH/PAM configuration. A password lock therefore does not establish complete account disablement. The trial opens a real session with key A, removes that key and attempts new connections without reusing an existing SSH connection. Key A is rejected, key B still works and the previously authenticated session still executes a command. The observed revocation concerns new authentication with that particular key. An incident may require identifying and ending sessions and processes, withdrawing other access methods or containing the host. Those additional actions need their own scope and authorization; this exercise does not present them as completed.
5. Close the change while retaining operational control
Repeating removal of key A produces no changes, and the separate automation access remains available on both nodes. The evidence distinguishes the intended effect from accidental loss of all administration. For a production service, the plan should identify affected accounts, the authorized recovery method, the server population and both positive and negative checks. After group changes, also consider already running processes, which may retain previous credentials; a current account-database read does not establish that every session has updated. This lab does not measure that group transition in existing processes. The handover summary should state exactly what changed, what was observed and which validations remain necessary.
# Original lab: content/labs/rhce-remote-accounts
# Run the complete disposable lab: python3 run.py --output evidence-local.json
# Requires images built with Dockerfile.node and Dockerfile.controller.
# The runner provisions keys and inventory; this play belongs to that fixture.
# Inside the prepared controller: ansible-playbook -i /lab/inventory.ini /lab/accounts.yml
- name: Converge disposable remote accounts and both authorized keys
hosts: managed
gather_facts: false
become: true
tasks:
- name: Establish required groups before referencing them
ansible.builtin.group:
name: '{{ item }}'
state: present
loop: [appops, legacyops, reporting]
- name: Declare primary and supplementary membership explicitly
ansible.builtin.user:
name: trainee
group: appops
groups: [legacyops]
append: false
shell: /bin/bash
create_home: true
password_lock: true
- name: Manage both keys as one exclusive set
ansible.posix.authorized_key:
user: trainee
key: "{{ lookup('ansible.builtin.file', '/lab/key_a.pub') ~ '\n' ~ lookup('ansible.builtin.file', '/lab/key_b.pub') }}"
exclusive: true
state: present
- name: Establish a handover directory with explicit permissions
ansible.builtin.file:
path: /srv/dr-handover
state: directory
owner: trainee
group: appops
mode: '0750'
- name: Install a validated narrowly scoped demonstration sudo rule
ansible.builtin.copy:
dest: /etc/sudoers.d/dr-trainee
content: "trainee ALL=(root) NOPASSWD: /usr/bin/id\n"
owner: root
group: root
mode: '0440'
validate: '/usr/sbin/visudo -cf %s'append: true retains legacyops; append: false removes it. After removing key A, a new connection fails while the previously opened session remains usable.
Common pitfalls
Confusing primary and supplementary groups; reading check-mode changed as an applied change; using exclusive per key in a loop; treating key removal as session termination.
Related topics: SSH, identity and validated escalation
Declare the set you manage and establish the observed effect of each grant or revocation.
Reference: User account and supplementary group management · EX294 current objectives inspected 2026-09-30; RHCE in Ansible framework effective 2026-05-11; booking product version not publicly pinned