Introduction
This article documents insights around password storage as well as how passwords are templated in LDIF files used during credential creation and deletion.
Problem
LDAP credential generation using a dynamic role requires a creation and deletion LDIF file in order to manage a dynamic LDAP user account in a connected system (Ex. Active Directory).
In certain workflows there is a requirement to rotate the password of a static credential instead of creating and deleting a user account when a lease expires.
While it's possible to include a static credential (existing LDAP user account) in the creation and deletion LDIF files with the intent to only rotate the user account password, this results in the password not being rotated when the dynamic credential lease expires.
In the examples below, the default templated {{.Username}} has been replaced by an existing LDAP user account named bob:
//creation.ldif
dn: CN=bob,CN=Users,DC=example,DC=com
changetype: add
objectClass: top
objectClass: person
objectClass: organizationalPerson
objectClass: user
userPrincipalName: bob@example.com
sAMAccountName: bob
dn: CN=bob,CN=Users,DC=example,DC=com
changetype: modify
replace: unicodePwd
unicodePwd::{{ printf "%q" .Password | utf16le | base64 }}
-
replace: userAccountControl
userAccountControl: 66048
-
//deletion.ldif
dn: CN=bob,CN=Users,DC=example,DC=com
changetype: modify
replace: unicodePwd
unicodePwd::{{ printf "%q" .Password | utf16le | base64 }}
-
Prerequisites (if applicable)
- Vault 1.13 and later (Community Edition or Enterprise)
- LDAP Secrets Engine configured against an Active Directory backend
Cause
Vault generates the required data during lease creation. This includes values such as a username, password, etc. These values are stored inside the actual lease once generated. The Go template language can be used to reference the generated values from the lease in the creation and deletion LDIF files.
Looking at the example deletion.ldif we specify that the unicodePwd attribute value needs to be quoted, UTF16LE formatted as well as base64 encoded (Active Directory Password Requirements).
unicodePwd::{{ printf "%q" .Password | utf16le | base64 }}
The lease contains the .Password value as well as the templated deletion.ldif. An example of the templated data section of the lease is shown below:
"internal_data": {
"deletion_ldif": "dn: CN=bob,CN=Users,DC=example,DC=com\nchangetype: modify\nreplace: description\ndescription: {{ printf \"%q\" .Password | utf16le | base64 }}\n-",
"name": "dynamic-role",
"secret_type": "creds",
"template_data": {
"Username": "v_user_1728901304",
"Password": "sKgV0Dota-",
"DisplayName": "root",
"RoleName": "dynamic-role",
"IssueTime": "2024-10-14T12:21:44+02:00",
"IssueTimeSeconds": 1728901304,
"ExpirationTime": "2024-10-14T12:31:44+02:00",
"ExpirationTimeSeconds": 1728901904
}
},
The Password value can't be changed since it's generated and stored in the lease during lease creation.
When the lease expires, the unicodePwd attribute replace operation specified in the deletion.ldif is sent to the connected system (Active Directory), effectively setting the same password resulting in what appears to be a password not being changed.
Overview of possible solutions
Solutions:
- Dynamic roles are intended to create and maintain dynamic credentials in a connected system. It's not recommended to mix static credentials with dynamic role.
- The LDAP Secrets engine has a Service Account Check-Out feature that is better suited for automatic password rotation of static users (Service Accounts).
- Since the dynamic role is unable to rotate passwords for static accounts, leveraging UserAccountControl flags in Active Directory is a possible workaround. This way an account can be disabled or locked out instead.
Setting the userAccountControl attribute value to 66050 would set:
- 65536 DONT_EXPIRE_PASSWORD
- 512 NORMAL ACCOUNT
- 2 ACCOUNTDISABLE
An example of disabling an account when the lease expires would be:
//deletion.ldif
dn: CN=bob,CN=Users,DC=example,DC=com
changetype: modify
replace: unicodePwd
unicodePwd::{{ printf "%q" .Password | utf16le | base64 }}
-
replace: userAccountControl
userAccountControl: 66050
-
Outcome
While changing passwords for static accounts using a dynamic role is not possible, disabling the account instead should provide a solution in workflows where this is a requirement.
Additional Information
- Go Documentation: Go Template
- Vault Documentation: Active Directory Password Requirements
- Microsoft Documentation: UserAccountControl