Rotate Active Directory Accounts
Overview
These scripts rotate the password on an Active Directory account that already exists — a service account, or any shared identity named outright rather than derived from a requester. They run on the Linux broker over LDAPS, so no Windows host and no RSAT module is needed.
Each is a single standalone file. Upload it as the rotation script on the resource type; nothing else has to be installed.
What you’ll accomplish:
- Pick the variant that matches what consumes the credential
- Configure the AD connection as resource attributes
- Verify the account rotated and the consumer followed
Britive generates the password, not the script. AD_NEW_PASSWORD is required and no script here can generate one. A password created inside the script would be known only to that process — the platform could neither store nor vend it, so the account would end up locked out of its own consumers with nobody holding the new secret.
Which Script
| Script | What it does with the new password |
|---|---|
rotate-ad-account.sh | Sets it in AD and stops there. The baseline rotation. |
rotate-ad-account-aws-secret.sh | Sets it in AD, then patches one key of an existing Secrets Manager secret. Every other field is preserved. |
rotate-ad-service-account.sh | Sets it in AD, then writes it onto a Windows service logon account over WinRM and restarts the service. |
All three also unlock the account and clear the must-change-password-at-next-logon flag that an administrative reset leaves behind. Without that second step the account cannot authenticate non-interactively — which is the only way a service account ever authenticates.
The repository also carries a with-library/ variant of each script that shares one copy of the LDAP plumbing instead of inlining it. It is optional and produces identical results — worth it only if you build the broker image yourself and run many AD scripts. See Active Directory/lib for the trade-off. Everything in this guide applies to both.
How It Works
flowchart TD
Sched["Britive rotation<br/>(schedule or on demand)"]
Script["rotate-ad-*.sh<br/>(runs on the Linux broker)"]
AD["Active Directory<br/>(LDAPS 636)"]
SM["AWS Secrets Manager"]
Svc["Windows service<br/>(WinRM)"]
Sched -->|"injects AD_NEW_PASSWORD (encrypted) + RESOURCE_* attributes"| Script
Script -->|"1. preflight the consumer"| Svc
Script -->|"2. reset unicodePwd, unlock, clear pwdLastSet"| AD
Script -->|"3. propagate"| SM
Script -->|"3. propagate"| Svc
Before You Begin
- The Access Broker is deployed and connected
- LDAPS reachable on port 636. Active Directory refuses
unicodePwdwrites over cleartext LDAP, so plain 389 cannot work ldapsearch,ldapmodify,python3,openssl, andbase64on the broker (all ship in thebritive/bridgeimage)- A bind account with Reset Password, and write on
lockoutTimeandpwdLastSet, over the target OU - The target account already exists — a rotation rotates, it never creates
- For the Windows service variant: WinRM reachable from the broker, and
pywinrmon the broker (ships in the base image)
Variables
Connection — set these as resource attributes
The broker delivers a resource’s attributes upper-cased with a RESOURCE_ prefix, so each script assigns them across at the top:
| Attribute on the resource | Arrives as | Script reads |
|---|---|---|
host | RESOURCE_HOST | AD_HOST |
base_dn | RESOURCE_BASE_DN | AD_BASE_DN |
secret | RESOURCE_SECRET | AD_SECRET |
region | RESOURCE_REGION | AWS_REGION |
ca_cert | RESOURCE_CA_CERT | AD_CA_CERT |
user_ou | RESOURCE_USER_OU | AD_USER_OU |
AD_SECRET holds the name of a Secrets Manager secret containing the bind credentials — not the credentials themselves. See How the Broker Passes Values to a Script.
Rotation variables — set these on the rotation
| Variable | Scripts | Required | Description |
|---|---|---|---|
AD_TARGET_USER | all | Yes | sAMAccountName of the account to rotate, e.g. svc-app01 |
AD_NEW_PASSWORD | all | Yes | Mark it encrypted; Britive generates it |
AWS_SECRET_ARN | AWS variant | Yes | ARN (or name) of the secret to patch |
AWS_SECRET_KEY | AWS variant | No | JSON key holding the password (default password) |
AD_TARGET_SERVER | service variant | Yes | Host running the service, reachable from the broker |
AD_SERVICE_NAME | service variant | Yes | The short service name, not the DisplayName |
AD_RESTART_SERVICE | service variant | No | true (default) or false |
AD_EMIT_PASSWORD | all | No | true prints the password in the output. Default false |
AD_VERBOSE | all | No | true logs progress live instead of buffering it |
Rotation variables arrive under exactly the name you configure — no prefix, no case change.
Inside the Script
Map the resource attributes
# A value set directly wins, so the script stays runnable by hand.
AD_HOST="${AD_HOST:-${RESOURCE_HOST:-}}"
AD_BASE_DN="${AD_BASE_DN:-${RESOURCE_BASE_DN:-}}"
AD_SECRET="${AD_SECRET:-${RESOURCE_SECRET:-}}"
AWS_REGION="${AWS_REGION:-${RESOURCE_REGION:-}}"
# AD_TARGET_USER and AD_NEW_PASSWORD need no mapping — the rotation passes
# them under their own names already.
: "${AD_TARGET_USER:?set AD_TARGET_USER on the rotation}"
: "${AD_NEW_PASSWORD:?Britive supplies this; configure the attribute on the rotation}"Reset the password
AD stores passwords in unicodePwd as the plaintext wrapped in literal double quotes, encoded UTF-16LE, then base64 for LDIF transport. Any deviation returns unwilling to perform with no further detail.
encode_unicode_pwd() {
printf '%s' "$1" | python3 -c '
import base64, sys
quoted = f"\"{sys.stdin.read()}\"".encode("utf-16-le")
sys.stdout.write(base64.b64encode(quoted).decode("ascii"))
'
}
# The bind password is written to a 0600 file and passed with -y, never on argv.
ldapmodify -x -H "ldaps://${AD_HOST}:636" -D "$BIND_DN" -y "$BIND_PW_FILE" <<EOF
dn: ${USER_DN}
changetype: modify
replace: unicodePwd
unicodePwd:: $(encode_unicode_pwd "$NEW_PASSWORD")
EOFreplace — not add or delete — is the administrative reset form, and it does not require the previous password.
Clear what the reset leaves behind
# Unlock. Best-effort: the reset already succeeded, and failing the whole
# rotation over a lockout flag would be worse than reporting it.
replace: lockoutTime
lockoutTime: 0
# Clear must-change-at-next-logon. NOT best-effort: an account left flagged
# cannot authenticate non-interactively, so the new credential would be useless.
replace: pwdLastSet
pwdLastSet: -1pwdLastSet accepts only two values: 0 forces a change at next logon, -1 stamps the current time and clears that requirement.
Propagating the New Password
Two systems that cannot be changed atomically means the order matters. Both variants put the cheap failures first.
rotate-ad-account-aws-secret.sh reads the secret and builds the patched JSON before AD is touched, so a bad ARN or a missing permission costs nothing:
# 1. Preflight — read the secret and confirm it is a JSON object.
CURRENT_SECRET="$(aws secretsmanager get-secret-value \
--secret-id "$AWS_SECRET_ARN" --query SecretString --output text)"
printf '%s' "$CURRENT_SECRET" | jq -e 'type == "object"' >/dev/null \
|| die "secret is not a JSON object — this script patches one key, it will not overwrite a plaintext value"
# 2. Reset in AD (shown above).
# 3. Patch only AWS_SECRET_KEY; every other field survives.
printf '%s' "$CURRENT_SECRET" \
| jq --arg k "$SECRET_KEY" --arg v "$NEW_PASSWORD" '.[$k] = $v' > "$SECRET_FILE"
aws secretsmanager put-secret-value \
--secret-id "$AWS_SECRET_ARN" --secret-string "file://${SECRET_FILE}"If step 3 fails after step 2 succeeded, the script reports DIVERGED and names both sides — the account has the new password, the secret still serves the old one.
Configure in Britive
Define the resource
In Resource Manager → Resource Types, create the AD resource type with host, base_dn, secret, region, ca_cert, and user_ou parameters, then add a resource that fills them in for your domain.
Add the rotation script
Paste the chosen script as the resource type’s rotation routine.
Add the rotation variables
Add AD_TARGET_USER with the account name. Add AD_NEW_PASSWORD and mark it encrypted. Add the variant’s own variables (AWS_SECRET_ARN, or AD_TARGET_SERVER and AD_SERVICE_NAME).
Set the schedule
Configure the rotation interval, then run it once by hand and read the broker log before trusting the schedule.
Verify
Check the script output
A successful run emits key: value lines, ready for a response template:
username: svc-app01
password_rotated: true
account_unlocked: trueThe AWS variant adds secret_arn, secret_key, and secret_version. The service variant adds service_updated and service_restarted.
Confirm the reset in AD
ldapsearch -x -H "ldaps://dc01.contoso.local:636" -D "$BIND_DN" -y "$BIND_PW_FILE" \
-b "$AD_BASE_DN" "(sAMAccountName=svc-app01)" pwdLastSet lockoutTimepwdLastSet carries a fresh timestamp and lockoutTime is 0.
Confirm the consumer followed
Read the secret back, or check that the Windows service is running under the rotated account.
The rotated password is not printed unless AD_EMIT_PASSWORD=true. Britive already holds the value it generated, so echoing it into the response only widens where the secret appears.
Troubleshoot
| Symptom | Cause | Fix |
|---|---|---|
unwilling to perform on the reset | Cleartext LDAP, or a malformed unicodePwd | Use LDAPS on 636; confirm the quoted-UTF-16LE-base64 encoding |
AD_NEW_PASSWORD is not set | The rotation has no such variable | Add it as an encrypted variable on the rotation |
| Password rejected by AD | Domain password policy, history, or minimum age | Match Britive’s generated-password rules to the domain policy |
| Account rotated but still can’t log on | Must-change flag not cleared | Confirm the bind account can write pwdLastSet |
does not exist in <base DN> | Wrong base_dn, or the account really is absent | These scripts only rotate existing accounts |
| WinRM preflight fails to resolve the host | The broker’s VPC does not use the domain controller for DNS | Use the private IP or a name the VPC resolves — NTLM authenticates by name or address alike |
| Log shows only INFO lines | Britive keeps ~250 characters of output | The scripts print the reason first and the trace after; set AD_VERBOSE=true for a hand-run |
Next Steps
- Rotate AWS Secrets Manager Secrets — when the secret, not a directory account, is what needs rotating
- Active Directory integration — scans, group membership, and checkout-based admin account rotation
- Source: britive/access-broker-examples — Active Directory