Attack Vectors, Traced — N1 Threat Assessment
Attack Vectors, Traced
Section titled “Attack Vectors, Traced”Each vector is walked gate by gate through N1’s real landscape and asked one question — with the controls that exist today, can you actually get in? Every verdict is backed by an artifact: a config read on the live production cluster, a file:line in the service code, or a Cloudflare control-plane record. There is no unauthenticated path from the open internet to patient data. Every realistic route runs through something N1 already trusts — a device, an account, or a signed-in person.
Metadata
Section titled “Metadata”- Source: Landscape + live verification
- Cluster: n1-production · 191421493115 · us-east-2
- CF snapshot: state.json · 2026-06-08
- Method: claim → artifact, or it doesn’t ship
How each verdict was decided — claim → artifact
Section titled “How each verdict was decided — claim → artifact”Verdict Summary
Section titled “Verdict Summary”- 4 Reachable — A grounded path exists with today’s controls. Worth real defensive weight.
- 7 Bounded — A genuine weakness, but a verified control stands in the way. Named each time.
- 2 Needs-evidence — The deciding artifact lives in a SaaS console, not in code. A question, not a finding.
- 3 Blocked — The controls close the path. Kept to show the work.
- 3 Amplifiers — Not a way in. Turns a small foothold into a large one. Matters after entry.
02 · A worked attack, start to finish — the most realistic chain
Section titled “02 · A worked attack, start to finish — the most realistic chain”Compromise a remote contractor’s laptop, then try to reach patient data.
Section titled “Compromise a remote contractor’s laptop, then try to reach patient data.”Entry layer: endpoints. Objective: PHI in Aurora / S3.
- Phish or infect an unmanaged laptop. Remote contractors work on personal machines — no device management, no endpoint detection on N1 devices.Landscape §2
- Ride the trusted session. The device holds an always-on WARP tunnel and a signed-in Google session; the attacker inherits an identity that already passed 2FA.§2, §4.2
- See only what that contractor can see. A contractor account is not an admin — no AWS access (only three named accounts), not on the prod-deploy list. Blast radius = that one person.§4.5, §7A.1
- Reach staging data over WARP. Any WARP-enrolled
@n1.healthcaredevice can resolvedb.staging.n1and read the staging database, read-only.§6.7 - Is staging real or synthetic? — open. The whole severity of step 4 turns on this. No seed script, fixture, or prod→staging copy job exists in any repo, and
synthetic-medical-reportsis an OCR-training generator, not a staging seeder. The absence of a seeder leans toward organic (possibly real) data — but that’s inference. Settles at the console: query staging Aurora for account emails / ages. - Production PHI — stops here. Production data is never exposed over WARP. Reaching it needs a production app session, AWS credentials (three people), or a pod foothold. The stolen laptop provides none.§6.7, §4.5, §17.1
Verdict — Reachable but bounded. The laptop is the realistic front door; it yields staging access and a trusted identity, but not production PHI by itself. The chain continues to the crown jewels only if (a) the laptop belongs to a privileged account, or (b) staging holds real records (step 5, open).
A second realistic chain — the in-cluster ArgoCD bypass. A foothold in any pod (via V-02, a supply-chain image) on the flat pod network (V-12) can reach argocd-server:8080 directly and authenticate as the enabled local admin account — bypassing Cloudflare Access, Google SSO and ArgoCD RBAC — for full production deploy control. See V-07 Finding A.
03 · Every vector, traced
Section titled “03 · Every vector, traced”Reachable
V-01: Unmanaged endpoint compromise — the keystone
Section titled “V-01: Unmanaged endpoint compromise — the keystone”Verdict: 🔴 Reachable | Category: Endpoints
No device management and no endpoint detection on N1 machines; access is gated by the session on the device, so stealing the session skips 2FA entirely. Stops at the privileges of the account on that laptop — a contractor device ≠ AWS ≠ prod deploy. The crown-jewel version requires a privileged-account device. §2, §4.5
V-02: Path-traversal write in the document parser
Section titled “V-02: Path-traversal write in the document parser”Verdict: 🔴 Reachable | Category: Application · ingestion
◕ signed-in clinician uploads› ✕ filename unsanitized› ✕ joined into a write path
An authenticated upload reaches api-backend, which takes the filename unsanitized — records.py:254 (file.filename or "unknown") — and passes it to parser-gate .../converters/office.py:191: input_path = temp_path / source_filename. A name like ../../etc/cron.d/evil.docx resolves outside the temp dir. It is a filesystem write, not command injection (no shell=True anywhere). Same flaw at office.py:141.
Bounded by container filesystem permissions — a non-root user with read-only rootfs largely neuters it (confirm in the Helm chart). No other reachable sink on the active path: mineru 2.7.6 emits its own image filenames; parser-sequential gets pre-parsed JSON; OcrRouter / pdf-converter are archived and off-path; helix-parser is a separate genetic pipeline.
Fix
Strip directory components: temp_path / Path(source_filename).name at office.py:141 and :191.
V-04: Dangling DNS takeover (plan.n1-research.com)
Section titled “V-04: Dangling DNS takeover (plan.n1-research.com)”Verdict: 🔴 Reachable | Category: Edge · DNS
A live CNAME points at an external Cloudflare Pages project not in N1’s account — a claimable dangling target on a trusted domain. Impact is phishing / brand abuse on *.n1-research.com, a stepping stone to V-01-style credential theft, not direct data access. The retired GKE tunnels behind other dead DNS are harmless 502 dead-ends; this CNAME is the only live takeover candidate. §6.4, §6.7
V-20: The alarm shares fate with the building
Section titled “V-20: The alarm shares fate with the building”Verdict: 🔴 Reachable | Category: Detection integrity
SigNoz sends alerts to Slack, and Slack federates to Google — so the channel that would warn about an identity compromise can be silenced by that same compromise. There is no out-of-band alert path independent of Google and Slack. A response gap, not an entry point. §13, §6.6
Bounded
V-03: Indirect prompt injection via uploaded records
Section titled “V-03: Indirect prompt injection via uploaded records”Verdict: 🟡 Bounded | Category: — nuisanceAI pipeline
CHR agents (forge-runner, on the AGNO framework) read patient data through MCP tools; instructions hidden in an uploaded document can influence the model the agent acts on. The deciding question — what those tools can reach — resolves against the attacker (V-10 below): the tools are read-only, single-patient, and have no attacker-controllable egress. A successful injection is therefore a nuisance (garbage in the generated report), not an exfiltration path.
Every tool reads only the one patient pinned in WORK_DIR (patient-data/server.py:27); nothing writes patient records; outbound calls go only to hardcoded NCBI / OpenTargets / N1 endpoints. No cross-patient read, no egress to an attacker.
V-07: ArgoCD authentication
Section titled “V-07: ArgoCD authentication”Verdict: 🟡 Bounded | Category: — in-cluster bypassKubernetes · deploy control
✓ CF Access app on prod host› ✓ @n1.healthcare SSO + deny-all› ✕ local admin bypasses it in-cluster
Production ArgoCD is gated by a Cloudflare Access application named “ArgoCD” whose self_hosted_domains include argocd.n1-research.com (CF snapshot, 2026-06-08). Policies: (1) allow email_domain=n1.healthcare + Google login; (2) deny everyone; (3) non-identity any_valid_service_token. The external door is shut. RBAC default is role:readonly; named deployers are jasper, umar.shah, arun, darpan.bafana. Three residuals from the live cluster:
Path A (green): internet → Cloudflare Access (Google SSO, deny-all) → tunnel → ArgoCD. Path B (red): a pod inside the flat network reaches argocd-server:8080 directly and logs in as the enabled admin account — no CF Access, no SSO, full RBAC bypass.
⚠ Finding A — local admin account enabled (real)
Live argocd-cm: admin.enabled: "true"; argocd-secret carries admin.password + admin.passwordMtime. CF Access only fronts the external hostname; with no NetworkPolicies (V-12), any pod can reach argocd-server:8080 and log in as admin (superuser), bypassing CF Access, SSO and RBAC — full prod deploy control. Fix: admin.enabled: "false" + a NetworkPolicy on the argocd namespace.
⚠ Finding B — service-token policy unscoped
Access policy #3 admits any_valid_service_token — any valid Cloudflare service token in the account (currently two SigNoz ingestion tokens) can reach prod ArgoCD non-interactively, around the human SSO gate. Fix: scope to a named token, or remove policy #3.
⚠ Finding C — jwt-proxy may be off the served path (drift)
Helm intends argocd-server → jwt-proxy :8888 (it mints the Cf-Access-Authenticated-User-Email header Dex authproxy needs). Live, that Service targets raw :8080; the :8888 path is on a separate argocd-server-proxy Service. If the dashboard-managed tunnel forwards to the raw service, SSO-by-header breaks and login falls back to the shared admin. server.insecure:true ⇒ in-cluster traffic is plaintext. Confirm: tunnel targets argocd-server-proxy:8888.
V-11: Phishing a privileged Google identity
Section titled “V-11: Phishing a privileged Google identity”Verdict: 🟡 Bounded | Category: Identity
2FA is mandatory; onsite staff use hardware YubiKeys that defeat remote phishing outright. A remote user on a TOTP authenticator app can be relay-phished, but the blast radius is that account’s reach; reaching the crown jewels needs one of the three AWS accounts, the hardest to phish. §4.3, §4.5
V-13: GitHub account → production code
Section titled “V-13: GitHub account → production code”Verdict: 🟡 Bounded | Category: Source & CI/CD
Org-wide 2FA, branch protection (no force-push, ≥1 approval), and signed commits on every branch of every repo. A stolen session can’t produce a signed commit without the key, nor merge without a second approver. The realistic version needs a full device compromise (V-01, for the key) plus a second account. §17.3
V-15: LiteLLM key theft
Section titled “V-15: LiteLLM key theft”Verdict: 🟡 Bounded | Category: AI pipeline
A LiteLLM key buys model access — cost abuse / denial-of-service — not a read of stored PHI; you’d already need the data to send it. Keys are Access-gated, self-service, 30-day expiry, invalidated on reissue. §11, §17.2
V-18: Cloudflare account / tunnel compromise
Section titled “V-18: Cloudflare account / tunnel compromise”Verdict: 🟡 Bounded | Category: Edge · second trust root
Catastrophic if it happens (owns ingress, the identity gate, TLS termination) but low-likelihood and dependent on Cloudflare’s own security. Tunnel tokens live in Secrets Manager, needing a prior AWS/pod foothold to extract. How the Cloudflare dashboard account itself is protected (2FA / SSO / admin-seat count) is not confirmed here, though the state.json snapshot likely captures part of it. §6.7, §8.8
V-19: Unpinned ingress image (cloudflared:latest)
Section titled “V-19: Unpinned ingress image (cloudflared:latest)”Verdict: 🟡 Bounded | Category: Supply chain
The sole ingress component is pulled by mutable tag, as are a few first-party Go proxies and mineru — no digest pin, so a bad upstream image lands on restart. Realistic only via upstream registry compromise. Base/tool images are Docker Hardened Images and the prod path runs a Grype scan. The public R2 wheel bucket is a read-only public-exposure issue, not an injection path unless write access is separately compromised. §9.2, §9.4, §8.2, §7A.5, §6.5
Needs-evidence
V-08: Rogue OAuth-app consent grant (Workspace)
Section titled “V-08: Rogue OAuth-app consent grant (Workspace)”Verdict: 🔵 Needs Evidence | Category: Identity
Whether Google Workspace restricts third-party OAuth app access (unconfigured = open; restricted = blocked). Not derivable from any repo or the cluster. Would need: the Workspace app-access / API-controls settings.
V-09: Staging holds real PHI
Section titled “V-09: Staging holds real PHI”Verdict: 🔵 Needs Evidence | Category: Data · developer access
Every enrolled @n1.healthcare device can read the staging DB and Redis, read-only — a broad internal surface (verified, §6.7). Whether those are real or synthetic records decides severity. No seed script, fixture, or prod→staging copy job exists in any repo, and synthetic-medical-reports is an OCR-training generator, not a staging seeder — the absence leans toward organic (possibly real) data, but that’s inference. Would need: staging Aurora account emails / ages, or the automated-testing image entrypoint.
Blocked
V-05: Broken object-level access (IDOR) on patient records
Section titled “V-05: Broken object-level access (IDOR) on patient records”Verdict: 🟢 Blocked | Category: Application · API
Object-level authorization is enforced at the router mount, not scattered per handler. Every PHI router is APIRouter(dependencies=[Depends(phi_scope_for(...))]) (records.py:57,59) → authorize() (policy.py:555,624), requiring the caller to be the data subject or hold a completed=True doctor-patient association (auth_service_client.py:164; pending associations don’t count). A CI test fails the build if any route lacks a guard (tests/unit/test_route_coverage.py); a second targets the IDOR class (tests/security/test_authz_full_audit.py). No bare lookup-by-path-id without an ownership check was found.
V-06: SSRF / injection in api-backend
Section titled “V-06: SSRF / injection in api-backend”Verdict: 🟢 Blocked | Category: Application · API
No user-controlled outbound-URL sink exists — every outbound URL is from environment config (support.py:39, base.py:721, hydra_auth.py:106). The one requests.post(external_api_url,...) is inside a docstring example (exceptions.py:495), not live code. SQL is SQLAlchemy ORM / parameterized; the two f-string SQL spots are a hardcoded isolation-level literal (biomarkers_service.py:53) and migration DDL. subprocess is test-only; no os.system / eval on user input.
V-17: Direct attack on AWS infrastructure
Section titled “V-17: Direct attack on AWS infrastructure”Verdict: 🟢 Blocked | Category: Cloud · network
Nothing to scan or connect to in AWS — ingress is a Cloudflare Tunnel that dials outward; no public IPs, load balancers, or inbound ports. The EKS API server is public but IAM/OIDC-authenticated and held by three accounts. This concentrates risk onto the edge (V-18) and identity (V-11). §17.1, §8.7
Amplifiers
V-12: Flat pod network — free lateral movement
Section titled “V-12: Flat pod network — free lateral movement”Verdict: ⚠️ Threat Amplifier | Category: Kubernetes
Each cluster has exactly two NetworkPolicies, both for Prometheus scraping; pod-to-pod traffic is otherwise unrestricted (NETWORK-SERVICES NS-1). Not a way in — it turns one compromised pod into reach across the whole cluster, and it is what makes V-07’s local-admin bypass reachable (any pod → argocd-server:8080). Staging and prod remain separate VPCs with no link.
V-14: No human gate after a merge to main
Section titled “V-14: No human gate after a merge to main”Verdict: ⚠️ Threat Amplifier | Category: Source & CI/CD
The deploy-approval step exists but is commented out of the shared workflow, so a merge to main ships to production with no second checkpoint. It removes the catch after the merge gate, so the entire defense collapses onto the PR review. Mitigated by SHA-pinned actions, safe-chain, and a Grype scan on the prod path. §17.3, §7A.5
V-16: Mass exfiltration via valid credentials
Section titled “V-16: Mass exfiltration via valid credentials”Verdict: ⚠️ Threat Amplifier | Category: Data · the objective
The objective, not an entry point — it only happens after another vector succeeds. S3 is fully public-access-blocked and KMS-encrypted at rest, so accidental exposure isn’t the risk; abuse via valid credentials is. The missing control is detection of bulk reads and unusual egress through the single NAT. §8.4, §8.7, §17.4
04 · What to act on, in priority order
Section titled “04 · What to act on, in priority order”- Disable the ArgoCD local
adminaccount (admin.enabled: "false") — closes the in-cluster bypass of Cloudflare Access. (V-07 A) - Scope the ArgoCD Access service-token policy to a named token, or drop policy #3. (V-07 B)
- Add NetworkPolicies to the
argocdand app namespaces — defangs V-07 A/B and is the V-12 fix. - Confirm the tunnel targets
argocd-server-proxy:8888, not raw:8080; reconcile the service drift. (V-07 C) - Patch the parser-gate path-traversal at
office.py:141,191. (V-02) - Endpoints (V-01) — no EDR; the keystone. Watch for session-token theft and unusual access from known-good devices.
- Data-read volume & NAT egress (V-16) — bulk reads from Aurora / S3 are not monitored.
- Answer V-09 at the console — real or synthetic PHI in staging.
- Out-of-band alerting (V-20) — an alert path independent of Google and Slack.
- DNS hygiene (V-04) — claim or remove the dangling
plan.n1-research.comCNAME.
Open questions
| Question | Status |
|---|---|
| Does staging hold real or synthetic PHI? | Open — narrowed (no seeder in any repo) |
| How is the Cloudflare dashboard account protected? | Open — snapshot may close it |
| Does Google Workspace restrict third-party OAuth apps? | Open — Workspace console |
Does the tunnel route ArgoCD through the jwt-proxy (:8888)? |
Open — confirm tunnel ingress target |
Verdicts reflect controls as configured on 2026-06-11 (production cluster 191421493115) and the Cloudflare snapshot of 2026-06-08, not as continuously re-verified against live config. Items marked Needs-evidence / Open are questions to resolve, not confirmed exposures. Method: n1-adversarial — a claim ships only with the artifact that backs it. |
