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-568
From 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",