How to manage least-privilege access to Kubernetes namespaces across teams

A practical guide to scoping teams to their own namespaces with IdP-group-bound Roles, avoiding ClusterRoles and broad built-in principals, and cutting standing access with just-in-time grants. See the Twingate Kubernetes documentation for the full reference.
In October 2026, Datadog Security Labs published an analysis of more than 65,000 Kubernetes clusters and found over 3,500 RoleBindings and ClusterRoleBindings handing dangerous permissions to built-in principals like system:authenticated and system:unauthenticated. In plenty of cases, namespace-scoped admins in "namespace as a service" setups had created the bindings themselves, exposing the whole cluster.
That's a governance problem, not a Kubernetes problem. RBAC works fine. The issue is how teams get mapped to it, who's allowed to edit it, and whether anyone is watching.
This post walks through a pattern that holds up at scale: bind namespace-scoped Roles to IdP groups, keep ClusterRoles reserved for the platform team, and make elevated access temporary. Twingate shows up in the middle of this, not to replace RBAC, but to carry the user's real identity into the cluster and keep the API server off the public internet.
Why namespace-scoped RBAC is the baseline
Kubernetes RBAC has two scopes.
Role and RoleBinding are namespace-scoped
ClusterRole and ClusterRoleBinding apply across the entire cluster
The temptation, especially in shared clusters, is to grant a team edit or admin (both built-in ClusterRoles) at the namespace level and move on. Bound with a RoleBinding, that's technically namespace-scoped.
But the Datadog finding is instructive: namespace admins can then create their own bindings inside their namespace, and some of those bindings reference system:authenticated, a group containing every authenticated identity in the cluster.
The result is a lateral path. Team A's namespace admin writes a RoleBinding granting system:authenticated read access to a secret. Team B, authenticated for its own unrelated namespace, can now read it.
The fix has three parts:
Define your own Roles per namespace, scoped to what the team actually needs. Don't reuse
editoradminas a shortcut.Bind Roles to IdP groups, not individual users or service accounts.
Treat the ability to create RoleBindings as a cluster-admin privilege, not a namespace-admin one. That means no team Role grants write access to
rolesorrolebindings, or thebindandescalateverbs.
The pattern
Here's the model this post builds toward:
Layer | Who manages it | What it controls |
|---|---|---|
IdP groups | Identity team | Who is in which team (e.g., |
Namespace Roles | Platform team | What actions are allowed inside a namespace |
RoleBindings | Platform team (via GitOps) | Which IdP group gets which Role in which namespace |
Network access and identity | Twingate | Who can reach the cluster at all, from which devices, and which identity arrives at the API server |
Elevated access | Twingate (just-in-time) | Short-lived membership in higher-privilege groups |
The platform team owns the RBAC surface. Development teams don't write their own bindings. The API server isn't on the public internet. And when someone needs break-glass access, they request it, get it for an hour, and it disappears.
Step 1: Model teams as IdP groups
Start in your identity provider. For each team, create a group per environment and sensitivity level:
k8s-payments-devk8s-payments-stagingk8s-payments-prod-readonlyk8s-payments-prod-oncall(used for just-in-time elevation)
Keep the naming consistent. The group name is going to show up in RoleBindings, audit logs, and access request flows. If the mapping between group and namespace is obvious, nobody has to look it up.
Step 2: Write Roles, not ClusterRoles
For each namespace, define the minimum set of verbs and resources the team actually needs. Here's an example Role for a dev namespace that lets the team deploy and debug without direct access to secrets or RBAC objects. Each rule covers one API group, so the Role grants exactly what it lists.
apiVersion: rbac.authorization.k8s.io/v1 kind: Role metadata: namespace: payments-dev name: payments-dev-developer rules: # Core workloads and config - apiGroups: [""] resources: ["pods", "services", "configmaps"] verbs: ["get", "list", "watch", "create", "update", "patch", "delete"] # Read-only debugging - apiGroups: [""] resources: ["pods/log", "events"] verbs: ["get", "list", "watch"] # Interactive debugging (create for SPDY, get for WebSocket clients) - apiGroups: [""] resources: ["pods/exec", "pods/portforward"] verbs: ["get", "create"] - apiGroups: ["apps"] resources: ["deployments", "replicasets", "statefulsets"] verbs: ["get", "list", "watch", "create", "update", "patch", "delete"] - apiGroups: ["apps"] resources: ["deployments/scale"] verbs: ["get", "update", "patch"] - apiGroups: ["batch"] resources: ["jobs", "cronjobs"] verbs: ["get", "list", "watch", "create", "update", "patch", "delete"]
Notice what's missing: secrets, serviceaccounts, roles, rolebindings. The team can't create a binding; only the platform team can.
One caveat matters here. Leaving secrets out of a Role blocks the secrets API, not the secrets themselves. Anyone who can create pods or exec into them can read whatever those pods mount, and can run a pod as any service account in the namespace. So keep production credentials out of dev namespaces, and treat create on workloads and pods/exec as access to the namespace's secrets when you review a Role.
Then bind the Role to the IdP group:
apiVersion: rbac.authorization.k8s.io/v1 kind: RoleBinding metadata: namespace: payments-dev name: payments-dev-developer subjects: - kind: Group name: k8s-payments-dev apiGroup: rbac.authorization.k8s.io roleRef: kind: Role name: payments-dev-developer apiGroup: rbac.authorization.k8s.io
The name: k8s-payments-dev has to match exactly what the API server sees as the user's group. If your cluster adds a prefix to groups from your IdP (for example, an OIDC groups prefix that turns it into oidc:k8s-payments-dev), the subject needs that prefix too, or the binding silently matches no one. Which brings us to the authentication problem.
Step 3: Get the user's IdP identity to the API server
Kubernetes RBAC authorizes based on the user and groups presented by the authentication layer. If every developer connects as the same service account (or worse, with a shared kubeconfig and a long-lived token), the groups in your RoleBindings never match the real user, and your audit log is useless.
You need the user's actual IdP identity to reach the API server. There are a few ways to do this:
OIDC authentication configured on the API server, with kubectl using an OIDC token.
A reverse proxy or identity-aware gateway in front of the API server that passes the user's identity through.
Twingate's Kubernetes support, which brokers the connection and passes the authenticated user's identity through to the cluster.
With Twingate, the recommended setup is the Twingate Kubernetes Operator. Each cluster then appears in the Admin Console as a Resource, and two components do the work inside your environment. A Twingate Connector provides the private network path. The Twingate Gateway, an open-source reverse proxy that runs in your environment rather than Twingate's, sits in front of the API server and propagates the user's identity.
The user authenticates against your IdP through the Twingate Client, which also keeps their kubeconfig in sync. The Gateway presents that identity to the API server, and Kubernetes RBAC does what it was designed to do: check the user's groups against RoleBindings. Users don't need separate Kubernetes credentials.
The authorization decision stays in Kubernetes. Twingate handles authentication and the network path.
Step 4: Keep the API server off the public internet
A surprising number of production clusters still have publicly reachable API servers behind IP allowlists or bearer tokens. Allowlists drift. Tokens leak. And a publicly reachable control plane is a scannable target.
Twingate Connectors deploy inside the cluster (via the Helm chart or the Operator) or in the same VPC. All Connector traffic is outbound-only, so you don't open inbound firewall rules to give developers access. Developers run kubectl as usual against the kubeconfig the Twingate Client syncs for them, and that path is only available to authenticated users who match your Twingate policies. Once that's working, disable the API server's public endpoint; Twingate doesn't need it, and leaving it open undoes the point.
This is also where you can scope access by device posture. If you want kubectl access restricted to managed devices only, that policy lives in Twingate, not Kubernetes. The cluster doesn't need to know anything about device identity. It just sees an authenticated user who arrived through an approved path.
Step 5: Replace standing production access with just-in-time grants
Most production incidents don't need every on-call engineer to have standing edit access. They need one engineer, for an hour, when something breaks.
Here's the pattern:
Create an IdP group like
k8s-payments-prod-oncallthat is empty by default.Write a RoleBinding in the production namespace that grants elevated permissions to that group. It can reference a Role you define or a shared ClusterRole; either way, the RoleBinding keeps the grant inside that one namespace. Avoid a ClusterRoleBinding here, since it would apply across the whole cluster.
Configure a just-in-time access request in Twingate so that when an engineer requests production access, they're added to the group for a defined window (one hour, four hours, whatever your policy is) and removed automatically when it expires.
The binding looks like this:
apiVersion: rbac.authorization.k8s.io/v1 kind: RoleBinding metadata: namespace: payments-prod name: payments-prod-oncall subjects: - kind: Group name: k8s-payments-prod-oncall apiGroup: rbac.authorization.k8s.io roleRef: kind: Role name: payments-prod-oncall apiGroup: rbac.authorization.k8s.io
Just-in-time and ephemeral access are available on Twingate's Business plan and above.
The audit trail is split by design. Twingate logs who requested access and who approved it. The Twingate Gateway logs every command run on the cluster, attributed to the user, and records sessions for replay; those logs are written in your own infrastructure, not uploaded to Twingate, so you ship them to your SIEM or storage. The Kubernetes audit log adds the API server's view. All three are tied together by the user's real identity.
Standing production access becomes the exception, not the default. And when something goes wrong, the logs show a specific person during a specific window, not a shared service account.
What the Datadog finding actually teaches
The 3,500 dangerous bindings aren't the result of RBAC being hard. They're the result of three decisions that drift in opposite directions:
Letting namespace users create their own bindings (because it's convenient)
Reusing built-in principals like
system:authenticated(because they're already there)Not enforcing any review process on binding changes (because nobody set one up)
The pattern in this post pushes back on all three. RoleBindings live in Git, reviewed by the platform team. Teams get the Roles they need, scoped to their namespaces, and reviewers read those Roles for what they really allow, including the secrets that workload and exec permissions reach. The authority to create bindings is a platform-level privilege. And the user hitting the API server is always a real person, carried in from your IdP.
Twingate doesn't do authorization inside the cluster. Kubernetes does. What Twingate does is make sure the right identity shows up at the API server, that the API server isn't sitting on the public internet, and that elevated access has a clock on it.
Closing
For the full setup, including deploying the Twingate Kubernetes Operator, configuring the Connector (version 1.82.0 or later) and the Gateway, and syncing kubeconfigs to your users, see the Twingate Kubernetes documentation and the just-in-time access documentation.
New to Twingate? You can use Twingate for free for up to 5 users, request a personalized demo, or reach out to the team over on the Twingate subreddit.
Rapidly implement a modern Zero Trust network that is more secure and maintainable than VPNs.
How to manage least-privilege access to Kubernetes namespaces across teams

A practical guide to scoping teams to their own namespaces with IdP-group-bound Roles, avoiding ClusterRoles and broad built-in principals, and cutting standing access with just-in-time grants. See the Twingate Kubernetes documentation for the full reference.
In October 2026, Datadog Security Labs published an analysis of more than 65,000 Kubernetes clusters and found over 3,500 RoleBindings and ClusterRoleBindings handing dangerous permissions to built-in principals like system:authenticated and system:unauthenticated. In plenty of cases, namespace-scoped admins in "namespace as a service" setups had created the bindings themselves, exposing the whole cluster.
That's a governance problem, not a Kubernetes problem. RBAC works fine. The issue is how teams get mapped to it, who's allowed to edit it, and whether anyone is watching.
This post walks through a pattern that holds up at scale: bind namespace-scoped Roles to IdP groups, keep ClusterRoles reserved for the platform team, and make elevated access temporary. Twingate shows up in the middle of this, not to replace RBAC, but to carry the user's real identity into the cluster and keep the API server off the public internet.
Why namespace-scoped RBAC is the baseline
Kubernetes RBAC has two scopes.
Role and RoleBinding are namespace-scoped
ClusterRole and ClusterRoleBinding apply across the entire cluster
The temptation, especially in shared clusters, is to grant a team edit or admin (both built-in ClusterRoles) at the namespace level and move on. Bound with a RoleBinding, that's technically namespace-scoped.
But the Datadog finding is instructive: namespace admins can then create their own bindings inside their namespace, and some of those bindings reference system:authenticated, a group containing every authenticated identity in the cluster.
The result is a lateral path. Team A's namespace admin writes a RoleBinding granting system:authenticated read access to a secret. Team B, authenticated for its own unrelated namespace, can now read it.
The fix has three parts:
Define your own Roles per namespace, scoped to what the team actually needs. Don't reuse
editoradminas a shortcut.Bind Roles to IdP groups, not individual users or service accounts.
Treat the ability to create RoleBindings as a cluster-admin privilege, not a namespace-admin one. That means no team Role grants write access to
rolesorrolebindings, or thebindandescalateverbs.
The pattern
Here's the model this post builds toward:
Layer | Who manages it | What it controls |
|---|---|---|
IdP groups | Identity team | Who is in which team (e.g., |
Namespace Roles | Platform team | What actions are allowed inside a namespace |
RoleBindings | Platform team (via GitOps) | Which IdP group gets which Role in which namespace |
Network access and identity | Twingate | Who can reach the cluster at all, from which devices, and which identity arrives at the API server |
Elevated access | Twingate (just-in-time) | Short-lived membership in higher-privilege groups |
The platform team owns the RBAC surface. Development teams don't write their own bindings. The API server isn't on the public internet. And when someone needs break-glass access, they request it, get it for an hour, and it disappears.
Step 1: Model teams as IdP groups
Start in your identity provider. For each team, create a group per environment and sensitivity level:
k8s-payments-devk8s-payments-stagingk8s-payments-prod-readonlyk8s-payments-prod-oncall(used for just-in-time elevation)
Keep the naming consistent. The group name is going to show up in RoleBindings, audit logs, and access request flows. If the mapping between group and namespace is obvious, nobody has to look it up.
Step 2: Write Roles, not ClusterRoles
For each namespace, define the minimum set of verbs and resources the team actually needs. Here's an example Role for a dev namespace that lets the team deploy and debug without direct access to secrets or RBAC objects. Each rule covers one API group, so the Role grants exactly what it lists.
apiVersion: rbac.authorization.k8s.io/v1 kind: Role metadata: namespace: payments-dev name: payments-dev-developer rules: # Core workloads and config - apiGroups: [""] resources: ["pods", "services", "configmaps"] verbs: ["get", "list", "watch", "create", "update", "patch", "delete"] # Read-only debugging - apiGroups: [""] resources: ["pods/log", "events"] verbs: ["get", "list", "watch"] # Interactive debugging (create for SPDY, get for WebSocket clients) - apiGroups: [""] resources: ["pods/exec", "pods/portforward"] verbs: ["get", "create"] - apiGroups: ["apps"] resources: ["deployments", "replicasets", "statefulsets"] verbs: ["get", "list", "watch", "create", "update", "patch", "delete"] - apiGroups: ["apps"] resources: ["deployments/scale"] verbs: ["get", "update", "patch"] - apiGroups: ["batch"] resources: ["jobs", "cronjobs"] verbs: ["get", "list", "watch", "create", "update", "patch", "delete"]
Notice what's missing: secrets, serviceaccounts, roles, rolebindings. The team can't create a binding; only the platform team can.
One caveat matters here. Leaving secrets out of a Role blocks the secrets API, not the secrets themselves. Anyone who can create pods or exec into them can read whatever those pods mount, and can run a pod as any service account in the namespace. So keep production credentials out of dev namespaces, and treat create on workloads and pods/exec as access to the namespace's secrets when you review a Role.
Then bind the Role to the IdP group:
apiVersion: rbac.authorization.k8s.io/v1 kind: RoleBinding metadata: namespace: payments-dev name: payments-dev-developer subjects: - kind: Group name: k8s-payments-dev apiGroup: rbac.authorization.k8s.io roleRef: kind: Role name: payments-dev-developer apiGroup: rbac.authorization.k8s.io
The name: k8s-payments-dev has to match exactly what the API server sees as the user's group. If your cluster adds a prefix to groups from your IdP (for example, an OIDC groups prefix that turns it into oidc:k8s-payments-dev), the subject needs that prefix too, or the binding silently matches no one. Which brings us to the authentication problem.
Step 3: Get the user's IdP identity to the API server
Kubernetes RBAC authorizes based on the user and groups presented by the authentication layer. If every developer connects as the same service account (or worse, with a shared kubeconfig and a long-lived token), the groups in your RoleBindings never match the real user, and your audit log is useless.
You need the user's actual IdP identity to reach the API server. There are a few ways to do this:
OIDC authentication configured on the API server, with kubectl using an OIDC token.
A reverse proxy or identity-aware gateway in front of the API server that passes the user's identity through.
Twingate's Kubernetes support, which brokers the connection and passes the authenticated user's identity through to the cluster.
With Twingate, the recommended setup is the Twingate Kubernetes Operator. Each cluster then appears in the Admin Console as a Resource, and two components do the work inside your environment. A Twingate Connector provides the private network path. The Twingate Gateway, an open-source reverse proxy that runs in your environment rather than Twingate's, sits in front of the API server and propagates the user's identity.
The user authenticates against your IdP through the Twingate Client, which also keeps their kubeconfig in sync. The Gateway presents that identity to the API server, and Kubernetes RBAC does what it was designed to do: check the user's groups against RoleBindings. Users don't need separate Kubernetes credentials.
The authorization decision stays in Kubernetes. Twingate handles authentication and the network path.
Step 4: Keep the API server off the public internet
A surprising number of production clusters still have publicly reachable API servers behind IP allowlists or bearer tokens. Allowlists drift. Tokens leak. And a publicly reachable control plane is a scannable target.
Twingate Connectors deploy inside the cluster (via the Helm chart or the Operator) or in the same VPC. All Connector traffic is outbound-only, so you don't open inbound firewall rules to give developers access. Developers run kubectl as usual against the kubeconfig the Twingate Client syncs for them, and that path is only available to authenticated users who match your Twingate policies. Once that's working, disable the API server's public endpoint; Twingate doesn't need it, and leaving it open undoes the point.
This is also where you can scope access by device posture. If you want kubectl access restricted to managed devices only, that policy lives in Twingate, not Kubernetes. The cluster doesn't need to know anything about device identity. It just sees an authenticated user who arrived through an approved path.
Step 5: Replace standing production access with just-in-time grants
Most production incidents don't need every on-call engineer to have standing edit access. They need one engineer, for an hour, when something breaks.
Here's the pattern:
Create an IdP group like
k8s-payments-prod-oncallthat is empty by default.Write a RoleBinding in the production namespace that grants elevated permissions to that group. It can reference a Role you define or a shared ClusterRole; either way, the RoleBinding keeps the grant inside that one namespace. Avoid a ClusterRoleBinding here, since it would apply across the whole cluster.
Configure a just-in-time access request in Twingate so that when an engineer requests production access, they're added to the group for a defined window (one hour, four hours, whatever your policy is) and removed automatically when it expires.
The binding looks like this:
apiVersion: rbac.authorization.k8s.io/v1 kind: RoleBinding metadata: namespace: payments-prod name: payments-prod-oncall subjects: - kind: Group name: k8s-payments-prod-oncall apiGroup: rbac.authorization.k8s.io roleRef: kind: Role name: payments-prod-oncall apiGroup: rbac.authorization.k8s.io
Just-in-time and ephemeral access are available on Twingate's Business plan and above.
The audit trail is split by design. Twingate logs who requested access and who approved it. The Twingate Gateway logs every command run on the cluster, attributed to the user, and records sessions for replay; those logs are written in your own infrastructure, not uploaded to Twingate, so you ship them to your SIEM or storage. The Kubernetes audit log adds the API server's view. All three are tied together by the user's real identity.
Standing production access becomes the exception, not the default. And when something goes wrong, the logs show a specific person during a specific window, not a shared service account.
What the Datadog finding actually teaches
The 3,500 dangerous bindings aren't the result of RBAC being hard. They're the result of three decisions that drift in opposite directions:
Letting namespace users create their own bindings (because it's convenient)
Reusing built-in principals like
system:authenticated(because they're already there)Not enforcing any review process on binding changes (because nobody set one up)
The pattern in this post pushes back on all three. RoleBindings live in Git, reviewed by the platform team. Teams get the Roles they need, scoped to their namespaces, and reviewers read those Roles for what they really allow, including the secrets that workload and exec permissions reach. The authority to create bindings is a platform-level privilege. And the user hitting the API server is always a real person, carried in from your IdP.
Twingate doesn't do authorization inside the cluster. Kubernetes does. What Twingate does is make sure the right identity shows up at the API server, that the API server isn't sitting on the public internet, and that elevated access has a clock on it.
Closing
For the full setup, including deploying the Twingate Kubernetes Operator, configuring the Connector (version 1.82.0 or later) and the Gateway, and syncing kubeconfigs to your users, see the Twingate Kubernetes documentation and the just-in-time access documentation.
New to Twingate? You can use Twingate for free for up to 5 users, request a personalized demo, or reach out to the team over on the Twingate subreddit.
Rapidly implement a modern Zero Trust network that is more secure and maintainable than VPNs.
How to manage least-privilege access to Kubernetes namespaces across teams

A practical guide to scoping teams to their own namespaces with IdP-group-bound Roles, avoiding ClusterRoles and broad built-in principals, and cutting standing access with just-in-time grants. See the Twingate Kubernetes documentation for the full reference.
In October 2026, Datadog Security Labs published an analysis of more than 65,000 Kubernetes clusters and found over 3,500 RoleBindings and ClusterRoleBindings handing dangerous permissions to built-in principals like system:authenticated and system:unauthenticated. In plenty of cases, namespace-scoped admins in "namespace as a service" setups had created the bindings themselves, exposing the whole cluster.
That's a governance problem, not a Kubernetes problem. RBAC works fine. The issue is how teams get mapped to it, who's allowed to edit it, and whether anyone is watching.
This post walks through a pattern that holds up at scale: bind namespace-scoped Roles to IdP groups, keep ClusterRoles reserved for the platform team, and make elevated access temporary. Twingate shows up in the middle of this, not to replace RBAC, but to carry the user's real identity into the cluster and keep the API server off the public internet.
Why namespace-scoped RBAC is the baseline
Kubernetes RBAC has two scopes.
Role and RoleBinding are namespace-scoped
ClusterRole and ClusterRoleBinding apply across the entire cluster
The temptation, especially in shared clusters, is to grant a team edit or admin (both built-in ClusterRoles) at the namespace level and move on. Bound with a RoleBinding, that's technically namespace-scoped.
But the Datadog finding is instructive: namespace admins can then create their own bindings inside their namespace, and some of those bindings reference system:authenticated, a group containing every authenticated identity in the cluster.
The result is a lateral path. Team A's namespace admin writes a RoleBinding granting system:authenticated read access to a secret. Team B, authenticated for its own unrelated namespace, can now read it.
The fix has three parts:
Define your own Roles per namespace, scoped to what the team actually needs. Don't reuse
editoradminas a shortcut.Bind Roles to IdP groups, not individual users or service accounts.
Treat the ability to create RoleBindings as a cluster-admin privilege, not a namespace-admin one. That means no team Role grants write access to
rolesorrolebindings, or thebindandescalateverbs.
The pattern
Here's the model this post builds toward:
Layer | Who manages it | What it controls |
|---|---|---|
IdP groups | Identity team | Who is in which team (e.g., |
Namespace Roles | Platform team | What actions are allowed inside a namespace |
RoleBindings | Platform team (via GitOps) | Which IdP group gets which Role in which namespace |
Network access and identity | Twingate | Who can reach the cluster at all, from which devices, and which identity arrives at the API server |
Elevated access | Twingate (just-in-time) | Short-lived membership in higher-privilege groups |
The platform team owns the RBAC surface. Development teams don't write their own bindings. The API server isn't on the public internet. And when someone needs break-glass access, they request it, get it for an hour, and it disappears.
Step 1: Model teams as IdP groups
Start in your identity provider. For each team, create a group per environment and sensitivity level:
k8s-payments-devk8s-payments-stagingk8s-payments-prod-readonlyk8s-payments-prod-oncall(used for just-in-time elevation)
Keep the naming consistent. The group name is going to show up in RoleBindings, audit logs, and access request flows. If the mapping between group and namespace is obvious, nobody has to look it up.
Step 2: Write Roles, not ClusterRoles
For each namespace, define the minimum set of verbs and resources the team actually needs. Here's an example Role for a dev namespace that lets the team deploy and debug without direct access to secrets or RBAC objects. Each rule covers one API group, so the Role grants exactly what it lists.
apiVersion: rbac.authorization.k8s.io/v1 kind: Role metadata: namespace: payments-dev name: payments-dev-developer rules: # Core workloads and config - apiGroups: [""] resources: ["pods", "services", "configmaps"] verbs: ["get", "list", "watch", "create", "update", "patch", "delete"] # Read-only debugging - apiGroups: [""] resources: ["pods/log", "events"] verbs: ["get", "list", "watch"] # Interactive debugging (create for SPDY, get for WebSocket clients) - apiGroups: [""] resources: ["pods/exec", "pods/portforward"] verbs: ["get", "create"] - apiGroups: ["apps"] resources: ["deployments", "replicasets", "statefulsets"] verbs: ["get", "list", "watch", "create", "update", "patch", "delete"] - apiGroups: ["apps"] resources: ["deployments/scale"] verbs: ["get", "update", "patch"] - apiGroups: ["batch"] resources: ["jobs", "cronjobs"] verbs: ["get", "list", "watch", "create", "update", "patch", "delete"]
Notice what's missing: secrets, serviceaccounts, roles, rolebindings. The team can't create a binding; only the platform team can.
One caveat matters here. Leaving secrets out of a Role blocks the secrets API, not the secrets themselves. Anyone who can create pods or exec into them can read whatever those pods mount, and can run a pod as any service account in the namespace. So keep production credentials out of dev namespaces, and treat create on workloads and pods/exec as access to the namespace's secrets when you review a Role.
Then bind the Role to the IdP group:
apiVersion: rbac.authorization.k8s.io/v1 kind: RoleBinding metadata: namespace: payments-dev name: payments-dev-developer subjects: - kind: Group name: k8s-payments-dev apiGroup: rbac.authorization.k8s.io roleRef: kind: Role name: payments-dev-developer apiGroup: rbac.authorization.k8s.io
The name: k8s-payments-dev has to match exactly what the API server sees as the user's group. If your cluster adds a prefix to groups from your IdP (for example, an OIDC groups prefix that turns it into oidc:k8s-payments-dev), the subject needs that prefix too, or the binding silently matches no one. Which brings us to the authentication problem.
Step 3: Get the user's IdP identity to the API server
Kubernetes RBAC authorizes based on the user and groups presented by the authentication layer. If every developer connects as the same service account (or worse, with a shared kubeconfig and a long-lived token), the groups in your RoleBindings never match the real user, and your audit log is useless.
You need the user's actual IdP identity to reach the API server. There are a few ways to do this:
OIDC authentication configured on the API server, with kubectl using an OIDC token.
A reverse proxy or identity-aware gateway in front of the API server that passes the user's identity through.
Twingate's Kubernetes support, which brokers the connection and passes the authenticated user's identity through to the cluster.
With Twingate, the recommended setup is the Twingate Kubernetes Operator. Each cluster then appears in the Admin Console as a Resource, and two components do the work inside your environment. A Twingate Connector provides the private network path. The Twingate Gateway, an open-source reverse proxy that runs in your environment rather than Twingate's, sits in front of the API server and propagates the user's identity.
The user authenticates against your IdP through the Twingate Client, which also keeps their kubeconfig in sync. The Gateway presents that identity to the API server, and Kubernetes RBAC does what it was designed to do: check the user's groups against RoleBindings. Users don't need separate Kubernetes credentials.
The authorization decision stays in Kubernetes. Twingate handles authentication and the network path.
Step 4: Keep the API server off the public internet
A surprising number of production clusters still have publicly reachable API servers behind IP allowlists or bearer tokens. Allowlists drift. Tokens leak. And a publicly reachable control plane is a scannable target.
Twingate Connectors deploy inside the cluster (via the Helm chart or the Operator) or in the same VPC. All Connector traffic is outbound-only, so you don't open inbound firewall rules to give developers access. Developers run kubectl as usual against the kubeconfig the Twingate Client syncs for them, and that path is only available to authenticated users who match your Twingate policies. Once that's working, disable the API server's public endpoint; Twingate doesn't need it, and leaving it open undoes the point.
This is also where you can scope access by device posture. If you want kubectl access restricted to managed devices only, that policy lives in Twingate, not Kubernetes. The cluster doesn't need to know anything about device identity. It just sees an authenticated user who arrived through an approved path.
Step 5: Replace standing production access with just-in-time grants
Most production incidents don't need every on-call engineer to have standing edit access. They need one engineer, for an hour, when something breaks.
Here's the pattern:
Create an IdP group like
k8s-payments-prod-oncallthat is empty by default.Write a RoleBinding in the production namespace that grants elevated permissions to that group. It can reference a Role you define or a shared ClusterRole; either way, the RoleBinding keeps the grant inside that one namespace. Avoid a ClusterRoleBinding here, since it would apply across the whole cluster.
Configure a just-in-time access request in Twingate so that when an engineer requests production access, they're added to the group for a defined window (one hour, four hours, whatever your policy is) and removed automatically when it expires.
The binding looks like this:
apiVersion: rbac.authorization.k8s.io/v1 kind: RoleBinding metadata: namespace: payments-prod name: payments-prod-oncall subjects: - kind: Group name: k8s-payments-prod-oncall apiGroup: rbac.authorization.k8s.io roleRef: kind: Role name: payments-prod-oncall apiGroup: rbac.authorization.k8s.io
Just-in-time and ephemeral access are available on Twingate's Business plan and above.
The audit trail is split by design. Twingate logs who requested access and who approved it. The Twingate Gateway logs every command run on the cluster, attributed to the user, and records sessions for replay; those logs are written in your own infrastructure, not uploaded to Twingate, so you ship them to your SIEM or storage. The Kubernetes audit log adds the API server's view. All three are tied together by the user's real identity.
Standing production access becomes the exception, not the default. And when something goes wrong, the logs show a specific person during a specific window, not a shared service account.
What the Datadog finding actually teaches
The 3,500 dangerous bindings aren't the result of RBAC being hard. They're the result of three decisions that drift in opposite directions:
Letting namespace users create their own bindings (because it's convenient)
Reusing built-in principals like
system:authenticated(because they're already there)Not enforcing any review process on binding changes (because nobody set one up)
The pattern in this post pushes back on all three. RoleBindings live in Git, reviewed by the platform team. Teams get the Roles they need, scoped to their namespaces, and reviewers read those Roles for what they really allow, including the secrets that workload and exec permissions reach. The authority to create bindings is a platform-level privilege. And the user hitting the API server is always a real person, carried in from your IdP.
Twingate doesn't do authorization inside the cluster. Kubernetes does. What Twingate does is make sure the right identity shows up at the API server, that the API server isn't sitting on the public internet, and that elevated access has a clock on it.
Closing
For the full setup, including deploying the Twingate Kubernetes Operator, configuring the Connector (version 1.82.0 or later) and the Gateway, and syncing kubeconfigs to your users, see the Twingate Kubernetes documentation and the just-in-time access documentation.
New to Twingate? You can use Twingate for free for up to 5 users, request a personalized demo, or reach out to the team over on the Twingate subreddit.
Solutions
Solutions
The VPN replacement your workforce will love.
Solutions