Skip to content

Commit f160356

Browse files
authored
Merge pull request kubernetes#73807 from dekkagaijin/discovery-hardening
harden the default RBAC discovery clusterrolebindings
2 parents e001276 + 9c7d319 commit f160356

File tree

5 files changed

+200
-14
lines changed

5 files changed

+200
-14
lines changed

pkg/registry/rbac/rest/storage_rbac.go

Lines changed: 50 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -114,11 +114,12 @@ func (p RESTStorageProvider) storage(version schema.GroupVersion, apiResourceCon
114114

115115
func (p RESTStorageProvider) PostStartHook() (string, genericapiserver.PostStartHookFunc, error) {
116116
policy := &PolicyData{
117-
ClusterRoles: append(bootstrappolicy.ClusterRoles(), bootstrappolicy.ControllerRoles()...),
118-
ClusterRoleBindings: append(bootstrappolicy.ClusterRoleBindings(), bootstrappolicy.ControllerRoleBindings()...),
119-
Roles: bootstrappolicy.NamespaceRoles(),
120-
RoleBindings: bootstrappolicy.NamespaceRoleBindings(),
121-
ClusterRolesToAggregate: bootstrappolicy.ClusterRolesToAggregate(),
117+
ClusterRoles: append(bootstrappolicy.ClusterRoles(), bootstrappolicy.ControllerRoles()...),
118+
ClusterRoleBindings: append(bootstrappolicy.ClusterRoleBindings(), bootstrappolicy.ControllerRoleBindings()...),
119+
Roles: bootstrappolicy.NamespaceRoles(),
120+
RoleBindings: bootstrappolicy.NamespaceRoleBindings(),
121+
ClusterRolesToAggregate: bootstrappolicy.ClusterRolesToAggregate(),
122+
ClusterRoleBindingsToSplit: bootstrappolicy.ClusterRoleBindingsToSplit(),
122123
}
123124
return PostStartHookName, policy.EnsureRBACPolicy(), nil
124125
}
@@ -130,6 +131,8 @@ type PolicyData struct {
130131
RoleBindings map[string][]rbacapiv1.RoleBinding
131132
// ClusterRolesToAggregate maps from previous clusterrole name to the new clusterrole name
132133
ClusterRolesToAggregate map[string]string
134+
// ClusterRoleBindingsToSplit maps from previous ClusterRoleBinding Name to a template for the new ClusterRoleBinding
135+
ClusterRoleBindingsToSplit map[string]rbacapiv1.ClusterRoleBinding
133136
}
134137

135138
func (p *PolicyData) EnsureRBACPolicy() genericapiserver.PostStartHookFunc {
@@ -166,6 +169,11 @@ func (p *PolicyData) EnsureRBACPolicy() genericapiserver.PostStartHookFunc {
166169
return false, nil
167170
}
168171

172+
if err := primeSplitClusterRoleBindings(p.ClusterRoleBindingsToSplit, clientset); err != nil {
173+
utilruntime.HandleError(fmt.Errorf("unable to prime split ClusterRoleBindings: %v", err))
174+
return false, nil
175+
}
176+
169177
// ensure bootstrap roles are created or reconciled
170178
for _, clusterRole := range p.ClusterRoles {
171179
opts := reconciliation.ReconcileRoleOptions{
@@ -334,3 +342,40 @@ func primeAggregatedClusterRoles(clusterRolesToAggregate map[string]string, clus
334342

335343
return nil
336344
}
345+
346+
// primeSplitClusterRoleBindings ensures the existence of target ClusterRoleBindings
347+
// by copying Subjects, Annotations, and Labels from the specified source
348+
// ClusterRoleBinding, if present.
349+
func primeSplitClusterRoleBindings(clusterRoleBindingToSplit map[string]rbacapiv1.ClusterRoleBinding, clusterRoleBindingClient rbacv1client.ClusterRoleBindingsGetter) error {
350+
for existingBindingName, clusterRoleBindingToCreate := range clusterRoleBindingToSplit {
351+
// If source ClusterRoleBinding does not exist, do nothing.
352+
existingRoleBinding, err := clusterRoleBindingClient.ClusterRoleBindings().Get(existingBindingName, metav1.GetOptions{})
353+
if apierrors.IsNotFound(err) {
354+
continue
355+
}
356+
if err != nil {
357+
return err
358+
}
359+
360+
// If the target ClusterRoleBinding already exists, do nothing.
361+
_, err = clusterRoleBindingClient.ClusterRoleBindings().Get(clusterRoleBindingToCreate.Name, metav1.GetOptions{})
362+
if err == nil {
363+
continue
364+
}
365+
if !apierrors.IsNotFound(err) {
366+
return err
367+
}
368+
369+
// If the source exists, but the target does not,
370+
// copy the subjects, labels, and annotations from the former to create the latter.
371+
klog.V(1).Infof("copying subjects, labels, and annotations from ClusterRoleBinding %q to template %q", existingBindingName, clusterRoleBindingToCreate.Name)
372+
newCRB := clusterRoleBindingToCreate.DeepCopy()
373+
newCRB.Subjects = existingRoleBinding.Subjects
374+
newCRB.Labels = existingRoleBinding.Labels
375+
newCRB.Annotations = existingRoleBinding.Annotations
376+
if _, err := clusterRoleBindingClient.ClusterRoleBindings().Create(newCRB); err != nil && !apierrors.IsAlreadyExists(err) {
377+
return err
378+
}
379+
}
380+
return nil
381+
}

plugin/pkg/auth/authorizer/rbac/bootstrappolicy/policy.go

Lines changed: 25 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -213,7 +213,15 @@ func ClusterRoles() []rbacv1.ClusterRole {
213213
rbacv1helpers.NewRule("create").Groups(authorizationGroup).Resources("selfsubjectaccessreviews", "selfsubjectrulesreviews").RuleOrDie(),
214214
},
215215
},
216-
216+
{
217+
// a role which provides just enough power read insensitive cluster information
218+
ObjectMeta: metav1.ObjectMeta{Name: "system:public-info-viewer"},
219+
Rules: []rbacv1.PolicyRule{
220+
rbacv1helpers.NewRule("get").URLs(
221+
"/healthz", "/version", "/version/",
222+
).RuleOrDie(),
223+
},
224+
},
217225
{
218226
// a role for a namespace level admin. It is `edit` plus the power to grant permissions to other users.
219227
ObjectMeta: metav1.ObjectMeta{Name: "admin"},
@@ -524,8 +532,9 @@ const systemNodeRoleName = "system:node"
524532
func ClusterRoleBindings() []rbacv1.ClusterRoleBinding {
525533
rolebindings := []rbacv1.ClusterRoleBinding{
526534
rbacv1helpers.NewClusterBinding("cluster-admin").Groups(user.SystemPrivilegedGroup).BindingOrDie(),
527-
rbacv1helpers.NewClusterBinding("system:discovery").Groups(user.AllAuthenticated, user.AllUnauthenticated).BindingOrDie(),
528-
rbacv1helpers.NewClusterBinding("system:basic-user").Groups(user.AllAuthenticated, user.AllUnauthenticated).BindingOrDie(),
535+
rbacv1helpers.NewClusterBinding("system:discovery").Groups(user.AllAuthenticated).BindingOrDie(),
536+
rbacv1helpers.NewClusterBinding("system:basic-user").Groups(user.AllAuthenticated).BindingOrDie(),
537+
rbacv1helpers.NewClusterBinding("system:public-info-viewer").Groups(user.AllAuthenticated, user.AllUnauthenticated).BindingOrDie(),
529538
rbacv1helpers.NewClusterBinding("system:node-proxier").Users(user.KubeProxy).BindingOrDie(),
530539
rbacv1helpers.NewClusterBinding("system:kube-controller-manager").Users(user.KubeControllerManager).BindingOrDie(),
531540
rbacv1helpers.NewClusterBinding("system:kube-dns").SAs("kube-system", "kube-dns").BindingOrDie(),
@@ -553,3 +562,16 @@ func ClusterRolesToAggregate() map[string]string {
553562
"view": "system:aggregate-to-view",
554563
}
555564
}
565+
566+
// ClusterRoleBindingsToSplit returns a map of Names of source ClusterRoleBindings
567+
// to copy Subjects, Annotations, and Labels to destination ClusterRoleBinding templates.
568+
func ClusterRoleBindingsToSplit() map[string]rbacv1.ClusterRoleBinding {
569+
bindingsToSplit := map[string]rbacv1.ClusterRoleBinding{}
570+
for _, defaultClusterRoleBinding := range ClusterRoleBindings() {
571+
switch defaultClusterRoleBinding.Name {
572+
case "system:public-info-viewer":
573+
bindingsToSplit["system:discovery"] = defaultClusterRoleBinding
574+
}
575+
}
576+
return bindingsToSplit
577+
}

plugin/pkg/auth/authorizer/rbac/bootstrappolicy/testdata/cluster-role-bindings.yaml

Lines changed: 20 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -51,9 +51,6 @@ items:
5151
- apiGroup: rbac.authorization.k8s.io
5252
kind: Group
5353
name: system:authenticated
54-
- apiGroup: rbac.authorization.k8s.io
55-
kind: Group
56-
name: system:unauthenticated
5754
- apiVersion: rbac.authorization.k8s.io/v1
5855
kind: ClusterRoleBinding
5956
metadata:
@@ -71,9 +68,6 @@ items:
7168
- apiGroup: rbac.authorization.k8s.io
7269
kind: Group
7370
name: system:authenticated
74-
- apiGroup: rbac.authorization.k8s.io
75-
kind: Group
76-
name: system:unauthenticated
7771
- apiVersion: rbac.authorization.k8s.io/v1
7872
kind: ClusterRoleBinding
7973
metadata:
@@ -155,6 +149,26 @@ items:
155149
- apiGroup: rbac.authorization.k8s.io
156150
kind: User
157151
name: system:kube-proxy
152+
- apiVersion: rbac.authorization.k8s.io/v1
153+
kind: ClusterRoleBinding
154+
metadata:
155+
annotations:
156+
rbac.authorization.kubernetes.io/autoupdate: "true"
157+
creationTimestamp: null
158+
labels:
159+
kubernetes.io/bootstrapping: rbac-defaults
160+
name: system:public-info-viewer
161+
roleRef:
162+
apiGroup: rbac.authorization.k8s.io
163+
kind: ClusterRole
164+
name: system:public-info-viewer
165+
subjects:
166+
- apiGroup: rbac.authorization.k8s.io
167+
kind: Group
168+
name: system:authenticated
169+
- apiGroup: rbac.authorization.k8s.io
170+
kind: Group
171+
name: system:unauthenticated
158172
- apiVersion: rbac.authorization.k8s.io/v1
159173
kind: ClusterRoleBinding
160174
metadata:

plugin/pkg/auth/authorizer/rbac/bootstrappolicy/testdata/cluster-roles.yaml

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1127,6 +1127,22 @@ items:
11271127
- create
11281128
- patch
11291129
- update
1130+
- apiVersion: rbac.authorization.k8s.io/v1
1131+
kind: ClusterRole
1132+
metadata:
1133+
annotations:
1134+
rbac.authorization.kubernetes.io/autoupdate: "true"
1135+
creationTimestamp: null
1136+
labels:
1137+
kubernetes.io/bootstrapping: rbac-defaults
1138+
name: system:public-info-viewer
1139+
rules:
1140+
- nonResourceURLs:
1141+
- /healthz
1142+
- /version
1143+
- /version/
1144+
verbs:
1145+
- get
11301146
- apiVersion: rbac.authorization.k8s.io/v1
11311147
kind: ClusterRole
11321148
metadata:

test/integration/auth/rbac_test.go

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import (
2323
"io/ioutil"
2424
"net/http"
2525
"net/http/httputil"
26+
"reflect"
2627
"strings"
2728
"testing"
2829
"time"
@@ -680,3 +681,91 @@ func TestBootstrapping(t *testing.T) {
680681
}
681682
t.Errorf("error bootstrapping roles: %s", string(healthBytes))
682683
}
684+
685+
// TestDiscoveryUpgradeBootstrapping is primarily meant to test the behavior of
686+
// primePublicInfoClusterRoleBinding in storage_rbac.go during cluster upgrades.
687+
func TestDiscoveryUpgradeBootstrapping(t *testing.T) {
688+
var tearDownFn func()
689+
defer func() {
690+
if tearDownFn != nil {
691+
tearDownFn()
692+
}
693+
}()
694+
695+
superUser := "admin/system:masters"
696+
697+
masterConfig := framework.NewIntegrationTestMasterConfig()
698+
masterConfig.GenericConfig.Authorization.Authorizer = newRBACAuthorizer(masterConfig)
699+
masterConfig.GenericConfig.Authentication.Authenticator = bearertoken.New(tokenfile.New(map[string]*user.DefaultInfo{
700+
superUser: {Name: "admin", Groups: []string{"system:masters"}},
701+
}))
702+
_, s, tearDownFn := framework.RunAMaster(masterConfig)
703+
704+
client := clientset.NewForConfigOrDie(&restclient.Config{BearerToken: superUser, Host: s.URL, ContentConfig: restclient.ContentConfig{GroupVersion: testapi.Groups[api.GroupName].GroupVersion()}})
705+
706+
// Modify the default RBAC discovery ClusterRoleBidnings to look more like the defaults that
707+
// existed prior to v1.14, but with user modifications.
708+
t.Logf("Modifying default `system:discovery` ClusterRoleBinding")
709+
discRoleBinding, err := client.Rbac().ClusterRoleBindings().Get("system:discovery", metav1.GetOptions{})
710+
discRoleBinding.Annotations["rbac.authorization.kubernetes.io/autoupdate"] = "false"
711+
discRoleBinding.Annotations["rbac-discovery-upgrade-test"] = "pass"
712+
discRoleBinding.Subjects = []rbacapi.Subject{
713+
{
714+
Name: "system:authenticated",
715+
Kind: "Group",
716+
APIGroup: "rbac.authorization.k8s.io",
717+
},
718+
}
719+
if discRoleBinding, err = client.Rbac().ClusterRoleBindings().Update(discRoleBinding); err != nil {
720+
t.Fatalf("Failed to update `system:discovery` ClusterRoleBinding: %v", err)
721+
}
722+
t.Logf("Modifying default `system:basic-user` ClusterRoleBinding")
723+
basicUserRoleBinding, err := client.Rbac().ClusterRoleBindings().Get("system:basic-user", metav1.GetOptions{})
724+
basicUserRoleBinding.Annotations["rbac.authorization.kubernetes.io/autoupdate"] = "false"
725+
basicUserRoleBinding.Annotations["rbac-discovery-upgrade-test"] = "pass"
726+
if basicUserRoleBinding, err = client.Rbac().ClusterRoleBindings().Update(basicUserRoleBinding); err != nil {
727+
t.Fatalf("Failed to update `system:basic-user` ClusterRoleBinding: %v", err)
728+
}
729+
t.Logf("Deleting default `system:public-info-viewer` ClusterRoleBinding")
730+
if err = client.Rbac().ClusterRoleBindings().Delete("system:public-info-viewer", &metav1.DeleteOptions{}); err != nil {
731+
t.Fatalf("Failed to delete `system:public-info-viewer` ClusterRoleBinding: %v", err)
732+
}
733+
734+
// Stop the first API server.
735+
tearDownFn()
736+
tearDownFn = nil
737+
738+
// Check that upgraded API servers inherit `system:public-info-viewer` settings from
739+
// `system:discovery`, and respect auto-reconciliation annotations.
740+
_, s, tearDownFn = framework.RunAMaster(masterConfig)
741+
742+
client = clientset.NewForConfigOrDie(&restclient.Config{BearerToken: superUser, Host: s.URL, ContentConfig: restclient.ContentConfig{GroupVersion: testapi.Groups[api.GroupName].GroupVersion()}})
743+
744+
newDiscRoleBinding, err := client.Rbac().ClusterRoleBindings().Get("system:discovery", metav1.GetOptions{})
745+
if err != nil {
746+
t.Fatalf("Failed to get `system:discovery` ClusterRoleBinding: %v", err)
747+
}
748+
if !reflect.DeepEqual(newDiscRoleBinding, discRoleBinding) {
749+
t.Errorf("`system:discovery` should have been unmodified. Wanted: %v, got %v", discRoleBinding, newDiscRoleBinding)
750+
}
751+
newBasicUserRoleBinding, err := client.Rbac().ClusterRoleBindings().Get("system:basic-user", metav1.GetOptions{})
752+
if err != nil {
753+
t.Fatalf("Failed to get `system:basic-user` ClusterRoleBinding: %v", err)
754+
}
755+
if !reflect.DeepEqual(newBasicUserRoleBinding, basicUserRoleBinding) {
756+
t.Errorf("`system:basic-user` should have been unmodified. Wanted: %v, got %v", basicUserRoleBinding, newBasicUserRoleBinding)
757+
}
758+
publicInfoViewerRoleBinding, err := client.Rbac().ClusterRoleBindings().Get("system:public-info-viewer", metav1.GetOptions{})
759+
if err != nil {
760+
t.Fatalf("Failed to get `system:public-info-viewer` ClusterRoleBinding: %v", err)
761+
}
762+
if publicInfoViewerRoleBinding.Annotations["rbac.authorization.kubernetes.io/autoupdate"] != "false" {
763+
t.Errorf("publicInfoViewerRoleBinding.Annotations[\"rbac.authorization.kubernetes.io/autoupdate\"] should be %v, got %v", publicInfoViewerRoleBinding.Annotations["rbac.authorization.kubernetes.io/autoupdate"], "false")
764+
}
765+
if publicInfoViewerRoleBinding.Annotations["rbac-discovery-upgrade-test"] != "pass" {
766+
t.Errorf("publicInfoViewerRoleBinding.Annotations[\"rbac-discovery-upgrade-test\"] should be %v, got %v", publicInfoViewerRoleBinding.Annotations["rbac-discovery-upgrade-test"], "pass")
767+
}
768+
if !reflect.DeepEqual(publicInfoViewerRoleBinding.Subjects, newDiscRoleBinding.Subjects) {
769+
t.Errorf("`system:public-info-viewer` should have inherited Subjects from `system:discovery` Wanted: %v, got %v", newDiscRoleBinding.Subjects, publicInfoViewerRoleBinding.Subjects)
770+
}
771+
}

0 commit comments

Comments
 (0)