Add Certificate signerName admission plugins

This commit is contained in:
James Munnelly 2020-01-07 23:05:45 +00:00
parent a983356caa
commit d7e10f9869
27 changed files with 1793 additions and 31 deletions

View File

@ -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",

View File

@ -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)

View File

@ -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",

View 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",
],
)

View 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

View 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"],
)

View 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
}

View 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"}
}

View 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"],
)

View 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
}

View 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"}
}

View 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"],
)

View File

@ -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
}

View File

@ -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
}

View 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+"/*")
}

View File

@ -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",

View File

@ -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(),
},

View File

@ -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) {

View File

@ -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:

View File

@ -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:

View File

@ -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",

View 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,
},
}
}

View 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"},
},
},
}
}

View File

@ -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)
}
})
}
}

View 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)
}
}

View File

@ -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"),
},

View File

@ -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
}