Skip to content
Rotate Active Directory Accounts

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

ScriptWhat it does with the new password
rotate-ad-account.shSets it in AD and stops there. The baseline rotation.
rotate-ad-account-aws-secret.shSets it in AD, then patches one key of an existing Secrets Manager secret. Every other field is preserved.
rotate-ad-service-account.shSets 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 unicodePwd writes over cleartext LDAP, so plain 389 cannot work
  • ldapsearch, ldapmodify, python3, openssl, and base64 on the broker (all ship in the britive/bridge image)
  • A bind account with Reset Password, and write on lockoutTime and pwdLastSet, 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 pywinrm on 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 resourceArrives asScript reads
hostRESOURCE_HOSTAD_HOST
base_dnRESOURCE_BASE_DNAD_BASE_DN
secretRESOURCE_SECRETAD_SECRET
regionRESOURCE_REGIONAWS_REGION
ca_certRESOURCE_CA_CERTAD_CA_CERT
user_ouRESOURCE_USER_OUAD_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

VariableScriptsRequiredDescription
AD_TARGET_USERallYessAMAccountName of the account to rotate, e.g. svc-app01
AD_NEW_PASSWORDallYesMark it encrypted; Britive generates it
AWS_SECRET_ARNAWS variantYesARN (or name) of the secret to patch
AWS_SECRET_KEYAWS variantNoJSON key holding the password (default password)
AD_TARGET_SERVERservice variantYesHost running the service, reachable from the broker
AD_SERVICE_NAMEservice variantYesThe short service name, not the DisplayName
AD_RESTART_SERVICEservice variantNotrue (default) or false
AD_EMIT_PASSWORDallNotrue prints the password in the output. Default false
AD_VERBOSEallNotrue 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")
EOF

replace — 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: -1

pwdLastSet 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: true

The 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 lockoutTime

pwdLastSet 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

SymptomCauseFix
unwilling to perform on the resetCleartext LDAP, or a malformed unicodePwdUse LDAPS on 636; confirm the quoted-UTF-16LE-base64 encoding
AD_NEW_PASSWORD is not setThe rotation has no such variableAdd it as an encrypted variable on the rotation
Password rejected by ADDomain password policy, history, or minimum ageMatch Britive’s generated-password rules to the domain policy
Account rotated but still can’t log onMust-change flag not clearedConfirm the bind account can write pwdLastSet
does not exist in <base DN>Wrong base_dn, or the account really is absentThese scripts only rotate existing accounts
WinRM preflight fails to resolve the hostThe broker’s VPC does not use the domain controller for DNSUse the private IP or a name the VPC resolves — NTLM authenticates by name or address alike
Log shows only INFO linesBritive keeps ~250 characters of outputThe scripts print the reason first and the trace after; set AD_VERBOSE=true for a hand-run

Next Steps

Last updated on