GuidesUpdated July 3, 2026
ODB SSH Access for Test and Administrative Users
guideodbsshaccess-controlawxansibleautomationactive-directorysecuritykey-management
ODB SSH Access Method for Test and Administrative OS Users
Overview
- SSH task code is in the
tasks/sshsubdirectory of theutilitiesrepo. - SSH playbook is located down the path
playbooks/epic-on-azure/pb_odb_ssh.ymlof theplaybooksrepo. - The SSH playbook is enabled within AWX as the
ODB SSH v3Job Template, which is further enabled by a set of 3 schedules - one each for CloudTest (non-functional due to environmental differences), non-prod, and prod. These run in the middle of the evening to hopefully reduce potential operational issues.
1. SSH Operational Documentation
The SSH automation pipeline:
- Discovers the authoritative user population from AD groups.
- The ODB inventory in AWX is where the values for
odb_grp_ssh_usersare currently sourced from, as of 9/24/2025.
- The ODB inventory in AWX is where the values for
- Mounts a secured network key share.
- The code and config does not mention this, but a user must have
epic_nas_npfor non-prod access orepic_nasaccess for prod access, as of 9/24/2025. The lack of these will typically manifest as an"unable to open file or directory"error visible in the user's mPuTTY window.
- The code and config does not mention this, but a user must have
- Ensures each discovered user has a key directory (and if absent, generates an ed25519 SSH keypair plus a PuTTY
.ppkfile). - Ensures each user’s public key is present in their
~/.ssh/authorized_keys. - Cleans up by unmounting the share.
- Removes
authorized_keysfiles for users who are no longer valid (deprovisioning enforcement).- NOTE - users in the var
odb_ssh_test_usersare explicitly ignored when removing users, which is good for maintaining admin/OS CLI users outside of the other typical AD group pattern (i.e. when using the issue-tracker method to add an SSH key for a user to login), however this will present an issue when deprovisioning, as this list will need to be regularly maintained to reflect the current state of users allowed exempt from the typical process. If wanting to edit that var, it is also present in the ODB inventory in AWX, and the code to perform the delete is here.
- NOTE - users in the var
The workflow expresses a closed-loop key lifecycle governed purely by AD group membership and a centralized storage share.
2. Deep Dive
2.1 Orchestration
Responsibilities:
- Drives the overall workflow; includes all other SSH task files.
- Mounts and unmounts the network share used as the canonical key repository.
- Prunes obsolete user authorized keys.
Notable Logic:
- Key generation is gated: a user directory’s absence triggers generation; presence implies reuse (idempotency).
- Removal of
authorized_keysis conservative: it deletes the entire file if the username is not invalid_users + odb_ssh_test_users. - Uses
find /home -maxdepth 3 -path '/home/*/.ssh/authorized_keys'rather than the Ansiblefindmodule for precise pattern matching.
2.2 User Discovery
Purpose: Resolve AD group membership into a canonical, unique, sorted list of OS usernames (valid_users).
Operational Flow:
- Ensure
python-ldaplibrary is installed (dependency for LDAP queries). - Include external DC discovery tasks (
linux/ad_get_dcs.yml). - Pick a random domain controller (
ad_site_dcs | random) to distribute load. - For each configured group (
odb_ssh_user_groups), search AD for group object and extract thememberattribute (DN list). - Flatten + dedupe collected DNs into
all_member_dns. - For each DN, perform a base-scope lookup to retrieve
sAMAccountName. - Aggregate, dedupe, sort, and register
valid_users.
2.3 Key Material Creation
Purpose: For users missing a directory on the network share, generate and place new SSH credentials:
Artifacts generated (per user):
id_ed25519(private key)id_ed25519.pub(public key)privatekey.ppk(PuTTY format)
Key Steps:
- Create a secure temporary directory (survives check mode).
- Generate ed25519 keypair (
community.crypto.openssh_keypair). - Convert private key to PPK via
puttygen(guarded bycreates:for idempotency). - Ensure per-user directory exists on share (
0700). - Copy key files with restrictive permissions (
0600). - Remove the temporary directory (sanitization).
Notes:
- Generation only occurs if a directory for the user is missing on the share.
ansible_check_modeguarded copy prevents deploying fake/dry-run artifacts.
2.4 Authorized Key Enforcement
Purpose: Ensure each valid user’s ~/.ssh/authorized_keys contains the centrally managed public key.
Key Steps:
- Ensure
/home/<user>exists with correct owner/mode (0700). - Retrieve the user’s public key from the mounted share with
slurp. - Install (append if necessary) the key using
ansible.posix.authorized_key(non-exclusive mode). - Skip key installation when the share lacks the file (e.g., user’s key has not yet been created—shouldn’t normally happen if orchestration ordering is intact).
Considerations:
- Does not create parent directories above
/home/<user>; expects standard home provisioning. exclusive: falseallows coexisting manually managed keys (may be a policy decision—could be changed to enforce stricter control).
3. End-to-End Flow (Sequence)
flowchart TD
A[Start Play] --> B[Include get_deduped_users]
B --> C[ldap_search groups]
C --> D[Resolve DNs -> sAMAccountNames]
D --> E[valid_users fact]
E --> F[Mount CIFS share]
F --> G[Find existing share user dirs]
G --> H{User dir exists?}
H -- No --> I[Include create_missing_keypairs for user]
H -- Yes --> J[Skip generation for user]
I --> K[Repeat for remaining users]
J --> K[All users evaluated]
K --> L[Include ensure_pubkey_exists per user]
L --> M[Unmount share]
M --> N[Find authorized_keys under /home]
N --> O{User still valid?}
O -- No --> P[Remove authorized_keys]
O -- Yes --> Q[Keep file]
P --> R[End]
Q --> R[End]
4. Variables & Facts Reference
4.1 Core Input Variables
| Variable | Description | Required |
|---|---|---|
odb_ssh_user_groups | List of AD group sAMAccountNames to expand. | Yes |
ssh_ad_base_dn | Base DN suffix for AD group search path. | Yes |
ad_bind_dn_suffix | LDAP bind DN suffix appended to service account CN. | Yes |
odb_ssh_mounted_key_share.username | Service account username (CN portion). | Yes |
odb_ssh_mounted_key_share.password | Service account password (vault). | Yes |
odb_ssh_mounted_key_share.unc | CIFS UNC path to key share. | Yes |
odb_ssh_mounted_key_share.mount_point | Local mount path on target node(s). | Yes |
pypi_index | PyPI/simple index for python-ldap installation. | Yes |
ssh_user_group | Primary group for user home directory ownership. | Yes |
odb_ssh_test_users | Users exempted from deletion logic (e.g., test accounts). | Optional |
4.2 Derived Facts
| Fact | Origin | Description |
|---|---|---|
valid_users | get_deduped_users | Final sorted canonical user list. |
all_member_dns | get_deduped_users | Intermediate list of unique user DNs. |
share_user_dirs | main | Directories currently present on key share (scan result). |
home_user_dirs | main | Paths to discovered authorized_keys files in /home. |
tempdir | create_missing_keypairs | Temporary directory for ephemeral key generation (per loop). |
5. Operational Runbook Guidance
| Scenario | Action |
|---|---|
| Add new AD group to scope | Append to odb_ssh_user_groups; re-run play. |
| Force key rotation | Remove user’s directory from share; next run regenerates. Optionally add rotation task that archives old keys. |
| Migrate algorithm (e.g., to RSA) | Parameterize key type (ssh_key_type) and adjust openssh_keypair arguments. |
| Validate share health before runtime | Pre-task: check mount reachability with stat or wait_for. |
| Large group performance issues | Batch or consolidate DN lookups with compound filter; optionally cache valid_users. |
| Debug missing user | Query AD manually; confirm group membership and base DN path. |
Escalations: If encountering an issue with the execution of this playbook or one of its effects, the best bet is to attend the EoA Office Hours call to seek help.
6. Example Ansible Role Invocation (Conceptual)
- name: Apply SSH key lifecycle enforcement
hosts: linux_ssh_managed
become: true
vars:
odb_ssh_user_groups:
- UNIX_SSH_ENG
- UNIX_SSH_PROD
ssh_ad_base_dn: "DC=corp,DC=example,DC=com"
ad_bind_dn_suffix: ",OU=Service Accounts,DC=corp,DC=example,DC=com"
odb_ssh_mounted_key_share:
unc: "//fileshare.example.com/sshkeys"
mount_point: "/mnt/sshkeys"
username: "svc_ssh_bind"
password: "{{ vault_ssh_bind_password }}"
ssh_user_group: "users"
odb_ssh_test_users: ["labuser1"]
roles:
- ohemr-ansible-role-misc-utilities
7. Quick Reference: Task-to-Outcome Mapping
| Desired Outcome | Tasks Involved |
|---|---|
| New user automatically gains SSH access | AD group membership → get_deduped_users.yml → create_missing_keypairs.yml → ensure_pubkey_exists.yml |
| User removed loses access | AD membership removal → valid_users shrinks → main.yml cleanup path removes authorized_keys |
| Key rotation (manual trigger) | Delete user’s share directory → rerun play |
| Audit current valid set | Inspect valid_users fact via callback / debug task |