Munki Repo Security: Four Locks Between Your plists and Every Mac You Own

Hero image for Munki Repo Security: Four Locks Between Your plists and Every Mac You Own
PC Drama
151 views

Part of the Munki and plist Files: How One Folder of Text Serves Software for Mac Clients. Start there for the wide view, then come back here for the close-up.

Munki repo security comes down to four locks: HTTPS on the wire, client authentication, package hash checking, and control over who can write to the repo. A fresh install enables only the hash check, and only for packages that carry a hash. The rest is up to you, and it matters because a Munki repository is a folder of plain text plists on a web server, and every Mac in your fleet installs whatever those files tell it to. Anyone with the URL can read them. Anyone with the file share can rewrite them. Each lock is a documented preference key or a server directive, so none of them needs a purchase order. The table maps the four, and the sections after it cover each in turn.

Lock What it stops Where it lives Default
The wire Reading or rewriting manifests in transit HTTPS on the server, SoftwareRepoCACertificate, FollowHTTPRedirects Plain HTTP works; redirects are not followed
The client Any device with the URL pulling licensed software AdditionalHttpHeaders (basic auth) or UseClientCertificate Open to anyone who can reach the server
The payload A swapped or corrupted installer installer_item_hash plus PackageVerificationMode hash: checked only when a hash exists
The write path Anyone rewriting a manifest or catalog File share permissions, Git over SSH, repo plugins Whatever your file server allows
Four translucent glass layers stacked and offset above a dark grid, the top layer edged in cyan light
Four sheets, four locks. Each one guards a different question, and none of them can answer the others.

The rows do not cover for one another. HTTPS protects the wire but has no idea who is standing at the far end of it. Client authentication decides who may read the repo, and says nothing about whether the files were edited at rest. Package hashes prove the installer matches its pkginfo, but the pkginfo itself carries no hash, so a rewritten catalog passes every check Munki runs. That gap is why the fourth lock is the one Munki cannot set for you. It lives in your file server, your Git host, or your repo plugin, and it is only as strict as the write permissions you grant there.

Munki's own documentation states the limit directly. HTTPS "only secures the contents of the data on the wire as it passes from host to host" and "does not have the facility to control which hosts or users can access a site." Encryption seals the envelope without ever checking the guest list, which is what the three locks after it are for.

Lock the Wire: HTTPS, the CA Cert, and Redirects

Link to section: Lock the Wire: HTTPS, the CA Cert, and Redirects

Serve the repo over TLS and point the client at a certificate authority it already trusts. Running an internal CA? Munki looks for the CA bundle at /Library/Managed Installs/certs/ca.pem by default, or wherever SoftwareRepoCACertificate points, with SoftwareRepoCAPath on hand if you would rather offer a whole directory of certificates. The SoftwareRepoURL itself should begin with https://, and the moment client certificates enter the picture, it must.

Out of the box, Munki refuses to follow redirects at all. The FollowHTTPRedirects preference accepts none, https, or all, and the middle value is the sensible one. It lets a load balancer hand a client to another secure host and rejects any redirect that would downgrade the connection to plain HTTP. Set it to all and a single stray redirect can send your entire fleet to a server you have never met.

Prove the Client: Basic Auth or Certificates

Link to section: Prove the Client: Basic Auth or Certificates
A brass key resting on a dark desk beside a closed laptop, with a blank smart card lying behind it
A shared password or a per-device certificate. Both open the door; only one knows who walked through.

Munki gives you two ways to make the server ask "who are you?" before it hands over a manifest. One is a password the whole fleet shares. The other is a certificate issued to one device.

HTTP basic auth SSL client certificates
Client setting AdditionalHttpHeaders array carrying Authorization: Basic and a base64 username:password UseClientCertificate true, PEM at /Library/Managed Installs/certs/
Server side An .htpasswd file and Require valid-user in the repo's directory block Your own CA, a secure virtual host, and SSLRequire rules matching the certificate's DN
Per-device identity Only if you issue one password per client Built in; UseClientCertificateCNAsClientIdentifier can even pick the manifest from the cert
Revocation Change the password, redeploy the header Revoke the certificate at the CA
Setup effort Minutes The wiki's own demo scripts come with a disclaimer

Basic auth is what the documentation itself calls "much easier to set up," and paired with HTTPS and a signed server certificate it earns a "reasonably secure." The weak point is where that password is stored. Written to /Library/Preferences/ManagedInstalls.plist, the header is readable by every user on the Mac, so any of them can copy the credential and go window shopping in your repo. The documented fix is to write AdditionalHttpHeaders into root's preferences at /private/var/root/Library/Preferences/ManagedInstalls.plist, which non-admins cannot read, or deliver it in a configuration profile from your MDM. Either way, confirm the client sees it with sudo managedsoftwareupdate --show-config.

"Setting up an SSL-secured webserver (one that requires clients to have certificates to connect) is probably the way to go, but setting this up is tricky."

Greg Neagle creator of Munki, on the munki-dev list in October 2010, as archived at Google Groups

Certificates are the stronger lock because they name the device instead of passing around a shared secret, and Munki 7.3, released in August 2026, added ClientCertificateAcceptableCAs so a client can pick the right identity even behind load balancers that do not advertise their CA list. The wiki's client certificate walkthrough still carries a warning: the scripts are "only intended to exemplify" the setup, and the sample certificates are 1024 bits, which newer Apache builds reject. If your MDM can install identities, place the PEM that way and skip the scripts. If it cannot, basic auth in root's preferences is the sound choice. A plain lock that is turned beats a better one still waiting on a free weekend.

Verify the Payload: installer_item_hash and hash_strict

Link to section: Verify the Payload: installer_item_hash and hash_strict
A stream of violet particles settling into a glowing rectangular lattice of cells, half filled and half still forming
A SHA-256 hash proves the download is the download. It says nothing about who wrote the pkginfo that named it.

When makepkginfo or munkiimport builds a pkginfo, it writes an installer_item_hash, the SHA-256 fingerprint of the installer item. On the client, PackageVerificationMode decides what to do with that fingerprint. The default, hash, checks a download only when the pkginfo brought a hash along. hash_strict checks every download and fails any item that shows up without one. none skips the check entirely, and the FAQ files that option under "not recommended," which is documentation for "please don't."

The default is the polite setting. It never breaks an old pkginfo from before hashing was a habit. It also lets a hand-edited pkginfo with its hash key deleted install whatever now sits at that path, unchecked. Turn on hash_strict, then re-import anything that fails, and treat each missing hash as a finding to explain. One boundary to keep in view: the hash proves the installer matches the pkginfo. It does not prove the pkginfo, the catalog, or the manifest still say what you wrote. That job belongs to HTTPS in transit and to write permissions at rest.

PackageVerificationMode, mode by mode
Value Behavior When an item has no installer_item_hash
none No integrity check at all Installs
hash (default) Checks the SHA-256 when one is present Installs
hash_strict Checks the SHA-256 on every download Fails with an integrity error

An "integrity check failed" message means the downloaded item's SHA-256 does not match the pkginfo. The FAQ's first suggestion is to re-import the item so the hash is regenerated. Deleting the key to clear the error is taking the battery out of the smoke detector.

Guard the Write Path: Who Can Change the Repo

Link to section: Guard the Write Path: Who Can Change the Repo
Top-down view of a small network switch on a dark desk with three patch cables, one port lit green, and a brass key beside it
Clients read the repo over HTTPS while admins write to it over SSH, and Munki secures only the first of those two paths.

Clients read over HTTPS. Admins write somewhere else entirely, and that somewhere is the lock Munki leaves to you. The admin tools reach the repo through a repo plugin. The default, FileRepo, handles a local folder or a file share reached by an afp://, smb://, or nfs:// URL, so the write boundary is whoever can mount that share. An https:// repo URL means a custom plugin such as MWA2APIRepo, where the API's own authentication draws the line instead.

The project's Git guide is the cleanest answer to who gets to write. Serve the repo over HTTP or HTTPS, keep write access to the Git repository on SSH with key authentication, and let every manifest change arrive as a commit with a name on it. The guide lists one benefit that works as a security control: "a change in the repo is not immediately available to clients." A review step between commit and deploy is the distance between a typo and an incident. Its bluntest rule covers the other failure: "Version control is not an alternative for backups. And backing up is not an alternative for version control. Use both."

Serve From the Cloud Without Opening the Bucket

Link to section: Serve From the Cloud Without Opening the Bucket

A repo in S3, Google Cloud Storage, or Azure Storage needs a signature the bucket will honor, and that is what Munki middleware is for: a module that "can alter an HTTP(S) request to work with servers (often cloud-based) that require specific headers, keys, or encrypted/signed requests." Only a single module loads. In Munki 7 it is a .plugin dylib in /usr/local/munki/middleware. Back in the Python era it was a middleware*.py file in /usr/local/munki, and the documentation told you to chown root and chmod 600 it for one reason: it holds credentials.

The CloudFront middleware shows the shape of a good one. It signs each request with a private key, every signature carries an expiry, and the default lifetime is 60 minutes, so a leaked URL goes stale before lunch. The key sits at /usr/local/munki/munkiaccess.pem owned by root with mode 400, and AWS allows two CloudFront keys per account so you can rotate without a outage. Since version 1.1 the certificate and settings can ship in a configuration profile, which turns key rotation into an MDM push. Treat that private key like the repo password, because it does the same job: it is the client lock for a cloud repo.

Middleware modules ported to Munki 7
Module Backend What it signs or adds
CloudFrontMiddleware Amazon CloudFront in front of S3 Signed URLs with an expiry
S3Middleware Amazon S3 direct AWS request signatures
GCSMiddleware Google Cloud Storage Signed requests
AzureStorageMiddleware Azure Blob Storage Shared access signatures
BunnyNetMiddleware Bunny.net CDN Token authentication headers
DemoMiddleware None Prints the request; a template for your own

The Munki wiki notes these ports are "believed to work" but "not officially supported at this time," and asks the admins who run them to take over maintenance. Consider it a volunteer sheet with your name already pencilled in.

Every lock above ends in a preference on the client, and Munki reads preferences from four places in a fixed order. A managed profile outranks root's ByHost plist, which outranks root's plist, which outranks the world-readable /Library/Preferences/ManagedInstalls.plist. Put the repo URL, the auth header, and the verification mode in a profile or in root's file, and leave the world-readable file for the settings Managed Software Center has to read as the logged-in user. Edit them with defaults or a profile, never a text editor, because macOS caches preferences and will cheerfully ignore your handiwork.

Two older protections deserve a mention because they are already switched on. Preflight and postflight scripts must be owned by the user running managedsoftwareupdate, share its group or wheel, and must not be world-writable, or Munki refuses to run them. And every official Munki 7 release is signed and notarized by Mac Admins Open Source, so the tool that checks your software is itself checkable.

Where Munki reads its preferences, highest precedence first
Location Who can read it Best for
MCX or configuration profiles Managed by MDM Everything, including AdditionalHttpHeaders
/private/var/root/Library/Preferences/ByHost/ManagedInstalls.XXXXXX.plist Root and admins Rare per-host overrides
/private/var/root/Library/Preferences/ManagedInstalls.plist Root and admins Repo URL, auth header, verification mode
/Library/Preferences/ManagedInstalls.plist Every user on the Mac Settings the GUI must read, such as ManagedInstallDir, HelpURL, and the MSC keys

Preferences set in root's file silently override the same keys in the world-readable file. The wiki flags this as a classic source of confusion when two admins stare at two different files and both swear theirs is the truth.

Frequently Asked Questions
Does Munki need HTTPS?
It runs over plain HTTP, but the project treats HTTPS as the floor for any repo holding licensed software, and both authentication methods assume it. Basic auth over unencrypted HTTP is described as trivially sniffable, and client certificates require an https:// repo URL outright.
Is basic auth or a client certificate more secure for Munki?
Client certificates identify each device and can be revoked one at a time, so they are the stronger lock. Basic auth is a shared secret that is far easier to deploy, and with HTTPS plus the header tucked into root's preferences or a profile, the documentation rates it reasonably secure.
What does hash_strict change?
The default hash mode verifies a download only when the pkginfo carries an installer_item_hash. hash_strict verifies every download and fails any item without one, so a pkginfo that misplaced its hash cannot install anything.
Does Munki verify manifests and catalogs?
No. The integrity hash covers installer items only. Manifests, catalogs, and pkginfo files lean on TLS in transit and on whatever controls write access to the repo at rest.
Can I keep the repo password out of the world-readable plist?
Yes. Write AdditionalHttpHeaders to root's preferences at /private/var/root/Library/Preferences/ManagedInstalls.plist, or deliver it in a configuration profile. Both outrank the /Library copy.

Munki is an open source project created by Greg Neagle and maintained on GitHub, with signed and notarized releases published through Mac Admins Open Source.

Four locks. Three of them are a preference key away, and the fourth is a decision about who can write to the repo. Set all four and the plain text folder that makes Munki such a pleasure to read stays a pleasure to read for exactly one audience: you. For the wide-angle view of how those pkginfo, catalog, and manifest files fit together in the first place, start with our guide to Munki and plist files. And if the web server under your repo could use a second set of eyes, our cybersecurity team hardens servers for a living.

Sources: Munki wiki, Using Basic Authentication, Munki wiki, Using Munki With SSL Client Certificates, Munki wiki, Preferences, Munki wiki, Supported Pkginfo Keys, Munki wiki, Middleware, Munki wiki, Repo Plugins, Munki wiki, Munki With Git, Munki wiki, FAQ, Munki wiki, Release Notes, Munki releases on GitHub, CloudFront Middleware README, munki-dev, How to protect munki repo (October 2010)

Related Articles