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.markingsSDK on the producer
TrackSample.markingsIngester (carries through WAL)
ConnFilter.principal_markingsWS handshake, from Principal.markings
MarkingPolicy::is_satisfied_bySubset 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

  1. Declare markings in the Org's catalogue (REST: POST /api/v1/admin/markings).
  2. Grant markings to users (REST: POST /api/v1/admin/users/{id}/markings) — or via SCIM attribute mapping.
  3. Tag streams with required_markings in the stream schema YAML.
  4. Set IngestEnvelope.markings on the SDK side for ad-hoc per-event labels.

Related

  • ABAC & Cedar — Cedar policies consume the same marking set.
  • PrincipalsPrincipal.markings populates ConnFilter.
  • SCIM — how IdPs map external attributes to markings.

Questions?

Reach out for help with integration, deployment, or custom domain codecs.