SIEM and SOC integration
Dependably exposes security telemetry that a SIEM can consume, so the registry is watched the same way the rest of your estate is. This page covers what is emitted, which transport carries which events, and how to build a collector that does not silently lose data.
The examples below were run against Wazuh; the mechanics generalize to Splunk, Elastic, Sentinel or anything that reads newline-delimited JSON.
Reference implementation for Wazuh
If Wazuh is your SIEM, you do not have to write rules or a dashboard from scratch.
Download the rules (dependably_rules.xml) and
download the dashboard
(dependably-dashboard.ndjson), then:
- Import the rules: open Server management, then Rules, then Import files, and tick
Overwrite if replacing a previous import. Then restart the manager: an imported
ruleset does not take effect until analysisd restarts, and
GET /rulesand/logtesteach report the rules as active before that restart actually happens, so neither is a reliable check. Confirm with a real search instead, onwazuh-alerts-*, filteringrule.groups:dependably. - Import the dashboard: open Dashboards Management, then Saved objects, then Import. The importer assigns new object ids on every import, so re-importing after an update creates a duplicate rather than replacing the original. Delete the old one.
- Refresh the index pattern's field list once. Open Index patterns, select
wazuh-alerts-*, and click the refresh icon, so panels that read poller-specific fields (like the feed health table) can find them. This is a one-time step; it does not need repeating on subsequent imports.
You need a working collector before either import produces anything. See Setup below first.

The rules do not read the Dependably feed directly. They match JSON records your
collector writes, with its fields under a dependably object:
instancenames the Dependably instance. The parent rule matches any record that carries it.record_typeisauditfor an event from the auth feed,activityfor one from the activity feed, andpoller_errorfor a failure the collector reports about itself.actionis the event's action, andliveistruefor an event collected as it happened orfalsefor backfilled history (see Mark backfilled events).- Individual rules also read
actor_id,purl,stage,message, and detail fields flattened todetail_<key>, such asdetail_reasonanddetail_key.
Nothing in the rules or dashboard is homelab-specific; they key off those
collector fields and rule.groups, not a hostname or an agent name. The rule id
space is 100100-100199; renumber before importing if you already use part of
that range.
The pull feeds
/api/v1/siem/events/auth reads the audit_log plane: authentication, credential and
capability refusals, MFA lifecycle, SAML identity changes, and other security and
configuration events. Filter it with a repeatable action= parameter.
/api/v1/siem/events/activity reads the activity plane: policy refusals (blocked_*,
every block-gate arm) and, when the instance sets SIEM_ACTIVITY_DOWNLOAD_EVENTS=true,
download. Filter it with a repeatable event= parameter: blocked for the whole
refusal family (the default), a specific blocked_<gate>, or download. Any other
value is a 400, and so is download while that variable is off.
Block-gate refusals are written only to the activity plane, so the auth feed never carries them. Poll each feed.
Each feed takes since and until as ISO 8601 timestamps. since defaults to 24 hours
ago. A since older than SIEM_MAX_LOOKBACK_DAYS (default 90) is moved forward to that
limit.
limit sets the page size, from 1 to 500 (default 100), and next_cursor in each
response pages through the rest of the window.
The Accept header picks the output format:
Accept |
Output |
|---|---|
application/json (default) |
A JSON envelope with items and next_cursor. The auth feed adds latest_event_at, matched and matched_capped; the activity feed adds since, until and lag_seconds. |
application/x-ndjson |
One event per line, then a trailer object. The auth feed's trailer carries next_cursor only, and only when there is another page. The activity feed writes a trailer on every page, with since, until, lag_seconds and next_cursor. |
application/x-cef |
One CEF record per line. A # next_cursor= comment line follows when there is another page, and the activity feed's last page adds # window_until=. |
Authentication and tenant scope
The SIEM endpoints accept a Bearer token carrying the read:audit capability, or a
signed-in session whose role grants it (admin, owner or auditor).
A token is pinned to its own organization. The ?org= parameter is ignored for
token callers (not rejected, ignored), so a token can neither read another tenant's
events nor use the endpoint to discover whether an organization slug exists. A
tenant user's session is pinned the same way.
Only a platform administrator's signed-in session (capability platform:*) reads
across tenants. On the auth feed it can omit ?org= to read every tenant, or name one.
On the activity feed it must name one organization per poll: that query is scoped by a
non-nullable organization id, so "every tenant at once" is not expressible, and
omitting ?org= is a 400 rather than an unscoped read.
Mint a dedicated token for your collector with read:audit and nothing else.
Setup
A working pull collector takes the steps below. None of them needs an instance restart.
1. Mint a collector token
As an admin or owner, open the Tokens page and create a token
with the audit scope, which grants read:audit and nothing else. The token is
pinned to the organization that mints it; on a multi-tenant instance, mint one per
tenant you want to watch.
2. Check the feed answers
curl -sS -H "Authorization: Bearer $DEPENDABLY_AUDIT_TOKEN" \
"https://repo.example.com/api/v1/siem/events/auth?limit=5"
A 200 carrying an items array is the whole server-side configuration. A 401
means the token was missing or did not resolve; a 403 means it resolved but lacks
read:audit.
3. Poll the feeds
GET /api/v1/siem/events/auth?since=<iso8601>&until=<iso8601>&limit=500
GET /api/v1/siem/events/activity?since=<iso8601>&until=<iso8601>&limit=500
Poll on whatever interval your SOC's detection latency allows; a minute is typical. Everything that makes the difference between a collector that works and one that loses events quietly is in Building a collector that does not lose data. Read that before you ship it.
The events worth alerting on
On the auth feed, every action below can be requested by exact name through action=,
and all of them except tenant.setting.change are in the set a collector receives when
it sends no action= at all. See
Ask for the actions you want, explicitly for the
filter's matching rule and where to get the full list. The blocked_* refusals are on the
activity feed and are selected with event= instead.
Refusals: a credential or caller was told no
| Action | Fires when | Feed |
|---|---|---|
auth.token.rejected |
a credential was presented and did not resolve. Reasons: invalid, tenant_mismatch. Counted per window |
auth |
auth.capability.denied |
a resolved credential lacked the capability for the operation; carries required and granted. Counted per window |
auth |
oci.scope_denied |
an OCI request was refused on the token's capabilities; names the token as the actor, with what the route required and what the token grants. Written at most once per token and route every 10 minutes | auth |
ratelimit.rejected |
a request was refused by a rate limiter. Counted per window | auth |
metrics.scrape_denied |
a /metrics scrape came from an address that is not allowed |
auth |
blocked_* |
a pull refused by policy: licence, KEV, malicious, provenance, release age and the rest | activity |
login.failure, lockout.triggered |
failed sign-in, lockout | auth |
"Counted per window" means the row is aggregated; see Aggregated events and what the count means.
auth.token.rejected with reason=tenant_mismatch is the cross-tenant credential probe: a valid
token from one organization presented against another. The row under the target organization is
deliberately actor-less, because naming the presenting credential there would leak one tenant's
token identity into another tenant's audit trail.
Identity and credential lifecycle
mfa.* (enrolment, disabling, recovery codes regenerated or used, trusted devices added or
used) is a real dotted family: one action=mfa filter selects all of it.
The SAML role and config-change actions look like they should be families too, but the family
filter matches only on the dot. auth.saml.role_assigned and saml.config_updated split on an
underscore, not a dot, so naming auth.saml.role or saml.config selects nothing. The nearest
dotted families, auth.saml and saml, are wider: auth.saml also brings SAML logins and user
provisioning. To get exactly the role and config changes, name each one:
auth.saml.role_assigned, auth.saml.role_changed, auth.saml.role_change_refused,
auth.saml.role_mapping_blocked, saml.config_updated, saml.config_deleted.
The same goes for user.password_changed and user.email_changed, which sit in the wider
user family, and for the flat names, which have no family at all: token_created,
token_revoked, service_token_created, service_token_revoked, member_role_changed,
member_removed, invite_accept_blocked, allowlist_blocked.
Configuration
tenant.setting.change carries the setting key with its prior_value and new_value. It
records changes to version_overwrite_policy, air_gapped, require_mfa and
rpm_upstream_mode. Treat a change to an enforcement control such as the overwrite policy or
the MFA requirement differently from a cosmetic one; weakening a control is the canonical
post-account-takeover step. It is not in the no-filter set, so name it in action=.
Other enforcement settings are recorded under their own actions, such as
license_policy_mode_changed and saml.config_updated. The full list is on
/api/v1/siem/actions.
Reading partition
Aggregated rows carry a partition identifying who was refused. It uses several namespaces and you
must branch on the prefix rather than parse it as an address: ip: or a bare address,
user:<subject>, token:<reference>, proto:<address>, and <address>:<policy>. Note token:
means different things on different actions: a truncated token id on auth.*, a hash prefix of the
presented credential on ratelimit.*. A partition of overflow marks a folded row (see
Aggregated events).
Building a collector that does not lose data
Each property below corresponds to a way a naive collector silently fails.
Advance the watermark only after a fully successful read
Poll [watermark, upper bound], follow next_cursor to exhaustion, and only then move
the watermark to that run's upper bound. If any page fails, leave it alone and re-read
the window next time. The cost is duplicates, which you can absorb; the alternative
is a silent hole nobody ever notices.
On the auth feed, send an explicit until and advance to it. Without one, until
is the moment the server handled the request, which the response does not report.
On the activity feed, advance to the until the response returns, not the one you
sent. The feed never serves past now minus SIEM_ACTIVITY_LAG_SECONDS (default 30),
because activity rows are timestamped before they are written, so the served window
can end earlier than you asked. It returns until only on the last page; on a page
with a next_cursor it is null, because older rows in the window are still behind
the cursor.
De-duplicate on event id
The window includes its start and its end, so re-supplying the previous upper bound as the next lower bound can return one event twice. Keep a small ring of recently emitted ids.
Ask for the actions you want, explicitly
GET /api/v1/siem/actions publishes the whole declared vocabulary for the auth feed:
every action name with a security_relevant flag, the ones the no-filter feed serves
(default_actions), the dotted families the vocabulary implies (family_prefixes), and
the limits on how many values one request may carry.
The repeatable action filter matches the action of exactly that name, plus every
action in its dotted family. action=checksum_failure selects that one action;
action=auth selects all of auth.*. A trailing separator is optional and ignored,
so action=login. and action=login are the same filter. An unrecognized value is
not an error; it matches nothing, so a collector written against a newer instance
keeps working against an older one.
Name the actions you want rather than inheriting the default. The default set is the
security vocabulary as of the release you are running, and it widens on upgrade, which
means new event types start arriving without anyone deciding they should. Pinning the
set your detections understand is the safer subscription and also the cheaper query:
naming a declared action is free, naming a family or an undeclared name is not. Diff
your pinned list against /api/v1/siem/actions when you upgrade, so a family added in
a release is a decision rather than a surprise.
That endpoint publishes the limits too. max_action_filters bounds the total values in
one request. max_family_filters bounds how many of them may be dotted families or names
this release does not declare. You will meet the family limit first: it is a single
budget shared between the families you name deliberately and any values the instance
does not recognize. A request over either limit is a 400 that names it.
Treat your own failure as an event
A dead collector and a quiet registry look identical from the SIEM. Emit a record
when a poll fails, and alert on it. On the auth feed, use latest_event_at in the JSON
envelope. It reports the most recent event visible to you regardless of your action
filter, which is what distinguishes "my filter matches nothing" from "nothing happened"
from "the writer is broken".
Mark backfilled events so they cannot fake a burst
Most SIEMs correlate on ingest time, not on the event's own timestamp. Replaying history therefore lands months of scattered events in one second and manufactures correlations that never happened. A first run that backfills 24 hours can trip a brute-force rule on traffic that did not occur. Stamp each record with whether it was fresh when collected, and exclude backfilled records from any time-window rule.
Aggregated events and what the count means
Some events are emitted on paths a caller can trigger at will: a rejected credential, a
capability denial, a rate-limit refusal. Writing one audit row per occurrence would let anyone
inflate your audit table and your SIEM bill, so auth.token.rejected, auth.capability.denied
and ratelimit.rejected are coalesced: one row per one-minute window carrying a count, rather
than one row per event.
Your detections must account for what follows from that.
Counts are per process. Dependably coalesces in memory, so a deployment running several replicas behind a load balancer emits one row per replica per window, each carrying only what that replica saw. Alert on the total across replicas, never on a single row's count. On a multi-replica deployment a single row always reads low, by a factor of roughly the replica count.
Sum rows sharing the same organization, partition, reason and window_start. That works because
the window label is derived from the instant the event was recorded, floored to a fixed wall-clock
minute, not from when a process started or last flushed. Every replica therefore labels the
same event identically, and the sum is the real total.
A burst spanning a minute boundary is reported as two windows rather than one. That is correct, not a rounding artefact, but a threshold rule of the form "N in one window" will see two smaller windows instead of one large one. Threshold over a rolling range rather than over a single window label.
Resolution degrades under a spray, the total does not. The accumulator is bounded. A caller
generating a very large number of distinct keys (many source addresses, say) will first cause
new keys to fold into an overflow bucket, and then into a saturation bucket. Those rows carry
overflow as their partition, and a saturation row carries overflow as its reason too. You
lose the ability to say which partition each denial came from; you do not lose the fact that
they happened, or how many there were.
If you are a tenant rather than the operator, the last-resort saturation rows are written without an organization, so they land in the operator plane and a tenant-scoped collector does not receive them. Under a spray heavy enough to reach saturation, a tenant feed sees the counted overflow rows but not the final fold. Alert on the total and treat the appearance of folded rows as itself a signal: something is generating keys faster than the instance will track them individually.
The payload carries window_start, window_end and a replica identifier (the host name
of the process that counted) so you can group and sum correctly.
Retention. Audit detail and source_ip are scrubbed after AUDIT_LOG_PII_DAYS (90 by
default), and the row itself is deleted after AUDIT_LOG_RETENTION_DAYS (365 by default). The
count, window and partition live in detail, so they do not survive the first horizon. The
event's action and ecosystem do. If you need the detail long-term, your SIEM's own retention is
what preserves it, not Dependably's.
Push, for lower latency or a syslog-native SIEM
Everything above is pull: you poll the feeds. Dependably can also push, opening an
outbound connection of its own to a collector you run, for lower latency or for a SIEM whose
native input is a webhook or a syslog listener rather than a REST API to poll. Set
SIEM_WEBHOOK_URL or SIEM_SYSLOG_HOST and restart the instance; push runs in addition to
pull, never instead of it, since pull is what backfills and survives a collector outage.
| Variable | Default | Effect |
|---|---|---|
SIEM_WEBHOOK_URL |
unset | HTTPS endpoint Dependably POSTs one NDJSON line per event to. A plaintext URL is refused at startup; set SIEM_WEBHOOK_ALLOW_INSECURE=true to allow one anyway. |
SIEM_WEBHOOK_BEARER |
unset | Bearer token sent with each POST, if your collector needs one. |
SIEM_WEBHOOK_ALLOW_PRIVATE |
true |
Set false to require a public collector address instead of an RFC 1918 one. Loopback, link-local and cloud-metadata addresses are refused either way. |
SIEM_SYSLOG_HOST |
unset | Syslog receiver hostname. Activates the syslog forwarder; ignored if SIEM_WEBHOOK_URL is also set. |
SIEM_SYSLOG_PORT |
514 |
Syslog receiver port. |
SIEM_SYSLOG_PROTO |
tls |
udp, tcp, or tls. udp and tcp send events in cleartext and log a startup warning naming the exposure. |
SIEM_SYSLOG_FORMAT |
cef |
cef (ArcSight Common Event Format) or rfc5424. |
SIEM_QUEUE_CAPACITY |
1024 |
Outbound queue depth. A full queue drops the new event, with a metric, rather than blocking the request that generated it. |
Only one forwarder runs at a time. Setting SIEM_WEBHOOK_URL and SIEM_SYSLOG_HOST
together activates the webhook one. A delivery that fails is retried after 1, 5 and 30 seconds, then
dropped.
Push carries less than pull. It has no source_ip, no ecosystem or purl, and its action
names are not the ones documented above for the same event: a hosted publish is push on the
pull feed and package.publish on push. Do not try to join pull and push records by action
name. Treat push as a latency add-on and pull as the feed a SOC actually alerts on.
Known limits
Be explicit with your SOC about these rather than letting them discover them.
source_ipis only as good as your proxy configuration, on Dependably's side and on the proxy's. Each has to be right on its own, and neither shows up as an error on the feed. WhenTRUSTED_PROXIESis unset, Dependably discards forwarded headers by design and records the immediate peer, which behind a reverse proxy or container bridge is the same address for every request; set it to your proxy's address (see Behind a reverse proxy). Separately, your reverse proxy has to actually be configured to sendX-Forwarded-Forin the first place. A proxy that isn't (some GUI-managed reverse-proxy tools default to not forwarding it) leaves nothing forTRUSTED_PROXIESto act on, and the symptom is identical either way: every event shows the proxy's own address. Confirm a fresh event'ssource_ipafter changing either setting; do not assume the fix landed from the config change alone. Once the proxy andTRUSTED_PROXIESare correct, per-source brute-force correlation, geo-IP enrichment and IP blocklisting all become usable.- Failed logins carry no actor. A
login.failurerow records neither the account attempted nor an email. You can count a burst; you cannot attribute it. - The feed names actors by identifier, not by name. On the auth feed,
orgSlugis served besideorgId, so an alert reads as a tenant rather than a 32-hex id, but actors are not resolved:actorEmailis deliberately always null, because a denormalized email would be personal data sitting outside the erasure and retention sweeps. The activity feed carriesorgIdonly; you named the organization when you asked. Enrich actor identifiers on the SIEM side if your analysts need names. - The feed is a personal-data egress point: forwarded events carry actor identifiers and payloads. If your collector or SIEM sits in another jurisdiction, that is a cross-border transfer to account for under your own compliance regime.