How to safely run untrusted AI-agent code on GCP

The moment your product lets an LLM agent write and execute code — a code interpreter, a data-analysis tool, an autonomous task runner — you are running untrusted code on your infrastructure. The model is not malicious, but its output is attacker-influenceable through prompt injection, and it has no concept of your security boundary. This is a defense-in-depth problem, and on GCP the defaults are dangerously permissive. Here is the threat model and the layered fix.

💡 Running agent workloads on GPUs too? The free GPU Idle-Cost Calculator shows what idle accelerator capacity is costing you — a two-minute number, no signup.

The threat model

Assume the code that runs is adversarial. Concretely, three things go wrong:

Defense 1 — Close the metadata credential path (do this first)

This is the highest-value single control, and the correct mechanism on GKE is Workload Identity — not a blanket firewall rule. Here is the important nuance: with Workload Identity enabled, the in-cluster GKE metadata server intercepts requests to 169.254.169.254 and only ever returns a token for the pod's own bound Google service account. It refuses to hand out the node's powerful default service-account credentials. So enabling Workload Identity is what actually neutralizes the credential-theft path; the metadata address stays reachable precisely because it is now the governed, least-privilege token broker.

# Enable on the cluster, then bind a K8s SA to a minimal Google SA:
gcloud container clusters update CLUSTER \
  --workload-pool=PROJECT_ID.svc.id.goog

gcloud iam service-accounts add-iam-policy-binding \
  agent-task@PROJECT_ID.iam.gserviceaccount.com \
  --role roles/iam.workloadIdentityUser \
  --member "serviceAccount:PROJECT_ID.svc.id.goog[agent-sandbox/agent-ksa]"

Do not simply blanket-block egress to 169.254.169.254 when you rely on Workload Identity — that same address is how the pod obtains its own least-privilege token, so blocking it breaks legitimate auth. The exception is a task that needs zero Google Cloud credentials: for that workload, additionally deny egress to the metadata IP so it can't reach the server at all.

apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata: {name: deny-metadata, namespace: agent-sandbox}
spec:
  # apply ONLY to no-cloud-access tasks, not Workload-Identity pods
  podSelector: {matchLabels: {needs-gcp-creds: "false"}}
  policyTypes: [Egress]
  egress:
  - to:
    - ipBlock:
        cidr: 0.0.0.0/0
        except: [169.254.169.254/32]   # block the metadata server

On plain Compute Engine there is no Workload Identity broker, so the metadata server always returns the instance's own service-account token on the v1 path — note that disabling the legacy v0.1/v1beta1 endpoints does not close that v1 token path. The hardening there is to attach a minimal (or no) service account to the instance so the token that leaks is nearly powerless, and for untrusted code to block egress to 169.254.169.254 at the firewall or host level so the token endpoint is simply unreachable. The principle is the same: untrusted code must never be able to obtain a token for anything more powerful than its own task identity.

Defense 2 — A real isolation boundary: gVisor or Cloud Run

Because a container shares the host kernel, use a stronger boundary for genuinely untrusted code. Two GCP-native options:

Whichever you pick, disallow the classic escape hatches: never privileged: true, never mount the Docker/containerd socket, and never mount host paths into the sandbox.

Want the whole pattern as a working blueprint? The Agent Sandbox on GCP pack ships the sandboxed node pool, the metadata-blocking NetworkPolicy, per-task service-account wiring, egress controls and the hardened PodSpec — the defense-in-depth stack from this article, ready to deploy.

And to size the compute behind it, the free GPU Idle-Cost Calculator →

Defense 3 — Per-task ephemeral service accounts, least privilege

The default Compute Engine service account is broadly privileged and attached to nodes automatically — never let untrusted code run as it. Instead, mint a dedicated service account per task type with only the IAM roles that task genuinely needs (a single bucket, a single dataset), bound to the pod through Workload Identity. Keep credentials short-lived and per-task so a stolen token expires fast and unlocks almost nothing. The test to apply: if this exact code were fully compromised, what could the identity it runs as touch? If the answer is more than the task requires, tighten the IAM binding.

Defense 4 — Egress controls

Workload Identity closes the credential path; egress controls close the exfiltration path. Default-deny outbound and allow only what the task needs. In practice that means a NetworkPolicy that denies egress except to a known allowlist, running the workload in a private cluster (no public node IPs) with tightly scoped Cloud NAT or none at all, and, for a strong perimeter around your data services, VPC Service Controls so tokens can't be used to pull data out to another project even if they leak. If the task legitimately needs the internet, proxy it through an allowlisting egress gateway rather than opening 0.0.0.0/0.

Defense 5 — Harden the pod: seccomp, no-new-privileges, limits, timeouts

Assume everything above can fail and make the container itself hostile terrain. A hardened securityContext plus resource caps and a hard deadline:

securityContext:
  runAsNonRoot: true
  allowPrivilegeEscalation: false      # no-new-privileges
  readOnlyRootFilesystem: true
  capabilities: {drop: ["ALL"]}
  seccompProfile: {type: RuntimeDefault}
resources:
  limits: {cpu: "1", memory: 1Gi}      # cap the blast radius
# on the Pod spec:
activeDeadlineSeconds: 300             # kill runaway tasks

allowPrivilegeEscalation: false sets no-new-privileges so a setuid binary can't gain rights; seccompProfile: RuntimeDefault filters dangerous syscalls; dropping all capabilities and a read-only root filesystem remove the easy footholds. Resource limits stop a fork-bomb or crypto-miner from taking the node, and activeDeadlineSeconds guarantees the task can't run forever. Mount scratch space as a size-limited emptyDir so writes go somewhere ephemeral and bounded.

Putting the layers together

LayerStopsMechanism on GCP
Metadata pathCredential theftWorkload Identity (deny 169.254.169.254 only for no-cred tasks)
gVisor / Cloud RunKernel escapeGKE Sandbox runtimeClass / Cloud Run isolation
Ephemeral least-priv SALateral movementPer-task SA, minimal IAM, short-lived
Egress controlsData exfiltrationDefault-deny NetworkPolicy, private cluster, VPC-SC
Pod hardeningPrivilege gain, runawaysseccomp, drop caps, limits, deadline

No single layer is sufficient — the point of defense in depth is that an agent has to defeat all of them, and the first two (Workload Identity closing the credential path and a real gVisor sandbox) already remove the two attacks that turn "the model wrote bad code" into "the attacker owns our project." This is the same governance discipline the wider agentic-AI wave demands: treat model output as untrusted input, and give it the least power that still lets it do its job.

Want a human to review your agent-execution setup — the service-account scopes, the egress, the escape hatches you forgot — book a GPU Cost & Reliability Audit → (we cover agent-infra security and cost together).
Get the next field note. Practical, occasional notes on AI-infra security and cost — the traps and the numbers. No spam.

Related reading

Run the compute behind it well: production vLLM on Kubernetes, why GPU clusters sit idle, and reduce LLM inference cost.