DerFlounder posted Updated script for obtaining, checking and renewing Bearer Tokens for the Classic and Jamf Pro APIs recently. It features some ways to load the credentials needed to run the script. Rich’s examples tend to become the canonical way of doing a thing, and for good reason — they’re clear and he explains things in a way people can understand.
In the comments, NinjaFez asks:
“For someone only just staring to use the API to make site changes in JAMF for computers with a script deployed, it makes sense to start using barer tokens out of the gate for future. thanks for putting it all in once place. However trying to avoid static accounts and passwords in the script and/or on the mac, would it make sense to pre base64 the account and password and put them in the script parameters so its already there and not need to do the convert, pass it strait on to the token command?”
If you really, really have to run an API command from a user’s workstation, and you understand the risks, you’ve got the right idea. But a user can absolutely get that information even though you’ve done some things to make it a little less obvious.
Ideally, API commands are for scripts running on administrator workstations or IT automations. If you supply them to client machines in any form, they’re vulnerable. Is there any way to avoid that for your application? Maybe your script could just prompt the user to pick a site, write their answer to a text file, and then call jamf recon? You could have a script extension attribute that reads the value from that file. Of course, then you’d need an administrator-run script that goes through all the devices not in a site and re-assigns them based on the information in the site extension attribute. That not as slick and easy as having a device set its own site, but it’s a lot safer.
Now, how about keeping the API credentials safe on the admin machines where api scripts are usually written and executed?
Recognizing that there is no “one-size-fits-all” approach, Rich demonstrates several methods for obtaining the api credentials in his script. Techniques people use include:
- URL/User/Password variables at the top of the script
- Read them from environment variables
- Read them from a preferences file
- Put them in the keychain
- Prompt the admin to enter the secrets interactively
In the comments on derflounder’s post, Richard P. says he likes to store his api credentials in base64, though he acknowledges that it’s just obfuscation. Maybe that would only thwart a really novice attacker, but the main thing is that he’s thinking about security and adapting. He’s not proposing obfuscations as a substitute for real security.
So, what should you be doing?
Just to get it out of the way, the “correct” answer is of course “none of the above.” In enterprise-grade environments, sensitive services are run on hosts that are the least likely to be compromised and have managed security/audit controls. That means they’d never put production secrets on a user device… definitely never on any end-user devices, but also not even on an admin’s. Instead, these kinds of orgs run all their sensitive code on things like locked-down AWS lambdas with secrets stored as encrypted strings in Configuration Manager. These are accessed using service/user IAMs. But all that is out of reach to all but a few Mac admins.
Returning to the world where most of us live, if you care about security, there are a couple things we might put in the category of “hard rules”:
1) Don’t deploy scripts with global API credentials to your end-users’ devices (unless you understand that they are exposed and the risk is acceptable to you).
2) Don’t put your credentials right in your API script. Not that they’re any more secure if you put them in environment variables or another file, but it will save you the embarrassment and ensuing scramble when you accidentally disclose them when you share your script.
3) Don’t use a single account for multiple applications/employees. It needs to be super-simple fast and simple to remove accounts when people leave.
4) Create API users with only the bare-minimum permissions needed for each application. For example, if you’re writing a reporting script, why give the user write/update/delete permission to anything?
5) Don’t email/chat/text secrets. Use something like Thycotic or OnePassword for Teams, or a service like https://onetimesecret.com.
There are also some things that are maybe not “rules” but might be considerations.
- Any time you call something like curl it will appear with all its parameters in the process stack which anything with privileges can read. Watch out also for secrets leaking into your application logs, bash command line history, system logs, top/ps, etc. This is a bigger deal on multi-user systems than on your single-user Mac, but malware privilege escalations happen. On a system with multiple admins, we might prefer
curl --netrc-file <my-password-file>over
curl --user <user:pass>because that prevents the password from being passed on the command line and into the process logs. Other clients like MySQL and the AWS Command Line Utility have this capability as well.
- If you don’t want to store plaintext secrets on your computer, one of the options offered by Rich’s script prompts the user to enter the API credentials interactively. Some may choose to store the URL and username but prompt for the password. That offers some protection since there’s less chance of pulling the secrets from data at rest, but you still have to watch out for key loggers and the issues in the preceding bullet point.
We should start with the assumption that any secrets on your device are going to be stolen if you get hacked. It does not matter how you try to hide them. So, your best/only defense is… don’t get hacked. If we work with sensitive data, we should eat our own dog food. We should be doing all the same things we tell our users to do and not think things don’t apply to us because we’re smart. Go listen to https://darknetdiaries.com/transcript/86/.
- If you do nothing else, appreciate this: Apple knows what they’re doing. Help them help you. Do your software updates religiously. Use Private Relay and turn on your firewall. Use FileVault. Think before you give PPPC entitlements to an app. If your DLP vendor says you need to disable SIP to install their kernel extension, tell them you’d love to speak to them again once they’ve got their act together.
- Do not click on that link/attachment in that email you weren’t expecting even though it’s from your co-worker/mother. What’s your social media profile look like? Could a threat actor find their way to you if they wanted to target your organization?
- Don’t use the same machine you use for production work to surf questionable sites or download anything you don’t absolutely trust.
- Don’t install any CA certificates you don’t absolutely trust.
- Use good/unique passwords. Use two-factor wherever you can. Use a password manager.
The lengths you’ll go to protect things depends on your own risk assessment. Are you an admin for something like military or biotech? If so, you might be one of those admins who will never run against production from your Mac, or you’ll get a second/clean Mac used only for your packaging/device admin work and you’ll reset contents and settings between projects. Are you an admin for an elementary school? You may not be a prime target for state-sponsored attacks, but ransomware and crypto miners are often indiscriminate. Maybe at least think about turning on Software Update for your admin Mac.
If you’re not installing a curl credentials file, here’s a project where you can grab code snippets for a few different methods to supply api credentials to your scripts. Pick the way you like best, or use the shell
source command to incorporate it into your scripts as-is so you can try all the methods.
For example, if your script needs a server URL, username, and password for a production reporting account, you would just download the script in that GitHub folder and add this line at the top of your API script:
source "getJamfInfo.sh" --environment "keychain" --method "production_auditor"
Then you have $apiURL, $apiUser, and $apiPass variables to available for use within your script without having to put them directly into your code.