Skip to content

Attack Vectors, Traced — N1 Threat Assessment

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.

  • 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”
  • 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.

  1. Phish or infect an unmanaged laptop. Remote contractors work on personal machines — no device management, no endpoint detection on N1 devices.Landscape §2
  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
  3. 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
  4. Reach staging data over WARP. Any WARP-enrolled @n1.healthcare device can resolve db.staging.n1 and read the staging database, read-only.§6.7
  5. 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-reports is 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.
  6. 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.

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.

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

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

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

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.

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.

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.

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.

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

  1. Disable the ArgoCD local admin account (admin.enabled: "false") — closes the in-cluster bypass of Cloudflare Access. (V-07 A)
  2. Scope the ArgoCD Access service-token policy to a named token, or drop policy #3. (V-07 B)
  3. Add NetworkPolicies to the argocd and app namespaces — defangs V-07 A/B and is the V-12 fix.
  4. Confirm the tunnel targets argocd-server-proxy:8888, not raw :8080; reconcile the service drift. (V-07 C)
  5. Patch the parser-gate path-traversal at office.py:141,191. (V-02)
  6. Endpoints (V-01) — no EDR; the keystone. Watch for session-token theft and unusual access from known-good devices.
  7. Data-read volume & NAT egress (V-16) — bulk reads from Aurora / S3 are not monitored.
  8. Answer V-09 at the console — real or synthetic PHI in staging.
  9. Out-of-band alerting (V-20) — an alert path independent of Google and Slack.
  10. DNS hygiene (V-04) — claim or remove the dangling plan.n1-research.com CNAME.

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.