mirror of https://github.com/k3s-io/k3s
Wire kubelet authn/authz
parent
a602ae77b8
commit
c83f5804d2
|
@ -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)
|
||||
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
@ -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 = [
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue