Caricash Nova Platform
Home
Home
  1. Clients
  • Default module
    • Clients
      • PBAC for Customer Authentication
      • Create Clients
        POST
    • Internal
      • Accounts, Transactioins and Ledger Implementatioin
      • Ensure real-time balance guarantees
      • Web App Scaffold
      • Database Migrations Guide
      • Microservices
      • Service Implementation
      • TO-DO
      • Authentication & Authorization
    • Customers
      • Onboarding
  • Release Schedule
    • Agency Operations
      • Agent APIs Specs
        • auth
          • POST /v1/agent/auth/login
          • POST /v1/agent/auth/logout
          • POST /v1/agent/auth/refresh
          • POST /v1/agent/auth/device-bind
          • POST /v1/agent/auth/otp/request
          • POST /v1/agent/auth/otp/verify
        • agent
          • GET /v1/agent/me
          • PATCH /v1/agent/me
          • GET /v1/agent/outlet
          • GET /v1/agent/capabilities
        • kyc
          • POST /v1/kyc/customers
          • POST /v1/kyc/customers/{customer_id}/upgrade
          • POST /v1/kyc/customers/{customer_id}/rekcy
          • GET /v1/kyc/customers/{customer_id}/status
        • transactions
          • POST /v1/txns/cashin
          • POST /v1/txns/cashout
          • POST /v1/txns/p2p/assist
          • GET /v1/txns/{txn_id}
          • POST /v1/txns/{txn_id}/reverse
        • wallets
          • GET /v1/wallets/{wallet_id}/balance
          • GET /v1/wallets/{wallet_id}/transactions
        • float
          • GET /v1/float
          • POST /v1/float/topup
          • POST /v1/float/redeem
          • GET /v1/float/instructions/{instruction_id}
        • commissions
          • GET /v1/agents/{agent_id}/commissions
          • POST /v1/agents/{agent_id}/commissions/payouts/preview
          • POST /v1/agents/{agent_id}/commissions/payouts/accept
        • disputes
          • POST /v1/disputes
          • GET /v1/disputes/{case_id}
          • POST /v1/disputes/{case_id}/attachments
        • reports
          • GET /v1/reports/eod
          • POST /v1/reports/eod/close
          • GET /v1/reports/txns
          • GET /v1/reports/float
        • content
          • GET /v1/announcements
        • training
          • GET /v1/training/courses
          • POST /v1/training/quizzes/{quiz_id}/submit
        • ussd
          • POST /v1/ussd/session
          • POST /v1/ussd/agent/menu
        • ops
          • GET /v1/health
      • Agent Scope
        • Agent Scope
    • Customer Operations
      • Customer Scope
        • Customer & Merchant Scope
    • Schemas
      • Agent Ops APIs
  • Nova Core Banking Service API
    • core
      • Create account
      • Get account
      • Get balances
      • Create posting (double-entry)
      • Reverse posting
      • Check limits
      • Generate statement
    • Schemas
      • Schemas
        • Amount
        • Account
        • BalanceSet
        • PostingEntry
        • Posting
        • Hold
        • LimitCheckResponse
        • SavingsProduct
        • OverdraftLine
        • StatementRequest
        • Error
Home
Home
  1. Clients

PBAC for Customer Authentication

PBAC for Customer Authentication & Transactions — Complete Implementation Blueprint

World-class, regulator-ready design for phone+PIN login with Purpose-Based Access Control (PBAC) — multi-tenant, auditable, and low-latency.


0) Executive summary

This document specifies an end-to-end implementation for customer authentication (phone + PIN) and authorization with PBAC, centered on the canonical purpose customer.transact (“the customer is moving money”). It integrates with ReBAC/OPA, database Row-Level Security (RLS), risk-adaptive step-up (AAL2), device binding, and tamper-evident auditing.

Outcomes

  • Security: Argon2id (+ per-tenant pepper) PIN hashing, non-enumeration controls, AAL2 step-up for money movement, DPoP/mTLS token binding options.
  • Policy clarity: Purpose registry drives who can do what, why, and with what data exposure.
  • Compliance: Clean evidence for PCI/PSD2/GDPR/NDPA; WORM audits; purpose-driven data minimization.
  • Performance: p99 decision < 5 ms (OPA partial eval), p99 PIN verify budgeted.

Assumptions: TypeScript/Node, pnpm, PostgreSQL (+ RLS), Redis, Envoy ext_authz, OPA (policy bundles), multi-tenant architecture.


1) Scope & goals

  • Authenticate customers via verified phone (E.164) + PIN.
  • Authorize actions via PBAC + ReBAC with OPA, and enforce tenant isolation at DB with RLS.
  • Harden high-risk actions (customer.transact → e.g., transfer.create) with AAL2 step-up and intent signing.
  • Observe & prove: full audit trail, dashboards, SLOs, and compliance mapping.
  • Forward-compatible with OPAQUE/PAKE and WebAuthn without breaking the contract.

2) Taxonomy — Purposes (minimal, durable)

Keep a short, hierarchical list (expand only when necessary):

  • customer.transact — create/confirm transfers, cash-out, pay bills
  • customer.account.view — balances, history (read-only)
  • customer.beneficiary.manage — add/remove payees
  • support.investigate — support agent troubleshooting (masked data)
  • fraud.review — fraud ops (elevated visibility, AAL2)
  • compliance.kyc — KYC & AML checks (strictly gated)
  • analytics.aggregate — aggregates only (no raw PII)
  • marketing.personalize — opt-in only, policy & consent gated

The star of this doc is customer.transact.


3) Architecture overview (request path)

sequenceDiagram
  autonumber
  participant Client (Mobile/Web)
  participant Edge (Envoy)
  participant AuthZ (OPA)
  participant API (Customer Service)
  participant Redis (Sessions)
  participant PG (PostgreSQL)

  Client->>Edge: POST /v1/transfers  (access JWT, no AAL2 yet)
  Edge->>API: forwards + inject Route-Purpose = "customer.transact"
  API->>API: Risk eval (IP, device, amount, SIM age) -> risk=med/high?
  API->>AuthZ: OPA /decision (subject, resource, action, purpose, aal)
  AuthZ-->>API: deny if purpose/action mismatch or AAL<2
  API-->>Client: 403 MFA_REQUIRED + bound challenge (orig_req_hash)

  Client->>API: POST /auth/stepup/complete (OTP or device-sign)
  API->>Redis: upgrade session (aal=2, short ttl)
  API->>AuthZ: OPA /decision (aal=2)
  AuthZ-->>API: allow
  API->>PG: RLS-enforced write (tenant-scoped)
  API-->>Client: 200 OK (purpose-shaped response)

Key: Client never controls the purpose; route/action defines it. OPA decides allow/deny using PBAC + ReBAC + AAL, DB enforces RLS as a backstop.


4) Identity & tokens

Access JWT (short-lived, 5–10 min)
Claims: sub, tid, jti, iat/exp, aud, aal, amr[], optional cnf (for DPoP/mTLS binding).
Refresh token: opaque, rotating, stored server-side (replay detection).
AAL: 1 = PIN, 2 = step-up (OTP or device biometric), 3 = hardware key (optional future).

Optional: DPoP (browser/mobile) and mTLS-bound tokens (service-to-service).


5) PBAC — Purpose Registry (authoritative source)

purposes.json (bundled with OPA data; versioned & signed)

{
  "purposes": [
    {
      "name": "customer.transact",
      "min_aal": 2,
      "resources": ["transaction", "beneficiary", "limit"],
      "actions": ["transfer.create", "transfer.confirm", "cashout.create"],
      "field_policies": { "transaction": "full", "beneficiary": "masked" },
      "consent_required": false,
      "retention_days": 365
    },
    {
      "name": "customer.account.view",
      "min_aal": 1,
      "resources": ["account", "transaction"],
      "actions": ["account.read", "transaction.read"],
      "field_policies": { "transaction": "masked", "account": "masked" },
      "consent_required": false,
      "retention_days": 365
    }
  ],
  "version": "2025-09-22T09:00:00Z",
  "signature": "cosign-attestation-or-kms-jws"
}

6) Route → Purpose mapping (server-side only)

Do not accept client-supplied purposes. Define a deterministic map in your gateway or API router.

export const ROUTE_PURPOSE: Record<string, string> = {
  "POST /v1/transfers": "customer.transact",
  "POST /v1/transfers/confirm": "customer.transact",
  "GET /v1/transactions": "customer.account.view",
  "POST /v1/beneficiaries": "customer.beneficiary.manage"
};

export function attachPurpose(req, _res, next) {
  const key = `${req.method} ${req.routePath}`;
  req.purpose = ROUTE_PURPOSE[key] ?? "operational";
  // Block spoofing: overwrite any client header
  req.headers["x-purpose"] = req.purpose;
  // Optionally set a DB session var for masking views:
  req.db.run(`select set_config('app.current_purpose',$1,true)`, [req.purpose]);
  next();
}

7) Authorization with OPA (Rego + PBAC + ReBAC + AAL)

Input contract (TypeScript → OPA)

export type AuthzInput = {
  tenant: { id: string, security?: Record<string, unknown> };
  subject: { id: string, type: "customer"|"user"|"service", roles?: string[], aal: number };
  resource: { type: string, id?: string, tenant_id: string, attrs?: Record<string, unknown> };
  action: string;             // e.g., "transfer.create"
  purpose: string;            // e.g., "customer.transact"
  context: { ip: string, ua: string, risk: "low"|"medium"|"high", time: string };
};

Rego policy (excerpt)

package payments.authz
default allow := false
default step_up_required := false

same_tenant { input.resource.tenant_id == input.tenant.id }

purpose_allowed {
  some p
  data.purposes[p].name == input.purpose
  data.purposes[p].resources[_] == input.resource.type
  data.purposes[p].actions[_] == input.action
}

aal_sufficient {
  some p
  data.purposes[p].name == input.purpose
  input.subject.aal >= data.purposes[p].min_aal
}

# ReBAC relation (e.g., customer owns the resource context)
related(subject, object, rel) {
  some i
  data.relationships[i].subject_ns == subject.type
  data.relationships[i].subject_id == subject.id
  data.relationships[i].relation   == rel
  data.relationships[i].object_ns  == object.type
  data.relationships[i].object_id  == object.id
  not expired(data.relationships[i])
}

expired(t) { t.caveat.expires_at != "" ; time.now_ns() > time.parse_ns_rfc3339(t.caveat.expires_at) }

# High-risk → step-up
step_up_required { input.purpose == "customer.transact" } else { input.context.risk == "high" }

# Allow if tenant OK, purpose OK, relation OK, and AAL is sufficient
allow { same_tenant; purpose_allowed; related(input.subject, {"type": "tenant", "id": input.tenant.id}, "member"); aal_sufficient }

# Or allow with step-up satisfied
allow { same_tenant; purpose_allowed; related(input.subject, {"type":"tenant","id":input.tenant.id}, "member"); step_up_required; input.subject.aal >= 2 }

Deploy as signed bundles (cosign/KMS). Use partial evaluation and in-memory data for p99 < 5 ms.


8) Database isolation & purpose-driven masking

8.1 Tenant isolation (RLS)

ALTER TABLE transactions ENABLE ROW LEVEL SECURITY;
CREATE POLICY tenant_isolation_txn ON transactions
  USING (tenant_id = current_setting('app.current_tenant', true));

Set app.current_tenant per request (connection/session variable) in the API.

8.2 Purpose-aware projection (server & SQL)

Server projection

const FIELD_POLICIES = {
  full: (t: any) => t,
  masked: (t: any) => ({ ...t, name: maskName(t.name), phone: maskPhone(t.phone), pan_suffix: t.pan_suffix, pan_full: undefined })
};

export function shapeForPurpose(entity: any, entityType: "transaction"|"beneficiary", purpose: string, registry: Registry) {
  const rule = registry.fieldPolicy(purpose, entityType); // "full"|"masked"|...
  return FIELD_POLICIES[rule](entity);
}

DB view with masking

CREATE FUNCTION mask_phone(p text) RETURNS text AS $$
  SELECT CASE WHEN p IS NULL THEN NULL ELSE substr(p,1,4)||'******'||substr(p, length(p)-1, 2) END;
$$ LANGUAGE sql IMMUTABLE;

CREATE VIEW v_transactions AS
SELECT
  t.id, t.tenant_id, t.amount, t.currency,
  CASE WHEN current_setting('app.current_purpose', true) IN ('support.investigate','customer.account.view')
       THEN mask_phone(t.counterparty_phone) ELSE t.counterparty_phone END AS counterparty_phone,
  CASE WHEN current_setting('app.current_purpose', true) = 'customer.transact'
       THEN t.pan_suffix ELSE NULL END AS pan_suffix,
  NULL::text AS pan_full,
  t.created_at
FROM transactions t
WHERE t.tenant_id = current_setting('app.current_tenant', true);

The API should consume v_transactions for reads to avoid accidental over-exposure.


9) Customer authentication — phone + PIN (hardened)

9.1 PIN storage

  • Argon2id with per-record salt (16–32B) and per-tenant pepper derived from a KMS-protected master.
  • Opportunistic rehash on login when policy ratchets (e.g., more memory).
function tenantPepper(tid: string, master: Buffer): Buffer {
  return createHmac("sha256", master).update(`pepper:${tid}`).digest();
}

Avoid enumeration: uniform errors; rate-limit keys are HMAC(tenant, phone) so existence isn’t leaked.

9.2 Rate-limiting & lockouts

  • Per (tenant, phone), per IP, per device buckets. Progressive backoff, short lockouts (e.g., 15 min after 5 fails).
  • If 10 fails/day → require OTP re-verification before next PIN attempt.

9.3 Login → session

  • On success: create Redis session (sess:{jti}), issue access JWT (aal:1) + rotating refresh.
  • On failure: increment counters, write audit, apply backoff/locks.

9.4 Step-up for customer.transact

  • If AAL<2 or risk high → 403 with bound challenge (hash of method|path|normalizedBody).
  • /auth/stepup/complete verifies OTP or device-key signature (preferred), upgrades to aal:2 with short expiry (e.g., 10 min).
const origHash = b64url(sha256(`${method}|${path}|${normalizeBody(body)}`));
const challenge = signJWT({ kind:"stepup", sub, tid, orig: origHash, exp: now+300 });

For transactions, include intent signing: the device signs the transfer payload (amount, currency, beneficiaryId, origHash).


10) Risk engine (inputs & outcomes)

Signals: IP reputation, device posture/attestation, SIM age/port-in, geo, velocity, amount vs limits, new beneficiary, time-of-day.

Outputs: risk = low|medium|high, plus explainable components for audit.
Policy: customer.transact always requires AAL2; risk can also trigger step-up for other purposes.


11) OpenAPI surface (authoritative subset)

openapi: 3.1.0
info:
  title: Customer Auth & PBAC
  version: 1.0.0
components:
  securitySchemes:
    AccessJWT:
      type: http
      scheme: bearer
      bearerFormat: JWT
paths:
  /customers/auth/otp/send:
    post:
      summary: Send phone verification OTP
      requestBody: { required: true, content: { application/json: { schema:
        type: object, required: [tenantId, phone], properties:
          tenantId: { type: string }
          phone:    { type: string, pattern: "^\\+\\d{7,15}$" } }}}}
      responses: { "202": { description: Accepted } }
  /customers/auth/otp/verify:
    post:
      summary: Verify OTP
      requestBody: { required: true, content: { application/json: { schema:
        type: object, required: [tenantId, phone, otp], properties:
          tenantId: { type: string }
          phone:    { type: string }
          otp:      { type: string, minLength: 4, maxLength: 8 } }}}}
      responses: { "200": { description: OK, content: { application/json:
        schema: { type: object, properties: { verificationToken: { type: string } } } } } }
  /customers/auth/pin/set:
    post:
      summary: Set PIN after verified phone
      requestBody: { required: true, content: { application/json: { schema:
        type: object, required: [tenantId, phone, pin, verificationToken], properties:
          tenantId: { type: string }
          phone:    { type: string }
          pin:      { type: string, pattern: "^\\d{4,6}$" }
          verificationToken: { type: string } }}}}
      responses: { "204": { description: No Content } }
  /customers/auth/login:
    post:
      summary: Login with phone + PIN
      requestBody: { required: true, content: { application/json: { schema:
        type: object, required: [tenantId, phone, pin], properties:
          tenantId: { type: string }
          phone:    { type: string }
          pin:      { type: string } }}}}
      responses:
        "200":
          description: Tokens
          content: { application/json: { schema: { type: object, properties:
            accessToken: { type: string }, refreshToken: { type: string },
            expiresIn: { type: integer }, sessionId: { type: string }, aal: { type: integer } } } }
        "401": { description: Invalid credentials }
  /customers/auth/stepup/complete:
    post:
      summary: Complete step-up for high-risk actions
      security: [ { AccessJWT: [] } ]
      requestBody: { required: true, content: { application/json: { schema:
        type: object, required: [challengeToken], properties:
          challengeToken: { type: string },
          otp: { type: string, nullable: true },
          deviceAssertion: { type: object, nullable: true } }}}}
      responses:
        "200": { description: Upgraded access, content: { application/json:
          schema: { type: object, properties: { accessToken: { type: string }, expiresIn: { type: integer }, aal: { type: integer, enum: [2] } } } } }
  /v1/transfers:
    post:
      summary: Create transfer (purpose = customer.transact)
      security: [ { AccessJWT: [] } ]
      responses:
        "200": { description: Created }
        "403": { description: MFA_REQUIRED (bound challenge) }

12) Key server modules (TypeScript)

12.1 Purpose middleware + AuthZ input

import { ROUTE_PURPOSE } from "./purpose-map";
import { z } from "zod";

export const AuthzInputSchema = z.object({
  tenant:  z.object({ id: z.string() }),
  subject: z.object({ id: z.string(), type: z.enum(["customer","user","service"]), aal: z.number().int().min(1) }),
  resource: z.object({ type: z.string(), id: z.string().optional(), tenant_id: z.string(), attrs: z.record(z.any()).optional() }),
  action: z.string(),
  purpose: z.string(),
  context: z.object({ ip: z.string(), ua: z.string().optional(), risk: z.enum(["low","medium","high"]), time: z.string() })
});

export function attachPurpose(req, _res, next) {
  const key = `${req.method} ${req.routePath}`;
  const purpose = ROUTE_PURPOSE[key] ?? "operational";
  req.purpose = purpose;
  req.headers["x-purpose"] = purpose;
  req.db.query("select set_config('app.current_purpose',$1,true)", [purpose]).catch(()=>{});
  next();
}

export function toAuthzInput(req): z.infer<typeof AuthzInputSchema> {
  return {
    tenant:  { id: req.headers["x-tenant-id"] },
    subject: { id: req.user.sub, type: "customer", aal: req.user.aal ?? 1 },
    resource:{ type: req.resource?.type ?? "transaction", id: req.resource?.id, tenant_id: req.headers["x-tenant-id"], attrs: req.resource?.attrs },
    action:  req.action,                // e.g., "transfer.create"
    purpose: req.purpose,               // set by attachPurpose
    context: { ip: req.ip, ua: req.headers["user-agent"], risk: req.risk ?? "low", time: new Date().toISOString() }
  };
}

12.2 OPA client (Envoy ext_authz or direct HTTP)

export async function authorize(input: any): Promise<{allow:boolean, reason?:string}> {
  const payload = { input };
  const res = await fetch(process.env.OPA_URL + "/v1/data/payments/authz/allow", {
    method: "POST",
    headers: { "content-type": "application/json" },
    body: JSON.stringify(payload),
    signal: AbortSignal.timeout(100)
  });
  if (!res.ok) throw new Error("authz_unavailable");
  const { result } = await res.json();
  return { allow: !!result };
}

12.3 Step-up bound challenge

function origReqHash(req) {
  return base64url(sha256(`${req.method}|${req.path}|${normalizeBody(req.body)}`));
}
export function issueChallenge(req, sub: string, tid: string) {
  return signJWT({ kind: "stepup", sub, tid, orig: origReqHash(req) }, 300); // 5 minutes
}
export function verifyChallenge(req, token: string) {
  const c = verifyJWT(token);
  if (c.kind !== "stepup" || c.orig !== origReqHash(req)) throw forbidden("challenge_mismatch");
  return c;
}

12.4 PIN hashing (Argon2id + per-tenant pepper)

const POLICY = { memoryMiB: 128, time: 3, parallel: 1, version: 3 };

export async function hashPin(pin: string, pepper: Buffer) {
  enforcePinPolicy(pin);
  const salt = randomBytes(16);
  const cfg  = { memoryCost: POLICY.memoryMiB*1024, timeCost: POLICY.time, parallelism: POLICY.parallel, raw: true, type: argon2.argon2id, salt };
  const hash = await argon2.hash(Buffer.concat([Buffer.from(pin), pepper]), cfg);
  return { hash, salt, version: POLICY.version };
}

export async function verifyPin(pin: string, pepper: Buffer, salt: Buffer, expected: Buffer, storedVersion: number) {
  const cfg = { memoryCost: POLICY.memoryMiB*1024, timeCost: POLICY.time, parallelism: POLICY.parallel, raw: true, type: argon2.argon2id, salt };
  const computed = await argon2.hash(Buffer.concat([Buffer.from(pin), pepper]), cfg);
  const ok = timingSafeEqual(computed, expected);
  if (ok && storedVersion < POLICY.version) queueRehashJob(/* ... */);
  return ok;
}

13) Data model (DDL excerpts)

-- Rel tuples (Zanzibar-style)
CREATE TABLE rel_tuples (
  subject_ns text, subject_id text, relation text,
  object_ns  text, object_id  text,
  caveat     jsonb,
  PRIMARY KEY (subject_ns, subject_id, relation, object_ns, object_id)
);
CREATE INDEX ON rel_tuples (object_ns, object_id, relation);
CREATE INDEX ON rel_tuples USING gin (caveat);

-- Customers
CREATE TABLE customers (
  id uuid PRIMARY KEY,
  tenant_id text NOT NULL,
  phone_e164 text NOT NULL,
  phone_verified_at timestamptz,
  status text NOT NULL DEFAULT 'active',
  created_at timestamptz NOT NULL DEFAULT now(),
  UNIQUE (tenant_id, phone_e164)
);

CREATE TABLE customer_credentials (
  customer_id uuid PRIMARY KEY REFERENCES customers(id) ON DELETE CASCADE,
  pin_hash bytea NOT NULL,
  pin_salt bytea NOT NULL,
  kdf_version smallint NOT NULL,
  pin_set_at timestamptz NOT NULL DEFAULT now()
);

CREATE TABLE audit_log (
  id bigserial PRIMARY KEY,
  ts timestamptz NOT NULL DEFAULT now(),
  tenant_id text NOT NULL,
  actor jsonb NOT NULL,    -- {type:"customer", id:"..."}
  action text NOT NULL,    -- "auth.login", "transfer.create"
  target jsonb NOT NULL,   -- {type:"transfer", id:"..."}
  decision jsonb NOT NULL, -- {allow:true, policy_version:"...", purpose:"customer.transact", aal:2, trace_id:"..."}
  attrs jsonb,             -- ip, ua, risk, masking_rule, registry_version
  prev_hash bytea,
  row_hash bytea
);

14) Auditing & evidence

  • Every decision stores: purpose, min AAL, effective AAL, policy version, registry version, masking rule, reason (allow/deny), trace id.
  • Maintain hash chain across rows; nightly WORM export to S3 with KMS manifest; scheduled verification.
  • Coverage SLO: 100% of mutations & auth decisions audited; alert if <100%.

15) Observability & SLOs

Dashboards

  • By purpose: request counts, allow/deny, decision latency p50/p95/p99.
  • Step-up: prompts/success for customer.transact, median completion time.
  • Login funnel: attempts → PIN ok → step-up → success.
  • OTP: delivery success by region/provider; circuit-breaker state.
  • Abuse: top IPs/ASNs blocked, SIM-swap triggers, refresh replay rate.

SLO targets

  • OPA decision p99 < 5 ms
  • PIN verify p99 < 300 ms (tune Argon2id to cap CPU under attack via throttles)
  • Step-up total p95 < 1.5 s (OTP path), < 800 ms (device-sign path)

Alerts: deny storms, AAL violations, masking leaks (unexpected full PII in support purpose), OTP delivery SLA breach, refresh replay spike.


16) Testing & assurance

  • Unit/integration: PIN policy, hashing, throttles, lockouts, OTP/device step-up, refresh rotation & replay kill-switch, OPA allow/deny by purpose & AAL.

  • Property-based: random attempt sequences across IP/device/phone; assert lockouts and throttle invariants.

  • Mutation testing: flip Rego rules and throttling comparisons → test suite must fail.

  • Chaos/game days: kill OTP provider, slow Redis, fail OPA; verify fail-closed and graceful UX.

  • Formal invariant (excerpt):

    • It is impossible to authorize transfer.create without purpose=customer.transact and aal ≥ 2.
    • It is impossible to return unmasked PII for support.investigate.

17) Compliance mapping (excerpt)

DomainControlImplementation
PCI DSS 7.xAccess controlOPA PBAC + ReBAC; RLS guardrail; AAL2 for transactions
PCI DSS 10.xLoggingHash-chained audit to WORM; nightly verification
PSD2/SCAStrong customer authAAL2 step-up for customer.transact; OTP or device biometric
GDPR/NDPAPurpose limitation & minimizationPBAC registry; response masking; retention by purpose
SOC 2Change managementSigned policy/data bundles; blue/green rollouts; evidence exports

18) Deployment & CI/CD

  • Policy & registry supply chain:

    • Lint & test Rego; validate purposes.json against schema; generate coverage.
    • Build compiled OPA bundles (partial eval); sign (cosign/KMS).
    • Store in OCI or S3; OPA verifies signature before load.
  • Blue/green policy clusters + canary (1–5%) with shadow-eval drift.

  • Secrets: peppers/keys in KMS; rotated & zeroized at runtime.

  • Envoy ext_authz mandatory (no bypass routes).


19) Migration plan (if retrofitting)

  1. Introduce route→purpose mapping and pass to OPA in shadow.
  2. Add PBAC checks (min AAL) for customer.transact; initially warn only.
  3. Enable 403 + bound challenge flow; release mobile SDK for device step-up.
  4. Flip to enforce PBAC for customer.transact; monitor dashboards.
  5. Gradually bring other purposes under PBAC; enable DB view masking.
  6. Optional: adopt OPAQUE/PAKE for PIN; keep throttles.

20) Runbooks (abridged)

  • AAL violations detected: block route; verify registry & policy versions; hotfix via registry bump; notify tenants.
  • Deny storm: compare canary vs stable policy decisions; rollback green; check OPAL tuple lag.
  • OTP outage: device-step-up preferred; allow queued retries; announce incident banner; do not lower AAL for customer.transact.
  • Refresh replay spike: revoke session tree; rotate keys; force re-login; investigate IP/ASN and automate blocks.

21) Appendices

A) Envoy ext_authz filter

http_filters:
- name: envoy.filters.http.ext_authz
  typed_config:
    "@type": type.googleapis.com/envoy.extensions.filters.http.ext_authz.v3.ExtAuthz
    grpc_service:
      envoy_grpc: { cluster_name: authz }
    with_request_body:
      max_request_bytes: 8192
      allow_partial_message: true
    include_peer_certificate: true

B) Relationship tuples (examples)

user:customer_123  member   tenant:acme
tenant:acme        parent   merchant:m_456
user:customer_123  payer    account:a_789   (caveat: {"expires_at":"2025-10-01T00:00:00Z"})

C) Threat model (top items & mitigations)

  • PIN brute force → Argon2id + throttles + lockouts + non-enumeration.
  • Token replay → short-lived access, DPoP/mTLS binding, refresh rotation with replay kill-switch.
  • Policy drift → signed bundles, blue/green, shadow-eval drift checks.
  • Data over-exposure → purpose-driven masking at API and DB view.
  • SIM-swap fraud → SIM-age signal gates step-up; new beneficiary/amount triggers intent signing.

TL;DR

  1. Set purpose in the server (never the client).
  2. OPA decides with PBAC + ReBAC; DB RLS backs it up.
  3. customer.transact requires AAL2 and intent signing; responses are purpose-shaped.
  4. Everything is signed, logged, measured, and reversible.
Modified at 2025-09-22 10:45:20
Previous
Clients
Next
Create Clients
Built with