Skip to main content

SMS Firewall Service — Event Schemas

Version: 1.0 Status: Draft Owner: Trust & Safety Last Updated: 2026-04-21 Companion: DOMAIN_MODEL · SYNC_CONTRACT · SECURITY_MODEL Related ADR: ADR-0004 §6

All events are published to NATS JetStream via the transactional outbox pattern. Every event carries schemaVersion, eventId (UUIDv4), traceId (W3C), and at (RFC 3339 µs UTC). All MSISDNs in event payloads appear masked (+CCNNN***); raw PDU bodies never appear; PDU body SHA-256 hash may appear.


1. Streams, subjects, retention

StreamSubjectsRetentionReplicasMirroringDedup window
FIREWALL_AUDITfirewall.audit.v113 months hot in JetStream; daily Parquet+zstd archive to MinIO Object-Lock 7 yr5 (3 in kbl, 2 in mzr)leaf mirror to dxb cold2 min
FIREWALL_ALERTSfirewall.alert.*.v190 days32 min
FIREWALL_QUARANTINEfirewall.quarantine.held.v1, .released.v1, .rejected.v1, .expired.v190 days hot; cold archive 7 yr32 min
FIREWALL_RULESfirewall.rule.changed.v1, .degraded.v1, .audit.policy.changed.v1365 days32 min
FIREWALL_BLOCKLISTfirewall.blocklist.changed.v1, .federated.v1, .entry.deactivated.v1365 days35 min
FIREWALL_FEDERATIONfirewall.federation.exported.v1, .heartbeat.v190 days3
FIREWALL_PEERSfirewall.peer_quarantine.entered.v1, firewall.alert.peer.degraded.v1, firewall.simbox.detected.v1, firewall.alert.ait.detected.v1365 days32 min
FIREWALL_OPSfirewall.mode.changed.v1, firewall.alert.mode.auto_panic.v1, firewall.alert.bind.missing.v1, firewall.transit.unavailable.v1365 days31 min

All streams have a corresponding .deadletter suffix. Consumers are durable with explicit ack per PLT-REQ-008/009.


2. Produced events — full schemas

2.1 firewall.audit.v1 (regulator-grade evidence)

Emitted for every FilterInbound / EvaluateTransit call regardless of verdict.

{
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "firewall.audit.v1",
"type": "object",
"required": ["schemaVersion","eventId","verdictId","verdict","direction",
"srcMsisdnMasked","dstMsisdnMasked","pduFingerprint",
"evaluatedRuleIds","evaluatedAt","at","traceId"],
"properties": {
"schemaVersion": { "const": "1" },
"eventId": { "type": "string", "format": "uuid" },
"verdictId": { "type": "string", "pattern": "^fv_[0-9a-f-]+$" },
"verdict": { "enum": ["ALLOW","FLAG","BLOCK","QUARANTINE"] },
"direction": { "enum": ["MO","TRANSIT_MT","EGRESS_DND_CHECK"] },
"srcMsisdnMasked":{ "type": "string", "pattern": "^\\+\\d{1,3}\\d{2,3}\\*+$" },
"dstMsisdnMasked":{ "type": "string", "pattern": "^\\+\\d{1,3}\\d{2,3}\\*+$" },
"senderId": { "type": ["string","null"], "maxLength": 11 },
"mnoBindId": { "type": ["string","null"] },
"peerAsn": { "type": ["integer","null"] },
"pduFingerprint": { "type": "string", "pattern": "^[0-9a-f]{64}$" },
"pduBodySha256": { "type": "string", "pattern": "^[0-9a-f]{64}$" },
"blockReason": { "type": ["string","null"], "enum":
[null,"ORIGIN_BLOCKLIST","CONTENT_FORBIDDEN","RATE_EXCEEDED","GEO_FORBIDDEN",
"DND_PRESENT","AIT_SIGNATURE","SIMBOX_SIGNATURE","REGULATOR_BLOCK",
"PEER_ASN_UNKNOWN","SENDER_ID_SPOOFED","SENDER_ID_SUSPENDED","GREY_ROUTE",
"PEER_QUARANTINED"] },
"evaluatedRuleIds": { "type": "array", "items": { "type": "string" } },
"ruleHits": {
"type": "array",
"items": {
"type": "object",
"required": ["ruleId","ruleType","action","severity"],
"properties": {
"ruleId": { "type": "string" },
"ruleType": { "type": "string" },
"action": { "enum": ["ALLOW","FLAG","BLOCK","QUARANTINE","RATE_LIMIT"] },
"severity": { "enum": ["CRITICAL","HIGH","MEDIUM","LOW"] },
"evidence": { "type": "string" },
"confidence": { "type": "number", "minimum": 0, "maximum": 1 }
}
}
},
"holdId": { "type": ["string","null"] },
"evaluationLatencyMs": { "type": "integer", "minimum": 0 },
"flags": { "type": "array", "items": { "type": "string" } },
"operatingMode": { "enum": ["NORMAL","DEGRADED","PANIC","MAINTENANCE"] },
"ruleSetVersion": { "type": "integer", "minimum": 1 },
"evaluatedAt": { "type": "string", "format": "date-time" },
"at": { "type": "string", "format": "date-time" },
"traceId": { "type": "string" }
}
}

2.2 firewall.alert.mo.blocked.v1

Subscriber-impacting BLOCK on inbound MO direction.

interface FirewallAlertMoBlocked {
schemaVersion: '1';
eventId: string;
verdictId: string;
srcMsisdnMasked: string;
dstMsisdnMasked: string;
mnoBindId: string;
blockReason: 'ORIGIN_BLOCKLIST'|'CONTENT_FORBIDDEN'|'RATE_EXCEEDED'|'GEO_FORBIDDEN'|
'DND_PRESENT'|'AIT_SIGNATURE'|'SIMBOX_SIGNATURE'|'REGULATOR_BLOCK';
triggerRuleIds: string[];
traceId: string;
at: string;
}

2.3 firewall.alert.transit.blocked.v1

interface FirewallAlertTransitBlocked {
schemaVersion: '1';
eventId: string;
verdictId: string;
peerAsn: number;
peerSystemId: string;
senderId: string | null;
dstMsisdnMasked: string;
blockReason: 'PEER_ASN_UNKNOWN'|'SENDER_ID_SPOOFED'|'SENDER_ID_SUSPENDED'|
'GREY_ROUTE'|'PEER_QUARANTINED'|'CONTENT_FORBIDDEN';
traceId: string;
at: string;
}

2.4 firewall.alert.greyroute.detected.v1

interface FirewallAlertGreyrouteDetected {
schemaVersion: '1';
eventId: string;
verdictId: string;
peerAsn: number;
peerSystemId: string;
dstHomeMnoId: 'AWCC'|'Roshan'|'Etisalat'|'MTN_AF'|'Salaam';
evidence: { hlrLookupSource: string; resolvedAt: string };
traceId: string;
at: string;
}

2.5 firewall.simbox.detected.v1

interface FirewallSimboxDetected {
schemaVersion: '1';
eventId: string;
signalId: string;
originatorMasked: string;
confidence: number; // 0..1
evidenceJson: {
imeiChurnCount24h?: number;
aNumberRotationCount24h?: number;
geoCellIdVolatility?: number;
fraudIntelModelId?: string;
};
firstSeenAt: string;
traceId: string;
at: string;
}

2.6 firewall.alert.ait.detected.v1

interface FirewallAlertAitDetected {
schemaVersion: '1';
eventId: string;
patternId: string;
patternType: 'OTP_HARVEST'|'PUMPED_TRAFFIC'|'IRSF'; // GSMA FF.21
dstMsisdnRange: string; // e.g. "+9370012XXXX"
confidence: number;
evidenceJson: Record<string, unknown>;
traceId: string;
at: string;
}

2.7 firewall.quarantine.held.v1

interface FirewallQuarantineHeld {
schemaVersion: '1';
eventId: string;
holdId: string;
verdictId: string;
direction: 'MO'|'TRANSIT_MT';
triggerRuleIds: string[];
reasonCode: string; // 'SENDER_ID_UNKNOWN' | 'PEER_HYGIENE_SCORE_LOW' | ...
expiresAt: string;
traceId: string;
at: string;
}

2.8 firewall.quarantine.released.v1

Carries skipFirewall: true so the re-injecting connector bypasses re-evaluation.

interface FirewallQuarantineReleased {
schemaVersion: '1';
eventId: string;
holdId: string;
reviewerUserId: string;
reviewNotes: string;
reInjectInstruction: {
skipFirewall: true;
targetConnectorBindId: string;
originalSmppSequenceNumber: number;
};
traceId: string;
at: string;
}

2.9 firewall.quarantine.rejected.v1

Identical shape to released.v1 minus reInjectInstruction; adds rejectionReason.

2.10 firewall.quarantine.expired.v1

interface FirewallQuarantineExpired {
schemaVersion: '1';
eventId: string;
holdId: string;
expiredAt: string;
traceId: string;
at: string;
}

2.11 firewall.blocklist.federated.v1

interface FirewallBlocklistFederated {
schemaVersion: '1';
eventId: string;
source: 'REGULATOR'|'PEER_MNO'|'INTERNAL'|'FRAUD_INTEL';
regulatorRef: string | null;
addedCount: number;
removedCount: number;
entryCountTotal: number;
bloomRebuildScheduled: boolean;
importBatchId: string;
traceId: string;
at: string;
}

2.12 firewall.federation.exported.v1

interface FirewallFederationExported {
schemaVersion: '1';
eventId: string;
exportedAt: string;
exportSha256: string; // hex
signature: string; // base64 PKCS#1v1.5 over exportSha256
signerKeyId: string; // pkcs11:object=ghasi-firewall-fed-signer
presignedUrl: string; // 24h validity
entryCountAdded: number;
entryCountRemoved: number;
diffPeriod: { from: string; to: string };
traceId: string;
at: string;
}

2.13 firewall.federation.heartbeat.v1

Even on zero-diff days. Consumers (peer MNOs) detect a stalled exporter when this stops.

interface FirewallFederationHeartbeat {
schemaVersion: '1';
eventId: string;
exportedAt: string; // last successful export ts
emptyDiff: boolean;
traceId: string;
at: string;
}

2.14 firewall.alert.federation.signature.invalid.v1

PagerDuty severity. Emitted when an inbound regulator event fails HSM signature validation.

interface FirewallAlertFederationSignatureInvalid {
schemaVersion: '1';
eventId: string;
inboundEventId: string;
expectedSigner: string;
observedSignerHint: string | null;
payloadSha256: string;
traceId: string;
at: string;
}

2.15 firewall.peer_quarantine.entered.v1

interface FirewallPeerQuarantineEntered {
schemaVersion: '1';
eventId: string;
peerId: string;
peerSystemId: string;
peerAsn: number;
hygieneScore: number;
reasons: string[]; // 'GREY_ROUTE_HEURISTIC' | 'SENDER_ID_SPOOF_RATE' | 'CONTENT_VIOLATION_RATE'
triggeredBy: 'AUTO_HYGIENE'|'FRAUD_INTEL_SIGNAL'|'OPERATOR_MANUAL';
traceId: string;
at: string;
}

2.16 firewall.alert.peer.degraded.v1

Hygiene score < 60 for 3 consecutive 5-minute windows.

interface FirewallAlertPeerDegraded {
schemaVersion: '1';
eventId: string;
peerId: string;
scoreHistory: number[]; // last 3 windows
contributingReasons: string[];
traceId: string;
at: string;
}

2.17 firewall.rule.changed.v1

Cache-invalidation signal. Consumers fetch new state via REST.

interface FirewallRuleChanged {
schemaVersion: '1';
eventId: string;
entityType: 'RULE'|'RULESET'|'BLOCKLIST_ENTRY'|'MNO_BIND'|'PEER_AGGREGATOR';
entityId: string;
action: 'CREATE'|'UPDATE'|'DELETE'|'ENABLE'|'DISABLE';
version: number | null;
actorUserId: string;
reason: string | null;
traceId: string;
at: string;
}

2.18 firewall.rule.degraded.v1

interface FirewallRuleDegraded {
schemaVersion: '1';
eventId: string;
ruleId: string;
reason: 'REGEX_TIMEOUT'|'CLASSIFIER_UNAVAILABLE'|'BUDGET_EXCEEDED';
observedLatencyMs: number;
autoDisabled: boolean;
traceId: string;
at: string;
}

2.19 firewall.mode.changed.v1

interface FirewallModeChanged {
schemaVersion: '1';
eventId: string;
previousMode: 'NORMAL'|'DEGRADED'|'PANIC'|'MAINTENANCE';
newMode: 'NORMAL'|'DEGRADED'|'PANIC'|'MAINTENANCE';
trigger: 'MANUAL_DUAL_APPROVAL'|'AUTO_LATENCY_BREACH'|'AUTO_LATENCY_RECOVERY'|'AUTO_PG_UNAVAILABLE';
approverUserIds: string[];
reason: string;
traceId: string;
at: string;
}

2.20 firewall.alert.mode.auto_panic.v1

interface FirewallAlertModeAutoPanic {
schemaVersion: '1';
eventId: string;
latencyP95Ms: number;
breachedForSeconds: number;
disabledRuleTypes: ('REGEX'|'CLASSIFIER')[];
traceId: string;
at: string;
}

2.21 firewall.alert.dnd.snapshot.stale.v1

interface FirewallAlertDndSnapshotStale {
schemaVersion: '1';
eventId: string;
lastSnapshotAt: string;
ageSeconds: number;
traceId: string;
at: string;
}

2.22 firewall.transit.unavailable.v1

interface FirewallTransitUnavailable {
schemaVersion: '1';
eventId: string;
observedBy: string; // smpp-connector-transit-rx pod
peerAsn: number;
errorCode: 'UNAVAILABLE'|'DEADLINE_EXCEEDED'|'INTERNAL';
failClosedAction: 'ESME_RSUBMITFAIL';
traceId: string;
at: string;
}

2.23 firewall.alert.bind.missing.v1

Connector heartbeat missing > 60 s.

interface FirewallAlertBindMissing {
schemaVersion: '1';
eventId: string;
mnoBindId: string;
mnoId: string;
lastHeartbeatAt: string;
ageSeconds: number;
traceId: string;
at: string;
}

3. Consumed events

SubjectProducerEffect
consent.dnd.snapshot.v1consent-ledger-serviceHourly snapshot URL → rebuild fw:dnd:bloom and firewall.dnd_snapshot
consent.revoked.v1consent-ledger-serviceAdd MSISDN to in-memory DND delta until next snapshot
fraud.detected.simbox.v1fraud-intel-serviceUpsert firewall.simbox_signals; matching MOs auto-BLOCK
fraud.detected.ait.v1fraud-intel-serviceUpsert firewall.ait_patterns; matching MOs auto-BLOCK
fraud.detected.greyroute.v1fraud-intel-serviceAdd implicated peer to firewall.peer_quarantine
regulator.blocklist.published.v1regulator-portal-serviceHSM-validated; upsert firewall.blocklist_entries source=REGULATOR
sender.id.suspended.v1sender-id-registry-serviceRefresh firewall.peer_senderid_allowlist cache
sender.id.registered.v1sender-id-registry-serviceSame — additive
tenant.data_residency.changed.v1tenant-serviceRefresh per-tenant geo policy cache for EGRESS_DND_CHECK rules

3.1 Consumed-event schema example: fraud.detected.simbox.v1

interface FraudDetectedSimbox {
schemaVersion: '1';
eventId: string;
modelId: string; // e.g. 'simbox-graph-v3'
modelVersion: string;
originator: string; // E.164 (full — fraud-intel scope)
confidence: number;
evidence: {
imeiChurnCount24h: number;
aNumberRotationCount24h: number;
cellIdSwitchCount24h: number;
correlatedSignals: string[];
};
emittedAt: string;
}

The firewall masks the originator (+CCNNN***) before re-emitting downstream.


4. Outbox pattern

All firewall state changes (audit insert, quarantine insert, blocklist mutation) write an event row to firewall.outbox in the same Postgres transaction. A relay worker publishes to NATS with at-least-once delivery and ack. Guarantees: no event without a persisted state change; no unpublished state change for more than a few seconds (see SYNC_CONTRACT §3).

CREATE TABLE firewall.outbox (
event_id UUID PRIMARY KEY,
subject TEXT NOT NULL,
payload JSONB NOT NULL,
partition_key TEXT, -- for sticky partition routing (e.g. mnoBindId)
published_at TIMESTAMPTZ,
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
CREATE INDEX ix_outbox_unpublished
ON firewall.outbox (created_at) WHERE published_at IS NULL;

5. Consumer contracts (downstream services)

ServiceSubjectsAction
cdr-mediation-servicefirewall.audit.v1Append firewall verdict to inbound MO CDR
fraud-intel-servicefirewall.audit.v1, firewall.simbox.detected.v1, firewall.alert.greyroute.*Feed ML training set; close-loop verdict-vs-truth comparisons
regulator-portal-servicefirewall.audit.v1, firewall.blocklist.federated.v1, firewall.federation.exported.v1Regulator submission pipeline
analytics-servicefirewall.audit.v1, firewall.alert.*, firewall.mode.changed.v1Long-term archival + dashboards
notification-servicefirewall.alert.*, firewall.peer_quarantine.entered.v1NOC + carrier-relations notifications
admin-dashboard (SSE)firewall.quarantine.*, firewall.alert.*, firewall.mode.changed.v1Live console

6. PII & security rules for events

  • Raw PDU body never appears in any firewall event.
  • MSISDNs in audit + alert events appear masked (+CCNNN***); raw MSISDNs persist only in Postgres firewall.audit (RLS-gated).
  • pduBodySha256 may appear (one-way hash — no PII content recoverable).
  • All event payloads signed by the producer's Ed25519 key (platform-wide signing); critical consumers (regulator-portal-service, cdr-mediation-service) verify opportunistically.
  • Events with PII-adjacent fields (masked MSISDN, senderId) are classified CONFIDENTIAL-INTERNAL; replication outside the platform requires Legal review.
  • Federation events (firewall.blocklist.federated.v1, firewall.federation.exported.v1) are signed with the dedicated PKCS#11 HSM key (pkcs11:object=ghasi-firewall-fed-signer).

7. Schema evolution

  • Additive optional fields are non-breaking within schemaVersion.
  • Breaking changes bump to firewall.<topic>.v2; subjects coexist for ≥ 90 days.
  • Enum values are additive. Consumers MUST treat unknown values as UNKNOWN and not fail.
  • A schema registry (Apicurio) is the source of truth; CI verifies all producers' published schemas are backward-compatible against the latest registered version.

8. Stream durability + DR

Per ADR-0004 §6:

  • FIREWALL_AUDIT is replicated 3× in kbl, mirrored 2× in mzr, and leaf-mirrored to dxb cold archive. Loss of kbl does not lose audit evidence.
  • Stream snapshots taken every 4h and uploaded to MinIO firewall-jetstream-snapshots/ for cold-recovery.
  • DR drill: quarterly, restore a snapshot into a clean cluster, verify hash-chain continuity over the last 24h of audit rows.