Markings & Propagation
Markings are per-Org security labels attached to data and to principals. A sample reaches a connection only if the principal holds every marking the sample carries. Because markings live on the data — not on the request — they propagate end-to-end: SDK ingest tags the envelope, the WAL persists them, and the WebSocket fan-out filters per-frame.
Schema
Two tables back markings:
SQL
-- Per-Org catalogue of marking ids the operator has declared.
CREATE TABLE markings (
id UUID PRIMARY KEY,
org_id UUID NOT NULL REFERENCES orgs(id) ON DELETE CASCADE,
name TEXT NOT NULL,
description TEXT,
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
UNIQUE (org_id, name)
);
-- Which users hold which markings.
CREATE TABLE marking_grants (
marking_id UUID NOT NULL REFERENCES markings(id) ON DELETE CASCADE,
user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
granted_at TIMESTAMPTZ NOT NULL DEFAULT now(),
PRIMARY KEY (marking_id, user_id)
);
CREATE INDEX marking_grants_user_idx ON marking_grants(user_id); End-to-end propagation
graph LR
SDK["SDK client"] -->|IngestEnvelope.markings| ING["Ingester"]
ING -->|TrackSample.markings| WAL["WAL"]
WAL --> BUS["EventBus"]
BUS --> WS["WebSocket fan-out"]
WS -->|ConnFilter.matches_markings| CONN["Connection<br/>(principal markings)"]
CONN -->|all required held| CLIENT["Client"]
CONN -->|missing marking| DROP["dropped silently"]
Marking carriers
| Carrier | Set by |
|---|---|
| IngestEnvelope.markings | SDK on the producer |
| TrackSample.markings | Ingester (carries through WAL) |
| ConnFilter.principal_markings | WS handshake, from Principal.markings |
| MarkingPolicy::is_satisfied_by | Subset check at evaluation time |
Filter rule
ConnFilter::matches_markings is a strict subset check: every marking on the sample must be in the principal’s held set.
Rust
/// true if every marking in sample_markings is also held by the principal.
/// Empty sample_markings means "unrestricted" and always passes —
/// including for principals with no markings at all.
pub fn matches_markings(&self, sample_markings: &[String]) -> bool {
sample_markings.iter().all(|m| self.principal_markings.contains(m))
} Tagging on ingest
Rust
use tetrapus_api::prelude::*;
let envelope = IngestEnvelope::new(track_sample)
.with_markings(["TLP:AMBER", "PII:HIPAA"]);
producer.send(envelope).await?; Configuration workflow
- Declare markings in the Org's catalogue (REST:
POST /api/v1/admin/markings). - Grant markings to users (REST:
POST /api/v1/admin/users/{id}/markings) — or via SCIM attribute mapping. - Tag streams with
required_markingsin the stream schema YAML. - Set
IngestEnvelope.markingson the SDK side for ad-hoc per-event labels.
Related
- ABAC & Cedar — Cedar policies consume the same marking set.
- Principals —
Principal.markingspopulatesConnFilter. - SCIM — how IdPs map external attributes to markings.
Questions?
Reach out for help with integration, deployment, or custom domain codecs.