SIEM and audit forwarding
Stream audit ledger records to syslog, HTTP, Splunk HEC, Microsoft Sentinel, Google SecOps and CloudWatch Logs, keep a write-once copy in object storage, and import provider audit logs into the ledger.
The audit ledger is MAQPNA's append-only, hash-chained record of every decision, approval, revocation and result. Your security team usually wants those records in its SIEM as well. The gateway streams them there itself: each sink has a durable cursor on the audit volume and delivers records at least once. You can also ship sealed ledger segments to write-once storage (WORM), and pull provider audit logs (AWS CloudTrail, OpenAI, Anthropic) into the ledger.
flowchart LR
G["MAQPNA gateway"] --> L[("audit ledger")]
L -->|"cursor per sink"| S1["syslog<br/>tcp:// or tls://"]
L --> S2["http<br/>OCSF, JSON"]
L --> S3["splunk-hec"]
L --> S4["sentinel"]
L --> S5["chronicle"]
L --> S6["cloudwatch"]
L --> S7["jsonl file"]
L -->|"sealed segments"| W[("WORM<br/>S3 Object Lock, GCS,<br/>Azure immutable Blob")]
I["maqpna audit import<br/>CloudTrail, OpenAI, Anthropic"] -->|"decision observe"| L
When to use it#
- Your SOC correlates agent activity with other security events.
- Regulation or policy needs a copy of the evidence outside the cluster that no one can delete.
- You want to see what happened at a model provider even when it did not pass the gateway.
For reading, verifying and exporting the ledger itself, see Read and verify the audit ledger.
Prerequisites#
- MAQPNA installed with the Helm chart. Keep the ledger on a persistent volume (
gateway.auditStorage.persistent: true) so cursors survive restarts. - Network access from the gateway to the SIEM endpoint. Every sink connection goes through the residency-checking dialer.
- With
sovereignty.enabled: sinks must use TLS (tls://orhttps://) and their host must be insovereignty.allowedEgressHosts. The chart refuses anything else, and the gateway checks it again at start-up.
Supported sinks#
type |
Destination | Required fields | Default format |
|---|---|---|---|
syslog |
Any syslog receiver (RFC 5424 messages) | address (tcp://host:port or tls://host:port) |
rfc5424-json |
http |
Any HTTP(S) collector | url |
ocsf |
jsonl |
A local file | path |
json |
splunk-hec |
Splunk HTTP Event Collector (/services/collector/event) |
url (HEC base URL, https:// unless loopback), tokenSecret |
json |
sentinel |
Microsoft Sentinel, through the Azure Monitor Logs Ingestion API | url (data collection endpoint), dcrImmutableId (dcr-<32 hex>), streamName (Custom-<Table>) |
json |
chronicle |
Google SecOps | url (instance URL, https://<region>-chronicle.googleapis.com/v1alpha/projects/<p>/locations/<l>/instances/<id>), chronicleMode (logs or udm), logType for logs |
json |
cloudwatch |
Amazon CloudWatch Logs (PutLogEvents); Security Lake through Firehose |
region, logGroup; logStream defaults to maqpna-gateway |
ocsf |
Formats are json, rfc5424-json, cef and ocsf. On sentinel and chronicle, only json and ocsf are allowed. Syslog always sends RFC 5424 messages; format selects their MSG part.
Common optional fields: batchSize (default 100), pollIntervalMillis (default 1000), timeoutSeconds (default 15), facility (syslog, default 13), sdId (default maqpna@32473), serverName, maxBatchBytes, deadLetterPath, deadLetterMaxBytes (default 64 MiB). Sink names must match [a-z0-9][a-z0-9-]{0,39}.
Step 1: Store the credentials in Secrets#
The chart mounts each secret under /etc/maqpna/sinks/<name>/ and fills in the file paths for you:
| Sink field | Secret keys | Used by |
|---|---|---|
tlsSecret |
ca.crt, plus tls.crt and tls.key with mtls: true |
syslog over tls://, https:// collectors |
headersSecret |
headers: one Name: value line per header |
http |
tokenSecret |
token: the HEC token, or an Entra / Google OAuth bearer token |
splunk-hec, sentinel, chronicle |
awsCredentialsSecret |
access-key, secret-key, optional session-token |
cloudwatch |
credentials (no Secret) |
Keyless workload identity: aws (cloudwatch), azure (sentinel), gcp (chronicle) |
Bind it with gateway.serviceAccount.annotations |
Example for Splunk HEC:
kubectl create secret generic maqpna-splunk-hec -n maqpna-system \
--from-literal=token='<your-hec-token>'
Step 2: Add the sinks to the Helm values#
audit:
sinks:
- name: splunk
type: splunk-hec
url: https://splunk.example.eu:8088
index: maqpna
sourceType: maqpna:audit # the default
tokenSecret: maqpna-splunk-hec
- name: soc-syslog
type: syslog
address: tls://siem.example.eu:6514
format: rfc5424-json
tlsSecret: maqpna-siem-tls
- name: sentinel
type: sentinel
url: https://maqpna-dce-abcd.westeurope-1.ingest.monitor.azure.com
dcrImmutableId: dcr-00000000000000000000000000000000
streamName: Custom-MaqpnaAudit_CL
credentials: azure # AKS Entra Workload ID
- name: cloudwatch
type: cloudwatch
region: eu-central-1
logGroup: /maqpna/audit
credentials: aws # EKS Pod Identity or IRSA
Apply with helm upgrade. Keyless credentials use the gateway's cloud workload identity; see Cloud identity & keys and Cloud profiles.
Step 3: Check delivery#
maqpna audit sinks
Output from a local run with one jsonl sink:
ledger head seq 7
NAME TYPE FORMAT CURSOR LAG DELIVERED ERRORS LAST ERROR
siem-file jsonl json 7 0 7 0
--max-lag N exits with status 3 when any sink lags more than N records or reports an error, so you can use it in a health check. The same data is on GET /v1/audit/sinks (admin API) and on the gateway's /metrics:
| Metric | Meaning |
|---|---|
maqpna_gateway_audit_sink_lag{sink} |
Ledger records not yet delivered |
maqpna_gateway_audit_sink_delivered_total{sink} |
Records delivered |
maqpna_gateway_audit_sink_errors_total{sink} |
Delivery errors (retried) |
maqpna_gateway_audit_sink_dead_lettered_total{sink} |
Records the collector rejected permanently |
Authentication, throttling and server errors are retried. Records a collector rejects permanently go to <auditPath>.sinks/<name>.deadletter.jsonl instead of stalling the stream. When the dead-letter file is full, delivery stops rather than drop records.
Step 4: Backfill a sink#
To resend a range of records from a ledger file, run the sink offline with the gateway's configuration:
maqpna audit stream ledger.jsonl --config gateway.json --sink splunk --from-seq 1200
Keep a write-once copy (WORM)#
audit.sink: worm ships sealed ledger segments to object storage with retention locks. At start-up the gateway checks that the store enforces immutability, and refuses to start if it does not.
audit:
sink: worm
worm:
provider: s3 # s3 | gcs | azure
endpoint: https://s3.eu-central-1.amazonaws.com
bucket: maqpna-audit # created with Object Lock
region: eu-central-1
objectLockDays: 365
credentials: static # or aws | gcp | azure (keyless)
credentialsSecret: maqpna-audit-s3 # keys access-key, secret-key
provider |
Store | Static credentials |
|---|---|---|
s3 |
S3 Object Lock (AWS, MinIO, Ceph RGW) | credentialsSecret |
gcs |
GCS Bucket Lock or Object Retention Lock | tokenSecret (key token) |
azure |
Version-level immutable Blob storage | tokenSecret or sasSecret (key sas) |
Import provider audit logs#
An inline gateway sees only what passes through it. maqpna audit import pulls provider logs into a ledger file as evidence. Each event becomes a record with decision observe and server import/<source>, so it is never mistaken for a MAQPNA decision. Imports are incremental and skip events already recorded.
export OPENAI_ADMIN_KEY='<your-openai-admin-key>'
maqpna audit import --source openai --ledger imports.jsonl --since 72h
maqpna audit import --source cloudtrail --ledger imports.jsonl --region eu-central-1 --namespace team-a
Sources are cloudtrail, openai and anthropic. Use a dedicated ledger file, or one no running gateway writes. See Hyperscaler interop for the provider side.
Edge gateways that are offline forward sealed segments to a central gateway with maqpna audit export-segments and maqpna audit forward; see Offline edge gateways.
Verify#
maqpna audit sinks --max-lag 100exits 0.- Search the SIEM for a session ID from
maqpna audit tail; the record'sseqandhashmatch. - With WORM: the gateway is running (it refuses to start against a store without immutability).
Troubleshooting#
| Symptom | Cause | Fix |
|---|---|---|
Helm fails: the sovereign profile requires a TLS endpoint |
A tcp:// or http:// sink with sovereignty.enabled |
Use tls:// or https:// |
Helm fails: SIEM host ... is outside sovereignty.allowedEgressHosts |
The SIEM host is not allowed | Add it to sovereignty.allowedEgressHosts (it must be in-jurisdiction) |
url ... the HEC token would be sent in cleartext |
splunk-hec with http:// to a remote host |
Use https:// |
format "cef" is not supported by type sentinel |
cef or rfc5424-json on a cloud sink |
Use json or ocsf |
Lag grows, ERRORS count rises |
SIEM unreachable, bad token, or the dialer refused the host | Read LAST ERROR; check the Secret and egress |
dead_lettered_total grows |
The collector rejects records (schema, size) | Inspect the dead-letter file; fix the DCR, parser or maxBatchBytes |
credentials ... excludes tokenFile |
Both credentials and a token Secret set |
Use one or the other |