mirror of
https://github.com/k3s-io/kubernetes.git
synced 2025-07-22 03:11:40 +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/admit:go_default_library",
|
||||||
"//plugin/pkg/admission/alwayspullimages:go_default_library",
|
"//plugin/pkg/admission/alwayspullimages:go_default_library",
|
||||||
"//plugin/pkg/admission/antiaffinity: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/defaulttolerationseconds:go_default_library",
|
||||||
"//plugin/pkg/admission/deny:go_default_library",
|
"//plugin/pkg/admission/deny:go_default_library",
|
||||||
"//plugin/pkg/admission/eventratelimit: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/admit"
|
||||||
"k8s.io/kubernetes/plugin/pkg/admission/alwayspullimages"
|
"k8s.io/kubernetes/plugin/pkg/admission/alwayspullimages"
|
||||||
"k8s.io/kubernetes/plugin/pkg/admission/antiaffinity"
|
"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/defaulttolerationseconds"
|
||||||
"k8s.io/kubernetes/plugin/pkg/admission/deny"
|
"k8s.io/kubernetes/plugin/pkg/admission/deny"
|
||||||
"k8s.io/kubernetes/plugin/pkg/admission/eventratelimit"
|
"k8s.io/kubernetes/plugin/pkg/admission/eventratelimit"
|
||||||
@ -87,6 +90,9 @@ var AllOrderedPlugins = []string{
|
|||||||
gc.PluginName, // OwnerReferencesPermissionEnforcement
|
gc.PluginName, // OwnerReferencesPermissionEnforcement
|
||||||
resize.PluginName, // PersistentVolumeClaimResize
|
resize.PluginName, // PersistentVolumeClaimResize
|
||||||
runtimeclass.PluginName, // RuntimeClass
|
runtimeclass.PluginName, // RuntimeClass
|
||||||
|
certapproval.PluginName, // CertificateApproval
|
||||||
|
certsigning.PluginName, // CertificateSigning
|
||||||
|
certsubjectrestriction.PluginName, // CertificateSubjectRestriction
|
||||||
|
|
||||||
// new admission plugins should generally be inserted above here
|
// new admission plugins should generally be inserted above here
|
||||||
// webhook, resourcequota, and deny plugins must go at the end
|
// webhook, resourcequota, and deny plugins must go at the end
|
||||||
@ -128,6 +134,9 @@ func RegisterAllAdmissionPlugins(plugins *admission.Plugins) {
|
|||||||
setdefault.Register(plugins)
|
setdefault.Register(plugins)
|
||||||
resize.Register(plugins)
|
resize.Register(plugins)
|
||||||
storageobjectinuseprotection.Register(plugins)
|
storageobjectinuseprotection.Register(plugins)
|
||||||
|
certapproval.Register(plugins)
|
||||||
|
certsigning.Register(plugins)
|
||||||
|
certsubjectrestriction.Register(plugins)
|
||||||
}
|
}
|
||||||
|
|
||||||
// DefaultOffAdmissionPlugins get admission plugins off by default for kube-apiserver.
|
// DefaultOffAdmissionPlugins get admission plugins off by default for kube-apiserver.
|
||||||
@ -146,6 +155,9 @@ func DefaultOffAdmissionPlugins() sets.String {
|
|||||||
podpriority.PluginName, //PodPriority
|
podpriority.PluginName, //PodPriority
|
||||||
nodetaint.PluginName, //TaintNodesByCondition
|
nodetaint.PluginName, //TaintNodesByCondition
|
||||||
runtimeclass.PluginName, //RuntimeClass, gates internally on the feature
|
runtimeclass.PluginName, //RuntimeClass, gates internally on the feature
|
||||||
|
certapproval.PluginName, // CertificateApproval
|
||||||
|
certsigning.PluginName, // CertificateSigning
|
||||||
|
certsubjectrestriction.PluginName, // CertificateSubjectRestriction
|
||||||
)
|
)
|
||||||
|
|
||||||
return sets.NewString(AllOrderedPlugins...).Difference(defaultOnPlugins)
|
return sets.NewString(AllOrderedPlugins...).Difference(defaultOnPlugins)
|
||||||
|
@ -14,6 +14,7 @@ filegroup(
|
|||||||
"//plugin/pkg/admission/admit:all-srcs",
|
"//plugin/pkg/admission/admit:all-srcs",
|
||||||
"//plugin/pkg/admission/alwayspullimages:all-srcs",
|
"//plugin/pkg/admission/alwayspullimages:all-srcs",
|
||||||
"//plugin/pkg/admission/antiaffinity:all-srcs",
|
"//plugin/pkg/admission/antiaffinity:all-srcs",
|
||||||
|
"//plugin/pkg/admission/certificates:all-srcs",
|
||||||
"//plugin/pkg/admission/defaulttolerationseconds:all-srcs",
|
"//plugin/pkg/admission/defaulttolerationseconds:all-srcs",
|
||||||
"//plugin/pkg/admission/deny:all-srcs",
|
"//plugin/pkg/admission/deny:all-srcs",
|
||||||
"//plugin/pkg/admission/eventratelimit: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 = [
|
deps = [
|
||||||
"//pkg/apis/rbac/v1:go_default_library",
|
"//pkg/apis/rbac/v1:go_default_library",
|
||||||
"//pkg/features: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/api/rbac/v1:go_default_library",
|
||||||
"//staging/src/k8s.io/apimachinery/pkg/api/meta: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",
|
"//staging/src/k8s.io/apimachinery/pkg/apis/meta/v1:go_default_library",
|
||||||
|
@ -21,6 +21,7 @@ import (
|
|||||||
|
|
||||||
"k8s.io/klog"
|
"k8s.io/klog"
|
||||||
|
|
||||||
|
capi "k8s.io/api/certificates/v1beta1"
|
||||||
rbacv1 "k8s.io/api/rbac/v1"
|
rbacv1 "k8s.io/api/rbac/v1"
|
||||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||||
utilfeature "k8s.io/apiserver/pkg/util/feature"
|
utilfeature "k8s.io/apiserver/pkg/util/feature"
|
||||||
@ -337,6 +338,13 @@ func buildControllerRoles() ([]rbacv1.ClusterRole, []rbacv1.ClusterRoleBinding)
|
|||||||
Rules: []rbacv1.PolicyRule{
|
Rules: []rbacv1.PolicyRule{
|
||||||
rbacv1helpers.NewRule("get", "list", "watch", "delete").Groups(certificatesGroup).Resources("certificatesigningrequests").RuleOrDie(),
|
rbacv1helpers.NewRule("get", "list", "watch", "delete").Groups(certificatesGroup).Resources("certificatesigningrequests").RuleOrDie(),
|
||||||
rbacv1helpers.NewRule("update").Groups(certificatesGroup).Resources("certificatesigningrequests/status", "certificatesigningrequests/approval").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(),
|
rbacv1helpers.NewRule("create").Groups(authorizationGroup).Resources("subjectaccessreviews").RuleOrDie(),
|
||||||
eventsRule(),
|
eventsRule(),
|
||||||
},
|
},
|
||||||
|
@ -17,6 +17,7 @@ limitations under the License.
|
|||||||
package bootstrappolicy
|
package bootstrappolicy
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
capi "k8s.io/api/certificates/v1beta1"
|
||||||
rbacv1 "k8s.io/api/rbac/v1"
|
rbacv1 "k8s.io/api/rbac/v1"
|
||||||
"k8s.io/apimachinery/pkg/api/meta"
|
"k8s.io/apimachinery/pkg/api/meta"
|
||||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||||
@ -459,6 +460,30 @@ func ClusterRoles() []rbacv1.ClusterRole {
|
|||||||
rbacv1helpers.NewRule(ReadUpdate...).Groups(legacyGroup).Resources("persistentvolumeclaims").RuleOrDie(),
|
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) {
|
if utilfeature.DefaultFeatureGate.Enabled(features.ServiceAccountIssuerDiscovery) {
|
||||||
|
@ -419,6 +419,78 @@ items:
|
|||||||
- certificatesigningrequests/selfnodeclient
|
- certificatesigningrequests/selfnodeclient
|
||||||
verbs:
|
verbs:
|
||||||
- create
|
- 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
|
- apiVersion: rbac.authorization.k8s.io/v1
|
||||||
kind: ClusterRole
|
kind: ClusterRole
|
||||||
metadata:
|
metadata:
|
||||||
|
@ -101,6 +101,25 @@ items:
|
|||||||
- certificatesigningrequests/status
|
- certificatesigningrequests/status
|
||||||
verbs:
|
verbs:
|
||||||
- update
|
- 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:
|
- apiGroups:
|
||||||
- authorization.k8s.io
|
- authorization.k8s.io
|
||||||
resources:
|
resources:
|
||||||
|
@ -3,16 +3,25 @@ load("@io_bazel_rules_go//go:def.bzl", "go_test")
|
|||||||
go_test(
|
go_test(
|
||||||
name = "go_default_test",
|
name = "go_default_test",
|
||||||
srcs = [
|
srcs = [
|
||||||
|
"admission_approval_test.go",
|
||||||
|
"admission_sign_test.go",
|
||||||
|
"admission_subjectrestriction_test.go",
|
||||||
|
"admission_test.go",
|
||||||
"defaulting_test.go",
|
"defaulting_test.go",
|
||||||
"field_selector_test.go",
|
"field_selector_test.go",
|
||||||
"main_test.go",
|
"main_test.go",
|
||||||
],
|
],
|
||||||
tags = ["integration"],
|
tags = ["integration"],
|
||||||
deps = [
|
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/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/apis/meta/v1:go_default_library",
|
||||||
"//staging/src/k8s.io/apimachinery/pkg/runtime/schema: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: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/kubernetes/typed/certificates/v1beta1:go_default_library",
|
||||||
"//staging/src/k8s.io/client-go/rest:go_default_library",
|
"//staging/src/k8s.io/client-go/rest:go_default_library",
|
||||||
"//test/integration/framework: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": {
|
"defaults to legacy-unknown if not recognised": {
|
||||||
csr: capi.CertificateSigningRequestSpec{
|
csr: capi.CertificateSigningRequestSpec{
|
||||||
Request: testCSRPEM,
|
Request: pemWithGroup(""),
|
||||||
Usages: []capi.KeyUsage{capi.UsageKeyEncipherment, capi.UsageDigitalSignature},
|
Usages: []capi.KeyUsage{capi.UsageKeyEncipherment, capi.UsageDigitalSignature},
|
||||||
},
|
},
|
||||||
expectedSignerName: capi.LegacyUnknownSignerName,
|
expectedSignerName: capi.LegacyUnknownSignerName,
|
||||||
},
|
},
|
||||||
"does not default signerName if an explicit value is provided": {
|
"does not default signerName if an explicit value is provided": {
|
||||||
csr: capi.CertificateSigningRequestSpec{
|
csr: capi.CertificateSigningRequestSpec{
|
||||||
Request: testCSRPEM,
|
Request: pemWithGroup(""),
|
||||||
Usages: []capi.KeyUsage{capi.UsageKeyEncipherment, capi.UsageDigitalSignature},
|
Usages: []capi.KeyUsage{capi.UsageKeyEncipherment, capi.UsageDigitalSignature},
|
||||||
SignerName: strPtr("example.com/my-custom-signer"),
|
SignerName: strPtr("example.com/my-custom-signer"),
|
||||||
},
|
},
|
||||||
|
@ -18,6 +18,11 @@ package certificates
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"crypto/ed25519"
|
||||||
|
"crypto/rand"
|
||||||
|
"crypto/x509"
|
||||||
|
"crypto/x509/pkix"
|
||||||
|
"encoding/pem"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
capi "k8s.io/api/certificates/v1beta1"
|
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"}}})
|
client := clientset.NewForConfigOrDie(&restclient.Config{Host: s.URL, ContentConfig: restclient.ContentConfig{GroupVersion: &schema.GroupVersion{Group: "", Version: "v1"}}})
|
||||||
csrClient := client.CertificatesV1beta1().CertificateSigningRequests()
|
csrClient := client.CertificatesV1beta1().CertificateSigningRequests()
|
||||||
csr1 := createTestingCSR(t, csrClient, "csr-1", "example.com/signer-name-1")
|
csr1 := createTestingCSR(t, csrClient, "csr-1", "example.com/signer-name-1", "")
|
||||||
csr2 := createTestingCSR(t, csrClient, "csr-2", "example.com/signer-name-2")
|
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
|
// csr3 has the same signerName as csr2 so we can ensure multiple items are returned when running a filtered
|
||||||
// LIST call.
|
// 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"})
|
signerOneList, err := client.CertificatesV1beta1().CertificateSigningRequests().List(context.TODO(), metav1.ListOptions{FieldSelector: "spec.signerName=example.com/signer-name-1"})
|
||||||
if err != nil {
|
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 {
|
func createTestingCSR(t *testing.T, certClient certclientset.CertificateSigningRequestInterface, name, signerName, groupName string) *capi.CertificateSigningRequest {
|
||||||
csr, err := certClient.Create(context.TODO(), buildTestingCSR(name, signerName), metav1.CreateOptions{})
|
csr, err := certClient.Create(context.TODO(), buildTestingCSR(name, signerName, groupName), metav1.CreateOptions{})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("failed to create testing CSR: %v", err)
|
t.Fatalf("failed to create testing CSR: %v", err)
|
||||||
}
|
}
|
||||||
return csr
|
return csr
|
||||||
}
|
}
|
||||||
|
|
||||||
func buildTestingCSR(name, signerName string) *capi.CertificateSigningRequest {
|
func buildTestingCSR(name, signerName, groupName string) *capi.CertificateSigningRequest {
|
||||||
return &capi.CertificateSigningRequest{
|
return &capi.CertificateSigningRequest{
|
||||||
ObjectMeta: metav1.ObjectMeta{
|
ObjectMeta: metav1.ObjectMeta{
|
||||||
Name: name,
|
Name: name,
|
||||||
},
|
},
|
||||||
Spec: capi.CertificateSigningRequestSpec{
|
Spec: capi.CertificateSigningRequestSpec{
|
||||||
SignerName: &signerName,
|
SignerName: &signerName,
|
||||||
Request: testCSRPEM,
|
Request: pemWithGroup(groupName),
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var (
|
func pemWithGroup(group string) []byte {
|
||||||
// The contents of this CSR do not matter, and it is only used to allow the
|
template := &x509.CertificateRequest{
|
||||||
// CSR resource submitted during integration tests to pass through
|
Subject: pkix.Name{
|
||||||
// validation.
|
Organization: []string{group},
|
||||||
testCSRPEM = []byte(`-----BEGIN CERTIFICATE REQUEST-----
|
},
|
||||||
MIICrzCCAZcCAQAwajENMAsGA1UECAwESE9OSzENMAsGA1UEBwwESE9OSzENMAsG
|
}
|
||||||
A1UECgwESE9OSzENMAsGA1UECwwESE9OSzENMAsGA1UEAwwESE9OSzEdMBsGCSqG
|
return pemWithTemplate(template)
|
||||||
SIb3DQEJARYOaG9ua0Bob25rLmhvbmswggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAw
|
}
|
||||||
ggEKAoIBAQDhw6t5C0Vtzzl4jQVMM9S2epAKOyKZCXRYC50sG8UFectfSALJHPUY
|
|
||||||
rv3LNfUTSkqg+EJO+5an1PeQS+GK94DUiJ2cUR2hBiTfXenyDAm2fGSDIqLQ/YcZ
|
func pemWithTemplate(template *x509.CertificateRequest) []byte {
|
||||||
fprwlqMu3YfpMH1KyyNORoOgWgsyWP0rBIRoWEFcFNaBu7BazaJHQIYNpcyRkHJC
|
_, key, err := ed25519.GenerateKey(rand.Reader)
|
||||||
610It4MV5dUqNFAfYqmxqlkMa4lR0U4f8cCA3J+lajNOMz/GkPotBINU+xX4bVob
|
if err != nil {
|
||||||
Q+ghAatgiZnEvC6pe0LqG788SHaIu7hArSK8ZG7+HcqCwISFLJiA8+A6HE24PhQC
|
panic(err)
|
||||||
69pGqHePAFO4a09c5/MTPfBfohYkEGX7AgMBAAGgADANBgkqhkiG9w0BAQsFAAOC
|
}
|
||||||
AQEAwg/7CWhWZICusSKEeIHJE+rgeSySAgL0S05KJKtwjHK1zf2B8Az4F2pe0aCe
|
|
||||||
r+mqNyFutmaLOXmNH7H1BJuw0wXeEg8wlT3nknRTJ4EWYf4G0H1dOICk/tB4Mgl1
|
csrDER, err := x509.CreateCertificateRequest(rand.Reader, template, key)
|
||||||
qgmMcP37QQRCMit5VY9BOKfXo+AHCH9rwmX91mXwzyejY/wO6Y3R6Y+GvMKA259F
|
if err != nil {
|
||||||
zRt2J8VJkeeXOE/H93putfT1KcmayTwO0gTzPFd7ZZzLSVMnpirxCUujkduxy8DK
|
panic(err)
|
||||||
dDcZdaTZofztqa5ej1gzptxU6fBfVvl3Wevc30yDH5Dum0aiohJbijncgIR6SQx5
|
}
|
||||||
6nuYWH340f/Ivm5b1gyEqb12ag==
|
|
||||||
-----END CERTIFICATE REQUEST-----`)
|
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