This is an automated email from the ASF dual-hosted git repository.

lahirujayathilake pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/airavata-custos.git


The following commit(s) were added to refs/heads/master by this push:
     new a0d55a9ab add Ansible playbook for linux node enrollment with COmanage 
LDAP and CILogon device-flow SSH auth
a0d55a9ab is described below

commit a0d55a9ab3c539e1ec0c4654ee9a8a7bfbefe759
Author: lahiruj <[email protected]>
AuthorDate: Fri Mar 20 18:08:43 2026 -0400

    add Ansible playbook for linux node enrollment with COmanage LDAP and 
CILogon device-flow SSH auth
---
 deployment/account-provisioning/README.md          | 151 ++++++++++
 deployment/account-provisioning/enroll-node.yml    | 334 +++++++++++++++++++++
 .../account-provisioning/files/pam_oauth2_sshd.te  |  45 +++
 .../group_vars/all.yml.example                     |  90 ++++++
 .../inventory/hosts.example.yml                    |  49 +++
 .../templates/99-pam-oauth2-device.conf.j2         |  28 ++
 .../templates/pam-oauth2-config.json.j2            |  46 +++
 .../account-provisioning/templates/sssd.conf.j2    |  59 ++++
 deployment/account-provisioning/verify.yml         | 137 +++++++++
 9 files changed, 939 insertions(+)

diff --git a/deployment/account-provisioning/README.md 
b/deployment/account-provisioning/README.md
new file mode 100644
index 000000000..ecf580260
--- /dev/null
+++ b/deployment/account-provisioning/README.md
@@ -0,0 +1,151 @@
+<!--
+Licensed to the Apache Software Foundation (ASF) under one
+or more contributor license agreements. See the NOTICE file
+distributed with this work for additional information
+regarding copyright ownership. The ASF licenses this file
+to you under the Apache License, Version 2.0 (the
+"License"); you may not use this file except in compliance
+with the License. You may obtain a copy of the License at
+
+http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing,
+software distributed under the License is distributed on an
+"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+KIND, either express or implied. See the License for the
+specific language governing permissions and limitations
+under the License.
+-->
+
+# HPC Node Enrollment — Account Provisioning
+
+Ansible playbook to enroll RHEL 9/10 nodes for COmanage-provisioned LDAP user 
resolution and CILogon device-flow SSH authentication.
+
+## Architecture
+
+This setup uses two independent systems that each answer one question:
+
+| System | Question | Technology |
+|--------|----------|------------|
+| **SSSD/LDAP** | "Does this Unix account exist?" | SSSD → 
COmanage-provisioned LDAP (posixAccount) |
+| **OIDC** | "Is this the right human?" | pam_oauth2_device → CILogon device 
authorization flow |
+
+**Key design decisions:**
+
+- **Sub→uid mapping via LDAP, not PAM config.** The SSH username is always a 
Unix username. The PAM module maps the CILogon `sub` to a local `uid` via an 
LDAP lookup (`voPersonExternalID` → `uid`) to verify the authenticated identity 
matches the login user. No static mapping or PAM-level account resolution is 
needed.
+- **Authentication then authorization.** The device flow always starts. After 
the user authenticates via CILogon, the PAM module's LDAP lookup verifies the 
`sub` maps to the SSH login username. If the user does not exist in LDAP, this 
lookup fails and login is denied.
+- **Revocation via COmanage.** When COmanage sets a person inactive, the LDAP 
entry is removed, SSSD stops resolving the user, and SSH login fails 
automatically. No SSH config changes needed.
+
+```
+SSH login flow:
+  1. ssh jdoe@node
+  2. PAM auth → pam_oauth2_device → CILogon device flow
+  3. User completes browser auth
+  4. PAM module queries LDAP: voPersonExternalID=<sub> → uid
+  5. Returned uid matches "jdoe"? No → reject. Yes → continue.
+  6. SSSD resolves "jdoe" → LDAP (posixAccount lookup)
+  7. pam_mkhomedir creates /home/jdoe if first login
+```
+
+## Prerequisites
+
+1. **LDAP server** with COmanage-provisioned entries having both 
`posixAccount` objectClass and `voPersonExternalID` attribute (from [voPerson 
schema](https://refeds.org/specifications/voperson)). The PAM module maps 
CILogon subjects to local usernames via the LDAP filter 
`(&(objectClass=posixAccount)(voPersonExternalID=%s))`. See [LDAP Schema 
Requirements](#ldap-schema-requirements) for details on required schemas.
+2. **CILogon OIDC client** — must be a **confidential client** (with a client 
secret), not a public client.
+3. **Ansible** 2.14+ on your control node.
+4. **RHEL 9/10** target nodes with SSH and sudo access.
+5. Target nodes must be able to reach the LDAP server (port 636) and CILogon 
(port 443).
+
+### LDAP Schema Requirements
+
+If you're setting up a fresh LDAP server for COmanage, these schemas must be 
imported. COmanage's LDAP provisioning plugin writes entries using these 
objectClasses.
+
+| Schema | Type | Purpose |
+|--------|------|---------|
+| **voPerson** | AUXILIARY | COmanage person attributes. Critical: 
`voPersonExternalID` stores the CILogon subject URI used for OIDC sub→Unix uid 
mapping |
+| **eduMember** | AUXILIARY | Group membership (`isMemberOf`, `hasMember`) |
+| **posixAccount** | STRUCTURAL | Unix account attributes (`uidNumber`, 
`gidNumber`, `homeDirectory`, `loginShell`). Required for SSSD |
+| **posixGroup** | STRUCTURAL | Unix group attributes |
+| **inetOrgPerson** | STRUCTURAL | Base person object (`cn`, `sn`, `mail`, 
`uid`) |
+
+## Quick Start
+
+```bash
+cd deployment/account-provisioning
+
+# 1. Configure
+cp inventory/hosts.example.yml inventory/hosts.yml
+cp group_vars/all.yml.example group_vars/all.yml
+# Edit both files with your values
+
+# 2. Encrypt secrets
+ansible-vault encrypt_string 'your-ldap-password' --name 
'vault_ldap_bind_password' >> group_vars/all.yml
+ansible-vault encrypt_string 'your-cilogon-secret' --name 
'vault_cilogon_client_secret' >> group_vars/all.yml
+
+# 3. Test LDAP connectivity first
+ansible-playbook -i inventory/hosts.yml enroll-node.yml --tags prereqs 
--ask-vault-pass
+
+# 4. Full enrollment
+ansible-playbook -i inventory/hosts.yml enroll-node.yml --ask-vault-pass
+
+# 5. Verify
+ansible-playbook -i inventory/hosts.yml verify.yml --ask-vault-pass
+```
+
+All variables are documented with inline comments in 
`group_vars/all.yml.example`.
+
+## What the Playbook Does
+
+The playbook has 4 tagged phases that can be run independently with `--tags`:
+
+| Tag | Phase | Key detail |
+|-----|-------|------------|
+| `prereqs` | Read-only LDAP connectivity check | Fails fast if LDAP is 
unreachable. No files modified. |
+| `sssd` | SSSD + NSS + mkhomedir | Configures `auth_provider = none` — PAM 
handles authentication, SSSD only resolves identity. Sets SELinux boolean 
`authlogin_nsswitch_use_ldap`. |
+| `pam` | Build pam_oauth2_device, configure PAM stack, SELinux policy | 
Builds from source (cyber-shuttle fork). Installs SELinux policy allowing 
`sshd_t` to connect to CILogon on port 443 — without it, auth silently falls 
through to password-auth. |
+| `sshd` | Keyboard-interactive auth config | Drops 
`99-pam-oauth2-device.conf` into `sshd_config.d/` — the `99-` prefix ensures it 
overrides RHEL defaults. |
+
+## Verification
+
+```bash
+# End-to-end SSH tests (verify.yml checks services/config, these test actual 
login):
+ssh jdoe@<node>                # should show device code prompt
+ssh fakeuser@<node>            # device flow starts, but login fails after 
auth (LDAP lookup finds no match)
+
+# Check effective sshd config:
+sudo sshd -T | egrep -i 
'usepam|kbdinteractiveauthentication|passwordauthentication|challengeresponseauthentication'
+# Expected: all "yes"
+```
+
+## Troubleshooting
+
+### SSSD won't start
+- Check permissions: `ls -la /etc/sssd/sssd.conf` — must be `0600` owned by 
root
+- Check logs: `journalctl -u sssd --since "5 minutes ago"`
+- Clear cache and restart: `rm -rf /var/lib/sss/db/* && systemctl restart sssd`
+
+### `getent passwd <user>` returns nothing
+- Verify LDAP connectivity: re-run `--tags prereqs`
+- Enable SSSD debug: add `debug_level = 6` under `[domain/...]` in sssd.conf, 
restart, check `/var/log/sssd/sssd_<domain>.log`
+
+### SSH shows no device code prompt
+- Check effective config: `sudo sshd -T | grep kbdinteractive` — if it shows 
`no`, another file in `/etc/ssh/sshd_config.d/` is overriding it. The `99-` 
prefix on our config ensures it loads last; verify the file exists and restart 
sshd.
+
+### SELinux blocking CILogon requests
+The PAM module runs inside sshd and makes HTTPS calls to CILogon. SELinux's 
`sshd_t` context blocks this by default.
+
+**Symptoms:** `journalctl -u sshd` shows `curl failed HTTP POST` or 
`Authentication failure`, and `sealert -a /var/log/audit/audit.log` shows 
`SELinux is preventing /usr/sbin/sshd from name_connect access on the 
tcp_socket port 443`.
+
+**Fix:** Re-run `--tags pam` to reinstall the SELinux policy. For manual 
diagnosis:
+```bash
+sudo dnf install -y setroubleshoot-server
+sudo sealert -a /var/log/audit/audit.log
+```
+
+### CILogon auth fails with missing claims
+Your CILogon client is likely a **public** client. See 
[Prerequisites](#prerequisites) — a confidential client with a client secret is 
required.
+
+## Operational Notes
+
+- **LDAP outage:** SSSD caches credentials (`cache_credentials = true`), so 
previously-resolved users can still log in when LDAP is temporarily unreachable.
+- **PAM config:** The playbook replaces `/etc/pam.d/sshd` entirely. Back up 
the existing file before running on nodes with custom PAM configurations.
+- **pam_oauth2_device version:** The playbook builds from the latest commit of 
the configured git repo. Pin to a specific tag by adding `version:` to the git 
task in `enroll-node.yml` for production stability.
diff --git a/deployment/account-provisioning/enroll-node.yml 
b/deployment/account-provisioning/enroll-node.yml
new file mode 100644
index 000000000..5a2075645
--- /dev/null
+++ b/deployment/account-provisioning/enroll-node.yml
@@ -0,0 +1,334 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+---
+# =============================================================================
+# Account Provisioning — Node Enrollment Playbook
+# =============================================================================
+#
+# Enrolls Linux nodes for COmanage-provisioned LDAP user resolution (SSSD)
+# and CILogon device-flow SSH authentication (pam_oauth2_device).
+#
+# Tested on: RHEL/Rocky 9, RHEL/Rocky 10
+#
+# Usage:
+#   ansible-playbook -i inventory/hosts.yml enroll-node.yml                  # 
full run
+#   ansible-playbook -i inventory/hosts.yml enroll-node.yml --tags prereqs   # 
LDAP check only
+#   ansible-playbook -i inventory/hosts.yml enroll-node.yml --tags sssd      # 
SSSD only
+#   ansible-playbook -i inventory/hosts.yml enroll-node.yml --tags pam       # 
PAM module only
+#   ansible-playbook -i inventory/hosts.yml enroll-node.yml --tags sshd      # 
SSHD config only
+#
+# Requires: Ansible 2.14+, LDAP server with posixAccount entries
+
+- name: Enroll node for COmanage LDAP + CILogon SSH authentication
+  hosts: target_nodes
+  become: true
+  gather_facts: true
+
+  # =========================================================================
+  # prereqs — Verify LDAP connectivity (fail-fast, read-only)
+  # =========================================================================
+  tasks:
+    - name: "[prereqs] Install openldap-clients for LDAP checks"
+      dnf:
+        name: openldap-clients
+        state: present
+      tags: [prereqs]
+
+    - name: "[prereqs] Check LDAP TLS connectivity"
+      shell: >
+        echo "" | openssl s_client
+        -connect {{ ldap_uri | regex_replace('^ldaps://', '') }}:636
+        -servername {{ ldap_uri | regex_replace('^ldaps://', '') }}
+        2>/dev/null | openssl x509 -noout -subject -dates
+      register: ldap_tls_check
+      changed_when: false
+      failed_when: ldap_tls_check.rc != 0
+      tags: [prereqs]
+
+    - name: "[prereqs] TLS certificate info"
+      debug:
+        msg: "{{ ldap_tls_check.stdout_lines }}"
+      tags: [prereqs]
+
+    - name: "[prereqs] Verify LDAP bind credentials"
+      command: >
+        ldapwhoami -x
+        -H {{ ldap_uri }}:636
+        -D "{{ ldap_bind_dn }}"
+        -w "{{ ldap_bind_password }}"
+      register: ldap_bind_check
+      changed_when: false
+      failed_when: ldap_bind_check.rc != 0
+      no_log: true
+      tags: [prereqs]
+
+    - name: "[prereqs] Verify LDAP search base is reachable"
+      command: >
+        ldapsearch -LLL -x
+        -H {{ ldap_uri }}:636
+        -D "{{ ldap_bind_dn }}"
+        -w "{{ ldap_bind_password }}"
+        -b "{{ ldap_search_base }}"
+        -s base "(objectClass=*)" dn
+      register: ldap_base_check
+      changed_when: false
+      failed_when: ldap_base_check.rc != 0
+      no_log: true
+      tags: [prereqs]
+
+    - name: "[prereqs] LDAP connectivity verified"
+      debug:
+        msg: >-
+          LDAP server {{ ldap_uri }} is reachable, bind succeeded, search base 
exists.
+          Target OS: {{ ansible_distribution }} {{ 
ansible_distribution_version }}
+      tags: [prereqs]
+
+    # =========================================================================
+    # sssd — Install and configure SSSD for LDAP user resolution
+    # =========================================================================
+
+    - name: "[sssd] Install SSSD and LDAP client packages"
+      dnf:
+        name:
+          - sssd
+          - sssd-ldap
+          - oddjob
+          - oddjob-mkhomedir
+          - openldap-clients
+        state: present
+      tags: [sssd]
+
+    - name: "[sssd] Template /etc/sssd/sssd.conf"
+      template:
+        src: templates/sssd.conf.j2
+        dest: /etc/sssd/sssd.conf
+        owner: root
+        group: root
+        mode: "0600"
+      notify: restart sssd
+      tags: [sssd]
+
+    - name: "[sssd] Configure authselect for SSSD with mkhomedir"
+      command: authselect select sssd with-mkhomedir --force
+      register: authselect_result
+      changed_when: "'already' not in authselect_result.stdout | default('')"
+      tags: [sssd]
+
+    - name: "[sssd] authselect result"
+      debug:
+        msg: "{{ authselect_result.stdout_lines | default(['done']) }}"
+      tags: [sssd]
+
+    - name: "[sssd] Set SELinux boolean for LDAP NSS lookups"
+      seboolean:
+        name: authlogin_nsswitch_use_ldap
+        state: true
+        persistent: true
+      tags: [sssd]
+
+    - name: "[sssd] Enable and start oddjobd"
+      systemd:
+        name: oddjobd
+        enabled: true
+        state: started
+      tags: [sssd]
+
+    - name: "[sssd] Enable and start sssd"
+      systemd:
+        name: sssd
+        enabled: true
+        state: started
+      tags: [sssd]
+
+    - name: "[sssd] Flush SSSD cache to force fresh LDAP lookup"
+      command: sss_cache -E
+      changed_when: false
+      tags: [sssd]
+
+    - name: "[sssd] Validate — getent passwd {{ test_username }}"
+      command: "getent passwd {{ test_username }}"
+      register: sssd_validate
+      changed_when: false
+      failed_when: sssd_validate.rc != 0
+      tags: [sssd]
+
+    - name: "[sssd] Validation result"
+      debug:
+        msg: "{{ sssd_validate.stdout }}"
+      tags: [sssd]
+
+    # =========================================================================
+    # pam — Build and install pam_oauth2_device
+    # =========================================================================
+
+    - name: "[pam] Install build dependencies"
+      dnf:
+        name:
+          - "@Development Tools"
+          - pam-devel
+          - libcurl-devel
+          - openldap-devel
+          - pkgconfig
+          - git
+        state: present
+      tags: [pam]
+
+    - name: "[pam] Clone pam_oauth2_device repository"
+      git:
+        repo: "{{ pam_oauth2_device_repo }}"
+        dest: "{{ pam_oauth2_device_src_dir }}"
+        force: true
+      tags: [pam]
+
+    - name: "[pam] Build pam_oauth2_device"
+      make:
+        chdir: "{{ pam_oauth2_device_src_dir }}"
+      tags: [pam]
+
+    - name: "[pam] Install pam_oauth2_device.so"
+      copy:
+        src: "{{ pam_oauth2_device_src_dir }}/pam_oauth2_device.so"
+        dest: /usr/lib64/security/pam_oauth2_device.so
+        remote_src: true
+        owner: root
+        group: root
+        mode: "0755"
+      tags: [pam]
+
+    - name: "[pam] Create /etc/pam_oauth2_device directory"
+      file:
+        path: /etc/pam_oauth2_device
+        state: directory
+        owner: root
+        group: root
+        mode: "0755"
+      tags: [pam]
+
+    - name: "[pam] Template pam_oauth2_device config.json"
+      template:
+        src: templates/pam-oauth2-config.json.j2
+        dest: /etc/pam_oauth2_device/config.json
+        owner: root
+        group: root
+        mode: "0600"
+      tags: [pam]
+
+    - name: "[pam] Configure PAM stack for sshd"
+      copy:
+        dest: /etc/pam.d/sshd
+        owner: root
+        group: root
+        mode: "0644"
+        content: |
+          #%PAM-1.0
+          auth       required     pam_sepermit.so
+          auth       sufficient   pam_oauth2_device.so 
/etc/pam_oauth2_device/config.json
+          auth       substack     password-auth
+          auth       include      postlogin
+          account    required     pam_sepermit.so
+          account    required     pam_nologin.so
+          account    include      password-auth
+          password   include      password-auth
+          # pam_selinux.so close should be the first session rule
+          session    required     pam_selinux.so close
+          session    required     pam_loginuid.so
+          # pam_selinux.so open should only be followed by sessions to be 
executed in the user context
+          session    required     pam_selinux.so open env_params
+          session    required     pam_namespace.so
+          session    optional     pam_keyinit.so force revoke
+          session    optional     pam_motd.so
+          session    include      password-auth
+          session    include      postlogin
+      notify: restart sshd
+      tags: [pam]
+
+    - name: "[pam] Install SELinux policy development tools"
+      dnf:
+        name:
+          - checkpolicy
+          - policycoreutils
+        state: present
+      tags: [pam]
+
+    - name: "[pam] Copy SELinux type enforcement policy for sshd HTTPS"
+      copy:
+        src: files/pam_oauth2_sshd.te
+        dest: /tmp/pam_oauth2_sshd.te
+        mode: "0644"
+      tags: [pam]
+
+    - name: "[pam] Check if pam_oauth2_sshd SELinux module is already loaded"
+      shell: semodule -l | grep -q pam_oauth2_sshd
+      register: selinux_module_check
+      changed_when: false
+      failed_when: false
+      tags: [pam]
+
+    - name: "[pam] Compile and install SELinux policy for sshd HTTPS (CILogon)"
+      shell: |
+        checkmodule -M -m -o /tmp/pam_oauth2_sshd.mod /tmp/pam_oauth2_sshd.te
+        semodule_package -o /tmp/pam_oauth2_sshd.pp -m /tmp/pam_oauth2_sshd.mod
+        semodule -i /tmp/pam_oauth2_sshd.pp
+      when: selinux_module_check.rc != 0
+      tags: [pam]
+
+    # =========================================================================
+    # sshd — Configure SSHD for keyboard-interactive auth
+    # =========================================================================
+
+    - name: "[sshd] Template sshd_config.d override"
+      template:
+        src: templates/99-pam-oauth2-device.conf.j2
+        dest: /etc/ssh/sshd_config.d/99-pam-oauth2-device.conf
+        owner: root
+        group: root
+        mode: "0600"
+      notify: restart sshd
+      tags: [sshd]
+
+    - name: "[sshd] Validate sshd configuration"
+      command: /usr/sbin/sshd -t
+      changed_when: false
+      tags: [sshd]
+
+    - name: "[sshd] Verify effective sshd settings"
+      shell: >
+        sshd -T | grep -iE
+        
'usepam|kbdinteractiveauthentication|passwordauthentication|challengeresponseauthentication'
+      register: sshd_effective
+      changed_when: false
+      tags: [sshd]
+
+    - name: "[sshd] Effective sshd configuration"
+      debug:
+        msg: "{{ sshd_effective.stdout_lines }}"
+      tags: [sshd]
+
+  # ===========================================================================
+  # Handlers
+  # ===========================================================================
+  handlers:
+    - name: restart sssd
+      systemd:
+        name: sssd
+        state: restarted
+
+    - name: restart sshd
+      systemd:
+        name: sshd
+        state: restarted
diff --git a/deployment/account-provisioning/files/pam_oauth2_sshd.te 
b/deployment/account-provisioning/files/pam_oauth2_sshd.te
new file mode 100644
index 000000000..706b1b91a
--- /dev/null
+++ b/deployment/account-provisioning/files/pam_oauth2_sshd.te
@@ -0,0 +1,45 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+# SELinux type enforcement policy: allow sshd to make HTTPS connections
+#
+# Problem: pam_oauth2_device runs inside sshd (sshd_t context) and uses
+# libcurl to contact CILogon over HTTPS (port 443). SELinux's default
+# policy for sshd_t does not permit outbound TCP connections to http_port_t.
+# Without this module, auth silently falls through to password-auth.
+#
+# Discovered from AVC denial:
+#   avc: denied { name_connect } for comm="sshd" dest=443
+#   scontext=system_u:system_r:sshd_t:s0
+#   tcontext=system_u:object_r:http_port_t:s0
+#   tclass=tcp_socket
+#
+# Build:
+#   checkmodule -M -m -o pam_oauth2_sshd.mod pam_oauth2_sshd.te
+#   semodule_package -o pam_oauth2_sshd.pp -m pam_oauth2_sshd.mod
+#   semodule -i pam_oauth2_sshd.pp
+
+module pam_oauth2_sshd 1.0;
+
+require {
+    type sshd_t;
+    type http_port_t;
+    class tcp_socket name_connect;
+}
+
+#============= sshd_t ==============
+allow sshd_t http_port_t:tcp_socket name_connect;
diff --git a/deployment/account-provisioning/group_vars/all.yml.example 
b/deployment/account-provisioning/group_vars/all.yml.example
new file mode 100644
index 000000000..9618de85a
--- /dev/null
+++ b/deployment/account-provisioning/group_vars/all.yml.example
@@ -0,0 +1,90 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+---
+# =============================================================================
+# Account Provisioning — Configuration Variables
+# =============================================================================
+# Copy this file to group_vars/all.yml and fill in your values.
+# Secrets should use Ansible Vault: ansible-vault encrypt_string 'secret'
+
+# -----------------------------------------------------------------------------
+# LDAP — COmanage-provisioned LDAP server
+# -----------------------------------------------------------------------------
+
+# LDAPS URI (must be ldaps:// for TLS)
+ldap_uri: "ldaps://xxx.xxx"
+
+# Base DN where posixAccount entries live
+ldap_search_base: "ou=people,o=xxx,o=CO,dc=xxx,dc=xxx,dc=xxx"
+
+# Bind DN for SSSD to authenticate to LDAP
+ldap_bind_dn: "uid=registry_user,ou=system,o=xxx,o=CO,dc=xxx,dc=xxx,dc=xxx"
+
+# Bind password — use ansible-vault to encrypt this value
+# Example: ansible-vault encrypt_string 'your-password' --name 
'ldap_bind_password'
+ldap_bind_password: "{{ vault_ldap_bind_password }}"
+
+# TLS CA certificate bundle on the target node
+ldap_tls_cacert: "/etc/pki/tls/certs/ca-bundle.crt"
+
+# -----------------------------------------------------------------------------
+# CILogon OAuth2 — Device authorization flow
+# -----------------------------------------------------------------------------
+
+# OIDC client credentials (must be a confidential client, not public)
+cilogon_client_id: "cilogon:/client_id/..."
+cilogon_client_secret: "{{ vault_cilogon_client_secret }}"
+
+# OAuth2 scopes — must include userinfo for username_attribute lookup
+cilogon_scope: "openid profile email org.cilogon.userinfo"
+
+# -----------------------------------------------------------------------------
+# SSSD
+# -----------------------------------------------------------------------------
+
+# Domain name used in sssd.conf [domain/<name>]
+sssd_domain_name: "xxx"
+
+# -----------------------------------------------------------------------------
+# pam_oauth2_device
+# -----------------------------------------------------------------------------
+
+# Git repo to clone for building pam_oauth2_device
+pam_oauth2_device_repo: 
"https://github.com/cyber-shuttle/pam_oauth2_device.git";
+
+# Where to clone the source on the target
+pam_oauth2_device_src_dir: "/usr/local/src/pam_oauth2_device"
+
+# LDAP settings for pam_oauth2_device user lookup
+# This lets the PAM module verify the SSH username exists in LDAP
+pam_oauth2_ldap_host: "{{ ldap_uri }}:636"
+pam_oauth2_ldap_basedn: "{{ ldap_search_base }}"
+pam_oauth2_ldap_user: "{{ ldap_bind_dn }}"
+pam_oauth2_ldap_passwd: "{{ ldap_bind_password }}"
+pam_oauth2_ldap_filter: "(&(objectClass=posixAccount)(voPersonExternalID=%s))"
+pam_oauth2_ldap_attr: "uid"
+
+# Enable debug logging in pam_oauth2_device (set false for production)
+pam_oauth2_debug: false
+
+# -----------------------------------------------------------------------------
+# Validation
+# -----------------------------------------------------------------------------
+
+# A known LDAP username to verify SSSD resolution works
+test_username: "{{ test_username }}"
diff --git a/deployment/account-provisioning/inventory/hosts.example.yml 
b/deployment/account-provisioning/inventory/hosts.example.yml
new file mode 100644
index 000000000..45328c075
--- /dev/null
+++ b/deployment/account-provisioning/inventory/hosts.example.yml
@@ -0,0 +1,49 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+---
+# =============================================================================
+# Account Provisioning — Inventory
+# =============================================================================
+# Copy to inventory/hosts.yml and update with your target nodes.
+#
+# Usage:
+#   ansible-playbook -i inventory/hosts.yml playbook.yml
+
+all:
+  children:
+    target_nodes:
+      hosts:
+        # Single node example
+        login-node-1:
+          ansible_host: 10.0.1.50
+          ansible_user: user
+          # ansible_ssh_private_key_file: ~/.ssh/my-key.pem
+
+        # Additional nodes — uncomment to add
+        # login-node-2:
+        #   ansible_host: 10.0.1.51
+        #   ansible_user: user
+
+        # login-node-3:
+        #   ansible_host: 10.0.2.10
+        #   ansible_user: user
+
+      vars:
+        # All target_nodes share these defaults (can be overridden per host)
+        ansible_become: true
+        ansible_become_method: sudo
diff --git 
a/deployment/account-provisioning/templates/99-pam-oauth2-device.conf.j2 
b/deployment/account-provisioning/templates/99-pam-oauth2-device.conf.j2
new file mode 100644
index 000000000..f3baae21d
--- /dev/null
+++ b/deployment/account-provisioning/templates/99-pam-oauth2-device.conf.j2
@@ -0,0 +1,28 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+# Managed by Ansible — do not edit manually
+# RHEL9 sshd_config.d override for pam_oauth2_device (CILogon device flow)
+#
+# RHEL9 ships default configs in /etc/ssh/sshd_config.d/ that may disable
+# KbdInteractiveAuthentication. This file loads last (99-) to override them.
+
+Match all
+    UsePAM yes
+    ChallengeResponseAuthentication yes
+    KbdInteractiveAuthentication yes
+    PasswordAuthentication yes
diff --git 
a/deployment/account-provisioning/templates/pam-oauth2-config.json.j2 
b/deployment/account-provisioning/templates/pam-oauth2-config.json.j2
new file mode 100644
index 000000000..8ff3a0dad
--- /dev/null
+++ b/deployment/account-provisioning/templates/pam-oauth2-config.json.j2
@@ -0,0 +1,46 @@
+{# Licensed to the Apache Software Foundation (ASF) under one
+   or more contributor license agreements. See the NOTICE file
+   distributed with this work for additional information
+   regarding copyright ownership. The ASF licenses this file
+   to you under the Apache License, Version 2.0 (the
+   "License"); you may not use this file except in compliance
+   with the License. You may obtain a copy of the License at
+
+   http://www.apache.org/licenses/LICENSE-2.0
+
+   Unless required by applicable law or agreed to in writing,
+   software distributed under the License is distributed on an
+   "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+   KIND, either express or implied. See the License for the
+   specific language governing permissions and limitations
+   under the License. #}
+{
+{% if pam_oauth2_debug | default(false) %}
+  "client_debug": true,
+{% endif %}
+  "oauth": {
+    "client": {
+      "id": "{{ cilogon_client_id }}",
+      "secret": "{{ cilogon_client_secret }}"
+    },
+    "device_authorization_endpoint": 
"https://cilogon.org/oauth2/device_authorization";,
+    "device_endpoint":               
"https://cilogon.org/oauth2/device_authorization";,
+    "token_endpoint":                "https://cilogon.org/oauth2/token";,
+    "userinfo_endpoint":             "https://cilogon.org/oauth2/userinfo";,
+    "verification_uri":              "https://cilogon.org/device/";,
+    "scope":                         "{{ cilogon_scope }}",
+    "require_mfa":                   false,
+    "username_attribute": "sub",
+    "local_username_suffix": ""
+  },
+  "ldap": {
+    "host": "{{ pam_oauth2_ldap_host }}",
+    "basedn": "{{ pam_oauth2_ldap_basedn }}",
+    "user": "{{ pam_oauth2_ldap_user }}",
+    "passwd": "{{ pam_oauth2_ldap_passwd }}",
+    "filter": "{{ pam_oauth2_ldap_filter }}",
+    "attr": "{{ pam_oauth2_ldap_attr }}"
+  },
+  "qr": { "show": false, "error_correction_level": 1 },
+  "tls": { "ca_bundle": "{{ ldap_tls_cacert }}" }
+}
diff --git a/deployment/account-provisioning/templates/sssd.conf.j2 
b/deployment/account-provisioning/templates/sssd.conf.j2
new file mode 100644
index 000000000..234bf2810
--- /dev/null
+++ b/deployment/account-provisioning/templates/sssd.conf.j2
@@ -0,0 +1,59 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+# Managed by Ansible — do not edit manually
+# SSSD configuration for COmanage-provisioned LDAP user resolution
+
+[sssd]
+services = nss, pam
+domains = {{ sssd_domain_name }}
+config_file_version = 2
+
+[nss]
+filter_groups = root
+filter_users = root
+reconnection_retries = 3
+
+[pam]
+
+[domain/{{ sssd_domain_name }}]
+id_provider = ldap
+auth_provider = none
+access_provider = permit
+
+ldap_uri = {{ ldap_uri }}
+ldap_search_base = {{ ldap_search_base }}
+
+ldap_default_bind_dn = {{ ldap_bind_dn }}
+ldap_default_authtok_type = password
+ldap_default_authtok = {{ ldap_bind_password }}
+
+ldap_schema = rfc2307
+ldap_user_object_class = posixAccount
+ldap_group_object_class = posixGroup
+
+ldap_user_name = uid
+ldap_user_uid_number = uidNumber
+ldap_user_gid_number = gidNumber
+ldap_user_home_directory = homeDirectory
+ldap_user_shell = loginShell
+
+ldap_tls_reqcert = demand
+ldap_tls_cacert = {{ ldap_tls_cacert }}
+
+cache_credentials = true
+enumerate = false
diff --git a/deployment/account-provisioning/verify.yml 
b/deployment/account-provisioning/verify.yml
new file mode 100644
index 000000000..f6a71cb03
--- /dev/null
+++ b/deployment/account-provisioning/verify.yml
@@ -0,0 +1,137 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+---
+# =============================================================================
+# Account Provisioning — Verification Playbook
+# =============================================================================
+#
+# Safe to run at any time. Makes no changes to the system.
+#
+# Usage:
+#   ansible-playbook -i inventory/hosts.yml verify.yml
+
+- name: Verify node enrollment
+  hosts: target_nodes
+  become: true
+  gather_facts: false
+
+  tasks:
+    - name: "[verify] getent passwd {{ test_username }}"
+      command: "getent passwd {{ test_username }}"
+      register: verify_getent
+      changed_when: false
+      failed_when: false
+
+    - name: "[verify] getent result"
+      debug:
+        msg: "{{ verify_getent.stdout | default('FAILED — user not found') }}"
+
+    - name: "[verify] id {{ test_username }}"
+      command: "id {{ test_username }}"
+      register: verify_id
+      changed_when: false
+      failed_when: false
+
+    - name: "[verify] id result"
+      debug:
+        msg: "{{ verify_id.stdout | default('FAILED — user not found') }}"
+
+    - name: "[verify] SSSD service status"
+      command: systemctl is-active sssd
+      register: verify_sssd
+      changed_when: false
+      failed_when: false
+
+    - name: "[verify] SSSD status"
+      debug:
+        msg: "sssd is {{ verify_sssd.stdout | default('unknown') }}"
+
+    - name: "[verify] oddjobd service status"
+      command: systemctl is-active oddjobd
+      register: verify_oddjobd
+      changed_when: false
+      failed_when: false
+
+    - name: "[verify] oddjobd status"
+      debug:
+        msg: "oddjobd is {{ verify_oddjobd.stdout | default('unknown') }}"
+
+    - name: "[verify] sshd service status"
+      command: systemctl is-active sshd
+      register: verify_sshd
+      changed_when: false
+      failed_when: false
+
+    - name: "[verify] sshd status"
+      debug:
+        msg: "sshd is {{ verify_sshd.stdout | default('unknown') }}"
+
+    - name: "[verify] Effective sshd auth settings"
+      shell: >
+        sshd -T | grep -iE
+        
'usepam|kbdinteractiveauthentication|passwordauthentication|challengeresponseauthentication|authenticationmethods'
+      register: verify_sshd_config
+      changed_when: false
+      failed_when: false
+
+    - name: "[verify] sshd effective config"
+      debug:
+        msg: "{{ verify_sshd_config.stdout_lines | default(['unable to read']) 
}}"
+
+    - name: "[verify] pam_oauth2_device.so installed"
+      stat:
+        path: /usr/lib64/security/pam_oauth2_device.so
+      register: verify_pam_so
+
+    - name: "[verify] pam_oauth2_device.so"
+      debug:
+        msg: "{{ 'INSTALLED' if verify_pam_so.stat.exists else 'MISSING' }}"
+
+    - name: "[verify] pam_oauth2_device config.json exists"
+      stat:
+        path: /etc/pam_oauth2_device/config.json
+      register: verify_pam_config
+
+    - name: "[verify] config.json"
+      debug:
+        msg: "{{ 'EXISTS' if verify_pam_config.stat.exists else 'MISSING' }}"
+
+    - name: "[verify] Recent SSSD logs"
+      shell: 'journalctl -u sssd --since "5 minutes ago" --no-pager -l 
2>/dev/null | tail -20'
+      register: verify_sssd_logs
+      changed_when: false
+      failed_when: false
+
+    - name: "[verify] SSSD journal (last 5 min)"
+      debug:
+        msg: "{{ verify_sssd_logs.stdout_lines | default(['no recent logs']) 
}}"
+
+    # =========================================================================
+    # Summary
+    # =========================================================================
+    - name: "[verify] Enrollment summary"
+      debug:
+        msg:
+          - "=== Enrollment Verification Summary ==="
+          - "getent passwd {{ test_username }}: {{ 'PASS' if verify_getent.rc 
== 0 else 'FAIL' }}"
+          - "id {{ test_username }}:             {{ 'PASS' if verify_id.rc == 
0 else 'FAIL' }}"
+          - "sssd service:                       {{ verify_sssd.stdout | 
default('UNKNOWN') }}"
+          - "oddjobd service:                    {{ verify_oddjobd.stdout | 
default('UNKNOWN') }}"
+          - "sshd service:                       {{ verify_sshd.stdout | 
default('UNKNOWN') }}"
+          - "pam_oauth2_device.so:               {{ 'INSTALLED' if 
verify_pam_so.stat.exists else 'MISSING' }}"
+          - "pam_oauth2_device config.json:      {{ 'EXISTS' if 
verify_pam_config.stat.exists else 'MISSING' }}"


Reply via email to