Secure Kubernetes Sandbox Access for AI Agents | Twingate

Secure Kubernetes Sandbox Access for AI Agents

Give AI agents a secure sandbox to access Kubernetes. No cluster credential or network access required.

AI agents are more useful when they can work with your Kubernetes clusters directly: reading logs, checking deployments, and restarting pods. The typical way to give them that access is a kubeconfig on the agent’s host with a long-lived token or client certificate, plus network access to the API server.

A standing credential on a VM is a risk before any agent is involved. More people usually have access to a VM through cloud IAM than you’d expect, and anyone who can reach the VM can use the credential or copy it somewhere else. Agents add a second problem. They’re persistent about finishing a task, so they’ll find credentials left in places that look harmless, or reach the cluster through network access or a cloud CLI. That can lead to actions against critical infrastructure that nobody intended, and prompt injection makes it easier to trigger.

You can sandbox the agent with Twingate instead. The agent holds no cluster credential and has no network access to the cluster. Its only path to the API server is through the Twingate Gateway. Authorization happens outside the agent’s environment, so the controls hold even when the agent misbehaves. With Privileged Access for Kubernetes:

This guide sets up the infrastructure needed for two agents at different access levels. One agent can only read. The other can also delete pods, scale deployments, and run kubectl exec.

Prerequisites

Isolate the agent host

The Gateway only controls requests that go through Twingate. It can’t close other paths from the agent host to the cluster. Before you give an agent access, check that its host:

The exact network and IAM configuration depends on your cloud provider.

How it works

When an agent runs a kubectl command, the request is forwarded through the Connector to the Gateway. The Gateway then injects the associated Service Account details necessary for Kubernetes RBAC to authorize the request. The following table shows what is included as the identity:

Key Value
Username Service Key ID
Group Service Account name, for example agent-readonly
Service Account ID
twingate:service-accounts
twingate:authenticated

Kubernetes also adds the system:authenticated group to every authenticated request, so it appears alongside these groups in kubectl auth whoami.

Kubernetes RBAC then authorizes the request against that identity based on Group membership. The agent is not able to modify its injected identity or escalate its permissions because the Gateway injects the identity, outside of the agent’s control.

RBAC should be bound to the Service Account rather than the Service Key to support stable and consistent access control, even when Service Keys rotate. The binding matches on the Service Account name, so renaming a Service Account changes which binding applies. Update the RBAC subjects whenever you rename one. Any existing binding that uses the same group name also applies to the agent, so choose Service Account names that aren’t already used as RBAC subjects in the cluster.

Neither ClusterRole in this guide grants an rbac.authorization.k8s.io verb like bind or escalate. Without one of those verbs, Kubernetes blocks a principal from creating or editing a binding to permissions it doesn’t already hold. Make sure not to grant these verbs. This is the key to preventing agents from escalating their privileges.

Create the Service Accounts

Create one Service Account per access level. This guide uses agent-readonly for the read-only agent and agent-platform for the agent that can modify workloads, matching the Group subjects in the RBAC bindings later in this guide. You can create these with Terraform, as shown below.

locals {
  agents = {
    readonly = "agent-readonly"
    platform = "agent-platform"
  }
}

resource "twingate_service_account" "agent" {
  for_each = local.agents

name = each.value
}

resource "twingate_service_account_key" "agent" {
  for_each = local.agents

service_account_id = twingate_service_account.agent[each.key].id
  name                = "${each.value} Headless Client"
}

output "agent_service_account_keys" {
  value     = { for k, v in twingate_service_account_key.agent : k => v.token }
  sensitive = true
}

Each key’s token attribute is the Service Key JSON document. Save it to a file and pass it to twingate setup --headless on the matching agent host.

A Service Key grants network access through Twingate, not access to the cluster. This is what allows the cluster to be completely isolated from the internet. Secure the key with a secrets manager, restrictive file permissions, and a rotation policy.

You can also create Service Accounts and keys from the Admin Console.

Grant the Service Accounts access to the cluster

The Operator grants access with a TwingateResourceAccess custom resource. Create one per Service Account, referencing the Kubernetes TwingateResource that the Gateway chart created and the Service Account’s ID:

apiVersion: twingate.com/v1beta
kind: TwingateResourceAccess
metadata:
  name: agent-readonly-access
  namespace: <operator-namespace>
spec:
  resourceRef:
    name: <kubernetes-cluster-twingateresource-name>
  principalId: "<agent-readonly-service-account-id>"

Create a second CR for agent-platform, pointing at the same resourceRef and using its own Service Account ID. Use principalId rather than a name-based reference: the ID stays valid if you rename the Service Account later, where a name-based reference would have to resolve again on every reconcile.

Configure Kubernetes RBAC

apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
  name: agent-reader
rules:
  - apiGroups: [""]
    resources: ["pods", "pods/log", "services", "namespaces"]
    verbs: ["get", "list", "watch"]
  - apiGroups: ["apps"]
    resources: ["deployments", "replicasets"]
    verbs: ["get", "list", "watch"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
  name: agent-operator
rules:
  - apiGroups: [""]
    resources: ["pods", "pods/log", "services"]
    verbs: ["get", "list", "watch"]
  - apiGroups: ["apps"]
    resources: ["deployments", "replicasets"]
    verbs: ["get", "list", "watch"]
  - apiGroups: [""]
    resources: ["pods"]
    verbs: ["delete"]
  - apiGroups: ["apps"]
    resources: ["deployments/scale"]
    verbs: ["get", "patch", "update"]
  - apiGroups: [""]
    resources: ["pods/exec"]
    verbs: ["create", "get"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
  name: agent-readonly-binding
subjects:
  - kind: Group
    name: agent-readonly
    apiGroup: rbac.authorization.k8s.io
roleRef:
  kind: ClusterRole
  name: agent-reader
  apiGroup: rbac.authorization.k8s.io
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
  name: agent-platform-binding
  namespace: <workload-namespace>
subjects:
  - kind: Group
    name: agent-platform
    apiGroup: rbac.authorization.k8s.io
roleRef:
  kind: ClusterRole
  name: agent-operator
  apiGroup: rbac.authorization.k8s.io

The read-only agent is bound cluster-wide with a ClusterRoleBinding. The elevated agent is bound with a RoleBinding, so its permissions apply only in <workload-namespace>. Create one RoleBinding per namespace the agent needs to operate in.

Configure the agent hosts

On each agent host, install and configure the Twingate Client in headless mode with that host’s Service Key:

sudo twingate setup --headless /path/to/service_key.json
sudo twingate start
twingate status

Then, as the agent’s own user, sync the kubeconfig and select the context:

mkdir -p ~/.kube
twingate kube config sync
kubectl config get-contexts
kubectl config use-context <context>

twingate kube config sync writes a context for the Kubernetes Resource but doesn’t set it as current, so kubectl falls back to localhost:8080 until you select the context explicitly. The synced kubeconfig has no embedded credential: its token field is the literal placeholder void, because the cluster authenticates the connection through Twingate rather than through anything in the file.

Verify access

Confirm the identity Twingate asserts on each agent host

kubectl auth whoami

The Groups list includes agent-readonly on the read-only agent and agent-platform on the elevated agent.

Read-only agent

Run the following commands on the agent host to verify the read-only agent’s access.

Read pods in the workload namespace

kubectl get pods -n <workload-namespace>

Succeeds and lists the pods in the namespace.

Try to delete a pod

kubectl delete pod <pod> -n <workload-namespace> --dry-run=server

Fails with a Forbidden error.

List the agent’s permissions

kubectl auth can-i --list

Lists the permissions granted by agent-reader.

Elevated agent

Run the following commands on the agent host to verify the elevated agent’s access.

Delete a pod in the workload namespace

kubectl delete pod <pod> -n <workload-namespace> --dry-run=server

Succeeds. The --dry-run=server flag checks the request without deleting the pod.

Try to delete a pod outside the workload namespace

kubectl delete pod <pod> -n <operator-namespace> --dry-run=server

Fails with a Forbidden error, because the elevated agent has no RoleBinding in that namespace.

Try to grant the agent cluster-admin

kubectl create clusterrolebinding self-admin --clusterrole=cluster-admin --group=agent-platform --dry-run=server

Fails with a Forbidden error. The agent can’t grant itself more permissions.

Check whether the agent can create bindings

kubectl auth can-i create clusterrolebindings

Returns no.

The Gateway logs every API call from both agents to stdout on your own infrastructure, attributed to the Service Key ID. It also records kubectl exec sessions as recordings you can replay.

Revoke agent access

Twingate offers two ways to cut an agent off, with different scopes. Both act before RBAC is evaluated, so they apply regardless of what the agent’s bindings allow, and neither affects other Service Accounts.

For related setup steps, see:

→ Kubernetes Kubeconfig Sync

→ How Service Accounts Work

→ Linux Headless Client

Last updated 6 days ago