Repository navigation
Multiple Impersonate-Group Not Possible #2474
Description
Describe the bug
The official Kubernetes documentation states that when performing user impersonation, multiple groups can be specified with a request through headers. The guidance is to place each group in an individual header entry, like so:
Impersonate-User: jane.doe@example.com
Impersonate-Group: developers
Impersonate-Group: admins
The challenge with this approach is that Node.js and other JavaScript runtime environments do not support adding identical keys to an object. This breaks the ability to specify multiple user groups through impersonation.
const kc = new k8s.KubeConfig();
kc.loadFromDefault();
const currentUser = kc.getCurrentUser()
const currentCluster = kc.getCurrentCluster()
const results: any = {}
const agent = new https.Agent({
ca: currentCluster?.caData
? Buffer.from(currentCluster.caData, 'base64').toString('utf8')
: undefined,
cert: currentUser?.certData
? Buffer.from(currentUser.certData, 'base64').toString('utf8')
: undefined,
key: currentUser?.keyData
? Buffer.from(currentUser.keyData, 'base64').toString('utf8')
: undefined,
keepAlive: true,
})
const opts = {
method: 'GET',
headers: {
'Impersonate-User': 'acme@example.com',
'Impersonate-Group': 'developer-write',
'Impersonate-Group': 'developer-read',
},
agent: agent,
}
await kc.applyToHTTPSOptions(opts)
const url = `${currentCluster?.server}/api/v1/pods`
try {
const response = await fetch(url, opts as RequestInit)
const body = await response.text()
const podList = JSON.parse(body)
results.pods = {
success: true,
items: podList.items?.map((pod: any) => pod.metadata.name) || [],
rawStatus: response.status,
}
} catch (err: any) {
results.pods = {
success: false,
error: err.message,
stack: err.stack,
}
}This will result in a single Impersonate-Group entry in the request with the value being the last entry of the identical key. In this example, that would be developer-read.
If I print out opts before the request I get this:
opts {
method: 'GET',
headers: {
'Impersonate-User': 'acme@example.com',
'Impersonate-Group': 'developer-read'
},
agent: Agent {
_events: [Object: null prototype] {
free: [Function (anonymous)],
newListener: [Function: maybeEnableKeylog]
},
_eventsCount: 2,
_maxListeners: undefined,
defaultPort: 443,
protocol: 'https:',
options: [Object: null prototype] {
ca: '-----BEGIN CERTIFICATE-----\n' +I'm not really sure what can get done from the SDK's perspective to fix this. Part of the reason I brought this issue to light is to help others who may encounter it. But also to see if we can maybe make some traction from a Kubernetes API perspective to pass in groups differently.
Client Version
Client Version: v1.33.1
Kustomize Version: v5.6.0
Server Version: v1.32.2
In package.json I have 1.3.0 of this SDK.
Server Version
e.g. 1.32.2
To Reproduce
You can create two roles with whatever permissions you want, such as list pods, and create a binding to the role and specify the group. Below is an example:
kubectl create clusterrole developer-read \
--verb=get,list,watch \
--resource=pods,services,configmaps \
--resource=deployments.apps \
--dry-run=client -o yaml | kubectl apply -f - && \
kubectl create clusterrolebinding developer-read-binding \
--clusterrole=developer-read \
--group=developer-read \
--dry-run=client -o yaml | kubectl apply -f - && \
kubectl create clusterrole developer-write \
--verb=create,update,delete,get,list \
--resource=pods,services,configmaps \
--resource=deployments.apps \
--dry-run=client -o yaml | kubectl apply -f - && \
kubectl create clusterrolebinding developer-write-binding \
--clusterrole=developer-write \
--group=developer-write \Try to use add multiple groups to the user impersonation header,
Expected behavior
I expect the same behavior I get with kubectl if I were to use the impersonation flags.
kubectl --as=acme@example.com --as-group=developer-read --as-group=developer-read get pods -n demo
NAME READY STATUS RESTARTS AGE
primary-55c477cf8b-5wwxc 1/1 Running 0 15m
secondary-77cd86bc9-8dhpg 1/1 Running 0 15m
dev-devspace-f8bb6b678-56jvv 2/2 Running 0 14m
cert-manager-cainjector-5c87f4477-g6spl 1/1 Running 0 15m
cert-manager-fd6dcf8cb-nd8fs 1/1 Running 0 15m
cert-manager-webhook-54cd859596-ghqnl 1/1 Running 0 15m
dex-58fd549d7b-h264k 1/1 Running 0 15m
helm-controller-69b7c9dbd7-8brdg 1/1 Running 0 15m
star-6d8c59f7c7-54zgw 1/1 Running 0 15m
ingress-nginx-controller-84cf557d69-bq2cq 1/1 Running 0 15m
kustomize-controller-657f4fdfcd-8z6fs 1/1 Running 0 15m
apple-manager-886567c48-zb8vd 1/1 Running 0 15m
source-controller-5dc6d5d47-xv4b5 1/1 Running 0 15m
zot-568From an SDK perspective, I expect to be able to achieve the same capability.
Example Code
See above.
Environment (please complete the following information):
- OS: Apline, MacOS
- Node.js version 22
- Cloud runtime N/A - Kind, and other k8s environments
Additional context
There are other SDKs that are experiencing this issue, but no solution has presented itself.
- Impersonate-Group header does not support comma-separated format
- Kubernetes API Authentication Proxy Request Headers
- [Response Headers]: set multiple headers of same key in response #771
If I try to pass in the values in a single entry comma-separated, I also get an error. The snipped below is from the Kubernetes Audit log
},
"impersonatedUser": {
"username": "acme@example.com",
"groups": [
"developer-write,developer-read",
"system:authenticated"
]
},
"sourceIPs": [
"10.244.0.23"
],
"userAgent": "node-fetch",
"objectRef": {
"resource": "pods",
"apiVersion": "v1"
},
"responseStatus": {
"metadata": {},
"status": "Failure",
"message": "pods is forbidden: User \"acme@example.com\" cannot list resource \"pods\" in API group \"\" at the cluster scope",
"reason": "Forbidden",
"details": {
"kind": "pods"
},
"code": 403
},But if I only include one of the groups
"impersonatedUser": {
"username": "acme@example.com",
"groups": [
"developer-read",
"system:authenticated"
]
},
"sourceIPs": [
"10.244.0.23"
],
"userAgent": "node-fetch",
"objectRef": {
"resource": "pods",
"apiVersion": "v1"
},
"responseStatus": {
"metadata": {},
"code": 200
},
"responseObject": {
"kind": "PodList",
"apiVersion": "v1",
Activity
cjihrig commentedon Jun 5, 2025
Node.js supports this use case by passing an array of strings. From the Node HTTP docs:
I haven't tested to see if anything needs to be changed in this library, but it should at least be possible in theory.
karl-cardenas-coding commentedon Jun 5, 2025
Hey @cjihrig thanks for replying. Yeah I tried that like so
In the Kubernetes audit logs I see the the two groups combined into a single string
"developer-write,developer-read". Below is the complete entry.{ "kind": "Event", "apiVersion": "audit.k8s.io/v1", "level": "RequestResponse", "auditID": "9078fc81-49d5-49f4-87b0-b412ce716065", "stage": "ResponseComplete", "requestURI": "/api/v1/pods", "verb": "list", "user": { "username": "system:serviceaccount:redacted****", "uid": "7532f6c9-8806-4a6b-ab3e-e3c07c5fa1b6", "groups": [ "system:serviceaccounts", "system:serviceaccounts:demo", "system:authenticated" ], "extra": { "authentication.kubernetes.io/credential-id": [ "JTI=5413f2f0-60c5-4035-8488-a037086b2851" ], "authentication.kubernetes.io/node-name": [ "kind-dev-control-plane" ], "authentication.kubernetes.io/node-uid": [ "c73bbe25-d743-46fb-8f9c-6212885c06b2" ], "authentication.kubernetes.io/pod-name": [ "dev-devspace-f8bb6b678-56jvv" ], "authentication.kubernetes.io/pod-uid": [ "ba8c4d5d-ccaa-477f-9513-f7b877514bbc" ] } }, "impersonatedUser": { "username": "acme@example.com", "groups": [ "developer-write,developer-read", "system:authenticated" ] }, "sourceIPs": [ "10.244.0.23" ], "userAgent": "node-fetch", "objectRef": { "resource": "pods", "apiVersion": "v1" }, "responseStatus": { "metadata": {}, "status": "Failure", "message": "pods is forbidden: User \"acme@example.com\" cannot list resource \"pods\" in API group \"\" at the cluster scope", "reason": "Forbidden", "details": { "kind": "pods" }, "code": 403 }, "responseObject": { "kind": "Status", "apiVersion": "v1", "metadata": {}, "status": "Failure", "message": "pods is forbidden: User \"acme@example.com\" cannot list resource \"pods\" in API group \"\" at the cluster scope", "reason": "Forbidden", "details": { "kind": "pods" }, "code": 403 }, "requestReceivedTimestamp": "2025-06-05T02:47:01.656892Z", "stageTimestamp": "2025-06-05T02:47:01.661320Z", "annotations": { "authorization.k8s.io/decision": "forbid", "authorization.k8s.io/reason": "" } }mstruebing commentedon Jun 5, 2025
I just tried it with plain node 24.1.0 and 22.14.0 because I was curious.
It seems to work without probs so it must be some issue with this library or perhaps the code generator.
karl-cardenas-coding commentedon Jun 5, 2025
Thank you @mstruebing - you nudged me in the right direction. I noticed you used the
httplibrary, whereas I was usingfetch. If I replace the request with the http library it works, and I see the audit log show the groups in a separate entry.The issue probably lies in how fetch generates the Headers object
cjihrig commentedon Jun 7, 2025
I guess we should close this issue as can't/won't fix. As far as I can tell, this is just a fundamental limitation of fetch (it combines multiple values into a single header instead of specifying multiple headers like native Node does).
karl-cardenas-coding commentedon Jun 7, 2025
@cjihrig Yeah that's fair. To help others in the future, is there a place we could document this?
cjihrig commentedon Jun 7, 2025
Maybe in https://github.com/kubernetes-client/javascript?tab=readme-ov-file#known-issues.
karl-cardenas-coding commentedon Jun 7, 2025
Sure. I can open up a PR with the change.