Secure RDS Read-Only MCP Server
by Mpurushotham
README.md
# secure-serverless-security-platform
[](https://github.com/Mpurushotham/secure-serverless-security-platform/actions/workflows/security-pipeline.yml)
**Securing AI agent access to regulated production data on AWS.**
An AI coding agent that can query production is a new class of principal: it holds
broad credentials, acts on instructions from text it reads, and is exactly as
trustworthy as the text it last processed. Most guidance treats this as a prompt
problem. It is an authorisation problem.
This repository takes one narrow, high-stakes case — an agent querying a pharmacy
database holding GDPR Article 9 health data — and builds the controls end to end,
with evidence rather than assertions.
---
## What is real vs. what is designed
Stated up front, because a repository that blurs this line is worse than one that
builds less.
| Component | Status | How to verify |
|---|---|---|
| MCP protocol core (JSON-RPC 2.0 over stdio, hand-written) | **Runs, 37 tests** | `make test` |
| PostgreSQL least-privilege baseline (roles, RLS, masked views) | **Runs against Postgres 17** | `make db-up && make evidence` |
| SQL guardrail (AST parse-then-execute) | **Runs, 37 attack payloads refused** | `make test` |
| Read-only RDS/Aurora MCP server | **Runs end to end** | `make mcp-demo` |
| PII leak assertions over the live transcript | **Runs, 27 assertions** | `make test` |
| Terraform: Aurora, Bedrock, agent IAM, GitHub OIDC, 8 detections | **checkov 199/0** | `make validate` |
| CI/CD security gates (SAST, deps, secrets, IaC, SBOM) | **Runs** | `.github/workflows/security-pipeline.yml` |
| Threat model (STRIDE + attack tree) | **Written** | `docs/01-threat-model.md` |
| AI secure-coding policy + training framework | **Written** | `docs/04-ai-secure-coding-policy.md` |
| JD coverage matrix + day-one operating plan | **Written** | `readiness/` |
| AWS posture MCP server (GuardDuty/Security Hub/IAM/S3/KMS/Config) | **Runs, 12 moto tests** | `make test` |
| CDK reference app + enforcing Aspects | **Synths clean, 12 tests** | `make validate` |
| Incident response playbooks (3) | **Written** | `docs/05-incident-response/` |
| Compliance map (GDPR / ISO 27001 / NIS2) | **Written** | `docs/06-compliance-map.md` |
| Vulnerability SLA + severity gate | **Runs** | `scripts/vuln_sla.py`, `scripts/severity_gate.py` |
| Usage guide, architecture, strategy, role analysis | **Written** | `docs/` |
| Serverless reference architecture (VPC, API, EventBridge, data lake, 6 scenarios) | **Written** | `docs/08-…` |
| AWS security services catalogue + sequencing | **Written** | `docs/09-…` |
| DevSecOps control catalogue (every gate: tool, purpose, **what it misses**) | **Written** | `docs/10-…` |
| GitHub org / MDM / endpoint / IDE hardening | **Written** (§2, §4 are design) | `docs/11-…` |
| GitHub branch rulesets + CODEOWNERS as code | **Applied artifacts** | `.github/rulesets/` |
| Pre-commit hooks + IDE protections | **Config in repo** | `.pre-commit-config.yaml`, `.vscode/` |
| Role readiness (JD matrix, day-one plan, drills, metrics, outcomes) | **Written** | `readiness/` |
| Shared CDK Aspects package — each one proven to *fire*, not just to pass | **Runs, 8 tests** | `cd platform/lib/cdk-security && npx jest` |
| AWS discovery: 21 read-only collectors, 41 rules, 25-point baseline | **Runs, 106 tests** | `make assess-offline` |
| **Assessment of a real AWS Organization** — 17 regions, 812 calls, 29 findings | **Executed under a purpose-built read-only role, 0 denied** | `platform/00-discovery/report/assessment.md` |
| Baseline IaC answering the assessment: SCPs, RCPs, delegated admin, org trail, detection | **Validated statically, planned live** | `platform/BASELINE.md` |
| Posture report: 9 metrics computed, 6 declared unmeasurable, delta between snapshots | **Runs, 13 tests** | `make posture` |
| Golden-path serverless API: Cognito → API GW → Lambda → DynamoDB, CMK, WAF | **Synths clean, 52 tests** | `cd platform/11-serverless && npm test` |
| Security-e2e suite: IDOR, auth bypass, injection, rate limit, header hygiene | **18 tests, needs a deployed stack** | `npm run test:security` |
| Observability: posture exporter, 12 alert rules, Grafana dashboard | **Runs** | `make rules-test`, `make obs-up` |
| Slack redaction layer — alerts carry a pointer, never a payload | **Runs, 25 tests** | `pytest platform/20-notifications` |
| AWS security platform (`platform/`) — golden path, observability, Slack | **In progress** | `platform/README.md` has the per-domain status |
| Upstream AWS samples: what was studied, taken, and refused | **Written** | `platform/docs/references.md` |
**Nothing here has been deployed to a live AWS account, and nothing here writes
to one.** IaC is validated **statically** — a deliberate choice, not a
limitation: it means anyone can clone this and verify every claim without
credentials or spend.
One row is different and is marked accordingly. The discovery tooling was
**executed read-only** against the author's own AWS Organization, and
`platform/00-discovery/report/assessment.md` is the output of that run rather
than an illustration. Three things keep that from weakening the claim above:
every call is a `Describe`/`List`/`Get` refused at three layers if it is not
(see `platform/00-discovery/README.md`); the committed snapshot and report are
pseudonymised, with raw output gitignored; and `make assess-offline` regenerates
the report from the committed snapshot with no AWS account at all, which is what
CI runs.
---
## Quick start
```bash
make setup # uv venv + dependencies
make db-up # Postgres 17 + schema + roles + masked views
make test # 134 tests
make mcp-demo # live stdio MCP session
make evidence # regenerate every artifact in evidence/
make db-down
```
Requires Docker and [uv](https://github.com/astral-sh/uv). No AWS account.
---
## The design in one picture
Three planes. The agent is modelled as a **semi-trusted principal**, never as part
of the application.
```mermaid
flowchart TB
A[AI agent] -->|stdio JSON-RPC| B[MCP server: protocol + tool allowlist]
B --> C[SQL AST guardrail: parse, then decide]
C --> D[(Aurora/PostgreSQL<br/>mcp_readonly · RLS · masked views)]
B --> E[Audit log: JSONL, arguments fingerprinted]
E --> F[EventBridge → Security Hub → responder]
D --> F
```
### Defence in depth is the invariant
Three independent layers, each assuming the one above it will eventually fail:
1. **`mcp_core`** — protocol shape, lifecycle ordering, tool allowlist.
2. **`guardrails.py`** — statement shape, relation allowlist, row and byte caps.
3. **The `mcp_readonly` database role** — grants, column-level privileges, RLS.
Layer 3 is the one that matters. Layers 1 and 2 are application code and can have
bugs; layer 3 is enforced by PostgreSQL and holds even if the server process is
fully compromised. `evidence/db-privilege-proof.txt` demonstrates this with the
application entirely out of the picture: 19 write, filesystem, and
privilege-escalation attempts, each refused by the engine.
---
## Evidence
Every artifact in `evidence/` is regenerated by `make evidence` — reproducible
output, not screenshots.
| Artifact | What it proves |
|---|---|
| `db-privilege-proof.txt` | PostgreSQL itself denies writes, raw PII reads, `COPY TO PROGRAM`, `pg_read_file`, and `SET ROLE` to the agent identity |
| `guardrail-bypass-report.md` | 37 documented escape techniques, each refused, each mapped to the control that caught it |
| `mcp-demo-transcript.jsonl` | A real stdio session returning masked data and refusing four attacks |
| `test-results.txt` | Full suite output |
| `iac-scan.txt` | terraform validate + fmt + tflint + checkov across all four modules |
| `checkov-suppressions.md` | Every policy suppression with its justification, split into false positives vs deliberate risk acceptances |
| `cdk-synth.txt` | CDK type-check, 11 security invariant tests, and a synth that must survive its own Aspects plus cdk-nag |
### Two findings this repository caught on itself
Both are documented rather than quietly fixed, because how a control fails is
more instructive than the control working.
**An inert RLS policy.** The consent policy on `prescriptions` was present in DDL
and enforced nothing. A Postgres view executes with its *owner's* privileges;
these views were owned by a superuser, and superusers bypass RLS unconditionally.
`FORCE ROW LEVEL SECURITY` was never consulted, and all four prescription rows
were visible including the two without consent. Caught by the privilege proof on
its first run. Fixed with `security_invoker = true` plus column-level grants that
withhold both `prescriber_hsa_id` and the consent flag itself — the filter column
is withheld because a readable filter column is an oracle for the hidden rows.
**A denial of service in the transport.** Oversized frames raised out of the
generator that reads them. A Python generator that raises is closed permanently,
so one oversized line ended the session — one bad frame, one dead connection.
Frames now carry the refusal as data, so the server answers and keeps serving.
---
## Why hand-write the protocol?
Because it demonstrates that the wire format and its trust boundaries are
understood rather than assumed, and because a security repository arguing for
supply-chain discipline should not pull forty transitive packages to parse JSON.
`mcp_core` has **zero runtime dependencies**.
**Production systems should use the official MCP SDK.** It is maintained,
spec-tracked, and tested far more broadly than this. That trade-off is stated
here rather than left for a reviewer to notice.
---
## Repository layout
```
mcp-servers/
mcp_core/ protocol layer — jsonrpc, transport, server, audit, errors
rds_readonly_mcp/ guardrails, PII classification, tools
sql/ roles, RLS, masked views ← the controls that actually hold
tests/ conformance · bypass suite · leak assertions
infra/ Terraform + CDK (static validation only)
platform/ AWS security platform — org-wide, not agent-specific
lib/cdk-security/ synth-time Aspects, shared by every CDK app here
docs/ upstream references, responsibility playbooks, case studies
scripts/ evidence generators
evidence/ regenerable proof artifacts
docs/ threat model, AI secure-coding policy, IR, compliance
readiness/ role readiness: JD coverage, operating plan, drills
```
Node dependencies are an **npm workspace rooted at the repository root** — run `npm ci` there, not
inside `infra/cdk`. The reason is a security one and is explained in
`platform/lib/cdk-security/README.md`: two copies of `constructs` would make every Aspect's
`instanceof` check silently return false, and the controls would evaporate without failing.
**New here?** `docs/07-usage.md` covers running it, wiring the MCP servers into
an agent, the tech-stack rationale, and the CI pipeline step by step.
---
## Context
Built as the technical dossier for a **Lead Security Engineer** application
(Core Technology team, Stockholm) at a Nordic online pharmacy referred to
throughout as **APT** — a role
whose posting asks specifically for *"secure practices for coding with AI
assistants, ensuring generated code meets security standards, avoids data leakage,
and aligns with regulations."*
The pharmacy schema is **entirely synthetic**. Every personnummer is deliberately
invalid, every email is on `example.com`, and every prescription is fabricated.
Seeding a demonstration like this with real data would contradict its own thesis.
Licensed MIT. **APT is an anonymised placeholder**, not a real organisation's
name; the hiring company is deliberately not identified anywhere in this
repository. Not affiliated with or endorsed by any pharmacy operator.