Local admin management

asher_wilkinson
New Contributor III

Is there a tool to manage local admin accounts similar to a LAPS tool that doesn't depend on Active Directory to manage the password?

I checked out the tool at the following link, which looked promising, but it appears dependent on AD to manage the password:

https://github.com/joshua-d-miller/macOSLAPS

Is there a tool like this that operates independently of an AD environment?

8 REPLIES 8

mark_mahabir
Valued Contributor

Yes, take a look at LAPSforMac.

asher_wilkinson
New Contributor III

@mark.mahabir Thanks! I have one concern with this option:

"As currently designed, this solution creates a local Admin account on every Mac enrolled into Casper and stores the account password in the Mac's inventory record as an Extension Attribute. "

It doesn't say it right out, but does it store that password in plaintext?

sshort
Valued Contributor

@asher.wilkinson Yep, it's in plaintext like any extension attribute. My org uses the same LAPS solution, and we just make use of the built-in Jamf user permissions to limit access to viewing or editing that field. Not everyone needs to have full admin access.

asher_wilkinson
New Contributor III

@sshort Thanks! That might work.

asher_wilkinson
New Contributor III

@sshort Nevermind. I just got a swift, hard no on that. Plaintext being a considerable security vulnerability, I guess it makes sense.

tfenna
New Contributor II

@asher.wilkinson I've actually just finished creating somehting like this.

It might not be exactly what you're looking for, as I doubt we're using the same password management tool, though hopefully it will spark a few bright ideas.

In our case we're using Password State as our password management tool, which has it's own API. You should be able to adapt this for use with another password management tool if it has it's own API (annoyingly not all of them do).
Our supporters then have access to Password State in case they need to retreive the local admin password.

I've added some comments into the script rather than trying to explain how it all works the description.

Seeing as we're doing this in the name of security, you could also look at going one step further and using encrypted strings for the old admin password and the API key within the script.

#!/bin/bash

# PASSWORD RANDOMISER

# README:
# This script will generate a random base64 string using OpenSSL.
# The string is then used as the new password for the Mac.
# New password is set using sysadminctl and the password
# is sent to Password State via the Password State API.


# Version 0.1 - Script created / Toby Fenna / 09/07/2019
# Version 0.2 - Logging added
# Version 0.3 - Functions 'currentWifi' & 'vpnState' added - for troubleshooting / information only

# Commands #
_AWK_="/usr/bin/awk"
_CURL_="/usr/bin/curl"
_DATE_="/bin/date"
_ECHO_="/bin/echo"
_SCUTIL_="/usr/sbin/scutil"
_PING_="/sbin/ping"
_DATE_="/bin/date"
_PS_="/bin/ps"
_TOUCH_="/usr/bin/touch"
_NETWORKSETUP_="/usr/sbin/networksetup"
_JAMF_="/usr/local/bin/jamf"
_SYS_PROFILER_="/usr/sbin/system_profiler"
# Commands #

# Edit below as required #
_JSS_URL_='https://yourOrg.jamfcloud.com'
_USERNAME_="yourJamfApiUserName"
_PASSWORD_="yourJamfApiPassword"

_LADMIN_USERNAME_="your_admin"
_OLD_ADMIN_PW_="your_admin_password" # Enter the current password of your local admin / management account

_PASSWDSTATE_URL_="https://pass.corp.yourOrg.com"
_PASSWDSTATE_API_="yourPasswordStateApiKey"
_PASSWORD_LIST_="7913" # The ID of your Password State list
# Edit above as required #

# Logging #
exec 2>&1
# Logging #

# Title of Script
_TITLE_=$(basename "$0")

toLog() {

# Get Process info
_PID_=$($_PS_ aux | grep "[j]amf policy" | sed -n '1p' | awk '{print $2}')

# Log to /var/log/jamf.log
$_ECHO_ "$(timestamp) $(computer) jamf[$_PID_]: Executing $_TITLE_" >> /var/log/jamf.log

}

timestamp() {

  $_DATE_ +"%a %b %d %I:%M:%S"

}

computer() {
  # Get name and save as upper case

  $_SCUTIL_ --get LocalHostName | awk '{print toupper($0)}'

}

currentWifi(){
    # Echo out the current wireless network. Not required, just for troubleshooting

    _INTERFACE_=$($_NETWORKSETUP_ -listallhardwareports | grep Wi-Fi -1 | grep Device | awk '{print $2}')
    _CURRENT_WIFI_=$($_NETWORKSETUP_ -getairportnetwork $_INTERFACE_ | grep 'Network:' | sed 's/^.*: //')

    $_ECHO_ "$(timestamp): Current wireless network is: $_CURRENT_WIFI_"

}

vpnState() {
  # Check if VPN (Cisco AnyConnect) is installed and connected
  # This isn't required, it just helps support to troubleshoot if anything was to go wrong

  $_ECHO_ "$(timestamp): Checking if Cisco AnyConnect is installed and connected"

  if [[ -f "/opt/cisco/anyconnect/bin/vpn" ]]; then
    $_ECHO_ "$(timestamp): AnyConnect is installed"
    _STATE_=$(/opt/cisco/anyconnect/bin/vpn state | grep state | awk '{print $4}' | sed -n 1p)
      if [[ $_STATE_ != Disconnected ]]; then
        $_ECHO_ "$(timestamp): VPN connected"
      else
        $_ECHO_ "$(timestamp): VPN is disconected"
      fi
  else
    $_ECHO_ "$(timestamp): AnyConnect is not installed"
  fi
}

checkConnection() {

  # Echo the current wireless network. Not required, just for troubleshooting
  $_ECHO_ "$(currentWifi)"

  # Checks that a stable connection is available to Password State & Jamf
  # Script exits at this point if no connection is available
  _PWSTATE_AVAILABILITY_=$($_PING_ -c 4 pass.corp.yourOrg.com 2>&1 | grep bytes | wc -l | sed -e 's/^[ 	]*//')
  _JAMF_CHECK_=$($_JAMF_ checkjssconnection | grep -w -o "available")

  if [[ $_PWSTATE_AVAILABILITY_ -gt 2  ]] && [[ $_JAMF_CHECK_ == "available" ]]; then
    $_ECHO_ "$(timestamp): Connection to Password State OK. $_PWSTATE_AVAILABILITY_ succesful pings. "
    $_ECHO_ "$(timestamp): Connection to Jamf OK. Status: $_JAMF_CHECK_."
  else
    $_ECHO_ "$(timestamp): Connection tests failed."
    $_ECHO_ "$(timestamp): Number of succesfull pings to Password State: $_PWSTATE_AVAILABILITY_"
    $_ECHO_ "$(timestamp): Jamf connection status - Blank means failed: $_JAMF_CHECK_"
    exit 0
  fi

}

randomPw() {

  $_ECHO_ "$(timestamp): Generating random base64 string. To be used as new password."

  # Generates a random 12 character base64 string to be used as the new admin password
  _RANDOM_PW_=$(openssl rand 10 -base64 | tr -d +!%=/ | head -c12;echo)

}

currentPw() {

  # Checks if a password record exists for the computer in Password State. This needs to run first, as if a record does not exist, the API call will just return results of other devices
  _TITLE_PRESENT_=$($_CURL_ -s -X GET "$_PASSWDSTATE_URL_/api/searchpasswords/$_PASSWORD_LIST_" -H "APIKey: $_PASSWDSTATE_API_" | grep -w -o "$(computer)")

  if [[ $_TITLE_PRESENT_ == "$(computer)" ]]; then
    $_ECHO_ "$(timestamp): Record present for $_TITLE_PRESENT_ in Password State."
    _CURRENT_PASSWORD_=$($_CURL_ -s -X GET "$_PASSWDSTATE_URL_/api/searchpasswords/$_PASSWORD_LIST_?title=$(computer)" -H "APIKey: $_PASSWDSTATE_API_" | sed -n 's|.*"Password":"([^"]*)".*|1|p')
    _PASSWORDID_=$($_CURL_ -s -X GET "$_PASSWDSTATE_URL_/api/searchpasswords/$_PASSWORD_LIST_?title=$(computer)" -H "APIKey: $_PASSWDSTATE_API_" | sed -n 's|.*"PasswordID":([^"]*),.*|1|p')
    $_ECHO_ "$(timestamp): The Password State PasswordID is: $_PASSWORDID_"
    _RECORD_FOUND="YES"
  else
    $_ECHO_ "$(timestamp): No password currently found in Password State."
    _RECORD_FOUND="NO"
  fi

}

resetPw(){
  # Let's randomise!

  $_ECHO_ "$(timestamp): Randomising password... ¯_(ツ)_/¯"


  if [[ $_TITLE_PRESENT_ == "" ]]; then # If this is the first time the password is being randomised, it will use the original local admin account password to change it
    sysadminctl -adminUser $_LADMIN_USERNAME_ -adminPassword $_OLD_ADMIN_PW_ -resetPasswordFor $_LADMIN_USERNAME_ -newPassword $_RANDOM_PW_
    $_ECHO_ "$(timestamp): Password being randomised and reset for the first time (◕‿◕)"
  elif [[ $_TITLE_PRESENT_ == $(computer) ]]; then # If the password has been changed previously, it will use that currnet/previous password stored in Password State to set the new one
    sysadminctl -adminUser $_LADMIN_USERNAME_ -adminPassword $_CURRENT_PASSWORD_ -resetPasswordFor $_LADMIN_USERNAME_ -newPassword $_RANDOM_PW_
    $_ECHO_ "$(timestamp): Password randomised and reset (ᵔᴥᵔ)"
  else
    $_ECHO_ "$(timestamp): Hmm, something went wrong here, if the password was being randomised for the first time, check that the original local admin password works locally on the Mac ( .-. )"
  fi

}

cleanChain() {
  # Deletes the login keychain for the local admin account
  # sysadminctl won't update the Keychain

  if [[ -d /private/var/$_LADMIN_USERNAME_/Library/keychains ]]; then
    $_ECHO_ "$(timestamp): Deleting login Keychain for $_LADMIN_USERNAME_."
    rm -rf /private/var/$_LADMIN_USERNAME_/Library/keychains
    $_ECHO_ "$(timestamp): $_LADMIN_USERNAME_ login Keychain deleted."
  fi

}

uploadNewPw() {
  # Inform Jamf of new management account password (if you are resetting the managemnt account password)- Jamf remote will not work without this
  # If you have a separate local account for support and Jamf remote (management account) then you may need to remove this step
  # Only use this if you truly are resetting and randomising the management account password. If you don't remove / keep this as necessary, Jamf remote may not work!
  $_ECHO_ "$(timestamp): Updating Jamf management account password via Recon."
  $_JAMF_ recon -sshUsername $_LADMIN_USERNAME_ -sshPassword $_RANDOM_PW_ > /dev/null
  $_ECHO_ "$(timestamp): Recon complete."

  # Update Password State with the new management account password
  if [[ $_TITLE_PRESENT_ == "" ]]; then
    $_ECHO_ "$(timestamp): No record present for $(computer) in Password state. Creating new record..."
    $_CURL_ -s -X POST "$_PASSWDSTATE_URL_/api/passwords" -H 'content-type: application/json' > /dev/null 
    -d '    {
          "PasswordListID":"'"$_PASSWORD_LIST_"'",
          "Title":"'"$(computer)"'",
          "Description":"Uploaded via API | Last updated '"$($_DATE_ +"%v %I:%M:%S")"'",
          "UserName":"'"$_LADMIN_USERNAME_"'",
          "password":"'"$_RANDOM_PW_"'",
          "APIKey":"'"$_PASSWDSTATE_API_"'"
      }'
  elif [[ $_TITLE_PRESENT_ == $(computer) ]]; then
    $_ECHO_ "$(timestamp): Uploading new password for $(computer) to Password State..."
    $_CURL_ -s -X PUT $_PASSWDSTATE_URL_/api/passwords -H 'Content-Type: application/json' > /dev/null 
    -d '    {
          "PasswordID":"'"$_PASSWORDID_"'",
          "Description":"Uploaded via API | Last updated '"$($_DATE_ +"%v %I:%M:%S")"'",
          "password":"'"$_RANDOM_PW_"'",
          "APIKey":"'"$_PASSWDSTATE_API_"'"
          }'
  else
        $_ECHO_ "$(timestamp): Not sure how I ran this far.. But something really messed up if you're reding this!"
  fi

}

checkSuccess(){
  # Checks that the new password was succesfully uploaded to Passwortd State

  # Re-runs currentPw to update the _CURRENT_PASSWORD_ variable
  currentPw > /dev/null 2>&1

  if [[ $_RANDOM_PW_ == $_CURRENT_PASSWORD_ ]]; then
    $_ECHO_ "$(timestamp): New password succesfully uploaded"
  elif [[ $_RECORD_FOUND == "NO" ]]; then
    $_ECHO_ "$(timestamp): Password failed to upload. No record found"
  else
    $_ECHO_ "$(timestamp): Something went wrong. Password may not have been changed."
  fi

}

toLog
vpnState
checkConnection
randomPw
currentPw
resetPw
cleanChain
uploadNewPw
checkSuccess

asher_wilkinson
New Contributor III

@tfenna Thanks a lot! And sorry for the late response. At a glance, this looks exactly like what I'm looking for! I'll see what I can do with it. Thank you so much!

francksartori
New Contributor III

You may also test EasyLAPS. I'm the author of this tool which is designed to regularly rotate the local administrator account password of a Mac and store it in a MDM like Jamf Pro or Jamf School.