Skip to content

Enable self credentials rotation for static roles#212

Open
jadeidev wants to merge 47 commits intohashicorp:mainfrom
jadeidev:enable-self-rotation
Open

Enable self credentials rotation for static roles#212
jadeidev wants to merge 47 commits intohashicorp:mainfrom
jadeidev:enable-self-rotation

Conversation

@jadeidev
Copy link
Copy Markdown

@jadeidev jadeidev commented Sep 22, 2025

Overview

A high level description of the contribution

Who the change affects or is for (stakeholders)?

This change is for Vault users and administrators who manage static LDAP accounts and require Vault to rotate credentials using the account’s own password, rather than a privileged bind DN. Stakeholders include security teams and operators who need tighter control over credential rotation for sensitive accounts.

What is the change?

This contribution enables support for "self-managed" static accounts in the Vault OpenLDAP secrets engine. When enabled, Vault rotates credentials by authenticating as the managed account itself, using its current password, rather than relying on a privileged bind DN.
This feature introduces new attributes to the static role schema:

  • self_managed (default: false) parameter is optional and indicates whether the role manages its own password rotation. If true, Vault will perform rotations by authenticating as this account using its current password (no privileged bind DN).
    This requires the "password" parameter to be set on creation, and the "dn" parameter to be set as well.
    This field is immutable after creation. If false (the default), Vault will use the configured bind DN to perform rotations.

  • password parameter is required only if "self_managed" is true, and configures the current password for the entry.
    This allows Vault to assume management of an existing account. The password will be rotated on creation unless
    the skip_import_rotation parameter is set to true. The password is not returned in read operations.

  • self_managed_max_invalid_attempts parameter is optional and configures the maximum number of invalid current-password attempts for self-managed accounts. A value equal to 0 means use the default (5), and a negative value means unlimited attempts. When the maximum number of attempts is reached, automatic rotation is suspended until the role is updated via the password parameter. This field is immutable after creation.

  • rotation_suspended field is only returned in read operations and indicates whether automatic rotation is currently suspended for this static account due to too many invalid current-password attempts on self-managed accounts.

Why is the change needed?

Some environments restrict privileged access or require that password changes be performed by the account owner. Supporting self-managed rotation allows Vault to manage credentials for accounts where privileged bind is not possible or not desired, improving flexibility and security.

How does this change affect the user experience (if at all)?

Users can now create static roles with the self_managed option, providing the current password and DN. Vault will rotate credentials using the account’s own authentication, and will suspend automatic rotation after a configurable number of failed attempts, requiring manual intervention. This enhances Vault’s ability to manage a wider range of LDAP accounts and improves compliance with organizational policies.

Related Issues/Pull Requests

Contributor Checklist

  • Add relevant docs to upstream Vault repository, or sufficient reasoning why docs won’t be added yet - not sure where to add it. please let me know ill be happy to do it
  • Add output for any tests not ran in CI to the PR description (eg, acceptance tests) - all tests run in CI
  • Backwards compatible
  • Changelog entry added. See Updating the Changelog.

PCI review checklist

  • I have documented a clear reason for, and description of, the change I am making.

  • If applicable, I've documented a plan to revert these changes if they require more than reverting the pull request.

  • If applicable, I've documented the impact of any changes to security controls.

    Examples of changes to security controls include using new access control methods, adding or removing logging pipelines, etc.

- Mark password field as sensitive in schema.
- Add validation for distinguished name (DN) when setting self-managed accounts.
- Improve error handling for password rotation with self-managed accounts.
…eue and update test assertions for password validation
@jadeidev jadeidev requested a review from a team as a code owner September 22, 2025 18:59
…tation is suspended due to invalid password attempts.

- Updated logic in password setting to mark rotation as suspended when max attempts are reached.
- Cleared the suspended flag upon successful password rotation.
- Enhanced tests to validate the behavior of the rotation_suspended flag.
@jadeidev
Copy link
Copy Markdown
Author

jadeidev commented Oct 16, 2025

Possible Enhancements to this PR for RACF

For RACF self rotation is currently set to be done in the same way regular rotation is done (requiring the account to be privileged). As an alternative to that

Unfortunately I don't have an RACF system to test either options. Please provide guidance if there is interest.

sample code for changing a user password or password phrase in RACF using SDBM

func racfSelfRotation(cfg *Config, currentPassword, newPassword string) error {
	conn, err := c.ldap.DialLDAP(cfg.ConfigEntry)
	if err != nil {
		return err
	}
	defer conn.Close()
	// RACF (SDBM backend) supports self password/phrase change via simple bind using
	// "current/new". Slashes and backslashes inside a password phrase must be escaped.
	if currentPassword == "" || newPassword == "" {
		return fmt.Errorf("must provide both current and new passwords")
	}
	combined := racfCombineForBind(currentPassword, newPassword)
	// Determine the user to bind with
	var bindUser string
	switch {
	case cfg.UPNDomain != "":
		bindUser = fmt.Sprintf("%s@%s", ldaputil.EscapeLDAPValue(cfg.BindDN), cfg.UPNDomain)
	case cfg.BindDN != "":
		bindUser = cfg.BindDN
	default:
		return errors.New("must provide binddn or upndomain")
	}
	if err := conn.Bind(bindUser, combined); err != nil {
		return fmt.Errorf("racf self-managed password change bind failed: %w", err)
	}
}

// racfCombineForBind escapes RACF password phrases then joins current/new for simple bind change.
func racfCombineForBind(current, new string) string {
    return racfEscapePhrase(current) + "/" + racfEscapePhrase(new)
}

// racfEscapePhrase:
//  - Escape backslash as \\
//  - Escape forward slash as \/ so it is not interpreted as the delimiter
func racfEscapePhrase(s string) string {
    s = strings.ReplaceAll(s, `\`, `\\`)
    s = strings.ReplaceAll(s, `/`, `\/`)
    return s
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant