Microsoft LAPS (Local Admin Password Solution)

jleomcdo
Contributor

MS LAPS
MS has released a "tool" that allows you to randomly change the local admin password and then store it in AD.
We are starting to use this at our company on the Windows machines. I'm looking for a way to do the same type of thing on the Mac's.

We are running OS X 10.10.5 and JAMF 9.72 and all of our macs are bound to AD.

Any thoughts?

7 REPLIES 7

nwiseman
Contributor

I would be interested in this as well. I know there was a feature request for this. https://jamfnation.jamfsoftware.com/featureRequest.html?id=2640 but it seems to show as "duplicate" now where at one time it showed up as "under review". Would love to know JAMF's stance on this.

jleomcdo
Contributor

I'm reaching out to our TAM to see if he has any ideas. I also contacted Apple Support to see if there is any built in tools in the OS that could help. I will update this post, once I find out anything.

jleomcdo
Contributor

Here is what Apple Support said:
"We do not have the ability to use the LAPS tool natively with OS X as the built in Active Directory plug-in does not support GPOs. You may be able to utilize a third party Active Directory plugin such as Thrusby's AdmitMac, Centrify or PowerBroker which support GPOs. You will want to check with the third party plugin vendor to verify if their product supports the use of LAPS."

JasonkMiller
New Contributor

I am looking forward to JAMF or potentially Apple trying to implement a LAPS like tool for OS X. I am not holding my breath for Apple to implement this tool however, maybe JAMF can provide this for us. In the meantime I have been talking in the Macadmins Slack Channel and a few of the users there have had this game concern and developed their own solution. I reached out to one individual who provided me with his version of LAPS but it is undocumented, and is meant to be played around with and tweaked for your environment. Check it out and see if it works for you. Check it out and see if it meets your need until something is developed by JAMF potentially.

#!/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()

Josh_S
Contributor III

Just a note on using this script. While it is helpful, it does allow anyone with read access to computer objects to view the "Helpdesk" password in cleartext which, if it allows someone to decrypt a machine, can be used to potentially compromise the machine. This includes any of your techs, any local accounts you've created for API requests, as well as if any of those passwords are compromised. It's also not audit able.

Make sure you critically examine the potential security implications before putting this script, or any similar solution, into your production environment. It might be better if you encrypt the password, prior to uploading it, using an accessible public key...

ryan_s
New Contributor II

Would it be possible to write a value to a particular pre-existing AD attribute, while also securing this value in-flight [to AD]? Basically I am wondering if we could randomize a password, have it write to the same attribute that LAPS utilizes and thus, use LAPS lookup methods across the board for Windows and Macs.

It seems simple, but I am positive this would be complex to implement -- curious if its a possibility!

djwojo
Contributor

Curious if anyone has had luck with these or other methods?