Files
homelab-infra/infrastructure/proxmox/ansible/roles/pve_auth/tasks/main.yml
T
panxiao81 88a02ababa
lint / yaml (push) Has been cancelled
lint / ansible (push) Has been cancelled
lint / terraform (push) Has been cancelled
Establish clean homelab infrastructure baseline
Reorganize the brownfield repository, remove retired and generated artifacts, harden ignore rules, and record the GitOps/IaC redesign.
2026-09-09 16:47:20 +00:00

202 lines
7.9 KiB
YAML

---
# Realm config lives in /etc/pve/domains.cfg, which is REPLICATED cluster-wide,
# so every task here runs once against a single node. Doing it per-host would
# just have three nodes racing to write the same file.
- name: Require the bind password
ansible.builtin.assert:
that: pve_auth_bind_password | length > 0
fail_msg: >-
pve_auth_bind_password is empty. It comes from vault_pve_bind_password in
samba-ad/ansible/group_vars/all/vault.yml — pass it via -e or a vars file.
quiet: true
run_once: true
- name: Check whether the realm already exists
ansible.builtin.command:
cmd: "pveum realm list --output-format json"
register: _realms
changed_when: false
check_mode: false
run_once: true
- name: Decide create vs update
ansible.builtin.set_fact:
_realm_exists: "{{ pve_auth_realm in (_realms.stdout | from_json | map(attribute='realm') | list) }}"
run_once: true
# --- shared option set, so create and update cannot drift apart -------------
# `--type` is deliberately NOT in here: a realm's type is immutable, and
# `pveum realm modify --type ad` fails with "Unknown option: type". It is passed
# only on the create path below.
- name: Build the realm option string
ansible.builtin.set_fact:
_realm_opts: >-
--domain {{ pve_auth_domain }}
--server1 {{ pve_auth_server1 }}
--port {{ pve_auth_port }}
--mode {{ pve_auth_mode }}
--verify {{ pve_auth_verify }}
--capath {{ pve_auth_capath }}
--base_dn '{{ pve_auth_base_dn }}'
--bind_dn '{{ pve_auth_bind_dn }}'
--user_attr {{ pve_auth_user_attr }}
--user_classes '{{ pve_auth_user_classes }}'
--group_dn '{{ pve_auth_group_dn }}'
--group_classes '{{ pve_auth_group_classes }}'
--filter '{{ pve_auth_filter }}'
--group_filter '{{ pve_auth_group_filter }}'
--sync_attributes '{{ pve_auth_sync_attributes }}'
--sync-defaults-options '{{ pve_auth_sync_defaults }}'
--case-sensitive {{ pve_auth_case_sensitive }}
--comment 'Samba AD (dc1) over verified LDAPS'
run_once: true
- name: Create the realm
# --check-connection makes PVE actually bind before saving, so a wrong DN,
# password, or an untrusted certificate fails HERE instead of silently
# producing a realm nobody can log in to.
ansible.builtin.shell:
cmd: >-
pveum realm add {{ pve_auth_realm }} --type ad {{ _realm_opts }}
--password '{{ pve_auth_bind_password }}'
--check-connection 1
when: not _realm_exists
run_once: true
# no_log hides the bind password, but it also hides WHY a failure happened.
# Keep it on (the password is on the command line) and rely on
# --check-connection plus the manual `pveum realm modify` path for diagnosis.
no_log: true
- name: Read the current realm config
ansible.builtin.command:
cmd: "pvesh get /access/domains/{{ pve_auth_realm }} --output-format json"
register: _realm_cur
changed_when: false
check_mode: false
when: _realm_exists
run_once: true
- name: Detect realm drift
# pveum has no diff mode, so compare the fields we manage. Without this the
# role reported "changed" on every single run, which makes real drift invisible.
ansible.builtin.set_fact:
_realm_drift: "{{ _realm_exists and (
(_realm_cur.stdout | from_json).get('server1') != pve_auth_server1 or
(_realm_cur.stdout | from_json).get('base_dn') != pve_auth_base_dn or
(_realm_cur.stdout | from_json).get('bind_dn') != pve_auth_bind_dn or
(_realm_cur.stdout | from_json).get('mode') != pve_auth_mode or
(_realm_cur.stdout | from_json).get('verify') | default(0) | int != pve_auth_verify | int or
(_realm_cur.stdout | from_json).get('filter') != pve_auth_filter | trim or
(_realm_cur.stdout | from_json).get('group_filter') != pve_auth_group_filter or
(_realm_cur.stdout | from_json).get('user_attr') != pve_auth_user_attr or
(_realm_cur.stdout | from_json).get('sync_attributes') != pve_auth_sync_attributes
) }}"
run_once: true
- name: Update the realm
ansible.builtin.shell:
cmd: >-
pveum realm modify {{ pve_auth_realm }} {{ _realm_opts }}
--password '{{ pve_auth_bind_password }}'
--check-connection 1
when: _realm_drift | default(false)
run_once: true
no_log: true
- name: Show the resulting realm
ansible.builtin.command:
cmd: "pveum realm list --output-format json"
register: _realm_after
changed_when: false
run_once: true
- name: Report it
ansible.builtin.debug:
msg: "{{ _realm_after.stdout | from_json | selectattr('realm', 'equalto', pve_auth_realm) | list }}"
run_once: true
# --- initial sync ----------------------------------------------------------
- name: Dry-run the sync first
# Proves the bind and filters work, and shows what WOULD be imported, without
# writing to user.cfg.
ansible.builtin.command:
cmd: "pveum realm sync {{ pve_auth_realm }} --scope both --dry-run 1"
register: _sync_dry
changed_when: false
run_once: true
- name: Show what the sync would import
ansible.builtin.debug:
msg: "{{ _sync_dry.stderr_lines | default([]) + _sync_dry.stdout_lines | default([]) }}"
run_once: true
- name: Sync users and groups
# Always reports "changed": this is a reconcile ACTION against a live
# directory, not a declared state, and pveum gives no no-op signal to key off.
# The realm config and ACL above ARE drift-detected, so a re-run showing
# changed=1 means "sync ran", not "something was wrong".
ansible.builtin.command:
cmd: "pveum realm sync {{ pve_auth_realm }} --scope both --enable-new 1 --remove-vanished none"
register: _sync
changed_when: true
run_once: true
# --- scheduled sync --------------------------------------------------------
- name: List existing realm-sync jobs
ansible.builtin.command:
cmd: "pvesh get /cluster/jobs/realm-sync --output-format json"
register: _jobs
changed_when: false
check_mode: false
run_once: true
- name: Create the scheduled sync job
ansible.builtin.command:
cmd: >-
pvesh create /cluster/jobs/realm-sync/{{ pve_auth_sync_job }}
--realm {{ pve_auth_realm }}
--schedule '{{ pve_auth_sync_schedule }}'
--scope both --enable-new 1 --remove-vanished none --enabled 1
--comment 'Nightly AD user/group sync'
when: pve_auth_sync_job not in (_jobs.stdout | from_json | map(attribute='id') | list)
run_once: true
# --- RBAC ------------------------------------------------------------------
- name: Read current ACLs
ansible.builtin.command:
cmd: "pveum acl list --output-format json"
register: _acl_cur
changed_when: false
check_mode: false
run_once: true
- name: Grant the admin group its role
# NOTE the group name: PVE appends "-<realm>" to every synced group, so AD's
# `pve-admins` is `pve-admins-ad` here. Granting to the AD name matches nothing.
ansible.builtin.command:
cmd: >-
pveum acl modify {{ pve_auth_admin_path }}
--group {{ pve_auth_admin_group }} --role {{ pve_auth_admin_role }}
when: >-
(_acl_cur.stdout | from_json
| selectattr('path', 'equalto', pve_auth_admin_path)
| selectattr('ugid', 'equalto', pve_auth_admin_group)
| selectattr('roleid', 'equalto', pve_auth_admin_role) | list | length) == 0
run_once: true
- name: Report users, groups and ACLs
ansible.builtin.shell:
cmd: |
echo "--- users ---"; pveum user list --output-format json | python3 -c "import json,sys;[print(' ',u['userid']) for u in json.load(sys.stdin)]"
echo "--- groups ---"; pveum group list --output-format json | python3 -c "import json,sys;[print(' ',g['groupid'], g.get('users','')) for g in json.load(sys.stdin)]"
echo "--- acls ---"; pveum acl list --output-format json | python3 -c "import json,sys;[print(' ',a['path'],a.get('ugid'),a.get('roleid')) for a in json.load(sys.stdin)]"
register: _final
changed_when: false
run_once: true
- name: Show it
ansible.builtin.debug:
msg: "{{ _final.stdout_lines }}"
run_once: true