Add Node authorization mode based on graph of node-related objects

parent fed30040
......@@ -76,6 +76,7 @@ ENABLE_APISERVER_BASIC_AUDIT=${ENABLE_APISERVER_BASIC_AUDIT:-false}
# RBAC Mode options
ALLOW_ANY_TOKEN=${ALLOW_ANY_TOKEN:-false}
ENABLE_RBAC=${ENABLE_RBAC:-false}
AUTHORIZATION_MODE=${AUTHORIZATION_MODE:-""}
KUBECONFIG_TOKEN=${KUBECONFIG_TOKEN:-""}
AUTH_ARGS=${AUTH_ARGS:-""}
......@@ -201,6 +202,8 @@ KUBELET_HOST=${KUBELET_HOST:-"127.0.0.1"}
API_CORS_ALLOWED_ORIGINS=${API_CORS_ALLOWED_ORIGINS:-/127.0.0.1(:[0-9]+)?$,/localhost(:[0-9]+)?$}
KUBELET_PORT=${KUBELET_PORT:-10250}
LOG_LEVEL=${LOG_LEVEL:-3}
# Use to increase verbosity on particular files, e.g. LOG_SPEC=token_controller*=5,other_controller*=4
LOG_SPEC=${LOG_SPEC:-""}
LOG_DIR=${LOG_DIR:-"/tmp"}
CONTAINER_RUNTIME=${CONTAINER_RUNTIME:-"docker"}
CONTAINER_RUNTIME_ENDPOINT=${CONTAINER_RUNTIME_ENDPOINT:-""}
......@@ -435,6 +438,12 @@ function start_apiserver {
if [[ "${ENABLE_RBAC}" = true ]]; then
authorizer_arg="--authorization-mode=RBAC "
fi
if [[ -n "${AUTHORIZATION_MODE}" ]]; then
if [[ "${ENABLE_RBAC}" = true ]]; then
warning "AUTHORIZATION_MODE=$AUTHORIZATION_MODE overrode ENABLE_RBAC=true"
fi
authorizer_arg="--authorization-mode=${AUTHORIZATION_MODE} "
fi
priv_arg=""
if [[ -n "${ALLOW_PRIVILEGED}" ]]; then
priv_arg="--allow-privileged "
......@@ -487,6 +496,7 @@ function start_apiserver {
${CONTROLPLANE_SUDO} "${GO_OUT}/hyperkube" apiserver ${swagger_arg} ${audit_arg} ${anytoken_arg} ${authorizer_arg} ${priv_arg} ${runtime_config}\
${advertise_address} \
--v=${LOG_LEVEL} \
--vmodule="${LOG_SPEC}" \
--cert-dir="${CERT_DIR}" \
--client-ca-file="${CERT_DIR}/client-ca.crt" \
--service-account-key-file="${SERVICE_ACCOUNT_KEY}" \
......@@ -894,7 +904,7 @@ if [[ "${START_MODE}" != "nokubelet" ]]; then
esac
fi
if [[ -n "${PSP_ADMISSION}" && "${ENABLE_RBAC}" = true ]]; then
if [[ -n "${PSP_ADMISSION}" && ("${ENABLE_RBAC}" = true || "${AUTHORIZATION_MODE}" = *RBAC* ) ]]; then
create_psp_policy
fi
......
......@@ -26,10 +26,13 @@ go_library(
deps = [
"//pkg/apis/rbac:go_default_library",
"//pkg/auth/authorizer/abac:go_default_library",
"//pkg/auth/nodeidentifier:go_default_library",
"//pkg/client/informers/informers_generated/internalversion:go_default_library",
"//pkg/client/listers/rbac/internalversion:go_default_library",
"//pkg/kubeapiserver/authorizer/modes:go_default_library",
"//plugin/pkg/auth/authorizer/node:go_default_library",
"//plugin/pkg/auth/authorizer/rbac:go_default_library",
"//plugin/pkg/auth/authorizer/rbac/bootstrappolicy:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/labels:go_default_library",
"//vendor/k8s.io/apiserver/pkg/authorization/authorizer:go_default_library",
"//vendor/k8s.io/apiserver/pkg/authorization/authorizerfactory:go_default_library",
......
......@@ -28,10 +28,13 @@ import (
"k8s.io/apiserver/plugin/pkg/authorizer/webhook"
rbacapi "k8s.io/kubernetes/pkg/apis/rbac"
"k8s.io/kubernetes/pkg/auth/authorizer/abac"
"k8s.io/kubernetes/pkg/auth/nodeidentifier"
informers "k8s.io/kubernetes/pkg/client/informers/informers_generated/internalversion"
rbaclisters "k8s.io/kubernetes/pkg/client/listers/rbac/internalversion"
"k8s.io/kubernetes/pkg/kubeapiserver/authorizer/modes"
"k8s.io/kubernetes/plugin/pkg/auth/authorizer/node"
"k8s.io/kubernetes/plugin/pkg/auth/authorizer/rbac"
"k8s.io/kubernetes/plugin/pkg/auth/authorizer/rbac/bootstrappolicy"
)
type AuthorizationConfig struct {
......@@ -107,6 +110,19 @@ func (config AuthorizationConfig) New() (authorizer.Authorizer, error) {
}
// Keep cases in sync with constant list above.
switch authorizationMode {
case modes.ModeNode:
graph := node.NewGraph()
node.AddGraphEventHandlers(
graph,
config.InformerFactory.Core().InternalVersion().Pods(),
config.InformerFactory.Core().InternalVersion().PersistentVolumes(),
)
nodeAuthorizer := node.NewAuthorizer(graph, nodeidentifier.NewDefaultNodeIdentifier(), bootstrappolicy.NodeRules())
authorizers = append(authorizers, nodeAuthorizer)
// Don't bind system:nodes to the system:node role
bootstrappolicy.AddClusterRoleBindingFilter(bootstrappolicy.OmitNodesGroupBinding)
case modes.ModeAlwaysAllow:
authorizers = append(authorizers, authorizerfactory.NewAlwaysAllowAuthorizer())
case modes.ModeAlwaysDeny:
......
......@@ -22,9 +22,10 @@ const (
ModeABAC string = "ABAC"
ModeWebhook string = "Webhook"
ModeRBAC string = "RBAC"
ModeNode string = "Node"
)
var AuthorizationModeChoices = []string{ModeAlwaysAllow, ModeAlwaysDeny, ModeABAC, ModeWebhook, ModeRBAC}
var AuthorizationModeChoices = []string{ModeAlwaysAllow, ModeAlwaysDeny, ModeABAC, ModeWebhook, ModeRBAC, ModeNode}
// IsValidAuthorizationMode returns true if the given authorization mode is a valid one for the apiserver
func IsValidAuthorizationMode(authzMode string) bool {
......
......@@ -24,6 +24,7 @@ filegroup(
name = "all-srcs",
srcs = [
":package-srcs",
"//plugin/pkg/auth/authorizer/node:all-srcs",
"//plugin/pkg/auth/authorizer/rbac:all-srcs",
],
tags = ["automanaged"],
......
package(default_visibility = ["//visibility:public"])
licenses(["notice"])
load(
"@io_bazel_rules_go//go:def.bzl",
"go_library",
"go_test",
)
go_test(
name = "go_default_test",
srcs = ["node_authorizer_test.go"],
library = ":go_default_library",
tags = ["automanaged"],
deps = [
"//pkg/api:go_default_library",
"//pkg/auth/nodeidentifier:go_default_library",
"//plugin/pkg/auth/authorizer/rbac/bootstrappolicy:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/apis/meta/v1:go_default_library",
"//vendor/k8s.io/apiserver/pkg/authentication/user:go_default_library",
"//vendor/k8s.io/apiserver/pkg/authorization/authorizer:go_default_library",
],
)
go_library(
name = "go_default_library",
srcs = [
"graph.go",
"graph_populator.go",
"node_authorizer.go",
],
tags = ["automanaged"],
deps = [
"//pkg/api:go_default_library",
"//pkg/api/persistentvolume:go_default_library",
"//pkg/api/pod:go_default_library",
"//pkg/apis/rbac:go_default_library",
"//pkg/auth/nodeidentifier:go_default_library",
"//pkg/client/informers/informers_generated/internalversion/core/internalversion:go_default_library",
"//plugin/pkg/auth/authorizer/rbac:go_default_library",
"//third_party/forked/gonum/graph:go_default_library",
"//third_party/forked/gonum/graph/simple:go_default_library",
"//third_party/forked/gonum/graph/traverse:go_default_library",
"//vendor/github.com/golang/glog:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/runtime/schema:go_default_library",
"//vendor/k8s.io/apiserver/pkg/authorization/authorizer:go_default_library",
"//vendor/k8s.io/client-go/tools/cache:go_default_library",
],
)
filegroup(
name = "package-srcs",
srcs = glob(["**"]),
tags = ["automanaged"],
visibility = ["//visibility:private"],
)
filegroup(
name = "all-srcs",
srcs = [":package-srcs"],
tags = ["automanaged"],
)
approvers:
- timstclair
- liggitt
- deads2k
reviewers:
- timstclair
- liggitt
- deads2k
- ericchiang
/*
Copyright 2017 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 node
import (
"sync"
"k8s.io/kubernetes/pkg/api"
pvutil "k8s.io/kubernetes/pkg/api/persistentvolume"
podutil "k8s.io/kubernetes/pkg/api/pod"
"k8s.io/kubernetes/third_party/forked/gonum/graph"
"k8s.io/kubernetes/third_party/forked/gonum/graph/simple"
)
// namedVertex implements graph.Node and remembers the type, namespace, and name of its related API object
type namedVertex struct {
name string
namespace string
id int
vertexType vertexType
}
func newNamedVertex(vertexType vertexType, namespace, name string, id int) *namedVertex {
return &namedVertex{
vertexType: vertexType,
name: name,
namespace: namespace,
id: id,
}
}
func (n *namedVertex) ID() int {
return n.id
}
func (n *namedVertex) String() string {
if len(n.namespace) == 0 {
return vertexTypes[n.vertexType] + ":" + n.name
}
return vertexTypes[n.vertexType] + ":" + n.namespace + "/" + n.name
}
// destinationEdge is a graph edge that includes a denormalized reference to the final destination vertex.
// This should only be used when there is a single leaf vertex reachable from T.
type destinationEdge struct {
F graph.Node
T graph.Node
Destination graph.Node
}
func newDestinationEdge(from, to, destination graph.Node) graph.Edge {
return &destinationEdge{F: from, T: to, Destination: destination}
}
func (e *destinationEdge) From() graph.Node { return e.F }
func (e *destinationEdge) To() graph.Node { return e.T }
func (e *destinationEdge) Weight() float64 { return 0 }
func (e *destinationEdge) DestinationID() int { return e.Destination.ID() }
// Graph holds graph vertices and a way to look up a vertex for a particular API type/namespace/name.
// All edges point toward the vertices representing Kubernetes nodes:
//
// node <- pod
// pod <- secret,configmap,pvc
// pvc <- pv
// pv <- secret
type Graph struct {
lock sync.RWMutex
graph *simple.DirectedAcyclicGraph
// vertices is a map of type -> namespace -> name -> vertex
vertices map[vertexType]namespaceVertexMapping
}
// namespaceVertexMapping is a map of namespace -> name -> vertex
type namespaceVertexMapping map[string]nameVertexMapping
// nameVertexMapping is a map of name -> vertex
type nameVertexMapping map[string]*namedVertex
func NewGraph() *Graph {
return &Graph{
vertices: map[vertexType]namespaceVertexMapping{},
graph: simple.NewDirectedAcyclicGraph(0, 0),
}
}
// vertexType indicates the type of the API object the vertex represents.
// represented as a byte to minimize space used in the vertices.
type vertexType byte
const (
configMapVertexType vertexType = iota
nodeVertexType
podVertexType
pvcVertexType
pvVertexType
secretVertexType
)
var vertexTypes = map[vertexType]string{
configMapVertexType: "configmap",
nodeVertexType: "node",
podVertexType: "pod",
pvcVertexType: "pvc",
pvVertexType: "pv",
secretVertexType: "secret",
}
// must be called under a write lock
func (g *Graph) getOrCreateVertex_locked(vertexType vertexType, namespace, name string) *namedVertex {
if vertex, exists := g.getVertex_rlocked(vertexType, namespace, name); exists {
return vertex
}
return g.createVertex_locked(vertexType, namespace, name)
}
// must be called under a read lock
func (g *Graph) getVertex_rlocked(vertexType vertexType, namespace, name string) (*namedVertex, bool) {
vertex, exists := g.vertices[vertexType][namespace][name]
return vertex, exists
}
// must be called under a write lock
func (g *Graph) createVertex_locked(vertexType vertexType, namespace, name string) *namedVertex {
typedVertices, exists := g.vertices[vertexType]
if !exists {
typedVertices = namespaceVertexMapping{}
g.vertices[vertexType] = typedVertices
}
namespacedVertices, exists := typedVertices[namespace]
if !exists {
namespacedVertices = map[string]*namedVertex{}
typedVertices[namespace] = namespacedVertices
}
vertex := newNamedVertex(vertexType, namespace, name, g.graph.NewNodeID())
namespacedVertices[name] = vertex
g.graph.AddNode(vertex)
return vertex
}
// must be called under write lock
func (g *Graph) deleteVertex_locked(vertexType vertexType, namespace, name string) {
vertex, exists := g.getVertex_rlocked(vertexType, namespace, name)
if !exists {
return
}
// find existing neighbors with a single edge (meaning we are their only neighbor)
neighborsToRemove := []graph.Node{}
g.graph.VisitFrom(vertex, func(neighbor graph.Node) bool {
// this downstream neighbor has only one edge (which must be from us), so remove them as well
if g.graph.Degree(neighbor) == 1 {
neighborsToRemove = append(neighborsToRemove, neighbor)
}
return true
})
g.graph.VisitTo(vertex, func(neighbor graph.Node) bool {
// this upstream neighbor has only one edge (which must be to us), so remove them as well
if g.graph.Degree(neighbor) == 1 {
neighborsToRemove = append(neighborsToRemove, neighbor)
}
return true
})
// remove the vertex
g.graph.RemoveNode(vertex)
delete(g.vertices[vertexType][namespace], name)
if len(g.vertices[vertexType][namespace]) == 0 {
delete(g.vertices[vertexType], namespace)
}
// remove neighbors that are now edgeless
for _, neighbor := range neighborsToRemove {
g.graph.RemoveNode(neighbor)
n := neighbor.(*namedVertex)
delete(g.vertices[n.vertexType][n.namespace], n.name)
if len(g.vertices[n.vertexType][n.namespace]) == 0 {
delete(g.vertices[n.vertexType], n.namespace)
}
}
}
// AddPod should only be called once spec.NodeName is populated.
// It sets up edges for the following relationships (which are immutable for a pod once bound to a node):
//
// pod -> node
//
// secret -> pod
// configmap -> pod
// pvc -> pod
func (g *Graph) AddPod(pod *api.Pod) {
g.lock.Lock()
defer g.lock.Unlock()
g.deleteVertex_locked(podVertexType, pod.Namespace, pod.Name)
podVertex := g.getOrCreateVertex_locked(podVertexType, pod.Namespace, pod.Name)
nodeVertex := g.getOrCreateVertex_locked(nodeVertexType, "", pod.Spec.NodeName)
g.graph.SetEdge(newDestinationEdge(podVertex, nodeVertex, nodeVertex))
podutil.VisitPodSecretNames(pod, func(secret string) bool {
g.graph.SetEdge(newDestinationEdge(g.getOrCreateVertex_locked(secretVertexType, pod.Namespace, secret), podVertex, nodeVertex))
return true
})
podutil.VisitPodConfigmapNames(pod, func(configmap string) bool {
g.graph.SetEdge(newDestinationEdge(g.getOrCreateVertex_locked(configMapVertexType, pod.Namespace, configmap), podVertex, nodeVertex))
return true
})
for _, v := range pod.Spec.Volumes {
if v.PersistentVolumeClaim != nil {
g.graph.SetEdge(newDestinationEdge(g.getOrCreateVertex_locked(pvcVertexType, pod.Namespace, v.PersistentVolumeClaim.ClaimName), podVertex, nodeVertex))
}
}
}
func (g *Graph) DeletePod(name, namespace string) {
g.lock.Lock()
defer g.lock.Unlock()
g.deleteVertex_locked(podVertexType, namespace, name)
}
// AddPV sets up edges for the following relationships:
//
// secret -> pv
//
// pv -> pvc
func (g *Graph) AddPV(pv *api.PersistentVolume) {
g.lock.Lock()
defer g.lock.Unlock()
// clear existing edges
g.deleteVertex_locked(pvVertexType, "", pv.Name)
// if we have a pvc, establish new edges
if pv.Spec.ClaimRef != nil {
pvVertex := g.getOrCreateVertex_locked(pvVertexType, "", pv.Name)
// since we don't know the other end of the pvc -> pod -> node chain (or it may not even exist yet), we can't decorate these edges with kubernetes node info
g.graph.SetEdge(simple.Edge{F: pvVertex, T: g.getOrCreateVertex_locked(pvcVertexType, pv.Spec.ClaimRef.Namespace, pv.Spec.ClaimRef.Name)})
pvutil.VisitPVSecretNames(pv, func(secret string) bool {
// This grants access to the named secret in the same namespace as the bound PVC
g.graph.SetEdge(simple.Edge{F: g.getOrCreateVertex_locked(secretVertexType, pv.Spec.ClaimRef.Namespace, secret), T: pvVertex})
return true
})
}
}
func (g *Graph) DeletePV(name string) {
g.lock.Lock()
defer g.lock.Unlock()
g.deleteVertex_locked(pvVertexType, "", name)
}
/*
Copyright 2017 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 node
import (
"github.com/golang/glog"
"k8s.io/client-go/tools/cache"
"k8s.io/kubernetes/pkg/api"
coreinformers "k8s.io/kubernetes/pkg/client/informers/informers_generated/internalversion/core/internalversion"
)
type graphPopulator struct {
graph *Graph
}
func AddGraphEventHandlers(graph *Graph, pods coreinformers.PodInformer, pvs coreinformers.PersistentVolumeInformer) {
g := &graphPopulator{
graph: graph,
}
pods.Informer().AddEventHandler(cache.ResourceEventHandlerFuncs{
AddFunc: g.addPod,
UpdateFunc: g.updatePod,
DeleteFunc: g.deletePod,
})
pvs.Informer().AddEventHandler(cache.ResourceEventHandlerFuncs{
AddFunc: g.addPV,
UpdateFunc: g.updatePV,
DeleteFunc: g.deletePV,
})
}
func (g *graphPopulator) addPod(obj interface{}) {
g.updatePod(nil, obj)
}
func (g *graphPopulator) updatePod(oldObj, obj interface{}) {
pod := obj.(*api.Pod)
if len(pod.Spec.NodeName) == 0 {
// No node assigned
glog.V(5).Infof("updatePod %s/%s, no node", pod.Namespace, pod.Name)
return
}
if oldPod, ok := oldObj.(*api.Pod); ok && oldPod != nil {
if (pod.Spec.NodeName == oldPod.Spec.NodeName) && (pod.UID == oldPod.UID) {
// Node and uid are unchanged, all object references in the pod spec are immutable
glog.V(5).Infof("updatePod %s/%s, node unchanged", pod.Namespace, pod.Name)
return
}
}
glog.V(4).Infof("updatePod %s/%s for node %s", pod.Namespace, pod.Name, pod.Spec.NodeName)
g.graph.AddPod(pod)
}
func (g *graphPopulator) deletePod(obj interface{}) {
if tombstone, ok := obj.(cache.DeletedFinalStateUnknown); ok {
obj = tombstone.Obj
}
pod, ok := obj.(*api.Pod)
if !ok {
glog.Infof("unexpected type %T", obj)
return
}
if len(pod.Spec.NodeName) == 0 {
glog.V(5).Infof("deletePod %s/%s, no node", pod.Namespace, pod.Name)
return
}
glog.V(4).Infof("deletePod %s/%s for node %s", pod.Namespace, pod.Name, pod.Spec.NodeName)
g.graph.DeletePod(pod.Name, pod.Namespace)
}
func (g *graphPopulator) addPV(obj interface{}) {
g.updatePV(nil, obj)
}
func (g *graphPopulator) updatePV(oldObj, obj interface{}) {
pv := obj.(*api.PersistentVolume)
// TODO: skip add if uid, pvc, and secrets are all identical between old and new
g.graph.AddPV(pv)
}
func (g *graphPopulator) deletePV(obj interface{}) {
if tombstone, ok := obj.(cache.DeletedFinalStateUnknown); ok {
obj = tombstone.Obj
}
pv, ok := obj.(*api.PersistentVolume)
if !ok {
glog.Infof("unexpected type %T", obj)
return
}
g.graph.DeletePV(pv.Name)
}
/*
Copyright 2017 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 node
import (
"fmt"
"github.com/golang/glog"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apiserver/pkg/authorization/authorizer"
"k8s.io/kubernetes/pkg/api"
rbacapi "k8s.io/kubernetes/pkg/apis/rbac"
"k8s.io/kubernetes/pkg/auth/nodeidentifier"
"k8s.io/kubernetes/plugin/pkg/auth/authorizer/rbac"
"k8s.io/kubernetes/third_party/forked/gonum/graph"
"k8s.io/kubernetes/third_party/forked/gonum/graph/traverse"
)
// NodeAuthorizer authorizes requests from kubelets, with the following logic:
// 1. If a request is not from a node (IdentifyNode() returns isNode=false), reject
// 2. If a specific node cannot be identified (IdentifyNode() returns nodeName=""), reject
// 3. If a request is for a secret, configmap, persistent volume or persistent volume claim, reject unless the verb is get, and the requested object is related to the requesting node:
// node <- pod
// node <- pod <- secret
// node <- pod <- configmap
// node <- pod <- pvc
// node <- pod <- pvc <- pv
// node <- pod <- pvc <- pv <- secret
// 4. For other resources, authorize all nodes uniformly using statically defined rules
type NodeAuthorizer struct {
graph *Graph
identifier nodeidentifier.NodeIdentifier
nodeRules []rbacapi.PolicyRule
}
// New returns a new node authorizer
func NewAuthorizer(graph *Graph, identifier nodeidentifier.NodeIdentifier, rules []rbacapi.PolicyRule) authorizer.Authorizer {
return &NodeAuthorizer{
graph: graph,
identifier: identifier,
nodeRules: rules,
}
}
var (
configMapResource = api.Resource("configmaps")
secretResource = api.Resource("secrets")
pvcResource = api.Resource("persistentvolumeclaims")
pvResource = api.Resource("persistentvolumes")
)
func (r *NodeAuthorizer) Authorize(attrs authorizer.Attributes) (bool, string, error) {
nodeName, isNode := r.identifier.NodeIdentity(attrs.GetUser())
if !isNode {
// reject requests from non-nodes
return false, "", nil
}
if len(nodeName) == 0 {
// reject requests from unidentifiable nodes
glog.V(2).Infof("NODE DENY: unknown node for user %q", attrs.GetUser().GetName())
return false, fmt.Sprintf("unknown node for user %q", attrs.GetUser().GetName()), nil
}
// subdivide access to specific resources
if attrs.IsResourceRequest() {
requestResource := schema.GroupResource{Group: attrs.GetAPIGroup(), Resource: attrs.GetResource()}
switch requestResource {
case secretResource:
return r.authorizeGet(nodeName, secretVertexType, attrs)
case configMapResource:
return r.authorizeGet(nodeName, configMapVertexType, attrs)
case pvcResource:
return r.authorizeGet(nodeName, pvcVertexType, attrs)
case pvResource:
return r.authorizeGet(nodeName, pvVertexType, attrs)
}
}
// Access to other resources is not subdivided, so just evaluate against the statically defined node rules
return rbac.RulesAllow(attrs, r.nodeRules...), "", nil
}
// authorizeGet authorizes "get" requests to objects of the specified type if they are related to the specified node
func (r *NodeAuthorizer) authorizeGet(nodeName string, startingType vertexType, attrs authorizer.Attributes) (bool, string, error) {
if attrs.GetVerb() != "get" || len(attrs.GetName()) == 0 {
glog.V(2).Infof("NODE DENY: %s %#v", nodeName, attrs)
return false, "can only get individual resources of this type", nil
}
if len(attrs.GetSubresource()) > 0 {
glog.V(2).Infof("NODE DENY: %s %#v", nodeName, attrs)
return false, "cannot get subresource", nil
}
ok, err := r.hasPathFrom(nodeName, startingType, attrs.GetNamespace(), attrs.GetName())
if err != nil {
glog.V(2).Infof("NODE DENY: %v", err)
return false, "no path found to object", nil
}
if !ok {
glog.V(2).Infof("NODE DENY: %s %#v", nodeName, attrs)
return false, "no path found to object", nil
}
return ok, "", nil
}
// hasPathFrom returns true if there is a directed path from the specified type/namespace/name to the specified Node
func (r *NodeAuthorizer) hasPathFrom(nodeName string, startingType vertexType, startingNamespace, startingName string) (bool, error) {
r.graph.lock.RLock()
defer r.graph.lock.RUnlock()
nodeVertex, exists := r.graph.getVertex_rlocked(nodeVertexType, "", nodeName)
if !exists {
return false, fmt.Errorf("unknown node %s cannot get %s %s/%s", nodeName, vertexTypes[startingType], startingNamespace, startingName)
}
startingVertex, exists := r.graph.getVertex_rlocked(startingType, startingNamespace, startingName)
if !exists {
return false, fmt.Errorf("node %s cannot get unknown %s %s/%s", nodeName, vertexTypes[startingType], startingNamespace, startingName)
}
found := false
traversal := &traverse.VisitingDepthFirst{
EdgeFilter: func(edge graph.Edge) bool {
if destinationEdge, ok := edge.(*destinationEdge); ok {
if destinationEdge.DestinationID() != nodeVertex.ID() {
// Don't follow edges leading to other nodes
return false
}
// We found an edge leading to the node we want
found = true
}
// Visit this edge
return true
},
}
traversal.Walk(r.graph.graph, startingVertex, func(n graph.Node) bool {
if n.ID() == nodeVertex.ID() {
// We found the node we want
found = true
}
// Stop visiting if we've found the node we want
return found
})
if !found {
return false, fmt.Errorf("node %s cannot get %s %s/%s, no path was found", nodeName, vertexTypes[startingType], startingNamespace, startingName)
}
return true, nil
}
......@@ -91,37 +91,37 @@ func NodeRules() []rbac.PolicyRule {
// Needed to check API access. These creates are non-mutating
rbac.NewRule("create").Groups(authenticationGroup).Resources("tokenreviews").RuleOrDie(),
rbac.NewRule("create").Groups(authorizationGroup).Resources("subjectaccessreviews", "localsubjectaccessreviews").RuleOrDie(),
// Needed to build serviceLister, to populate env vars for services
rbac.NewRule(Read...).Groups(legacyGroup).Resources("services").RuleOrDie(),
// Nodes can register themselves
// TODO: restrict to creating a node with the same name they announce
// Nodes can register Node API objects and report status.
// Use the NodeRestriction admission plugin to limit a node to creating/updating its own API object.
rbac.NewRule("create", "get", "list", "watch").Groups(legacyGroup).Resources("nodes").RuleOrDie(),
// TODO: restrict to the bound node once supported
rbac.NewRule("update", "patch").Groups(legacyGroup).Resources("nodes/status").RuleOrDie(),
rbac.NewRule("update", "patch", "delete").Groups(legacyGroup).Resources("nodes").RuleOrDie(),
// TODO: restrict to the bound node as creator once supported
// TODO: restrict to the bound node as creator in the NodeRestrictions admission plugin
rbac.NewRule("create", "update", "patch").Groups(legacyGroup).Resources("events").RuleOrDie(),
// TODO: restrict to pods scheduled on the bound node once supported
// TODO: restrict to pods scheduled on the bound node once field selectors are supported by list/watch authorization
rbac.NewRule(Read...).Groups(legacyGroup).Resources("pods").RuleOrDie(),
// TODO: remove once mirror pods are removed
// TODO: restrict deletion to mirror pods created by the bound node once supported
// Needed for the node to create/delete mirror pods
// Needed for the node to create/delete mirror pods.
// Use the NodeRestriction admission plugin to limit a node to creating/deleting mirror pods bound to itself.
rbac.NewRule("create", "delete").Groups(legacyGroup).Resources("pods").RuleOrDie(),
// TODO: restrict to pods scheduled on the bound node once supported
// Needed for the node to report status of pods it is running.
// Use the NodeRestriction admission plugin to limit a node to updating status of pods bound to itself.
rbac.NewRule("update").Groups(legacyGroup).Resources("pods/status").RuleOrDie(),
// TODO: restrict to secrets and configmaps used by pods scheduled on bound node once supported
// Needed for imagepullsecrets, rbd/ceph and secret volumes, and secrets in envs
// Needed for configmap volume and envs
// Use the NodeRestriction admission plugin to limit a node to get secrets/configmaps referenced by pods bound to itself.
rbac.NewRule("get").Groups(legacyGroup).Resources("secrets", "configmaps").RuleOrDie(),
// TODO: restrict to claims/volumes used by pods scheduled on bound node once supported
// Needed for persistent volumes
// Use the NodeRestriction admission plugin to limit a node to get pv/pvc objects referenced by pods bound to itself.
rbac.NewRule("get").Groups(legacyGroup).Resources("persistentvolumeclaims", "persistentvolumes").RuleOrDie(),
// TODO: restrict to namespaces of pods scheduled on bound node once supported
// TODO: change glusterfs to use DNS lookup so this isn't needed?
// TODO: add to the Node authorizer and restrict to endpoints referenced by pods or PVs bound to the node
// Needed for glusterfs volumes
rbac.NewRule("get").Groups(legacyGroup).Resources("endpoints").RuleOrDie(),
// Used to create a certificatesigningrequest for a node-specific client certificate, and watch
......@@ -362,18 +362,70 @@ func ClusterRoles() []rbac.ClusterRole {
return roles
}
// ClusterRoleBindingFilter can modify and return or omit (by returning nil) a role binding
type ClusterRoleBindingFilter func(*rbac.ClusterRoleBinding) *rbac.ClusterRoleBinding
// AddClusterRoleBindingFilter adds the given filter to the list that is invoked when determing bootstrap roles to reconcile.
func AddClusterRoleBindingFilter(filter ClusterRoleBindingFilter) {
clusterRoleBindingFilters = append(clusterRoleBindingFilters, filter)
}
// ClearClusterRoleBindingFilters removes any filters added using AddClusterRoleBindingFilter
func ClearClusterRoleBindingFilters() {
clusterRoleBindingFilters = nil
}
const systemNodeRoleName = "system:node"
var clusterRoleBindingFilters []ClusterRoleBindingFilter
// OmitNodesGroupBinding is a filter that omits the deprecated binding for the system:nodes group to the system:node role.
var OmitNodesGroupBinding = ClusterRoleBindingFilter(func(binding *rbac.ClusterRoleBinding) *rbac.ClusterRoleBinding {
if binding.RoleRef.Name == systemNodeRoleName {
subjects := []rbac.Subject{}
for _, subject := range binding.Subjects {
if subject.Kind == rbac.GroupKind && subject.Name == user.NodesGroup {
continue
}
subjects = append(subjects, subject)
}
binding.Subjects = subjects
}
return binding
})
// ClusterRoleBindings return default rolebindings to the default roles
func ClusterRoleBindings() []rbac.ClusterRoleBinding {
rolebindings := []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(),
rbac.NewClusterBinding("system:node").Groups(user.NodesGroup).BindingOrDie(),
rbac.NewClusterBinding("system:node-proxier").Users(user.KubeProxy).BindingOrDie(),
rbac.NewClusterBinding("system:kube-controller-manager").Users(user.KubeControllerManager).BindingOrDie(),
rbac.NewClusterBinding("system:kube-dns").SAs("kube-system", "kube-dns").BindingOrDie(),
rbac.NewClusterBinding("system:kube-scheduler").Users(user.KubeScheduler).BindingOrDie(),
// This default system:nodes binding is deprecated in 1.7 with the availability of the Node authorizer.
// If an admin wants to grant the system:node role (which cannot partition Node API access), they will need to create their own clusterrolebinding.
// TODO: Remove the subjects from this binding in 1.8 (leave the empty binding for tightening reconciliation), and remove AddClusterRoleBindingFilter()
rbac.NewClusterBinding(systemNodeRoleName).Groups(user.NodesGroup).BindingOrDie(),
}
addClusterRoleBindingLabel(rolebindings)
return rolebindings
retval := []rbac.ClusterRoleBinding{}
for i := range rolebindings {
binding := &rolebindings[i]
for _, filter := range clusterRoleBindingFilters {
binding = filter(binding)
if binding == nil {
break
}
}
if binding != nil {
retval = append(retval, *binding)
}
}
return retval
}
......@@ -12,6 +12,7 @@ go_test(
srcs = [
"accessreview_test.go",
"auth_test.go",
"node_test.go",
"rbac_test.go",
],
tags = [
......@@ -27,7 +28,10 @@ go_test(
"//pkg/apis/extensions:go_default_library",
"//pkg/apis/rbac:go_default_library",
"//pkg/auth/authorizer/abac:go_default_library",
"//pkg/auth/nodeidentifier:go_default_library",
"//pkg/client/clientset_generated/internalclientset:go_default_library",
"//pkg/client/informers/informers_generated/internalversion:go_default_library",
"//pkg/kubeapiserver/authorizer:go_default_library",
"//pkg/master:go_default_library",
"//pkg/registry/rbac/clusterrole:go_default_library",
"//pkg/registry/rbac/clusterrole/storage:go_default_library",
......@@ -38,10 +42,14 @@ go_test(
"//pkg/registry/rbac/rolebinding:go_default_library",
"//pkg/registry/rbac/rolebinding/storage:go_default_library",
"//plugin/pkg/admission/admit:go_default_library",
"//plugin/pkg/admission/noderestriction:go_default_library",
"//plugin/pkg/auth/authorizer/rbac:go_default_library",
"//plugin/pkg/auth/authorizer/rbac/bootstrappolicy:go_default_library",
"//test/integration:go_default_library",
"//test/integration/framework:go_default_library",
"//vendor/github.com/golang/glog:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/api/errors:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/api/resource:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/apis/meta/v1:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/runtime/schema:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/watch:go_default_library",
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment