Issue Updating User Password via OpenDirectory API with Root Daemon Privileges

Description:

I am attempting to use the OpenDirectory API ODRecord.changePassword to change a user's password without needing the old password, given that I have the appropriate permissions. The goal is to ensure that the password change operation bypasses third-party tools such as EDR or eBPF apps that might otherwise intercept commands, as the operation occurs directly via the API.

Problem:

When invoking the OpenDirectory API from a launch daemon with root privileges, I receive the following error message:

Error Domain=com.apple.OpenDirectory Code=4001 "Operation was denied because the current credentials do not have the appropriate privileges."

UserInfo={NSUnderlyingError=0x135907570 {Error Domain=com.apple.OpenDirectory Code=4001 "Credential cannot update user's SecureToken" UserInfo={NSDescription=Credential cannot update user's SecureToken}},

NSLocalizedDescription=Operation was denied because the current credentials do not have the appropriate privileges., 
NSLocalizedFailureReason=Operation was denied because the current credentials do not have the appropriate privileges.}

It seems the error is related to SecureToken, and the underlying issue is that the current credentials (even though they are root-level) do not have the necessary privileges to update the SecureToken status for the user.

Steps I’ve Taken:

  1. Tested the API via a launch daemon running with root privileges.
  2. Ensured that Full Disk Access was granted to the daemon, but this did not resolve the issue.

Request:

  1. Has anyone encountered this specific issue where root privileges are insufficient to update the user password via the OpenDirectory API ?

  2. What additional steps or permissions are required for a user password change?

  3. Is there a specific API or method to elevate the privileges for modifying SecureToken, or a workaround to overcome this limitation?

Any insights or guidance on this issue would be greatly appreciated!

Thank you in advance for your help!

Okay with few changes I am able to create a user and also reset the password for the admin account using OD APIs but I see that SecureToken is disabled whereas I was under the assumption that SecureToken gets enabled automatically for admin users.

  1. Any idea why secure token is not getting enabled for new account created via the OD APIs?

  2. I also noticed that its not possible to change the password of an admin user which has Secure token enabled. It throws above error as mentioned in the post.

Create User


    func createUserAccount(username: String, fullName: String, password: String) throws {
        // Get the local node
        let localNode = try ODNode(session: ODSession.default(), type: ODNodeType(kODNodeTypeLocalNodes))
        
        // Generate a unique user ID
        let uniqueID = "509"
        
        // Create the user record with properly formatted attributes
        let attributes: [String: [String]] = [
            kODAttributeTypeFullName: [fullName], // User's full name
            kODAttributeTypeUniqueID: [uniqueID], // Unique ID for the user
            kODAttributeTypePrimaryGroupID: ["80"], // Default group ID (staff group)
            kODAttributeTypeNFSHomeDirectory: ["/Users/\(username)"], // User's home directory
            kODAttributeTypeUserShell: ["/bin/bash"] // Default shell
        ]
        
        // Create a new user record
        let userRecord = try localNode.createRecord(
            withRecordType: kODRecordTypeUsers,
            name: username,
            attributes: attributes
        )
        
        // Set the user's password
        try userRecord.changePassword(nil, toPassword: password)
        
        // Add the user to the "admin" group
        let adminGroupRecord = try localNode.record(
            withRecordType: kODRecordTypeGroups,
            name: "admin",
            attributes: nil
        )
        
        try adminGroupRecord.addMemberRecord(userRecord)
        
        print("Admin account \(username) created successfully with UID \(uniqueID).")
    }

I just realised that root user doesn't have the secure token enabled and may be that's why account being created by a launch daemon doesn't have secure token enabled by default.

My understanding here is that only a user which has secure token enabled can create another user with secure token enabled.

So Is there a way to create a user account with secure token enabled using OD API ?

This is going to be tricky. There are three separate passwords in play here:

  • OD maintains the traditional password hash.

  • OD maintains this SecureToken, which was introduced to support FileVault full disk encryption.

  • And then there’s the data protection keychain.

The OD check you’re bumping into is designed to prevent the first and second items getting out of sync. My concern is that, even if you find a way around this, you’ll put the user into a position where the second and third items are out of sync )-:

Taking a step back, do you need to set the user’s password directly? Or would it be sufficient for you to trigger a password reset the next time they log in? The latter should help to keep everything consistent.

Share and Enjoy

Quinn “The Eskimo!” @ Developer Technical Support @ Apple
let myEmail = "eskimo" + "1" + "@" + "apple.com"

Thank you for your helpful response !

I need to change the user's password silently on every login.

Scenario Overview:

I’m working on a system where I want to create a secondary user account with SecureToken enabled. This account is hidden from the login window, and while the account is visible in FileVault, the goal is to dynamically reset the account password after each successful login, sending the new password to a secure mobile app for the next login. The password would change automatically after each login, and the user would not be required to remember it, improving security with password rotation.

Thank you again for your insights.

I’d like to clarify a few more details about the use case:

The new user account we create is exclusively for controlling FileVault authentication. The account is hidden from the login window and is not meant for direct user login. The intention is to leverage this account solely for managing access to FileVault during system startup.

Additionally, automatic FileVault login is disabled. This means that upon system boot, users will first authenticate using our hidden account (through FileVault unlocking), after which they can log into their respective accounts—the password for this account is also managed dynamically by our iOS app.

This system ensures that while the users will never directly interact with the hidden account, the password for FileVault access is securely rotated after each login, providing continuous security improvements.

Thanks for the big picture info. That was super helpful.

Unfortunately I don’t see any way to do this. Regardless of the SecureToken stuff, the data protection keychain is becoming increasingly important and I’m not aware of any way to change its password.

Lemme think about this some more and then get back to you.

Share and Enjoy

Quinn “The Eskimo!” @ Developer Technical Support @ Apple
let myEmail = "eskimo" + "1" + "@" + "apple.com"

Thanks for your insights

I was able to achieve my use case using sysadminctl commands, which worked as expected. The main issue, however, is that this approach requires admin credentials to be passed as a parameter to the command. This, of course, introduces a potential security risk, as some endpoint security solutions might intercept these commands and expose the credentials.

Currently, I'm executing the sysadminctl command from a daemon, but I’m wondering if there’s a way to hide or obfuscate the admin credentials passed as parameters to prevent such exposure.

Do you have any recommendations for mitigating this risk ?

I was able to achieve my use case using sysadminctl commands

Yeah, that’s kinda the direction I was heading. And I’d prefer not to be heading in that direction because there are all sorts of problems with using command-line tools as API. However, there just isn’t an API alternative in this case. Under the covers, sysadminctl uses a bunch of SPI, some of which are guarded by entitlements that are only available to code built in to the OS.

The main issue, however, is that this approach requires admin credentials to be passed as a parameter to the command.

The sysadminctl help suggests that you can use a - to request that it prompt for passwords. Did you try that?

Share and Enjoy

Quinn “The Eskimo!” @ Developer Technical Support @ Apple
let myEmail = "eskimo" + "1" + "@" + "apple.com"

Thank you so much for your prompt and helpful response! I must admit, I completely overlooked using the - flag to prompt for passwords. It worked perfectly for resetting the password.

Here's the output I received:

2025-01-13 10:05:49.547 sysadminctl[41068:1823531] resetting password for TestUser. (Keychain will not be updated!)

2025-01-13 10:05:52.409 sysadminctl[41068:1823531] - Done

As you pointed out, it looks like there’s a keychain password sync issue, since the keychain isn't updated along with the system password.

Is there a command or method to update the keychain password to match the system password?

Also, just a thought—if the sysadminctl reset command is causing this out-of-sync issue, wouldn't it make the command effectively useless? Since the keychain wouldn't be updated, the reset wouldn't actually work as expected, right?

Just to be clear, you’re using the -resetPasswordFor subcommand, right?

Share and Enjoy

Quinn “The Eskimo!” @ Developer Technical Support @ Apple
let myEmail = "eskimo" + "1" + "@" + "apple.com"

Yes, I am using the -resetPasswordFor command.

I also noticed an issue where creating an account via sysadminctl doesn't enable the secure token, while creating an account using dscl does enable it.

Example:

sudo sysadminctl -addUser testuser -fullName "Test User" -password - -adminUser AdminUser -adminPassword -

If I try to manually enable the secure token I get an error: "Operation is not permitted without secure token unlock."

Example:

sudo sysadminctl -addUser testuser -fullName "Test User" -password - -admin -secureTokenOn AdminUser -passwordFor -

Secure token for the current account is already enabled

OK. Then the keychain password behaviour you’re seeing has a ready explanation. Your login keychain is effectively encrypted with your login password. So, to set a new password you have to have the old password. That’s what -newPassword does. When you use -resetPasswordFor there’s nowhere to supply the old password and thus you lose the login keychain contents.

You see this exact same phenomenon in System Settings. If an admin sets a new password for some other user, the resulting UI explains that it doesn’t reset that user’s keychain password.

The issue here for you is that -newPassword is expecting to be run in the user’s context, not from your daemon. Indeed, it doesn’t even take a user name argument.

Share and Enjoy

Quinn “The Eskimo!” @ Developer Technical Support @ Apple
let myEmail = "eskimo" + "1" + "@" + "apple.com"

Issue Updating User Password via OpenDirectory API with Root Daemon Privileges
 
 
Q