Skip to main content

sender-id-registry-service — Security Model

Version: 1.0 Status: Draft Owner: Security + Trust & Safety + Regulator-facing Last Updated: 2026-04-21 Companion: API_CONTRACTS · DATA_MODEL · EVENT_SCHEMAS · docs/13-security-compliance-tenancy.md · ADR-0004 §11


1. Authentication

1.1 gRPC plane — Verify, GetReputation, BatchVerify

  • mTLS required. The gRPC server accepts only connections presenting a SPIFFE-issued workload SVID (per ADR-0004 §12 — service mesh + SPIRE) signed by the platform CA.
  • Authorised callers (SAN allowlist):
    • spiffe://ghasi.local/ns/sms-platform/sa/compliance-engine
    • spiffe://ghasi.local/ns/sms-platform/sa/routing-engine
    • spiffe://ghasi.local/ns/sms-platform/sa/sms-firewall-service
    • spiffe://ghasi.local/ns/sms-platform/sa/channel-router-service
  • Other certs are rejected with UNAUTHENTICATED.
  • SVIDs rotate every 1 hour (per ADR-0004 §11 — service mesh row).
  • Local dev bypass via GRPC_TLS_ENABLED=false is prohibited in non-local environments (start-up guard).

1.2 REST plane — admin + tenant + customer-portal

  • 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-Roles headers on the authenticated upstream request. sender-id-registry-service never parses JWTs directly — it trusts Kong's injection.
  • For tenant endpoints (/v1/sender-ids/*), the service sets SET LOCAL app.current_tenant_id = <X-Tenant-Id> per request; Row-Level Security on sender_ids and kyc_documents enforces tenant isolation at the DB level (belt and braces).

1.3 Public plane — /v1/sender-ids/public/*

  • Anonymous (no auth). Edge-cached with per-IP rate limiting (Kong) and service-layer tarpit on abuse (UC-13).

1.4 Regulator plane — /v1/admin/sender-ids/export*

  • mTLS with regulator-issued client cert pinned by SAN allowlist. Clients:
    • spiffe://atra.gov.af/regulator-portal-service (when ATRA's portal connects directly)
    • Self-issued cert for regulator-portal-service (Ghasi-side proxy) until ATRA-issued cert is provisioned.
  • Bearer JWT also required as belt-and-braces.

1.5 IdP-agnostic

Per ADR-0002, the IdP that authenticated the caller is irrelevant to this service. The idp claim is captured in audit.before/after for forensic purposes only.


2. Authorization (RBAC)

Role definitions live in auth-service; this service enforces scope/role checks at the handler boundary.

RoleCapabilities
platform.sid.adminFull CRUD on sender-IDs (suspend/reactivate/revoke); restricted-pattern CRUD; trigger on-demand regulator export; view full KYC documents
platform.sid.reviewerClaim from queue; approve/reject/request-info on registrations; view KYC documents (audit-logged); approve notarised verifications (dual-control)
platform.auditorRead-only on audit log, regulator exports, reputation history; cannot view KYC content; cannot change any state
platform.regulatorRead-only on regulator-export endpoints; mTLS-bound; cannot read raw KYC content
platform.supportRead-only on sender-ID list (heavily redacted) for L2 support triage
Tenant sms:sid:read scopeRead own tenant's sender-IDs, verifications, status
Tenant sms:sid:write scopeSubmit registration; submit verification artefacts
Public (anonymous)/v1/sender-ids/public/* only — read-only public projection

Enforcement points (defence in depth):

  1. NestJS RoleGuard — runs first, rejects with 403 INSUFFICIENT_SCOPE before handler entry.
  2. Per-handler @RequireRoles(...) decorator — declarative and contract-tested.
  3. Postgres RLS on sender_ids, kyc_documents, verifications — final defence for tenant isolation.
  4. KYC-content interceptor — serializes inline KYC-doc content (watermarked PDF stream) only when caller role ∈ {platform.sid.admin, platform.sid.reviewer}; otherwise returns 403.
  5. Mandatory reason enforcement on suspend/revoke/reject endpoints (see UC-10/12 invariants).

3. Data protection

3.1 PII inventory & classification

FieldClassificationStorageTransit
kyc_documents content (S3 blob)RESTRICTED (national ID, regulator letter, financial licence)Encrypted at rest with per-tenant KEK (Vault Transit envelope; AES-256-GCM); DEK wrapped per-objectTLS 1.2+ only; signed URLs ≤ 15 min; watermarked sidecar render
sender_ids.registrant_contact_emailCONFIDENTIALPlain in DB (DB-level TDE)TLS 1.2+ only
sender_ids.registrant_contact_msisdnCONFIDENTIALSame; masked in admin list viewTLS 1.2+ only
sender_ids.registrant_org_namePUBLICPlain (this is the public projection target)TLS 1.2+
verifications.challenge.otp_hashCONFIDENTIALSHA-256 onlyTLS 1.2+
OTP plaintextRESTRICTEDRedis only, ≤ 5 min TTLTLS 1.2+
audit.before/afterCONFIDENTIAL (may include PII excerpts)Plain (table-level TDE)TLS 1.2+
regulator_exports (file content)CONFIDENTIAL (registry metadata, no KYC)Object storage WORM bucket; signedTLS 1.2+; SFTP

3.2 Encryption keys

KeyStoreRotation
Per-tenant KEK for KYC envelopeVault Transit (transit/ghasi-sid-kyc-<tenantId>)Annual, or on tenant request / incident
DEK for each KYC blobWrapped inline, unwrapped per read via Vault TransitImplicit (DEKs are per-object)
mTLS server cert + keyVault PKI / SPIRE-issued SVIDs1 hour (SPIRE)
Regulator-export signing keyHSM (PKCS#11) per ADR-0004 §11Annual
PostgreSQL TDE master keyHSM365 d
Notary-whitelist signing key (whitelist file integrity)Vault KV180 d

3.3 Document handling — NIST cryptographic guidance alignment

KYC document storage and access aligns with NIST SP 800-122 (PII protection) and NIST SP 800-57 (key management):

  • At rest: AES-256-GCM with per-tenant KEK, DEK per object.
  • In transit: TLS 1.2+ (TLS 1.3 preferred where supported).
  • Access logging: Every KYC view writes an entityType = KYC_DOC_VIEW audit row; bulk-view patterns trigger SidKycBulkAccessAlert.
  • Watermarking: Every inline view rendered through the watermark sidecar with viewer_user_id + timestamp overlay (per US-SID-003). Watermark is centred + diagonal repeat to defeat trivial cropping.
  • Secondary copy lifetime: Watermarked artefacts in s3://ghasi-sid-kyc-views-{region}/ purged after 1 hour by lifecycle policy.

3.4 Redaction rules

  • In events. Registrant contact email/MSISDN are never in events. Only tenantId, value, registrantOrgName, state, level fields (per EVENT_SCHEMAS).
  • In logs. Pino redactor masks registrantContactMsisdn (+9370****), registrantContactEmail (r***@example.af); forbids kyc_doc_content, otp_plaintext entirely. ESLint rule forbids logging these fields at PR time.
  • In REST responses. KYC blob content is delivered only via the watermarked-view endpoint; never inlined in JSON responses.

3.5 KYC document never leaves trust boundary

  • AI/ML on KYC content runs on-cluster only (per AI_INTEGRATION). External LLM calls on KYC content are forbidden by NetworkPolicy.

4. Audit

All state changes recorded in sender_id_registry.audit with actor, before/after snapshots, IP, user agent, trace ID. The table is append-only at the database level (Postgres rules reject UPDATE and DELETE — see DATA_MODEL §3.1). Retention ≥ 13 months hot, ≥ 7 years cold.

KYC document view actions (UC-03) are audited with entityType = KYC_DOC_VIEW so unauthorised reading patterns are detectable. A daily report ranks reviewer KYC-view counts; outliers flagged.

Events mirroring audit-relevant state changes are emitted on sender.id.* subjects per EVENT_SCHEMAS. The analytics-service subscribes for long-term archival; regulator-portal-service subscribes for regulator-side reconciliation; dxb leaf node mirrors audit subset to immutable WORM (per ADR-0004 §13).


5. Fail-closed posture

The service and its callers are designed so that availability attacks cannot bypass policy — at worst they delay registration / verification or cause traffic to be HOLDed by compliance-engine.

  • gRPC Verify errors translate to caller fail-closed (compliance HOLD; routing reject; firewall block).
  • Submission endpoint returns 503 if Postgres / Vault / Object storage is unavailable — tenant MUST retry; we do not accept KYC submissions we cannot persist or encrypt.
  • OTP verification submit returns 503 if Redis is unavailable (no plaintext to compare against).
  • Reputation cron failure → no auto-suspension that cycle; existing snapshots persist; alert.
  • Regulator export failure → file persisted locally; transmission retried; never silently lost.

6. Tenant isolation

  • Postgres RLS on sender_ids, kyc_documents, verifications keyed on tenant_id.
  • Verify cache in Redis keyed by {tenantId} so one tenant's invalidation never affects another.
  • Per-tenant KEK for KYC content ensures a key compromise is scoped to one tenant.
  • Restricted-pattern enforcement is platform-wide by design (a tenant cannot register BANK* even if no other tenant has done so); this is intentional national-authority behaviour.
  • platform.sid.* roles are platform-wide; tenants self-serve via portal endpoints only with RLS enforcement.

7. Secrets

SecretStoreInjected as
gRPC server SVIDSPIREFile mount, hot-reloaded
PostgreSQL credentialsVault DB dynamic secretEnv var
Redis credentialsVault KVEnv var
NATS credentialsVault KVEnv var
Object storage credentialsVault KVEnv var
KMS keys (per-tenant KEK) for KYCVault Transit (referenced, not exported)
Regulator-export signing keyHSM PKCS#11 (referenced via slot id)
ATRA SFTP private keyVault KVFile mount
Local LLM API token (if any)Vault KVEnv var

No secret is ever written to logs, events, or config files. Pre-commit gitleaks scan blocks accidental commits.


8. Threat model

ThreatMitigation
Impersonation registration (e.g. tenant submits BNAK to mimic BANK)Restricted-pattern regex + AI fuzzy-match (per AI_INTEGRATION §5); reviewer human-in-loop; mandatory NOTARISED + regulator letter for any bank/government/MNO match
KYC document forgeryOCR + LLM forgery indicators surface to reviewer (AI_INTEGRATION §4); dual-control on NOTARISED; notary whitelist; tamper detection nightly compares stored SHA-256 to S3 ETag
Compromised reviewer account approves fraudulent registrationMFA enforced for reviewer accounts; all decisions audit-logged with actor + IP + trace ID; SOC channel receives sender.id.kyc_approved.v1 for fraud-pattern detection; second-reviewer dual-control on NOTARISED
DNS-TXT verification spoofing via DNS hijackTwo-resolver consensus (DoT to 1.1.1.1 AND 8.8.8.8); challenge nonce includes senderIdInternalId so a leaked challenge cannot be replayed elsewhere
OTP harvesting via verification endpoint enumerationOTP rate-limited to 3/hour per registrant MSISDN; per-verification 3-attempt cap; verifications.challenge stores hash only
Public-search abuse / scraping of registryPer-IP rate-limit (100 RPS); cache-then-tarpit; >1000 distinct queries / hour triggers alert
ReDoS via restricted-pattern adminre2 engine (linear time) + ReDoS screen at save + 10 ms eval timeout
Audit log tamperingPostgres rules reject UPDATE/DELETE; WORM mirror to dxb; regular integrity verification cron
KYC bulk exfiltration by insiderBulk-view alert (SidKycBulkAccessAlert); watermarking on every view; periodic access pattern review
Regulator export integrity attackDetached signature using HSM-held key; ATRA verifies signature on receipt
Multi-region split-brain on tenant assignment of same valueHLC-based LWW + UUID tiebreaker; conflict triggers SidSplitBrainConflict for manual reconciliation (per SYNC_CONTRACT §4)
Reputation gaming (sender pumps benign traffic to inflate score)Reputation formula weights compliance hits and complaints heavily; score formula is auditable but not directly gamable; threshold actions (auto-suspend < 30) require sustained signal not single-event

9. GDPR & regulatory

  • Right to erasure (GDPR Art. 17): auth.user.erased.v1 is consumed.
    • Tombstone affected KycDocument rows (set tombstoned_at).
    • Overwrite the S3 KYC blob with a sealed redaction marker (object retains its key for audit referential integrity but content is zeroed).
    • Redact registrant_contact_email and registrant_contact_msisdn to [REDACTED].
    • Do not delete the SenderId row or audit rows — retained per regulatory evidence obligation.
    • Public-search projection redacts registrantOrgName to "Withdrawn registration".
  • Data portability: Tenants can export their own sender-ID metadata (not KYC content) via REST.
  • Audit evidence window: ≥ 13 months hot, ≥ 7 years cold (regulatory).
  • Sub-processor list: No external sub-processors. Local-only AI/ML; ATRA is the only external recipient and only of the registry export (no PII).
  • Cross-border transfer: Sender-ID data replicated to mzr (within Afghanistan); audit subset cold-archived to dxb (sovereign-allowed per ADR-0004 §5). KYC blobs are never replicated outside Afghan regions.

10. Security testing

  • Contract tests per API_CONTRACTS §6.
  • Restricted-pattern enforcement tests — every default seed pattern + 50 known evasion attempts.
  • Property-based tests on state-machine transitions (no transition out of REVOKED; no skipping verification level).
  • ZAP baseline + API scan run on each main-branch build.
  • Quarterly penetration test scoped to REST surface and gRPC surface.
  • Role-matrix integration test — every endpoint × every role — verifies 200/403 behaviour.
  • KYC document tamper-detection drill — quarterly: deliberately corrupt a test KYC blob's stored hash and verify the nightly cron alerts.
  • Watermark integrity test — verify every viewer in a sampled set sees their own user-id rendered.
  • Secret scanning in CI (gitleaks); dependency scanning (osv-scanner); container scanning (trivy).
  • Dual-control violation tests — attempt to have one reviewer self-approve a NOTARISED verification; verify rejection.