Is anyone doing anything similar with Casper to Microsoft's LAPS

cmarker
Contributor

The objective being to set a randomized password for a local account on each computer then save the result in the JSS as an extension attribute.

I know there is a FR that asked for this a month ago, but I was wondering if anyone out there has implement this themselves on their Mac with a combination of scripts and EA or some other solution.

8 REPLIES 8

Josh_Smith
Contributor III

I haven't seen anything. I don't think it would be too tough to do in one script:

1) generate random password
2) change local admin password to random password
3) put the random password in the JSS with the API (computerextensionatributes)
4) run monthly/weekly/daily as desired

alexjdale
Valued Contributor III

It's definitely doable, but the password will not be escrowed as securely as with Microsoft's solution. We're looking at doing something similar, possibly outside the JSS though.

My idea was to have an algorithm that generates the random password using a salted hash of a known quantity like the serial number of the system. A script sets that password for each system using that same algorithm. The tech could visit a site, enter the serial number, and the site would return the password for that system. Passwords are unique to each system, and we can change the salt to change the passwords. This way we don't need to worry about password storage.

This would be a great JSS feature: randomized password for a local administrator account that is stored as securely as the FV recovery key, and its retrieval is audited and it can be easily rotated.

Josh_S
Contributor III

cmarker
Contributor

This is the solution I've come up with so far for anyone interested, the part with FileVault not updating the changed password was the most painful part of the whole thing.

#!/bin/sh



#These variables are passed in from the JSS Policy, they can instead be hard coded if desired.
passphrase=$4
adminAccount=$5
oldAdminpassword=$6


#Determine if FileVault is currently on.
#This sets a variable to determine if the reset steps need to be done later.
fvstatus=$(fdesetup status)
if [[ $fvstatus == *"On"* ]]
then
    FVReset="yes"
else 
    FVReset="no"
fi

#Check to see if the script has run before.
#If it has run before, it will copy the encrypted file to make a backup in the same location
#If not, it will use the password that you have specified for the previous NON Random local admin password
if [ ! -f "/var/root/admpass.txt" ]
then
    lastpass=$oldAdminpassword
else
    cp /var/root/admpass.txt /var/root/lastpass.txt
    lastpass=$(openssl aes-256-ecb -d -in /var/root/lastpass.txt  -k $passphrase)
fi

#Generate the random password, output to file and encrypt
env LC_CTYPE=C tr -dc "a-zA-Z0-9-_?" < /dev/urandom | head -c 16 | openssl aes-256-ecb -out /var/root/admpass.txt -k $passphrase

#Decrypt the file and save the new password
newPass=$(openssl aes-256-ecb -d -in /var/root/admpass.txt  -k $passphrase)

#This checks to see if FileVault is enabled or not.
#When File Vault is enabled, the script add a temporary user then gives that user permissions to unlock the drive.
#Once the temp account has been added, the Local Admin Account is removed from FileVault.
#The Local Admin Account's Password is then set to the new random string.
#Then the Local Admin Account is added back to FileVault, using the password specified with the Temp Account.
#The Temp Account is then removed from FileVault then deleted, removing the home directory as well.
if [[ $FVReset == "yes" ]]
then
    #Create a Temp Account
    jamf createaccount -username TempAccount -realname TempAccount -password $newPass

    #Add the Temp Account to the FV Users
    expect -c "spawn sudo /usr/bin/fdesetup add -usertoadd "TempAccount"; expect ":"; send "${lastpass}
" ; expect ":"; send "${newPass}
"; expect eof"

    #Reset the Local Admin Password
    jamf resetPassword -username temp-admin -password $newPass  

    #Remote Local Admin from FV Users
    fdesetup remove -user $adminAccount

    #Use Temp Account to Add Local Admin Account to FV Users
    expect -c "spawn sudo /usr/bin/fdesetup add -usertoadd "${adminAccount}"; expect ":"; send "${newPass}
" ; expect ":"; send "${newPass}
"; expect eof"

    #Remove Temp Account from FV Users
    fdesetup remove -user TempAccount

    #Delete the Temp Account
    jamf deleteaccount -username TempAccount -deleteHomeDirectory
#If FileVault is not enabled, the Local Admin Account password is simply reset instead.
else
    #Just Reset the Local Admin Password
    jamf resetPassword -username temp-admin -password $newPass
fi

#Output the reset date to a file, can be read into Casper with a simple Extension Attribute
echo $(date +"%D") > /var/root/PWResetDate.txt

mm2270
Legendary Contributor III

You'd be better off adding the account to FV2 using a plist as an input file than doing it with expect commands, but if it works for you...

alexjdale
Valued Contributor III

I've written something similar to @Marker.43's solution. It leverages the API to rotate the local admin password, encrypts it in a similar fashion, and writes it directly to the computer's extension attribute via the API. I am also storing the previous password and the dates they were set, so I can have them rotate automatically (via policy scoped to a smart group based on the last change date) once the date is >90 days. All parameters passed to the script (like API credentials) are also encrypted. No password is stored or transmitted in plaintext. It is not stored locally at all: I use the API because recon can be unreliable, and the script performs an API read on the password to validate it was stored correctly before actually changing it locally.

Support staff would run a Self Service policy and paste the UDID of the system into a dialog, which would use the API to get and then decrypt the password to present to the user. After retrieving the password, it is marked for an early rotation in about a day. I capture the number of times the password was retrieved and the date it was last retrieved. Policy logs capture the user who ran the retrieval tool, on which computer, for which computer, and when.

I just wrote it this week so it's just a POC for now. It's working quite well but I would much rather have JAMF do the heavy lifting. :-)

colincorbin
New Contributor II

Is there any chance that I could have a sanitized copy of your script Alex?

JasonkMiller
New Contributor

Hello All,

This is a similar conversation to this one. https://jamfnation.jamfsoftware.com/discussion.html?id=16977 Please check out what's there and see if that will help solve some of your problems or give you an idea of where you can start to take the script.

#!/usr/bin/python

import subprocess
import random
import string
import sys
import os
import plistlib
import xml.etree.ElementTree as ET

#user is not really needed, can be removed.
user = 'helpdesk'
UserName='YourAPIUsername'
PassWord='YourAPIPassword'
jss_url = 'https://jss.yourcompany.com:8443'

def random_password():
    size=8
    chars=string.letters + string.digits + '$'
    random.seed = (os.urandom(1024))
    new_password = ''.join(random.choice(chars) for i in range(size))
    logging("New Password for helpdesk: %s" % new_password)
    return new_password
def change_password(random_password):
    passwd = subprocess.Popen(['/usr/bin/dscl', '.', '-passwd', '/Users/helpdesk', 'password', random_password])
    (err, out) = passwd.communicate()
    if passwd.returncode == 0:
        logging("Password Change Successful")
    else:
        logging(random_password)
        logging(passwd.returncode)
        sys.exit('Failed to change Password')
    fdeRemove =  subprocess.Popen(['/usr/bin/fdesetup', 'remove', '-user', 'helpdesk'])
    (err, out) = fdeRemove.communicate()
    if fdeRemove.returncode == 0:
        logging("Successfully removed from FileVault")
    else:
        logging(fdeRemove.returncode)
        sys.exit('Failed to remove user from FileVault')
    the_settings={ 'Username': "Temp",
                 'Password': "Password1!",
                 'AdditionalUsers': [ { 'Username': 'Helpdesk', 'Password': random_password } ],
                }
    input_plist = plistlib.writePlistToString(the_settings)
    fdeUpdate = subprocess.Popen(['/usr/bin/fdesetup', 'add', '-inputplist'], stdin=subprocess.PIPE)
    (err, out) = fdeUpdate.communicate(input_plist)
    if fdeUpdate.returncode == 0:
        logging("Sucessfully added helpdesk back to FileVault")
        update_JSS(random_password)
    else:
        print fdeUpdate.returncode
        sys.exit('Failed to add user to FileVault')
def update_JSS(new_pass):
    get_machine_serial()
    PUTxml = '''<computer>
        <extension_attributes>
            <attribute>
                <name>Helpdesk Password</name>
                <value>%s</value>
            </attribute>
        </extension_attributes>
    </computer>''' % new_pass
    request = subprocess.Popen(['/usr/bin/curl', '--header', '''Content-Type: text/xml; charset=utf-8''', '--request', 'PUT', '--data', PUTxml, '--insecure', '--user', UserName + ':' + PassWord, jss_url + '/JSSResource/computers/id/' + computer_id])
    (err, out) = request.communicate()
    if request.returncode == 0:
        logging("Communication Successful")
    else:
        logging("Failed to communicate with JSS")
        sys.exit('Failed to communicate with JSS')


def get_machine_serial():
    serial_number = subprocess.Popen(["ioreg", "-l"], stdout=subprocess.PIPE)
    serialout, serialerr = serial_number.communicate()
    lines = serialout.split('
')
    raw_line = ''
    for i in lines:
        if i.find('IOPlatformSerialNumber') > 0:
            serial_number = i.split('=')[-1]
            serial_number = serial_number.strip()
            serial_number = serial_number.strip('"')
            get_machine_id(serial_number)
def get_machine_id(serial):

    request = subprocess.Popen(['/usr/bin/curl', '--header', '''Content-Type: text/xml; charset=utf-8''', '--request', 'GET', '--insecure', '--user', UserName + ':' + PassWord, jss_url + '/JSSResource/computers/serialnumber/' + serial], stdout=subprocess.PIPE)
    (stdout, stderr) = request.communicate()
    if request.returncode == 0:
        logging("Communication Successful")
    else:
        sys.exit('Failed to communicate with JSS')

    JSSResponse = str(stdout)
    xml = ET.fromstring(JSSResponse)
    for id in xml.iter('id'):
        global computer_id
        computer_id = id.text
        break
    return computer_id
def logging(text):
    file = '/var/log/Helpdesk_Password_Log.log'
    if not os.path.exists(file):
        f = open(file, 'w+')
    else:
        f = open(file, 'a+')

    f.write(str(text) + '
')
    f.close()

def account_removal():
    removal = subprocess.Popen(['/usr/bin/dscl', '.', 'delete', '/users/temp'])
    (stdout, stderr) = removal.communicate()
    if removal.returncode == 0:
        logging("Temp account removed")
    else:
        logging("Failed to remove temp account")
        sys.exit("Failed to remove account")


# Start of Main Code:
change_password(random_password())
account_removal()