Wire kubelet authn/authz

pull/6/head
Jordan Liggitt 2016-10-08 00:45:15 -04:00
parent a602ae77b8
commit c83f5804d2
No known key found for this signature in database
GPG Key ID: 24E7ADF9A3B42012
6 changed files with 340 additions and 6 deletions

131
cmd/kubelet/app/auth.go Normal file
View File

@ -0,0 +1,131 @@
/*
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 app
import (
"errors"
"fmt"
"reflect"
"k8s.io/kubernetes/pkg/apis/componentconfig"
"k8s.io/kubernetes/pkg/auth/authenticator"
"k8s.io/kubernetes/pkg/auth/authenticator/bearertoken"
"k8s.io/kubernetes/pkg/auth/authorizer"
"k8s.io/kubernetes/pkg/auth/group"
"k8s.io/kubernetes/pkg/client/clientset_generated/internalclientset"
authenticationclient "k8s.io/kubernetes/pkg/client/clientset_generated/internalclientset/typed/authentication/unversioned"
authorizationclient "k8s.io/kubernetes/pkg/client/clientset_generated/internalclientset/typed/authorization/unversioned"
alwaysallowauthorizer "k8s.io/kubernetes/pkg/genericapiserver/authorizer"
"k8s.io/kubernetes/pkg/kubelet/server"
"k8s.io/kubernetes/pkg/types"
"k8s.io/kubernetes/pkg/util/cert"
"k8s.io/kubernetes/plugin/pkg/auth/authenticator/request/anonymous"
unionauth "k8s.io/kubernetes/plugin/pkg/auth/authenticator/request/union"
"k8s.io/kubernetes/plugin/pkg/auth/authenticator/request/x509"
webhooktoken "k8s.io/kubernetes/plugin/pkg/auth/authenticator/token/webhook"
webhooksar "k8s.io/kubernetes/plugin/pkg/auth/authorizer/webhook"
)
func buildAuth(nodeName types.NodeName, client internalclientset.Interface, config componentconfig.KubeletConfiguration) (server.AuthInterface, error) {
// Get clients, if provided
var (
tokenClient authenticationclient.TokenReviewInterface
sarClient authorizationclient.SubjectAccessReviewInterface
)
if client != nil && !reflect.ValueOf(client).IsNil() {
tokenClient = client.Authentication().TokenReviews()
sarClient = client.Authorization().SubjectAccessReviews()
}
authenticator, err := buildAuthn(tokenClient, config.Authentication)
if err != nil {
return nil, err
}
attributes := server.NewNodeAuthorizerAttributesGetter(nodeName)
authorizer, err := buildAuthz(sarClient, config.Authorization)
if err != nil {
return nil, err
}
return server.NewKubeletAuth(authenticator, attributes, authorizer), nil
}
func buildAuthn(client authenticationclient.TokenReviewInterface, authn componentconfig.KubeletAuthentication) (authenticator.Request, error) {
authenticators := []authenticator.Request{}
// x509 client cert auth
if len(authn.X509.ClientCAFile) > 0 {
clientCAs, err := cert.NewPool(authn.X509.ClientCAFile)
if err != nil {
return nil, fmt.Errorf("unable to load client CA file %s: %v", authn.X509.ClientCAFile, err)
}
verifyOpts := x509.DefaultVerifyOptions()
verifyOpts.Roots = clientCAs
authenticators = append(authenticators, x509.New(verifyOpts, x509.CommonNameUserConversion))
}
// bearer token auth that uses authentication.k8s.io TokenReview to determine userinfo
if authn.Webhook.Enabled {
if client == nil {
return nil, errors.New("no client provided, cannot use webhook authentication")
}
tokenAuth, err := webhooktoken.NewFromInterface(client, authn.Webhook.CacheTTL.Duration)
if err != nil {
return nil, err
}
authenticators = append(authenticators, bearertoken.New(tokenAuth))
}
if len(authenticators) == 0 {
if authn.Anonymous.Enabled {
return anonymous.NewAuthenticator(), nil
}
return nil, errors.New("No authentication method configured")
}
authenticator := group.NewGroupAdder(unionauth.New(authenticators...), []string{"system:authenticated"})
if authn.Anonymous.Enabled {
authenticator = unionauth.NewFailOnError(authenticator, anonymous.NewAuthenticator())
}
return authenticator, nil
}
func buildAuthz(client authorizationclient.SubjectAccessReviewInterface, authz componentconfig.KubeletAuthorization) (authorizer.Authorizer, error) {
switch authz.Mode {
case componentconfig.KubeletAuthorizationModeAlwaysAllow:
return alwaysallowauthorizer.NewAlwaysAllowAuthorizer(), nil
case componentconfig.KubeletAuthorizationModeWebhook:
if client == nil {
return nil, errors.New("no client provided, cannot use webhook authorization")
}
return webhooksar.NewFromInterface(
client,
authz.Webhook.CacheAuthorizedTTL.Duration,
authz.Webhook.CacheUnauthorizedTTL.Duration,
)
case "":
return nil, fmt.Errorf("No authorization mode specified")
default:
return nil, fmt.Errorf("Unknown authorization mode %s", authz.Mode)
}
}

View File

@ -62,6 +62,7 @@ import (
kubetypes "k8s.io/kubernetes/pkg/kubelet/types"
"k8s.io/kubernetes/pkg/runtime"
"k8s.io/kubernetes/pkg/types"
"k8s.io/kubernetes/pkg/util/cert"
certutil "k8s.io/kubernetes/pkg/util/cert"
utilconfig "k8s.io/kubernetes/pkg/util/config"
"k8s.io/kubernetes/pkg/util/configz"
@ -399,6 +400,18 @@ func run(s *options.KubeletServer, kubeDeps *kubelet.KubeletDeps) (err error) {
kubeDeps.EventClient = eventClient
}
if kubeDeps.Auth == nil {
nodeName, err := getNodeName(kubeDeps.Cloud, nodeutil.GetHostname(s.HostnameOverride))
if err != nil {
return err
}
auth, err := buildAuth(nodeName, kubeDeps.KubeClient, s.KubeletConfiguration)
if err != nil {
return err
}
kubeDeps.Auth = auth
}
if kubeDeps.CAdvisorInterface == nil {
kubeDeps.CAdvisorInterface, err = cadvisor.New(uint(s.CAdvisorPort), s.ContainerRuntime)
if err != nil {
@ -501,12 +514,22 @@ func InitializeTLS(kc *componentconfig.KubeletConfiguration) (*server.TLSOptions
// Can't use TLSv1.0 because of POODLE and BEAST using CBC cipher
// Can't use TLSv1.1 because of RC4 cipher usage
MinVersion: tls.VersionTLS12,
// Populate PeerCertificates in requests, but don't yet reject connections without certificates.
ClientAuth: tls.RequestClientCert,
},
CertFile: kc.TLSCertFile,
KeyFile: kc.TLSPrivateKeyFile,
}
if len(kc.Authentication.X509.ClientCAFile) > 0 {
clientCAs, err := cert.NewPool(kc.Authentication.X509.ClientCAFile)
if err != nil {
return nil, fmt.Errorf("unable to load client CA file %s: %v", kc.Authentication.X509.ClientCAFile, err)
}
// Specify allowed CAs for client certificates
tlsOptions.Config.ClientCAs = clientCAs
// Populate PeerCertificates in requests, but don't reject connections without verified certificates
tlsOptions.Config.ClientAuth = tls.RequestClientCert
}
return tlsOptions, nil
}

View File

@ -26,6 +26,7 @@ go_library(
"//pkg/api/validation:go_default_library",
"//pkg/auth/authenticator:go_default_library",
"//pkg/auth/authorizer:go_default_library",
"//pkg/auth/user:go_default_library",
"//pkg/healthz:go_default_library",
"//pkg/httplog:go_default_library",
"//pkg/kubelet/cm:go_default_library",
@ -53,7 +54,10 @@ go_library(
go_test(
name = "go_default_test",
srcs = ["server_test.go"],
srcs = [
"auth_test.go",
"server_test.go",
],
library = "go_default_library",
tags = ["automanaged"],
deps = [

View File

@ -17,8 +17,14 @@ limitations under the License.
package server
import (
"net/http"
"strings"
"github.com/golang/glog"
"k8s.io/kubernetes/pkg/auth/authenticator"
"k8s.io/kubernetes/pkg/auth/authorizer"
"k8s.io/kubernetes/pkg/auth/user"
"k8s.io/kubernetes/pkg/types"
)
// KubeletAuth implements AuthInterface
@ -35,3 +41,74 @@ type KubeletAuth struct {
func NewKubeletAuth(authenticator authenticator.Request, authorizerAttributeGetter authorizer.RequestAttributesGetter, authorizer authorizer.Authorizer) AuthInterface {
return &KubeletAuth{authenticator, authorizerAttributeGetter, authorizer}
}
func NewNodeAuthorizerAttributesGetter(nodeName types.NodeName) authorizer.RequestAttributesGetter {
return nodeAuthorizerAttributesGetter{nodeName: nodeName}
}
type nodeAuthorizerAttributesGetter struct {
nodeName types.NodeName
}
func isSubpath(subpath, path string) bool {
path = strings.TrimSuffix(path, "/")
return subpath == path || (strings.HasPrefix(subpath, path) && subpath[len(path)] == '/')
}
// GetRequestAttributes populates authorizer attributes for the requests to the kubelet API.
// Default attributes are: {apiVersion=v1,verb=<http verb from request>,resource=nodes,name=<node name>,subresource=proxy}
// More specific verb/resource is set for the following request patterns:
// /stats/* => verb=<api verb from request>, resource=nodes, name=<node name>, subresource=stats
// /metrics/* => verb=<api verb from request>, resource=nodes, name=<node name>, subresource=metrics
// /logs/* => verb=<api verb from request>, resource=nodes, name=<node name>, subresource=log
// /spec/* => verb=<api verb from request>, resource=nodes, name=<node name>, subresource=spec
func (n nodeAuthorizerAttributesGetter) GetRequestAttributes(u user.Info, r *http.Request) authorizer.Attributes {
apiVerb := ""
switch r.Method {
case "POST":
apiVerb = "create"
case "GET":
apiVerb = "get"
case "PUT":
apiVerb = "update"
case "PATCH":
apiVerb = "patch"
case "DELETE":
apiVerb = "delete"
}
requestPath := r.URL.Path
// Default attributes mirror the API attributes that would allow this access to the kubelet API
attrs := authorizer.AttributesRecord{
User: u,
Verb: apiVerb,
Namespace: "",
APIGroup: "",
APIVersion: "v1",
Resource: "nodes",
Subresource: "proxy",
Name: string(n.nodeName),
ResourceRequest: true,
Path: requestPath,
}
// Override subresource for specific paths
// This allows subdividing access to the kubelet API
switch {
case isSubpath(requestPath, statsPath):
attrs.Subresource = "stats"
case isSubpath(requestPath, metricsPath):
attrs.Subresource = "metrics"
case isSubpath(requestPath, logsPath):
// "log" to match other log subresources (pods/log, etc)
attrs.Subresource = "log"
case isSubpath(requestPath, specPath):
attrs.Subresource = "spec"
}
glog.V(5).Infof("Node request attributes: attrs=%#v", attrs)
return attrs
}

View File

@ -0,0 +1,53 @@
/*
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 server
import "testing"
func TestIsSubPath(t *testing.T) {
testcases := map[string]struct {
subpath string
path string
expected bool
}{
"empty": {subpath: "", path: "", expected: true},
"match 1": {subpath: "foo", path: "foo", expected: true},
"match 2": {subpath: "/foo", path: "/foo", expected: true},
"match 3": {subpath: "/foo/", path: "/foo/", expected: true},
"match 4": {subpath: "/foo/bar", path: "/foo/bar", expected: true},
"subpath of root 1": {subpath: "/foo", path: "/", expected: true},
"subpath of root 2": {subpath: "/foo/", path: "/", expected: true},
"subpath of root 3": {subpath: "/foo/bar", path: "/", expected: true},
"subpath of path 1": {subpath: "/foo", path: "/foo", expected: true},
"subpath of path 2": {subpath: "/foo/", path: "/foo", expected: true},
"subpath of path 3": {subpath: "/foo/bar", path: "/foo", expected: true},
"mismatch 1": {subpath: "/foo", path: "/bar", expected: false},
"mismatch 2": {subpath: "/foo", path: "/foobar", expected: false},
"mismatch 3": {subpath: "/foobar", path: "/foo", expected: false},
}
for k, tc := range testcases {
result := isSubpath(tc.subpath, tc.path)
if result != tc.expected {
t.Errorf("%s: expected %v, got %v", k, tc.expected, result)
}
}
}

View File

@ -605,10 +605,56 @@ func TestAuthFilters(t *testing.T) {
}
}
methodToAPIVerb := map[string]string{"GET": "get", "POST": "create", "PUT": "update"}
pathToSubresource := func(path string) string {
switch {
// Cases for subpaths we expect specific subresources for
case isSubpath(path, statsPath):
return "stats"
case isSubpath(path, specPath):
return "spec"
case isSubpath(path, logsPath):
return "log"
case isSubpath(path, metricsPath):
return "metrics"
// Cases for subpaths we expect to map to the "proxy" subresource
case isSubpath(path, "/attach"),
isSubpath(path, "/configz"),
isSubpath(path, "/containerLogs"),
isSubpath(path, "/debug"),
isSubpath(path, "/exec"),
isSubpath(path, "/healthz"),
isSubpath(path, "/pods"),
isSubpath(path, "/portForward"),
isSubpath(path, "/run"),
isSubpath(path, "/runningpods"):
return "proxy"
default:
panic(fmt.Errorf(`unexpected kubelet API path %s.
The kubelet API has likely registered a handler for a new path.
If the new path has a use case for partitioned authorization when requested from the kubelet API,
add a specific subresource for it in auth.go#GetRequestAttributes() and in TestAuthFilters().
Otherwise, add it to the expected list of paths that map to the "proxy" subresource in TestAuthFilters().`, path))
}
}
attributesGetter := NewNodeAuthorizerAttributesGetter(types.NodeName("test"))
for _, tc := range testcases {
var (
expectedUser = &user.DefaultInfo{Name: "test"}
expectedAttributes = &authorizer.AttributesRecord{User: expectedUser}
expectedAttributes = authorizer.AttributesRecord{
User: expectedUser,
APIGroup: "",
APIVersion: "v1",
Verb: methodToAPIVerb[tc.Method],
Resource: "nodes",
Name: "test",
Subresource: pathToSubresource(tc.Path),
ResourceRequest: true,
Path: tc.Path,
}
calledAuthenticate = false
calledAuthorize = false
@ -624,12 +670,12 @@ func TestAuthFilters(t *testing.T) {
if u != expectedUser {
t.Fatalf("%s: expected user %v, got %v", tc.Path, expectedUser, u)
}
return expectedAttributes
return attributesGetter.GetRequestAttributes(u, req)
}
fw.fakeAuth.authorizeFunc = func(a authorizer.Attributes) (authorized bool, reason string, err error) {
calledAuthorize = true
if a != expectedAttributes {
t.Fatalf("%s: expected attributes %v, got %v", tc.Path, expectedAttributes, a)
t.Fatalf("%s: expected attributes\n\t%#v\ngot\n\t%#v", tc.Path, expectedAttributes, a)
}
return false, "", nil
}