diff --git a/cmd/kubelet/app/auth.go b/cmd/kubelet/app/auth.go new file mode 100644 index 00000000000..ea7b58900a0 --- /dev/null +++ b/cmd/kubelet/app/auth.go @@ -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) + + } +} diff --git a/cmd/kubelet/app/server.go b/cmd/kubelet/app/server.go index aecfd322f39..4ed7674ebd6 100644 --- a/cmd/kubelet/app/server.go +++ b/cmd/kubelet/app/server.go @@ -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 } diff --git a/pkg/kubelet/server/BUILD b/pkg/kubelet/server/BUILD index d6b3f9e3651..8525528fd49 100644 --- a/pkg/kubelet/server/BUILD +++ b/pkg/kubelet/server/BUILD @@ -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 = [ diff --git a/pkg/kubelet/server/auth.go b/pkg/kubelet/server/auth.go index a153ff4e4f3..a1daf92d587 100644 --- a/pkg/kubelet/server/auth.go +++ b/pkg/kubelet/server/auth.go @@ -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=,resource=nodes,name=,subresource=proxy} +// More specific verb/resource is set for the following request patterns: +// /stats/* => verb=, resource=nodes, name=, subresource=stats +// /metrics/* => verb=, resource=nodes, name=, subresource=metrics +// /logs/* => verb=, resource=nodes, name=, subresource=log +// /spec/* => verb=, resource=nodes, 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 +} diff --git a/pkg/kubelet/server/auth_test.go b/pkg/kubelet/server/auth_test.go new file mode 100644 index 00000000000..844be3852f7 --- /dev/null +++ b/pkg/kubelet/server/auth_test.go @@ -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) + } + } +} diff --git a/pkg/kubelet/server/server_test.go b/pkg/kubelet/server/server_test.go index 4674a6ae4e8..e9350c67b24 100644 --- a/pkg/kubelet/server/server_test.go +++ b/pkg/kubelet/server/server_test.go @@ -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 }