Event stores
Zentral events are persisted and forwarded by event stores. Multiple stores can be used at the same time: each event is offered to every store, and each store decides whether to keep it, based on its event filters.
Each store that can write events gets its own queue and its own store worker. One store – the admin console store – is also used to fetch the events displayed in the Zentral UI.
Available backends
| Backend | Options key | Writes events | Admin console | Batching | Terraform |
|---|---|---|---|---|---|
CLICKHOUSE
|
clickhouse_kwargs |
yes | yes | 1 – 1000, default 100 | no |
DATADOG
|
datadog_kwargs |
yes | yes | no | no |
ELASTICSEARCH
|
elasticsearch_kwargs |
yes | yes | 1 – 500, default 1 | no |
HTTP
|
http_kwargs |
yes | no | no, but see concurrency |
yes |
KINESIS
|
kinesis_kwargs |
yes | no | 1 – 500, default 1 | yes |
OPENSEARCH
|
opensearch_kwargs |
yes | yes | 1 – 500, default 1 | no |
PANTHER
|
panther_kwargs |
yes | no | 1 – 100, default 1 | yes |
S3_PARQUET
|
s3_parquet_kwargs |
yes | no | 100 – 100000, default 10000 | no |
SNOWFLAKE
|
snowflake_kwargs |
no | yes | n/a | no |
SPLUNK
|
splunk_kwargs |
yes | with search_url |
1 – 100, default 1 | yes |
SUMO_LOGIC
|
sumo_logic_kwargs |
yes | no | 1 – 100, default 1 | no |
Options key is the object that carries the options of the backend, alongside the common store options
. Terraform indicates whether the zentral_store
resource covers the backend; its nested attribute is the options key without the _kwargs suffix. Admin console indicates whether the backend can fetch events back for display in the Zentral UI. Some backends – Datadog, Elasticsearch, OpenSearch and Splunk – can also display links to the events in their own UI; those require extra options, listed in the section of each backend.
The DATADOG, PANTHER, SNOWFLAKE, SPLUNK and SUMO_LOGIC backends are part of the Zentral Pro Edition.
Where stores are configured
Stores are database objects, not a section of the base.json configuration file. There are three ways to manage them:
- the API, at
/api/stores/(see API for the authentication options), - the Zentral Terraform provider
, with the
zentral_storeresource, which is built on that API. It covers four of the backends – see the backend overview , - provisioning, from the Zentral configuration, for stores that must be deployed with the instance.
The Zentral UI at /stores/ is read-only: it lists the configured stores and displays their settings, with the secrets redacted. The stores.view_store permission is required.
A store created by provisioning carries a provisioning_uid and can only be changed by updating the configuration: the API refuses to update or delete it, and its backend and backend options are omitted from the API representation. Stores created through the API are called custom stores, and their number is limited – see max_custom_store_count
.
Configuration section options
The event store app itself is configured under the apps → zentral.core.stores key.
max_custom_store_count
OPTIONAL
An integer, 3 by default. The maximum number of stores that can be created through the API. Stores created by provisioning are not counted.
provisioning
OPTIONAL
A dictionary with a single stores key, itself a dictionary of store specifications. The key of each specification is its provisioning UID – a stable identifier of your choosing, used to match a specification with the store it created. Renaming the key on an existing store creates a second store.
{
…
"apps": {
"zentral.core.stores": {
"max_custom_store_count": 5,
"provisioning": {
"stores": {
"main-store": {
"name": "ClickHouse",
"admin_console": true,
"backend": "CLICKHOUSE",
"clickhouse_kwargs": {
"host": "clickhouse.example.com",
"password": "{{ env:CLICKHOUSE_PASSWORD }}"
}
}
}
}
}
}
}
The standard configuration substitutions are available for every value: "{{ env:ENV_VAR_NAME }}" to read an environment variable, "{{ file:FILE_PATH }}" to read a file, and "{{ secret:NAME_OF_THE_SECRET }}" to read an AWS or GCP secret. Use them for the secrets – they are encrypted with the configured secret engine
when the store is saved.
Common store options
These options apply to every store, whatever the backend, and are used both by the API and by provisioning.
| Option | Type | Required | Default | Description |
|---|---|---|---|---|
name |
string | yes | A unique name. Its slugified form must be unique too – it is used to derive the name of the store queue on some queue backends. | |
description |
string | no | "" |
A free-form description. |
admin_console |
boolean | no | false |
Use this store to fetch the events displayed in the Zentral UI. Only one store can have it, and its backend must support reading events – see the backend overview . |
event_filters |
object | no | {} |
Which events the store keeps. See event_filters
. |
events_url_authorized_roles |
list | no | [] |
Restrict the links to the events in the store to the members of these roles . Empty means all users get the links. In the API, roles are referenced by primary key; in a provisioning specification, by the provisioning UID of a provisioned role. |
backend |
string | yes | The backend, as an upper-case identifier. See the backend overview . | |
<backend>_kwargs |
object | yes | The options of the chosen backend. Exactly one such key must be present, and it must be the one matching backend – for example splunk_kwargs for the SPLUNK backend. |
event_filters
An object with two optional keys, excluded_event_filters and included_event_filters. In Terraform, this is the event_filters
attribute of zentral_store. Each is a list of filters, and each filter is an object with optional tags, event_type and routing_key attributes, whose values are non-empty lists of strings.
{
"event_filters": {
"excluded_event_filters": [
{"tags": ["munki", "santa"]},
{"event_type": ["osquery_result"], "routing_key": ["important"]},
{"event_type": ["zentral_login", "zentral_logout"]}
]
}
}
With these filters, the following events are excluded:
munkiorsantatagged eventsosquery_resultevents with theimportantrouting_keyvaluezentral_loginorzentral_logoutevents
Boolean combinations: arrays/lists → OR, dictionaries/objects → AND.
- Within
excluded_event_filters– orincluded_event_filters– the different filters are combined using theORoperator. - Within each filter, the different attributes must all match (
AND). - For each filter attribute, at least one value must match (
OR).
The excluded_event_filters take precedence over the included_event_filters. If an event is a match for the excluded_event_filters, the included_event_filters are not evaluated, and the event is excluded.
If neither excluded_event_filters nor included_event_filters is set, all events are included in the store.
Backend options
One section per backend, listing what goes in its <backend>_kwargs object.
ClickHouse
The events table, a few aggregation tables and their materialized views are created by Zentral when the store worker starts. The events table is partitioned by day, and the events expire after ttl_days. The aggregation tables – used for the event type and tag histograms, and for the machine heartbeats – have their own fixed retention of 31 days.
| Option | Type | Required | Default | Description |
|---|---|---|---|---|
host |
string | yes | The ClickHouse host. | |
port |
integer | no | 8443 |
1 – 65535. |
secure |
boolean | no | true |
Use HTTPS. |
verify |
boolean | no | true |
Verify the TLS certificate. |
compress |
boolean | no | true |
Compress the requests and responses. |
username |
string | no | Used with password. |
|
password |
string | no | "" |
Secret. Used with username. |
access_token |
string | no | Secret. JWT access token. Cannot be combined with username or password. |
|
database |
string | no | default |
Must match ^[a-zA-Z_][0-9a-zA-Z_]*$. |
table_name |
string | no | zentral_events |
Must match ^[a-zA-Z_][0-9a-zA-Z_]*$. |
table_engine |
string | no | MergeTree |
Must match ^[a-zA-Z_][0-9a-zA-Z_]*$. |
ttl_days |
integer | no | 90 |
Minimum 1. How long the events are kept. |
connect_timeout |
integer | no | 10 |
In seconds. Minimum 1. |
send_receive_timeout |
integer | no | 300 |
In seconds. Minimum 1. |
batch_size |
integer | no | 100 |
1 – 1000. |
Example
{
"name": "ClickHouse",
"admin_console": true,
"backend": "CLICKHOUSE",
"clickhouse_kwargs": {
"host": "clickhouse.example.com",
"username": "zentral",
"password": "{{ env:CLICKHOUSE_PASSWORD }}",
"database": "zentral",
"ttl_days": 400,
"batch_size": 500
}
}
Datadog
Events are sent to the Datadog log intake. Reading the events back – required for the admin console store – uses the Datadog log API, and needs an application_key in addition to the api_key.
| Option | Type | Required | Default | Description |
|---|---|---|---|---|
site |
string | yes | One of datadoghq.com, us3.datadoghq.com, us5.datadoghq.com, datadoghq.eu, ddog-gov.com, ap1.datadoghq.com, ap2.datadoghq.com. See the Datadog site documentation
. |
|
api_key |
string | yes | Secret. Used to send the events. | |
application_key |
string | no | Secret. Required to read the events back, i.e. to use the store as the admin console store. | |
service |
string | no | Zentral |
The service value of the Datadog logs. |
source |
string | no | zentral |
The ddsource value of the Datadog logs. |
Links to the events in the Datadog UI are always available.
Example
{
"name": "Datadog",
"admin_console": true,
"backend": "DATADOG",
"datadog_kwargs": {
"site": "datadoghq.eu",
"api_key": "{{ env:DATADOG_API_KEY }}",
"application_key": "{{ env:DATADOG_APPLICATION_KEY }}",
"service": "zentral.example.com"
}
}
Elasticsearch
| Option | Type | Required | Default | Description |
|---|---|---|---|---|
hosts |
list of strings | yes | At least one http:// or https:// URL. |
|
verify_certs |
boolean | no | true |
Verify the TLS certificates. |
ssl_show_warn |
boolean | no | true |
Display a warning when the TLS certificates are not verified. |
username |
string | no | Basic authentication. Requires password. |
|
password |
string | no | Secret. Basic authentication. Requires username. |
|
index |
string | no | The index the events are written to. Mutually exclusive with indices; one of the two is required. |
|
indices |
list of objects | no | Multiple indices, chosen per event. See Multiple indices . | |
read_index |
string | no | index |
The index or alias used to read the events back. Required when indices is used. |
number_of_shards |
integer | no | 1 |
Minimum 1. Used when Zentral creates an index. |
number_of_replicas |
integer | no | 0 |
Minimum 0. Used when Zentral creates an index. |
batch_size |
integer | no | 1 |
1 – 500. |
kibana_discover_url |
string | no | For example https://kibana.example.com/app/discover. If set, links to the events in Kibana are displayed in the Zentral UI. |
|
kibana_index_pattern_uuid |
string | no | The UUID of the Kibana data view used in those links. |
Multiple indices
Instead of a single index, a list of indices can be configured, so that different events land in different indices – to give them a different retention or a different storage class, for example.
Each entry is an object with a name, a priority and, optionally, included_event_filters and excluded_event_filters – the same filter syntax as event_filters
. For each event, the matching index with the highest priority wins.
- All the names must be different, and all the priorities must be different.
- The index with the lowest priority is the default index. It cannot be filtered, so that no event is left without an index.
read_indexis required, and should point to an alias covering every index you want to search from the Zentral UI.
Example
{
"name": "Elasticsearch",
"admin_console": true,
"backend": "ELASTICSEARCH",
"elasticsearch_kwargs": {
"hosts": ["https://elasticsearch.example.com:9200"],
"username": "zentral",
"password": "{{ env:ELASTICSEARCH_PASSWORD }}",
"batch_size": 100,
"indices": [
{"name": "zentral-osquery", "priority": 10,
"included_event_filters": [{"tags": ["osquery"]}]},
{"name": "zentral-other", "priority": 1}
],
"read_index": "zentral-all",
"kibana_discover_url": "https://kibana.example.com/app/discover",
"kibana_index_pattern_uuid": "00000000-0000-0000-0000-000000000000"
}
}
HTTP
Terraform resource: zentral_store
, with an http
attribute.
Events are POSTed one by one, as JSON, to an arbitrary endpoint.
| Option | Type | Required | Default | Description |
|---|---|---|---|---|
endpoint_url |
string | yes | The http:// or https:// URL the events are POSTed to. For example https://acme.service-now.com/api/now/import/zentral_events. |
|
verify_tls |
boolean | no | true |
Verify the TLS certificate. |
username |
string | no | Basic authentication. Requires password. |
|
password |
string | no | Secret. Basic authentication. Requires username. |
|
headers |
list of objects | no | [] |
Extra headers, each an object with a name and a value. The values are secrets. The Content-Type header is set to application/json. An Authorization header cannot be combined with username/password. |
request_timeout |
integer | no | 120 |
In seconds. 1 – 600. |
max_retries |
integer | no | 3 |
1 – 5. |
concurrency |
integer | no | 1 |
1 – 20. The number of threads used to post the events, to increase the throughput of the store worker. Only works with the AWS SNS/SQS queues backend. |
Example
{
"name": "ServiceNow",
"backend": "HTTP",
"event_filters": {
"included_event_filters": [
{"event_type": ["add_machine", "add_machine_os_version", "remove_machine_os_version"]}
]
},
"http_kwargs": {
"endpoint_url": "https://acme.service-now.com/api/now/import/zentral_events",
"username": "Zentral",
"password": "{{ env:SERVICE_NOW_API_PASSWORD }}",
"concurrency": 5
}
}
Kinesis
Terraform resource: zentral_store
, with a kinesis
attribute.
This store is capable of batch operation. The maximum batch_size is 500. See the kinesis:PutRecords
documentation for more details.
| Option | Type | Required | Default | Description |
|---|---|---|---|---|
stream |
string | yes | The name of the Kinesis stream. | |
region_name |
string | yes | The AWS region of the stream. | |
serialization_format |
string | yes | zentral for the Zentral canonical serialization, or firehose_v1 for a format optimized for Kinesis Firehose. |
|
aws_access_key_id |
string | no | Requires aws_secret_access_key. Cannot be combined with assume_role_arn. |
|
aws_secret_access_key |
string | no | Secret. Requires aws_access_key_id. |
|
assume_role_arn |
string | no | The ARN of a role to assume. Cannot be combined with aws_access_key_id. |
|
batch_size |
integer | no | 1 |
1 – 500. |
AWS authentication and authorization
When operating in AWS, it is recommended to use a role attached to the EC2 instance or to the container to authenticate the calls to the Kinesis API, and to leave aws_access_key_id and aws_secret_access_key empty.
Example of an IAM policy to allow Zentral to write to the Kinesis stream:
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "AllowKinesisPut",
"Action": [
"kinesis:PutRecords",
"kinesis:PutRecord"
],
"Effect": "Allow",
"Resource": "arn:aws:kinesis:<AWS_REGION>:<AWS_ACCOUNT_ID>:stream/<KINESIS_STREAM_NAME>"
}
]
}
The PutRecord action can be omitted if the store is configured for batch operations, i.e. with a batch_size greater than 1.
Example
{
"name": "Kinesis",
"backend": "KINESIS",
"kinesis_kwargs": {
"stream": "name_of_the_stream",
"region_name": "us-east-1",
"serialization_format": "firehose_v1",
"assume_role_arn": "arn:aws:iam::<ACCOUNT_ID>:role/<NAME_OF_THE_ROLE>",
"batch_size": 500
}
}
OpenSearch
Use this store with a managed AWS OpenSearch domain. It takes all the Elasticsearch options
– including multiple indices
– plus aws_auth, to sign the API calls with the standard AWS signatures.
| Option | Type | Required | Default | Description |
|---|---|---|---|---|
aws_auth |
object | no | If omitted, the API calls are not signed. Cannot be combined with username/password. |
|
aws_auth.region_name |
string | yes | The AWS region of the OpenSearch domain. Required when aws_auth is used. |
|
aws_auth.aws_access_key_id |
string | no | Requires aws_auth.aws_secret_access_key. |
|
aws_auth.aws_secret_access_key |
string | no | Secret. Requires aws_auth.aws_access_key_id. |
It is recommended to use a role attached to the EC2 instance or to the container, and to leave aws_auth.aws_access_key_id and aws_auth.aws_secret_access_key empty: when they are, the credentials are resolved with the default boto3 mechanisms
.
Use kibana_discover_url and kibana_index_pattern_uuid for the OpenSearch Dashboards, for example https://example-00000000000000000000000000.us-east-1.es.amazonaws.com/_dashboards.
Example
{
"name": "OpenSearch",
"admin_console": true,
"backend": "OPENSEARCH",
"opensearch_kwargs": {
"hosts": ["https://example-00000000000000000000000000.us-east-1.es.amazonaws.com"],
"index": "zentral-events",
"batch_size": 100,
"kibana_discover_url": "https://example-00000000000000000000000000.us-east-1.es.amazonaws.com/_dashboards",
"kibana_index_pattern_uuid": "00000000-0000-0000-0000-000000000000",
"aws_auth": {"region_name": "us-east-1"}
}
}
Panther
Terraform resource: zentral_store
, with a panther
attribute.
Zentral can send events to a Panther HTTP log source, with Bearer authentication. A custom schema must be configured in Panther – use schema.yml
from the Zentral repository.
| Option | Type | Required | Default | Description |
|---|---|---|---|---|
endpoint_url |
string | yes | The Panther HTTP Log Source
URL. For example https://logs.example.runpanther.net/http/00000000-0000-0000-0000-000000000000. |
|
bearer_token |
string | yes | Secret. The token used for Bearer authentication. | |
batch_size |
integer | no | 1 |
1 – 100. |
Example
{
"name": "Panther",
"backend": "PANTHER",
"panther_kwargs": {
"endpoint_url": "https://logs.example.runpanther.net/http/00000000-0000-0000-0000-000000000000",
"bearer_token": "{{ env:PANTHER_BEARER_TOKEN }}",
"batch_size": 100
}
}
S3 Parquet
Events are batched and written to an S3 bucket as Parquet files, for querying with Athena, Snowflake, DuckDB, … Each file is written to <prefix>/<YYYY>/<MM>/<DD>/<writer id>/<batch index>.parquet, where the writer ID is unique to each store worker run.
This backend only operates in batch mode: batch_size cannot be lower than 100. The max_batch_age_seconds option bounds how long a partial batch is held before being flushed.
| Option | Type | Required | Default | Description |
|---|---|---|---|---|
bucket |
string | yes | The name of the S3 bucket. | |
region_name |
string | yes | The AWS region of the bucket. | |
prefix |
string | no | "" |
A prefix for the object keys. Include the trailing / if you want one. |
aws_access_key_id |
string | no | Requires aws_secret_access_key. Cannot be combined with assume_role_arn. |
|
aws_secret_access_key |
string | no | Secret. Requires aws_access_key_id. |
|
assume_role_arn |
string | no | The ARN of a role to assume. Cannot be combined with aws_access_key_id. |
|
batch_size |
integer | no | 10000 |
100 – 100000. The number of events per Parquet file. |
max_batch_age_seconds |
integer | no | 300 |
10 – 1200. How long a partial batch is held before it is written. |
As with the other AWS backends, it is recommended to use a role attached to the EC2 instance or to the container, and to leave aws_access_key_id and aws_secret_access_key empty. The role must be allowed to perform the s3:PutObject action on the bucket.
Example
{
"name": "S3 Parquet",
"backend": "S3_PARQUET",
"s3_parquet_kwargs": {
"bucket": "acme-zentral-events",
"prefix": "events/",
"region_name": "us-east-1",
"batch_size": 50000,
"max_batch_age_seconds": 600
}
}
Snowflake
The Snowflake backend is read-only: it has no store worker, and can only be used as the admin console store. To get the events into Snowflake, set up a pipeline with the Kinesis or S3 Parquet backend – with Kinesis Firehose or Snowpipe, for example.
| Option | Type | Required | Default | Description |
|---|---|---|---|---|
account |
string | yes | The name of the Snowflake account. | |
user |
string | yes | The name of the Snowflake user. | |
password |
string | yes | Secret. The password of the Snowflake user. | |
database |
string | yes | The name of the Snowflake database. | |
role |
string | yes | The name of the Snowflake role. | |
warehouse |
string | yes | The name of the Snowflake warehouse. | |
schema |
string | no | PUBLIC |
The name of the Snowflake schema. |
session_timeout |
integer | no | 13800 |
In seconds, minimum 60. Defaults to 4 hours – the Snowflake default – minus 10 minutes. After the current session has timed out, a new connection is established if necessary. |
Example
{
"name": "Snowflake",
"admin_console": true,
"backend": "SNOWFLAKE",
"snowflake_kwargs": {
"account": "acme",
"user": "ZENTRAL",
"password": "{{ env:SNOWFLAKE_PASSWORD }}",
"database": "ZENTRAL",
"schema": "ZENTRAL",
"role": "ZENTRAL",
"warehouse": "DEFAULTWH"
}
}
Splunk
Terraform resource: zentral_store
, with a splunk
attribute.
Events are written with the Splunk HTTP Event Collector. Two optional extras are available:
- set
search_app_urlto display links to the events in the Splunk UI, - set both
search_urlandsearch_tokento let Zentral fetch the events back, i.e. to use the store as the admin console store.
| Option | Type | Required | Default | Description |
|---|---|---|---|---|
hec_url |
string | yes | The base URL of the Splunk HTTP Event Collector, for example https://splunk.example.com:8088. The path to the collector endpoint must not be included. |
|
hec_token |
string | yes | Secret. The HEC token. | |
hec_extra_headers |
list of objects | no | [] |
Extra headers for the HEC requests, each an object with a name and a value. The values are secrets. Can be used to authenticate with a proxy. The Authorization and Content-Type headers cannot be changed. |
hec_request_timeout |
integer | no | 300 |
In seconds, minimum 1. |
hec_index |
string | no | The name of the Splunk index. Do not use it if the index is set by the HEC. | |
hec_source |
string | no | The source of the Splunk events. Do not use it if the source is set by the HEC. |
|
batch_size |
integer | no | 1 |
1 – 100. |
serial_number_field |
string | no | machine_serial_number |
The Splunk event field used for the machine serial number. |
computer_name_as_host_sources |
list of strings | no | [] |
Inventory source names used to find a hostname to set as the host value of the Splunk event. The first source with a non-empty value wins. Empty means the machine serial number is used. |
custom_host_field |
string | no | If set, the event metadata host field value is copied to this event field. | |
verify_tls |
boolean | no | true |
Verify the TLS certificates, for both the HEC and the search requests. |
search_app_url |
string | no | The URL of the Splunk search app, for example https://splunk.example.com/en-US/app/search/search. If set, links to the events in Splunk are displayed in the Zentral UI. |
|
search_url |
string | no | The base URL of the Splunk API server, for example https://splunk.example.com:8089. With search_token, allows the store to be used as the admin console store. |
|
search_token |
string | no | Secret. The authentication token for the Splunk API server. With search_url, allows the store to be used as the admin console store. |
|
search_extra_headers |
list of objects | no | [] |
Extra headers for the search API requests, each an object with a name and a value. The values are secrets. The Authorization and Content-Type headers cannot be changed. |
search_index |
string | no | If set, an index filter is added to the search jobs and URLs. |
|
search_source |
string | no | If set, a source filter is added to the search jobs and URLs. Use this for example if a single Splunk index is used for multiple Zentral instances. |
|
search_request_timeout |
integer | no | 300 |
In seconds, minimum 1. |
Example
{
"name": "Splunk",
"admin_console": true,
"backend": "SPLUNK",
"splunk_kwargs": {
"hec_url": "https://splunk.example.com:8088",
"hec_token": "{{ env:SPLUNK_HEC_TOKEN }}",
"hec_extra_headers": [
{"name": "CF-Access-Client-Id", "value": "123"},
{"name": "CF-Access-Client-Secret", "value": "{{ env:SPLUNK_HEC_CF_ACCESS_CLIENT_SECRET }}"}
],
"hec_request_timeout": 30,
"hec_index": "zentral",
"hec_source": "zentral.example.com",
"batch_size": 100,
"serial_number_field": "serial_number",
"computer_name_as_host_sources": ["santa", "osquery"],
"search_app_url": "https://splunk.example.com/en-US/app/search/search",
"search_url": "https://splunk.example.com:8089",
"search_token": "{{ env:SPLUNK_SEARCH_TOKEN }}",
"search_extra_headers": [
{"name": "CF-Access-Client-Id", "value": "456"},
{"name": "CF-Access-Client-Secret", "value": "{{ env:SPLUNK_SEARCH_CF_ACCESS_CLIENT_SECRET }}"}
],
"search_source": "zentral.example.com",
"verify_tls": true
}
}
Sumo Logic
Events are sent to a Sumo Logic HTTP collector.
| Option | Type | Required | Default | Description |
|---|---|---|---|---|
collector_url |
string | yes | Secret. The URL of the Sumo Logic HTTP source. It carries the authentication, and is stored encrypted. | |
batch_size |
integer | no | 1 |
1 – 100. |
Example
{
"name": "Sumo Logic",
"backend": "SUMO_LOGIC",
"sumo_logic_kwargs": {
"collector_url": "{{ env:SUMO_LOGIC_COLLECTOR_URL }}",
"batch_size": 100
}
}