Skip to main content

SMS Firewall Service — Domain Model

Version: 1.0 Status: Draft Owner: Trust & Safety Last Updated: 2026-04-21 Companion: SERVICE_OVERVIEW · APPLICATION_LOGIC · API_CONTRACTS · DATA_MODEL · EVENT_SCHEMAS · SECURITY_MODEL Related ADR: ADR-0004 National-Backbone Resilience §3


1. Bounded Context

National Perimeter / Trust & Safety. The sms-firewall-service owns every policy decision about whether an SMS PDU may enter the platform — both inbound MO (deliver_sm from MNO peers) and inbound transit MT (submit_sm from peer aggregators) — together with the regulator-grade evidence trail that proves it. It is a peer to compliance-engine (outbound tenant SMS), consent-ledger-service (subscriber consent), and routing-engine (egress operator selection); it is not an aspect of any of those services.

The context boundary is drawn such that:

  • Inside the boundary: firewall rules, rule versions, verdicts, evaluation log (audit), national + federated blocklists, blocklist history, MNO bind registry, peer ASN allowlist, peer sender-ID allowlist, peer-aggregator hygiene scores, peer quarantine, AIT/SIM-box signatures, DND projection, quarantine queue, operating-mode switches.
  • Outside the boundary: subscriber consent ownership (consent-ledger-service), ML model training (fraud-intel-service), sender-ID registration (sender-id-registry-service), HLR/MNP truth (number-intelligence-service), CDR generation (cdr-mediation-service), routing decisions (routing-engine), tenant identity (tenant-service / auth-service).

The firewall consumes signals from these adjacent services but never persists their authoritative state; it persists only its own verdicts and the evidence underpinning them.


2. Aggregates

2.1 FirewallRule

The authoring unit of perimeter policy. One rule is one CEL-style sandboxed predicate over a MoContext or TransitMtContext.

FieldTypeNotes
ruleIdUUIDv4Identity (external prefix fr_)
namestringHuman-readable, unique within scope
descriptionstring | nullReviewer-facing rationale
scopeenum FirewallDirectionMO · TRANSIT_MT · EGRESS_DND_CHECK
typeenum RuleTypeORIGIN_BLOCKLIST · CONTENT_KEYWORD · CONTENT_REGEX · RATE_VOLUME · GEO_RESTRICTION · DND_PRESENT · AIT_SIGNATURE · SIMBOX_SIGNATURE · GREY_ROUTE · SENDER_ID_VERIFY · PEER_ASN · CLASSIFIER · COMPOSITE
expressionstringCEL-style expression over typed inputs (src.msisdn, dst.msisdn, mno.id, pdu.body, pdu.coding, peer.asn, consent.dndPresent, senderId)
actionenum FirewallActionALLOW · FLAG · BLOCK · QUARANTINE · RATE_LIMIT
blockReasonCodeenum BlockReason | nullMandatory when action ∈ {BLOCK, QUARANTINE}
priorityintLower = evaluated earlier (default 1000)
severityenumCRITICAL · HIGH · MEDIUM · LOW
enabledbooleanSoft-disable without deletion
versionintMonotonic; bumped on every mutation
createdBy, updatedByUUIDv4Operator identities
createdAt, updatedAt, deletedAtTIMESTAMPTZ | nullSoft-delete

Invariants

  • scope = MO rules MUST NOT reference peer.asn (transit-only field); admission rejects with RULE_INVALID_INPUT_REF.
  • scope = TRANSIT_MT rules MUST NOT reference consent.dndPresent (no DND on transit MT — that is a routing-engine concern at egress).
  • type = REGEX patterns must compile under the re2 engine, pass the ReDoS screen, and be ≤ 500 chars (per SECURITY_MODEL §3).
  • type = COMPOSITE children resolve within maxDepth = 4 and contain no cycles (DFS at save time + runtime guard).
  • type = CLASSIFIER rules must declare a fallbackAction; the platform default is QUARANTINE (fail-closed per ADR-0004 §3).
  • Rule mutations are append-only at the version level — a FirewallRuleVersion snapshot is written for every change.

2.2 FirewallRuleVersion

Append-only snapshot of a FirewallRule at a specific version. Never mutated.

FieldNotes
ruleId + versionComposite identity
snapshotJSONB of full rule state
changedByOperator UUID
changedAtTIMESTAMPTZ
changeReasonstring | null

2.3 FirewallVerdict (transient value, persisted into evidence)

The atomic unit of evaluation output, returned over the gRPC response and persisted to firewall.audit.

FieldTypeNotes
verdictIdUUIDv4External prefix fv_
traceIdstringOTel trace context (W3C)
verdictenum FirewallActionALLOW · FLAG · BLOCK · QUARANTINE
directionenum FirewallDirectionMO · TRANSIT_MT
mnoBindIdstring | nullPresent for MO; null for transit
peerAsnint | nullPresent for transit; null for MO
srcMsisdnE.164 stringA-number
dstMsisdnE.164 stringB-number
senderIdstring | nullAlphanumeric or numeric originator (transit MT)
pduFingerprintstringsha256 of srcMsisdn:dstMsisdn:senderId:body (cache key)
evaluatedRuleIdsUUIDv4[]Ordered list of rules that ran
ruleHitsRuleHit[]Rules that matched (with ruleId, severity, evidence redacted)
blockReasonenum BlockReason | nullPopulated when verdict = BLOCK/QUARANTINE
holdIdUUIDv4 | nullPresent when verdict = QUARANTINE
evaluationLatencyMsintHot-path metric
effectiveTtlSecondsintCaller may cache this verdict for repeat fingerprints
evaluatedAtTIMESTAMPTZUTC, microsecond precision
flagsstring[]MAINTENANCE_MODE, RATE_GOVERNOR_DEGRADED, BLOOM_DEGRADED, NUMINT_UNAVAILABLE, BUDGET_EXCEEDED

2.4 BlocklistEntry

A single blocklist tuple — the smallest unit a verdict can match.

FieldTypeNotes
entryIdUUIDv4External prefix be_
typeenumMSISDN · MSISDN_RANGE · SENDER_ID · KEYWORD · KEYWORD_REGEX · MCC_MNC · PEER_ASN
valuestringCanonicalised value (E.164 for MSISDN; uppercase for senderId; re2 for KEYWORD_REGEX)
sourceenum BlocklistSourceREGULATOR · PEER_MNO · INTERNAL · FRAUD_INTEL · OPERATOR_MANUAL
regulatorRefstring | nullRegulator-issued reference (mandatory when source = REGULATOR)
sourcesJSONBArray of {sourceId, sourceType, reportedAt} for federation
confidenceScorenumeric(3,2)0.00–1.00 (see formula §6)
autoApplybooleanDerived: confidenceScore >= 0.8
shareWithPeersbooleanIf true, included in daily federation export
activebooleanSoft-delete flag
addedByUUIDv4 | nullOperator (null for federation imports)
addedAt, deactivatedAt, expiresAtTIMESTAMPTZLifecycle

Invariants

  • (source, regulatorRef, type, value) is unique — federation replay is idempotent.
  • active may transition TRUE → FALSE (soft-delete). Once FALSE, the row is never deleted; it remains for regulator audit.
  • An entry with confidenceScore < 0.4 is auto-deactivated by the reputation worker.

2.5 NationalBlocklist

The authoritative collection of blocklist entries for a single direction (MO, TRANSIT_MT, or EGRESS_DND_CHECK).

FieldNotes
blocklistIdUUIDv4 (external prefix bl_)
nameHuman-readable (e.g. national-mo-blocklist, national-transit-mt-blocklist, dnd-snapshot)
directionenum FirewallDirection
entryCountint (cached; recomputed nightly)
bloomFilterCapacityint
bloomFalsePositiveRatenumeric(4,3) (default 0.010)
lastFederatedAtTIMESTAMPTZ | null
versionint
entriesBlocklistEntry[] (1:N, lazy)

2.6 FirewallAuditEntry

Append-only record of every evaluation (regardless of verdict). The primary regulator-defensible evidence feed.

FieldNotes
auditIdUUIDv4
verdictIdUUIDv4 (FK)
traceId, evaluationLatencyMsOperational
verdict, direction, srcMsisdn, dstMsisdn, senderId, mnoBindId, peerAsn, pduFingerprint, blockReason, evaluatedRuleIds[], ruleHits (JSONB)Verdict snapshot
pduBodySha256hex string
prevHashhex string
rowHashhex string
verdictAtTIMESTAMPTZ

Invariants

  • Append-only at the database level. UPDATE/DELETE blocked by Postgres rules (see DATA_MODEL §3.1).
  • Every row carries rowHash chained to the previous row's rowHash within the partition, enabling offline integrity verification by the audit-verifier worker.
  • Retention ≥ 7 years for regulated deployments (ATRA national-asset SLA).

2.7 FirewallQuarantineEntry

A PDU placed on hold for NOC manual review.

FieldNotes
holdIdUUIDv4 (external prefix fq_)
verdictIdUUIDv4 (FK to verdict that triggered hold)
directionMO · TRANSIT_MT
originalPduCipherbytea (AES-256-GCM, per-tenant or per-MNO KEK)
triggerRuleIdsUUIDv4[]
reasonCodeenum (e.g. SENDER_ID_UNKNOWN, PEER_HYGIENE_SCORE_LOW, FRAUD_INTEL_SIGNAL)
statusenum
reviewerUserId, reviewNotes, reviewedAtReview fields
heldAt, expiresAtTIMESTAMPTZ (default expiresAt = heldAt + 24h)

Invariants

  • One-way state machine: PENDING → REVIEWING → {RELEASED, REJECTED}. AUTO_EXPIRED reachable only from PENDING via the auto-expiry cron.
  • RELEASED re-injects the original PDU through the originating connector via firewall.quarantine.released.v1; the connector skips firewall re-evaluation (decision is governing).
  • After a terminal state, the row is retained 7 years in cold archive.

2.8 MnoBindRegistryEntry

The registry of every authorised smpp-connector-{mno}-{rx|trx|tx} deployment.

FieldNotes
mnoBindIdstring PK (e.g. awcc-rx-01)
mnoIdenum (AWCC, Roshan, Etisalat, MTN_AF, Salaam, Transit)
directionMO · MT · MO_MT
permittedCountryCodesstring[] (E.164 CCs, e.g. ["+93"])
permittedSenderIdsstring[]
lastHeartbeatAtTIMESTAMPTZ
activeboolean

2.9 PeerAggregator

External transit-MT peer (international or domestic non-MNO).

FieldNotes
peerIdUUIDv4 (external prefix pa_)
peerSystemIdstring (SMPP system_id)
peerAsnint (BGP ASN of source)
permittedSenderIdsstring[]
permittedDstMnoIdsstring[] (peer routes only to these MNOs)
quarantinedboolean
quarantinedReasonstring | null
hygieneScoreint 0–100 (rolling 24h)

2.10 SimBoxSignal and AitPattern

Derived evidence aggregates fed by fraud-intel-service and the firewall's own per-source counters.

SimBoxSignal fieldNotes
signalIdUUIDv4
originatorE.164 string
confidencenumeric(3,2)
evidenceJsonJSONB (call-detail patterns, IMEI churn, A-number rotation)
firstSeenAt, lastSeenAtTIMESTAMPTZ
activeboolean
AitPattern fieldNotes
patternIdUUIDv4
patternTypeOTP_HARVEST · PUMPED_TRAFFIC · IRSF (International Revenue Share Fraud per GSMA FF.21)
dstMsisdnRangestring (e.g. +9370012XXXX)
confidence, evidenceJson, firstSeenAt, lastSeenAt, activeAs above

3. Value Objects

VOShapeInvariants
MsisdnE.164 stringValidated against ^\+[1-9]\d{6,14}$; canonicalised on construction
SenderIdstring ≤ 11 chars (alphanumeric) or E.164 numericUppercased; whitespace trimmed; control chars rejected
FirewallDirectionenumMO · TRANSIT_MT · EGRESS_DND_CHECK
FirewallActionenumALLOW · FLAG · BLOCK · QUARANTINE · RATE_LIMIT
BlockReasonenumORIGIN_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
MoContext{ srcMsisdn, dstMsisdn, mnoBindId, pduBody, pduCoding (0/3/8), pduTon, pduNpi, recvTs, traceId, smppSequenceNumber }pduBody ≤ 1600 chars; recvTs within 60 s of now
TransitMtContext{ peerAsn, peerSystemId, srcAddr, dstMsisdn, senderId, pduBody, pduTon, pduNpi, registeredDelivery, esmClass }peerAsn ∈ [0, 4294967295]
RuleHit{ ruleId, ruleName, ruleType, action, severity, evidence (redacted), confidence? }evidence is redacted; matched span replaced by *** with ≥ 4-char prefix/suffix context
PduFingerprintsha256(srcMsisdn:dstMsisdn:senderId:body)Deterministic; cache key (fw:verdict:{fp})

4. Domain Events (produced)

Detailed schemas in EVENT_SCHEMAS.md.

EventTriggerPayload core
firewall.audit.v1Every FilterInbound / EvaluateTransit call (regardless of verdict)verdictId, verdict, direction, pduFingerprint, evaluatedRuleIds, ruleHits, blockReason?, holdId?
firewall.alert.mo.blocked.v1Verdict = BLOCK on MO directionverdictId, srcMsisdn (masked), dstMsisdn (masked), mnoBindId, blockReason
firewall.alert.transit.blocked.v1Verdict = BLOCK on TRANSIT_MTverdictId, peerAsn, peerSystemId, senderId, dstMsisdn (masked), blockReason
firewall.alert.greyroute.detected.v1GREY_ROUTE rule triggeredpeerAsn, dstHomeMnoId, evidence
firewall.alert.greyroute.heuristic.v1Peer >30% MT to non-peered MNO over last 1000 submissionspeerId, nonPeeredFraction
firewall.simbox.detected.v1New SimBoxSignal added with confidence ≥ 0.7originator, confidence, evidenceJson
firewall.alert.ait.detected.v1New AitPattern added with confidence ≥ 0.7patternType, dstMsisdnRange, confidence
firewall.quarantine.held.v1Verdict = QUARANTINEholdId, triggerRuleIds, expiresAt
firewall.quarantine.released.v1NOC releaseholdId, reviewerUserId, reviewNotes
firewall.quarantine.rejected.v1NOC rejectSame shape
firewall.quarantine.expired.v1Auto-expiryholdId, expiredAt
firewall.blocklist.changed.v1Blocklist entry add/remove/deactivateentryId, action, actorUserId
firewall.blocklist.federated.v1Federation import completesource, addedCount, removedCount, regulatorRef
firewall.federation.exported.v1Daily export job completeexportSha256, signature, presignedUrl
firewall.federation.heartbeat.v1Daily even on zero-diffexportedAt
firewall.alert.federation.signature.invalid.v1HSM signature check failedeventId, signer
firewall.peer_quarantine.entered.v1Peer auto-quarantined (hygiene < 30)peerId, score, reasons
firewall.alert.peer.degraded.v1Hygiene score < 60 for 3 windowspeerId, scoreHistory
firewall.rule.changed.v1Rule CRUDentityType, entityId, action, version, actorUserId
firewall.rule.degraded.v1Rule auto-disabled (e.g. regex timeout)ruleId, reason
firewall.mode.changed.v1Operating-mode switchpreviousMode, newMode, actorUserIds[], reason
firewall.alert.mode.auto_panic.v1Auto-trip to PANIClatencyP95Ms, breachedFor
firewall.alert.dnd.snapshot.stale.v1DND snapshot older than 6hlastSnapshotAt, ageSeconds
firewall.transit.unavailable.v1Connector observed firewall UNAVAILABLEpeerAsn, errorCode

5. Consumed Events

SubjectProducerEffect
fraud.detected.simbox.v1fraud-intel-serviceUpsert SimBoxSignal; matching MOs auto-BLOCK with SIMBOX_SIGNATURE
fraud.detected.ait.v1fraud-intel-serviceUpsert AitPattern; matching MOs auto-BLOCK with AIT_SIGNATURE
fraud.detected.greyroute.v1fraud-intel-serviceAdd implicated peer to firewall.peer_quarantine; future MT auto-QUARANTINE
consent.dnd.snapshot.v1consent-ledger-serviceMaterialise national DND projection into fw:dnd:bloom and firewall.dnd_snapshot
consent.revoked.v1consent-ledger-serviceAdd msisdn to in-memory DND delta until next snapshot rebuild
regulator.blocklist.published.v1regulator-portal-serviceValidate HSM signature; upsert into firewall.blocklist_entries with source=REGULATOR
sender.id.suspended.v1sender-id-registry-serviceRefresh local firewall.peer_senderid_allowlist cache; matching transit MT now BLOCK with SENDER_ID_SUSPENDED
sender.id.registered.v1sender-id-registry-serviceRefresh local cache for the affected peer

6. Confidence-Score Formula (Federated Blocklist)

Blocklist entries carry a confidenceScore derived from federation source attribution:

confidence_score = clamp(
regulator_count * 1.0
+ peer_count * 0.5
+ internal_count * 0.7
+ fraud_intel_count * 0.6,
0.0, 1.0
)

regulator_count = count(sources WHERE sourceType='REGULATOR')
peer_count = count(sources WHERE sourceType='PEER_MNO')
internal_count = count(sources WHERE sourceType='INTERNAL' OR 'OPERATOR_MANUAL')
fraud_intel_count = count(sources WHERE sourceType='FRAUD_INTEL')

Tier mapping:

  • score ≥ 0.8auto_apply = TRUE (MATCH → BLOCK)
  • 0.4 ≤ score < 0.8 → probation: MATCH → QUARANTINE for 24h, then re-evaluate
  • score < 0.4 → auto-deactivate; emit firewall.blocklist.entry.deactivated.v1

7. Global Invariants

  • Fail-closed (national-asset bar). No FirewallVerdict other than ALLOW or FLAG allows a PDU to proceed. Absence of a verdict (timeout, Postgres unavailable + Redis cold) prevents proceeding — the connector returns ESME_RSUBMITFAIL (command_status = 0x00000045) for transit MT and routes MO to local-disk WAL for replay (preserving subscriber relationship per SERVICE_OVERVIEW §10).
  • Allowlist-first. Rules with action = ALLOW evaluate before any restriction rule and short-circuit on match.
  • Append-only audit. firewall.audit rejects UPDATE/DELETE at the DB level. Hash-chain integrity is verified offline by the audit-verifier worker every 24h.
  • No tenant ID on inbound MO firewall path. Inbound MO does not yet belong to a tenant — keying is by srcMsisdn, dstMsisdn, mnoBindId. Tenant resolution is performed downstream by consent-ledger / routing-engine.
  • Body confidentiality. Message body is never stored in plain text. Audit stores only pduBodySha256. Quarantine stores AES-256-GCM ciphertext under per-MNO KEK; only NOC reviewers (role=tns-noc) with quorum can decrypt.
  • Rule selection determinism. Same (MoContext|TransitMtContext, ruleSetVersion) always produces the same verdict; cached in Redis under fw:verdict:{pduFingerprint} for effectiveTtlSeconds (default 60s for ALLOW, 0 for BLOCK to force re-evaluation).
  • Release is authoritative. Once NOC releases a quarantined PDU, re-injection skips the firewall (skipFirewall: true flag on firewall.quarantine.released.v1) to prevent quarantine→release→quarantine loops. The reviewer's decision is governing, captured in audit.
  • Hash-chain continuity. Every audit-row's prevHash MUST equal the previous row's rowHash within the partition. A break is a CRITICAL alert (FirewallAuditChainBreak).

8. State Machines

8.1 Quarantine Entry State Machine

┌──────────┐
│ PENDING │◀── verdict = QUARANTINE
└────┬─────┘
│ NOC opens hold (REST GET)

┌──────────┐
│REVIEWING │
└─┬──────┬─┘
release(notes) reject(reason) expires_at < now()
│ │ │
▼ ▼ ▼
┌────────┐ ┌────────┐ ┌──────────────┐
│RELEASED│ │REJECTED│ ◀───┐ │ AUTO_EXPIRED │
└────────┘ └────────┘ │ └──────────────┘
│ ▲
└─── from PENDING only ──┘

8.2 Operating-Mode State Machine (per SERVICE_OVERVIEW §11)

┌────────┐ P95 > 100ms for 60s ┌─────────┐
│ NORMAL │ ─────────────────────▶ │ PANIC │
└───┬────┘ └────┬────┘
PG/Redis│ unhealthy │ P95 < 30ms 5m
▼ ▼
┌─────────┐ cluster recovers ┌────────┐
│DEGRADED │ ─────────────────────▶│ NORMAL │
└─────────┘ └────────┘
admin POST /v1/admin/firewall/mode {targetMode: MAINTENANCE}
requires dual-approval (60s window)
─────────────────────────▶ MAINTENANCE

8.3 Peer Hygiene State Machine

score >= 60 ─── NORMAL
60 > score >= 30 (3 windows) ─── DEGRADED → alert.peer.degraded.v1
score < 30 ─── AUTO_QUARANTINE → peer_quarantine.entered.v1
(manual dual-approval to release)

9. Aggregate Lifecycle Summary

AggregateCreated byMutated byRetired by
FirewallRuleT&S admin via REST POST /v1/admin/firewall/rulesT&S admin via PUT (new version row); auto-disable on regex timeoutSoft-delete (admin)
BlocklistEntryFederation import OR operator REST OR fraud-intel signalReputation worker (score) OR operatorSoft-delete (active=FALSE); never hard-deleted
FirewallVerdict / FirewallAuditEntryEvery FilterInbound/EvaluateTransit callImmutablePartition pruned after 7 years (regulated)
FirewallQuarantineEntryVerdict = QUARANTINENOC review OR auto-expire cronTerminal state; cold archive 7y
MnoBindRegistryEntryCarrier-relations adminHeartbeat updates lastHeartbeatAtSoft-delete
PeerAggregatorCarrier-relations adminHygiene-score worker; quarantine workerSoft-delete (de-peer)
SimBoxSignal / AitPatternfraud-intel-service eventReputation expiry (60d sliding window)active=FALSE