Skip to main content

SMS Firewall Service — Data Model

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

Schema: firewall. Owned exclusively by sms-firewall-service. No other service reads or writes these tables directly — integration is over gRPC (FilterInbound/EvaluateTransit/CheckOutboundEgress), HTTP REST admin, or NATS events.


1. Postgres tables

-- =====================================================================
-- ENUMERATED TYPES
-- =====================================================================

CREATE SCHEMA IF NOT EXISTS firewall;

CREATE TYPE firewall.direction AS ENUM ('MO','TRANSIT_MT','EGRESS_DND_CHECK');

CREATE TYPE firewall.action AS ENUM ('ALLOW','FLAG','BLOCK','QUARANTINE','RATE_LIMIT');

CREATE TYPE firewall.block_reason AS ENUM (
'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'
);

CREATE TYPE firewall.rule_type AS ENUM (
'ORIGIN_BLOCKLIST','CONTENT_KEYWORD','CONTENT_REGEX','RATE_VOLUME',
'GEO_RESTRICTION','DND_PRESENT','AIT_SIGNATURE','SIMBOX_SIGNATURE',
'GREY_ROUTE','SENDER_ID_VERIFY','PEER_ASN','CLASSIFIER','COMPOSITE'
);

CREATE TYPE firewall.severity AS ENUM ('CRITICAL','HIGH','MEDIUM','LOW');

CREATE TYPE firewall.blocklist_source AS ENUM (
'REGULATOR','PEER_MNO','INTERNAL','FRAUD_INTEL','OPERATOR_MANUAL'
);

CREATE TYPE firewall.blocklist_entry_type AS ENUM (
'MSISDN','MSISDN_RANGE','SENDER_ID','KEYWORD','KEYWORD_REGEX','MCC_MNC','PEER_ASN'
);

CREATE TYPE firewall.hold_status AS ENUM (
'PENDING','REVIEWING','RELEASED','REJECTED','AUTO_EXPIRED'
);

CREATE TYPE firewall.operating_mode AS ENUM (
'NORMAL','DEGRADED','PANIC','MAINTENANCE'
);

-- =====================================================================
-- FIREWALL RULES
-- =====================================================================

CREATE TABLE firewall.rules (
rule_id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
name TEXT NOT NULL,
description TEXT,
scope firewall.direction NOT NULL,
type firewall.rule_type NOT NULL,
expression TEXT NOT NULL, -- CEL-style
expression_inputs TEXT[] NOT NULL DEFAULT '{}',-- GIN indexed
action firewall.action NOT NULL,
block_reason_code firewall.block_reason,
priority INT NOT NULL DEFAULT 1000,
severity firewall.severity NOT NULL DEFAULT 'MEDIUM',
enabled BOOLEAN NOT NULL DEFAULT TRUE,
version INT NOT NULL DEFAULT 1,
created_by UUID NOT NULL,
updated_by UUID NOT NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
deleted_at TIMESTAMPTZ,
CONSTRAINT chk_block_reason_when_blocking
CHECK (action IN ('BLOCK','QUARANTINE') = (block_reason_code IS NOT NULL))
);
CREATE INDEX ix_rules_scope_active_priority
ON firewall.rules (scope, enabled, priority)
WHERE deleted_at IS NULL;
CREATE INDEX ix_rules_type ON firewall.rules (type) WHERE deleted_at IS NULL;
CREATE INDEX ix_rules_expression_inputs ON firewall.rules USING GIN (expression_inputs);

CREATE TABLE firewall.rule_versions (
rule_id UUID NOT NULL,
version INT NOT NULL,
snapshot JSONB NOT NULL,
changed_by UUID NOT NULL,
change_reason TEXT,
changed_at TIMESTAMPTZ NOT NULL DEFAULT now(),
PRIMARY KEY (rule_id, version)
);

-- =====================================================================
-- NATIONAL BLOCKLIST
-- =====================================================================

CREATE TABLE firewall.blocklists (
blocklist_id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
name TEXT NOT NULL UNIQUE,
direction firewall.direction NOT NULL,
entry_count BIGINT NOT NULL DEFAULT 0,
bloom_capacity BIGINT NOT NULL DEFAULT 10000000,
bloom_fp_rate NUMERIC(4,3) NOT NULL DEFAULT 0.010,
last_federated_at TIMESTAMPTZ,
version INT NOT NULL DEFAULT 1,
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
);

CREATE TABLE firewall.blocklist_entries (
entry_id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
blocklist_id UUID NOT NULL REFERENCES firewall.blocklists(blocklist_id),
type firewall.blocklist_entry_type NOT NULL,
value TEXT NOT NULL,
source firewall.blocklist_source NOT NULL,
regulator_ref TEXT, -- mandatory when source='REGULATOR'
sources JSONB NOT NULL DEFAULT '[]', -- [{sourceId, sourceType, reportedAt}]
confidence_score NUMERIC(3,2) NOT NULL DEFAULT 0.00,
auto_apply BOOLEAN NOT NULL DEFAULT FALSE,
share_with_peers BOOLEAN NOT NULL DEFAULT FALSE,
active BOOLEAN NOT NULL DEFAULT TRUE,
added_by UUID,
added_at TIMESTAMPTZ NOT NULL DEFAULT now(),
deactivated_at TIMESTAMPTZ,
expires_at TIMESTAMPTZ,
CONSTRAINT uq_blocklist_entry_idempotency
UNIQUE (source, regulator_ref, type, value),
CONSTRAINT chk_regulator_ref_when_regulator
CHECK ((source = 'REGULATOR') = (regulator_ref IS NOT NULL))
);
CREATE INDEX ix_blentries_list_active
ON firewall.blocklist_entries (blocklist_id, active);
CREATE INDEX ix_blentries_value_active
ON firewall.blocklist_entries (type, value) WHERE active = TRUE;
CREATE INDEX ix_blentries_share_with_peers
ON firewall.blocklist_entries (updated_at) WHERE share_with_peers = TRUE AND active = TRUE;

CREATE TABLE firewall.blocklist_audit (
audit_id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
entry_id UUID NOT NULL,
action TEXT NOT NULL, -- 'created'|'source_added'|'confidence_changed'|...
before JSONB,
after JSONB,
actor TEXT NOT NULL, -- operator UUID or 'SYSTEM' or regulator_ref
reason TEXT,
occurred_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
CREATE INDEX ix_bl_audit_entry ON firewall.blocklist_audit (entry_id, occurred_at DESC);

-- =====================================================================
-- AUDIT LOG (partitioned monthly)
-- Append-only, hash-chained, regulator-grade evidence feed
-- =====================================================================

CREATE TABLE firewall.audit (
audit_id UUID NOT NULL DEFAULT gen_random_uuid(),
verdict_id UUID NOT NULL,
trace_id TEXT NOT NULL,
verdict firewall.action NOT NULL,
direction firewall.direction NOT NULL,
src_msisdn TEXT,
dst_msisdn TEXT,
sender_id TEXT,
mno_bind_id TEXT,
peer_asn BIGINT,
pdu_fingerprint TEXT NOT NULL, -- sha256 hex
pdu_body_sha256 TEXT NOT NULL, -- sha256 hex of body (never body itself)
block_reason firewall.block_reason,
evaluated_rule_ids UUID[] NOT NULL DEFAULT '{}',
rule_hits JSONB NOT NULL DEFAULT '[]',
rule_set_version BIGINT NOT NULL,
operating_mode firewall.operating_mode NOT NULL,
flags TEXT[] NOT NULL DEFAULT '{}',
evaluation_latency_ms INT NOT NULL,
prev_hash TEXT NOT NULL, -- 64-hex; chain predecessor
row_hash TEXT NOT NULL, -- 64-hex; computed = sha256(prev_hash || canonicalJson(row))
verdict_at TIMESTAMPTZ NOT NULL,
PRIMARY KEY (verdict_at, audit_id)
) PARTITION BY RANGE (verdict_at);

-- BRIN index for efficient time-range scans over the append-only table
CREATE INDEX ix_audit_verdict_at_brin ON firewall.audit USING BRIN (verdict_at);
CREATE INDEX ix_audit_src_msisdn
ON firewall.audit (src_msisdn, verdict_at DESC) WHERE src_msisdn IS NOT NULL;
CREATE INDEX ix_audit_dst_msisdn
ON firewall.audit (dst_msisdn, verdict_at DESC) WHERE dst_msisdn IS NOT NULL;
CREATE INDEX ix_audit_peer_asn
ON firewall.audit (peer_asn, verdict_at DESC) WHERE peer_asn IS NOT NULL;
CREATE INDEX ix_audit_mno_bind_id
ON firewall.audit (mno_bind_id, verdict_at DESC) WHERE mno_bind_id IS NOT NULL;
CREATE INDEX ix_audit_pdu_fingerprint
ON firewall.audit (pdu_fingerprint, verdict_at DESC);

-- Example monthly partitions (PartitionMaintenanceWorker pre-creates 3 months ahead)
-- CREATE TABLE firewall.audit_2026_04
-- PARTITION OF firewall.audit
-- FOR VALUES FROM ('2026-04-01') TO ('2026-05-01');

-- =====================================================================
-- QUARANTINE QUEUE
-- =====================================================================

CREATE TABLE firewall.quarantine_queue (
hold_id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
verdict_id UUID NOT NULL,
direction firewall.direction NOT NULL,
original_pdu_cipher BYTEA NOT NULL, -- AES-256-GCM (per-MNO KEK)
original_pdu_iv BYTEA NOT NULL,
original_pdu_tag BYTEA NOT NULL,
kek_id TEXT NOT NULL, -- Vault Transit key ref
trigger_rule_ids UUID[] NOT NULL DEFAULT '{}',
reason_code TEXT NOT NULL,
status firewall.hold_status NOT NULL DEFAULT 'PENDING',
reviewer_user_id UUID,
review_notes TEXT,
reviewed_at TIMESTAMPTZ,
held_at TIMESTAMPTZ NOT NULL DEFAULT now(),
expires_at TIMESTAMPTZ NOT NULL
);
CREATE INDEX ix_quarantine_status_expiry
ON firewall.quarantine_queue (status, expires_at)
WHERE status IN ('PENDING','REVIEWING');

-- =====================================================================
-- MNO BIND REGISTRY
-- =====================================================================

CREATE TABLE firewall.mno_bind_registry (
mno_bind_id TEXT PRIMARY KEY,
mno_id TEXT NOT NULL,
direction TEXT NOT NULL, -- 'MO' | 'MT' | 'MO_MT'
permitted_country_codes TEXT[] NOT NULL DEFAULT '{}',
permitted_sender_ids TEXT[] NOT NULL DEFAULT '{}',
pod_name TEXT,
last_heartbeat_at TIMESTAMPTZ,
active BOOLEAN NOT NULL DEFAULT TRUE,
notes TEXT,
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
CREATE INDEX ix_mno_binds_heartbeat
ON firewall.mno_bind_registry (last_heartbeat_at) WHERE active = TRUE;

-- =====================================================================
-- PEER AGGREGATORS / ASN ALLOWLIST
-- =====================================================================

CREATE TABLE firewall.peer_aggregators (
peer_id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
peer_system_id TEXT NOT NULL UNIQUE,
peer_asn BIGINT NOT NULL,
permitted_sender_ids TEXT[] NOT NULL DEFAULT '{}',
permitted_dst_mno_ids TEXT[] NOT NULL DEFAULT '{}',
hygiene_score INT NOT NULL DEFAULT 100,
quarantined BOOLEAN NOT NULL DEFAULT FALSE,
quarantined_reason TEXT,
quarantined_at TIMESTAMPTZ,
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
CREATE INDEX ix_peers_asn ON firewall.peer_aggregators (peer_asn);
CREATE INDEX ix_peers_quarantined ON firewall.peer_aggregators (quarantined) WHERE quarantined = TRUE;

CREATE TABLE firewall.peer_asn_allowlist (
peer_asn BIGINT PRIMARY KEY,
notes TEXT,
active BOOLEAN NOT NULL DEFAULT TRUE,
added_by UUID NOT NULL,
added_at TIMESTAMPTZ NOT NULL DEFAULT now()
);

CREATE TABLE firewall.peer_senderid_allowlist (
peer_id UUID NOT NULL REFERENCES firewall.peer_aggregators(peer_id),
sender_id TEXT NOT NULL,
active BOOLEAN NOT NULL DEFAULT TRUE,
added_at TIMESTAMPTZ NOT NULL DEFAULT now(),
PRIMARY KEY (peer_id, sender_id)
);

CREATE TABLE firewall.peer_mno_routes (
peer_id UUID NOT NULL REFERENCES firewall.peer_aggregators(peer_id),
mno_id TEXT NOT NULL,
PRIMARY KEY (peer_id, mno_id)
);

CREATE TABLE firewall.peer_hygiene_scores (
peer_id UUID NOT NULL,
window_start TIMESTAMPTZ NOT NULL,
window_end TIMESTAMPTZ NOT NULL,
score INT NOT NULL,
sample_count BIGINT NOT NULL,
contributing_reasons JSONB NOT NULL DEFAULT '{}',
PRIMARY KEY (peer_id, window_end)
);

-- =====================================================================
-- FRAUD INTEL SIGNAL MIRRORS
-- =====================================================================

CREATE TABLE firewall.simbox_signals (
signal_id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
originator TEXT NOT NULL, -- E.164
confidence NUMERIC(3,2) NOT NULL,
evidence_json JSONB NOT NULL,
first_seen_at TIMESTAMPTZ NOT NULL,
last_seen_at TIMESTAMPTZ NOT NULL,
active BOOLEAN NOT NULL DEFAULT TRUE,
CONSTRAINT uq_simbox_originator UNIQUE (originator)
);
CREATE INDEX ix_simbox_active ON firewall.simbox_signals (originator) WHERE active = TRUE;

CREATE TABLE firewall.ait_patterns (
pattern_id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
pattern_type TEXT NOT NULL, -- 'OTP_HARVEST'|'PUMPED_TRAFFIC'|'IRSF'
dst_msisdn_range TEXT NOT NULL,
confidence NUMERIC(3,2) NOT NULL,
evidence_json JSONB NOT NULL,
first_seen_at TIMESTAMPTZ NOT NULL,
last_seen_at TIMESTAMPTZ NOT NULL,
active BOOLEAN NOT NULL DEFAULT TRUE
);
CREATE INDEX ix_ait_range ON firewall.ait_patterns (dst_msisdn_range) WHERE active = TRUE;

-- =====================================================================
-- DND SNAPSHOT (from consent-ledger)
-- =====================================================================

CREATE TABLE firewall.dnd_snapshot (
msisdn TEXT PRIMARY KEY,
revoked_at TIMESTAMPTZ NOT NULL,
source_snapshot TEXT NOT NULL, -- snapshot URL/hash from consent-ledger
loaded_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
CREATE INDEX ix_dnd_loaded_at ON firewall.dnd_snapshot (loaded_at);

CREATE TABLE firewall.dnd_snapshot_meta (
snapshot_id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
snapshot_url TEXT NOT NULL,
sha256 TEXT NOT NULL,
entry_count BIGINT NOT NULL,
loaded_at TIMESTAMPTZ NOT NULL DEFAULT now(),
current BOOLEAN NOT NULL DEFAULT TRUE
);

-- =====================================================================
-- RATE OVERRIDES + FEDERATION LOG
-- =====================================================================

CREATE TABLE firewall.rate_overrides (
scope_type TEXT NOT NULL, -- 'SRC_MSISDN'|'PEER'|'MNO_BIND'
scope_value TEXT NOT NULL,
window TEXT NOT NULL, -- '1s'|'1m'|'5m'|'1h'|'24h'
threshold BIGINT NOT NULL,
reason TEXT,
added_by UUID NOT NULL,
added_at TIMESTAMPTZ NOT NULL DEFAULT now(),
PRIMARY KEY (scope_type, scope_value, window)
);

CREATE TABLE firewall.federation_log (
log_id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
direction TEXT NOT NULL, -- 'IMPORT' | 'EXPORT'
source TEXT NOT NULL, -- 'REGULATOR' | 'PEER_MNO' | 'SELF'
batch_id TEXT NOT NULL,
sha256 TEXT,
signature TEXT,
entries_added BIGINT NOT NULL DEFAULT 0,
entries_removed BIGINT NOT NULL DEFAULT 0,
signer_key_id TEXT,
occurred_at TIMESTAMPTZ NOT NULL DEFAULT now()
);

-- =====================================================================
-- OPERATING MODE + OUTBOX
-- =====================================================================

CREATE TABLE firewall.operating_mode (
id INT PRIMARY KEY DEFAULT 1,
current_mode firewall.operating_mode NOT NULL DEFAULT 'NORMAL',
set_at TIMESTAMPTZ NOT NULL DEFAULT now(),
set_by TEXT NOT NULL DEFAULT 'SYSTEM',
reason TEXT,
CONSTRAINT only_one_row CHECK (id = 1)
);

CREATE TABLE firewall.operating_mode_history (
change_id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
previous_mode firewall.operating_mode NOT NULL,
new_mode firewall.operating_mode NOT NULL,
trigger TEXT NOT NULL, -- 'MANUAL_DUAL_APPROVAL'|'AUTO_LATENCY_BREACH'|...
approver_user_ids UUID[] NOT NULL DEFAULT '{}',
reason TEXT,
occurred_at TIMESTAMPTZ NOT NULL DEFAULT now()
);

CREATE TABLE firewall.outbox (
event_id UUID PRIMARY KEY,
subject TEXT NOT NULL,
payload JSONB NOT NULL,
partition_key TEXT,
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;

2. Append-only protection (hash-chain + triggers)

-- Block UPDATE/DELETE on the audit table
CREATE RULE fw_audit_no_update AS
ON UPDATE TO firewall.audit DO INSTEAD NOTHING;
CREATE RULE fw_audit_no_delete AS
ON DELETE TO firewall.audit DO INSTEAD NOTHING;

CREATE RULE fw_blaudit_no_update AS
ON UPDATE TO firewall.blocklist_audit DO INSTEAD NOTHING;
CREATE RULE fw_blaudit_no_delete AS
ON DELETE TO firewall.blocklist_audit DO INSTEAD NOTHING;

-- Hash-chain insertion trigger
CREATE OR REPLACE FUNCTION firewall.compute_audit_hash()
RETURNS trigger LANGUAGE plpgsql AS $$
DECLARE
v_prev_hash TEXT;
BEGIN
SELECT row_hash INTO v_prev_hash
FROM firewall.audit
WHERE verdict_at <= NEW.verdict_at
ORDER BY verdict_at DESC, audit_id DESC
LIMIT 1;
IF v_prev_hash IS NULL THEN
v_prev_hash := repeat('0', 64); -- genesis row
END IF;
NEW.prev_hash := v_prev_hash;
NEW.row_hash := encode(
digest(
v_prev_hash || to_jsonb(NEW) #- '{row_hash}'::text[]::text,
'sha256'
),
'hex'
);
RETURN NEW;
END $$;

CREATE TRIGGER trg_audit_hash
BEFORE INSERT ON firewall.audit
FOR EACH ROW EXECUTE FUNCTION firewall.compute_audit_hash();

Retention is managed by dropping old partitions via the PartitionMaintenanceWorker, never by DELETE.


3. Row-Level Security

The firewall schema is platform-scoped (no tenant FK on hot-path rows), so RLS is used narrowly to gate operator roles:

-- NOC can only read quarantine holds that have not yet been archived;
-- regulator auditors can read audit+quarantine but not decrypt payloads.
ALTER TABLE firewall.quarantine_queue ENABLE ROW LEVEL SECURITY;

CREATE POLICY q_noc_read ON firewall.quarantine_queue
FOR SELECT
USING (current_setting('app.caller_role') IN ('noc','tns-admin'));

CREATE POLICY q_regulator_metadata ON firewall.quarantine_queue
FOR SELECT
USING (current_setting('app.caller_role') = 'regulator-auditor')
WITH CHECK (false); -- metadata-only; decryption happens in application layer with role check

The REST layer sets SET LOCAL app.caller_role = '<role>' per request based on Kong-injected X-Roles.


4. Redis keys

All keys namespaced fw:*.

KeyTTLPurpose
fw:verdict:{pduFingerprint}effectiveTtlSeconds (default 60s ALLOW, 0 BLOCK)Verdict cache
fw:rate:src-msisdn:{e164}:1s5sSliding-window ZSET
fw:rate:src-msisdn:{e164}:1m120sSliding-window ZSET
fw:rate:src-msisdn:{e164}:1h4000sSliding-window ZSET
fw:rate:dst-msisdn:{e164}:1m120sSame pattern
fw:rate:peer:{peerId}:1m120sPer-peer
fw:rate:mno-bind:{bindId}:1m120sPer-bind flood guard
fw:blocklist:nationalno TTL (rebuilt nightly + on event)BF.RESERVE Redis Bloom, capacity 10M, fp 0.01
fw:blocklist:transitno TTLSeparate Bloom for transit direction
fw:dnd:bloomno TTLNational DND Bloom (from consent-ledger hourly snapshot)
fw:rules:active:{scope}60sProcess-cache invalidation marker
fw:senderid:peer-allowlist:{peerId}3600sCached from sender-id-registry
fw:ait:pattern:{dstPrefix}3600sAitPattern lookup
fw:simbox:{originator}86400sSimBoxSignal lookup
fw:peer:hygiene:{peerId}300sLatest score
fw:mode:currentno TTLActive operating mode (watched for changes)
fw:fed:export:lock3600sDistributed cron lock
fw:quarantine:expiry:lock300sDistributed cron lock
fw:audit:verify:lock7200sDistributed cron lock
fw:bind:heartbeat:{bindId}120sConnector liveness

5. MinIO buckets

BucketObject LockRetentionPurpose
firewall-audit-archiveCompliance mode7 yearsDaily Parquet+zstd partition export, HSM-signed; regulator-submittable
firewall-federation-outCompliance7 yearsDaily HSM-signed federation export (JSON Lines + .sig)
firewall-federation-inGovernance1 yearCache of regulator-sent imports (validated before apply)
firewall-quarantine-archiveCompliance7 yearsEncrypted PDU ciphertext for terminal quarantine holds
firewall-jetstream-snapshotsGovernance90 daysJetStream stream snapshots for DR

6. ID prefixes (external)

PrefixEntity
fr_FirewallRule
fv_FirewallVerdict (audit row key)
bl_Blocklist
be_BlocklistEntry
fq_QuarantineEntry
fp_PeerAggregator (pa_ in some contexts; standardised to fp_)
fa_Audit query receipt
fm_MnoBind (the raw mnoBindId e.g. awcc-rx-01 is the canonical external ID)

Entities use UUIDv4 internally; prefixes apply only to externally-exposed identifiers in REST responses.


7. PII, encryption & retention

FieldClassProtection
firewall.audit.src_msisdn / dst_msisdnCONFIDENTIALVisible in full to tns-admin; masked to regulator-auditor. Not encrypted at column level (DB encryption at rest via Postgres TDE + disk).
firewall.quarantine_queue.original_pdu_cipherRESTRICTED (message content)AES-256-GCM; per-MNO KEK in Vault Transit. Decrypted only inside noc handler code paths.
firewall.audit.pdu_body_sha256INTERNALOne-way hash — no PII recoverable.
firewall.blocklist_entries.value (MSISDN type)CONFIDENTIALPlain in table; masked in API responses for regulator-auditor.
firewall.dnd_snapshot.msisdnCONFIDENTIALPlain in table; API access restricted to tns-admin + internal consumers.
firewall.federation_log.signatureINTERNALPlain (signature is non-secret).

Retention:

  • firewall.audit: 13 months hot (JetStream) + 7 years cold (MinIO Object-Lock Compliance).
  • firewall.blocklist_audit: permanent (append-only).
  • firewall.quarantine_queue: 180 days post-terminal; then cold archive 7 years.
  • firewall.rule_versions: permanent.
  • firewall.operating_mode_history: permanent.
  • firewall.peer_hygiene_scores: 90 days rolling; then BRIN-compressed to firewall.peer_hygiene_archive for 2 years.

8. Migration strategy

  • Partitions are provisioned 3 months ahead by PartitionMaintenanceWorker. Missing future partitions trigger a HIGH alert (FirewallPartitionMissing).
  • Retention is enforced by dropping partitions older than the retention window, never via DELETE (retain append-only guarantee).
  • Schema changes follow expand/contract; all migrations forward-only. Any column that holds PII (e.g. new MSISDN-derived column) requires Security review.
  • Bloom filter format changes are versioned: keys fw:blocklist:national:v2 run in parallel for 1 h during cutover.