Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Question about Macs with existing LAPS account #6

Closed
jelockwood opened this issue Jun 5, 2023 · 25 comments
Closed

Question about Macs with existing LAPS account #6

jelockwood opened this issue Jun 5, 2023 · 25 comments

Comments

@jelockwood
Copy link

jelockwood commented Jun 5, 2023

@PezzaD84
I have recently joined an organisation with existing Macs and Jamf and currently existing local admin accounts all using the same password.

I am looking to implement your macOSLAPS solution and would like to use it to take over the existing local admin accounts. As they currently all have the same password it would in theory be possible to do either of the following.

  1. Have your script(s) use a (known) default password if the user account already exists and then randomise it
  2. Have your script(s) default to doing a password reset for an existing account

It would seem that the first option would be preferable however it looks like your current setup is not geared up for this.

Clearly I could as per your documentation create a new (additional) local admin account but that is not desirable either. Do you have an existing approach for this or would it be possible to add a new additional option to allow this?

Note: As we are using Jamf Connect it would be responsible for creating the local admin account initially so it would exist before your script gets a chance to create it. It is potentially possible that the initial password as created via Jamf Connect would not be a specific chosen password but the FileVault Personal Recovery Key which Jamf Connect would have enabled and as per (other) Jamf settings escrowed to Jamf. It is possible to read this recovery key via the Jamf API so you could also potentially build this capability in. See - https://travellingtechguy.blog/jamf-connect-and-laps/

@PezzaD84
Copy link
Owner

PezzaD84 commented Jun 6, 2023

Hi @jelockwood

So your options 1 and 2 are both very doable but I've not implemented these workflows as they would both require an existing admin password to be inputed in plain text somewhere in the script or in a Jamf variable.

One idea you could do is to have the existing account removed just before the LAPS script runs. That way it will not interfere with the LAPS script creating the new one.

Alternatively as you have pointed out that JAMF Connect can indeed create and cycle an existing account. This can be used with my script if the script is placed in a prestage where it runs before the user has been created or logged in. The issue with this is that JAMF Connect sets the password to the recovery key and then there isn't a way for my script currently to cycle the password after then. I guess in theory you could add some logic to check if the recovery key and local password are the same and then have the LAPS account password cycled but to me it's just adding more complexity and more points of failure.

I have set my script up for many customers where it runs during the prestage process and is created before any user logs in through JAMF Connect. This workflow is mainly for zero-touch provisioning where an IT Engineer is not creating an admin account with JAMF Connect but rather the end user logs in and creates a standard account. Granted this does leave the LAPS account without a secure token but then you will still have the option to use the recovery key if you ever do need to unlock the drive.

@jelockwood
Copy link
Author

jelockwood commented Jun 6, 2023

@PezzaD84
I have knocked up the following demo code which might help with this.
The code uses the Jamf API (not the classic API) to retrieve the FileVault Personal Recovery Code, it would also 'in real life' use a script parameter to receive the 'default' password, this could be 'in real life' encoded the same way as your current script.

So $10 would be the default password (encoded) and $11 would be the JSSID i.e. the computer ID record number of the computer so this can be used to retrieve the FileVault recovery key. The Jamf API does not use a serial number for this record unlike the classic API.

It would then check to see if the existing password equals the FileVault recovery key, if not it then tries the default password.

Obviously if there is an existing record with an existing random password that should be tried as well.

I put some example values in as I was trying this outside of Jamf.

#!/bin/bash


username="username"
password="password
url="https://host.jamfcloud.com
#defaultPass=$10
defaultPass="defaultpassword"
#id=$11
#id=$JSSID
id=132
LAPSaccount="localadmin"

#Variable declarations
bearerToken=""
tokenExpirationEpoch="0"

getBearerToken() {
	response=$(curl -s -u "$username":"$password" "$url"/api/v1/auth/token -X POST)
	bearerToken=$(echo "$response" | plutil -extract token raw -)
	tokenExpiration=$(echo "$response" | plutil -extract expires raw - | awk -F . '{print $1}')
	tokenExpirationEpoch=$(date -j -f "%Y-%m-%dT%T" "$tokenExpiration" +"%s")
}

checkTokenExpiration() {
    nowEpochUTC=$(date -j -f "%Y-%m-%dT%T" "$(date -u +"%Y-%m-%dT%T")" +"%s")
    if [[ tokenExpirationEpoch -gt nowEpochUTC ]]
    then
        echo "Token valid until the following epoch time: " "$tokenExpirationEpoch"
    else
        echo "No valid token available, getting new token"
        getBearerToken
    fi
}

invalidateToken() {
	responseCode=$(curl -w "%{http_code}" -H "Authorization: Bearer ${bearerToken}" $url/api/v1/auth/invalidate-token -X POST -s -o /dev/null)
	if [[ ${responseCode} == 204 ]]
	then
		echo "Token successfully invalidated"
		bearerToken=""
		tokenExpirationEpoch="0"
	elif [[ ${responseCode} == 401 ]]
	then
		echo "Token already invalid"
	else
		echo "An unknown error occurred invalidating the token"
	fi
}

checkTokenExpiration
curl -s -H "Authorization: Bearer ${bearerToken}" $url/api/v1/jamf-pro-version -X GET
checkTokenExpiration

fvcode=$(curl --request GET --url "$url/api/v1/computers-inventory/$id/filevault" -H "Authorization:Bearer ${bearerToken}" | grep "personalRecoveryKey" | awk '{print $3}' | tr -d '",')

echo "fv = $fvcode"

invalidateToken
curl -s -H "Authorization: Bearer ${bearerToken}" $url/api/v1/jamf-pro-version -X GET

OLDpasswd="xxxx"

if dscl . -list /Users | grep -x -i "$LAPSaccount";
then
	echo "Local Admin exists"
	pswdCheck=$(dscl /Local/Default -authonly "$LAPSaccount" "$OLDpasswd" 2>&1)
	if  echo "$pswdCheck" | grep eDSAuthFailed;
	then
		echo "not stored password"
		pswdCheck=$(dscl /Local/Default -authonly "$LAPSaccount" "$fvcode" 2>&1)
		if echo "$pswdCheck" | grep eDSAuthFailed;
		then
			echo "not FileVault code"
			pswdCheck=$(dscl /Local/Default -authonly "$LAPSaccount" "$defaultPass" 2>&1)
			if echo "$pswdCheck" | grep eDSAuthFailed;
			then
				echo "not default password"
			else
				echo "is default password"
			fi
		else
			echo "is FileVault code"
		fi
	else
		echo "is stored password"
	fi
else
	echo "Local Admin does not exist"
fi

@jelockwood
Copy link
Author

@PezzaD84
If you are not going to be able to add additional capability to your script to cope with existing user accounts, can you tell me how your script differentiates between a new and existing account?

Potentially I could write my own script which for example puts initial values in the LAPS Secret and LAPS Crypt Key extension attributes. I could use a smart group so your script only runs after the extension attributes are filled in and hence when your script runs it treats it as an ongoing process and just updates the content.

@PezzaD84
Copy link
Owner

Hi @jelockwood

Sorry I completely forgot to reply to your last message. Yes this would work but again would need to pass a password in plain text at some point.

In reply to the latest message, there is some logic in the script that checks if the script has run before and to check for accounts with the same name specified in the JAMF variable. If the script has not run and the account name doesn't exist locally then the creation runs. If the script has not run and the account name exists then it will fail.

@jelockwood
Copy link
Author

@PezzaD84
If the decryption key for the initial LAPS password aka secret is filled in to the respective LAPS crypt key extension attribute along with the encrypted password in the other extension attribute why do you need a plain text password? You can read both and decrypt the secret, then you can use it to authorise changing it then you can write the new password and new encryption key to the same extension attributes.

I need to understand your script logic and how it determines if it has not run before and therefore would fail for an existing account. I need to trick your script to thinking it has run before and for it to simply read the values and carry on as normal.

@PezzaD84
Copy link
Owner

@jelockwood
You bring up a very good point. Let me take this away and plan out the work flow for managing existing admin accounts. It might possibly require some offline local workflows that then need to be plugged into the variables to manage passing existing credentials to devices but I can probably make it work.
My only concern with this approach is that there could well be variations in local admin passwords if certain engineers have used something different over the years which would result in failures which is why I had originally gone for a clean slate approach.

@jelockwood
Copy link
Author

jelockwood commented Jun 14, 2023

@PezzaD84
Thanks, I look forward to your results. In my case I know that currently ALL our Macs have the same local admin with the same password which is why I want to implement your LAPS solution 😄

@PezzaD84
Copy link
Owner

Hi @jelockwood

I've attached an updated script which now includes the ability to run against an existing local admin account. Please now this is still in beta testing so there could still be the odd bug here and there. So far testing has been working really well and the existing password is being cycled and reset and then managed by the existing LAPS policies as normal.

You will need to encode the password first using the attached script which will give you a secret key and the encoded string which you need to now specify in $10 and $11.
You will also need to create another policy which will be the first LAPS policy to run. I have been specifying this to run once per device and then use the other policies to manage the cycle and reset of the passwords after first run.

My current policy looks like this but it could change in the final documentation once I refine it a little.
Screenshot 2023-06-19 at 10 23 36
Screenshot 2023-06-19 at 10 21 41

Encode Existing Password.zip
LAPS - Create and Cycle LAPS Account V2.3.zip

@jelockwood
Copy link
Author

@PezzaD84
Thank you for looking at making this change.

I have started incorporating it in to my workflow. I have created a script for use as a Jamf Policy. This script uses an encryption 'Secret' to encrypt the initial default password and then writes the encrypted password and the 'Secret' to the extension attributes as initial values.

Your modified script should be able to read them and then use them as the starting point.

I will be intending to combine your script, my script to set initial values and then link to Super via another script I am writing so that Super can use the LAPS credentials itself.

Have a look at the following which is the script I wrote to populate (your) LAPS extension attributes. I will be using a smart group to look for computers in Jamf with empty values, and then fill them in. Once filled in i.e. not empty, I will then have another smart group to trigger running your LAPS script.

See - https://github.com/jelockwood/Super-Glue/blob/main/laps-populate.sh

@PezzaD84
Copy link
Owner

Nice, I'll check out what you've been working on when I get a free moment.
So the revised script now lets you specify the encoded local admin password and the secret to uncode it so you don't actually need to pre-populate the extension attributes. Might save you some extra work?

@jelockwood
Copy link
Author

jelockwood commented Jun 20, 2023

@PezzaD84
Yes that sounds like it would save some work and mean also the process of initiating your solution should run quicker as it will not need to wait for my initial pre-populating the extension attributes.

Oops, re-read above and see the latest script and settings including API credentials, however if you have a list of Jamf permissions you need for the api account that would still be useful. I will give this a try tomorrow.

@jelockwood
Copy link
Author

@PezzaD84
Ok, I have put the new version of your script in to Jamf, I have named the parameter fields and filled them with the right values.

I am however not clear on what policies need to be created, your comment above implies an additional policy but is not clear on what trigger it has compared to the other policies. The notes seem to imply that there would be two copies of the "LAPS | Create local admin & password" policy. How do they behave differently? Is this by having one without the admin account values specified? I would not want it to accidentally reset the local admin account and destroy the secure token/volume ownership status.

It might be clearest to list all the policies including the additional one so I can double check my setup.

@PezzaD84
Copy link
Owner

@jelockwood
So the policy I made is set to run once per device as this will initially reset the existing local admin account as it will have the extra variables $10 and $11 populated so we don't want this to run all the time.
The existing 4 policies in my documentation should take over the management of the password cycles etc after the first initial policy runs.
Ideally this would be built into a build script or whatever sort of enrollment workflow you use.

Your question about Filevault is a good one which I still haven't tested fully yet so I would test it out before you roll this out to an estate. This version I've sent you isn't public yet as I do need to test it on filevault devices and check the behaviour is ok.

@jelockwood
Copy link
Author

@PezzaD84
Ok, I made a clone of the "LAPS | Create local admin & password" policy to run once per computer as the first stage with the two extra fields filled in. I scoped this to one Mac and ran the policy and I see it has filled in the CryptKey and Secret extension attributes but not the Reset attribute.

I enabled the other policies and did another Jamf Policy command, I can see it changed the value of the extension attributes and I then decoded the new value and checked this on the Mac for the user account in Terminal, System Preferences etc, and also checked it still had a secure token and volume owner ship.

All good so far, it was changed, the decoded values worked, it still has a secure token and volume owner ship so FileVault should also be unaffected.

I am however struggling with Jamf Self Service on my test Mac. It is crashing when launched.

You provide a laps.pkg installer, what does this install and where? If it is an app to retrieve the password I could manually run it if I know where it is. (I have installed separately Swift Dialog.)

On a different Mac I am able to successfully via Jamf Self Service use the LAPS Self Service option to retrieve and decrypt a value.

@jelockwood
Copy link
Author

@PezzaD84
Rebooting the test laptop seems to have fixed Jamf Self Service. 👍

So, looking good.

@jelockwood
Copy link
Author

@PezzaD84
Oops seems to be a problem.

Whilst the first run seemed to successfully update the Jamf extension attributes and the actual account, I did end up unable to login with that account. I rebooted in to recovery and reset it back to the original default password and was able to login. Now it is running the first one time policy and setting the extension attributes to new randomised values but DOES NOT actually change the account password. I cannot see any error messages, the laps decrypt policy decrypts successfully but testing via Terminal, screen saver and System preferences suggests it did not actually change the account password.

I flushed the policy so it would run again as a first time command and I also emptied the extension attributes first.

Can you add an additional log command to show whether your script has actually changed the account password. It looks like the first run is now only generating new values and not changing anything.

Here is what the policy results indicate

Executing Policy LAPS | Create local admin & password copy

Downloading LAPS.pkg...
Downloading https://euw2-jcds.services.jamfcloud.com//download/5d57d9e24b2845be8c4ea1db67852a4e/LAPS.pkg...
Installing LAPS.pkg...
Successfully installed LAPS.pkg.
Running script LAPS FOR JAMF - Create & Cycle Password Script...
Script exit code: 0
Script result: Password length has been set to 12 charactersA Special character has been set in the password Log already exists. Continuing setup..... ***** LAPS Account cycled 21/06/2023 14:45:16 Existing Local Admin account was specified. Skipping account creation.... 132132CryptKey and SecretKey Escrowed to Jamf successfully Device serial is C02F9RU3Q6L4 JAMF ID is 132 LAPS Configuration was successful No slack URL configured
Running Recon...
Retrieving inventory preferences from https://gpub-jamf-01.gherson.com:8443/...
Locating accounts...
Locating package receipts...
Searching path: /System/Applications
Locating software updates...
Locating printers...
Gathering application usage information from the JamfDaemon...
Searching path: /Applications
Locating hardware information (macOS 13.4.0)...

@jelockwood
Copy link
Author

@PezzaD84
Sorry to nag, I was wondering if you had any thoughts regarding the script appearing not to actually change the user password even though it is filling in the extension attributes - as per Jamf result above.

@jelockwood
Copy link
Author

@PezzaD84
Ok, I think I have spotted why it did not re-run successfully.

I had deliberately restored the initial default password, I had cleared the extension attributes and I had flushed the one-time policy. It was then not doing a password change but did set the extension attributes. It turns out your script logic has a check to see if the log file already exists, if it does then it skips changing the password but does set the extension attributes and hence gets out of sync.

Normally this one time execution for an existing local admin with a default password will only ever be run once but you might want to logic at your script logic regarding this.

I then cleared the log file and the other stuff tried again and this time it has both set the extension attributes and changed the password for the account from the default initial value.

I can now double check FileVault etc. as I was intending 😄

@PezzaD84
Copy link
Owner

Hi @jelockwood

Sorry for the delayed reply work has been hectic!

So yes thats why I'm currently running the new script as a "run once" because otherwise it will try to reset the existing local admin password and ends up putting the whole thing out of sync.
Need to look at some additional logic to manage this feature but at the moment it has to be set to run once and then the existing policies take over.

@jelockwood
Copy link
Author

@PezzaD84
Ah, so effectively I need the new script in Jamf as a run once, and your 'official' version as the create/cycle script.

This was what I was starting to suspect based only my results. I will try this and see how it goes. Good thing I had not rolled it out to other Macs. 😉

@jelockwood
Copy link
Author

@PezzaD84
I had thought if I ran a copy of the beta script ONCE with the two extra parameters supplied, and then ran that same script in a different policy without those parameters this would achieve the desired goal.

However it seemed with this to be working for the very first run, but after that it constantly marked the password as needing a reset because whilst it updated the extension attributes it did not actually change the account password. Hence the extension attributes got out of sync with the actual password.

I am now using the beta script for the first run only, and the original script for subsequent runs. I have set the interval to daily but even so it will take two or three days to see if this is working.

I am still suspicious that it is going to keep marking the account as needing to be reset but again it will on subsequent runs update the extension attributes but fail to actually change the password. I will repeat the testing to see what happens.

I presume that if the LAPS Reset attribute contains 'yes' then it indicates it needs to reset the password and thinks it is out of sync. Currently it is in sync - I have test both the account password and the keychain passwords and I have made sure this extension attribute is currently empty.

@jelockwood
Copy link
Author

@PezzaD84
Ok, it has run again and it did change the extension attributes, it did also successfully change the account password and it did also successfully change the login keychain password.

What it failed to do is change the password for the local items keychain, or at least it has ended up with a password that does not work. For the local items keychain none of the original password, the rest set password, nor the second set password work. For the login keychain the second set password is working.

It also ended up with the LAPS Reset Password extension attribute set to 'Yes' meaning that next time it runs it will reset the account.

So, a couple of related faults there. This is with the first time run being the beta script and the subsequent runs being the released script.

@jelockwood
Copy link
Author

@PezzaD84
Here is the log, maybe this will shed some clues.
LAPS.log

Repository owner deleted a comment from TramsGuardian Jun 28, 2023
@PezzaD84
Copy link
Owner

Thanks for the feedback and the logs.
From the log the decoding of the password is setting the reset password EA to Yes so thats expected behaviour. The keychain stuff is a little worrying as I thought this could be an issue.

When the password is cycled the script is meant to delete the existing keychain and so a new one will be created at log in but clearly it's not clearing all the keychain items.

I will have to do some more testing to see whats going wrong and where.

@jelockwood
Copy link
Author

@PezzaD84
It is possible to change the Login keychain via a terminal/script command

security set-keychain-password -o oldpassword -p newpassword
or
security set-keychain-password -o oldpassword -p newpassword path-to-keychain

However I get the impression that the dreaded local items keychain cannot be manipulated at all at least by the same security command. As I am sure you are aware the Login keychain is at

~/Library/Keychains/login.keychain.db

and the local items keychain is at

~/Library/Keychains/[UUID]/keychain-2.db.*

It would be preferable to preserve this keychain but for the LAPS account it is les important than a user account so if needed just deleting the local items keychain sub-folder would be ok.

It could be Apple have changed things in new macOS versions but previously I used the LAPS solution here - https://github.com/NU-ITS/LAPSforMac

When I last used it I do not recall it causing any keychain problems. It would therefore be worth having a look at its script. Be aware its code for reading and modifying its extension attribute is out of date, as an example its xpath command needs changing on Big Sur and later. You only need to look really at its code for changing the user/keychain passwords and see if it works better.

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

No branches or pull requests

2 participants