diff --git a/plugin/pkg/auth/authorizer/rbac/bootstrappolicy/controller_policy.go b/plugin/pkg/auth/authorizer/rbac/bootstrappolicy/controller_policy.go index b92483e5a04..21045024f95 100644 --- a/plugin/pkg/auth/authorizer/rbac/bootstrappolicy/controller_policy.go +++ b/plugin/pkg/auth/authorizer/rbac/bootstrappolicy/controller_policy.go @@ -58,6 +58,7 @@ func init() { addControllerRole(rbac.ClusterRole{ ObjectMeta: api.ObjectMeta{Name: saRolePrefix + "replication-controller"}, Rules: []rbac.PolicyRule{ + // 1.0 controllers needed get, update, so without these old controllers break on new servers rbac.NewRule("get", "list", "watch", "update").Groups(legacyGroup).Resources("replicationcontrollers").RuleOrDie(), rbac.NewRule("update").Groups(legacyGroup).Resources("replicationcontrollers/status").RuleOrDie(), rbac.NewRule("list", "watch", "create", "delete").Groups(legacyGroup).Resources("pods").RuleOrDie(), diff --git a/plugin/pkg/auth/authorizer/rbac/bootstrappolicy/policy.go b/plugin/pkg/auth/authorizer/rbac/bootstrappolicy/policy.go index a8b3084bb44..89bde5eebb4 100644 --- a/plugin/pkg/auth/authorizer/rbac/bootstrappolicy/policy.go +++ b/plugin/pkg/auth/authorizer/rbac/bootstrappolicy/policy.go @@ -23,10 +23,22 @@ import ( ) var ( - readWrite = []string{"get", "list", "watch", "create", "update", "patch", "delete", "deletecollection"} - read = []string{"get", "list", "watch"} + ReadWrite = []string{"get", "list", "watch", "create", "update", "patch", "delete", "deletecollection"} + Read = []string{"get", "list", "watch"} +) - legacyGroup = "" +const ( + legacyGroup = "" + appsGroup = "apps" + authenticationGroup = "authentication.k8s.io" + authorizationGroup = "authorization.k8s.io" + autoscalingGroup = "autoscaling" + batchGroup = "batch" + certificatesGroup = "certificates.k8s.io" + extensionsGroup = "extensions" + policyGroup = "policy" + rbacGroup = "rbac.authorization.k8s.io" + storageGroup = "storage.k8s.io" ) // ClusterRoles returns the cluster roles to bootstrap an API server with @@ -47,6 +59,92 @@ func ClusterRoles() []rbac.ClusterRole { rbac.NewRule("get").URLs("/version", "/api", "/api/*", "/apis", "/apis/*").RuleOrDie(), }, }, + { + // a role which provides minimal resource access to allow a "normal" user to learn information about themselves + ObjectMeta: api.ObjectMeta{Name: "system:basic-user"}, + Rules: []rbac.PolicyRule{ + // TODO add future selfsubjectrulesreview, project request APIs, project listing APIs + rbac.NewRule("create").Groups(authorizationGroup).Resources("selfsubjectaccessreviews").RuleOrDie(), + }, + }, + + { + // a role for a namespace level admin. It is `edit` plus the power to grant permissions to other users. + ObjectMeta: api.ObjectMeta{Name: "admin"}, + Rules: []rbac.PolicyRule{ + rbac.NewRule(ReadWrite...).Groups(legacyGroup).Resources("pods", "pods/attach", "pods/proxy", "pods/exec", "pods/portforward").RuleOrDie(), + rbac.NewRule(ReadWrite...).Groups(legacyGroup).Resources("replicationcontrollers", "replicationcontrollers/scale", "serviceaccounts", + "services", "services/proxy", "endpoints", "persistentvolumeclaims", "configmaps", "secrets").RuleOrDie(), + rbac.NewRule(Read...).Groups(legacyGroup).Resources("limitranges", "resourcequotas", "bindings", "events", + "pods/status", "resourcequotas/status", "namespaces/status", "replicationcontrollers/status", "pods/log").RuleOrDie(), + // read access to namespaces at the namespace scope means you can read *this* namespace. This can be used as an + // indicator of which namespaces you have access to. + rbac.NewRule(Read...).Groups(legacyGroup).Resources("namespaces").RuleOrDie(), + rbac.NewRule("impersonate").Groups(legacyGroup).Resources("serviceaccounts").RuleOrDie(), + + rbac.NewRule(ReadWrite...).Groups(appsGroup).Resources("petsets").RuleOrDie(), + + rbac.NewRule(ReadWrite...).Groups(autoscalingGroup).Resources("horizontalpodautoscalers").RuleOrDie(), + + rbac.NewRule(ReadWrite...).Groups(batchGroup).Resources("jobs", "scheduledjobs").RuleOrDie(), + + rbac.NewRule(ReadWrite...).Groups(extensionsGroup).Resources("jobs", "daemonsets", "horizontalpodautoscalers", + "replicationcontrollers/scale", "replicasets", "replicasets/scale", "deployments", "deployments/scale").RuleOrDie(), + + // additional admin powers + rbac.NewRule("create").Groups(authorizationGroup).Resources("localsubjectaccessreviews").RuleOrDie(), + rbac.NewRule(ReadWrite...).Groups(rbacGroup).Resources("roles", "rolebindings").RuleOrDie(), + }, + }, + { + // a role for a namespace level editor. It grants access to all user level actions in a namespace. + // It does not grant powers for "privileged" resources which are domain of the system: `/status` + // subresources or `quota`/`limits` which are used to control namespaces + ObjectMeta: api.ObjectMeta{Name: "edit"}, + Rules: []rbac.PolicyRule{ + rbac.NewRule(ReadWrite...).Groups(legacyGroup).Resources("pods", "pods/attach", "pods/proxy", "pods/exec", "pods/portforward").RuleOrDie(), + rbac.NewRule(ReadWrite...).Groups(legacyGroup).Resources("replicationcontrollers", "replicationcontrollers/scale", "serviceaccounts", + "services", "services/proxy", "endpoints", "persistentvolumeclaims", "configmaps", "secrets").RuleOrDie(), + rbac.NewRule(Read...).Groups(legacyGroup).Resources("limitranges", "resourcequotas", "bindings", "events", + "pods/status", "resourcequotas/status", "namespaces/status", "replicationcontrollers/status", "pods/log").RuleOrDie(), + // read access to namespaces at the namespace scope means you can read *this* namespace. This can be used as an + // indicator of which namespaces you have access to. + rbac.NewRule(Read...).Groups(legacyGroup).Resources("namespaces").RuleOrDie(), + rbac.NewRule("impersonate").Groups(legacyGroup).Resources("serviceaccounts").RuleOrDie(), + + rbac.NewRule(ReadWrite...).Groups(appsGroup).Resources("petsets").RuleOrDie(), + + rbac.NewRule(ReadWrite...).Groups(autoscalingGroup).Resources("horizontalpodautoscalers").RuleOrDie(), + + rbac.NewRule(ReadWrite...).Groups(batchGroup).Resources("jobs", "scheduledjobs").RuleOrDie(), + + rbac.NewRule(ReadWrite...).Groups(extensionsGroup).Resources("jobs", "daemonsets", "horizontalpodautoscalers", + "replicationcontrollers/scale", "replicasets", "replicasets/scale", "deployments", "deployments/scale").RuleOrDie(), + }, + }, + { + // a role for namespace level viewing. It grants Read-only access to non-escalating resources in + // a namespace. + ObjectMeta: api.ObjectMeta{Name: "view"}, + Rules: []rbac.PolicyRule{ + rbac.NewRule(Read...).Groups(legacyGroup).Resources("pods", "replicationcontrollers", "replicationcontrollers/scale", "serviceaccounts", + "services", "endpoints", "persistentvolumeclaims", "configmaps").RuleOrDie(), + rbac.NewRule(Read...).Groups(legacyGroup).Resources("limitranges", "resourcequotas", "bindings", "events", + "pods/status", "resourcequotas/status", "namespaces/status", "replicationcontrollers/status", "pods/log").RuleOrDie(), + // read access to namespaces at the namespace scope means you can read *this* namespace. This can be used as an + // indicator of which namespaces you have access to. + rbac.NewRule(Read...).Groups(legacyGroup).Resources("namespaces").RuleOrDie(), + + rbac.NewRule(Read...).Groups(appsGroup).Resources("petsets").RuleOrDie(), + + rbac.NewRule(Read...).Groups(autoscalingGroup).Resources("horizontalpodautoscalers").RuleOrDie(), + + rbac.NewRule(Read...).Groups(batchGroup).Resources("jobs", "scheduledjobs").RuleOrDie(), + + rbac.NewRule(Read...).Groups(extensionsGroup).Resources("jobs", "daemonsets", "horizontalpodautoscalers", + "replicationcontrollers/scale", "replicasets", "replicasets/scale", "deployments", "deployments/scale").RuleOrDie(), + }, + }, } } @@ -55,5 +153,6 @@ func ClusterRoleBindings() []rbac.ClusterRoleBinding { return []rbac.ClusterRoleBinding{ rbac.NewClusterBinding("cluster-admin").Groups(user.SystemPrivilegedGroup).BindingOrDie(), rbac.NewClusterBinding("system:discovery").Groups(user.AllAuthenticated, user.AllUnauthenticated).BindingOrDie(), + rbac.NewClusterBinding("system:basic-user").Groups(user.AllAuthenticated, user.AllUnauthenticated).BindingOrDie(), } } diff --git a/plugin/pkg/auth/authorizer/rbac/bootstrappolicy/policy_test.go b/plugin/pkg/auth/authorizer/rbac/bootstrappolicy/policy_test.go new file mode 100644 index 00000000000..e4b98b9f06d --- /dev/null +++ b/plugin/pkg/auth/authorizer/rbac/bootstrappolicy/policy_test.go @@ -0,0 +1,138 @@ +/* +Copyright 2016 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package bootstrappolicy_test + +import ( + "testing" + + rbac "k8s.io/kubernetes/pkg/apis/rbac" + rbacvalidation "k8s.io/kubernetes/pkg/apis/rbac/validation" + "k8s.io/kubernetes/pkg/util/sets" + "k8s.io/kubernetes/plugin/pkg/auth/authorizer/rbac/bootstrappolicy" +) + +// semanticRoles is a few enumerated roles for which the relationships are well established +// and we want to maintain symmetric roles +type semanticRoles struct { + admin *rbac.ClusterRole + edit *rbac.ClusterRole + view *rbac.ClusterRole +} + +func getSemanticRoles(roles []rbac.ClusterRole) semanticRoles { + ret := semanticRoles{} + for i := range roles { + role := roles[i] + switch role.Name { + case "admin": + ret.admin = &role + case "edit": + ret.edit = &role + case "view": + ret.view = &role + } + } + return ret +} + +// Some roles should always cover others +func TestCovers(t *testing.T) { + semanticRoles := getSemanticRoles(bootstrappolicy.ClusterRoles()) + + if covers, miss := rbacvalidation.Covers(semanticRoles.admin.Rules, semanticRoles.edit.Rules); !covers { + t.Errorf("failed to cover: %#v", miss) + } + if covers, miss := rbacvalidation.Covers(semanticRoles.admin.Rules, semanticRoles.view.Rules); !covers { + t.Errorf("failed to cover: %#v", miss) + } + if covers, miss := rbacvalidation.Covers(semanticRoles.edit.Rules, semanticRoles.view.Rules); !covers { + t.Errorf("failed to cover: %#v", miss) + } +} + +// additionalAdminPowers is the list of powers that we expect to be different than the editor role. +// one resource per rule to make the "does not already contain" check easy +var additionalAdminPowers = []rbac.PolicyRule{ + rbac.NewRule("create").Groups("authorization.k8s.io").Resources("localsubjectaccessreviews").RuleOrDie(), + rbac.NewRule(bootstrappolicy.ReadWrite...).Groups("rbac.authorization.k8s.io").Resources("rolebindings").RuleOrDie(), + rbac.NewRule(bootstrappolicy.ReadWrite...).Groups("rbac.authorization.k8s.io").Resources("roles").RuleOrDie(), +} + +func TestAdminEditRelationship(t *testing.T) { + semanticRoles := getSemanticRoles(bootstrappolicy.ClusterRoles()) + + // confirm that the edit role doesn't already have extra powers + for _, rule := range additionalAdminPowers { + if covers, _ := rbacvalidation.Covers(semanticRoles.edit.Rules, []rbac.PolicyRule{rule}); covers { + t.Errorf("edit has extra powers: %#v", rule) + } + } + semanticRoles.edit.Rules = append(semanticRoles.edit.Rules, additionalAdminPowers...) + + // at this point, we should have a two way covers relationship + if covers, miss := rbacvalidation.Covers(semanticRoles.admin.Rules, semanticRoles.edit.Rules); !covers { + t.Errorf("admin has lost rules for: %#v", miss) + } + if covers, miss := rbacvalidation.Covers(semanticRoles.edit.Rules, semanticRoles.admin.Rules); !covers { + t.Errorf("edit is missing rules for: %#v\nIf these should only be admin powers, add them to the list. Otherwise, add them to the edit role.", miss) + } +} + +// viewEscalatingNamespaceResources is the list of rules that would allow privilege escalation attacks based on +// ability to view (GET) them +var viewEscalatingNamespaceResources = []rbac.PolicyRule{ + rbac.NewRule(bootstrappolicy.Read...).Groups("").Resources("pods/attach").RuleOrDie(), + rbac.NewRule(bootstrappolicy.Read...).Groups("").Resources("pods/proxy").RuleOrDie(), + rbac.NewRule(bootstrappolicy.Read...).Groups("").Resources("pods/exec").RuleOrDie(), + rbac.NewRule(bootstrappolicy.Read...).Groups("").Resources("pods/portforward").RuleOrDie(), + rbac.NewRule(bootstrappolicy.Read...).Groups("").Resources("secrets").RuleOrDie(), + rbac.NewRule(bootstrappolicy.Read...).Groups("").Resources("services/proxy").RuleOrDie(), +} + +func TestEditViewRelationship(t *testing.T) { + readVerbs := sets.NewString(bootstrappolicy.Read...) + semanticRoles := getSemanticRoles(bootstrappolicy.ClusterRoles()) + + // modify the edit role rules to make then read-only for comparison against view role rules + for i := range semanticRoles.edit.Rules { + rule := semanticRoles.edit.Rules[i] + remainingVerbs := []string{} + for _, verb := range rule.Verbs { + if readVerbs.Has(verb) { + remainingVerbs = append(remainingVerbs, verb) + } + } + rule.Verbs = remainingVerbs + semanticRoles.edit.Rules[i] = rule + } + + // confirm that the view role doesn't already have extra powers + for _, rule := range viewEscalatingNamespaceResources { + if covers, _ := rbacvalidation.Covers(semanticRoles.view.Rules, []rbac.PolicyRule{rule}); covers { + t.Errorf("view has extra powers: %#v", rule) + } + } + semanticRoles.view.Rules = append(semanticRoles.view.Rules, viewEscalatingNamespaceResources...) + + // at this point, we should have a two way covers relationship + if covers, miss := rbacvalidation.Covers(semanticRoles.edit.Rules, semanticRoles.view.Rules); !covers { + t.Errorf("edit has lost rules for: %#v", miss) + } + if covers, miss := rbacvalidation.Covers(semanticRoles.view.Rules, semanticRoles.edit.Rules); !covers { + t.Errorf("view is missing rules for: %#v\nIf these are escalating powers, add them to the list. Otherwise, add them to the view role.", miss) + } +}