Security bundle
Use this bundle for Drasi control-plane exposure, authentication, network boundaries, secrets, identity, RBAC, supply chain, logging hygiene, and data protection.
Secure-by-default baseline for new projects
- Private network access by default.
- Public management access only through authenticated front door or APIM.
- TLS everywhere outside local development.
- Managed identity where provider-documented and supported.
- Key Vault or Kubernetes Secrets for credentials, with least-privilege access.
- No raw secrets in YAML, source code, pipeline variables, logs, screenshots, or issue comments.
- No public unauthenticated access to Drasi Server REST,
/api/v1/docs/, or/api/v1/openapi.json. - Minimal source-system permissions.
- Redacted structured logs.
- Pinned images and dependency versions.
Control-plane exposure
Drasi Server REST APIs manage sources, queries, and reactions. Treat them as administrative APIs.
If exposed beyond a trusted local/private network, require:
- Authentication.
- Authorization.
- TLS.
- Rate limiting or abuse protection.
- Audit logging.
- Restricted origins if browsers are involved.
- MCP Streamable HTTP Origin validation when MCP endpoints are reachable from browser-capable or local-network contexts.
- Clear separation between management APIs and public application APIs.
Identity and credentials
Use this order:
- Microsoft Entra Workload ID where the Drasi provider or reaction documents support.
- Managed identity for Azure host-to-Azure-resource access.
- Key Vault references for Azure-hosted secrets.
- Kubernetes Secrets for Kubernetes-hosted provider credentials.
- Inline values only for disposable local examples, never production.
Verify federated credential subjects exactly. For Kubernetes-hosted providers and reactions, the service account name often includes the resource type and resource name, for example source.<name> or reaction.<name> where documented.
RBAC and least privilege
- Source credentials should read only the data needed for the query.
- Kubernetes Source credentials should list/watch only required resource types and scopes.
- Reaction identities should write only to the intended downstream target.
- CI/CD identities should deploy only the target environment.
- Human break-glass access should be documented and audited.
Network controls
For Azure-hosted Drasi Server:
- Prefer internal Container Apps ingress.
- Use private endpoints or VNet integration for databases and internal dependencies where appropriate.
- Use APIM, Application Gateway, Front Door, or equivalent only when it provides the intended auth and routing boundary.
- Keep management API separate from public app APIs.
For Drasi for Kubernetes:
- Do not expose internal Drasi services publicly unless docs and architecture require it.
- Use port-forward only for local dev/test.
- Lock down reaction services that receive callbacks or expose MCP endpoints.
- For MCP endpoints, require authentication, TLS, Origin validation where applicable, rate limits, audit logs, and explicit resource-level data minimization.
Audit-grade verification
These are the minimum commands an auditor or operator can run to prove the security posture is actually enforced - not just documented.
TLS enforcement on a public Drasi Server endpoint
# Negotiate TLS 1.3 explicitly; expect "SSL connection using TLSv1.3" in stderr
curl -v --tlsv1.3 --tls-max 1.3 https://<drasi-host>/health 2>&1 | grep -E "TLSv1|cipher"
# Reject TLS 1.0/1.1; expect handshake failure
curl --tls-max 1.1 https://<drasi-host>/health || echo "TLS 1.1 correctly rejected"
# Confirm HSTS header is sent
curl -sI https://<drasi-host>/health | grep -i strict-transport-securityPrivate ingress for Drasi for Kubernetes
# Drasi services must be ClusterIP, never LoadBalancer or NodePort
kubectl -n drasi-system get svc -o jsonpath='{range .items[*]}{.metadata.name}{"\t"}{.spec.type}{"\n"}{end}'
# Confirm no public Ingress / Gateway resources expose Drasi to the internet
kubectl -n drasi-system get ingress,gateway -o wideSupply-chain integrity
# Pin by digest; record the digest in your repo
docker pull ghcr.io/drasi-project/drasi-server:<tag>
docker inspect ghcr.io/drasi-project/drasi-server:<tag> --format '{{index .RepoDigests 0}}'
# Verify cosign signature if the publisher signs releases (re-check current publisher status before relying on this)
cosign verify ghcr.io/drasi-project/drasi-server@sha256:<digest> \
--certificate-identity-regexp 'https://github.com/drasi-project/.*' \
--certificate-oidc-issuer https://token.actions.githubusercontent.com
# Generate or fetch an SBOM
syft ghcr.io/drasi-project/drasi-server@sha256:<digest> -o spdx-json > drasi-server.sbom.json
grype sbom:drasi-server.sbom.json --fail-on highRe-check the Drasi project's published signing identity and SBOM policy before relying on cosign verification - it may not be enabled on every release.
Data classification and redaction
Drasi propagates source rows through query results into reactions. Any field that is a regulated identifier (PII, PHI, payment, secret) must be classified before it leaves the source boundary.
Classification scheme to record per query
| Class | Examples | Allowed in query result | Allowed in logs | Allowed in metrics/traces |
|---|---|---|---|---|
| Public | Product IDs, statuses | Yes | Yes | Yes |
| Internal | Internal user IDs | Yes | Hash only | Hash only |
| Confidential | Email, phone, address | Yes (downstream-only) | Redacted | Tokenized only |
| Restricted | SSN, payment, health | Avoid in result; if unavoidable, encrypted with KEK | Never | Never |
Reaction-side redaction pattern (HTTP webhook example)
The reaction payload is the only thing a downstream system sees. Strip restricted fields and tokenize confidential ones before send. Example (illustrative):
// Source row
{"customerId": "c-12345", "email": "user@example.com", "ssn": "***", "amount": 240}
// Logged at reaction (after redaction)
{"customerId": "c-12345", "email_hash": "sha256:9f86d...", "amount": 240}Logging rules
- Never log full request bodies for Source or Reaction connectors. Log only the connector name, query ID, change kind, and a stable hash of the payload.
- Never log Authorization, Cookie, or
X-Api-Keyheaders. - For PostgreSQL Source: do not log the connection string. Log only the host, port, database, and a hash of the credential reference.
- For Drasi Server REST: avoid
RUST_LOG=traceordrasi_server=traceoutside development.traceemits payload-level data.
Verification
# Quick grep over recent logs to catch obvious leaks
kubectl -n drasi-system logs -l app.kubernetes.io/part-of=drasi --since=1h \
| grep -E "Bearer |Basic |password=|@example|@gmail|@outlook|[0-9]{3}-[0-9]{2}-[0-9]{4}" \
&& echo "POSSIBLE LEAK — investigate" || echo "no obvious leaks"Record the classification matrix in the ContinuousQuery result contract (see continuous-queries/guide.md) so reaction authors know what they must redact.
Logging and data protection
Reject changes that log:
- Authorization headers.
- Passwords, tokens, connection strings, client secrets, or raw kubeconfigs.
- Full event payloads containing PII, health, financial, customer, or security-sensitive data.
Prefer correlation ids, query/reaction names, operation type, status code, redacted error message, and latency.
Supply chain and release safety
- Pin container images and actions.
- Prefer image digests for production.
- Record Drasi Server/platform/CLI versions.
- Scan container images where the repository has scanning tools.
- Keep generated manifests reviewable.
- Treat cross-minor pre-1.0 upgrades as potentially breaking until proven safe.
Exit criteria
Security review is complete only when:
- Management APIs and docs/OpenAPI endpoints are private or authenticated.
- Every secret is stored through the approved mechanism for the runtime.
- Each Source and Reaction has least-privilege credentials or identity.
- Public or cross-network endpoints have TLS, auth, rate limiting, logging, and origin/CORS controls where relevant.
- Logs and validation evidence are redacted.
- Image, crate, CLI, and action versions are pinned for production paths.
- Residual risks are recorded, especially for pre-1.0 components and MCP endpoints.
Trust boundaries and STRIDE mapping
Drasi crosses several distinct trust boundaries. Each must be analyzed against the STRIDE categories (Spoofing, Tampering, Repudiation, Information disclosure, Denial of service, Elevation of privilege). Use this table as the starting point for a project threat model (see templates/drasi-threat-model.md).
| Boundary | S (Spoofing) | T (Tampering) | R (Repudiation) | I (Information disclosure) | D (Denial of service) | E (Elevation of privilege) |
|---|---|---|---|---|---|---|
| Operator ↔ control plane (REST API) | OIDC bearer / mTLS / managed identity; reject anonymous | TLS in transit; signed manifests where supported | Append-only audit log (see audit schema) | TLS; redact secrets in error responses | Rate limits at gateway; per-principal quotas | RBAC scoped to namespace/resource; no shared admin tokens |
| Drasi ↔ source DB | Per-source workload identity or rotated DB credential | TLS to DB; verify server cert | DB audit log + Drasi connector log with correlation id | Least-privilege DB role; column-level grants where supported | Connection-pool limits; read-replica preferred | One identity per source; no shared DB role across sources |
| Drasi ↔ reaction endpoint | HMAC signature over body using shared secret, or mTLS | TLS; signed payload; replay window (timestamp + nonce) | Reaction-side ack log with correlation id | TLS; redact restricted fields before send | Per-reaction concurrency and retry caps | One workload identity per reaction (see "Reaction identity scope") |
| Agent ↔ MCP Reaction | OIDC bearer or workload identity; Origin validation | TLS; reject unauthenticated subscribe | Audit subscribe/unsubscribe and resource read events | Resource scope per token (token → allowed drasi://query/{id} set) |
Per-token rate limits; subscription caps | Token cannot escalate to other queries or tenants |
| Secrets at rest | N/A by design (data plane, not principal) | Key Vault / Kubernetes Secret with encryption at rest; signed access policies | Key Vault access log; SIEM forwarding | Envelope encryption; CMK where required by policy | Rotate before quota exhaustion (see rotation runbook) | Access policies scoped per workload identity, not shared |
| drasi-lib host process ↔ embedded library | Host process authenticates upstream callers; no separate Drasi auth surface | Host-process integrity (signed binary, FS permissions) | Host application log; correlate with library events | Host-process memory isolation; no IPC surface added by drasi-lib | Host-process resource limits (cgroups, ulimits) | drasi-lib runs with host process privileges - host MUST drop privileges before loading |
Record per-boundary residual risks in the project threat model.
Network egress controls
Ingress controls protect Drasi from being attacked. Egress controls protect everything else from a compromised Drasi component. Both are required. A tampered Reaction with unrestricted egress can exfiltrate query result rows - and therefore source data - to an attacker-controlled URL. Treat egress as a first-class boundary.
Kubernetes NetworkPolicy: deny default egress, allowlist explicitly
# Default-deny egress in the Drasi namespace
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: default-deny-egress
namespace: drasi-system
spec:
podSelector: {}
policyTypes: [Egress]
egress: []
---
# Allow a specific reaction pod to reach only its intended destination
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: reaction-<name>-egress
namespace: drasi-system
spec:
podSelector:
matchLabels:
drasi.io/resource: reaction
drasi.io/name: <reaction-name>
policyTypes: [Egress]
egress:
- to:
- namespaceSelector:
matchLabels:
kubernetes.io/metadata.name: <destination-ns>
ports:
- protocol: TCP
port: 443
# DNS resolution
- to:
- namespaceSelector: {}
podSelector:
matchLabels:
k8s-app: kube-dns
ports:
- protocol: UDP
port: 53Apply the analogous pattern to source connectors: allow only the source DB endpoint and the Drasi control plane.
Azure Container Apps egress lockdown
For Drasi Server on Azure Container Apps, deploy into a VNet-integrated environment with workload profiles and use NSG / UDR / Azure Firewall to restrict egress to source DB private endpoints, reaction destinations, and Entra ID. Do not allow 0.0.0.0/0 egress from production environments.
Kill switch for a runaway Reaction
When a Reaction misbehaves (excess retries, suspected exfiltration, log floods), cut its network reach immediately, then scale it to zero. This stops the bleed before you debug.
# 1. Remove the reaction's egress NetworkPolicy (deny-all then applies)
kubectl -n drasi-system delete networkpolicy reaction-<name>-egress
# 2. Scale the reaction deployment to zero
kubectl -n drasi-system scale deployment reaction.<name> --replicas=0
# 3. Capture state for post-mortem
kubectl -n drasi-system logs deployment/reaction.<name> --previous --tail=2000 > runaway-reaction.logFor Azure Container Apps, the equivalent kill switch is az containerapp update --min-replicas 0 --max-replicas 0 plus removing the egress NSG rule.
Verification: prove egress is locked down
# Exec into a reaction pod and try to reach an arbitrary external endpoint
kubectl -n drasi-system exec deployment/reaction.<name> -- \
curl --max-time 5 -sS https://example.com/ \
&& echo "EGRESS LEAK — investigate" || echo "egress correctly blocked"Without egress controls, all other reaction controls (HMAC, redaction, audit) are bypassable: a tampered reaction can simply POST source rows to https://attacker.example/.
Secret rotation runbook
Every secret that grants access to a Drasi component or that Drasi uses to reach a source or reaction must have a rotation cadence, a rotation mechanism, and a documented Drasi-side action. The table below is the consolidated runbook.
| Secret | Cadence target | Rotation mechanism | Drasi-side action on rotation |
|---|---|---|---|
| PostgreSQL source role password | 90d | Key Vault rotation event or ALTER ROLE ... PASSWORD driven by an automation account |
If credential is consumed via env-var Secret: restart source pod. If projected via mounted file: reload (file watcher) or rolling restart. If Workload Identity: none. |
| SQL Server SQL auth password | 90d | Key Vault rotation event or ALTER LOGIN ... WITH PASSWORD |
Same as Postgres: env-var → restart; projected file → reload or rolling restart; Workload Identity → none. |
| Entra Workload ID federated credential | Federated credentials do not have rotating secrets; rotate the underlying OIDC trust subject only when service account names change | Update federatedIdentityCredentials subject to match new system:serviceaccount:<ns>:<name> |
None: token exchange happens per request; no Drasi-side restart. |
| Webhook (reaction) HMAC signing key | 90d, or immediately on suspected compromise | Generate new key in Key Vault; dual-publish for one cycle so receiver can accept old + new | Roll signing key in reaction config; if env-var: restart. Verify receiver accepts new key before retiring old. |
| MCP bearer / API gateway token | 30–90d depending on token blast radius | Issue new token from identity provider; revoke old token at gateway | If gateway-managed (recommended): no Drasi-side action. If Drasi reaction holds the token: env-var → restart; projected file → reload. |
| Container registry pull token | 90d, or per platform default for managed identity ACR pull | Prefer managed identity ACR pull (no token). Otherwise rotate imagePullSecret |
Recreate imagePullSecret; pods pick up new credential on next pull. No restart needed for already-running pods. |
| Drasi Server admin / break-glass credential | 30d for break-glass; should be unused in steady state | Manual rotation, recorded in audit log; alarm on use | Restart control plane if credential is env-injected; none if served via OIDC. |
Rotation principles
- Prefer Workload Identity / managed identity wherever the provider supports it: nothing to rotate on the Drasi side.
- For env-var secrets, expect a pod restart on rotation. Plan rolling updates so source connectors do not all restart at once.
- For projected file secrets, use a sidecar or built-in watcher so the connector reloads without a restart where possible.
- Dual-publish keys (old + new accepted simultaneously) for HMAC and bearer tokens so receivers never have a window of rejection.
- Alarm on any secret older than 1.5× cadence.
Verification
# List Key Vault secrets with last-rotation timestamps
az keyvault secret list --vault-name <vault> --query "[].{name:name,updated:attributes.updated}" -o table
# Confirm no Drasi pod has been running longer than the cadence (env-var rotations require restart)
kubectl -n drasi-system get pods -o jsonpath='{range .items[*]}{.metadata.name}{"\t"}{.status.startTime}{"\n"}{end}'Audit log schema
Every control-plane mutation against Drasi Server REST APIs must produce an audit record. Use this minimum schema:
| Field | Type | Required | Example |
|---|---|---|---|
timestamp |
RFC 3339 UTC | Yes | 2026-05-15T14:22:08.412Z |
principal |
string (subject of authenticated caller) | Yes | alice@contoso.com / system:serviceaccount:ops:deployer |
source_ip |
string | Yes | 10.42.0.18 |
action |
string (HTTP verb + path template) | Yes | POST /api/v1/sources, DELETE /api/v1/queries/{id} |
target |
object {kind, name, namespace} |
Yes | {kind: "ContinuousQuery", name: "vehicles-on-route", namespace: "drasi-system"} |
correlation_id |
string | Yes | c-7b1f... |
result |
enum success / denied / error |
Yes | success |
request_hash |
sha256 of request body (NOT the body itself) | Recommended | sha256:9f86d... |
error_class |
string (when result != success) |
Conditional | AuthorizationDenied |
Retention: ≥ 365 days for production. Longer if regulatory (HIPAA, PCI) applies.
Immutability: append-only sink. Write to an immutable store (Azure Storage immutability policy, AWS S3 Object Lock, or a SIEM-managed sink). The Drasi Server process must not have delete or overwrite permission on its own audit stream.
SIEM handoff: ship audit events to the SIEM in near-real-time. Recommended pattern is a sidecar or platform agent (Azure Monitor / Fluent Bit / Vector) tailing the audit stream and forwarding to the SIEM ingestion endpoint. Alarm on:
- Any
result=deniedfor a privileged action. - A burst of
POST /api/v1/sourcesfrom one principal. - Any
DELETE /api/v1/queries/{id}outside change windows. - Any action whose
principalis not in the approved automation or human operator list.
Record audit-log location, retention, and SIEM destination in the architecture decision record.
Multi-tenancy stance
Default posture: single-tenant per Drasi instance. Pre-1.0 Drasi does not provide built-in tenant isolation. Sharing a single Drasi Server (or Drasi for Kubernetes installation) across tenants can leak data across tenant boundaries because:
- Query results are not tenant-scoped by the platform.
- Source replication slots (e.g., Postgres logical replication) are visible to any operator with control-plane access.
- Reaction queues and MCP resource URIs are not tenant-namespaced.
If multi-tenancy is mandatory, isolate at the infrastructure layer using these primitives:
| Primitive | Mechanism |
|---|---|
| Namespace per tenant | One Kubernetes namespace per tenant in Drasi for Kubernetes; NetworkPolicy enforces no cross-namespace pod-to-pod traffic. |
| Instance per tenant (Drasi Server) | One Drasi Server deployment per tenant. Do not share a Drasi Server process across tenants. |
| Managed identity per tenant | Distinct workload identities per tenant so a compromised tenant cannot read another tenant's source. |
| Gateway-enforced scope on MCP | API gateway maps each token to its tenant's drasi://query/{queryId} prefix and rejects out-of-scope subscribes. |
| Network isolation | Per-tenant NetworkPolicy / NSG; no shared egress allowlist. |
| Audit segregation | Per-tenant audit stream so one tenant's SIEM cannot see another tenant's principals. |
Explicit warning: until Drasi documents a tenancy model, treat any shared instance as a shared-fate trust domain. A tampered query in tenant A can affect tenant B's source DB load, reaction queue depth, and operator audit clarity.
Control-plane authentication
/api/v1/sources, /api/v1/queries, /api/v1/reactions, and any other mutating endpoint MUST require authentication. Accepted mechanisms (any one of):
- OIDC bearer JWT issued by an enterprise identity provider. Validate
iss(issuer),aud(audience matches Drasi Server), andexp(not expired). Reject tokens missing any of these claims. - mTLS with a client certificate chain anchored to an internal CA. The control plane validates the chain and pins the expected subject.
- API gateway fronting Drasi Server with managed identity (APIM, Application Gateway with managed identity, or equivalent). Drasi Server then trusts only the gateway's caller identity (mTLS or signed header).
Shared API keys are not acceptable for production control-plane access.
Verification: unauthenticated POST must be rejected with 401
# Expect HTTP 401 (or 403); absolutely NOT 200/201
curl -s -o /dev/null -w "%{http_code}\n" \
-X POST https://<drasi-host>/api/v1/sources \
-H "Content-Type: application/json" \
-d '{}'
# Expected output: 401If this returns 200 or 201, the control plane is exposed unauthenticated. Treat as a sev-1 incident.
Reaction identity scope (preventing EOP)
Rule: scope workload identity grants as narrowly as the platform allows.
For Drasi for Kubernetes on AKS, production verification (2026-06-23) confirms per-resource service accounts:
system:serviceaccount:<namespace>:source.<source-name>
system:serviceaccount:<namespace>:reaction.<reaction-name>Use one managed identity per source/reaction boundary where practical, or at minimum keep role assignments narrowly scoped to each downstream resource.
Re-check kubectl get pod <pod> -n <ns> -o jsonpath='{.spec.serviceAccountName}' on each upgrade and update federated credential subjects if naming patterns change.
Verification (current Drasi for Kubernetes)
# Confirm source/reaction pods use per-resource SAs
kubectl -n drasi-system get pods \
-o jsonpath='{range .items[*]}{.metadata.name}{"\t"}{.spec.serviceAccountName}{"\n"}{end}'
# Confirm each relevant source/reaction SA has the workload identity annotation and label
kubectl -n drasi-system get sa source.<source-name> reaction.<reaction-name> \
-o jsonpath='{.metadata.annotations}{"\n"}{.metadata.labels}{"\n"}'
# Audit role assignments on the managed identity principal
az role assignment list \
--assignee <managed-identity-principal-id> \
--query "[].{role:roleDefinitionName,scope:scope}" -o tableIf the managed identity has overly broad role assignments (e.g. subscription-scope Contributor), narrow them to the specific resource and role needed by each source or reaction.
ContinuousQuery resource quotas (DoS prevention)
A ContinuousQuery is a long-running computation over an expanding state. Without per-query bounds, a single misbehaving query can starve every other query on the same Drasi instance. Required controls:
- Per-query CPU and memory limits. Enforce as Kubernetes resource requests/limits on the query pod, or as a container-level limit in Drasi Server. A query with no upper memory bound will exhaust the node and OOM-kill unrelated queries.
- Per-query result-row quota (optional but strongly recommended). Cap result-set cardinality (e.g., 100k rows max) and alarm before reaching the cap. A query whose result set has grown unbounded almost always indicates a join cardinality bug.
- Pre-merge query review gate. Before any new or modified ContinuousQuery merges, a human reviewer must reject unbounded Cartesian joins and verify expected cardinality. See
bundles/continuous-queries/guide.mdfor the join cardinality guidance and review checklist. - Runtime guardrails. Alarm on query state size, result-set growth rate, and per-query CPU. Page on sustained breach of the alarm thresholds.
A DoS in Drasi rarely looks like a flood of external traffic - it usually looks like one query whose state has quietly grown past the available memory.
drasi-lib threat surface
When Drasi is embedded via drasi-lib (rather than deployed as Drasi Server or Drasi for Kubernetes), the threat surface shifts:
- The host process IS the trust boundary. There is no REST control plane to attack from the network. The relevant attackers are anyone who can run code in, or alter the configuration of, the host process.
- No REST surface to protect. Skip the control-plane auth and ingress-policy sections; they do not apply.
- Credentials live in host process config. Source DB credentials, downstream secrets, and any embedded reaction config are loaded by the host process - usually from environment variables, the host's secret store, or a managed identity. Apply the host application's secret-management discipline, not Drasi's.
- No over-broad filesystem access for the host. If the host process writes Drasi state to disk, restrict that path. The drasi-lib state is as sensitive as the query results it produces.
- Supply-chain audit of
drasi-libupdates. Runcargo auditandcargo denyagainst the host crate before promoting a newdrasi-libversion. Treat a new minor version ofdrasi-libas a dependency upgrade requiring the same review as any other Rust dependency. - Restart and recovery inherit from the host. Source recovery semantics (replication slot replay, change cursor) are still owned by Drasi, but pod / process restart policy is owned by the host application's deployment. If the host crashes, the host's restart policy governs how Drasi resumes - there is no separate Drasi-managed restart.
Use this section in the threat model when the project's runtime form (recorded in the architecture decision record) is drasi-lib.