Active Directory Playbooks
The AD lifecycle playbooks manage objects in Active Directory — OUs, computer objects, security groups, and gMSAs. All of them are idempotent and safe to re-run.
| Playbook | Purpose |
|---|---|
playbooks/create-ad-ous.yml |
Creates the standard OU tree (OU=<ad_ou_root> containing Servers with one child OU per application, ServiceAccounts, and Groups), deriving the per-application server OUs from the inventory's tag-derived application variable. Also links the AADDS default AADDC Computers GPO to OU=Servers (see below). |
playbooks/ensure-computer-ou.yml |
Moves existing computer objects into the OU given by each host's computer_ou inventory variable. Needed because domain join only honors the OU when it creates the computer account — re-joining a rebuilt VM reuses the old object wherever it currently sits. |
playbooks/create-gmsa.yml |
Creates security groups and gMSAs from gmsa_definitions, then purges the Kerberos ticket cache on member hosts in the play so the new group membership applies without a reboot — see the gMSA usage guide. |
playbooks/create-ad-groups.yml |
Ensures plain AD security groups exist (create-if-absent) from ad_security_groups, in OU=Groups by default. Membership is only touched when an entry supplies members (add-if-absent). Companion to create-gmsa.yml for RBAC groups — e.g. a group granted access in Kuiper via kuiper_security_groups[].domainGroups. |
playbooks/create-ad-users.yml |
Ensures plain AD service accounts exist (create-if-absent) from ad_service_accounts, in OU=ServiceAccounts by default — see Service accounts. |
playbooks/remove-computer-ad.yml |
Removes computer objects: -e computers=<name> or -e '{"computers": ["name1", "name2"]}'. The computers being removed do not need to be reachable, in the play, or even exist any more. |
The AADDC Computers GPO link (Azure/AADDS)
AADDS attaches its default AADDC Computers GPO only to the built-in
AADDC Computers OU. That GPO is what grants AAD DC Administrators local
Administrator rights on domain-joined machines — AADDS otherwise seeds local
Administrators with only Domain Admins, a group no AADDS account can belong
to. Because machines here join into OU=<application>,OU=Servers,OU=<ad_ou_root>
(our own tree, outside AADDC Computers), they would never receive that grant.
create-ad-ous.yml therefore links the AADDC Computers GPO to OU=Servers,
from which it inherits down to every application child OU. Kuiper's deploy
credential (AADDS\AAD DC Administrators) then becomes a local Administrator on
each managed machine natively, which is required for it to open PSSessions for
certificate and package deployment. The link is written directly as the OU's
gPLink attribute with microsoft.ad.object, over the same LDAP bind the
OU-creation tasks use — deliberately not through GPMC / microsoft.ad.gpo with
become: runas. Driving it as the domain admin via runas fails before the
hosts are joined: the local-logon right runas needs is exactly the one this
GPO grants, and pre-join the account cannot even be translated to a SID on the
target. Machines pick up the grant on the reboot that domain join performs, so
the OU tree and this link must exist before hosts are joined.
This GPO is specific to AADDS. On non-AADDS Active Directory (on-prem, AWS Managed AD) it does not exist, so the playbook looks it up first and skips the link when it is absent rather than failing — those environments grant the deploy credential local admin by other means.
The AD management host (ad_management_host)
All AD operations in these playbooks are delegated to a single inventory
host, ad_management_host, rather than running on the play's own targets.
The management host must be a Windows host with network line of sight to the
domain controllers, and the playbooks install the RSAT-AD-PowerShell feature
on it automatically. It does not need to be domain-joined when explicit
domain credentials are supplied; it does when they are omitted in favour of the
Kerberos path described under Credentials.
Default
The default is defined once, in group_vars/all/ad.yml:
That is: the first Kuiper server, taken from the tag-derived
app_kuiper group (see Application & Environment Groups).
There is deliberately no fallback — an environment without a Kuiper server
fails fast with an explicit message instead of delegating to an arbitrary
Windows host that may not be able to reach the DCs.
Overriding
- One-off:
-e ad_management_host=<inventory_hostname>on the command line. - Per environment: set
ad_management_hostin a group or host var — group and host vars take precedence over thegroup_vars/alldefault.
Removing the management host itself
When the computer object being removed is the default management host (e.g. rebuilding the Kuiper VM), pin the delegation to a different Windows host:
ansible-playbook -e computers=azcusikpr001 -e ad_management_host=azcusihsw001 playbooks/remove-computer-ad.yml
Service accounts (ad_service_accounts)
create-ad-users.yml creates plain domain users — the accounts an application
binds to AD with, such as System Pulse's LDAP bind account. Each entry needs a
name (used as the sAMAccountName, CN, and UPN prefix) and a password, and
may set description and path. An entry with create: false instead
references an account managed outside this playbook — it is asserted to
exist and never created or modified (no password needed), for environments
where the account is pre-provisioned by the customer's AD team:
ad_service_accounts:
- name: svc-systempulse
password: "{{ system_pulse_ldap_bind_password }}"
path: OU=ServiceAccounts,OU=IRE,DC=aadds,DC=ire-copier,DC=sapphirehealth,DC=org
description: System Pulse LDAP bind account.
path defaults to OU=ServiceAccounts under ad_ou_root. On AADDS the
built-in Users container is read-only, so the account must land in a custom
OU — the one create-ad-ous.yml creates.
The variable is read per host, so each application group declares the
accounts it needs in its own group_vars (the example above lives in
group_vars/ire_copier_azure_app_systempulse/vars.yml, next to the vaulted
password it references). Hosts that do not define it are skipped.
Accounts are created with password_never_expires and
user_cannot_change_password, and the password is applied only at creation —
re-running never rewrites an existing account's password.
Credentials
The playbooks resolve the domain and admin credentials from domain_name /
domain_admin_user / domain_admin_password (normally vaulted — see
Secret Management), falling back to the
DOMAIN_NAME / DOMAIN_ADMIN_USER / DOMAIN_ADMIN_PASSWORD environment
variables. A domain controller is discovered automatically via a DNS SRV
lookup against the domain.
Only the domain and the domain controller are mandatory. When
domain_admin_user and domain_admin_password are both unset, the playbooks
omit them and the microsoft.ad modules bind to AD as the identity the
connection to the management host is already running under — the Kerberos path,
for a management host reached over WinRM with Kerberos authentication and
credential delegation. Set both or neither; supplying only one fails the
pre-flight assertion rather than the module.
To run this way, connect with Kerberos instead of NTLM: leave server_admin_user
and server_admin_password unset so no explicit connection credential is built,
set ansible_psrp_auth: kerberos, and make sure the controller has a valid
ticket. playbooks/configure-ansible-for-kerberos-auth.yml writes the realm
config from the domain's SRV records (a repo-relative krb5.conf by default, or
/etc/krb5.conf with -e krb5_conf_path=/etc/krb5.conf -e krb5_conf_become=true),
and playbooks/configure-winrm-kerberos.yml registers the WSMAN SPNs on the target.
The delegated identity needs the same directory rights the domain admin account
would have used, so this is an alternative to storing a domain admin password,
not a way to run these playbooks with fewer privileges.