Warden — MDM Configuration & Bundle Signing
This guide covers managed policy fields and signed bundle delivery.
Installation is covered in getting_started.md. This page starts after Warden is already installed on endpoints.
For the rule syntax itself, see rules_reference.md.
1. Overview
Warden reads its configuration from a managed policy — a file or profile pushed to endpoints by your MDM solution (Jamf, Kandji, Mosyle, Fleet, etc.).
There are two ways to deliver rules to endpoints:
| Method | Description |
|---|---|
rules-url |
Warden downloads a signed zip bundle from a URL you host. Easy to update — push a new zip without redeploying the policy. |
rules (inline) |
Rules are base64-encoded directly in the policy. No hosting required. Good for a small, stable set of baseline rules. |
You can use both together: inline rules provide a guaranteed baseline, while remote rules add dynamic policies that can be updated independently.
Warning
Inline rules can only be updated by redeploying the MDM profile.
When using rules-url, Warden verifies the bundle's integrity and authenticity before loading any rules.
Note
Every signed bundle includes a signatures.json file generated automatically
by warden bundle create. It contains an Ed25519 signature for each rule file
in the bundle, plus a manifest checksum — a signature over the complete
list of files.
On the endpoint, Warden verifies this in two passes:
-
Manifest integrity — the list of files in
signatures.jsonmust match what is actually in the bundle. If a file has been added or removed since the bundle was signed, verification fails and no rules are loaded. -
Per-file integrity — each rule's content is verified individually against its stored signature. A single byte change in any rule file causes that check to fail.
This uses public key cryptography (Ed25519). The private key signs the
bundle on the admin machine and is never distributed. Only the corresponding
public key is pushed to endpoints via MDM as bundle-signing-public-key.
Without the private key it is computationally infeasible to produce a valid
signature, so endpoints can trust that any bundle that passes verification was
produced by your security team and has not been modified in transit.
2. Managed policy fields
rules-url
| Type | string (URL) |
| Required | No |
A URL pointing to a zip file containing signed rule files. Warden downloads
and extracts the zip on startup, replacing any previously downloaded rules.
If bundle-signing-public-key is set, the bundle must pass signature
verification before any rules are loaded.
rules-url-headers
| Type | object (key-value string pairs) |
| Required | No |
Additional HTTP headers sent with every request to rules-url. Use this when
the bundle server requires authentication or other request metadata.
rules (inline rules)
| Type | array of strings |
| Required | No |
An array where each entry is the full content of a rule YAML file, base64-encoded. Inline rules are decoded on every Warden startup and loaded directly — no signature verification applies to them, since they are part of the managed policy itself and therefore already under MDM control.
To encode a rule file:
Paste the resulting string as an entry in the rules array.
allow-code-rules
| Type | boolean |
| Required | No |
| Default | false |
Controls whether rules may use the code action to execute Python scripts on
the endpoint. Disabled by default. Only enable this if you fully trust the
rules being delivered — code rules run with the same permissions as the Warden
process.
When rules-url is in use, any Python handler referenced by a code action is
also verified against the bundle signature before execution. A missing or
tampered handler is blocked regardless of this setting.
refresh-interval
| Type | integer (minutes) |
| Required | No |
| Default | 0 (disabled) |
How often, in minutes, Warden re-downloads rules from rules-url. When 0 or
omitted, Warden fetches rules once at startup. Set to 30 or 60 to get
near-real-time policy updates without redeploying the MDM profile.
bundle-signing-public-key
| Type | string (base64-encoded Ed25519 public key) |
| Required | Required when rules-url is set |
The public key used to verify signed rule bundles. Generate a key pair with
warden bundle keygen — the output prints the public key in the correct format
for this field. See Deploying rules below.
When set, Warden verifies the bundle before loading any rules:
- Checks the bundle's manifest checksum — an Ed25519 signature over the entire file list, proving the manifest itself has not been tampered with.
- Verifies the signature of each rule file individually.
- Verifies the signature of any Python handler before executing it.
If any verification step fails, Warden loads no rules from that bundle and
reports the failure to bundle-error-url (if configured).
Warden will refuse to start if
rules-urlis set without abundle-signing-public-key.
bundle-error-url
| Type | string (URL) |
| Required | No |
A URL that receives a POST request whenever bundle verification fails on an endpoint. Use this to alert your security team in real time when a tampered or unsigned bundle is detected.
The POST body is:
Point this at a Slack webhook, PagerDuty event endpoint, or any SIEM ingest URL. Each failure produces a separate POST with a specific error message (missing file, invalid signature, etc.).
3. Platform configuration
macOS — Managed profile
On macOS, Warden reads managed preferences from:
Deploy a .mobileconfig profile through your MDM solution. You can start from the sample profile in github.com/luisfontes19/warden/blob/master/packaging/macos/warden.mobileconfig.
Warning
Do not deploy that sample profile unchanged. The PayloadIdentifier and
PayloadUUID values in a .mobileconfig should be unique for your
organization and profile instance. Reusing the example values can cause
collisions with other profiles and makes profile management harder.
To generate a new macOS policy file, use the sample as a template:
Then update these fields before uploading it to your MDM:
- Replace both
PayloadUUIDvalues with new UUIDs, for example withuuidgen. - Replace both
PayloadIdentifiervalues with identifiers under your own naming scheme, such ascom.example.security.wardenandcom.example.security.warden.settings. - Edit the Warden settings under
mcx_preference_settingswith yourrules-url, inlinerules,refresh-interval, and any other managed policy fields you need.
Example UUID generation:
Keep the managed preference domain io.github.luisfontes19.warden inside
mcx_preference_settings unchanged unless the application domain itself
changes.
For installation sequence and package deployment, see getting_started.md.
4. Deploying rules to endpoints
Rules are distributed as a signed zip bundle that Warden downloads and verifies on every endpoint. The steps below cover the full flow: write rules, sign them, host the bundle, and point the MDM policy at it.
Step 1 — Write your rules
Collect your rule YAML files in a folder. Python handlers for code actions
can live in the same folder.
my-rules/
block-debug.yml
approved-mcps.yml
cleanup.py ← Python handler (only if using code actions)
Note
Files whose names begin with . (dotfiles such as .gitignore or
.DS_Store) are automatically excluded — they are neither signed nor
included in the zip.
Step 2 — Generate a signing key pair (first time only)
If you do not already have a key pair, generate one on the admin machine that will build bundles:
Output:
Private key saved to: /root/.warden/key.ed25519
Public key saved to: /root/.warden/key.ed25519.pub
Public key (add to MDM policy as 'bundle-signing-public-key'):
<base64-encoded public key>
Copy the base64 public key value and set it as bundle-signing-public-key in
your MDM policy. This is the only key value that ever leaves the admin machine.
Key security:
- The private key is written with mode
0o600(owner-readable only). Options:
Step 3 — Create the bundle
Run bundle create from the admin machine to sign and package the rules:
This signs each file with the private key, writes a signatures.json manifest,
and produces my-rules.zip ready to host.
Options:
| Flag | Description |
|---|---|
--rules-folder PATH |
(required) Folder containing rules to sign |
--output PATH |
(required) Destination zip file |
--key PATH |
Private key path (default: ~/.warden/key.ed25519) |
Step 4 — Host the bundle
Upload my-rules.zip to any HTTPS server your endpoints can reach, then set
rules-url in the MDM policy to the download URL:
Warden downloads and verifies the bundle on startup, and again on every
refresh-interval cycle. To push a rule change, rebuild the bundle, upload the
new zip to the same URL, and Warden picks it up on the next refresh — no MDM
profile redeploy needed.
How verification works on the endpoint
When Warden downloads and extracts a bundle, it verifies it in this order:
- Manifest checksum — verifies the Ed25519 signature over the complete
file list in
signatures.json. If this fails, the entire bundle is rejected immediately and an error is reported. - Per-file signatures — each rule file (
.yml) and Python handler (.py) listed in the manifest is verified individually. Files with an invalid signature are skipped; valid ones are loaded. - Missing files — if a file listed in the manifest is absent from the extracted bundle, it is reported as an error.
Any failure triggers a POST to bundle-error-url with a specific error
message identifying exactly what went wrong.