Why Jamf encrypted script parameters break on modern macOS
A policy that worked for years suddenly logs 'bad magic number' after a macOS update. Here's why LibreSSL breaks Jamf's encrypted script parameters — and the openssl flags that fix it for good.
A policy that had run fine for a year started failing on freshly-upgraded Macs. The script used Jamf’s Encrypted Script Parameters to stash an API account password, and on the new machines every Jamf API call came back as the “Unauthorized” HTML login page — so the script blew up trying to parse XML. The real culprit was one line of openssl output buried at the top: bad magic number.
The setup
Encrypted Script Parameters is the standard way to keep a secret out of a Jamf script in cleartext. You encrypt the secret once with openssl, store the encrypted string, the salt, and the passphrase as script parameters, and the script decrypts the secret at runtime. The decrypt line looks like this:
pass=$(echo "$4" | openssl enc -aes256 -d -a -A -S "$5" -k "$6")
Parameter positions vary from script to script — the log-collection script I hit this on used different ones — but the shape is always the same: -S "$salt" -k "$passphrase".
The symptom
On macOS Ventura and later, that same decrypt returns:
bad magic number
prepended to (or instead of) the password. So $pass ends up empty or garbage. Every authenticated curl to the Jamf API then returns the login/Unauthorized HTML page instead of XML, and any xmllint / xpath parsing dies with something like mismatched tag from the XML parser.
This is what makes the bug a time-sink: it looks like an API or permissions problem, but it’s a decryption problem. The credentials were never the issue.
Why it breaks
Check your openssl version on an old machine and a new one:
openssl version
The encrypted blob in my case was generated years earlier on LibreSSL 2.8.3 (Monterey). Back then, passing an explicit salt with -S wrote the raw ciphertext without the Salted__ header that openssl normally puts at the front. LibreSSL 3.x (Ventura and up — I was on 3.3.6) expects that Salted__ header at decryption time, even when you also pass -S. No header, no decryption: bad magic number.
You can confirm it directly. Decode the blob and look at the first eight bytes:
echo "$ENCRYPTED" | openssl base64 -d -A | xxd | head -1
If they aren’t 53 61 6c 74 65 64 5f 5f — that’s Salted__ in ASCII — your blob has no header, and modern LibreSSL won’t decrypt it with -S.
The fix: stop using -S
The robust, version-independent approach is to let openssl manage the salt itself. Encrypt without -S, so the salt and header are embedded in the output:
echo "my-secret" | openssl enc -aes256 -a -A -k "$PASSPHRASE"
and decrypt without -S, reading the salt back from that embedded header:
pass=$(echo "$ENCRYPTED" | openssl enc -aes256 -d -a -A -k "$6")
The separate salt parameter is now unnecessary — you can drop it entirely. Because the salt travels inside the ciphertext, this behaves the same on every LibreSSL version you’ll meet in the field. Re-encrypt your secrets once in this format, update the script and the policy’s parameters, and upgrade-day breakage goes away for good.
If you can’t re-encrypt right now
Maybe that blob is wired into a dozen policies and you just need today’s run to work. As long as you still have the salt and passphrase, you can rebuild the header openssl is looking for and decrypt the old blob as-is:
{ printf 'Salted__'; printf '%s' "$SALT" | xxd -r -p; \
echo "$ENCRYPTED" | openssl base64 -d -A; } \
| openssl enc -aes256 -d -k "$PASSPHRASE"
That prepends Salted__ plus the 8-byte salt to the header-less ciphertext — exactly what modern LibreSSL wants. Treat it as a bridge, then migrate to the no--S format when you have a moment.
Takeaway
bad magic number from openssl enc almost always means the ciphertext header doesn’t match what your current openssl expects — usually because it was encrypted on a different version. In Jamf land, the fix is to stop pinning the salt with -S and let openssl embed it. Encrypt once in the portable format, and your encrypted parameters stop being a macOS-upgrade landmine.
Subscribe to gen/os
New write-ups on Apple device management — Jamf, Intune, Mosyle, scripting, and automation. Straight to your inbox, no spam, unsubscribe anytime.
Found this useful? Subscribe via RSS for new posts, orget in touch if I got something wrong.
Comments