// guides

Developer Environment Red Team: Repo to Production

Developer environment red team diagram from a poisoned repo through CI to a cloud role

A developer environment red team starts in a poisoned repo, issue, extension, or MCP server and measures whether developer tooling, CI identities, and AI coding agents can reach production. A web pentest never walks that path, so the cloud role on the runner stays untested.

The short answer

  • A developer environment red team starts from a poisoned repo, issue, extension, or MCP server and measures whether CI identities and AI coding agents can reach production.
  • The path a web pentest misses is workstation and workflow trust into OIDC and cloud IAM, not another bug in the application.
  • Test coding agents in a sink account with no production tokens; rank findings by the role that was assumed.
  • Fixes that survive retest: no untrusted checkout on pull_request_target, pinned OIDC subjects, and approval gates bound to the tool call.

Developer environment red team concept: laptop path leading into a server room
The objective is a cloud role, reached from developer tooling.

Key takeaways

  • A developer environment red team scores identity hops from repo content, IDE extensions, and MCP servers into CI and cloud roles, not web findings.
  • pull_request_target plus a checkout of untrusted head code is dangerous when that job also has secrets or id-token: write.
  • The agent's privilege is the union of tokens in its session. OIDC trust must pin repo, ref or environment, and audience.
  • Test coding agents in a sink account with metadata and production egress blocked. Prove paths with canaries, not production changes.
  • Severity is the role that was assumed and the data or deploy actions that role can perform. A rude model reply is not a finding.
  • The report is one page per successful path, a trust-policy diff, detective-control timing, and retest criteria written before the fix sprint.

What is a developer environment red team, and how does it differ from a pentest?

A developer environment red team tests the trust chain from a developer workstation and its tools into CI/CD and cloud IAM. Production access is the objective. The unit of work is identity hops, not a list of web findings.

A normal application pentest starts at a URL, an API, or a mobile binary and stops at the app boundary. A classic red team often starts with phishing or an assumed breach on a user laptop, then hunts Active Directory or Entra ID.

This engagement starts in the software factory: untrusted repository content, IDE extensions, MCP servers, and the non-human identities those systems may assume.

The question for the CTO is narrow. If an attacker can influence what a developer or an AI coding agent reads, or what a pipeline checks out, which cloud role, registry, or production secret becomes reachable without a separate application exploit? Severity is the role that was assumed and the data or deploy actions that role can perform.

Grey-box fits this work. Tool wiring, OIDC trust policies, and runner secrets are invisible from the internet. Black-box still answers a different question: what an outsider can do with public repos and marketplace listings.

What does poisoned repo to production mean in practice?

Poisoned repo to production means untrusted content in a repository, issue, or pull request runs with the privileges of a maintainer workflow, a developer session, or a coding agent that already holds cloud credentials.

The content does not have to be a binary exploit. A GitHub Actions workflow that uses pull_request_target and then checks out the pull request head runs attacker-controlled code in the base repository. That job holds secrets or an OIDC token only when the workflow grants them. The OIDC token specifically requires id-token: write.

A README, issue comment, or AGENTS.md file can carry instructions an AI coding agent loads as project policy, then follows with a shell, a git remote, or a cloud CLI. A planted file in Downloads was enough to make an agent run code, covered in the AGENTS.md downloads hijack.

A typical hop sequence:

  • Untrusted input lands where a trusted principal reads it: a fork pull request, an issue, a dependency, an extension, or an MCP tool description.
  • A workflow, IDE task, or agent runs code or fetches remote content using a maintainer token, a GITHUB_TOKEN with id-token: write, or a logged-in cloud CLI.
  • OIDC or a long-lived key assumes a role whose trust policy is wider than one repo, one ref, and one environment.
  • That role can read secrets, push an image, or change infrastructure. Production access is the role’s API, not a shell on a server.

The engagement proves the hop with canaries: a distinct role, a marker secret, a log line, and a timestamp. It does not need to push a real production change to show the path exists.

Which systems, identities, and teams belong in scope?

Scope is the deployed developer system: workstations or golden images, IDEs, extensions, MCP servers, source hosting (GitHub or GitLab), CI, OIDC providers, and the cloud roles CI and agents can assume.

Name the systems in the statement of work. For most US and EU product companies that means GitHub or GitLab, GitHub Actions or the equivalent, self-hosted and hosted runners, artifact registries, cloud accounts used for build and deploy, SSO apps on those consoles, secrets managers, and any internal index the coding agent queries.

Include the AI coding agent configuration: rules files, memory, plugins, and which credentials sit in the same OS session as the agent. The same exclusion rule as in our AI agent penetration test scope checklist applies: every exclusion has an owner and a reason.

Identities matter more than hostnames. List human developers by role (contractor versus full-time, production access versus not), plus non-human identities: workflow OIDC subjects, deploy service accounts, workload identity, bot users, and marketplace publisher accounts.

The agent’s privilege is the union of tokens it can read, not the policy written on a slide. Teams in the room are Platform or DevEx, identity, cloud security, AppSec, and whoever owns the coding-agent rollout. Production writes stay out unless a canary account and a named objective say otherwise.

Can a malicious VS Code extension or MCP server lead to cloud compromise?

Yes. When the tool runs as the developer, and that session already holds SSO, cloud CLI credentials, or a path to a CI role, the install is the exploit. You do not need a new cloud vulnerability.

VS Code, and editors that reuse its extension model, run extension code with the user’s filesystem and often with the user’s environment. Clients that install from the Cursor marketplace sit in that same class. A theme or helper with an install hook can read SSH keys, cloud credential files, and token caches.

From there the path is ordinary. The extension assumes the same role the human uses for kubectl, the AWS CLI, or gcloud, or it pushes a workflow change the human’s git credentials allow.

An MCP server is a tool the agent is allowed to call. If the schema says the server can run a shell, fetch a URL, or apply a patch, untrusted text can steer that call. Confused-deputy cases show up when the MCP process holds a broader token than the user thought they granted: a personal access token with repo and workflow scope, or a cloud credential file mounted into the agent container.

We test that wiring in MCP server and tool-chain security testing. Marketplace compromise stays in scope even when developers install only verified tools. Pin versions, review publisher changes, and treat a new extension the way you treat a new production dependency.

The finding that matters is which identity the extension or server could use on the day of the test, and whether that identity was production.

Malicious VS Code extension idea: a small device bridging a desk to a data center
Extensions and MCP servers inherit the workstation identity.

How do you test AI coding agents, and which access mode fits?

Run the agent against a mirror: a canary repo, CI with no production secrets, cloud roles that can only touch a sink account, and a network policy that blocks production endpoints and the cloud metadata service.

Give the operator grey-box access to agent config, tool schemas, and logs. Do not point the agent at the company source host with a maintainer token to make the test realistic. Copy the real permission set onto identities that terminate in the sink. If production SSO is required to see a control, use a read-only view or a cloned conditional-access policy.

  • The agent runs in a VM or container that does not mount the host keychain, SSH agent, or Docker socket.
  • Egress is an allowlist. Deny 169.254.169.254 and, on AWS, the IPv6 IMDS address fd00:ec2::254. Block the metadata hostname your cloud actually uses, plus production API hostnames.
  • Tool calls that would push, deploy, or send data off-box stop at an approval gate the operator controls. The test is whether the gate binds the actual tool arguments.
  • Poisoned fixtures are canary files and canary issues in the lab repo, clearly labeled, and removed at the end.
  • A kill switch revokes the lab role and the agent token in one step.

If the same path would have worked against production, the report says so and shows the trust-policy diff, not a production change.

Choose grey-box when you need to know which of your real roles and workflows are reachable. Choose black-box for the public attack surface only. Choose assumed breach when you already grant that a workstation or a repo is hostile and you want blast radius.

Mode Starting point What you learn What you miss
Black-box Public repos, public packages, marketplace listings, leaked tokens you can legally use What an outsider can pull without insider docs OIDC policies, private workflows, agent config, runner secrets
Grey-box Read access to org settings, workflow files, IAM trust policies, agent configs, and a lab identity The actual path from repo content to a role, with evidence Stealth against your SOC; this is a control test
Assumed breach A planted foothold: lab workstation image, low-priv collaborator, or an accepted poisoned fixture How far that foothold goes through CI and cloud, and which detective controls fire The initial install path, unless you add it as a separate objective

Most CTOs should buy grey-box plus one assumed-breach objective. The failures that matter live in trust policies and token scope, and those are not on the public internet. An assumed breach assessment fits when leadership wants time-to-detect from a developer foothold, with the SOC in or out of the know by written agreement.

Write the mode into the contract. Switching from grey-box to a live phishing objective mid-test changes legal scope, identity risk, and what production means.

What fails most often, and which controls cut the path?

The repeat findings are trust wider than the job: pull_request_target with an untrusted checkout, OIDC subjects that match too much, long-lived cloud keys beside the agent, and self-hosted runners that share a filesystem with other builds.

On CI, the pattern is a privileged trigger plus a checkout of fork or pull-request code, GITHUB_TOKEN left at write, and id-token: write on the same job. Actions are pinned by tag instead of commit SHA. Environment protection rules exist for production and are bypassed by a workflow that deploys from a branch with no reviewer.

On the workstation, coding agents load repository instructions, issues, and docs into context and also have a shell. Secrets live in .env files, shell history, or agent memory. MCP servers and extensions update themselves. A developer cloud CLI is logged in with a human role that can read production, and the agent may call that CLI. An internal code index returns another team’s chunks because there is no ACL at query time.

Approval gates fail in one specific way: a human approves a sentence in chat, and the executed tool call differs in target, arguments, or destination. Rank by blast radius. A jailbreak string that makes the model rude is not a finding. A tool call that assumed a deploy role is.

pull_request_target and CI trust

Stop running untrusted code in a workflow that has secrets or an OIDC token. pull_request_target is for privileged, trusted code paths. pull_request is for anything that comes from a fork or an unreviewed head.

pull_request_target executes in the base repository context, so it can receive secrets and, with id-token: write, mint an OIDC token. A checkout of the pull-request ref then runs the attacker’s steps or build scripts with those privileges.

  • Do not check out pull-request head code, submodules, or third-party actions from a pull_request_target workflow.
  • If you need a privileged comment or label bot, keep that job free of build steps and free of id-token: write.
  • Split CI: untrusted build on pull_request with a read-only token and no cloud credentials. Deploy only from protected branches after a required review, using an environment with reviewers.
  • Pin actions to a full commit SHA. Set GITHUB_TOKEN permissions to the minimum per job. contents: write and id-token: write do not belong on a job that also builds untrusted code.
  • Prefer reusable workflows in a trusted repo over copy-pasted privileged YAML in every service repo.
  • For self-hosted runners, use ephemeral instances. A persistent runner that survives a poisoned job is a shared admin box.

Retest by opening a fork pull request that only tries to read a canary and assume a lab role. The pass condition is that no secret and no token are present in that job, and the lab role’s trust policy rejects the subject.

Indirect prompt injection and exfiltration

Indirect prompt injection becomes exfiltration only when untrusted text, a tool that can send data, and a credential or private corpus sit in the same agent session. Remove one of the three and the path dies.

Untrusted text in a coding agent is the repo: issues, pull-request bodies, commit messages, dependency READMEs, pages the agent fetches, retrieved chunks, and instruction files such as AGENTS.md. You will not reliably filter the prompt after the model has tools. Put the control on the side effect.

  • Tool allowlists follow repo trust. Public and fork checkouts get read and local build tools. Network, secret, and cloud tools stay off.
  • The approval gate, the same broken control as in the findings above, must show the tool name and arguments. The runtime rejects a mismatch. Do not re-approve a chat sentence and hope the call matches.
  • Secrets stay out of the agent process environment. A broker mints short-lived, audience-bound tokens after approval.
  • Egress allowlists sit on the agent proxy. A canary string planted in a private file must not leave the network.
  • Query-time ACLs apply to any retrieval index. Chunking is not an authorization boundary.
  • Render tool output as data. Do not pass raw tool output back into a channel that can schedule another tool without a check.

Measure the control with a canary document and a canary URL. The finding closes when the trace shows the document was retrieved and the outbound call was blocked.

OIDC trust and non-human identities

Govern non-human identities as production principals. One workflow, or one agent runtime, maps to one cloud role. The OIDC trust policy pins repository, ref or environment, and audience.

A subject claim of any repo in the org, or any branch, means a poisoned workflow in a sandbox repo can assume the deploy role. Wildcards on the subject claim, reusable roles shared by production and lab, and static access keys baked into runner images are the same bug in three different guises. Workload identity federation replaces long-lived keys only when the trust policy is tighter than the key was.

  • Separate roles for plan, build, and deploy. Deploy roles trust a protected environment subject, not pull_request.
  • Condition on audience so a token minted for one cloud account cannot be replayed to another.
  • Do not let trust policies match newly created repos. New repos start with no cloud trust until platform reviews the workflow.
  • No human SSO session inside CI. Humans review. Workloads authenticate as themselves.
  • Keep session length in minutes. Log AssumeRole data events, and alert when a subject appears that is not on the inventory.
  • Every non-human identity has a team, a repo, and a quarterly access review. Orphaned bot users and old deploy keys are findings.

Coding-agent identities belong on the same inventory. An agent that deploys is a workload. It gets its own role, its own audit stream, and no right to mint further roles.

What should the report, timeline, and retest include?

Demand a path diagram from the initial untrusted input to the cloud API call, with the identity at each hop, the control that failed, and the control that would have stopped it.

The readout is for a decision. Ask for these artifacts:

  • Objectives and mode (black-box, grey-box, assumed breach), and every exclusion with an owner.
  • One page per successful path: entry (repo file, issue, extension, MCP tool), principal that acted, token or OIDC subject, role assumed, API called in the sink, timestamp, and log IDs.
  • Blast radius in business terms: which production role shares that trust policy today, and what it can deploy or read.
  • Detective controls: which alerts fired, which did not, and the clock time from action to ticket.
  • Fixes a platform team can merge: workflow permission blocks, trust-policy diff, runner standard, agent tool policy. Name the log source and the query.
  • Retest criteria written before the fix sprint, so pass or fail is mechanical.

CVSS on a workflow file alone will understate a deploy role and overstate a rude model output. Score autonomy and privilege. If a human must approve the exact tool call, say so. If the agent proceeds alone, say so.

Plan three to four weeks for a grey-box developer environment red team on one primary source host and one cloud, plus a one to two week retest after the agreed fixes land.

Week one is scope, identity inventory, and the lab: sink account, canary repo, mirrored workflows, agent image. Weeks two and three are the paths: CI trust, OIDC, extensions and MCP, coding-agent tool abuse. The last days are evidence packs and the CTO readout.

A second cloud or a second source host is a separate objective, agreed before kickoff. Retest replays the same canary pull request, the same extension or MCP fixture, and the same agent prompt against the fixed trust policy. Pass means the lab role is not assumed and the canary does not leave the network.

Poisoned repo to production shown as ink moving from files into a locked cabinet
Untrusted repo content only matters if a privileged identity runs it.

FAQ

How is a developer environment red team different from a normal pentest?

An application pentest stops at the app boundary. This engagement starts in the software factory and follows maintainer tokens, runner OIDC, and coding-agent tools into cloud roles. The result is a path, not a list of URL findings.

Does pull_request_target always mint an OIDC token?

No. pull_request_target runs in the base repository context, so it can receive secrets and mint OIDC only when the workflow grants those permissions. The OIDC token requires id-token: write. Checking out the pull-request head in that job is what runs attacker-controlled code with those rights.

Can a VS Code extension or MCP server reach production cloud?

Yes, when the extension or MCP server runs as the developer and that session already holds SSO, a cloud CLI, or a path to a CI role. The test records which identity was usable that day, in a sink account, and whether the same trust policy exists in production.

How do you test AI coding agents without touching production?

Use a canary repo, CI with no production secrets, and roles that terminate in a sink account. Block metadata addresses, including 169.254.169.254 and AWS fd00:ec2::254, and block production API hostnames. Approval gates must bind tool arguments. A kill switch revokes the lab role and the agent token together.

What should a CTO demand in the readout?

A diagram from untrusted input to the cloud API, the identity at each hop, and the control that failed. Each successful path needs log IDs, the production role that shares the trust policy, alert timing, a mergeable fix, and a mechanical retest. Score the role and the autonomy, not a CVSS on the YAML file alone.

How long does the engagement take, and how do you retest?

Plan three to four weeks for one source host and one cloud, then one to two weeks to retest after fixes land. A second cloud or forge is a separate objective. Retest replays the canary pull request, the extension or MCP fixture, and the agent prompt. Pass means the lab role is not assumed and the canary stays on the network.

agent::offense runs developer environment red teams that start at the repo, the IDE, and the MCP tool and stop at the cloud role those paths can assume. If you want that path tested against a sink account before a coding-agent rollout, start with an assumed breach assessment scoped to CI identities and agent tools.



// get started

Work with AgentOffense

Tell us about your target and goals. We’ll reply with scope and a fixed-price quote — usually within one business day.

./request_engagement