Skip to main content
Glama
Mpurushotham

Secure RDS Read-Only MCP Server

by Mpurushotham
README.md
# secure-serverless-security-platform

[![security-pipeline](https://github.com/Mpurushotham/secure-serverless-security-platform/actions/workflows/security-pipeline.yml/badge.svg)](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.