Number Intelligence Service — Security Model
Version: 1.0 Status: Draft Owner: Messaging Core + Security Last Updated: 2026-04-21 Companion: API_CONTRACTS · DATA_MODEL · EVENT_SCHEMAS · 13 Security, Compliance, Tenancy · ADR-0004 §14
1. Authentication
1.1 gRPC plane — NumberIntelligenceService
- mTLS + SPIFFE required. The gRPC server accepts only client certificates signed by the platform CA and presenting a SPIFFE SVID in the allowlist:
spiffe://ghasi.platform/ns/routing/sa/routing-enginespiffe://ghasi.platform/ns/firewall/sa/sms-firewall-servicespiffe://ghasi.platform/ns/compliance/sa/compliance-enginespiffe://ghasi.platform/ns/router/sa/channel-router-servicespiffe://ghasi.platform/ns/fraud/sa/fraud-intel-servicespiffe://ghasi.platform/ns/orchestrator/sa/sms-orchestratorspiffe://ghasi.platform/ns/gateway/sa/tenant-sdk-gateway(tenant gRPC SDK — metered)
- Client certs are issued from Vault PKI via the Vault Agent Sidecar Injector, rotated every 30 days; the server hot-reloads on file change.
- Internal calls from
ni-hlr-gatewayDaemonSet pods to the main gRPC API use a dedicated SPIFFE IDspiffe://ghasi.platform/ns/numint/sa/hlr-gateway; the gateway itself authenticates outbound per-MNO endpoints with either SS7 M3UA (no app-layer auth; network-layer SSCTP security) or REST bearer JWT issued via client-credentials. - Local-dev TLS bypass via
GRPC_TLS_ENABLED=falseis prohibited in any non-local environment. Startup guard refuses to boot with TLS disabled whenNODE_ENV != 'development'.
1.2 REST plane — admin, tenant, regulator
- Kong validates the platform JWT (issued by
auth-service, RS256, JWKS-backed). Requests without a valid token are rejected at the edge. - Kong forwards
X-Tenant-Id,X-Account-Id,X-User-Id,X-Rolesheaders.number-intelligence-servicenever parses platform JWTs directly. - For tenant Public Lookup API endpoints (
/v1/lookup/*), the service readsX-Tenant-Idand enforcesTenantLookupQuotabefore processing. Lookup audit rows (numint.lookup_audit) use per-tenant-salt MSISDN hashing. - Per-tenant gRPC SDK uses the same mTLS server posture with a
tenant-sdk-gatewaySPIFFE ID; the gateway re-issues per-tenant identity as signedX-Tenant-Idto NI (gateway-signed HMAC over the tenant claim).
1.3 IdP-agnostic
Per ADR-0002, the identity provider (Keycloak / tenant OIDC / tenant SAML / Firebase legacy) is irrelevant to NI. The idp claim is captured in numint.lookup_audit.actor_sub prefix for forensics only.
2. Authorization (RBAC)
| Role | Capabilities |
|---|---|
platform.numint.admin | Full admin: MNP conflict resolution, MNP on-demand ingest, EIR resync, adapter config, manual NumberRecord overrides (dual-control — requires X-Approver-User-Id), tenant quota overrides |
platform.regulator | Read-only on MNP history, MNP chain-verification, MNP audit, EIR; cannot mutate |
platform.support | Read-only attribution lookup for troubleshooting (MSISDN masked); view cache / HLR probe metrics |
Tenant numint:lookup | Call GET /v1/lookup/{msisdn} and POST /v1/lookup/batch for own tenant; subject to quota |
Tenant numint:lookup_bulk | All numint:lookup + POST /v1/lookup/bulk-csv |
Tenant numint:audit_read | Read own tenant's lookup_audit rows for the past 90 days |
| Internal services (via SPIFFE SAN) | gRPC calls; not metered; not subject to per-tenant quota |
Enforcement points (in order):
- Kong — JWT validation,
consumer.acl_groups, declarative scope check; rejects at edge. - SPIFFE SAN guard (gRPC only) — server rejects peers not in the allowlist.
- NestJS RoleGuard — runs first inside the service; rejects with
403 INSUFFICIENT_SCOPEbefore handler entry. @RequireRoles(...)decorator — declarative; contract-tested.- Tenant quota middleware — RPS + monthly quota enforcement; 429 before handler work.
- Postgres RLS on
lookup_audit— defence in depth for tenant self-service reads.
3. Data protection
3.1 PII inventory & classification
| Field | Classification | Storage | Transit |
|---|---|---|---|
numint.number_records.e164 | INTERNAL-PLATFORM | MSISDN-to-MNO mapping is public telecom fact (an E.164 prefix table analogue); plaintext retained for SQL joinability with MNO prefix lookups; protected by authenticated API access + encryption-at-rest (per platform 13-sct §3); not classified as subscriber PII per ATRA guidance on carrier attribution | TLS 1.2+ |
numint.number_records.msisdn_hash | INTERNAL | `sha256(e164 | |
numint.portability_history.msisdn_hash | INTERNAL | Same as above | TLS 1.2+ |
numint.lookup_audit.msisdn_hash | INTERNAL | `sha256(e164 | |
numint.eir_records.imei | RESTRICTED | Optional plaintext retained for admin forensics; hash is the primary key | TLS 1.2+ |
numint.eir_records.imei_hash | INTERNAL | `sha256(imei | |
numint.hlr_probes.result_snapshot | INTERNAL | JSONB with MSISDN-hashes only; VLR plaintext; IMSI-prefix plaintext (MCC+MNC public telecom fact) | TLS 1.2+ |
| HLR PCAP samples | RESTRICTED | Encrypted with KMS key numint-pcap-kek; retained 90 days in MinIO | TLS 1.2+ to MinIO |
| Per-MNO REST adapter JWT | RESTRICTED | Vault KV; short-lived (≤ 1 h) | TLS 1.2+ |
| SFTP keys (MNO MNP, ATRA CEIR) | RESTRICTED | Vault KV | SSH |
3.2 Encryption keys
| Key | Store | Rotation |
|---|---|---|
| Platform MSISDN pepper | Vault KV (secret/ghasi/numint/msisdn_pepper) | Quarterly; envelope re-hash via background job; pepper_version tracks each row |
| Platform IMEI pepper | Vault KV (secret/ghasi/numint/imei_pepper) | Quarterly |
| Per-tenant audit salt | Vault KV (secret/ghasi/numint/tenant-salts/{tenantId}) | Annual; or on tenant request / incident |
| PCAP KEK | Vault Transit (transit/ghasi-numint-pcap-kek) | Annual |
| Audit chain signing key | Vault Transit (transit/ghasi-numint-audit-signing) | Annual; key version embedded per row |
| mTLS server + client certs | Vault PKI (pki/ghasi-numint) | 30 days |
| Postgres credentials | Vault DB dynamic secret | 24 h TTL |
| Redis credentials | Vault KV | Quarterly |
| NATS credentials | Vault KV | Quarterly |
| Per-MNO REST adapter client JWT | Vault KV | Short-lived (≤ 1 h) |
| MNO MNP SFTP keys | Vault KV | Annual or on regulator/MNO request |
3.3 Redaction rules
- In events. MSISDN appears only as
msisdnHashand (where policy permits)msisdnMasked(+CC NNN ***). Raw MSISDN never appears on NATS. - In logs. Pino redactor masks
msisdn,imei,vlr,imsi_prefixplaintext in application logs; an ESLint rule (no-pii-in-log) blockslogger.*({msisdn: …})patterns at PR time. - In REST responses. The Public Lookup API does return raw MSISDN — the tenant submitted it, so echoing it is safe. Admin responses mask MSISDN unless the caller has
platform.numint.adminrole. - In audit payloads.
lookup_audit.msisdn_hash(tenant-salted) only; raw MSISDN never recorded in audit. - In the LLM pipeline (per AI_INTEGRATION §2.3). Raw MSISDN replaced with indexed token before any LLM call.
3.4 Canonical serialisation for audit hash
- RFC 8785 JSON Canonicalization Scheme (sorted keys, UTF-8 NFC, no insignificant whitespace).
- Field ordering deterministic.
signing_key_idembedded per row; verifier handles multi-version chains through rotations.
3.5 Data residency
- All
numintschema rows, NATS subjects, Redis cache, and Vault namespaces reside in Afghan regions only (Kabul + Mazar active-active; Dubai cold-DR holds only AES-GCM-wrapped PG backups whose unwrap key never leaves Kabul HSM). Per ADR-0004 §14. - SS7 SIGTRAN and MNO REST endpoints are per-MNO in-country by construction (Afghan MNOs).
- MinIO buckets for MNP raw archive, HLR PCAP, and audit cold archive all in
af-kabul-1with sync replication toaf-mzr-1. - A deploy-time residency test (
tests/residency/numint_residency.spec.ts) asserts every configured external endpoint resolves to Afghan IP space (MNO endpoints) or platform-internal (Vault, Redis, Postgres, NATS). Any offshore IP → deploy blocked.
4. Audit
numint.lookup_auditcaptures every Public Lookup API call with hash-chained integrity; internal gRPC calls are not in the audit store (they appear in structured logs + Prometheus only).numint.audit_logcaptures administrative state changes (MNP conflict resolution, adapter config change, manual NumberRecord override, quota override).- Daily hash-chain verifier runs at 04:30 Asia/Kabul (see APPLICATION_LOGIC UC-AuditChainVerify);
NumIntAuditChainBrokenCRITICAL on any discovered break. - Regulator SIEM receives a mirror of
numint.audit.v1+numint.mnp.changed.v1+numint.reconciliation.completed.v1viaregulator-portal-service. consent-ledger-serviceuses a similar hash-chain design — implementations share theChainVerifierlibrary for consistency.
5. Fail-degraded posture (not fail-closed)
Unlike consent-ledger-service / compliance-engine, number-intelligence-service is fail-degraded:
- On PG outage: serve from Redis; return
confidence = LOW. - On Redis outage: serve from PG; latency degrades;
confidenceunchanged. - On both PG + Redis out: serve from prefix-table;
source = PREFIX_FALLBACK,confidence = UNKNOWN. - On live HLR gateway down: return last-known persisted row;
confidence = LOW. - On MNP reconciliation feed stale: serve last-known MNP state;
NumIntMnpReconciliationStalealert fires.
Security-wise, fail-degraded means attribution can momentarily be wrong — but it is never catastrophically unavailable. Downstream callers have their own fallbacks (prefix table on routing-engine; geo-default on compliance-engine). Availability attacks cannot mis-route at scale because routing-engine's prefix-table fallback is deterministic and defensible.
The single hard exception: LookupEir on an IMEI that would be BLACKLIST but cannot be served (EIR store completely unreachable) returns UNKNOWN — the caller (sms-firewall-service) must then apply its own default policy, which for BLACKLIST-critical traffic is configurable per tenant as FAIL_CLOSED_ON_UNKNOWN.
6. Tenant isolation
- Per-tenant salt on
lookup_audit.msisdn_hashensures two tenants looking up the same MSISDN produce different audit hashes. - Per-tenant Redis quota keys (
numint:quota:lookup:{tenantId}:{yyyymm}) isolate quota accounting. - Public Lookup responses include no cross-tenant data — only the attribution fact for the MSISDN the tenant submitted.
- Cross-tenant leakage tests: integration suite asserts tenant A cannot read tenant B's
lookup_auditrows even when both look up the same MSISDN; RLS policy verified.
7. Secrets
| Secret | Store | Injected as |
|---|---|---|
| gRPC server cert + key | Vault PKI → K8s Secret (Vault Agent) | File mount |
| gRPC client cert (per caller) | Vault PKI | File mount in caller pods |
| PostgreSQL credentials | Vault DB dynamic secret | Env var (24 h TTL) |
| Redis credentials | Vault KV | Env var |
| NATS credentials | Vault KV | Env var |
| MSISDN pepper | Vault KV | Env var (refreshed via Vault Agent on rotation) |
| IMEI pepper | Vault KV | Env var |
| Per-tenant salts | Vault KV (lazy fetch; cached ≤ 5 min) | Runtime |
| MNO MNP SFTP keys | Vault KV | File mount |
| Per-MNO REST adapter client JWT credentials | Vault KV | Runtime (client-credentials flow) |
| PCAP KEK | Vault Transit (referenced, not exported) | — |
| Audit chain signing key | Vault Transit (referenced, not exported) | — |
No secret is ever written to logs, events, or config files. Pre-commit gitleaks scan blocks accidental commits.
8. Threat model
| Threat | Mitigation |
|---|---|
| Attacker scrapes Public Lookup API to enumerate MSISDNs | Per-tenant quota (RPS + monthly); Kong adaptive rate-limit + JA3 fingerprint blocking; tenant-salted audit hash prevents cross-tenant correlation; anti-enumeration: uniform response time on unknown vs known MSISDN; Kong bot-detection plugin |
| Malicious tenant forces live HLR on every request (cost amplification) | Separate freshLookupRpsLimit (default 2 req/s, lower than plan RPS); per-MNO TPS governor caps absolute SS7 traffic regardless of tenant mix |
| MNP file tampering (attacker modifies SFTP mid-transit) | SFTP SSH host-key pinning; file SHA-256 recorded before ingest; per-MNO mutual-auth where MNO supports it; MNP run emits fileSha256 for tamper correlation across runs |
| MNP file replay (attacker re-ships yesterday's file with date header advanced) | ReconciliationRun idempotency keyed on file_sha256; identical hash rejected with "duplicate file" error; MNO file date header cross-checked against SFTP server-side mtime |
Attacker tampers with portability_history or lookup_audit | Append-only DB rules; hash-chain; daily verifier; CRITICAL alert |
Compromised ni-hlr-gateway pod injects fake live attribution | Gateway responses signed with gateway's SPIFFE SVID; NI verifies signature against the allow-listed SPIFFE ID; pcap sampling captures the raw SS7 bytes for spot-check audit |
Side-channel: reverse-lookup MSISDN from msisdn_hash | Platform pepper rotated quarterly; msisdnHash exposed only inside platform — never to tenants, citizens, or events unredacted; tenant-salted variant for audit rows |
| Regulator order to disclose all lookups for a specific MSISDN in the last 90 days | Supported by design: regulator uses platform.regulator role + the relevant tenant salt (held in Vault, auditable access) to re-derive the per-tenant hash; query completes ≤ 5 min |
| LLM prompt injection via conflict summaries (use case A) | Redactor removes MSISDN/tenant IDs; LLM output constrained to JSON with grammar; output used for triage ordering only — no LLM value ever writes to numint.* tables |
Denial-of-service on ni-hlr-gateway via forced-fresh spam | Per-MNO Redis token bucket; per-tenant fresh-lookup sub-bucket; Kong global cap; HPA on grpc_inflight_requests |
Operator malicious insider changes MnoSnapshot.hlr_endpoint to attacker-controlled host | Dual-control on adapter config updates; X-Approver-User-Id required; audit; deploy-time residency test catches offshore endpoints |
| National-scale SIM-swap laundering via coordinated MNP abuse | MNP-churn anomaly detector (see AI_INTEGRATION §3) forwards numint.mnp.churn_anomaly.v1 signals to fraud-intel-service |
9. Regulatory posture
- ATRA Numbering Plan conformance. NI implements ITU-T E.164 with Afghan country code
+93; MSISDN length is validated against the ATRA-published national numbering plan (10 digits after CC); prefix-to-MNO table mirrored from ATRA assignments. - GSMA AA.13 MNP. MNP reconciliation patterns follow GSMA AA.13 Section 4 conventions for donor/recipient file exchange cadence and schema.
- Regulator audit path. All MNP transitions, reconciliation runs, and conflict resolutions are hash-chained; regulator can demand verification at any time via
GET /v1/regulator/numint/mnp/verify. - Tenant billing evidence.
numint.lookup_auditis retained 13 months hot + 7 years cold; a billing dispute is resolvable within that window. - Lawful basis. MSISDN-to-MNO attribution is carrier-public fact; ATRA guidance places this outside the subscriber-PII envelope. Lookup API tenants attest lawful basis at account signup; the attestation is stored in
auth-service.
10. Security testing
- Contract tests (
tests/contract/numint-*.spec.ts) verify every caller's defence-in-depth pattern on NI fail-degraded paths. - Property-based tests (
tests/prop/msisdn.spec.ts) — E.164 conformance, Unicode confusables, NFKC canonicalisation, pathological inputs. - Property-based tests on Luhn for IMEI (3GPP TS 23.003 §6.2.1).
- Hash-chain tamper test: mutate a payload byte; verifier MUST detect (
tests/integ/chain-tamper.spec.ts). - Role-matrix test — every endpoint × every role — verifies 200/401/403/429 behaviour.
- SS7 MAP fuzz tests against
ni-hlr-gatewayusing synthetic MAP PDUs with invalid opcodes / oversized fields. - Residency test at deploy time.
- ZAP baseline + API scan on each main build.
- Quarterly penetration test scoped to Public Lookup API, admin REST, and the
ni-hlr-gatewayinternal gRPC. - Secret scanning (
gitleaks), dependency scan (osv-scanner), container scan (trivy).