mirror of
https://github.com/k3s-io/kubernetes.git
synced 2025-07-21 02:41:25 +00:00
Add Certificate signerName admission plugins
This commit is contained in:
parent
a983356caa
commit
d7e10f9869
@ -26,6 +26,9 @@ go_library(
|
||||
"//plugin/pkg/admission/admit:go_default_library",
|
||||
"//plugin/pkg/admission/alwayspullimages:go_default_library",
|
||||
"//plugin/pkg/admission/antiaffinity:go_default_library",
|
||||
"//plugin/pkg/admission/certificates/approval:go_default_library",
|
||||
"//plugin/pkg/admission/certificates/signing:go_default_library",
|
||||
"//plugin/pkg/admission/certificates/subjectrestriction:go_default_library",
|
||||
"//plugin/pkg/admission/defaulttolerationseconds:go_default_library",
|
||||
"//plugin/pkg/admission/deny:go_default_library",
|
||||
"//plugin/pkg/admission/eventratelimit:go_default_library",
|
||||
|
@ -24,6 +24,9 @@ import (
|
||||
"k8s.io/kubernetes/plugin/pkg/admission/admit"
|
||||
"k8s.io/kubernetes/plugin/pkg/admission/alwayspullimages"
|
||||
"k8s.io/kubernetes/plugin/pkg/admission/antiaffinity"
|
||||
certapproval "k8s.io/kubernetes/plugin/pkg/admission/certificates/approval"
|
||||
certsigning "k8s.io/kubernetes/plugin/pkg/admission/certificates/signing"
|
||||
certsubjectrestriction "k8s.io/kubernetes/plugin/pkg/admission/certificates/subjectrestriction"
|
||||
"k8s.io/kubernetes/plugin/pkg/admission/defaulttolerationseconds"
|
||||
"k8s.io/kubernetes/plugin/pkg/admission/deny"
|
||||
"k8s.io/kubernetes/plugin/pkg/admission/eventratelimit"
|
||||
@ -87,6 +90,9 @@ var AllOrderedPlugins = []string{
|
||||
gc.PluginName, // OwnerReferencesPermissionEnforcement
|
||||
resize.PluginName, // PersistentVolumeClaimResize
|
||||
runtimeclass.PluginName, // RuntimeClass
|
||||
certapproval.PluginName, // CertificateApproval
|
||||
certsigning.PluginName, // CertificateSigning
|
||||
certsubjectrestriction.PluginName, // CertificateSubjectRestriction
|
||||
|
||||
// new admission plugins should generally be inserted above here
|
||||
// webhook, resourcequota, and deny plugins must go at the end
|
||||
@ -128,6 +134,9 @@ func RegisterAllAdmissionPlugins(plugins *admission.Plugins) {
|
||||
setdefault.Register(plugins)
|
||||
resize.Register(plugins)
|
||||
storageobjectinuseprotection.Register(plugins)
|
||||
certapproval.Register(plugins)
|
||||
certsigning.Register(plugins)
|
||||
certsubjectrestriction.Register(plugins)
|
||||
}
|
||||
|
||||
// DefaultOffAdmissionPlugins get admission plugins off by default for kube-apiserver.
|
||||
@ -146,6 +155,9 @@ func DefaultOffAdmissionPlugins() sets.String {
|
||||
podpriority.PluginName, //PodPriority
|
||||
nodetaint.PluginName, //TaintNodesByCondition
|
||||
runtimeclass.PluginName, //RuntimeClass, gates internally on the feature
|
||||
certapproval.PluginName, // CertificateApproval
|
||||
certsigning.PluginName, // CertificateSigning
|
||||
certsubjectrestriction.PluginName, // CertificateSubjectRestriction
|
||||
)
|
||||
|
||||
return sets.NewString(AllOrderedPlugins...).Difference(defaultOnPlugins)
|
||||
|
@ -14,6 +14,7 @@ filegroup(
|
||||
"//plugin/pkg/admission/admit:all-srcs",
|
||||
"//plugin/pkg/admission/alwayspullimages:all-srcs",
|
||||
"//plugin/pkg/admission/antiaffinity:all-srcs",
|
||||
"//plugin/pkg/admission/certificates:all-srcs",
|
||||
"//plugin/pkg/admission/defaulttolerationseconds:all-srcs",
|
||||
"//plugin/pkg/admission/deny:all-srcs",
|
||||
"//plugin/pkg/admission/eventratelimit:all-srcs",
|
||||
|
32
plugin/pkg/admission/certificates/BUILD
Normal file
32
plugin/pkg/admission/certificates/BUILD
Normal file
@ -0,0 +1,32 @@
|
||||
load("@io_bazel_rules_go//go:def.bzl", "go_library")
|
||||
|
||||
package(default_visibility = ["//visibility:public"])
|
||||
|
||||
filegroup(
|
||||
name = "package-srcs",
|
||||
srcs = glob(["**"]),
|
||||
tags = ["automanaged"],
|
||||
visibility = ["//visibility:private"],
|
||||
)
|
||||
|
||||
filegroup(
|
||||
name = "all-srcs",
|
||||
srcs = [
|
||||
":package-srcs",
|
||||
"//plugin/pkg/admission/certificates/approval:all-srcs",
|
||||
"//plugin/pkg/admission/certificates/signing:all-srcs",
|
||||
"//plugin/pkg/admission/certificates/subjectrestriction:all-srcs",
|
||||
],
|
||||
tags = ["automanaged"],
|
||||
)
|
||||
|
||||
go_library(
|
||||
name = "go_default_library",
|
||||
srcs = ["util.go"],
|
||||
importpath = "k8s.io/kubernetes/plugin/pkg/admission/certificates",
|
||||
deps = [
|
||||
"//staging/src/k8s.io/apiserver/pkg/authentication/user:go_default_library",
|
||||
"//staging/src/k8s.io/apiserver/pkg/authorization/authorizer:go_default_library",
|
||||
"//vendor/k8s.io/klog:go_default_library",
|
||||
],
|
||||
)
|
8
plugin/pkg/admission/certificates/OWNERS
Normal file
8
plugin/pkg/admission/certificates/OWNERS
Normal file
@ -0,0 +1,8 @@
|
||||
# See the OWNERS docs at https://go.k8s.io/owners
|
||||
|
||||
approvers:
|
||||
- sig-auth-certificates-approvers
|
||||
reviewers:
|
||||
- sig-auth-certificates-approvers
|
||||
labels:
|
||||
- sig/auth
|
44
plugin/pkg/admission/certificates/approval/BUILD
Normal file
44
plugin/pkg/admission/certificates/approval/BUILD
Normal file
@ -0,0 +1,44 @@
|
||||
load("@io_bazel_rules_go//go:def.bzl", "go_library", "go_test")
|
||||
|
||||
go_library(
|
||||
name = "go_default_library",
|
||||
srcs = ["admission.go"],
|
||||
importpath = "k8s.io/kubernetes/plugin/pkg/admission/certificates/approval",
|
||||
visibility = ["//visibility:public"],
|
||||
deps = [
|
||||
"//pkg/apis/certificates:go_default_library",
|
||||
"//plugin/pkg/admission/certificates:go_default_library",
|
||||
"//staging/src/k8s.io/apiserver/pkg/admission:go_default_library",
|
||||
"//staging/src/k8s.io/apiserver/pkg/admission/initializer:go_default_library",
|
||||
"//staging/src/k8s.io/apiserver/pkg/authorization/authorizer:go_default_library",
|
||||
"//vendor/k8s.io/klog:go_default_library",
|
||||
],
|
||||
)
|
||||
|
||||
go_test(
|
||||
name = "go_default_test",
|
||||
srcs = ["admission_test.go"],
|
||||
embed = [":go_default_library"],
|
||||
deps = [
|
||||
"//pkg/apis/certificates:go_default_library",
|
||||
"//staging/src/k8s.io/apimachinery/pkg/runtime:go_default_library",
|
||||
"//staging/src/k8s.io/apimachinery/pkg/runtime/schema:go_default_library",
|
||||
"//staging/src/k8s.io/apiserver/pkg/admission:go_default_library",
|
||||
"//staging/src/k8s.io/apiserver/pkg/authentication/user:go_default_library",
|
||||
"//staging/src/k8s.io/apiserver/pkg/authorization/authorizer:go_default_library",
|
||||
],
|
||||
)
|
||||
|
||||
filegroup(
|
||||
name = "package-srcs",
|
||||
srcs = glob(["**"]),
|
||||
tags = ["automanaged"],
|
||||
visibility = ["//visibility:private"],
|
||||
)
|
||||
|
||||
filegroup(
|
||||
name = "all-srcs",
|
||||
srcs = [":package-srcs"],
|
||||
tags = ["automanaged"],
|
||||
visibility = ["//visibility:public"],
|
||||
)
|
99
plugin/pkg/admission/certificates/approval/admission.go
Normal file
99
plugin/pkg/admission/certificates/approval/admission.go
Normal file
@ -0,0 +1,99 @@
|
||||
/*
|
||||
Copyright 2020 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 approval
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
|
||||
"k8s.io/klog"
|
||||
|
||||
"k8s.io/apiserver/pkg/admission"
|
||||
genericadmissioninit "k8s.io/apiserver/pkg/admission/initializer"
|
||||
"k8s.io/apiserver/pkg/authorization/authorizer"
|
||||
|
||||
api "k8s.io/kubernetes/pkg/apis/certificates"
|
||||
"k8s.io/kubernetes/plugin/pkg/admission/certificates"
|
||||
)
|
||||
|
||||
// PluginName is a string with the name of the plugin
|
||||
const PluginName = "CertificateApproval"
|
||||
|
||||
// Register registers a plugin
|
||||
func Register(plugins *admission.Plugins) {
|
||||
plugins.Register(PluginName, func(config io.Reader) (admission.Interface, error) {
|
||||
return NewPlugin(), nil
|
||||
})
|
||||
}
|
||||
|
||||
// Plugin holds state for and implements the admission plugin.
|
||||
type Plugin struct {
|
||||
*admission.Handler
|
||||
authz authorizer.Authorizer
|
||||
}
|
||||
|
||||
// SetAuthorizer sets the authorizer.
|
||||
func (p *Plugin) SetAuthorizer(authz authorizer.Authorizer) {
|
||||
p.authz = authz
|
||||
}
|
||||
|
||||
// ValidateInitialization ensures an authorizer is set.
|
||||
func (p *Plugin) ValidateInitialization() error {
|
||||
if p.authz == nil {
|
||||
return fmt.Errorf("%s requires an authorizer", PluginName)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
var _ admission.ValidationInterface = &Plugin{}
|
||||
var _ genericadmissioninit.WantsAuthorizer = &Plugin{}
|
||||
|
||||
// NewPlugin creates a new CSR approval admission plugin
|
||||
func NewPlugin() *Plugin {
|
||||
return &Plugin{
|
||||
Handler: admission.NewHandler(admission.Update),
|
||||
}
|
||||
}
|
||||
|
||||
var csrGroupResource = api.Resource("certificatesigningrequests")
|
||||
|
||||
// Validate verifies that the requesting user has permission to approve
|
||||
// CertificateSigningRequests for the specified signerName.
|
||||
func (p *Plugin) Validate(ctx context.Context, a admission.Attributes, _ admission.ObjectInterfaces) error {
|
||||
// Ignore all calls to anything other than 'certificatesigningrequests/approval'.
|
||||
// Ignore all operations other than UPDATE.
|
||||
if a.GetSubresource() != "approval" ||
|
||||
a.GetResource().GroupResource() != csrGroupResource {
|
||||
return nil
|
||||
}
|
||||
|
||||
// We check permissions against the *old* version of the resource, in case
|
||||
// a user is attempting to update the SignerName when calling the approval
|
||||
// endpoint (which is an invalid/not allowed operation)
|
||||
csr, ok := a.GetOldObject().(*api.CertificateSigningRequest)
|
||||
if !ok {
|
||||
return admission.NewForbidden(a, fmt.Errorf("expected type CertificateSigningRequest, got: %T", a.GetOldObject()))
|
||||
}
|
||||
|
||||
if !certificates.IsAuthorizedForSignerName(ctx, p.authz, a.GetUserInfo(), "approve", csr.Spec.SignerName) {
|
||||
klog.V(4).Infof("user not permitted to approve CertificateSigningRequest %q with signerName %q", csr.Name, csr.Spec.SignerName)
|
||||
return admission.NewForbidden(a, fmt.Errorf("user not permitted to approve requests with signerName %q", csr.Spec.SignerName))
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
206
plugin/pkg/admission/certificates/approval/admission_test.go
Normal file
206
plugin/pkg/admission/certificates/approval/admission_test.go
Normal file
@ -0,0 +1,206 @@
|
||||
/*
|
||||
Copyright 2020 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 approval
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
"k8s.io/apiserver/pkg/admission"
|
||||
"k8s.io/apiserver/pkg/authentication/user"
|
||||
"k8s.io/apiserver/pkg/authorization/authorizer"
|
||||
|
||||
certificatesapi "k8s.io/kubernetes/pkg/apis/certificates"
|
||||
)
|
||||
|
||||
func TestPlugin_Validate(t *testing.T) {
|
||||
tests := map[string]struct {
|
||||
attributes admission.Attributes
|
||||
allowedName string
|
||||
allowed bool
|
||||
authzErr error
|
||||
}{
|
||||
"wrong type": {
|
||||
attributes: &testAttributes{
|
||||
resource: certificatesapi.Resource("certificatesigningrequests"),
|
||||
subresource: "approval",
|
||||
oldObj: &certificatesapi.CertificateSigningRequestList{},
|
||||
operation: admission.Update,
|
||||
},
|
||||
allowed: false,
|
||||
},
|
||||
"reject requests if looking up permissions fails": {
|
||||
attributes: &testAttributes{
|
||||
resource: certificatesapi.Resource("certificatesigningrequests"),
|
||||
subresource: "approval",
|
||||
oldObj: &certificatesapi.CertificateSigningRequest{Spec: certificatesapi.CertificateSigningRequestSpec{
|
||||
SignerName: "abc.com/xyz",
|
||||
}},
|
||||
operation: admission.Update,
|
||||
},
|
||||
authzErr: errors.New("forced error"),
|
||||
allowed: false,
|
||||
},
|
||||
"should allow request if user is authorized for specific signerName": {
|
||||
allowedName: "abc.com/xyz",
|
||||
attributes: &testAttributes{
|
||||
resource: certificatesapi.Resource("certificatesigningrequests"),
|
||||
subresource: "approval",
|
||||
oldObj: &certificatesapi.CertificateSigningRequest{Spec: certificatesapi.CertificateSigningRequestSpec{
|
||||
SignerName: "abc.com/xyz",
|
||||
}},
|
||||
operation: admission.Update,
|
||||
},
|
||||
allowed: true,
|
||||
},
|
||||
"should allow request if user is authorized with wildcard": {
|
||||
allowedName: "abc.com/*",
|
||||
attributes: &testAttributes{
|
||||
resource: certificatesapi.Resource("certificatesigningrequests"),
|
||||
subresource: "approval",
|
||||
oldObj: &certificatesapi.CertificateSigningRequest{Spec: certificatesapi.CertificateSigningRequestSpec{
|
||||
SignerName: "abc.com/xyz",
|
||||
}},
|
||||
operation: admission.Update,
|
||||
},
|
||||
allowed: true,
|
||||
},
|
||||
"should deny request if user does not have permission for this signerName": {
|
||||
allowedName: "notabc.com/xyz",
|
||||
attributes: &testAttributes{
|
||||
resource: certificatesapi.Resource("certificatesigningrequests"),
|
||||
subresource: "approval",
|
||||
oldObj: &certificatesapi.CertificateSigningRequest{Spec: certificatesapi.CertificateSigningRequestSpec{
|
||||
SignerName: "abc.com/xyz",
|
||||
}},
|
||||
operation: admission.Update,
|
||||
},
|
||||
allowed: false,
|
||||
},
|
||||
"should deny request if user attempts to update signerName to a new value they *do* have permission to approve for": {
|
||||
allowedName: "allowed.com/xyz",
|
||||
attributes: &testAttributes{
|
||||
resource: certificatesapi.Resource("certificatesigningrequests"),
|
||||
subresource: "approval",
|
||||
oldObj: &certificatesapi.CertificateSigningRequest{Spec: certificatesapi.CertificateSigningRequestSpec{
|
||||
SignerName: "notallowed.com/xyz",
|
||||
}},
|
||||
obj: &certificatesapi.CertificateSigningRequest{Spec: certificatesapi.CertificateSigningRequestSpec{
|
||||
SignerName: "allowed.com/xyz",
|
||||
}},
|
||||
operation: admission.Update,
|
||||
},
|
||||
allowed: false,
|
||||
},
|
||||
}
|
||||
|
||||
for n, test := range tests {
|
||||
t.Run(n, func(t *testing.T) {
|
||||
p := Plugin{
|
||||
authz: fakeAuthorizer{
|
||||
t: t,
|
||||
verb: "approve",
|
||||
allowedName: test.allowedName,
|
||||
decision: authorizer.DecisionAllow,
|
||||
err: test.authzErr,
|
||||
},
|
||||
}
|
||||
err := p.Validate(context.Background(), test.attributes, nil)
|
||||
if err == nil && !test.allowed {
|
||||
t.Errorf("Expected authorization policy to reject CSR but it was allowed")
|
||||
}
|
||||
if err != nil && test.allowed {
|
||||
t.Errorf("Expected authorization policy to accept CSR but it was rejected: %v", err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
type fakeAuthorizer struct {
|
||||
t *testing.T
|
||||
verb string
|
||||
allowedName string
|
||||
decision authorizer.Decision
|
||||
err error
|
||||
}
|
||||
|
||||
func (f fakeAuthorizer) Authorize(ctx context.Context, a authorizer.Attributes) (authorizer.Decision, string, error) {
|
||||
if f.err != nil {
|
||||
return f.decision, "forced error", f.err
|
||||
}
|
||||
if a.GetVerb() != f.verb {
|
||||
return authorizer.DecisionDeny, fmt.Sprintf("unrecognised verb '%s'", a.GetVerb()), nil
|
||||
}
|
||||
if a.GetAPIGroup() != "certificates.k8s.io" {
|
||||
return authorizer.DecisionDeny, fmt.Sprintf("unrecognised groupName '%s'", a.GetAPIGroup()), nil
|
||||
}
|
||||
if a.GetAPIVersion() != "*" {
|
||||
return authorizer.DecisionDeny, fmt.Sprintf("unrecognised apiVersion '%s'", a.GetAPIVersion()), nil
|
||||
}
|
||||
if a.GetResource() != "signers" {
|
||||
return authorizer.DecisionDeny, fmt.Sprintf("unrecognised resource '%s'", a.GetResource()), nil
|
||||
}
|
||||
if a.GetName() != f.allowedName {
|
||||
return authorizer.DecisionDeny, fmt.Sprintf("unrecognised resource name '%s'", a.GetName()), nil
|
||||
}
|
||||
if !a.IsResourceRequest() {
|
||||
return authorizer.DecisionDeny, fmt.Sprintf("unrecognised IsResourceRequest '%t'", a.IsResourceRequest()), nil
|
||||
}
|
||||
return f.decision, "", nil
|
||||
}
|
||||
|
||||
type testAttributes struct {
|
||||
resource schema.GroupResource
|
||||
subresource string
|
||||
operation admission.Operation
|
||||
obj, oldObj runtime.Object
|
||||
name string
|
||||
|
||||
admission.Attributes // nil panic if any other methods called
|
||||
}
|
||||
|
||||
func (t *testAttributes) GetResource() schema.GroupVersionResource {
|
||||
return t.resource.WithVersion("ignored")
|
||||
}
|
||||
|
||||
func (t *testAttributes) GetSubresource() string {
|
||||
return t.subresource
|
||||
}
|
||||
|
||||
func (t *testAttributes) GetObject() runtime.Object {
|
||||
return t.obj
|
||||
}
|
||||
|
||||
func (t *testAttributes) GetOldObject() runtime.Object {
|
||||
return t.oldObj
|
||||
}
|
||||
|
||||
func (t *testAttributes) GetName() string {
|
||||
return t.name
|
||||
}
|
||||
|
||||
func (t *testAttributes) GetOperation() admission.Operation {
|
||||
return t.operation
|
||||
}
|
||||
|
||||
func (t *testAttributes) GetUserInfo() user.Info {
|
||||
return &user.DefaultInfo{Name: "ignored"}
|
||||
}
|
44
plugin/pkg/admission/certificates/signing/BUILD
Normal file
44
plugin/pkg/admission/certificates/signing/BUILD
Normal file
@ -0,0 +1,44 @@
|
||||
load("@io_bazel_rules_go//go:def.bzl", "go_library", "go_test")
|
||||
|
||||
go_library(
|
||||
name = "go_default_library",
|
||||
srcs = ["admission.go"],
|
||||
importpath = "k8s.io/kubernetes/plugin/pkg/admission/certificates/signing",
|
||||
visibility = ["//visibility:public"],
|
||||
deps = [
|
||||
"//pkg/apis/certificates:go_default_library",
|
||||
"//plugin/pkg/admission/certificates:go_default_library",
|
||||
"//staging/src/k8s.io/apiserver/pkg/admission:go_default_library",
|
||||
"//staging/src/k8s.io/apiserver/pkg/admission/initializer:go_default_library",
|
||||
"//staging/src/k8s.io/apiserver/pkg/authorization/authorizer:go_default_library",
|
||||
"//vendor/k8s.io/klog:go_default_library",
|
||||
],
|
||||
)
|
||||
|
||||
go_test(
|
||||
name = "go_default_test",
|
||||
srcs = ["admission_test.go"],
|
||||
embed = [":go_default_library"],
|
||||
deps = [
|
||||
"//pkg/apis/certificates:go_default_library",
|
||||
"//staging/src/k8s.io/apimachinery/pkg/runtime:go_default_library",
|
||||
"//staging/src/k8s.io/apimachinery/pkg/runtime/schema:go_default_library",
|
||||
"//staging/src/k8s.io/apiserver/pkg/admission:go_default_library",
|
||||
"//staging/src/k8s.io/apiserver/pkg/authentication/user:go_default_library",
|
||||
"//staging/src/k8s.io/apiserver/pkg/authorization/authorizer:go_default_library",
|
||||
],
|
||||
)
|
||||
|
||||
filegroup(
|
||||
name = "package-srcs",
|
||||
srcs = glob(["**"]),
|
||||
tags = ["automanaged"],
|
||||
visibility = ["//visibility:private"],
|
||||
)
|
||||
|
||||
filegroup(
|
||||
name = "all-srcs",
|
||||
srcs = [":package-srcs"],
|
||||
tags = ["automanaged"],
|
||||
visibility = ["//visibility:public"],
|
||||
)
|
106
plugin/pkg/admission/certificates/signing/admission.go
Normal file
106
plugin/pkg/admission/certificates/signing/admission.go
Normal file
@ -0,0 +1,106 @@
|
||||
/*
|
||||
Copyright 2020 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 signing
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"reflect"
|
||||
|
||||
"k8s.io/klog"
|
||||
|
||||
"k8s.io/apiserver/pkg/admission"
|
||||
genericadmissioninit "k8s.io/apiserver/pkg/admission/initializer"
|
||||
"k8s.io/apiserver/pkg/authorization/authorizer"
|
||||
|
||||
api "k8s.io/kubernetes/pkg/apis/certificates"
|
||||
"k8s.io/kubernetes/plugin/pkg/admission/certificates"
|
||||
)
|
||||
|
||||
// PluginName is a string with the name of the plugin
|
||||
const PluginName = "CertificateSigning"
|
||||
|
||||
// Register registers a plugin
|
||||
func Register(plugins *admission.Plugins) {
|
||||
plugins.Register(PluginName, func(config io.Reader) (admission.Interface, error) {
|
||||
return NewPlugin(), nil
|
||||
})
|
||||
}
|
||||
|
||||
// Plugin holds state for and implements the admission plugin.
|
||||
type Plugin struct {
|
||||
*admission.Handler
|
||||
authz authorizer.Authorizer
|
||||
}
|
||||
|
||||
// SetAuthorizer sets the authorizer.
|
||||
func (p *Plugin) SetAuthorizer(authz authorizer.Authorizer) {
|
||||
p.authz = authz
|
||||
}
|
||||
|
||||
// ValidateInitialization ensures an authorizer is set.
|
||||
func (p *Plugin) ValidateInitialization() error {
|
||||
if p.authz == nil {
|
||||
return fmt.Errorf("%s requires an authorizer", PluginName)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
var _ admission.ValidationInterface = &Plugin{}
|
||||
var _ genericadmissioninit.WantsAuthorizer = &Plugin{}
|
||||
|
||||
// NewPlugin creates a new CSR approval admission plugin
|
||||
func NewPlugin() *Plugin {
|
||||
return &Plugin{
|
||||
Handler: admission.NewHandler(admission.Update),
|
||||
}
|
||||
}
|
||||
|
||||
var csrGroupResource = api.Resource("certificatesigningrequests")
|
||||
|
||||
// Validate verifies that the requesting user has permission to approve
|
||||
// CertificateSigningRequests for the specified signerName.
|
||||
func (p *Plugin) Validate(ctx context.Context, a admission.Attributes, o admission.ObjectInterfaces) error {
|
||||
// Ignore all calls to anything other than 'certificatesigningrequests/approval'.
|
||||
// Ignore all operations other than UPDATE.
|
||||
if a.GetSubresource() != "status" ||
|
||||
a.GetResource().GroupResource() != csrGroupResource {
|
||||
return nil
|
||||
}
|
||||
|
||||
oldCSR, ok := a.GetOldObject().(*api.CertificateSigningRequest)
|
||||
if !ok {
|
||||
return admission.NewForbidden(a, fmt.Errorf("expected type CertificateSigningRequest, got: %T", a.GetOldObject()))
|
||||
}
|
||||
csr, ok := a.GetObject().(*api.CertificateSigningRequest)
|
||||
if !ok {
|
||||
return admission.NewForbidden(a, fmt.Errorf("expected type CertificateSigningRequest, got: %T", a.GetObject()))
|
||||
}
|
||||
|
||||
// only run if the status.certificate field has been changed
|
||||
if reflect.DeepEqual(oldCSR.Status.Certificate, csr.Status.Certificate) {
|
||||
return nil
|
||||
}
|
||||
|
||||
if !certificates.IsAuthorizedForSignerName(ctx, p.authz, a.GetUserInfo(), "sign", oldCSR.Spec.SignerName) {
|
||||
klog.V(4).Infof("user not permitted to sign CertificateSigningRequest %q with signerName %q", oldCSR.Name, oldCSR.Spec.SignerName)
|
||||
return admission.NewForbidden(a, fmt.Errorf("user not permitted to sign requests with signerName %q", oldCSR.Spec.SignerName))
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
260
plugin/pkg/admission/certificates/signing/admission_test.go
Normal file
260
plugin/pkg/admission/certificates/signing/admission_test.go
Normal file
@ -0,0 +1,260 @@
|
||||
/*
|
||||
Copyright 2020 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 signing
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
"k8s.io/apiserver/pkg/admission"
|
||||
"k8s.io/apiserver/pkg/authentication/user"
|
||||
"k8s.io/apiserver/pkg/authorization/authorizer"
|
||||
|
||||
certificatesapi "k8s.io/kubernetes/pkg/apis/certificates"
|
||||
)
|
||||
|
||||
func TestPlugin_Validate(t *testing.T) {
|
||||
tests := map[string]struct {
|
||||
attributes admission.Attributes
|
||||
allowedName string
|
||||
allowed bool
|
||||
authzErr error
|
||||
}{
|
||||
"wrong type": {
|
||||
attributes: &testAttributes{
|
||||
resource: certificatesapi.Resource("certificatesigningrequests"),
|
||||
subresource: "status",
|
||||
oldObj: &certificatesapi.CertificateSigningRequestList{},
|
||||
obj: &certificatesapi.CertificateSigningRequestList{},
|
||||
operation: admission.Update,
|
||||
},
|
||||
allowed: false,
|
||||
},
|
||||
"allowed if the 'certificate' field has not changed": {
|
||||
attributes: &testAttributes{
|
||||
resource: certificatesapi.Resource("certificatesigningrequests"),
|
||||
subresource: "status",
|
||||
oldObj: &certificatesapi.CertificateSigningRequest{Status: certificatesapi.CertificateSigningRequestStatus{
|
||||
Certificate: []byte("data"),
|
||||
}},
|
||||
obj: &certificatesapi.CertificateSigningRequest{Status: certificatesapi.CertificateSigningRequestStatus{
|
||||
Certificate: []byte("data"),
|
||||
}},
|
||||
operation: admission.Update,
|
||||
},
|
||||
allowed: true,
|
||||
authzErr: errors.New("faked error"),
|
||||
},
|
||||
"deny request if authz lookup fails": {
|
||||
allowedName: "abc.com/xyz",
|
||||
attributes: &testAttributes{
|
||||
resource: certificatesapi.Resource("certificatesigningrequests"),
|
||||
subresource: "status",
|
||||
oldObj: &certificatesapi.CertificateSigningRequest{Spec: certificatesapi.CertificateSigningRequestSpec{
|
||||
SignerName: "abc.com/xyz",
|
||||
}},
|
||||
obj: &certificatesapi.CertificateSigningRequest{
|
||||
Spec: certificatesapi.CertificateSigningRequestSpec{
|
||||
SignerName: "abc.com/xyz",
|
||||
},
|
||||
Status: certificatesapi.CertificateSigningRequestStatus{
|
||||
Certificate: []byte("data"),
|
||||
},
|
||||
},
|
||||
operation: admission.Update,
|
||||
},
|
||||
authzErr: errors.New("test"),
|
||||
allowed: false,
|
||||
},
|
||||
"allow request if user is authorized for specific signerName": {
|
||||
allowedName: "abc.com/xyz",
|
||||
attributes: &testAttributes{
|
||||
resource: certificatesapi.Resource("certificatesigningrequests"),
|
||||
subresource: "status",
|
||||
oldObj: &certificatesapi.CertificateSigningRequest{Spec: certificatesapi.CertificateSigningRequestSpec{
|
||||
SignerName: "abc.com/xyz",
|
||||
}},
|
||||
obj: &certificatesapi.CertificateSigningRequest{
|
||||
Spec: certificatesapi.CertificateSigningRequestSpec{
|
||||
SignerName: "abc.com/xyz",
|
||||
},
|
||||
Status: certificatesapi.CertificateSigningRequestStatus{
|
||||
Certificate: []byte("data"),
|
||||
},
|
||||
},
|
||||
operation: admission.Update,
|
||||
},
|
||||
allowed: true,
|
||||
},
|
||||
"allow request if user is authorized with wildcard": {
|
||||
allowedName: "abc.com/*",
|
||||
attributes: &testAttributes{
|
||||
resource: certificatesapi.Resource("certificatesigningrequests"),
|
||||
subresource: "status",
|
||||
oldObj: &certificatesapi.CertificateSigningRequest{Spec: certificatesapi.CertificateSigningRequestSpec{
|
||||
SignerName: "abc.com/xyz",
|
||||
}},
|
||||
obj: &certificatesapi.CertificateSigningRequest{
|
||||
Spec: certificatesapi.CertificateSigningRequestSpec{
|
||||
SignerName: "abc.com/xyz",
|
||||
},
|
||||
Status: certificatesapi.CertificateSigningRequestStatus{
|
||||
Certificate: []byte("data"),
|
||||
},
|
||||
},
|
||||
operation: admission.Update,
|
||||
},
|
||||
allowed: true,
|
||||
},
|
||||
"should deny request if user does not have permission for this signerName": {
|
||||
allowedName: "notabc.com/xyz",
|
||||
attributes: &testAttributes{
|
||||
resource: certificatesapi.Resource("certificatesigningrequests"),
|
||||
subresource: "status",
|
||||
oldObj: &certificatesapi.CertificateSigningRequest{Spec: certificatesapi.CertificateSigningRequestSpec{
|
||||
SignerName: "abc.com/xyz",
|
||||
}},
|
||||
obj: &certificatesapi.CertificateSigningRequest{
|
||||
Spec: certificatesapi.CertificateSigningRequestSpec{
|
||||
SignerName: "abc.com/xyz",
|
||||
},
|
||||
Status: certificatesapi.CertificateSigningRequestStatus{
|
||||
Certificate: []byte("data"),
|
||||
},
|
||||
},
|
||||
operation: admission.Update,
|
||||
},
|
||||
allowed: false,
|
||||
},
|
||||
"should deny request if user attempts to update signerName to a new value they *do* have permission to sign for": {
|
||||
allowedName: "allowed.com/xyz",
|
||||
attributes: &testAttributes{
|
||||
resource: certificatesapi.Resource("certificatesigningrequests"),
|
||||
subresource: "status",
|
||||
oldObj: &certificatesapi.CertificateSigningRequest{Spec: certificatesapi.CertificateSigningRequestSpec{
|
||||
SignerName: "notallowed.com/xyz",
|
||||
}},
|
||||
obj: &certificatesapi.CertificateSigningRequest{
|
||||
Spec: certificatesapi.CertificateSigningRequestSpec{
|
||||
SignerName: "allowed.com/xyz",
|
||||
},
|
||||
Status: certificatesapi.CertificateSigningRequestStatus{
|
||||
Certificate: []byte("data"),
|
||||
},
|
||||
},
|
||||
operation: admission.Update,
|
||||
},
|
||||
allowed: false,
|
||||
},
|
||||
}
|
||||
|
||||
for n, test := range tests {
|
||||
t.Run(n, func(t *testing.T) {
|
||||
p := Plugin{
|
||||
authz: fakeAuthorizer{
|
||||
t: t,
|
||||
verb: "sign",
|
||||
allowedName: test.allowedName,
|
||||
decision: authorizer.DecisionAllow,
|
||||
err: test.authzErr,
|
||||
},
|
||||
}
|
||||
err := p.Validate(context.Background(), test.attributes, nil)
|
||||
if err == nil && !test.allowed {
|
||||
t.Errorf("Expected authorization policy to reject CSR but it was allowed")
|
||||
}
|
||||
if err != nil && test.allowed {
|
||||
t.Errorf("Expected authorization policy to accept CSR but it was rejected: %v", err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
type fakeAuthorizer struct {
|
||||
t *testing.T
|
||||
verb string
|
||||
allowedName string
|
||||
decision authorizer.Decision
|
||||
err error
|
||||
}
|
||||
|
||||
func (f fakeAuthorizer) Authorize(ctx context.Context, a authorizer.Attributes) (authorizer.Decision, string, error) {
|
||||
if f.err != nil {
|
||||
return f.decision, "forced error", f.err
|
||||
}
|
||||
if a.GetVerb() != f.verb {
|
||||
return authorizer.DecisionDeny, fmt.Sprintf("unrecognised verb '%s'", a.GetVerb()), nil
|
||||
}
|
||||
if a.GetAPIGroup() != "certificates.k8s.io" {
|
||||
return authorizer.DecisionDeny, fmt.Sprintf("unrecognised groupName '%s'", a.GetAPIGroup()), nil
|
||||
}
|
||||
if a.GetAPIVersion() != "*" {
|
||||
return authorizer.DecisionDeny, fmt.Sprintf("unrecognised apiVersion '%s'", a.GetAPIVersion()), nil
|
||||
}
|
||||
if a.GetResource() != "signers" {
|
||||
return authorizer.DecisionDeny, fmt.Sprintf("unrecognised resource '%s'", a.GetResource()), nil
|
||||
}
|
||||
if a.GetName() != f.allowedName {
|
||||
return authorizer.DecisionDeny, fmt.Sprintf("unrecognised resource name '%s'", a.GetName()), nil
|
||||
}
|
||||
if !a.IsResourceRequest() {
|
||||
return authorizer.DecisionDeny, fmt.Sprintf("unrecognised IsResourceRequest '%t'", a.IsResourceRequest()), nil
|
||||
}
|
||||
return f.decision, "", nil
|
||||
}
|
||||
|
||||
type testAttributes struct {
|
||||
resource schema.GroupResource
|
||||
subresource string
|
||||
operation admission.Operation
|
||||
oldObj, obj runtime.Object
|
||||
name string
|
||||
|
||||
admission.Attributes // nil panic if any other methods called
|
||||
}
|
||||
|
||||
func (t *testAttributes) GetResource() schema.GroupVersionResource {
|
||||
return t.resource.WithVersion("ignored")
|
||||
}
|
||||
|
||||
func (t *testAttributes) GetSubresource() string {
|
||||
return t.subresource
|
||||
}
|
||||
|
||||
func (t *testAttributes) GetOldObject() runtime.Object {
|
||||
return t.oldObj
|
||||
}
|
||||
|
||||
func (t *testAttributes) GetObject() runtime.Object {
|
||||
return t.obj
|
||||
}
|
||||
|
||||
func (t *testAttributes) GetName() string {
|
||||
return t.name
|
||||
}
|
||||
|
||||
func (t *testAttributes) GetOperation() admission.Operation {
|
||||
return t.operation
|
||||
}
|
||||
|
||||
func (t *testAttributes) GetUserInfo() user.Info {
|
||||
return &user.DefaultInfo{Name: "ignored"}
|
||||
}
|
41
plugin/pkg/admission/certificates/subjectrestriction/BUILD
Normal file
41
plugin/pkg/admission/certificates/subjectrestriction/BUILD
Normal file
@ -0,0 +1,41 @@
|
||||
load("@io_bazel_rules_go//go:def.bzl", "go_library", "go_test")
|
||||
|
||||
go_library(
|
||||
name = "go_default_library",
|
||||
srcs = ["admission.go"],
|
||||
importpath = "k8s.io/kubernetes/plugin/pkg/admission/certificates/subjectrestriction",
|
||||
visibility = ["//visibility:public"],
|
||||
deps = [
|
||||
"//pkg/apis/certificates:go_default_library",
|
||||
"//staging/src/k8s.io/api/certificates/v1beta1:go_default_library",
|
||||
"//staging/src/k8s.io/apiserver/pkg/admission:go_default_library",
|
||||
"//vendor/k8s.io/klog:go_default_library",
|
||||
],
|
||||
)
|
||||
|
||||
go_test(
|
||||
name = "go_default_test",
|
||||
srcs = ["admission_test.go"],
|
||||
embed = [":go_default_library"],
|
||||
deps = [
|
||||
"//pkg/apis/certificates:go_default_library",
|
||||
"//staging/src/k8s.io/api/certificates/v1beta1:go_default_library",
|
||||
"//staging/src/k8s.io/apimachinery/pkg/runtime:go_default_library",
|
||||
"//staging/src/k8s.io/apimachinery/pkg/runtime/schema:go_default_library",
|
||||
"//staging/src/k8s.io/apiserver/pkg/admission:go_default_library",
|
||||
],
|
||||
)
|
||||
|
||||
filegroup(
|
||||
name = "package-srcs",
|
||||
srcs = glob(["**"]),
|
||||
tags = ["automanaged"],
|
||||
visibility = ["//visibility:private"],
|
||||
)
|
||||
|
||||
filegroup(
|
||||
name = "all-srcs",
|
||||
srcs = [":package-srcs"],
|
||||
tags = ["automanaged"],
|
||||
visibility = ["//visibility:public"],
|
||||
)
|
@ -0,0 +1,93 @@
|
||||
/*
|
||||
Copyright 2020 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 subjectrestriction
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
|
||||
certificatesv1beta1 "k8s.io/api/certificates/v1beta1"
|
||||
"k8s.io/apiserver/pkg/admission"
|
||||
"k8s.io/klog"
|
||||
certificatesapi "k8s.io/kubernetes/pkg/apis/certificates"
|
||||
)
|
||||
|
||||
// PluginName is a string with the name of the plugin
|
||||
const PluginName = "CertificateSubjectRestriction"
|
||||
|
||||
// Register registers the plugin
|
||||
func Register(plugins *admission.Plugins) {
|
||||
plugins.Register(PluginName, func(config io.Reader) (admission.Interface, error) {
|
||||
return NewPlugin(), nil
|
||||
})
|
||||
}
|
||||
|
||||
// Plugin holds state for and implements the admission plugin.
|
||||
type Plugin struct {
|
||||
*admission.Handler
|
||||
}
|
||||
|
||||
// ValidateInitialization always returns nil.
|
||||
func (p *Plugin) ValidateInitialization() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
var _ admission.ValidationInterface = &Plugin{}
|
||||
|
||||
// NewPlugin constructs a new instance of the CertificateSubjectRestrictions admission interface.
|
||||
func NewPlugin() *Plugin {
|
||||
return &Plugin{
|
||||
Handler: admission.NewHandler(admission.Create),
|
||||
}
|
||||
}
|
||||
|
||||
var csrGroupResource = certificatesapi.Resource("certificatesigningrequests")
|
||||
|
||||
// Validate ensures that if the signerName on a CSR is set to
|
||||
// `kubernetes.io/kube-apiserver-client`, that its organization (group)
|
||||
// attribute is not set to `system:masters`.
|
||||
func (p *Plugin) Validate(_ context.Context, a admission.Attributes, _ admission.ObjectInterfaces) error {
|
||||
if a.GetResource().GroupResource() != csrGroupResource || a.GetSubresource() != "" {
|
||||
return nil
|
||||
}
|
||||
|
||||
csr, ok := a.GetObject().(*certificatesapi.CertificateSigningRequest)
|
||||
if !ok {
|
||||
return admission.NewForbidden(a, fmt.Errorf("expected type CertificateSigningRequest, got: %T", a.GetObject()))
|
||||
}
|
||||
|
||||
if csr.Spec.SignerName != certificatesv1beta1.KubeAPIServerClientSignerName {
|
||||
return nil
|
||||
}
|
||||
|
||||
csrParsed, err := certificatesapi.ParseCSR(csr)
|
||||
if err != nil {
|
||||
return admission.NewForbidden(a, fmt.Errorf("failed to parse CSR: %v", err))
|
||||
}
|
||||
|
||||
for _, group := range csrParsed.Subject.Organization {
|
||||
if group == "system:masters" {
|
||||
klog.V(4).Infof("CSR %s rejected by admission plugin %s for attempting to use signer %s with system:masters group",
|
||||
csr.Name, PluginName, certificatesv1beta1.KubeAPIServerClientSignerName)
|
||||
return admission.NewForbidden(a, fmt.Errorf("use of %s signer with system:masters group is not allowed",
|
||||
certificatesv1beta1.KubeAPIServerClientSignerName))
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
@ -0,0 +1,189 @@
|
||||
/*
|
||||
Copyright 2020 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 subjectrestriction
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/ed25519"
|
||||
"crypto/rand"
|
||||
"crypto/x509"
|
||||
"crypto/x509/pkix"
|
||||
"encoding/pem"
|
||||
"testing"
|
||||
|
||||
certificatesv1beta1 "k8s.io/api/certificates/v1beta1"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
"k8s.io/apiserver/pkg/admission"
|
||||
certificatesapi "k8s.io/kubernetes/pkg/apis/certificates"
|
||||
)
|
||||
|
||||
func TestPlugin_Validate(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
a admission.Attributes
|
||||
wantErr string
|
||||
}{
|
||||
{
|
||||
name: "ignored resource",
|
||||
a: &testAttributes{
|
||||
resource: schema.GroupResource{
|
||||
Group: "foo",
|
||||
Resource: "bar",
|
||||
},
|
||||
},
|
||||
wantErr: "",
|
||||
},
|
||||
{
|
||||
name: "ignored subresource",
|
||||
a: &testAttributes{
|
||||
resource: certificatesapi.Resource("certificatesigningrequests"),
|
||||
subresource: "approve",
|
||||
},
|
||||
wantErr: "",
|
||||
},
|
||||
{
|
||||
name: "wrong type",
|
||||
a: &testAttributes{
|
||||
resource: certificatesapi.Resource("certificatesigningrequests"),
|
||||
obj: &certificatesapi.CertificateSigningRequestList{},
|
||||
name: "panda",
|
||||
},
|
||||
wantErr: `certificatesigningrequests.certificates.k8s.io "panda" is forbidden: expected type CertificateSigningRequest, got: *certificates.CertificateSigningRequestList`,
|
||||
},
|
||||
{
|
||||
name: "some other signer",
|
||||
a: &testAttributes{
|
||||
resource: certificatesapi.Resource("certificatesigningrequests"),
|
||||
obj: &certificatesapi.CertificateSigningRequest{Spec: certificatesapi.CertificateSigningRequestSpec{
|
||||
Request: pemWithGroup("system:masters"),
|
||||
SignerName: certificatesv1beta1.KubeAPIServerClientKubeletSignerName,
|
||||
}},
|
||||
},
|
||||
wantErr: "",
|
||||
},
|
||||
{
|
||||
name: "invalid request",
|
||||
a: &testAttributes{
|
||||
resource: certificatesapi.Resource("certificatesigningrequests"),
|
||||
obj: &certificatesapi.CertificateSigningRequest{Spec: certificatesapi.CertificateSigningRequestSpec{
|
||||
Request: []byte("this is not a CSR"),
|
||||
SignerName: certificatesv1beta1.KubeAPIServerClientSignerName,
|
||||
}},
|
||||
name: "bear",
|
||||
},
|
||||
wantErr: `certificatesigningrequests.certificates.k8s.io "bear" is forbidden: failed to parse CSR: PEM block type must be CERTIFICATE REQUEST`,
|
||||
},
|
||||
{
|
||||
name: "some other group",
|
||||
a: &testAttributes{
|
||||
resource: certificatesapi.Resource("certificatesigningrequests"),
|
||||
obj: &certificatesapi.CertificateSigningRequest{Spec: certificatesapi.CertificateSigningRequestSpec{
|
||||
Request: pemWithGroup("system:admin"),
|
||||
SignerName: certificatesv1beta1.KubeAPIServerClientSignerName,
|
||||
}},
|
||||
},
|
||||
wantErr: "",
|
||||
},
|
||||
{
|
||||
name: "request for system:masters",
|
||||
a: &testAttributes{
|
||||
resource: certificatesapi.Resource("certificatesigningrequests"),
|
||||
obj: &certificatesapi.CertificateSigningRequest{Spec: certificatesapi.CertificateSigningRequestSpec{
|
||||
Request: pemWithGroup("system:masters"),
|
||||
SignerName: certificatesv1beta1.KubeAPIServerClientSignerName,
|
||||
}},
|
||||
name: "pooh",
|
||||
},
|
||||
wantErr: `certificatesigningrequests.certificates.k8s.io "pooh" is forbidden: use of kubernetes.io/kube-apiserver-client signer with system:masters group is not allowed`,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
p := &Plugin{}
|
||||
if err := p.Validate(context.TODO(), tt.a, nil); errStr(err) != tt.wantErr {
|
||||
t.Errorf("Validate() error = %v, wantErr %v", err, tt.wantErr)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
type testAttributes struct {
|
||||
resource schema.GroupResource
|
||||
subresource string
|
||||
obj runtime.Object
|
||||
name string
|
||||
|
||||
admission.Attributes // nil panic if any other methods called
|
||||
}
|
||||
|
||||
func (t *testAttributes) GetResource() schema.GroupVersionResource {
|
||||
return t.resource.WithVersion("ignored")
|
||||
}
|
||||
|
||||
func (t *testAttributes) GetSubresource() string {
|
||||
return t.subresource
|
||||
}
|
||||
|
||||
func (t *testAttributes) GetObject() runtime.Object {
|
||||
return t.obj
|
||||
}
|
||||
|
||||
func (t *testAttributes) GetName() string {
|
||||
return t.name
|
||||
}
|
||||
|
||||
func errStr(err error) string {
|
||||
if err == nil {
|
||||
return ""
|
||||
}
|
||||
es := err.Error()
|
||||
if len(es) == 0 {
|
||||
panic("invalid empty error")
|
||||
}
|
||||
return es
|
||||
}
|
||||
|
||||
func pemWithGroup(group string) []byte {
|
||||
template := &x509.CertificateRequest{
|
||||
Subject: pkix.Name{
|
||||
Organization: []string{group},
|
||||
},
|
||||
}
|
||||
|
||||
_, key, err := ed25519.GenerateKey(rand.Reader)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
csrDER, err := x509.CreateCertificateRequest(rand.Reader, template, key)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
csrPemBlock := &pem.Block{
|
||||
Type: "CERTIFICATE REQUEST",
|
||||
Bytes: csrDER,
|
||||
}
|
||||
|
||||
p := pem.EncodeToMemory(csrPemBlock)
|
||||
if p == nil {
|
||||
panic("invalid pem block")
|
||||
}
|
||||
|
||||
return p
|
||||
}
|
75
plugin/pkg/admission/certificates/util.go
Normal file
75
plugin/pkg/admission/certificates/util.go
Normal file
@ -0,0 +1,75 @@
|
||||
/*
|
||||
Copyright 2020 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 certificates
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strings"
|
||||
|
||||
"k8s.io/apiserver/pkg/authentication/user"
|
||||
"k8s.io/apiserver/pkg/authorization/authorizer"
|
||||
"k8s.io/klog"
|
||||
)
|
||||
|
||||
// IsAuthorizedForSignerName returns true if 'info' is authorized to perform the given
|
||||
// 'verb' on the synthetic 'signers' resource with the given signerName.
|
||||
// If the user does not have permission to perform the 'verb' on the given signerName,
|
||||
// it will also perform an authorization check against {domain portion}/*, for example
|
||||
// `kubernetes.io/*`. This allows an entity to be granted permission to 'verb' on all
|
||||
// signerNames with a given 'domain portion'.
|
||||
func IsAuthorizedForSignerName(ctx context.Context, authz authorizer.Authorizer, info user.Info, verb, signerName string) bool {
|
||||
// First check if the user has explicit permission to 'verb' for the given signerName.
|
||||
attr := buildAttributes(info, verb, signerName)
|
||||
decision, reason, err := authz.Authorize(ctx, attr)
|
||||
switch {
|
||||
case err != nil:
|
||||
klog.V(3).Infof("cannot authorize %q %q for policy: %v,%v", verb, attr.GetName(), reason, err)
|
||||
case decision == authorizer.DecisionAllow:
|
||||
return true
|
||||
}
|
||||
|
||||
// If not, check if the user has wildcard permissions to 'verb' for the domain portion of the signerName, e.g.
|
||||
// 'kubernetes.io/*'.
|
||||
attr = buildWildcardAttributes(info, verb, signerName)
|
||||
decision, reason, err = authz.Authorize(ctx, attr)
|
||||
switch {
|
||||
case err != nil:
|
||||
klog.V(3).Infof("cannot authorize %q %q for policy: %v,%v", verb, attr.GetName(), reason, err)
|
||||
case decision == authorizer.DecisionAllow:
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
func buildAttributes(info user.Info, verb, signerName string) authorizer.Attributes {
|
||||
return authorizer.AttributesRecord{
|
||||
User: info,
|
||||
Verb: verb,
|
||||
Name: signerName,
|
||||
APIGroup: "certificates.k8s.io",
|
||||
APIVersion: "*",
|
||||
Resource: "signers",
|
||||
ResourceRequest: true,
|
||||
}
|
||||
}
|
||||
|
||||
func buildWildcardAttributes(info user.Info, verb, signerName string) authorizer.Attributes {
|
||||
parts := strings.Split(signerName, "/")
|
||||
domain := parts[0]
|
||||
return buildAttributes(info, verb, domain+"/*")
|
||||
}
|
@ -17,6 +17,7 @@ go_library(
|
||||
deps = [
|
||||
"//pkg/apis/rbac/v1:go_default_library",
|
||||
"//pkg/features:go_default_library",
|
||||
"//staging/src/k8s.io/api/certificates/v1beta1:go_default_library",
|
||||
"//staging/src/k8s.io/api/rbac/v1:go_default_library",
|
||||
"//staging/src/k8s.io/apimachinery/pkg/api/meta:go_default_library",
|
||||
"//staging/src/k8s.io/apimachinery/pkg/apis/meta/v1:go_default_library",
|
||||
|
@ -21,6 +21,7 @@ import (
|
||||
|
||||
"k8s.io/klog"
|
||||
|
||||
capi "k8s.io/api/certificates/v1beta1"
|
||||
rbacv1 "k8s.io/api/rbac/v1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
utilfeature "k8s.io/apiserver/pkg/util/feature"
|
||||
@ -337,6 +338,13 @@ func buildControllerRoles() ([]rbacv1.ClusterRole, []rbacv1.ClusterRoleBinding)
|
||||
Rules: []rbacv1.PolicyRule{
|
||||
rbacv1helpers.NewRule("get", "list", "watch", "delete").Groups(certificatesGroup).Resources("certificatesigningrequests").RuleOrDie(),
|
||||
rbacv1helpers.NewRule("update").Groups(certificatesGroup).Resources("certificatesigningrequests/status", "certificatesigningrequests/approval").RuleOrDie(),
|
||||
rbacv1helpers.NewRule("approve").Groups(certificatesGroup).Resources("signers").Names(capi.KubeAPIServerClientKubeletSignerName).RuleOrDie(),
|
||||
rbacv1helpers.NewRule("sign").Groups(certificatesGroup).Resources("signers").Names(
|
||||
capi.LegacyUnknownSignerName,
|
||||
capi.KubeAPIServerClientSignerName,
|
||||
capi.KubeAPIServerClientKubeletSignerName,
|
||||
capi.KubeletServingSignerName,
|
||||
).RuleOrDie(),
|
||||
rbacv1helpers.NewRule("create").Groups(authorizationGroup).Resources("subjectaccessreviews").RuleOrDie(),
|
||||
eventsRule(),
|
||||
},
|
||||
|
@ -17,6 +17,7 @@ limitations under the License.
|
||||
package bootstrappolicy
|
||||
|
||||
import (
|
||||
capi "k8s.io/api/certificates/v1beta1"
|
||||
rbacv1 "k8s.io/api/rbac/v1"
|
||||
"k8s.io/apimachinery/pkg/api/meta"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
@ -459,6 +460,30 @@ func ClusterRoles() []rbacv1.ClusterRole {
|
||||
rbacv1helpers.NewRule(ReadUpdate...).Groups(legacyGroup).Resources("persistentvolumeclaims").RuleOrDie(),
|
||||
},
|
||||
},
|
||||
{
|
||||
ObjectMeta: metav1.ObjectMeta{Name: "system:certificates.k8s.io:legacy-unknown-approver"},
|
||||
Rules: []rbacv1.PolicyRule{
|
||||
rbacv1helpers.NewRule("approve").Groups(certificatesGroup).Resources("signers").Names(capi.LegacyUnknownSignerName).RuleOrDie(),
|
||||
},
|
||||
},
|
||||
{
|
||||
ObjectMeta: metav1.ObjectMeta{Name: "system:certificates.k8s.io:kubelet-serving-approver"},
|
||||
Rules: []rbacv1.PolicyRule{
|
||||
rbacv1helpers.NewRule("approve").Groups(certificatesGroup).Resources("signers").Names(capi.KubeletServingSignerName).RuleOrDie(),
|
||||
},
|
||||
},
|
||||
{
|
||||
ObjectMeta: metav1.ObjectMeta{Name: "system:certificates.k8s.io:kube-apiserver-client-approver"},
|
||||
Rules: []rbacv1.PolicyRule{
|
||||
rbacv1helpers.NewRule("approve").Groups(certificatesGroup).Resources("signers").Names(capi.KubeAPIServerClientSignerName).RuleOrDie(),
|
||||
},
|
||||
},
|
||||
{
|
||||
ObjectMeta: metav1.ObjectMeta{Name: "system:certificates.k8s.io:kube-apiserver-client-kubelet-approver"},
|
||||
Rules: []rbacv1.PolicyRule{
|
||||
rbacv1helpers.NewRule("approve").Groups(certificatesGroup).Resources("signers").Names(capi.KubeAPIServerClientKubeletSignerName).RuleOrDie(),
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
if utilfeature.DefaultFeatureGate.Enabled(features.ServiceAccountIssuerDiscovery) {
|
||||
|
@ -419,6 +419,78 @@ items:
|
||||
- certificatesigningrequests/selfnodeclient
|
||||
verbs:
|
||||
- create
|
||||
- apiVersion: rbac.authorization.k8s.io/v1
|
||||
kind: ClusterRole
|
||||
metadata:
|
||||
annotations:
|
||||
rbac.authorization.kubernetes.io/autoupdate: "true"
|
||||
creationTimestamp: null
|
||||
labels:
|
||||
kubernetes.io/bootstrapping: rbac-defaults
|
||||
name: system:certificates.k8s.io:kube-apiserver-client-approver
|
||||
rules:
|
||||
- apiGroups:
|
||||
- certificates.k8s.io
|
||||
resourceNames:
|
||||
- kubernetes.io/kube-apiserver-client
|
||||
resources:
|
||||
- signers
|
||||
verbs:
|
||||
- approve
|
||||
- apiVersion: rbac.authorization.k8s.io/v1
|
||||
kind: ClusterRole
|
||||
metadata:
|
||||
annotations:
|
||||
rbac.authorization.kubernetes.io/autoupdate: "true"
|
||||
creationTimestamp: null
|
||||
labels:
|
||||
kubernetes.io/bootstrapping: rbac-defaults
|
||||
name: system:certificates.k8s.io:kube-apiserver-client-kubelet-approver
|
||||
rules:
|
||||
- apiGroups:
|
||||
- certificates.k8s.io
|
||||
resourceNames:
|
||||
- kubernetes.io/kube-apiserver-client-kubelet
|
||||
resources:
|
||||
- signers
|
||||
verbs:
|
||||
- approve
|
||||
- apiVersion: rbac.authorization.k8s.io/v1
|
||||
kind: ClusterRole
|
||||
metadata:
|
||||
annotations:
|
||||
rbac.authorization.kubernetes.io/autoupdate: "true"
|
||||
creationTimestamp: null
|
||||
labels:
|
||||
kubernetes.io/bootstrapping: rbac-defaults
|
||||
name: system:certificates.k8s.io:kubelet-serving-approver
|
||||
rules:
|
||||
- apiGroups:
|
||||
- certificates.k8s.io
|
||||
resourceNames:
|
||||
- kubernetes.io/kubelet-serving
|
||||
resources:
|
||||
- signers
|
||||
verbs:
|
||||
- approve
|
||||
- apiVersion: rbac.authorization.k8s.io/v1
|
||||
kind: ClusterRole
|
||||
metadata:
|
||||
annotations:
|
||||
rbac.authorization.kubernetes.io/autoupdate: "true"
|
||||
creationTimestamp: null
|
||||
labels:
|
||||
kubernetes.io/bootstrapping: rbac-defaults
|
||||
name: system:certificates.k8s.io:legacy-unknown-approver
|
||||
rules:
|
||||
- apiGroups:
|
||||
- certificates.k8s.io
|
||||
resourceNames:
|
||||
- kubernetes.io/legacy-unknown
|
||||
resources:
|
||||
- signers
|
||||
verbs:
|
||||
- approve
|
||||
- apiVersion: rbac.authorization.k8s.io/v1
|
||||
kind: ClusterRole
|
||||
metadata:
|
||||
|
@ -101,6 +101,25 @@ items:
|
||||
- certificatesigningrequests/status
|
||||
verbs:
|
||||
- update
|
||||
- apiGroups:
|
||||
- certificates.k8s.io
|
||||
resourceNames:
|
||||
- kubernetes.io/kube-apiserver-client-kubelet
|
||||
resources:
|
||||
- signers
|
||||
verbs:
|
||||
- approve
|
||||
- apiGroups:
|
||||
- certificates.k8s.io
|
||||
resourceNames:
|
||||
- kubernetes.io/kube-apiserver-client
|
||||
- kubernetes.io/kube-apiserver-client-kubelet
|
||||
- kubernetes.io/kubelet-serving
|
||||
- kubernetes.io/legacy-unknown
|
||||
resources:
|
||||
- signers
|
||||
verbs:
|
||||
- sign
|
||||
- apiGroups:
|
||||
- authorization.k8s.io
|
||||
resources:
|
||||
|
@ -3,16 +3,25 @@ load("@io_bazel_rules_go//go:def.bzl", "go_test")
|
||||
go_test(
|
||||
name = "go_default_test",
|
||||
srcs = [
|
||||
"admission_approval_test.go",
|
||||
"admission_sign_test.go",
|
||||
"admission_subjectrestriction_test.go",
|
||||
"admission_test.go",
|
||||
"defaulting_test.go",
|
||||
"field_selector_test.go",
|
||||
"main_test.go",
|
||||
],
|
||||
tags = ["integration"],
|
||||
deps = [
|
||||
"//cmd/kube-apiserver/app/testing:go_default_library",
|
||||
"//staging/src/k8s.io/api/authorization/v1:go_default_library",
|
||||
"//staging/src/k8s.io/api/certificates/v1beta1:go_default_library",
|
||||
"//staging/src/k8s.io/api/rbac/v1:go_default_library",
|
||||
"//staging/src/k8s.io/apimachinery/pkg/apis/meta/v1:go_default_library",
|
||||
"//staging/src/k8s.io/apimachinery/pkg/runtime/schema:go_default_library",
|
||||
"//staging/src/k8s.io/apimachinery/pkg/util/wait:go_default_library",
|
||||
"//staging/src/k8s.io/client-go/kubernetes:go_default_library",
|
||||
"//staging/src/k8s.io/client-go/kubernetes/typed/authorization/v1:go_default_library",
|
||||
"//staging/src/k8s.io/client-go/kubernetes/typed/certificates/v1beta1:go_default_library",
|
||||
"//staging/src/k8s.io/client-go/rest:go_default_library",
|
||||
"//test/integration/framework:go_default_library",
|
||||
|
147
test/integration/certificates/admission_approval_test.go
Normal file
147
test/integration/certificates/admission_approval_test.go
Normal file
@ -0,0 +1,147 @@
|
||||
/*
|
||||
Copyright 2020 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 certificates
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
certv1beta1 "k8s.io/api/certificates/v1beta1"
|
||||
rbacv1 "k8s.io/api/rbac/v1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
clientset "k8s.io/client-go/kubernetes"
|
||||
restclient "k8s.io/client-go/rest"
|
||||
|
||||
kubeapiservertesting "k8s.io/kubernetes/cmd/kube-apiserver/app/testing"
|
||||
"k8s.io/kubernetes/test/integration/framework"
|
||||
)
|
||||
|
||||
// Verifies that the CSR approval admission plugin correctly enforces that a
|
||||
// user has permission to approve CSRs for the named signer
|
||||
func TestCSRSignerNameApprovalPlugin(t *testing.T) {
|
||||
tests := map[string]struct {
|
||||
allowedSignerName string
|
||||
signerName string
|
||||
error string
|
||||
}{
|
||||
"should admit when a user has permission for the exact signerName": {
|
||||
allowedSignerName: "example.com/something",
|
||||
signerName: "example.com/something",
|
||||
},
|
||||
"should admit when a user has permission for the wildcard-suffixed signerName": {
|
||||
allowedSignerName: "example.com/*",
|
||||
signerName: "example.com/something",
|
||||
},
|
||||
"should deny if a user does not have permission for the given signerName": {
|
||||
allowedSignerName: "example.com/not-something",
|
||||
signerName: "example.com/something",
|
||||
error: `certificatesigningrequests.certificates.k8s.io "csr" is forbidden: user not permitted to approve requests with signerName "example.com/something"`,
|
||||
},
|
||||
}
|
||||
for name, test := range tests {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
// Run an apiserver with the default configuration options.
|
||||
s := kubeapiservertesting.StartTestServerOrDie(t, kubeapiservertesting.NewDefaultTestServerOptions(), []string{"--authorization-mode=RBAC"}, framework.SharedEtcd())
|
||||
defer s.TearDownFn()
|
||||
client := clientset.NewForConfigOrDie(s.ClientConfig)
|
||||
|
||||
// Grant 'test-user' permission to approve CertificateSigningRequests with the specified signerName.
|
||||
const username = "test-user"
|
||||
grantUserPermissionToApproveFor(t, client, username, test.allowedSignerName)
|
||||
// Create a CSR to attempt to approve.
|
||||
csr := createTestingCSR(t, client.CertificatesV1beta1().CertificateSigningRequests(), "csr", test.signerName, "")
|
||||
|
||||
// Create a second client, impersonating the 'test-user' for us to test with.
|
||||
testuserConfig := restclient.CopyConfig(s.ClientConfig)
|
||||
testuserConfig.Impersonate = restclient.ImpersonationConfig{UserName: username}
|
||||
testuserClient := clientset.NewForConfigOrDie(testuserConfig)
|
||||
|
||||
// Attempt to update the Approved condition.
|
||||
csr.Status.Conditions = append(csr.Status.Conditions, certv1beta1.CertificateSigningRequestCondition{
|
||||
Type: certv1beta1.CertificateApproved,
|
||||
Reason: "AutoApproved",
|
||||
Message: "Approved during integration test",
|
||||
})
|
||||
_, err := testuserClient.CertificatesV1beta1().CertificateSigningRequests().UpdateApproval(csr)
|
||||
if err != nil && test.error != err.Error() {
|
||||
t.Errorf("expected error %q but got: %v", test.error, err)
|
||||
}
|
||||
if err == nil && test.error != "" {
|
||||
t.Errorf("expected to get an error %q but got none", test.error)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func grantUserPermissionToApproveFor(t *testing.T, client clientset.Interface, username string, signerNames ...string) {
|
||||
resourceName := "signername-" + username
|
||||
cr := buildApprovalClusterRoleForSigners(resourceName, signerNames...)
|
||||
crb := buildClusterRoleBindingForUser(resourceName, username, cr.Name)
|
||||
if _, err := client.RbacV1().ClusterRoles().Create(context.TODO(), cr, metav1.CreateOptions{}); err != nil {
|
||||
t.Fatalf("unable to create test fixture RBAC rules: %v", err)
|
||||
}
|
||||
if _, err := client.RbacV1().ClusterRoleBindings().Create(context.TODO(), crb, metav1.CreateOptions{}); err != nil {
|
||||
t.Fatalf("unable to create test fixture RBAC rules: %v", err)
|
||||
}
|
||||
approveRule := cr.Rules[0]
|
||||
updateRule := cr.Rules[1]
|
||||
waitForNamedAuthorizationUpdate(t, client.AuthorizationV1(), username, "", approveRule.Verbs[0], approveRule.ResourceNames[0], schema.GroupResource{Group: approveRule.APIGroups[0], Resource: approveRule.Resources[0]}, true)
|
||||
waitForNamedAuthorizationUpdate(t, client.AuthorizationV1(), username, "", updateRule.Verbs[0], "", schema.GroupResource{Group: updateRule.APIGroups[0], Resource: updateRule.Resources[0]}, true)
|
||||
}
|
||||
|
||||
func buildApprovalClusterRoleForSigners(name string, signerNames ...string) *rbacv1.ClusterRole {
|
||||
return &rbacv1.ClusterRole{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: name,
|
||||
},
|
||||
Rules: []rbacv1.PolicyRule{
|
||||
// must have permission to 'approve' the 'certificatesigners' named
|
||||
// 'signerName' to approve CSRs with the given signerName.
|
||||
{
|
||||
Verbs: []string{"approve"},
|
||||
APIGroups: []string{certv1beta1.SchemeGroupVersion.Group},
|
||||
Resources: []string{"signers"},
|
||||
ResourceNames: signerNames,
|
||||
},
|
||||
{
|
||||
Verbs: []string{"update"},
|
||||
APIGroups: []string{certv1beta1.SchemeGroupVersion.Group},
|
||||
Resources: []string{"certificatesigningrequests/approval"},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func buildClusterRoleBindingForUser(name, username, clusterRoleName string) *rbacv1.ClusterRoleBinding {
|
||||
return &rbacv1.ClusterRoleBinding{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: name,
|
||||
},
|
||||
Subjects: []rbacv1.Subject{
|
||||
{
|
||||
Kind: rbacv1.UserKind,
|
||||
Name: username,
|
||||
},
|
||||
},
|
||||
RoleRef: rbacv1.RoleRef{
|
||||
APIGroup: rbacv1.SchemeGroupVersion.Group,
|
||||
Kind: "ClusterRole",
|
||||
Name: clusterRoleName,
|
||||
},
|
||||
}
|
||||
}
|
124
test/integration/certificates/admission_sign_test.go
Normal file
124
test/integration/certificates/admission_sign_test.go
Normal file
@ -0,0 +1,124 @@
|
||||
/*
|
||||
Copyright 2020 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 certificates
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
certv1beta1 "k8s.io/api/certificates/v1beta1"
|
||||
rbacv1 "k8s.io/api/rbac/v1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
clientset "k8s.io/client-go/kubernetes"
|
||||
restclient "k8s.io/client-go/rest"
|
||||
|
||||
kubeapiservertesting "k8s.io/kubernetes/cmd/kube-apiserver/app/testing"
|
||||
"k8s.io/kubernetes/test/integration/framework"
|
||||
)
|
||||
|
||||
// Verifies that the CSR approval admission plugin correctly enforces that a
|
||||
// user has permission to sign CSRs for the named signer
|
||||
func TestCSRSignerNameSigningPlugin(t *testing.T) {
|
||||
tests := map[string]struct {
|
||||
allowedSignerName string
|
||||
signerName string
|
||||
error string
|
||||
}{
|
||||
"should admit when a user has permission for the exact signerName": {
|
||||
allowedSignerName: "example.com/something",
|
||||
signerName: "example.com/something",
|
||||
},
|
||||
"should admit when a user has permission for the wildcard-suffixed signerName": {
|
||||
allowedSignerName: "example.com/*",
|
||||
signerName: "example.com/something",
|
||||
},
|
||||
"should deny if a user does not have permission for the given signerName": {
|
||||
allowedSignerName: "example.com/not-something",
|
||||
signerName: "example.com/something",
|
||||
error: `certificatesigningrequests.certificates.k8s.io "csr" is forbidden: user not permitted to sign requests with signerName "example.com/something"`,
|
||||
},
|
||||
}
|
||||
for name, test := range tests {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
// Run an apiserver with the default configuration options.
|
||||
s := kubeapiservertesting.StartTestServerOrDie(t, kubeapiservertesting.NewDefaultTestServerOptions(), []string{"--authorization-mode=RBAC"}, framework.SharedEtcd())
|
||||
defer s.TearDownFn()
|
||||
client := clientset.NewForConfigOrDie(s.ClientConfig)
|
||||
|
||||
// Grant 'test-user' permission to sign CertificateSigningRequests with the specified signerName.
|
||||
const username = "test-user"
|
||||
grantUserPermissionToSignFor(t, client, username, test.allowedSignerName)
|
||||
// Create a CSR to attempt to sign.
|
||||
csr := createTestingCSR(t, client.CertificatesV1beta1().CertificateSigningRequests(), "csr", test.signerName, "")
|
||||
|
||||
// Create a second client, impersonating the 'test-user' for us to test with.
|
||||
testuserConfig := restclient.CopyConfig(s.ClientConfig)
|
||||
testuserConfig.Impersonate = restclient.ImpersonationConfig{UserName: username}
|
||||
testuserClient := clientset.NewForConfigOrDie(testuserConfig)
|
||||
|
||||
// Attempt to 'sign' the certificate.
|
||||
csr.Status.Certificate = []byte("dummy data")
|
||||
_, err := testuserClient.CertificatesV1beta1().CertificateSigningRequests().UpdateStatus(context.TODO(), csr, metav1.UpdateOptions{})
|
||||
if err != nil && test.error != err.Error() {
|
||||
t.Errorf("expected error %q but got: %v", test.error, err)
|
||||
}
|
||||
if err == nil && test.error != "" {
|
||||
t.Errorf("expected to get an error %q but got none", test.error)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func grantUserPermissionToSignFor(t *testing.T, client clientset.Interface, username string, signerNames ...string) {
|
||||
resourceName := "signername-" + username
|
||||
cr := buildSigningClusterRoleForSigners(resourceName, signerNames...)
|
||||
crb := buildClusterRoleBindingForUser(resourceName, username, cr.Name)
|
||||
if _, err := client.RbacV1().ClusterRoles().Create(context.TODO(), cr, metav1.CreateOptions{}); err != nil {
|
||||
t.Fatalf("failed to create test fixtures: %v", err)
|
||||
}
|
||||
if _, err := client.RbacV1().ClusterRoleBindings().Create(context.TODO(), crb, metav1.CreateOptions{}); err != nil {
|
||||
t.Fatalf("failed to create test fixtures: %v", err)
|
||||
}
|
||||
signRule := cr.Rules[0]
|
||||
statusRule := cr.Rules[1]
|
||||
waitForNamedAuthorizationUpdate(t, client.AuthorizationV1(), username, "", signRule.Verbs[0], signRule.ResourceNames[0], schema.GroupResource{Group: signRule.APIGroups[0], Resource: signRule.Resources[0]}, true)
|
||||
waitForNamedAuthorizationUpdate(t, client.AuthorizationV1(), username, "", statusRule.Verbs[0], "", schema.GroupResource{Group: statusRule.APIGroups[0], Resource: statusRule.Resources[0]}, true)
|
||||
}
|
||||
|
||||
func buildSigningClusterRoleForSigners(name string, signerNames ...string) *rbacv1.ClusterRole {
|
||||
return &rbacv1.ClusterRole{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: name,
|
||||
},
|
||||
Rules: []rbacv1.PolicyRule{
|
||||
// must have permission to 'approve' the 'certificatesigners' named
|
||||
// 'signerName' to approve CSRs with the given signerName.
|
||||
{
|
||||
Verbs: []string{"sign"},
|
||||
APIGroups: []string{certv1beta1.SchemeGroupVersion.Group},
|
||||
Resources: []string{"signers"},
|
||||
ResourceNames: signerNames,
|
||||
},
|
||||
{
|
||||
Verbs: []string{"update"},
|
||||
APIGroups: []string{certv1beta1.SchemeGroupVersion.Group},
|
||||
Resources: []string{"certificatesigningrequests/status"},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
@ -0,0 +1,70 @@
|
||||
/*
|
||||
Copyright 2020 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 certificates
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
certv1beta1 "k8s.io/api/certificates/v1beta1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
clientset "k8s.io/client-go/kubernetes"
|
||||
|
||||
kubeapiservertesting "k8s.io/kubernetes/cmd/kube-apiserver/app/testing"
|
||||
"k8s.io/kubernetes/test/integration/framework"
|
||||
)
|
||||
|
||||
// Verifies that the CertificateSubjectRestriction admission controller works as expected.
|
||||
func TestCertificateSubjectRestrictionPlugin(t *testing.T) {
|
||||
tests := map[string]struct {
|
||||
signerName string
|
||||
group string
|
||||
error string
|
||||
}{
|
||||
"should reject a request if signerName is kube-apiserver-client and group is system:masters": {
|
||||
signerName: certv1beta1.KubeAPIServerClientSignerName,
|
||||
group: "system:masters",
|
||||
error: `certificatesigningrequests.certificates.k8s.io "csr" is forbidden: use of kubernetes.io/kube-apiserver-client signer with system:masters group is not allowed`,
|
||||
},
|
||||
"should admit a request if signerName is NOT kube-apiserver-client and org is system:masters": {
|
||||
signerName: certv1beta1.LegacyUnknownSignerName,
|
||||
group: "system:masters",
|
||||
},
|
||||
"should admit a request if signerName is kube-apiserver-client and group is NOT system:masters": {
|
||||
signerName: certv1beta1.KubeAPIServerClientSignerName,
|
||||
group: "system:notmasters",
|
||||
},
|
||||
}
|
||||
for name, test := range tests {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
// Run an apiserver with the default configuration options.
|
||||
s := kubeapiservertesting.StartTestServerOrDie(t, kubeapiservertesting.NewDefaultTestServerOptions(), []string{""}, framework.SharedEtcd())
|
||||
defer s.TearDownFn()
|
||||
client := clientset.NewForConfigOrDie(s.ClientConfig)
|
||||
|
||||
// Attempt to create the CSR resource.
|
||||
csr := buildTestingCSR("csr", test.signerName, test.group)
|
||||
_, err := client.CertificatesV1beta1().CertificateSigningRequests().Create(context.TODO(), csr, metav1.CreateOptions{})
|
||||
if err != nil && test.error != err.Error() {
|
||||
t.Errorf("expected error %q but got: %v", test.error, err)
|
||||
}
|
||||
if err == nil && test.error != "" {
|
||||
t.Errorf("expected to get an error %q but got none", test.error)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
59
test/integration/certificates/admission_test.go
Normal file
59
test/integration/certificates/admission_test.go
Normal file
@ -0,0 +1,59 @@
|
||||
/*
|
||||
Copyright 2020 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 certificates
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
authorizationv1 "k8s.io/api/authorization/v1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
"k8s.io/apimachinery/pkg/util/wait"
|
||||
v1authorization "k8s.io/client-go/kubernetes/typed/authorization/v1"
|
||||
)
|
||||
|
||||
// waitForNamedAuthorizationUpdate checks if the given user can perform the named verb and action on the named resource.
|
||||
// Copied from k8s.io/kubernetes/test/e2e/framework/auth.
|
||||
func waitForNamedAuthorizationUpdate(t *testing.T, c v1authorization.SubjectAccessReviewsGetter, user, namespace, verb, resourceName string, resource schema.GroupResource, allowed bool) {
|
||||
review := &authorizationv1.SubjectAccessReview{
|
||||
Spec: authorizationv1.SubjectAccessReviewSpec{
|
||||
ResourceAttributes: &authorizationv1.ResourceAttributes{
|
||||
Group: resource.Group,
|
||||
Verb: verb,
|
||||
Resource: resource.Resource,
|
||||
Namespace: namespace,
|
||||
Name: resourceName,
|
||||
},
|
||||
User: user,
|
||||
},
|
||||
}
|
||||
|
||||
if err := wait.Poll(time.Millisecond*100, time.Second*5, func() (bool, error) {
|
||||
response, err := c.SubjectAccessReviews().Create(context.TODO(), review, metav1.CreateOptions{})
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
if response.Status.Allowed != allowed {
|
||||
return false, nil
|
||||
}
|
||||
return true, nil
|
||||
}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
@ -43,14 +43,14 @@ func TestCSRSignerNameDefaulting(t *testing.T) {
|
||||
}{
|
||||
"defaults to legacy-unknown if not recognised": {
|
||||
csr: capi.CertificateSigningRequestSpec{
|
||||
Request: testCSRPEM,
|
||||
Request: pemWithGroup(""),
|
||||
Usages: []capi.KeyUsage{capi.UsageKeyEncipherment, capi.UsageDigitalSignature},
|
||||
},
|
||||
expectedSignerName: capi.LegacyUnknownSignerName,
|
||||
},
|
||||
"does not default signerName if an explicit value is provided": {
|
||||
csr: capi.CertificateSigningRequestSpec{
|
||||
Request: testCSRPEM,
|
||||
Request: pemWithGroup(""),
|
||||
Usages: []capi.KeyUsage{capi.UsageKeyEncipherment, capi.UsageDigitalSignature},
|
||||
SignerName: strPtr("example.com/my-custom-signer"),
|
||||
},
|
||||
|
@ -18,6 +18,11 @@ package certificates
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/ed25519"
|
||||
"crypto/rand"
|
||||
"crypto/x509"
|
||||
"crypto/x509/pkix"
|
||||
"encoding/pem"
|
||||
"testing"
|
||||
|
||||
capi "k8s.io/api/certificates/v1beta1"
|
||||
@ -37,11 +42,11 @@ func TestCSRSignerNameFieldSelector(t *testing.T) {
|
||||
|
||||
client := clientset.NewForConfigOrDie(&restclient.Config{Host: s.URL, ContentConfig: restclient.ContentConfig{GroupVersion: &schema.GroupVersion{Group: "", Version: "v1"}}})
|
||||
csrClient := client.CertificatesV1beta1().CertificateSigningRequests()
|
||||
csr1 := createTestingCSR(t, csrClient, "csr-1", "example.com/signer-name-1")
|
||||
csr2 := createTestingCSR(t, csrClient, "csr-2", "example.com/signer-name-2")
|
||||
csr1 := createTestingCSR(t, csrClient, "csr-1", "example.com/signer-name-1", "")
|
||||
csr2 := createTestingCSR(t, csrClient, "csr-2", "example.com/signer-name-2", "")
|
||||
// csr3 has the same signerName as csr2 so we can ensure multiple items are returned when running a filtered
|
||||
// LIST call.
|
||||
csr3 := createTestingCSR(t, csrClient, "csr-3", "example.com/signer-name-2")
|
||||
csr3 := createTestingCSR(t, csrClient, "csr-3", "example.com/signer-name-2", "")
|
||||
|
||||
signerOneList, err := client.CertificatesV1beta1().CertificateSigningRequests().List(context.TODO(), metav1.ListOptions{FieldSelector: "spec.signerName=example.com/signer-name-1"})
|
||||
if err != nil {
|
||||
@ -68,45 +73,55 @@ func TestCSRSignerNameFieldSelector(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func createTestingCSR(t *testing.T, certClient certclientset.CertificateSigningRequestInterface, name, signerName string) *capi.CertificateSigningRequest {
|
||||
csr, err := certClient.Create(context.TODO(), buildTestingCSR(name, signerName), metav1.CreateOptions{})
|
||||
func createTestingCSR(t *testing.T, certClient certclientset.CertificateSigningRequestInterface, name, signerName, groupName string) *capi.CertificateSigningRequest {
|
||||
csr, err := certClient.Create(context.TODO(), buildTestingCSR(name, signerName, groupName), metav1.CreateOptions{})
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create testing CSR: %v", err)
|
||||
}
|
||||
return csr
|
||||
}
|
||||
|
||||
func buildTestingCSR(name, signerName string) *capi.CertificateSigningRequest {
|
||||
func buildTestingCSR(name, signerName, groupName string) *capi.CertificateSigningRequest {
|
||||
return &capi.CertificateSigningRequest{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: name,
|
||||
},
|
||||
Spec: capi.CertificateSigningRequestSpec{
|
||||
SignerName: &signerName,
|
||||
Request: testCSRPEM,
|
||||
Request: pemWithGroup(groupName),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
var (
|
||||
// The contents of this CSR do not matter, and it is only used to allow the
|
||||
// CSR resource submitted during integration tests to pass through
|
||||
// validation.
|
||||
testCSRPEM = []byte(`-----BEGIN CERTIFICATE REQUEST-----
|
||||
MIICrzCCAZcCAQAwajENMAsGA1UECAwESE9OSzENMAsGA1UEBwwESE9OSzENMAsG
|
||||
A1UECgwESE9OSzENMAsGA1UECwwESE9OSzENMAsGA1UEAwwESE9OSzEdMBsGCSqG
|
||||
SIb3DQEJARYOaG9ua0Bob25rLmhvbmswggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAw
|
||||
ggEKAoIBAQDhw6t5C0Vtzzl4jQVMM9S2epAKOyKZCXRYC50sG8UFectfSALJHPUY
|
||||
rv3LNfUTSkqg+EJO+5an1PeQS+GK94DUiJ2cUR2hBiTfXenyDAm2fGSDIqLQ/YcZ
|
||||
fprwlqMu3YfpMH1KyyNORoOgWgsyWP0rBIRoWEFcFNaBu7BazaJHQIYNpcyRkHJC
|
||||
610It4MV5dUqNFAfYqmxqlkMa4lR0U4f8cCA3J+lajNOMz/GkPotBINU+xX4bVob
|
||||
Q+ghAatgiZnEvC6pe0LqG788SHaIu7hArSK8ZG7+HcqCwISFLJiA8+A6HE24PhQC
|
||||
69pGqHePAFO4a09c5/MTPfBfohYkEGX7AgMBAAGgADANBgkqhkiG9w0BAQsFAAOC
|
||||
AQEAwg/7CWhWZICusSKEeIHJE+rgeSySAgL0S05KJKtwjHK1zf2B8Az4F2pe0aCe
|
||||
r+mqNyFutmaLOXmNH7H1BJuw0wXeEg8wlT3nknRTJ4EWYf4G0H1dOICk/tB4Mgl1
|
||||
qgmMcP37QQRCMit5VY9BOKfXo+AHCH9rwmX91mXwzyejY/wO6Y3R6Y+GvMKA259F
|
||||
zRt2J8VJkeeXOE/H93putfT1KcmayTwO0gTzPFd7ZZzLSVMnpirxCUujkduxy8DK
|
||||
dDcZdaTZofztqa5ej1gzptxU6fBfVvl3Wevc30yDH5Dum0aiohJbijncgIR6SQx5
|
||||
6nuYWH340f/Ivm5b1gyEqb12ag==
|
||||
-----END CERTIFICATE REQUEST-----`)
|
||||
)
|
||||
func pemWithGroup(group string) []byte {
|
||||
template := &x509.CertificateRequest{
|
||||
Subject: pkix.Name{
|
||||
Organization: []string{group},
|
||||
},
|
||||
}
|
||||
return pemWithTemplate(template)
|
||||
}
|
||||
|
||||
func pemWithTemplate(template *x509.CertificateRequest) []byte {
|
||||
_, key, err := ed25519.GenerateKey(rand.Reader)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
csrDER, err := x509.CreateCertificateRequest(rand.Reader, template, key)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
csrPemBlock := &pem.Block{
|
||||
Type: "CERTIFICATE REQUEST",
|
||||
Bytes: csrDER,
|
||||
}
|
||||
|
||||
p := pem.EncodeToMemory(csrPemBlock)
|
||||
if p == nil {
|
||||
panic("invalid pem block")
|
||||
}
|
||||
|
||||
return p
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user