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-enginespiffe://ghasi.local/ns/sms-platform/sa/routing-enginespiffe://ghasi.local/ns/sms-platform/sa/sms-firewall-servicespiffe://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=falseis 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-Rolesheaders on the authenticated upstream request.sender-id-registry-servicenever parses JWTs directly — it trusts Kong's injection. - For tenant endpoints (
/v1/sender-ids/*), the service setsSET LOCAL app.current_tenant_id = <X-Tenant-Id>per request; Row-Level Security onsender_idsandkyc_documentsenforces 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.
| Role | Capabilities |
|---|---|
platform.sid.admin | Full CRUD on sender-IDs (suspend/reactivate/revoke); restricted-pattern CRUD; trigger on-demand regulator export; view full KYC documents |
platform.sid.reviewer | Claim from queue; approve/reject/request-info on registrations; view KYC documents (audit-logged); approve notarised verifications (dual-control) |
platform.auditor | Read-only on audit log, regulator exports, reputation history; cannot view KYC content; cannot change any state |
platform.regulator | Read-only on regulator-export endpoints; mTLS-bound; cannot read raw KYC content |
platform.support | Read-only on sender-ID list (heavily redacted) for L2 support triage |
Tenant sms:sid:read scope | Read own tenant's sender-IDs, verifications, status |
Tenant sms:sid:write scope | Submit registration; submit verification artefacts |
| Public (anonymous) | /v1/sender-ids/public/* only — read-only public projection |
Enforcement points (defence in depth):
- NestJS
RoleGuard— runs first, rejects with 403INSUFFICIENT_SCOPEbefore handler entry. - Per-handler
@RequireRoles(...)decorator — declarative and contract-tested. - Postgres RLS on
sender_ids,kyc_documents,verifications— final defence for tenant isolation. - KYC-content interceptor — serializes inline KYC-doc content (watermarked PDF stream) only when caller role ∈
{platform.sid.admin, platform.sid.reviewer}; otherwise returns 403. - Mandatory
reasonenforcement on suspend/revoke/reject endpoints (see UC-10/12 invariants).
3. Data protection
3.1 PII inventory & classification
| Field | Classification | Storage | Transit |
|---|---|---|---|
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-object | TLS 1.2+ only; signed URLs ≤ 15 min; watermarked sidecar render |
sender_ids.registrant_contact_email | CONFIDENTIAL | Plain in DB (DB-level TDE) | TLS 1.2+ only |
sender_ids.registrant_contact_msisdn | CONFIDENTIAL | Same; masked in admin list view | TLS 1.2+ only |
sender_ids.registrant_org_name | PUBLIC | Plain (this is the public projection target) | TLS 1.2+ |
verifications.challenge.otp_hash | CONFIDENTIAL | SHA-256 only | TLS 1.2+ |
| OTP plaintext | RESTRICTED | Redis only, ≤ 5 min TTL | TLS 1.2+ |
audit.before/after | CONFIDENTIAL (may include PII excerpts) | Plain (table-level TDE) | TLS 1.2+ |
regulator_exports (file content) | CONFIDENTIAL (registry metadata, no KYC) | Object storage WORM bucket; signed | TLS 1.2+; SFTP |
3.2 Encryption keys
| Key | Store | Rotation |
|---|---|---|
| Per-tenant KEK for KYC envelope | Vault Transit (transit/ghasi-sid-kyc-<tenantId>) | Annual, or on tenant request / incident |
| DEK for each KYC blob | Wrapped inline, unwrapped per read via Vault Transit | Implicit (DEKs are per-object) |
| mTLS server cert + key | Vault PKI / SPIRE-issued SVIDs | 1 hour (SPIRE) |
| Regulator-export signing key | HSM (PKCS#11) per ADR-0004 §11 | Annual |
| PostgreSQL TDE master key | HSM | 365 d |
| Notary-whitelist signing key (whitelist file integrity) | Vault KV | 180 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_VIEWaudit row; bulk-view patterns triggerSidKycBulkAccessAlert. - Watermarking: Every inline view rendered through the watermark sidecar with
viewer_user_id + timestampoverlay (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); forbidskyc_doc_content,otp_plaintextentirely. 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
Verifyerrors 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,verificationskeyed ontenant_id. Verifycache 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
| Secret | Store | Injected as |
|---|---|---|
| gRPC server SVID | SPIRE | File mount, hot-reloaded |
| PostgreSQL credentials | Vault DB dynamic secret | Env var |
| Redis credentials | Vault KV | Env var |
| NATS credentials | Vault KV | Env var |
| Object storage credentials | Vault KV | Env var |
| KMS keys (per-tenant KEK) for KYC | Vault Transit (referenced, not exported) | — |
| Regulator-export signing key | HSM PKCS#11 (referenced via slot id) | — |
| ATRA SFTP private key | Vault KV | File mount |
| Local LLM API token (if any) | Vault KV | Env var |
No secret is ever written to logs, events, or config files. Pre-commit gitleaks scan blocks accidental commits.
8. Threat model
| Threat | Mitigation |
|---|---|
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 forgery | OCR + 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 registration | MFA 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 hijack | Two-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 enumeration | OTP rate-limited to 3/hour per registrant MSISDN; per-verification 3-attempt cap; verifications.challenge stores hash only |
| Public-search abuse / scraping of registry | Per-IP rate-limit (100 RPS); cache-then-tarpit; >1000 distinct queries / hour triggers alert |
| ReDoS via restricted-pattern admin | re2 engine (linear time) + ReDoS screen at save + 10 ms eval timeout |
| Audit log tampering | Postgres rules reject UPDATE/DELETE; WORM mirror to dxb; regular integrity verification cron |
| KYC bulk exfiltration by insider | Bulk-view alert (SidKycBulkAccessAlert); watermarking on every view; periodic access pattern review |
| Regulator export integrity attack | Detached signature using HSM-held key; ATRA verifies signature on receipt |
Multi-region split-brain on tenant assignment of same value | HLC-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.v1is consumed.- Tombstone affected
KycDocumentrows (settombstoned_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_emailandregistrant_contact_msisdnto[REDACTED]. - Do not delete the
SenderIdrow orauditrows — retained per regulatory evidence obligation. - Public-search projection redacts
registrantOrgNameto "Withdrawn registration".
- Tombstone affected
- 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 todxb(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.