regulator-portal-service — Deployment Topology
Version: 1.0 Status: Draft Owner: Regulator-facing + Legal + SRE Last Updated: 2026-04-21 References: SERVICE_OVERVIEW.md, docs/architecture/ADR-0004-national-backbone-resilience.md
Runtime and Kubernetes topology. The service runs as three Deployments (Web BFF, REST API, SIEM forwarder) plus CronJobs for scheduled reports and evidence collection.
1. Runtime
| Dimension | Choice |
|---|---|
| Language | TypeScript 5.x strict |
| Framework | NestJS + Fastify |
| Web BFF | Next.js 14 (App Router) |
| Node.js | 20 LTS |
| ORM | Prisma 5.x |
| PDF generation | Handlebars + Puppeteer (for signed PDFs) |
| HSM | PKCS#11 via @ghasi/hsm-client |
| SFTP | ssh2-sftp-client with strict host-key |
| Container | Distroless gcr.io/distroless/nodejs20 |
2. Kubernetes Resources
2.1 Web BFF Deployment
apiVersion: apps/v1
kind: Deployment
metadata: { name: regulator-portal-web, namespace: ghasi-prod }
spec:
replicas: 3
selector: { matchLabels: { app: regulator-portal, component: web } }
template:
metadata:
labels: { app: regulator-portal, component: web, tier: regulator-facing }
spec:
serviceAccountName: regulator-portal
nodeSelector: { node-pool: np-ctrl }
containers:
- name: web
image: ghcr.io/ghasi/regulator-portal-web:<digest>
ports: [{ name: http, containerPort: 3081 }, { name: metrics, containerPort: 9464 }]
envFrom:
- { configMapRef: { name: regulator-portal-config } }
- { secretRef: { name: regulator-portal-secrets } }
resources:
requests: { cpu: "250m", memory: "256Mi" }
limits: { cpu: "1000m", memory: "1Gi" }
readinessProbe: { httpGet: { path: /health/ready, port: http }, periodSeconds: 5 }
livenessProbe: { httpGet: { path: /health/live, port: http }, periodSeconds: 10 }
securityContext:
runAsNonRoot: true
readOnlyRootFilesystem: true
capabilities: { drop: [ALL] }
2.2 REST API Deployment
Handles LI workflow, complaint ingest, report generation, attestation catalog.
apiVersion: apps/v1
kind: Deployment
metadata: { name: regulator-portal-api, namespace: ghasi-prod }
spec:
replicas: 3
selector: { matchLabels: { app: regulator-portal, component: api } }
template:
metadata:
labels: { app: regulator-portal, component: api, tier: regulator-facing }
spec:
serviceAccountName: regulator-portal
nodeSelector:
node-pool: np-ctrl
hsm-accessible: "true"
containers:
- name: api
image: ghcr.io/ghasi/regulator-portal-service:<digest>
args: ["node", "dist/apps/api/main.js"]
ports: [{ name: http, containerPort: 3082 }, { name: metrics, containerPort: 9465 }]
envFrom:
- { configMapRef: { name: regulator-portal-config } }
- { secretRef: { name: regulator-portal-secrets } }
resources:
requests: { cpu: "500m", memory: "512Mi" }
limits: { cpu: "2000m", memory: "2Gi" }
volumeMounts:
- { name: tmp, mountPath: /tmp }
- { name: hsm-socket, mountPath: /var/run/hsm }
volumes:
- name: tmp
emptyDir: {}
- name: hsm-socket
hostPath: { path: /var/run/hsm, type: Socket }
2.3 SIEM Forwarder Deployment
apiVersion: apps/v1
kind: Deployment
metadata: { name: regulator-portal-siem-forwarder, namespace: ghasi-prod }
spec:
replicas: 2 # HA; distributed-lock ensures single runner per destination
selector: { matchLabels: { app: regulator-portal, component: siem-forwarder } }
template:
metadata:
labels: { app: regulator-portal, component: siem-forwarder, tier: regulator-facing }
spec:
serviceAccountName: regulator-portal
nodeSelector: { node-pool: np-ctrl }
containers:
- name: siem-forwarder
image: ghcr.io/ghasi/regulator-portal-service:<digest>
args: ["node", "dist/apps/siem-forwarder/main.js"]
ports: [{ name: metrics, containerPort: 9466 }]
envFrom:
- { configMapRef: { name: regulator-portal-config } }
- { secretRef: { name: regulator-portal-secrets } }
resources:
requests: { cpu: "250m", memory: "256Mi" }
limits: { cpu: "1000m", memory: "1Gi" }
volumeMounts:
- { name: siem-wal, mountPath: /var/lib/siem-wal } # disk-WAL for backpressure
volumes:
- name: siem-wal
persistentVolumeClaim:
claimName: regulator-portal-siem-wal
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata: { name: regulator-portal-siem-wal, namespace: ghasi-prod }
spec:
accessModes: [ReadWriteOnce]
resources: { requests: { storage: "50Gi" } }
storageClassName: fast-ssd
2.4 HPA (Web + API)
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata: { name: regulator-portal-api, namespace: ghasi-prod }
spec:
scaleTargetRef: { apiVersion: apps/v1, kind: Deployment, name: regulator-portal-api }
minReplicas: 3
maxReplicas: 10
metrics:
- type: Resource
resource: { name: cpu, target: { type: Utilization, averageUtilization: 65 } }
2.5 PodDisruptionBudgets
Each Deployment has minAvailable: 2 (or 1 for small forwarder).
2.6 Services
apiVersion: v1
kind: Service
metadata: { name: regulator-portal-web, namespace: ghasi-prod }
spec: { selector: { app: regulator-portal, component: web }, ports: [{ port: 3081, targetPort: http }], type: ClusterIP }
---
apiVersion: v1
kind: Service
metadata: { name: regulator-portal-api, namespace: ghasi-prod }
spec: { selector: { app: regulator-portal, component: api }, ports: [{ port: 3082, targetPort: http }], type: ClusterIP }
2.7 Ingress (mTLS)
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: regulator-portal-ingress
namespace: ghasi-prod-edge
annotations:
kong.ingress.kubernetes.io/plugins: mtls-regulator,request-id
spec:
ingressClassName: kong
tls:
- hosts: [regulator.ghasi.io]
secretName: regulator-portal-tls
rules:
- host: regulator.ghasi.io
http:
paths:
- path: /
pathType: Prefix
backend: { service: { name: regulator-portal-web, port: { number: 3081 } } }
- path: /v1/
pathType: Prefix
backend: { service: { name: regulator-portal-api, port: { number: 3082 } } }
Kong mtls-regulator plugin configured to require national-PKI client certs with revocation-check (CRL + OCSP staple).
2.8 NetworkPolicy
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata: { name: regulator-portal-ingress, namespace: ghasi-prod }
spec:
podSelector: { matchLabels: { app: regulator-portal } }
policyTypes: [Ingress]
ingress:
- from:
- namespaceSelector: { matchLabels: { name: ghasi-prod-edge } }
podSelector: { matchLabels: { app: kong } }
ports: [{ port: 3081, protocol: TCP }, { port: 3082, protocol: TCP }]
---
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata: { name: regulator-portal-egress, namespace: ghasi-prod }
spec:
podSelector: { matchLabels: { app: regulator-portal } }
policyTypes: [Egress]
egress:
- to: [{ podSelector: { matchLabels: { app: postgres-primary } } }]
ports: [{ port: 5432, protocol: TCP }]
- to: [{ podSelector: { matchLabels: { app: redis-cluster } } }]
ports: [{ port: 6379, protocol: TCP }]
- to: [{ podSelector: { matchLabels: { app: nats } } }]
ports: [{ port: 4222, protocol: TCP }]
- to: [{ podSelector: { matchLabels: { app: minio } } }]
ports: [{ port: 9000, protocol: TCP }]
- to: [{ podSelector: { matchLabels: { app: hsm-proxy } } }]
ports: [{ port: 9211, protocol: TCP }]
- to: # gRPC mesh to upstream read-through services
- podSelector: { matchLabels: { app: compliance-engine } }
- podSelector: { matchLabels: { app: consent-ledger-service } }
- podSelector: { matchLabels: { app: sender-id-registry-service } }
- podSelector: { matchLabels: { app: cdr-mediation-service } }
- podSelector: { matchLabels: { app: analytics-service } }
ports: [{ port: 50061, protocol: TCP }, { port: 50071, protocol: TCP }, { port: 50091, protocol: TCP }]
- to: # SIEM destinations (external CIDRs per configured destination)
- ipBlock: { cidr: 198.18.1.0/24 } # Splunk example
- ipBlock: { cidr: 198.18.2.0/24 } # QRadar example
- ipBlock: { cidr: 198.18.3.0/24 } # Logstash example
ports: [{ port: 443, protocol: TCP }, { port: 8089, protocol: TCP }] # Splunk HEC
- to: # ATRA SFTP + HTTPS
- ipBlock: { cidr: 198.18.0.0/24 }
ports: [{ port: 22, protocol: TCP }, { port: 443, protocol: TCP }]
3. CronJobs
| Name | Schedule | Purpose |
|---|---|---|
regulator-daily-report | 30 6 * * * (daily 06:30 UTC) | Generates daily CDR-status report |
regulator-monthly-summary | 0 6 1 * * (1st day, 06:00 UTC) | Monthly compliance summary |
regulator-evidence-collector | 0 3 * * * (daily 03:00 UTC) | Collects auto-evidence |
regulator-auditor-token-sweep | */30 * * * * (every 30 min) | Revokes expired auditor tokens |
regulator-annual-bundle | 0 2 1 1 * (Jan 1, 02:00 UTC) | Annual attestation bundle |
4. Region Affinity
Regulator portal is sovereign-region only (kbl). Mazar region is read-only standby; no regulator-write surface.
5. Infrastructure Dependencies
| Dependency | Purpose |
|---|---|
| PostgreSQL 16 | regulator schema |
| Redis 7 | Hot cache + distributed locks |
| NATS JetStream | SIEM consumer source + event publishing |
| HSM (PKCS#11) | PDF + bundle signing |
| Vault | SFTP creds, SIEM creds, signing key handles |
| S3 (MinIO-compat) | Reports, bundles, evidence |
| ClickHouse (optional, via analytics-service) | Long-range audit queries |
| SPIRE / SPIFFE | Workload identity |
| upstream gRPC services | compliance-engine, consent-ledger, sender-id-registry, cdr-mediation, analytics-service |
6. Secrets (Vault)
| Secret | Path |
|---|---|
| Postgres dynamic cred | secret/data/regulator-portal/db |
| NATS NKey | secret/data/regulator-portal/nats-nkey |
| HSM PIN | secret/data/regulator-portal/hsm-pin |
| ATRA SFTP key | secret/data/regulator-portal/atra-sftp |
| SIEM destination credentials | secret/data/regulator-portal/siem/{destination} |
| Auditor CA trust anchors | secret/data/regulator-portal/auditor-ca |
7. Config (ConfigMap)
apiVersion: v1
kind: ConfigMap
metadata: { name: regulator-portal-config, namespace: ghasi-prod }
data:
LOG_LEVEL: "info"
REGION: "kbl"
TZ: "UTC"
REGULATOR_DOMAIN: "regulator.ghasi.io"
AUDITOR_DOMAIN: "auditor.ghasi.io"
CRL_URL_PRIMARY: "https://atra-pki.example.af/crl"
OCSP_STAPLE_REQUIRED: "true"
CRL_CACHE_TTL_SECONDS: "14400"
LI_SLA_ACK_HOURS: "1"
LI_SLA_INPROGRESS_HOURS: "4"
LI_SLA_DELIVERED_HOURS: "24"
COMPLAINT_SLA_DAYS: "5"
REPORT_RETENTION_DAYS_HOT: "90"
REPORT_RETENTION_YEARS_COLD: "7"
SIEM_WAL_MAX_BYTES: "5368709120" # 5 GB
AUDITOR_ACCESS_DEFAULT_DAYS: "30"
8. Scaling
- Web + API scale on CPU; expected peak ~100 RPS (regulator users are few; auditors sporadic).
- SIEM forwarder scales via manual replica adjustment; throughput driven by NATS subscriber count, not Web RPS.
- Report builders are IO-bound on upstream reads; scale-up affects PG + gRPC load on upstream services.
9. Deployment Gate
- All 16 spec docs Complete.
- mTLS test with real ATRA staging cert.
- SIEM mock test with all 3 destinations.
- 7-day WAL-drain drill.
- HSM sign test in staging.
- Rollback validated.
- On-call approves.
10. Cost Envelope (monthly, sovereign region only)
| Component | Cost |
|---|---|
| Web + API pods | ~$150 |
| SIEM forwarder + WAL disk | ~$40 |
| CronJobs | ~$20 |
| Postgres (shared) | ~$30 |
| Redis (shared) | ~$15 |
| NATS (shared) | ~$15 |
| HSM (amortised) | ~$50 |
| S3 reports + bundles | ~$10 |
| Egress (ATRA + SIEM) | nominal |