LLD — Security¶
All data is synthetic, but the system is built as if it were not. The controls below are what a regulated Indian fintech would expect from a service exposing customer data to AI agents.
1. Trust boundaries¶
flowchart LR
subgraph internet["Internet (untrusted)"]
client["MCP client / agent"]
llm["Client-side LLM"]
end
subgraph edge["Edge"]
cf["CloudFront"]
apigw["API Gateway"]
end
subgraph app["Application (VPC)"]
gw["mcp-gateway"]
api["core-api"]
pg[("PostgreSQL")]
end
subgraph aws["AWS managed"]
cognito["Cognito"]
sm["Secrets Manager"]
kms["KMS"]
end
ext["OpenAI, screening vendor"]
client --> cf --> apigw --> gw
llm -.->|"tool arguments are untrusted input"| client
gw --> api --> pg
gw --> cognito
api --> sm
api --> kms
api --> ext
Notice: tool arguments originate from a model that may be manipulated, so they are treated as untrusted input even when the token is valid.
2. STRIDE threat model¶
| Threat | Example | Mitigation |
|---|---|---|
| Spoofing | Forged or replayed token | RS256 JWT verification against Cognito JWKS; iss, aud, exp, token_use checked; short access-token lifetime (60 min) |
| Spoofing | Token issued for another resource used here | Audience must equal the gateway resource identifier (RFC 8707 resource indicator) |
| Tampering | Altered pagination cursor to read another account | HMAC-signed cursors bound to subject and query hash |
| Tampering | Audit rows edited after the fact | UPDATE/DELETE revoked; hash chain over rows (see §5) |
| Tampering | Chaos header injected by a client | HMAC signature, expiry, operator role, disabled in production by default |
| Repudiation | "I never raised that dispute" | Audit row per call with actor_sub, client_id, args hash, outcome, trace ID |
| Information disclosure | Analyst sees full PAN | Scope kyc:pii required; masking at gateway; PAN only decryptable by core-api role |
| Information disclosure | Support agent retrieves restricted SOP | Postgres RLS ceiling per role |
| Information disclosure | Stack traces or SQL in errors | Error taxonomy returns code + message + trace ID only |
| Denial of service | Request flood | CloudFront, API Gateway throttling, per-client token bucket, load shedding |
| Denial of service | Expensive queries (huge date ranges) | Input limits (366-day window, page ≤ 100), statement timeout, response byte cap |
| Elevation of privilege | Prompt-injected agent calls write tools | Write tools need separate scopes, idempotency keys and confirmation; KB tools are read-only |
| Elevation of privilege | SQL injection via filters | Parameterised SQL only; ruff + bandit rules ban string-built SQL |
3. Identity and access¶
| Principal | Mechanism | Grants |
|---|---|---|
| Human via MCP client | Cognito authorization code + PKCE via FastMCP OAuth proxy | Scopes from Cognito group (role) |
| Service-to-service (agents in CI, e2e tests) | Cognito client credentials | Scopes fixed on the app client |
mcp-gateway → core-api |
Private network + shared service token from Secrets Manager (Proposed (to validate in P6): IAM-signed requests via API Gateway private integration) | Full internal API |
| Lambdas → AWS services | Execution roles, least privilege, generated from CDK grants | Per-function |
| GitHub Actions → AWS | OIDC federation, role trust restricted to repo:<owner>/ledgerlens-mcp:environment:<env> |
Deploy only |
The gateway forwards the verified principal to core-api in signed internal headers; core-api sets kosha.actor and kosha.kb_ceiling per transaction.
4. Secrets and encryption¶
| Secret / data | Storage | Rotation |
|---|---|---|
| OpenAI API key | Secrets Manager | Manual, documented runbook |
| Database credentials | Secrets Manager (RDS-managed) | Automatic rotation |
| Cursor HMAC key, chaos HMAC key, service token | Secrets Manager | Dual-key verification window during rotation |
| PAN | KMS envelope encryption | Data key per record, key policy limits decrypt to core-api role |
| Data at rest | RDS storage encryption, S3 SSE, SQS SSE, DynamoDB encryption | AWS-managed keys |
| Data in transit | TLS everywhere; sslmode=verify-full to RDS |
PAN envelope encryption:
sequenceDiagram
autonumber
participant A as core-api
participant K as KMS
participant P as PostgreSQL
A->>K: GenerateDataKey (KeyId = pan-key, context = {customer_id})
K-->>A: plaintext data key + encrypted data key
A->>A: AES-256-GCM encrypt PAN with plaintext key, discard key
A->>P: store pan_token = (encrypted data key, nonce, ciphertext), pan_last4
Note over A,P: Read path with kyc:pii scope
A->>P: read pan_token
A->>K: Decrypt (encrypted data key, same encryption context)
K-->>A: plaintext data key
A->>A: decrypt PAN, return, discard key
Notice: the encryption context binds each ciphertext to its customer, so a token copied onto another customer's row cannot be decrypted.
Plaintext secrets never appear in environment variables at build time; Lambdas fetch them at cold start through the Parameters and Secrets extension cache.
5. Audit log¶
| Property | Implementation |
|---|---|
| Coverage | Every tool call, including denied and failed ones |
| Content | actor_sub, client_id, roles, tool_name, args_hash (SHA-256 of canonical args, never raw args), resource_ids, pii_unmasked, outcome, error_code, latency_ms, trace_id |
| Immutability | Application role has INSERT only; a row_hash = sha256(prev_row_hash || row) chain makes tampering detectable (Proposed (to validate in P3)) |
| Availability | Audit write failures never fail the tool call; they emit a metric with an alarm |
6. OWASP Top 10 for LLM applications¶
| Risk | Relevance to an MCP server | Control |
|---|---|---|
| LLM01 Prompt injection | Retrieved SOP text or customer-entered remarks steer the agent | Content returned as data with source fields, injection heuristics flag suspects, no write capability from KB tools |
| LLM02 Sensitive information disclosure | Agent asks for full PII | Scopes, masking, RLS, minimal fields per view model |
| LLM03 Supply chain | Compromised dependency | uv.lock pinned hashes, pip-audit, Dependabot, Trivy image scan, minimal base image |
| LLM04 Data and model poisoning | Malicious document ingested | Corpus only from the repository via CI; checksums; classification required in header |
| LLM05 Improper output handling | Tool output rendered as HTML or executed downstream | Structured JSON output; no markup; size caps |
| LLM06 Excessive agency | Agent performs writes the user did not intend | Separate write scopes, idempotency, elicitation confirmation, audit |
| LLM07 System prompt leakage | Prompts expose internal data | MCP prompts contain no secrets or data; templates only |
| LLM08 Vector and embedding weaknesses | Cross-tenant retrieval | RLS ceiling enforced in the database, not in application filters |
| LLM09 Misinformation | Superseded SOP cited as current | Only effective documents retrieved by default; every chunk carries version and effective date |
| LLM10 Unbounded consumption | Agent loops on expensive tools | Per-client rate limits, deadlines, page and byte caps |
7. Supply-chain and CI controls¶
| Control | Tool |
|---|---|
| Secret scanning | gitleaks (pre-commit and CI) |
| Static analysis | ruff (including S security rules), bandit |
| Dependency audit | pip-audit against uv.lock |
| Image scanning | Trivy, fail on HIGH/CRITICAL with a fix available |
| IaC policy | cdk-nag (AWS Solutions pack) |
| Provenance | Images built in CI only, pushed by digest, deployed by digest |