Santa
Santa is a binary authorization system for macOS. Zentral can act as a sync server for Santa, to configure the rules, and collect the events.
Zentral configuration
To activate the santa module, you need to add a zentral.contrib.santa section to the apps section in base.json.
flatten_events_signing_chain
OPTIONAL
The Santa events have a signing_chain key that is an array of certificate objects. This can be difficult to use in some event stores, like Elasticsearch. To fix this issue, Zentral flattens the signing chain using the signing_cert_0, signing_cert_1 and signing_cert_2 keys. To prevent Zentral from altering the events (default behaviour), set this boolean option to false.
Santa deployment
Create a Santa agent configuration
In Zentral, go to Setup > Santa configurations. Click on the [Create] button. The form mirrors the Santa configuration keys (some of them are omitted and will be set automatically by Zentral).
WARNING be careful and do not configure Santa in lockdown mode unless you know what you are doing!!!
You can start with the default values, and simply pick a name (must be unique) for the configuration, then click the [Save] button at the bottom.
The page of a configuration has four tabs:
- Overview: the attributes of the configuration, its voting groups and its enrollments.
- Rules: the rules , with the rule search.
- Scoped client modes: the scoped client modes , with a filter on the mode.
- Scoped path regexes: the scoped path regexes , with a filter on the policy, and a search on the name and on the pattern.
Each tab has its own URL. The title of the last three tabs gives their number of items. The number counts only the entries that you can view, and on the open tab it counts the results of the search. These tabs are paginated, with the number of items per page of your profile. A link to a rule, to a scoped client mode or to a scoped path regex opens its tab with a search that finds it.
Create an enrollment
Once you have created a Santa configuration in Zentral, you can create an enrollement for it using the [Create] button in the Enrollment section of the configuration. An enrollment is a configuration that is applied to the machines the first time the Santa agent makes a preflight query to Zentral.
Machine segmentation
You can pick a Meta business unit to segment your machines. You can also segment the machines during the enrollment by picking tags.
Enrollment restrictions
Enrollments can be restricted by machine serial numbers and UUIDs – all machines are allowed if the Serial numbers and UUIDs fields are left blank. You can also set a maximum number of enrollments – machines will always be allowed if the Quota field is left blank.
Save and download
Save the enrollment form, you will be redirected to the configuration, and the new enrollment will be available. You can download two different versions of the enrollment:
- a plist containing only the Santa specific configuration keys. This plist is can be uploaded to Jamf, to create a custom settings payload for the
com.northpolesec.santaPreference Domain. - a configuration profile with a ManagedPreferences payload, that can be further customized or distributed as is.
How it works
Each enrollment has a secret associated with it, and this secret is part of the Santa SyncBaseURL that is set in the plists or configuration profiles when you download them. This is how Zentral associate machines with configurations. Machines can be re-enrolled to a different configuration by simply deploying a different santa payload. The old rules will be erased and replaced by the new configuration rules.
Distribute the payloads
4 different payloads need to be distributed to configure and activate the Santa agent.
Main santa configuration
This is the payload that is generated when creating an enrollment on a Zentral santa configuration (see previous section.) You can further customize this payload to add for example the MachineOwner Santa configuration key
using Jamf Payload Variables
.
Privacy preference policy control
Santa, the santa daemon, and the santa bundle service need access to all protected files, including system administration files. A privacy preference policy control payload
must be distributed to allow the System Policy All Files (SystemPolicyAllFiles key) Service
for these three santa components, identified by their bundle IDs and code requirements.
| IdentifierType | Identifier | CodeRequirement | Allowed |
|---|---|---|---|
| bundleID | com.northpolesec.santa | identifier “com.northpolesec.santa” and anchor apple generic and certificate 1[field.1.2.840.113635.100.6.2.6] /* exists */ and certificate leaf[field.1.2.840.113635.100.6.1.13] /* exists */ and certificate leaf[subject.OU] = ZMCG7MLDV9 | true |
| bundleID | com.northpolesec.santa.daemon | identifier “com.northpolesec.santa.daemon” and anchor apple generic and certificate 1[field.1.2.840.113635.100.6.2.6] /* exists */ and certificate leaf[field.1.2.840.113635.100.6.1.13] /* exists */ and certificate leaf[subject.OU] = ZMCG7MLDV9 | true |
| bundleID | com.northpolesec.santa.bundleservice | identifier “com.northpolesec.santa.bundleservice” and anchor apple generic and certificate 1[field.1.2.840.113635.100.6.2.6] /* exists */ and certificate leaf[field.1.2.840.113635.100.6.1.13] /* exists */ and certificate leaf[subject.OU] = ZMCG7MLDV9 | true |
Code requirements can be validated using the following command:
$ codesign -dr - \
/Applications/Santa.app \
/Applications/Santa.app/Contents/Library/SystemExtensions/com.northpolesec.santa.daemon.systemextension/Contents/MacOS/com.northpolesec.santa.daemon \
/Applications/Santa.app/Contents/MacOS/santabundleservice
Everything after designated =>should be included in the profile.
System Extension
To listen for the endpoint security events, and be able to act on them, santa installs a system extension. To activate this system extension, a SystemExtensions payload needs to be distributed.
| Key | Team Identifier | Value |
|---|---|---|
| AllowedSystemExtensionTypes | ZMCG7MLDV9 | EndpointSecurityExtension |
| AllowedSystemExtensions | ZMCG7MLDV9 | com.northpolesec.santa.daemon |
The values can be validated using the following command:
$ systemextensionsctl list
Notifications
Santa notifies the user when the main mode is changed (Lockdown / Monitor). To allow those notifications, a Notifications payload needs to be distributed.
The payload is an array of NotificationSettingItem . Suggested values:
| Key | Value |
|---|---|
| AlertType | 1 |
| BadgesEnabled | true |
| BundleIdentifier | com.northpolesec.santa |
| CriticalAlertEnabled | true |
| NotificationsEnabled | true |
| ShowInLockScreen | true |
| ShowInNotificationCenter | true |
| SoundsEnabled | false |
Santa rules
Definitions
A Zentral Santa rule combines a target and a policy. The target can be of type cdhash, Binary, Signing ID, Certificate or Team ID. A target is uniquely identified by its type, and its identifier: a sha256 hexdigest for Binary and Certificate, a 40 character hexdigest for cdhash, TEAMID:SIGNINGID for Signing ID, and the 10 character team ID for Team ID. Bundle is a sixth type that only exists in Zentral, see below. The policy can be one of Allowlist, Allowlist compiler, Blocklist, Silent blocklist and CEL.
A configuration has at most one rule per target and policy. Several rules on one target are several statements about it, and Rule resolution picks one for each machine.
Rule precedence applies. You can have a Blocklist rule on a certificate, and an Allowlist rule on one of the binaries signed using the certificate.
Bundle targets are only available when Zentral managed to get the full bundle information from Santa. This happens only when Santa blocks a binary that is part of a bundle, and when Enable bundles is set to true in the Zentral Santa configuration. Rules on a bundle target only exist in Zentral. There are expanded to a list of binary rules when sent to the Santa agent – this is the reason why we need the bundle information.
Allowlist compiler rules will only generate local transitive rules when Enable transitive rules is set to true in the Zentral Santa configuration. A transitive Allowlist rule will be created locally for each file written by the targets of those Santa rules.
This policy is only available for cdhash, Binary and Signing ID targets. The Santa agent does not accept the compiler policy on a Certificate or a Team ID rule. It drops such a rule when it evaluates it, and the target keeps no rule at all, which blocks it in lockdown mode. Zentral rejects these rules.
CEL rules carry an expression that the Santa agent evaluates for each execution of the target. Zentral stores the expression and sends it to the agent with the rule. See the Santa documentation for the syntax of the expression and for the values that it can return.
Quick start
Open the configuration, and select its Rules tab. It lists the rules of the configuration.
You can filter the list using the search form at the top. From this list, you can edit or delete existing rules (if they are not part of a ruleset, see API section below), and add more rules. To add a rule, click on the [Add] button at the top, and select the kind of rule you want to add. We will start with a “Base rule”.
To get the necessary information about a binary or a certificate you want to block or allow, use the santactl fileinfo command
.
Once you have set the rule type, the identifier and the policy, you can click on the [Save] button, and the rule will be added to the configuration for all the machines.
If you do not want to wait for a full sync to happen on your machine, you can trigger one using the following command:
$ sudo santactl sync
You should see a rule being downloaded in the command output.
Rule forms
Zentral is collecting information about all the binary, bundles and certificates that are present in the events that Santa uploads.
Using this information, it is possible to build a rule without knowing the identifier, using the “Binary rule”, “Bundle rule”, “Certificate rule”, or “Team ID rule” options in Rules > [Add] dropdown menu. But it might be that the binary, bundle or certificate information is not in Zentral. In that case, use the “Base rule” form.
Rule scope
By default, a rule is synced to all the machines enrolled on its Zentral Santa configuration.
A rule has three scope fields, each one with an exclusion: Serial numbers and Excluded serial numbers, Primary users and Excluded primary users, Tags and Excluded tags. The serial numbers and the primary users are lists separated by ,. The tags are selected in the form.
The primary user is the one that Santa reports. Configure it with the MachineOwner key of the Santa payload, or with the MachineOwnerPlist and MachineOwnerKey keys and a local plist on each machine.
A field matches a machine when it contains the serial number, the primary user, or one of the tags of the machine. Zentral looks at the fields from the narrowest to the widest, and the first field that matches the machine decides:
- The serial numbers. In
Serial numbers, the rule is in scope. InExcluded serial numbers, it is not. - The primary users, the same way.
- The tags. A machine can carry a tag of each field: the exclusion wins.
- If no field matches the machine, the rule is in scope only when it has no scope field at all. Such a rule is for every machine that its exclusions do not match.
An exclusion removes the machine from that rule only. Another rule can still match the machine.
| Rule | Machine | Decided by | In scope |
|---|---|---|---|
serial numbers S1, tags vip |
S1, no tag |
the serial number | yes |
serial numbers S1, tags vip |
S2, tag vip |
the tag | yes |
serial numbers S1, tags vip |
S3, no tag |
nothing, and the rule has scope fields | no |
tags fleet, excluded serial numbers S1 |
S1, tag fleet |
the excluded serial number | no |
serial numbers S1, excluded tags contractors |
S1, tag contractors |
the serial number, the tags are not looked at | yes |
tags vip, excluded tags contractors |
S6, tags vip and contractors |
the tags, the exclusion wins | no |
no scope field, excluded tags contractors |
S4, tag vip |
nothing, and the rule has no scope field | yes |
primary users alice |
S5, no primary user |
nothing, and the rule has scope fields | no |
tags fleet, excluded primary users bob |
S5, tag fleet, no primary user |
the tag | yes |
A machine that reports no primary user is not matched by the primary user fields.
An exception is written for one machine or one person, and a rule for a population. The order of the fields lets the exception win: a listed serial number keeps the rule when a tag excludes the machine, and an excluded serial number loses it when a tag matches the machine.
Rule resolution
A target can have several rules in a configuration, one per policy at most. Each rule is a statement about a population. For one machine, Zentral keeps one rule per target, the first one in this order:
- The narrowest match: the rule that its serial number decided, then its primary user, then a tag, then a rule with no scope field.
- The strictest policy:
Blocklist,Silent blocklist,CEL,Allowlist,Allowlist compiler.
The candidates for one target all have different policies, so the second step never ties.
| Order | Policy | Position |
|---|---|---|
| 1 | Blocklist |
The strictest. |
| 2 | Silent blocklist |
The same decision, without the notification. The loud block first, so a machine under both gets the message. |
| 3 | CEL |
Decides at run time, and can block: below an unconditional block, above an unconditional allow. |
| 4 | Allowlist |
|
| 5 | Allowlist compiler |
Allows the target and what the process writes: the most permissive. |
Nested exceptions go inside the rules, each level narrower than the previous one. Four levels of intent for one Team ID: block the fleet, allow the tag vip, but not the vip machines that are contractors, but yes for S1, a vip contractor:
| Rule | Policy | Fields |
|---|---|---|
| A | Blocklist |
none, every machine |
| B | Allowlist |
tags vip, serial numbers S1, excluded tags contractors |
| Machine | B | Candidates | Winner | Result |
|---|---|---|---|---|
tag fleet |
not matched | A | A | blocked |
tag vip |
matched by the tag | A, B | B, the narrower match | runs |
tags vip and contractors |
out, the exclusion wins at the tag level | A | A | blocked |
S1, tags vip and contractors |
matched by the serial number | A, B | B, the narrower match | runs |
An exception to a population rule needs no exclusion on it when it is narrower. At equal match the stricter policy wins: a Blocklist for the tag fleet and an Allowlist for the tag vip block a machine with the two tags, and the exception needs an excluded tag vip on the block rule.
A target with a voting rule takes no other rule, and a target with a rule takes no vote. Reset the target to change that.
The Create a rule menu of a target page offers a configuration when the target has a policy left in it, and the rule form then offers those policies only. The generic rule form, where you type the target, offers every policy and checks on submit.
Scoped client modes
A Santa configuration gives the same client mode to all its machines. A scoped client mode gives a different mode to some of them. Open the configuration in Setup > Santa configurations, select its Scoped client modes tab, and use the [Create] button.
Zentral resolves the mode of each machine when it answers the preflight . The machine applies it during its next full synchronization. There is no payload to distribute, and the Santa agent keeps no scope of its own.
Scope
An entry has the same scope fields as a rule: Serial numbers, Primary users and Tags, each one with an exclusion. See Rule scope
for the fields, and for the Santa payload keys that set the primary user.
The scope of an entry is resolved like the scope of a rule: the first field that matches the machine decides, and an entry with no scope field at all is for every machine that its exclusions do not match.
Resolution
More than one entry can be in scope for a machine. Zentral sorts the entries that match, and applies the first one:
- The narrowest match wins: the entry that its serial number decided, then its primary user, then a tag, then an entry with no scope field. An entry with serial numbers and tags is a serial number match for a listed machine, and a tag match for a machine it reaches by a tag.
Lockdownwins overMonitor.
A configuration has one Monitor entry and one Lockdown entry at most, so two entries in scope for a machine always have different modes, and the second step never ties. Nested exceptions go inside the entry, each level narrower than the previous one, as for a rule
.
An exception is usually written for one machine or for one user, and a policy for a population. This order lets the exception win. If no entry is in scope, the machine keeps the client mode of the configuration.
Block notification button
Each entry has its own Event detail source. The choices are the ones of the configuration, without Local configuration, and with Inherit added:
Inherit(the default): the machines in scope keep the button of the configuration. The entry changes the mode only.Voting portal: a link to the user portal of the voting realm of the configuration. An entry has no realm of its own.Custom: theEvent detail URLand theEvent detail textof the entry.None: the machines in scope get no button.
Local configuration is absent on purpose. It tells Zentral to send nothing, and the configuration already made that choice for the machines that no entry covers.
Permissions
Four PBAC actions manage the entries. viewScopedClientMode is a member of the Santa::Action::"AdminActions", "UserActions" and "ViewerActions" groups, and the three that write are members of "AdminActions" only, like the actions on the rules. A policy that uses a group covers them. There is no Django permission for them, so a policy that names each action must be extended.
createScopedClientMode takes the configuration as its resource:
permit (
principal in Role::"6",
action == Santa::Action::"createScopedClientMode",
resource == Santa::Configuration::"3"
);
viewScopedClientMode, updateScopedClientMode and deleteScopedClientMode take the entry, which has the configuration as its parent. One policy covers every entry of a configuration:
permit (
principal in Role::"6",
action in [Santa::Action::"viewScopedClientMode",
Santa::Action::"updateScopedClientMode",
Santa::Action::"deleteScopedClientMode"],
resource in Santa::Configuration::"3"
);
resource in [A, B] is a parse error, so a role that manages two configurations needs one policy for each of them.
The configuration is the boundary. A role that can write the entries of a configuration can give any client mode to any of its machines.
Scoped path regexes
A Santa configuration has one allow path regex and one block path regex for all its machines. A scoped path regex adds a pattern for some of them. Open the configuration in Setup > Santa configurations, select its Scoped path regexes tab, and use the [Create] button.
Santa accepts one pattern for each policy, so Zentral combines them when it answers the preflight . The machine applies the result during its next full synchronization.
Scope
An entry has the same scope fields as a rule: Serial numbers, Primary users and Tags, each one with an exclusion. See Rule scope
for the fields, and for how the first field that matches the machine decides.
Composition
Zentral combines the pattern of the configuration and the patterns of the entries in scope into one pattern for each policy:
^(?:(?:the configuration)|(?:the first entry)|(?:the second entry))
Two entries with the same pattern are two statements about one path, like two rules for one binary. A configuration has one entry per pattern and policy at most. For each pattern, Zentral keeps one entry for the machine: the narrowest match wins, and at equal match Block wins over Allow. A leading ^ is removed when the entry is saved, because the combination removes it: ^/tmp/ and /tmp/ are one pattern. Every other entry in scope is included.
Entries for /Users/.*/Downloads/ |
Fleet machine | Machine with the tag devs |
|---|---|---|
Block with no scope field, Allow with the tag devs |
in the block pattern | in the allow pattern |
Without this step, the machine with the tag devs would get the pattern in its two regexes, and Santa would block the path: it checks the block regex first.
Three properties of the result:
- It is anchored at the start of the path. Santa adds a
^to a pattern that does not have one, so an unanchored alternation makes only the first alternative an anchored one. Use a leading.*to match a path segment in the middle, for example.*/Downloads/. - The entries are in the alphabetical order of their names. Santa removes all its decision caches when the pattern changes, so the order must be the same between two preflights.
- If no pattern applies, Zentral sends a pattern that cannot match. Santa keeps the pattern it has if the key is not in the response, and it uses the pattern of the configuration profile if the key is empty.
Accepted patterns
Santa uses the ICU regular expression syntax. Zentral compiles each pattern with Python, which is not the same syntax at the margins. Python does not accept \p{…}, \X, \R and \Q…\E. [[:alpha:]] is more dangerous: Python accepts it as a set in a set, which is not the POSIX class that ICU applies.
Three limits come from the composition:
- At most 512 characters. A configuration has one entry per pattern and policy, and the pattern is part of that key. Split a long alternation into entries.
- No capture group. Each entry becomes a group of the combined pattern, and a capture group changes the number of the groups after it. Use a group that does not capture:
(?:abc). - Inline flags must have a scope. Write
(?i:abc), not(?i)abc. - The pattern must not match an empty path.
(?:),|,.*and(?:abc)?all match an empty path. One of them in the combination makes it match every path, because the combination is anchored and an empty match is a match at the start. Use.+and not.*.
The order in the Santa agent
Santa reads the block regex, then the allow regex, then the client mode. Each step gives an answer immediately. Two results come from that order:
- A path that the two regexes match is blocked. An allow entry with the same pattern as a block entry makes an exception for the machines it matches more precisely, see Composition . An allow entry with another pattern cannot: write a more accurate block pattern, or an allow rule for the identity of the binary, which Santa reads earlier.
- A rule is more important than a path. A block rule for a binary blocks it also when an allow entry matches its path.
Each policy has one mode where it is necessary, and one mode where it is a trap:
| Monitor | Lockdown | |
|---|---|---|
| Block | the only block by path | makes an exception to an allow path, and blocks scripts |
| Allow | stops the reports, it does not permit | the mechanism for exceptions |
In Lockdown mode a block entry is the only way to block a file that is not a Mach-O file by its path, because Santa permits such a file in all modes before it reads the client mode.
In Monitor mode an unknown binary already runs, so an allow entry permits nothing. It changes the decision from ALLOW_UNKNOWN to ALLOW_SCOPE, and Santa sends only the first one. An allow entry in Monitor mode stops the reports for the paths that it matches. Use a scoped client mode if you want to see what runs.
Permissions
Four PBAC actions manage the entries. viewScopedPathRegex is a member of the Santa::Action::"AdminActions", "UserActions" and "ViewerActions" groups, and the three that write are members of "AdminActions" only. There is no Django permission for them, so a policy that names each action must be extended.
createScopedPathRegex takes the configuration as its resource:
permit (
principal in Role::"6",
action == Santa::Action::"createScopedPathRegex",
resource == Santa::Configuration::"3"
);
viewScopedPathRegex, updateScopedPathRegex and deleteScopedPathRegex take the entry, which has the configuration as its parent. One policy covers every entry of a configuration:
permit (
principal in Role::"6",
action in [Santa::Action::"viewScopedPathRegex",
Santa::Action::"updateScopedPathRegex",
Santa::Action::"deleteScopedPathRegex"],
resource in Santa::Configuration::"3"
);
resource in [A, B] is a parse error, so a role that manages two configurations needs one policy for each of them.
The configuration is the boundary. A role that can write the entries of a configuration can add an allow path or a block path for any of its machines.
Machines
A machine is a serial number. Santa > Machines lists the machines that are enrolled, and each row opens a page that shows what the machine gets from Zentral, and why.
The list
One row for each serial number, with its primary user, its configuration, the client mode and the Santa version it reported, the number of rules in its rule database, the time of its last preflight and of its last postflight, and its synchronization state. The filters:
| Filter | Values |
|---|---|
| Search | a part of the serial number or of the primary user |
| Configuration | one of the Santa configurations |
| Last sync | the age of the last postflight: at most 1, 7, 14, 30, 45 or 90 days, older, or never |
| Client mode | the mode the machine reported at its last preflight, not the mode Zentral resolved for it |
| Santa version | one of the versions the machines report |
| Sync state | OK, mismatch, never synced, or clean sync queued |
The age buckets are the buckets of the Prometheus metrics. The young ones hold the same machines. The old ones do not: the metrics count enrollments, and the list counts machines.
The Santa index has the same link on the machine count of each configuration.
A search that has only one result opens the page of that machine.
The machine page
The page describes the current enrollment of the machine, see Enrollments . It gives what the machine reported at its last preflight — the client mode, the Santa version, the rule counts, the time of the last preflight and of the last postflight, the result of the last rule comparison and the clean sync that is queued — the inputs of the scope — the serial number, the primary user and the tags — and what Zentral configured for the machine: its client mode and its block notification button, each one with a link to where it comes from, the configuration or the scoped client mode that decided. The Action menu queues a clean sync, or cancels the one that is queued.
The page uses the tags of now. The last synchronization used the tags of its preflight.
The Rules tab
One row for each target the machine has a rule for, on the server, on the device, or on both. The candidates come from the statement the rule download itself reads, so the tab cannot disagree with what the next synchronization sends:
| State | Meaning | The next synchronization |
|---|---|---|
| On device | The device confirmed the rule of the server, with the same policy and the same version. | sends nothing |
| Not yet on device | The server has a rule the device does not have, or has with another policy or another version. A rule sent during the current session is in this state until the postflight: Santa writes its database at the end of the download. | sends the rule |
| Still on device | The device has a rule the server does not have anymore, because the rule was deleted or the machine went out of its scope. | sends the removal |
| Skipped | The only rules in scope for the target have the CEL policy, and the Santa version of the machine is too old to evaluate one. |
sends nothing |
The column is what a synchronization that starts now sends. A synchronization that is already running has sent some of it: a rule or a removal it sent in an earlier batch is not sent again, because the device holds nothing new for it until its postflight. During a clean synchronization every rule of the server is on its way back, because the client rebuilds its database from that session alone.
The columns are the target type, the identifier, the policy and the version of the server, what decided for
the machine — Serial number, Primary user, Tag, or All machines for a rule with no scope field — the
policy and the version of the device, and the state. The two read the same way, so a version that differs
is as visible as a policy that differs. Only a row with no rule on the server decides nothing, and gives
-: a rule the device still has, or one that is skipped. The policy links to the rule that decided. When
the configuration has more than one rule for the target, the column gives their number with a link to all
of them: the others are wider, or less strict, see Rule resolution
.
The filters — target type, policy, voting, state — and the identifier search give the count of each value next to it.
The tab gives the policy and the version of every rule the machine has, with viewEnrolledMachine alone:
they are the state of the machine, like the client mode of the Overview. view_rule is only necessary for
the two links, which open the rules of the configuration. The Path regexes tab works the same way: the
patterns are what the machine enforces, and the name of an entry needs viewScopedPathRegex.
The Path regexes tab
One row for each part of the two patterns Zentral sends: the patterns of the configuration itself, with -
as their name and Configuration as what decided, and the entry that won each pattern, with what decided
for the machine. The rows are in the order Zentral composes the patterns in. See
Scoped path regexes
. An entry you cannot view has no name and no link, and its
pattern is there: it is part of what the machine enforces.
Enrollments
A serial number has one enrollment for each (enrollment, hardware UUID) pair. It gets a second one when the machine enrolls through another enrollment, or when a change of logic board gives it a new hardware UUID.
The current enrollment is the one with the most recent preflight: the row the device talks to. The Enrollments section of the machine page gives all of them, the current one first. The list, the machine page, the clean sync actions and the Santa section of the inventory machine page all use the current one.
The API endpoints work on one enrollment, not on one machine, see /api/santa/enrolled_machines/ .
Permissions
The Santa::Action::"viewEnrolledMachine" PBAC action opens the list and the machine page. It is a member of the Santa::Action::"AdminActions", "UserActions" and "ViewerActions" groups. There is no Django permission for it, so a policy that names each action must be extended.
The decision is not scoped to a machine: the action takes System as its resource, so a role that has it sees every machine that is enrolled. The note on the list endpoint
explains why.
permit (
principal in Role::"6",
action == Santa::Action::"viewEnrolledMachine",
resource
);
Santa sync
The Santa agent is configured to sync periodically with the Zentral server. The Full sync interval can be adjusted for each Santa configuration – 10 min by default, cannot be shorter than 10 min. No need to distribute the updated Santa payload. The agent will apply the new interval during the next sync.
To check the santa sync configuration and status, use the following command:
$ santactl status
Verify that the Sync Server URL is pointing to your Santa server.
To manually trigger a full synchronization (for example, for applying new rules without having to wait for a full sync interval), use the following command:
$ sudo santactl sync
A full synchronization has 4 phases:
Preflight
The Santa agent sends some information about the system (os version, identifiers, …) and itself (version, number of rule for each rule types, …). Zentral responds with the updated sync server provided configuration . Using this mechanism, some of the updated Zentral Santa configuration attributes will be applied without having to deploy new payloads. For example, it is possible to switch from Monitor to Lockdown mode, or to increase the full sync interval to 20min.
Zentral will also request a clean sync if the machine is new – never seen before or previonsly enrolled on a different configuration. Santa will delete all the existing rule in the local database during a clean sync.
Clean syncs
Zentral answers the preflight with the type of sync Santa has to perform:
| Sync type | What Santa deletes before applying the received rules |
|---|---|
NORMAL |
Nothing. The received rules are applied on top of the existing ones, and a rule with the REMOVE policy deletes its target. |
CLEAN |
Every rule Zentral synced with the machine, and every file access rule. The transitive rules the client created on its own are kept. |
CLEAN_ALL |
Every rule, including the transitive ones. |
Zentral asks for a clean sync when the machine has just enrolled or re-enrolled, when the machine asks for one itself (santactl sync --clean), when the machine reports no rule at all although some were synced with it, when a clean sync session was lost before the machine could confirm it, and when an operator queued one.
To queue one, open the machine in the inventory or in Santa > Machines and pick Force clean sync or Force clean all sync in the Action menu. The two pages report the queued sync until the machine preflights, and Cancel queued clean sync takes it back. The same is available on the API .
A few properties of the protocol are worth knowing:
- Zentral cannot downgrade a clean sync. A machine that ran
santactl sync --clean-allperforms aCLEAN_ALLwhatever Zentral answers, and the preflight request only carries a boolean, so Zentral cannot tell it apart fromsantactl sync --clean. - Zentral answers agents older than
2024.1with the deprecatedclean_syncboolean instead of thesync_typekey. That boolean can only ask for aCLEAN– it is read as aCLEAN_ALLonly when the agent itself asked for one – so a queuedCLEAN_ALLis degraded to aCLEANon those machines. - Santa skips the deletion altogether when it receives no rule at all, so a clean sync never leaves a machine without rules.
Events upload
The Santa agent will then proceed to upload the events it has gathered. These are for example ALLOW UNKNOWN execution events for binaries not targeted by any rule in Monitor mode. These events contain useful information about the binaries and their signatures, that Zentral stores to help you build the necessary rules for your deployment. You can change the number of events sent in one request using the Batch size attribute of the Zentral Santa configurations. This attribute is part of the dynamic sync server configuration and is applied during each preflight phase – no need to distribute a new santa payload.
NB: Block events are usually sent when they happen, outside of the full synchronization.
Rules download
Once the events have been uploaded, the rules are downloaded. Zentral will send batches of Batch size rules, and only mark them as present on the machine when Santa asks for the next batch. Rules that have been deleted, or are not anymore in scope for the machine will be removed.
Postflight
Santa finally makes an extra request to indicate the end of the full synchronization.
HTTP API
The HTTP API endpoints are documented below.
Requests
Authentication
API requests are authenticated using a token in the Authorization HTTP header:
Authorization: Token the_token_string
See API authentication for how to create a service account, issue a token for it and set an expiry.
Content type
Zentral will parse the body of the request based on the Content-Type HTTP header:
Content-Type: application/jsonContent-Type: application/yaml
/api/santa/enrolled_machines/
List the enrolled machines
- method: GET
- Content-Type: application/json
- PBAC action:
Santa::Action::"viewEnrolledMachine" - Optional search parameters:
serial_number: the serial number of the machine.hardware_uuid: the hardware UUID of the machine – themachine_idof the sync protocol.configuration_id: the ID of the Zentral Santa configuration the machine is enrolled on.enrollment_id: the ID of the enrollment the machine is enrolled with.primary_user: the primary user reported by the agent.last_sync_ok:truefor the machines whose reported rules matched the ledger during their last preflight,falsefor the ones that did not.forced_sync_type:CLEANorCLEAN_ALL, to find the machines with a clean sync of that type queued.forced_sync_type__isnull:falseto find every machine with a clean sync queued.
- Optional ordering parameter, prefix with
-to reverse it:ordering:created_at(the default, reversed),updated_at,last_preflight_atorlast_postflight_at.
The results are paginated, use the limit and offset parameters to page through them.
NOTE: Unlike
forceCleanSync, this action cannot be restricted to a meta business unit. The PBAC engine takes one decision per request, and narrowing a list would require one decision per machine – which cannot be done once the page has been cut. GrantingviewEnrolledMachinetherefore exposes every enrolled machine.
Example:
curl -H "Authorization: Token $ZTL_API_TOKEN" \
"https://$ZTL_FQDN/api/santa/enrolled_machines/?last_sync_ok=false" \
|python3 -m json.tool
Response:
{
"count": 1,
"next": null,
"previous": null,
"results": [
{
"id": 1,
"configuration": 1,
"enrollment": 1,
"hardware_uuid": "8791c9dd-0a26-4d63-a1e9-9ef1d4e5b3d1",
"serial_number": "C02ZZZZZZZZZ",
"primary_user": "yolo@example.com",
"client_mode": 1,
"santa_version": "2026.4",
"binary_rule_count": 12,
"cdhash_rule_count": 0,
"certificate_rule_count": 1,
"compiler_rule_count": 0,
"signingid_rule_count": 3,
"transitive_rule_count": 0,
"teamid_rule_count": 7,
"last_sync_ok": false,
"forced_sync_type": null,
"forced_sync_type_at": null,
"last_preflight_at": "2026-08-20T09:12:03.418211",
"last_postflight_at": "2026-08-20T09:12:04.007122",
"created_at": "2026-05-02T11:41:52.114203",
"updated_at": "2026-08-20T09:12:03.418299"
}
]
}
/api/santa/enrolled_machines/<int:pk>/force_clean_sync/
Queue a clean sync
- method: POST
- Content-Type: application/json
- PBAC action:
Santa::Action::"forceCleanSync" - Optional attribute:
sync_type:CLEAN(the default) orCLEAN_ALL. See Clean syncs for what Santa deletes in each case.
Use this endpoint to make an enrolled machine rebuild its rule database during its next preflight. Nothing is pushed to the machine – Santa syncs on its own schedule, so the clean sync happens within the Full sync interval of its configuration, 10 min by default.
The queued clean sync is consumed by the first clean sync the machine is answered, whatever asked for it. If the machine never confirms that session with a postflight, Zentral asks for another clean sync during the next preflight.
Queueing a clean sync posts an audit event on the machine. Posting the same sync type twice is accepted and changes nothing, so no second event is posted.
The forceCleanSync action takes the machine as its resource and the sync type as its context, so a policy can be restricted to a meta business unit, to a sync type, or to a configuration:
permit (
principal in Role::"6",
action == Santa::Action::"forceCleanSync",
resource in Inventory::MetaBusinessUnit::"3"
) when { context.syncType == "CLEAN" };
Example:
curl -X POST \
-H "Authorization: Token $ZTL_API_TOKEN" \
-H "Content-Type: application/json" \
-d '{"sync_type": "CLEAN_ALL"}' \
"https://$ZTL_FQDN/api/santa/enrolled_machines/1/force_clean_sync/" \
|python3 -m json.tool
Response: the enrolled machine, with the same attributes as in the list above. The two that just changed:
{
"…": "…",
"forced_sync_type": "CLEAN_ALL",
"forced_sync_type_at": "2026-08-20T14:22:57.183920"
}
Cancel a queued clean sync
- method: DELETE
- PBAC action:
Santa::Action::"forceCleanSync"
Use this endpoint to take back a queued clean sync, as long as the machine has not preflighted yet.
The cancellation is authorized against the sync type it takes back, so a policy allowing only a CLEAN cannot cancel a queued CLEAN_ALL. Cancelling when nothing is queued is accepted and changes nothing.
Example:
curl -X DELETE \
-H "Authorization: Token $ZTL_API_TOKEN" \
"https://$ZTL_FQDN/api/santa/enrolled_machines/1/force_clean_sync/" \
|python3 -m json.tool
/api/santa/rules/
List all Santa rules
- method: GET
- Content-Type: application/json
- PBAC action:
Santa::Action::"viewRule" - Optional search parameters:
target_type: the type (BINARY,CERTIFICATE, …) of the rule target.target_identifier: the identifier of the rule target.configuration_id: the ID of the Zentral Santa configuration the rule is attached to.
Use this endpoint to get a list of the Santa rules.
Example:
curl -H "Authorization: Token $ZTL_API_TOKEN" \
-H "Content-Type: application/json" \
"https://$ZTL_FQDN/api/santa/rules/" \
|python3 -m json.tool
$ curl -H "Authorization: Token $ZTL_API_TOKEN" \
-H "Content-Type: application/json" \
"https://$ZTL_FQDN/api/santa/rules/?target_type=TEAMID" \
|python3 -m json.tool
Response:
[
{
"id": 1,
"target_type": "TEAMID",
"target_identifier": "9BNSXJN65R",
"ruleset": null,
"version": 1,
"policy": 1,
"custom_msg": "",
"description": "",
"serial_numbers": [],
"excluded_serial_numbers": [],
"primary_users": [],
"excluded_primary_users": [],
"created_at": "2023-01-17T13:08:19.503831",
"updated_at": "2023-01-17T13:08:19.503837",
"configuration": 1,
"tags": [
2
],
"excluded_tags": [
1
]
},
{
"id": 2,
"target_type": "TEAMID",
"target_identifier": "1234567890",
"ruleset": null,
"version": 1,
"policy": 1,
"custom_msg": "",
"description": "",
"serial_numbers": [],
"excluded_serial_numbers": [],
"primary_users": [],
"excluded_primary_users": [],
"created_at": "2023-01-20T06:10:15.059545",
"updated_at": "2023-01-20T06:10:15.059556",
"configuration": 1,
"tags": [],
"excluded_tags": []
}
]
Add new Santa rule
- method: POST
- Content-Type: application/json
- PBAC action:
Santa::Action::"createRule"
Use this endpoint to add a new Santa rule.
NOTEs: The
rulesetattribute is read-only
Example:
rule.json:
{
"target_type": "BINARY",
"target_identifier": "2e4c209792b8c847063b94422adeee4ebeb523a1c28a8becfd99a77588c1c247",
"policy": 1,
"description": "Allow the yes binary on macOS 12.5",
"configuration": 1,
"tags": [1],
"excluded_tags": [2],
"serial_numbers": ["1234567890", "1234567891"],
"excluded_serial_numbers": ["1234567892", "1234567893"],
"primary_users": ["john", "jane"],
"excluded_primary_users": ["joe"]
}
$ curl -X POST \
-H "Authorization: Token $ZTL_API_TOKEN" \
-H 'Content-Type: application/json' \
-d @rule.json \
"https://$ZTL_FQDN/api/santa/rules/"\
|python3 -m json.tool
Response:
{
"id": 8,
"target_type": "BINARY",
"target_identifier": "2e4c209792b8c847063b94422adeee4ebeb523a1c28a8becfd99a77588c1c247",
"ruleset": null,
"version": 1,
"policy": 1,
"custom_msg": "",
"description": "Allow the yes binary on macOS 12.5",
"serial_numbers": [
"1234567890",
"1234567891"
],
"excluded_serial_numbers": [
"1234567892",
"1234567893"
],
"primary_users": [
"john",
"jane"
],
"excluded_primary_users": [
"joe"
],
"created_at": "2023-01-30T08:51:13.826830",
"updated_at": "2023-01-30T08:51:13.826841",
"configuration": 1,
"tags": [
1
],
"excluded_tags": [
2
]
}
/api/santa/rules/<int: pk>/
Get a rule
- method: GET
- Content-Type: application/json
- PBAC action:
Santa::Action::"viewRule" <int:pk>: the primary key of the rule.
Example
$ curl -H "Authorization: Token $ZTL_API_TOKEN" \
"https://$ZTL_FQDN/api/santa/rules/1/" \
|python3 -m json.tool
Response:
{
"id": 1,
"target_type": "TEAMID",
"target_identifier": "9BNSXJN65R",
"ruleset": null,
"version": 1,
"policy": 1,
"custom_msg": "",
"description": "",
"serial_numbers": [],
"excluded_serial_numbers": [],
"primary_users": [],
"excluded_primary_users": [],
"created_at": "2023-01-17T13:08:19.503831",
"updated_at": "2023-01-17T13:08:19.503837",
"configuration": 1,
"tags": [
2
],
"excluded_tags": [
1
]
}
Update a rule
- method: PUT
- Content-Type: application/json
- PBAC action:
Santa::Action::"updateRule"
Use this endpoint to update a Santa rule.
PUT is a full update. Send the whole rule: Zentral writes an empty value for cel_expr, custom_msg, custom_url and the six scope attributes when the body does not carry them. description is the exception, and keeps its stored value. cel_expr is required when the policy is CEL.
NOTEs: The
rulesetattribute is read-only
Example:
rule_update.json:
{
"target_type": "BINARY",
"target_identifier": "2e4c209792b8c847063b94422adeee4ebeb523a1c28a8becfd99a77588c1c247",
"policy": 2,
"description": "Block the yes binary on macOS 12.5",
"configuration": 1,
"custom_msg": "This binary is not allowed on this machine",
"tags": [1],
"excluded_tags": [2],
"serial_numbers": ["1234567890", "1234567891"],
"excluded_serial_numbers": ["1234567892"],
"primary_users": ["joe", "mary"],
"excluded_primary_users": ["jack"]
}
$ curl -X PUT \
-H "Authorization: Token $ZTL_API_TOKEN" \
-H 'Content-Type: application/json' \
-d @rule_update.json \
"https://$ZTL_FQDN/api/santa/rules/8/"\
|python3 -m json.tool
Response:
{
"id": 8,
"target_type": "BINARY",
"target_identifier": "2e4c209792b8c847063b94422adeee4ebeb523a1c28a8becfd99a77588c1c247",
"ruleset": null,
"version": 2,
"policy": 2,
"custom_msg": "This binary is not allowed on this machine",
"description": "Block the yes binary on macOS 12.5",
"serial_numbers": [
"1234567890",
"1234567891"
],
"excluded_serial_numbers": [
"1234567892"
],
"primary_users": [
"joe",
"mary"
],
"excluded_primary_users": [
"jack"
],
"created_at": "2023-01-30T08:51:13.826830",
"updated_at": "2023-01-30T08:54:21.860380",
"configuration": 1,
"tags": [
1
],
"excluded_tags": [
2
]
}
Delete a rule
- method: DELETE
- PBAC action:
Santa::Action::"deleteRule" <int:pk>: the primary key of the rule.
Use this endpoint to delete a Santa rule.
Example
$ curl -X DELETE \
-H "Authorization: Token $ZTL_API_TOKEN" \
"https://$ZTL_FQDN/api/santa/rules/8/"
/api/santa/ingest/fileinfo/
- method: POST
- Content-Type: application/json
This endpoint is designed to ingest the JSON output of the santactl fileinfo command
. This can be used to quickly and automatically upload information about binaries and certificates to Zentral. This information will be used to add context to rules identifiers, and in the rule forms.
Example:
$ santactl fileinfo --json \
--filter "Type=Executable" \
-r /Applications/TeamViewer.app/ \
| curl -XPOST \
-H "Authorization: Token $ZTL_API_TOKEN" \
-H "Content-Type: application/json" \
-d @- \
https://$ZTL_FQDN/api/santa/ingest/fileinfo/
This response is a JSON object with some counters:
{
"deserialization_errors": 0,
"db_errors": 0,
"present": 0,
"added": 9,
"ignored": 0
}
This operation is idempotent. The second time you run the command, and if the application has not changed, you will get the following response:
{
"deserialization_errors": 0,
"db_errors": 0,
"present": 9,
"added": 0,
"ignored": 0
}
/api/santa/rulesets/update/
- method: POST
- Content-Type: application/json or application/yaml
This endpoint is designed to help automatically maintain Zentral Santa configuration rulesets. It can be used in a CI/CD workflow.
To make a dry-run request, use ?dryRun or ?dryRun=All as query string.
Definition of a ruleset
A ruleset is a set of rules, with a unique name, that can be applied to some Zentral configurations.
Ruleset updates are applied idempotently. Rules will be added, updated or deleted in the scoped configurations to match the definition of the posted ruleset.
The key for each rule is the target (type, identifier) and the policy. Only one rule can exist for a given target and policy in a configuration. A rule whose policy changes in the ruleset is a new rule, and the old one is deleted.
Ruleset allows to automatically manage a set of rules in a configuration, without modifying rules from a different ruleset, or manually created in Zentral.
But if a manual rule or a rule from a different ruleset on a given target and policy already exists, or a voting rule on the target, adding a rule on the same target and policy in a ruleset will create a conflict, and the update will be rejected.
Finally, rules belonging to a ruleset cannot be manually edited in Zentral.
Payloads
The rulesets can be posted in JSON or YAML format. See Examples below, and the different Content-Type header values.
Ruleset attributes
| Attribute | Mandatory | Value |
|---|---|---|
name |
✓ | Unique name of the ruleset. Used as key to determine if it is a create or update operation |
rules |
✓ | A list of rule objects (see below) |
Rule attributes
| Attribute | Mandatory | Value |
|---|---|---|
rule_type |
✓ | Either CDHASH, BINARY, SIGNINGID, CERTIFICATE, or TEAMID |
identifier |
✓ | The BINARY or CERTIFICATE sha256 hex digest, the CDHASH hex digest, the SIGNINGID as TEAMID:BUNDLEID (or platform:BUNDLEID), or the TEAMID of the signing certificate |
policy |
✓ | Either ALLOWLIST, ALLOWLIST_COMPILER, BLOCKLIST, or SILENT_BLOCKLIST. ALLOWLIST_COMPILER is only valid for the SIGNINGID, BINARY and CDHASH rule types |
custom_msg |
Optional message to show when the application is blocked. Only valid for a BLOCKLIST policy |
|
description |
Optional description to add context to a rule. Only displayed in the Zentral GUI. | |
serial_numbers |
A list of machine serial numbers. If set, only those machines will receive the rule | |
excluded_serial_numbers |
A list of machine serial numbers. If set, those machines will not receive the rule | |
primary_users |
A list of machine owners. If set, only the machines associated with those owners (see Santa MachineOwner) will receive the rule |
|
excluded_primary_users |
A list of machine owners. If set, the machines associated with those owners (see Santa MachineOwner) will not receive the rule |
|
tags |
A list of machine tags. If set, only the machines with any one of those tags will receive the rule | |
excluded_tags |
A list of machine tags. If set, the machines with any one of those tags will not receive the rule |
Examples
ruleset.json
{
"name": "First ruleset test",
"rules": [
{
"rule_type": "BINARY",
"identifier": "1111111111111111111111111111111111111111111111111111111111111111",
"policy": "ALLOWLIST",
"description": "First rule of the first ruleset…"
},
{
"rule_type": "BINARY",
"identifier": "2222222222222222222222222222222222222222222222222222222222222222",
"policy": "ALLOWLIST",
"serial_numbers": ["SN1", "SN2"],
"excluded_serial_numbers": ["SN3"],
"primary_users": ["user1@example.com", "user2@example.com"],
"excluded_primary_users": ["user3@example.com"],
"tags": ["tag1", "tag2"],
"excluded_tags": ["tag3"]
}
]
}
Post the ruleset.json update to Zentral:
$ curl -XPOST \
-H "Authorization: Token $ZTL_API_TOKEN" \
-H 'Content-Type: application/json' \
-d @ruleset.json \
https://$ZTL_FQDN/api/santa/rulesets/update/\
|python3 -m json.tool
You should get a response close to this one:
{
"ruleset": {
"pk": 1,
"name": "First ruleset test"
},
"dry_run": false,
"result": "created",
"configurations": [
{
"name": "Name of your configuration",
"pk": 1,
"rule_results": {
"created": 2,
"deleted": 0,
"present": 0,
"updated": 0
}
}
]
}
If you post the same file again, you will get this answer:
{
"ruleset": {
"pk": 1,
"name": "First ruleset test"
},
"dry_run": false,
"result": "present",
"configurations": [
{
"name": "Name of your configuration",
"pk": 1,
"rule_results": {
"created": 0,
"deleted": 0,
"present": 2,
"updated": 0
}
}
]
}
ruleset2.json, scoped to only one configuration, but with a conflict with ruleset.json:
{
"name": "Second ruleset test",
"configurations": ["Name of your configuration"],
"rules": [
{
"rule_type": "BINARY",
"identifier": "1111111111111111111111111111111111111111111111111111111111111111",
"policy": "ALLOWLIST"
}
]
}
If you POST it, you will get the following error:
{
"rules": {
"0": {
"non_field_errors": [
"BINARY/1111111111111111111111111111111111111111111111111111111111111111: conflict"
]
}
}
}
This indicates that there is an existing rule in the configuration, on the same target as the first rule in ruleset2.json, but not belonging to this ruleset. If we change the identifier, a new rule will be created without conflict, and without modifying the manual rules, or the rules from ruleset.json.
{
"name": "Second ruleset test",
"configurations": ["Name of your configuration"],
"rules": [
{
"rule_type": "BINARY",
"identifier": "9876987698769876987698769876987698769876987698769876987698769876",
"policy": "ALLOWLIST"
}
]
}
{
"ruleset": {
"pk": 2,
"name": "Second ruleset test"
},
"dry_run": false,
"result": "created",
"configurations": [
{
"name": "Name of your configuration",
"pk": 1,
"rule_results": {
"created": 1,
"deleted": 0,
"present": 0,
"updated": 0
}
}
]
}
You can also use a YAML payload. This can be useful if you would like to use comments in the source.
ruleset2.yml
---
name: Second ruleset test
configurations:
- Name of your configuration
rules:
- rule_type: BINARY
identifier: 9876987698769876987698769876987698769876987698769876987698769876
policy: ALLOWLIST
Post the yml source to Zentral:
$ curl -XPOST \
-H "Authorization: Token $ZTL_API_TOKEN" \
-H 'Content-Type: application/yaml' \
--data-binary @ruleset2.yml \
https://$ZTL_FQDN/api/santa/rulesets/update/\
|python3 -m json.tool
Nothing was changed:
{
"ruleset": {
"pk": 2,
"name": "Second ruleset test"
},
"dry_run": false,
"result": "present",
"configurations": [
{
"name": "Name of your configuration",
"pk": 1,
"rule_results": {
"created": 0,
"deleted": 0,
"present": 1,
"updated": 0
}
}
]
}
/api/santa/configurations/
List all Santa configurations.
- method: GET
- Content-Type: application/json
- PBAC action:
Santa::Action::"viewConfiguration" - Optional filter parameter:
name: the name of the configuration target.
Examples
$ curl -H "Authorization: Token $ZTL_API_TOKEN" \
https://$ZTL_FQDN/api/santa/configurations/ \
|python3 -m json.tool
$ curl -H "Authorization: Token $ZTL_API_TOKEN" \
https://$ZTL_FQDN/api/santa/configurations/?name=Default \
|python3 -m json.tool
Response:
[
{
"id": 1,
"name": "Default",
"client_mode": 1,
"client_certificate_auth": false,
"batch_size": 50,
"full_sync_interval": 600,
"enable_bundles": true,
"enable_transitive_rules": false,
"allowed_path_regex": "",
"blocked_path_regex": "",
"event_detail_source": "LOCAL",
"event_detail_url": "",
"event_detail_text": "",
"block_usb_mount": false,
"remount_usb_mode": [],
"allow_unknown_shard": 100,
"enable_all_event_upload_shard": 0,
"sync_incident_severity": 0,
"created_at": "2023-01-06T13:07:23.768829",
"updated_at": "2023-01-12T12:15:30.457577"
}
]
Add new Santa configuration.
- method: POST
- Content-Type: application/json
- PBAC action:
Santa::Action::"createConfiguration" - Optional attributes:
event_detail_source: where the button of the Santa block notification comes from. One of:LOCAL(default): Zentral distributes nothing, and the button is left to the Santa configuration profile.VOTING_PORTAL: Zentral distributes a link to the user portal. Requires a voting realm with the user portal enabled. If the user portal becomes unavailable after the configuration is saved, Zentral removes the button.CUSTOM: Zentral distributesevent_detail_urlandevent_detail_text.NONE: no button.
event_detail_url: the URL of the button, required with theCUSTOMsource. The%file_identifier%,%bundle_or_file_identifier%,%file_bundle_id%,%team_id%,%signing_id%,%cdhash%,%username%,%machine_id%,%hostname%,%uuid%and%serial%sequences are replaced by the Santa agent.event_detail_text: the label of the button. Defaults toMore infowith theVOTING_PORTALsource.
Switching a configuration back to the LOCAL source stops the distribution of the button, but does not remove a button already distributed to a machine – force a clean sync
to do that.
Example
configuration.json
{
"blocked_path_regex": "",
"event_detail_source": "LOCAL",
"event_detail_url": "",
"event_detail_text": "",
"client_mode": 1,
"enable_bundles": true,
"batch_size": 50,
"block_usb_mount": false,
"client_certificate_auth": false,
"full_sync_interval": 600,
"allowed_path_regex": "",
"allow_unknown_shard": 100,
"sync_incident_severity": 0,
"remount_usb_mode": [
],
"enable_transitive_rules": false,
"enable_all_event_upload_shard": 0,
"name": "test"
}
$ curl -X POST \
-H "Authorization: Token $ZTL_API_TOKEN" \
-H 'Content-Type: application/json' \
-d @configuration.json \
https://$ZTL_FQDN/api/santa/configurations/\
|python3 -m json.tool
Response:
{
"id": 3,
"blocked_path_regex": "",
"event_detail_source": "LOCAL",
"event_detail_url": "",
"event_detail_text": "",
"client_mode": 1,
"enable_bundles": true,
"batch_size": 50,
"block_usb_mount": false,
"client_certificate_auth": false,
"full_sync_interval": 600,
"allowed_path_regex": "",
"allow_unknown_shard": 100,
"sync_incident_severity": 0,
"created_at": "2023-01-12T12:04:53.124658",
"remount_usb_mode": [
],
"enable_transitive_rules": false,
"updated_at": "2023-01-12T12:04:53.124667",
"enable_all_event_upload_shard": 0,
"name": "test"
}
/api/santa/configurations/<int:pk>/
Get Santa configuration.
- method: GET
- Content-Type: application/json
- PBAC action:
Santa::Action::"viewConfiguration" <int:pk>: the primary key of the configuration.
Example
$ curl -H "Authorization: Token $ZTL_API_TOKEN" \
https://$ZTL_FQDN/api/santa/configurations/1/ \
|python3 -m json.tool
Response:
{
"id": 1,
"blocked_path_regex": "",
"event_detail_source": "LOCAL",
"event_detail_url": "",
"event_detail_text": "",
"client_mode": 1,
"enable_bundles": true,
"batch_size": 50,
"block_usb_mount": false,
"client_certificate_auth": false,
"full_sync_interval": 600,
"allowed_path_regex": "",
"allow_unknown_shard": 100,
"sync_incident_severity": 0,
"created_at": "2023-01-06T13:07:23.768829",
"remount_usb_mode": [
],
"enable_transitive_rules": false,
"updated_at": "2023-01-06T13:07:23.768838",
"enable_all_event_upload_shard": 0,
"name": "Default"
}
Update Santa configuration.
- method: PUT
- Content-Type: application/json
- PBAC action:
Santa::Action::"updateConfiguration" <int:pk>: the primary key of the configuration.
PUT is a full update, but an attribute that is not in the body keeps its stored value. The block notification button is the exception: event_detail_source, event_detail_url and event_detail_text go together. Send the three, or send none of them. One of them on its own is a 400, because it cannot say what the button has to be.
The source decides the rest. CUSTOM needs a URL. The other sources clear the URL, and LOCAL and NONE clear the label too. VOTING_PORTAL needs a voting realm with the user portal: the one in the body, or the one the configuration has when the body does not name it.
Example
configuration.json
{
"blocked_path_regex": "",
"event_detail_source": "LOCAL",
"event_detail_url": "",
"event_detail_text": "",
"client_mode": 1,
"enable_bundles": true,
"batch_size": 50,
"block_usb_mount": false,
"client_certificate_auth": false,
"full_sync_interval": 600,
"allowed_path_regex": "",
"allow_unknown_shard": 100,
"sync_incident_severity": 0,
"remount_usb_mode": [
],
"enable_transitive_rules": false,
"enable_all_event_upload_shard": 0,
"name": "configuration-renamed"
}
$ curl -X PUT \
-H "Authorization: Token $ZTL_API_TOKEN" \
-H 'Content-Type: application/json' \
-d @configuration.json \
https://$ZTL_FQDN/api/santa/configurations/1/\
|python3 -m json.tool
Response:
{
"id": 1,
"blocked_path_regex": "",
"event_detail_source": "LOCAL",
"event_detail_url": "",
"event_detail_text": "",
"client_mode": 1,
"enable_bundles": true,
"batch_size": 50,
"block_usb_mount": false,
"client_certificate_auth": false,
"full_sync_interval": 600,
"allowed_path_regex": "",
"allow_unknown_shard": 100,
"sync_incident_severity": 0,
"created_at": "2023-01-06T13:07:23.768829",
"remount_usb_mode": [
],
"enable_transitive_rules": false,
"updated_at": "2023-01-12T12:14:19.952299",
"enable_all_event_upload_shard": 0,
"name": "configuration-renamed"
}
Delete Santa configuration.
- method: DELETE
- PBAC action:
Santa::Action::"deleteConfiguration" <int:pk>: the primary key of the configuration.
Example
$ curl -X DELETE \
-H "Authorization: Token $ZTL_API_TOKEN" \
https://$ZTL_FQDN/api/santa/configurations/1/
/api/santa/scoped_client_modes/
The scoped client modes of a Santa configuration. See Scoped client modes .
List the scoped client modes of a configuration.
- method: GET
- Content-Type: application/json
- PBAC action:
Santa::Action::"viewScopedClientMode" - Required filter parameter:
configuration_id: the ID of the Santa configuration. An ID that names no configuration is a 400.
The results are paginated, use the limit and the offset parameters to page through them. They are in the alphabetical order of the names.
NOTE: An entry the caller is not allowed to see is absent from the results. The endpoint does not answer with a 403.
Example:
$ curl -H "Authorization: Token $ZTL_API_TOKEN" \
"https://$ZTL_FQDN/api/santa/scoped_client_modes/?configuration_id=1" \
|python3 -m json.tool
Response:
{
"count": 1,
"next": null,
"previous": null,
"results": [
{
"id": 1,
"configuration": 1,
"name": "vendorcorp-rollout",
"description": "",
"client_mode": 1,
"event_detail_source": "INHERIT",
"event_detail_url": "",
"event_detail_text": "",
"serial_numbers": [],
"excluded_serial_numbers": [],
"primary_users": [],
"excluded_primary_users": [],
"tags": [3],
"excluded_tags": [],
"created_at": "2026-09-16T15:06:56.168151",
"updated_at": "2026-09-16T15:06:56.168154"
}
]
}
Add a scoped client mode.
- method: POST
- Content-Type: application/json
- PBAC action:
Santa::Action::"createScopedClientMode", with the configuration as its resource. - Required attributes:
configuration: the ID of the Santa configuration.name: unique in the configuration.client_mode:1for Monitor,2for Lockdown.
- Optional attributes:
descriptionevent_detail_source: where the button of the block notification comes from, for the machines in scope. One ofINHERIT(default, the button of the configuration),VOTING_PORTAL,CUSTOMorNONE.VOTING_PORTALrequires a voting realm with the user portal enabled on the configuration, andCUSTOMrequiresevent_detail_url.event_detail_url,event_detail_textserial_numbers,excluded_serial_numbers,primary_users,excluded_primary_users: arrays of strings.tags,excluded_tags: arrays of tag IDs.
Example:
$ curl -X POST \
-H "Authorization: Token $ZTL_API_TOKEN" \
-H "Content-Type: application/json" \
-d '{"configuration": 1, "name": "vendorcorp-rollout", "client_mode": 1, "tags": [3]}' \
https://$ZTL_FQDN/api/santa/scoped_client_modes/ \
|python3 -m json.tool
/api/santa/scoped_client_modes/<int:pk>/
Get a scoped client mode.
- method: GET
- Content-Type: application/json
- PBAC action:
Santa::Action::"viewScopedClientMode"
Update a scoped client mode.
- method: PUT
- Content-Type: application/json
- PBAC action:
Santa::Action::"updateScopedClientMode" - The attributes of the POST.
configuration, the scope and the event detail are required here.
PUT is a full update: send every attribute. An attribute that Zentral validates with another one is a 400 if it is missing, and the others keep their stored value. PATCH gives a 405, like everywhere else in the Zentral API.
NOTE: An entry cannot change configuration. A different one in the body of a
PUTis a 400. Delete the entry and create it again on the other configuration.
Delete a scoped client mode.
- method: DELETE
- PBAC action:
Santa::Action::"deleteScopedClientMode"
Example:
$ curl -X DELETE \
-H "Authorization: Token $ZTL_API_TOKEN" \
https://$ZTL_FQDN/api/santa/scoped_client_modes/1/
/api/santa/scoped_path_regexes/
The scoped path regexes of a Santa configuration. See Scoped path regexes .
List the scoped path regexes of a configuration.
- method: GET
- Content-Type: application/json
- PBAC action:
Santa::Action::"viewScopedPathRegex" - Required filter parameter:
configuration_id: the ID of the Santa configuration. An ID that names no configuration is a 400.
The results are paginated and ordered like the scoped client modes , and an entry the caller is not allowed to see is absent from them.
Example:
$ curl -H "Authorization: Token $ZTL_API_TOKEN" \
"https://$ZTL_FQDN/api/santa/scoped_path_regexes/?configuration_id=1" \
|python3 -m json.tool
Response:
{
"count": 1,
"next": null,
"previous": null,
"results": [
{
"id": 1,
"configuration": 1,
"name": "vendorcorp-installer",
"description": "",
"policy": "ALLOW",
"regex": "/Library/VendorCorp/",
"serial_numbers": [],
"excluded_serial_numbers": [],
"primary_users": [],
"excluded_primary_users": [],
"tags": [3],
"excluded_tags": [],
"created_at": "2026-09-16T15:06:56.169657",
"updated_at": "2026-09-16T15:06:56.169659"
}
]
}
Add a scoped path regex.
- method: POST
- Content-Type: application/json
- PBAC action:
Santa::Action::"createScopedPathRegex", with the configuration as its resource. - Required attributes:
configuration: the ID of the Santa configuration.name: unique in the configuration.policy:ALLOWorBLOCK.regex: see Accepted patterns . A pattern that Zentral cannot compile, a capture group, an inline flag group without a scope, a pattern that matches an empty path and a pattern longer than 512 characters are all a 400. A leading^is removed.
- Optional attributes:
descriptionserial_numbers,excluded_serial_numbers,primary_users,excluded_primary_users: arrays of strings.tags,excluded_tags: arrays of tag IDs.
Example:
$ curl -X POST \
-H "Authorization: Token $ZTL_API_TOKEN" \
-H "Content-Type: application/json" \
-d '{"configuration": 1, "name": "vendorcorp-installer", "policy": "ALLOW", "regex": "/Library/VendorCorp/", "tags": [3]}' \
https://$ZTL_FQDN/api/santa/scoped_path_regexes/ \
|python3 -m json.tool
/api/santa/scoped_path_regexes/<int:pk>/
Get a scoped path regex.
- method: GET
- Content-Type: application/json
- PBAC action:
Santa::Action::"viewScopedPathRegex"
Update a scoped path regex.
- method: PUT
- Content-Type: application/json
- PBAC action:
Santa::Action::"updateScopedPathRegex" - The attributes of the POST, with the rules of a scoped client mode
: a full update, and another
configurationin the body is a 400.
Delete a scoped path regex.
- method: DELETE
- PBAC action:
Santa::Action::"deleteScopedPathRegex"
Example:
$ curl -X DELETE \
-H "Authorization: Token $ZTL_API_TOKEN" \
https://$ZTL_FQDN/api/santa/scoped_path_regexes/1/
/api/santa/enrollments/
List all Santa enrollments.
- method: GET
- Content-Type: application/json
- PBAC action:
Santa::Action::"viewEnrollment" - Optional filter parameter:
configuration_id: the id of the configuration target.
Example
$ curl -H "Authorization: Token $ZTL_API_TOKEN" \
https://$ZTL_FQDN/api/santa/enrollments/ \
|python3 -m json.tool
$ curl -H "Authorization: Token $ZTL_API_TOKEN" \
https://$ZTL_FQDN/api/santa/enrollments/?configuration_id=1 \
|python3 -m json.tool
Response:
[
{
"secret": {
"secret": "AzZhxoWDXDqpUr06O8SQG53eE7fkiOy0U02uOghjQG3zowXMlJqpblSFXvkk05ak",
"request_count": 0,
"id": 3,
"serial_numbers": [
],
"meta_business_unit": 1,
"quota": null,
"tags": [
],
"udids": [
]
},
"id": 2,
"configuration_profile_download_url": "https://zentral.example.com/api/santa/enrollments/1/configuration_profile/",
"created_at": "2023-01-10T11:02:51.831544",
"configuration": 1,
"enrolled_machines_count": 0,
"version": 1,
"updated_at": "2023-01-10T11:02:51.831553",
"plist_download_url": "https://zentral.example.com/api/santa/enrollments/1/plist/"
}
]
Add new Santa enrollment.
- method: POST
- Content-Type: application/json
- PBAC action:
Santa::Action::"createEnrollment"
Example
enrollment.json
{
"secret": {
"meta_business_unit": 1
},
"configuration": 1
}
$ curl -X POST \
-H "Authorization: Token $ZTL_API_TOKEN" \
-H 'Content-Type: application/json' \
-d @enrollment.json \
https://$ZTL_FQDN/api/santa/enrollments/\
|python3 -m json.tool
Response:
{
"secret": {
"secret": "DfuWkO8aFPFABUAkbu2SuYxlKbChHxeEdU2cXelxnui7lZaeVuRjrlzYT3YPNu2P",
"request_count": 0,
"id": 6,
"serial_numbers": null,
"meta_business_unit": 1,
"quota": null,
"tags": [
],
"udids": null
},
"id": 5,
"configuration_profile_download_url": "https://zentral.example.com/api/santa/enrollments/5/configuration_profile/",
"created_at": "2023-01-12T12:47:17.030386",
"configuration": 1,
"enrolled_machines_count": 0,
"version": 1,
"updated_at": "2023-01-12T12:47:17.030394",
"plist_download_url": "https://zentral.example.com/api/santa/enrollments/5/plist/"
}
/api/santa/enrollments/<int:pk>/
Get Santa enrollment.
- method: GET
- Content-Type: application/json
- PBAC action:
Santa::Action::"viewEnrollment" <int:pk>: the primary key of the enrollments.
Example
$ curl -H "Authorization: Token $ZTL_API_TOKEN" \
https://$ZTL_FQDN/api/santa/enrollments/1/ \
|python3 -m json.tool
Response:
{
"id": 1,
"secret": {
"id": 2,
"secret": "HN3kfyxq3UuLYleRonGgHcttjer4rldR3GGgCKWU6YbdKLs565eHks7bHhpISCz9",
"meta_business_unit": 1,
"tags": [],
"serial_numbers": [],
"udids": [],
"quota": null,
"request_count": 1
},
"enrolled_machines_count": 1,
"plist_download_url": "https://zentral.example.com/api/santa/enrollments/1/plist/",
"configuration_profile_download_url": "https://zentral.example.com/api/santa/enrollments/1/configuration_profile/",
"version": 3,
"created_at": "2023-01-06T13:07:31.933243",
"updated_at": "2023-01-12T12:15:30.459785",
"configuration": 1
}
Update Santa enrollment.
- method: PUT
- Content-Type: application/json
- PBAC action:
Santa::Action::"updateEnrollment" <int:pk>: the primary key of the configuration.
Example
enrollment.json
{
"secret": {
"meta_business_unit": 1
},
"configuration": 2
}
$ curl -X PUT \
-H "Authorization: Token $ZTL_API_TOKEN" \
-H 'Content-Type: application/json' \
-d @configuration.json \
https://$ZTL_FQDN/api/santa/enrollments/1/\
|python3 -m json.tool
Response:
{
"id": 1,
"secret": {
"id": 2,
"secret": "HN3kfyxq3UuLYleRonGgHcttjer4rldR3GGgCKWU6YbdKLs565eHks7bHhpISCz9",
"meta_business_unit": 1,
"tags": [],
"serial_numbers": [],
"udids": [],
"quota": null,
"request_count": 1
},
"enrolled_machines_count": 1,
"plist_download_url": "https://zentral.example.com/api/santa/enrollments/1/plist/",
"configuration_profile_download_url": "https://zentral.example.com/api/santa/enrollments/1/configuration_profile/",
"version": 3,
"created_at": "2023-01-06T13:07:31.933243",
"updated_at": "2023-01-12T12:15:30.459785",
"configuration": 2
}
Delete Santa enrollment.
- method: DELETE
- PBAC action:
Santa::Action::"deleteEnrollment" <int:pk>: the primary key of the configuration.
Example
$ curl -X DELETE \
-H "Authorization: Token $ZTL_API_TOKEN" \
https://$ZTL_FQDN/api/santa/enrollments/5/
/api/santa/enrollments/<int:pk>/plist/
Download Santa enrollment plist file.
- method: GET
- PBAC action:
Santa::Action::"viewEnrollment" <int:pk>: the primary key of the configuration.
Example
$ curl -H "Authorization: Token $ZTL_API_TOKEN" \
https://$ZTL_FQDN/api/santa/enrollments/1/plist/ \
--output zentral_santa_configuration.enrollment.plist
/api/santa/enrollments/<int:pk>/configuration_profile/
Download Santa enrollment configuration profile file.
- method: GET
- PBAC action:
Santa::Action::"viewEnrollment" <int:pk>: the primary key of the enrollment.
Example
$ curl -H "Authorization: Token $ZTL_API_TOKEN" \
https://$ZTL_FQDN/api/santa/enrollments/1/configuration_profile/ \
--output com.example.zentral.santa_configuration.mobileconfig