mirror of
https://github.com/k3s-io/kubernetes.git
synced 2025-09-13 13:14:05 +00:00
Add Certificate signerName admission plugins
This commit is contained in:
@@ -14,6 +14,7 @@ filegroup(
|
||||
"//plugin/pkg/admission/admit:all-srcs",
|
||||
"//plugin/pkg/admission/alwayspullimages:all-srcs",
|
||||
"//plugin/pkg/admission/antiaffinity:all-srcs",
|
||||
"//plugin/pkg/admission/certificates:all-srcs",
|
||||
"//plugin/pkg/admission/defaulttolerationseconds:all-srcs",
|
||||
"//plugin/pkg/admission/deny:all-srcs",
|
||||
"//plugin/pkg/admission/eventratelimit:all-srcs",
|
||||
|
32
plugin/pkg/admission/certificates/BUILD
Normal file
32
plugin/pkg/admission/certificates/BUILD
Normal file
@@ -0,0 +1,32 @@
|
||||
load("@io_bazel_rules_go//go:def.bzl", "go_library")
|
||||
|
||||
package(default_visibility = ["//visibility:public"])
|
||||
|
||||
filegroup(
|
||||
name = "package-srcs",
|
||||
srcs = glob(["**"]),
|
||||
tags = ["automanaged"],
|
||||
visibility = ["//visibility:private"],
|
||||
)
|
||||
|
||||
filegroup(
|
||||
name = "all-srcs",
|
||||
srcs = [
|
||||
":package-srcs",
|
||||
"//plugin/pkg/admission/certificates/approval:all-srcs",
|
||||
"//plugin/pkg/admission/certificates/signing:all-srcs",
|
||||
"//plugin/pkg/admission/certificates/subjectrestriction:all-srcs",
|
||||
],
|
||||
tags = ["automanaged"],
|
||||
)
|
||||
|
||||
go_library(
|
||||
name = "go_default_library",
|
||||
srcs = ["util.go"],
|
||||
importpath = "k8s.io/kubernetes/plugin/pkg/admission/certificates",
|
||||
deps = [
|
||||
"//staging/src/k8s.io/apiserver/pkg/authentication/user:go_default_library",
|
||||
"//staging/src/k8s.io/apiserver/pkg/authorization/authorizer:go_default_library",
|
||||
"//vendor/k8s.io/klog:go_default_library",
|
||||
],
|
||||
)
|
8
plugin/pkg/admission/certificates/OWNERS
Normal file
8
plugin/pkg/admission/certificates/OWNERS
Normal file
@@ -0,0 +1,8 @@
|
||||
# See the OWNERS docs at https://go.k8s.io/owners
|
||||
|
||||
approvers:
|
||||
- sig-auth-certificates-approvers
|
||||
reviewers:
|
||||
- sig-auth-certificates-approvers
|
||||
labels:
|
||||
- sig/auth
|
44
plugin/pkg/admission/certificates/approval/BUILD
Normal file
44
plugin/pkg/admission/certificates/approval/BUILD
Normal file
@@ -0,0 +1,44 @@
|
||||
load("@io_bazel_rules_go//go:def.bzl", "go_library", "go_test")
|
||||
|
||||
go_library(
|
||||
name = "go_default_library",
|
||||
srcs = ["admission.go"],
|
||||
importpath = "k8s.io/kubernetes/plugin/pkg/admission/certificates/approval",
|
||||
visibility = ["//visibility:public"],
|
||||
deps = [
|
||||
"//pkg/apis/certificates:go_default_library",
|
||||
"//plugin/pkg/admission/certificates:go_default_library",
|
||||
"//staging/src/k8s.io/apiserver/pkg/admission:go_default_library",
|
||||
"//staging/src/k8s.io/apiserver/pkg/admission/initializer:go_default_library",
|
||||
"//staging/src/k8s.io/apiserver/pkg/authorization/authorizer:go_default_library",
|
||||
"//vendor/k8s.io/klog:go_default_library",
|
||||
],
|
||||
)
|
||||
|
||||
go_test(
|
||||
name = "go_default_test",
|
||||
srcs = ["admission_test.go"],
|
||||
embed = [":go_default_library"],
|
||||
deps = [
|
||||
"//pkg/apis/certificates:go_default_library",
|
||||
"//staging/src/k8s.io/apimachinery/pkg/runtime:go_default_library",
|
||||
"//staging/src/k8s.io/apimachinery/pkg/runtime/schema:go_default_library",
|
||||
"//staging/src/k8s.io/apiserver/pkg/admission:go_default_library",
|
||||
"//staging/src/k8s.io/apiserver/pkg/authentication/user:go_default_library",
|
||||
"//staging/src/k8s.io/apiserver/pkg/authorization/authorizer:go_default_library",
|
||||
],
|
||||
)
|
||||
|
||||
filegroup(
|
||||
name = "package-srcs",
|
||||
srcs = glob(["**"]),
|
||||
tags = ["automanaged"],
|
||||
visibility = ["//visibility:private"],
|
||||
)
|
||||
|
||||
filegroup(
|
||||
name = "all-srcs",
|
||||
srcs = [":package-srcs"],
|
||||
tags = ["automanaged"],
|
||||
visibility = ["//visibility:public"],
|
||||
)
|
99
plugin/pkg/admission/certificates/approval/admission.go
Normal file
99
plugin/pkg/admission/certificates/approval/admission.go
Normal file
@@ -0,0 +1,99 @@
|
||||
/*
|
||||
Copyright 2020 The Kubernetes Authors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package approval
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
|
||||
"k8s.io/klog"
|
||||
|
||||
"k8s.io/apiserver/pkg/admission"
|
||||
genericadmissioninit "k8s.io/apiserver/pkg/admission/initializer"
|
||||
"k8s.io/apiserver/pkg/authorization/authorizer"
|
||||
|
||||
api "k8s.io/kubernetes/pkg/apis/certificates"
|
||||
"k8s.io/kubernetes/plugin/pkg/admission/certificates"
|
||||
)
|
||||
|
||||
// PluginName is a string with the name of the plugin
|
||||
const PluginName = "CertificateApproval"
|
||||
|
||||
// Register registers a plugin
|
||||
func Register(plugins *admission.Plugins) {
|
||||
plugins.Register(PluginName, func(config io.Reader) (admission.Interface, error) {
|
||||
return NewPlugin(), nil
|
||||
})
|
||||
}
|
||||
|
||||
// Plugin holds state for and implements the admission plugin.
|
||||
type Plugin struct {
|
||||
*admission.Handler
|
||||
authz authorizer.Authorizer
|
||||
}
|
||||
|
||||
// SetAuthorizer sets the authorizer.
|
||||
func (p *Plugin) SetAuthorizer(authz authorizer.Authorizer) {
|
||||
p.authz = authz
|
||||
}
|
||||
|
||||
// ValidateInitialization ensures an authorizer is set.
|
||||
func (p *Plugin) ValidateInitialization() error {
|
||||
if p.authz == nil {
|
||||
return fmt.Errorf("%s requires an authorizer", PluginName)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
var _ admission.ValidationInterface = &Plugin{}
|
||||
var _ genericadmissioninit.WantsAuthorizer = &Plugin{}
|
||||
|
||||
// NewPlugin creates a new CSR approval admission plugin
|
||||
func NewPlugin() *Plugin {
|
||||
return &Plugin{
|
||||
Handler: admission.NewHandler(admission.Update),
|
||||
}
|
||||
}
|
||||
|
||||
var csrGroupResource = api.Resource("certificatesigningrequests")
|
||||
|
||||
// Validate verifies that the requesting user has permission to approve
|
||||
// CertificateSigningRequests for the specified signerName.
|
||||
func (p *Plugin) Validate(ctx context.Context, a admission.Attributes, _ admission.ObjectInterfaces) error {
|
||||
// Ignore all calls to anything other than 'certificatesigningrequests/approval'.
|
||||
// Ignore all operations other than UPDATE.
|
||||
if a.GetSubresource() != "approval" ||
|
||||
a.GetResource().GroupResource() != csrGroupResource {
|
||||
return nil
|
||||
}
|
||||
|
||||
// We check permissions against the *old* version of the resource, in case
|
||||
// a user is attempting to update the SignerName when calling the approval
|
||||
// endpoint (which is an invalid/not allowed operation)
|
||||
csr, ok := a.GetOldObject().(*api.CertificateSigningRequest)
|
||||
if !ok {
|
||||
return admission.NewForbidden(a, fmt.Errorf("expected type CertificateSigningRequest, got: %T", a.GetOldObject()))
|
||||
}
|
||||
|
||||
if !certificates.IsAuthorizedForSignerName(ctx, p.authz, a.GetUserInfo(), "approve", csr.Spec.SignerName) {
|
||||
klog.V(4).Infof("user not permitted to approve CertificateSigningRequest %q with signerName %q", csr.Name, csr.Spec.SignerName)
|
||||
return admission.NewForbidden(a, fmt.Errorf("user not permitted to approve requests with signerName %q", csr.Spec.SignerName))
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
206
plugin/pkg/admission/certificates/approval/admission_test.go
Normal file
206
plugin/pkg/admission/certificates/approval/admission_test.go
Normal file
@@ -0,0 +1,206 @@
|
||||
/*
|
||||
Copyright 2020 The Kubernetes Authors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package approval
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
"k8s.io/apiserver/pkg/admission"
|
||||
"k8s.io/apiserver/pkg/authentication/user"
|
||||
"k8s.io/apiserver/pkg/authorization/authorizer"
|
||||
|
||||
certificatesapi "k8s.io/kubernetes/pkg/apis/certificates"
|
||||
)
|
||||
|
||||
func TestPlugin_Validate(t *testing.T) {
|
||||
tests := map[string]struct {
|
||||
attributes admission.Attributes
|
||||
allowedName string
|
||||
allowed bool
|
||||
authzErr error
|
||||
}{
|
||||
"wrong type": {
|
||||
attributes: &testAttributes{
|
||||
resource: certificatesapi.Resource("certificatesigningrequests"),
|
||||
subresource: "approval",
|
||||
oldObj: &certificatesapi.CertificateSigningRequestList{},
|
||||
operation: admission.Update,
|
||||
},
|
||||
allowed: false,
|
||||
},
|
||||
"reject requests if looking up permissions fails": {
|
||||
attributes: &testAttributes{
|
||||
resource: certificatesapi.Resource("certificatesigningrequests"),
|
||||
subresource: "approval",
|
||||
oldObj: &certificatesapi.CertificateSigningRequest{Spec: certificatesapi.CertificateSigningRequestSpec{
|
||||
SignerName: "abc.com/xyz",
|
||||
}},
|
||||
operation: admission.Update,
|
||||
},
|
||||
authzErr: errors.New("forced error"),
|
||||
allowed: false,
|
||||
},
|
||||
"should allow request if user is authorized for specific signerName": {
|
||||
allowedName: "abc.com/xyz",
|
||||
attributes: &testAttributes{
|
||||
resource: certificatesapi.Resource("certificatesigningrequests"),
|
||||
subresource: "approval",
|
||||
oldObj: &certificatesapi.CertificateSigningRequest{Spec: certificatesapi.CertificateSigningRequestSpec{
|
||||
SignerName: "abc.com/xyz",
|
||||
}},
|
||||
operation: admission.Update,
|
||||
},
|
||||
allowed: true,
|
||||
},
|
||||
"should allow request if user is authorized with wildcard": {
|
||||
allowedName: "abc.com/*",
|
||||
attributes: &testAttributes{
|
||||
resource: certificatesapi.Resource("certificatesigningrequests"),
|
||||
subresource: "approval",
|
||||
oldObj: &certificatesapi.CertificateSigningRequest{Spec: certificatesapi.CertificateSigningRequestSpec{
|
||||
SignerName: "abc.com/xyz",
|
||||
}},
|
||||
operation: admission.Update,
|
||||
},
|
||||
allowed: true,
|
||||
},
|
||||
"should deny request if user does not have permission for this signerName": {
|
||||
allowedName: "notabc.com/xyz",
|
||||
attributes: &testAttributes{
|
||||
resource: certificatesapi.Resource("certificatesigningrequests"),
|
||||
subresource: "approval",
|
||||
oldObj: &certificatesapi.CertificateSigningRequest{Spec: certificatesapi.CertificateSigningRequestSpec{
|
||||
SignerName: "abc.com/xyz",
|
||||
}},
|
||||
operation: admission.Update,
|
||||
},
|
||||
allowed: false,
|
||||
},
|
||||
"should deny request if user attempts to update signerName to a new value they *do* have permission to approve for": {
|
||||
allowedName: "allowed.com/xyz",
|
||||
attributes: &testAttributes{
|
||||
resource: certificatesapi.Resource("certificatesigningrequests"),
|
||||
subresource: "approval",
|
||||
oldObj: &certificatesapi.CertificateSigningRequest{Spec: certificatesapi.CertificateSigningRequestSpec{
|
||||
SignerName: "notallowed.com/xyz",
|
||||
}},
|
||||
obj: &certificatesapi.CertificateSigningRequest{Spec: certificatesapi.CertificateSigningRequestSpec{
|
||||
SignerName: "allowed.com/xyz",
|
||||
}},
|
||||
operation: admission.Update,
|
||||
},
|
||||
allowed: false,
|
||||
},
|
||||
}
|
||||
|
||||
for n, test := range tests {
|
||||
t.Run(n, func(t *testing.T) {
|
||||
p := Plugin{
|
||||
authz: fakeAuthorizer{
|
||||
t: t,
|
||||
verb: "approve",
|
||||
allowedName: test.allowedName,
|
||||
decision: authorizer.DecisionAllow,
|
||||
err: test.authzErr,
|
||||
},
|
||||
}
|
||||
err := p.Validate(context.Background(), test.attributes, nil)
|
||||
if err == nil && !test.allowed {
|
||||
t.Errorf("Expected authorization policy to reject CSR but it was allowed")
|
||||
}
|
||||
if err != nil && test.allowed {
|
||||
t.Errorf("Expected authorization policy to accept CSR but it was rejected: %v", err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
type fakeAuthorizer struct {
|
||||
t *testing.T
|
||||
verb string
|
||||
allowedName string
|
||||
decision authorizer.Decision
|
||||
err error
|
||||
}
|
||||
|
||||
func (f fakeAuthorizer) Authorize(ctx context.Context, a authorizer.Attributes) (authorizer.Decision, string, error) {
|
||||
if f.err != nil {
|
||||
return f.decision, "forced error", f.err
|
||||
}
|
||||
if a.GetVerb() != f.verb {
|
||||
return authorizer.DecisionDeny, fmt.Sprintf("unrecognised verb '%s'", a.GetVerb()), nil
|
||||
}
|
||||
if a.GetAPIGroup() != "certificates.k8s.io" {
|
||||
return authorizer.DecisionDeny, fmt.Sprintf("unrecognised groupName '%s'", a.GetAPIGroup()), nil
|
||||
}
|
||||
if a.GetAPIVersion() != "*" {
|
||||
return authorizer.DecisionDeny, fmt.Sprintf("unrecognised apiVersion '%s'", a.GetAPIVersion()), nil
|
||||
}
|
||||
if a.GetResource() != "signers" {
|
||||
return authorizer.DecisionDeny, fmt.Sprintf("unrecognised resource '%s'", a.GetResource()), nil
|
||||
}
|
||||
if a.GetName() != f.allowedName {
|
||||
return authorizer.DecisionDeny, fmt.Sprintf("unrecognised resource name '%s'", a.GetName()), nil
|
||||
}
|
||||
if !a.IsResourceRequest() {
|
||||
return authorizer.DecisionDeny, fmt.Sprintf("unrecognised IsResourceRequest '%t'", a.IsResourceRequest()), nil
|
||||
}
|
||||
return f.decision, "", nil
|
||||
}
|
||||
|
||||
type testAttributes struct {
|
||||
resource schema.GroupResource
|
||||
subresource string
|
||||
operation admission.Operation
|
||||
obj, oldObj runtime.Object
|
||||
name string
|
||||
|
||||
admission.Attributes // nil panic if any other methods called
|
||||
}
|
||||
|
||||
func (t *testAttributes) GetResource() schema.GroupVersionResource {
|
||||
return t.resource.WithVersion("ignored")
|
||||
}
|
||||
|
||||
func (t *testAttributes) GetSubresource() string {
|
||||
return t.subresource
|
||||
}
|
||||
|
||||
func (t *testAttributes) GetObject() runtime.Object {
|
||||
return t.obj
|
||||
}
|
||||
|
||||
func (t *testAttributes) GetOldObject() runtime.Object {
|
||||
return t.oldObj
|
||||
}
|
||||
|
||||
func (t *testAttributes) GetName() string {
|
||||
return t.name
|
||||
}
|
||||
|
||||
func (t *testAttributes) GetOperation() admission.Operation {
|
||||
return t.operation
|
||||
}
|
||||
|
||||
func (t *testAttributes) GetUserInfo() user.Info {
|
||||
return &user.DefaultInfo{Name: "ignored"}
|
||||
}
|
44
plugin/pkg/admission/certificates/signing/BUILD
Normal file
44
plugin/pkg/admission/certificates/signing/BUILD
Normal file
@@ -0,0 +1,44 @@
|
||||
load("@io_bazel_rules_go//go:def.bzl", "go_library", "go_test")
|
||||
|
||||
go_library(
|
||||
name = "go_default_library",
|
||||
srcs = ["admission.go"],
|
||||
importpath = "k8s.io/kubernetes/plugin/pkg/admission/certificates/signing",
|
||||
visibility = ["//visibility:public"],
|
||||
deps = [
|
||||
"//pkg/apis/certificates:go_default_library",
|
||||
"//plugin/pkg/admission/certificates:go_default_library",
|
||||
"//staging/src/k8s.io/apiserver/pkg/admission:go_default_library",
|
||||
"//staging/src/k8s.io/apiserver/pkg/admission/initializer:go_default_library",
|
||||
"//staging/src/k8s.io/apiserver/pkg/authorization/authorizer:go_default_library",
|
||||
"//vendor/k8s.io/klog:go_default_library",
|
||||
],
|
||||
)
|
||||
|
||||
go_test(
|
||||
name = "go_default_test",
|
||||
srcs = ["admission_test.go"],
|
||||
embed = [":go_default_library"],
|
||||
deps = [
|
||||
"//pkg/apis/certificates:go_default_library",
|
||||
"//staging/src/k8s.io/apimachinery/pkg/runtime:go_default_library",
|
||||
"//staging/src/k8s.io/apimachinery/pkg/runtime/schema:go_default_library",
|
||||
"//staging/src/k8s.io/apiserver/pkg/admission:go_default_library",
|
||||
"//staging/src/k8s.io/apiserver/pkg/authentication/user:go_default_library",
|
||||
"//staging/src/k8s.io/apiserver/pkg/authorization/authorizer:go_default_library",
|
||||
],
|
||||
)
|
||||
|
||||
filegroup(
|
||||
name = "package-srcs",
|
||||
srcs = glob(["**"]),
|
||||
tags = ["automanaged"],
|
||||
visibility = ["//visibility:private"],
|
||||
)
|
||||
|
||||
filegroup(
|
||||
name = "all-srcs",
|
||||
srcs = [":package-srcs"],
|
||||
tags = ["automanaged"],
|
||||
visibility = ["//visibility:public"],
|
||||
)
|
106
plugin/pkg/admission/certificates/signing/admission.go
Normal file
106
plugin/pkg/admission/certificates/signing/admission.go
Normal file
@@ -0,0 +1,106 @@
|
||||
/*
|
||||
Copyright 2020 The Kubernetes Authors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package signing
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"reflect"
|
||||
|
||||
"k8s.io/klog"
|
||||
|
||||
"k8s.io/apiserver/pkg/admission"
|
||||
genericadmissioninit "k8s.io/apiserver/pkg/admission/initializer"
|
||||
"k8s.io/apiserver/pkg/authorization/authorizer"
|
||||
|
||||
api "k8s.io/kubernetes/pkg/apis/certificates"
|
||||
"k8s.io/kubernetes/plugin/pkg/admission/certificates"
|
||||
)
|
||||
|
||||
// PluginName is a string with the name of the plugin
|
||||
const PluginName = "CertificateSigning"
|
||||
|
||||
// Register registers a plugin
|
||||
func Register(plugins *admission.Plugins) {
|
||||
plugins.Register(PluginName, func(config io.Reader) (admission.Interface, error) {
|
||||
return NewPlugin(), nil
|
||||
})
|
||||
}
|
||||
|
||||
// Plugin holds state for and implements the admission plugin.
|
||||
type Plugin struct {
|
||||
*admission.Handler
|
||||
authz authorizer.Authorizer
|
||||
}
|
||||
|
||||
// SetAuthorizer sets the authorizer.
|
||||
func (p *Plugin) SetAuthorizer(authz authorizer.Authorizer) {
|
||||
p.authz = authz
|
||||
}
|
||||
|
||||
// ValidateInitialization ensures an authorizer is set.
|
||||
func (p *Plugin) ValidateInitialization() error {
|
||||
if p.authz == nil {
|
||||
return fmt.Errorf("%s requires an authorizer", PluginName)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
var _ admission.ValidationInterface = &Plugin{}
|
||||
var _ genericadmissioninit.WantsAuthorizer = &Plugin{}
|
||||
|
||||
// NewPlugin creates a new CSR approval admission plugin
|
||||
func NewPlugin() *Plugin {
|
||||
return &Plugin{
|
||||
Handler: admission.NewHandler(admission.Update),
|
||||
}
|
||||
}
|
||||
|
||||
var csrGroupResource = api.Resource("certificatesigningrequests")
|
||||
|
||||
// Validate verifies that the requesting user has permission to approve
|
||||
// CertificateSigningRequests for the specified signerName.
|
||||
func (p *Plugin) Validate(ctx context.Context, a admission.Attributes, o admission.ObjectInterfaces) error {
|
||||
// Ignore all calls to anything other than 'certificatesigningrequests/approval'.
|
||||
// Ignore all operations other than UPDATE.
|
||||
if a.GetSubresource() != "status" ||
|
||||
a.GetResource().GroupResource() != csrGroupResource {
|
||||
return nil
|
||||
}
|
||||
|
||||
oldCSR, ok := a.GetOldObject().(*api.CertificateSigningRequest)
|
||||
if !ok {
|
||||
return admission.NewForbidden(a, fmt.Errorf("expected type CertificateSigningRequest, got: %T", a.GetOldObject()))
|
||||
}
|
||||
csr, ok := a.GetObject().(*api.CertificateSigningRequest)
|
||||
if !ok {
|
||||
return admission.NewForbidden(a, fmt.Errorf("expected type CertificateSigningRequest, got: %T", a.GetObject()))
|
||||
}
|
||||
|
||||
// only run if the status.certificate field has been changed
|
||||
if reflect.DeepEqual(oldCSR.Status.Certificate, csr.Status.Certificate) {
|
||||
return nil
|
||||
}
|
||||
|
||||
if !certificates.IsAuthorizedForSignerName(ctx, p.authz, a.GetUserInfo(), "sign", oldCSR.Spec.SignerName) {
|
||||
klog.V(4).Infof("user not permitted to sign CertificateSigningRequest %q with signerName %q", oldCSR.Name, oldCSR.Spec.SignerName)
|
||||
return admission.NewForbidden(a, fmt.Errorf("user not permitted to sign requests with signerName %q", oldCSR.Spec.SignerName))
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
260
plugin/pkg/admission/certificates/signing/admission_test.go
Normal file
260
plugin/pkg/admission/certificates/signing/admission_test.go
Normal file
@@ -0,0 +1,260 @@
|
||||
/*
|
||||
Copyright 2020 The Kubernetes Authors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package signing
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
"k8s.io/apiserver/pkg/admission"
|
||||
"k8s.io/apiserver/pkg/authentication/user"
|
||||
"k8s.io/apiserver/pkg/authorization/authorizer"
|
||||
|
||||
certificatesapi "k8s.io/kubernetes/pkg/apis/certificates"
|
||||
)
|
||||
|
||||
func TestPlugin_Validate(t *testing.T) {
|
||||
tests := map[string]struct {
|
||||
attributes admission.Attributes
|
||||
allowedName string
|
||||
allowed bool
|
||||
authzErr error
|
||||
}{
|
||||
"wrong type": {
|
||||
attributes: &testAttributes{
|
||||
resource: certificatesapi.Resource("certificatesigningrequests"),
|
||||
subresource: "status",
|
||||
oldObj: &certificatesapi.CertificateSigningRequestList{},
|
||||
obj: &certificatesapi.CertificateSigningRequestList{},
|
||||
operation: admission.Update,
|
||||
},
|
||||
allowed: false,
|
||||
},
|
||||
"allowed if the 'certificate' field has not changed": {
|
||||
attributes: &testAttributes{
|
||||
resource: certificatesapi.Resource("certificatesigningrequests"),
|
||||
subresource: "status",
|
||||
oldObj: &certificatesapi.CertificateSigningRequest{Status: certificatesapi.CertificateSigningRequestStatus{
|
||||
Certificate: []byte("data"),
|
||||
}},
|
||||
obj: &certificatesapi.CertificateSigningRequest{Status: certificatesapi.CertificateSigningRequestStatus{
|
||||
Certificate: []byte("data"),
|
||||
}},
|
||||
operation: admission.Update,
|
||||
},
|
||||
allowed: true,
|
||||
authzErr: errors.New("faked error"),
|
||||
},
|
||||
"deny request if authz lookup fails": {
|
||||
allowedName: "abc.com/xyz",
|
||||
attributes: &testAttributes{
|
||||
resource: certificatesapi.Resource("certificatesigningrequests"),
|
||||
subresource: "status",
|
||||
oldObj: &certificatesapi.CertificateSigningRequest{Spec: certificatesapi.CertificateSigningRequestSpec{
|
||||
SignerName: "abc.com/xyz",
|
||||
}},
|
||||
obj: &certificatesapi.CertificateSigningRequest{
|
||||
Spec: certificatesapi.CertificateSigningRequestSpec{
|
||||
SignerName: "abc.com/xyz",
|
||||
},
|
||||
Status: certificatesapi.CertificateSigningRequestStatus{
|
||||
Certificate: []byte("data"),
|
||||
},
|
||||
},
|
||||
operation: admission.Update,
|
||||
},
|
||||
authzErr: errors.New("test"),
|
||||
allowed: false,
|
||||
},
|
||||
"allow request if user is authorized for specific signerName": {
|
||||
allowedName: "abc.com/xyz",
|
||||
attributes: &testAttributes{
|
||||
resource: certificatesapi.Resource("certificatesigningrequests"),
|
||||
subresource: "status",
|
||||
oldObj: &certificatesapi.CertificateSigningRequest{Spec: certificatesapi.CertificateSigningRequestSpec{
|
||||
SignerName: "abc.com/xyz",
|
||||
}},
|
||||
obj: &certificatesapi.CertificateSigningRequest{
|
||||
Spec: certificatesapi.CertificateSigningRequestSpec{
|
||||
SignerName: "abc.com/xyz",
|
||||
},
|
||||
Status: certificatesapi.CertificateSigningRequestStatus{
|
||||
Certificate: []byte("data"),
|
||||
},
|
||||
},
|
||||
operation: admission.Update,
|
||||
},
|
||||
allowed: true,
|
||||
},
|
||||
"allow request if user is authorized with wildcard": {
|
||||
allowedName: "abc.com/*",
|
||||
attributes: &testAttributes{
|
||||
resource: certificatesapi.Resource("certificatesigningrequests"),
|
||||
subresource: "status",
|
||||
oldObj: &certificatesapi.CertificateSigningRequest{Spec: certificatesapi.CertificateSigningRequestSpec{
|
||||
SignerName: "abc.com/xyz",
|
||||
}},
|
||||
obj: &certificatesapi.CertificateSigningRequest{
|
||||
Spec: certificatesapi.CertificateSigningRequestSpec{
|
||||
SignerName: "abc.com/xyz",
|
||||
},
|
||||
Status: certificatesapi.CertificateSigningRequestStatus{
|
||||
Certificate: []byte("data"),
|
||||
},
|
||||
},
|
||||
operation: admission.Update,
|
||||
},
|
||||
allowed: true,
|
||||
},
|
||||
"should deny request if user does not have permission for this signerName": {
|
||||
allowedName: "notabc.com/xyz",
|
||||
attributes: &testAttributes{
|
||||
resource: certificatesapi.Resource("certificatesigningrequests"),
|
||||
subresource: "status",
|
||||
oldObj: &certificatesapi.CertificateSigningRequest{Spec: certificatesapi.CertificateSigningRequestSpec{
|
||||
SignerName: "abc.com/xyz",
|
||||
}},
|
||||
obj: &certificatesapi.CertificateSigningRequest{
|
||||
Spec: certificatesapi.CertificateSigningRequestSpec{
|
||||
SignerName: "abc.com/xyz",
|
||||
},
|
||||
Status: certificatesapi.CertificateSigningRequestStatus{
|
||||
Certificate: []byte("data"),
|
||||
},
|
||||
},
|
||||
operation: admission.Update,
|
||||
},
|
||||
allowed: false,
|
||||
},
|
||||
"should deny request if user attempts to update signerName to a new value they *do* have permission to sign for": {
|
||||
allowedName: "allowed.com/xyz",
|
||||
attributes: &testAttributes{
|
||||
resource: certificatesapi.Resource("certificatesigningrequests"),
|
||||
subresource: "status",
|
||||
oldObj: &certificatesapi.CertificateSigningRequest{Spec: certificatesapi.CertificateSigningRequestSpec{
|
||||
SignerName: "notallowed.com/xyz",
|
||||
}},
|
||||
obj: &certificatesapi.CertificateSigningRequest{
|
||||
Spec: certificatesapi.CertificateSigningRequestSpec{
|
||||
SignerName: "allowed.com/xyz",
|
||||
},
|
||||
Status: certificatesapi.CertificateSigningRequestStatus{
|
||||
Certificate: []byte("data"),
|
||||
},
|
||||
},
|
||||
operation: admission.Update,
|
||||
},
|
||||
allowed: false,
|
||||
},
|
||||
}
|
||||
|
||||
for n, test := range tests {
|
||||
t.Run(n, func(t *testing.T) {
|
||||
p := Plugin{
|
||||
authz: fakeAuthorizer{
|
||||
t: t,
|
||||
verb: "sign",
|
||||
allowedName: test.allowedName,
|
||||
decision: authorizer.DecisionAllow,
|
||||
err: test.authzErr,
|
||||
},
|
||||
}
|
||||
err := p.Validate(context.Background(), test.attributes, nil)
|
||||
if err == nil && !test.allowed {
|
||||
t.Errorf("Expected authorization policy to reject CSR but it was allowed")
|
||||
}
|
||||
if err != nil && test.allowed {
|
||||
t.Errorf("Expected authorization policy to accept CSR but it was rejected: %v", err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
type fakeAuthorizer struct {
|
||||
t *testing.T
|
||||
verb string
|
||||
allowedName string
|
||||
decision authorizer.Decision
|
||||
err error
|
||||
}
|
||||
|
||||
func (f fakeAuthorizer) Authorize(ctx context.Context, a authorizer.Attributes) (authorizer.Decision, string, error) {
|
||||
if f.err != nil {
|
||||
return f.decision, "forced error", f.err
|
||||
}
|
||||
if a.GetVerb() != f.verb {
|
||||
return authorizer.DecisionDeny, fmt.Sprintf("unrecognised verb '%s'", a.GetVerb()), nil
|
||||
}
|
||||
if a.GetAPIGroup() != "certificates.k8s.io" {
|
||||
return authorizer.DecisionDeny, fmt.Sprintf("unrecognised groupName '%s'", a.GetAPIGroup()), nil
|
||||
}
|
||||
if a.GetAPIVersion() != "*" {
|
||||
return authorizer.DecisionDeny, fmt.Sprintf("unrecognised apiVersion '%s'", a.GetAPIVersion()), nil
|
||||
}
|
||||
if a.GetResource() != "signers" {
|
||||
return authorizer.DecisionDeny, fmt.Sprintf("unrecognised resource '%s'", a.GetResource()), nil
|
||||
}
|
||||
if a.GetName() != f.allowedName {
|
||||
return authorizer.DecisionDeny, fmt.Sprintf("unrecognised resource name '%s'", a.GetName()), nil
|
||||
}
|
||||
if !a.IsResourceRequest() {
|
||||
return authorizer.DecisionDeny, fmt.Sprintf("unrecognised IsResourceRequest '%t'", a.IsResourceRequest()), nil
|
||||
}
|
||||
return f.decision, "", nil
|
||||
}
|
||||
|
||||
type testAttributes struct {
|
||||
resource schema.GroupResource
|
||||
subresource string
|
||||
operation admission.Operation
|
||||
oldObj, obj runtime.Object
|
||||
name string
|
||||
|
||||
admission.Attributes // nil panic if any other methods called
|
||||
}
|
||||
|
||||
func (t *testAttributes) GetResource() schema.GroupVersionResource {
|
||||
return t.resource.WithVersion("ignored")
|
||||
}
|
||||
|
||||
func (t *testAttributes) GetSubresource() string {
|
||||
return t.subresource
|
||||
}
|
||||
|
||||
func (t *testAttributes) GetOldObject() runtime.Object {
|
||||
return t.oldObj
|
||||
}
|
||||
|
||||
func (t *testAttributes) GetObject() runtime.Object {
|
||||
return t.obj
|
||||
}
|
||||
|
||||
func (t *testAttributes) GetName() string {
|
||||
return t.name
|
||||
}
|
||||
|
||||
func (t *testAttributes) GetOperation() admission.Operation {
|
||||
return t.operation
|
||||
}
|
||||
|
||||
func (t *testAttributes) GetUserInfo() user.Info {
|
||||
return &user.DefaultInfo{Name: "ignored"}
|
||||
}
|
41
plugin/pkg/admission/certificates/subjectrestriction/BUILD
Normal file
41
plugin/pkg/admission/certificates/subjectrestriction/BUILD
Normal file
@@ -0,0 +1,41 @@
|
||||
load("@io_bazel_rules_go//go:def.bzl", "go_library", "go_test")
|
||||
|
||||
go_library(
|
||||
name = "go_default_library",
|
||||
srcs = ["admission.go"],
|
||||
importpath = "k8s.io/kubernetes/plugin/pkg/admission/certificates/subjectrestriction",
|
||||
visibility = ["//visibility:public"],
|
||||
deps = [
|
||||
"//pkg/apis/certificates:go_default_library",
|
||||
"//staging/src/k8s.io/api/certificates/v1beta1:go_default_library",
|
||||
"//staging/src/k8s.io/apiserver/pkg/admission:go_default_library",
|
||||
"//vendor/k8s.io/klog:go_default_library",
|
||||
],
|
||||
)
|
||||
|
||||
go_test(
|
||||
name = "go_default_test",
|
||||
srcs = ["admission_test.go"],
|
||||
embed = [":go_default_library"],
|
||||
deps = [
|
||||
"//pkg/apis/certificates:go_default_library",
|
||||
"//staging/src/k8s.io/api/certificates/v1beta1:go_default_library",
|
||||
"//staging/src/k8s.io/apimachinery/pkg/runtime:go_default_library",
|
||||
"//staging/src/k8s.io/apimachinery/pkg/runtime/schema:go_default_library",
|
||||
"//staging/src/k8s.io/apiserver/pkg/admission:go_default_library",
|
||||
],
|
||||
)
|
||||
|
||||
filegroup(
|
||||
name = "package-srcs",
|
||||
srcs = glob(["**"]),
|
||||
tags = ["automanaged"],
|
||||
visibility = ["//visibility:private"],
|
||||
)
|
||||
|
||||
filegroup(
|
||||
name = "all-srcs",
|
||||
srcs = [":package-srcs"],
|
||||
tags = ["automanaged"],
|
||||
visibility = ["//visibility:public"],
|
||||
)
|
@@ -0,0 +1,93 @@
|
||||
/*
|
||||
Copyright 2020 The Kubernetes Authors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package subjectrestriction
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
|
||||
certificatesv1beta1 "k8s.io/api/certificates/v1beta1"
|
||||
"k8s.io/apiserver/pkg/admission"
|
||||
"k8s.io/klog"
|
||||
certificatesapi "k8s.io/kubernetes/pkg/apis/certificates"
|
||||
)
|
||||
|
||||
// PluginName is a string with the name of the plugin
|
||||
const PluginName = "CertificateSubjectRestriction"
|
||||
|
||||
// Register registers the plugin
|
||||
func Register(plugins *admission.Plugins) {
|
||||
plugins.Register(PluginName, func(config io.Reader) (admission.Interface, error) {
|
||||
return NewPlugin(), nil
|
||||
})
|
||||
}
|
||||
|
||||
// Plugin holds state for and implements the admission plugin.
|
||||
type Plugin struct {
|
||||
*admission.Handler
|
||||
}
|
||||
|
||||
// ValidateInitialization always returns nil.
|
||||
func (p *Plugin) ValidateInitialization() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
var _ admission.ValidationInterface = &Plugin{}
|
||||
|
||||
// NewPlugin constructs a new instance of the CertificateSubjectRestrictions admission interface.
|
||||
func NewPlugin() *Plugin {
|
||||
return &Plugin{
|
||||
Handler: admission.NewHandler(admission.Create),
|
||||
}
|
||||
}
|
||||
|
||||
var csrGroupResource = certificatesapi.Resource("certificatesigningrequests")
|
||||
|
||||
// Validate ensures that if the signerName on a CSR is set to
|
||||
// `kubernetes.io/kube-apiserver-client`, that its organization (group)
|
||||
// attribute is not set to `system:masters`.
|
||||
func (p *Plugin) Validate(_ context.Context, a admission.Attributes, _ admission.ObjectInterfaces) error {
|
||||
if a.GetResource().GroupResource() != csrGroupResource || a.GetSubresource() != "" {
|
||||
return nil
|
||||
}
|
||||
|
||||
csr, ok := a.GetObject().(*certificatesapi.CertificateSigningRequest)
|
||||
if !ok {
|
||||
return admission.NewForbidden(a, fmt.Errorf("expected type CertificateSigningRequest, got: %T", a.GetObject()))
|
||||
}
|
||||
|
||||
if csr.Spec.SignerName != certificatesv1beta1.KubeAPIServerClientSignerName {
|
||||
return nil
|
||||
}
|
||||
|
||||
csrParsed, err := certificatesapi.ParseCSR(csr)
|
||||
if err != nil {
|
||||
return admission.NewForbidden(a, fmt.Errorf("failed to parse CSR: %v", err))
|
||||
}
|
||||
|
||||
for _, group := range csrParsed.Subject.Organization {
|
||||
if group == "system:masters" {
|
||||
klog.V(4).Infof("CSR %s rejected by admission plugin %s for attempting to use signer %s with system:masters group",
|
||||
csr.Name, PluginName, certificatesv1beta1.KubeAPIServerClientSignerName)
|
||||
return admission.NewForbidden(a, fmt.Errorf("use of %s signer with system:masters group is not allowed",
|
||||
certificatesv1beta1.KubeAPIServerClientSignerName))
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
@@ -0,0 +1,189 @@
|
||||
/*
|
||||
Copyright 2020 The Kubernetes Authors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package subjectrestriction
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/ed25519"
|
||||
"crypto/rand"
|
||||
"crypto/x509"
|
||||
"crypto/x509/pkix"
|
||||
"encoding/pem"
|
||||
"testing"
|
||||
|
||||
certificatesv1beta1 "k8s.io/api/certificates/v1beta1"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
"k8s.io/apiserver/pkg/admission"
|
||||
certificatesapi "k8s.io/kubernetes/pkg/apis/certificates"
|
||||
)
|
||||
|
||||
func TestPlugin_Validate(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
a admission.Attributes
|
||||
wantErr string
|
||||
}{
|
||||
{
|
||||
name: "ignored resource",
|
||||
a: &testAttributes{
|
||||
resource: schema.GroupResource{
|
||||
Group: "foo",
|
||||
Resource: "bar",
|
||||
},
|
||||
},
|
||||
wantErr: "",
|
||||
},
|
||||
{
|
||||
name: "ignored subresource",
|
||||
a: &testAttributes{
|
||||
resource: certificatesapi.Resource("certificatesigningrequests"),
|
||||
subresource: "approve",
|
||||
},
|
||||
wantErr: "",
|
||||
},
|
||||
{
|
||||
name: "wrong type",
|
||||
a: &testAttributes{
|
||||
resource: certificatesapi.Resource("certificatesigningrequests"),
|
||||
obj: &certificatesapi.CertificateSigningRequestList{},
|
||||
name: "panda",
|
||||
},
|
||||
wantErr: `certificatesigningrequests.certificates.k8s.io "panda" is forbidden: expected type CertificateSigningRequest, got: *certificates.CertificateSigningRequestList`,
|
||||
},
|
||||
{
|
||||
name: "some other signer",
|
||||
a: &testAttributes{
|
||||
resource: certificatesapi.Resource("certificatesigningrequests"),
|
||||
obj: &certificatesapi.CertificateSigningRequest{Spec: certificatesapi.CertificateSigningRequestSpec{
|
||||
Request: pemWithGroup("system:masters"),
|
||||
SignerName: certificatesv1beta1.KubeAPIServerClientKubeletSignerName,
|
||||
}},
|
||||
},
|
||||
wantErr: "",
|
||||
},
|
||||
{
|
||||
name: "invalid request",
|
||||
a: &testAttributes{
|
||||
resource: certificatesapi.Resource("certificatesigningrequests"),
|
||||
obj: &certificatesapi.CertificateSigningRequest{Spec: certificatesapi.CertificateSigningRequestSpec{
|
||||
Request: []byte("this is not a CSR"),
|
||||
SignerName: certificatesv1beta1.KubeAPIServerClientSignerName,
|
||||
}},
|
||||
name: "bear",
|
||||
},
|
||||
wantErr: `certificatesigningrequests.certificates.k8s.io "bear" is forbidden: failed to parse CSR: PEM block type must be CERTIFICATE REQUEST`,
|
||||
},
|
||||
{
|
||||
name: "some other group",
|
||||
a: &testAttributes{
|
||||
resource: certificatesapi.Resource("certificatesigningrequests"),
|
||||
obj: &certificatesapi.CertificateSigningRequest{Spec: certificatesapi.CertificateSigningRequestSpec{
|
||||
Request: pemWithGroup("system:admin"),
|
||||
SignerName: certificatesv1beta1.KubeAPIServerClientSignerName,
|
||||
}},
|
||||
},
|
||||
wantErr: "",
|
||||
},
|
||||
{
|
||||
name: "request for system:masters",
|
||||
a: &testAttributes{
|
||||
resource: certificatesapi.Resource("certificatesigningrequests"),
|
||||
obj: &certificatesapi.CertificateSigningRequest{Spec: certificatesapi.CertificateSigningRequestSpec{
|
||||
Request: pemWithGroup("system:masters"),
|
||||
SignerName: certificatesv1beta1.KubeAPIServerClientSignerName,
|
||||
}},
|
||||
name: "pooh",
|
||||
},
|
||||
wantErr: `certificatesigningrequests.certificates.k8s.io "pooh" is forbidden: use of kubernetes.io/kube-apiserver-client signer with system:masters group is not allowed`,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
p := &Plugin{}
|
||||
if err := p.Validate(context.TODO(), tt.a, nil); errStr(err) != tt.wantErr {
|
||||
t.Errorf("Validate() error = %v, wantErr %v", err, tt.wantErr)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
type testAttributes struct {
|
||||
resource schema.GroupResource
|
||||
subresource string
|
||||
obj runtime.Object
|
||||
name string
|
||||
|
||||
admission.Attributes // nil panic if any other methods called
|
||||
}
|
||||
|
||||
func (t *testAttributes) GetResource() schema.GroupVersionResource {
|
||||
return t.resource.WithVersion("ignored")
|
||||
}
|
||||
|
||||
func (t *testAttributes) GetSubresource() string {
|
||||
return t.subresource
|
||||
}
|
||||
|
||||
func (t *testAttributes) GetObject() runtime.Object {
|
||||
return t.obj
|
||||
}
|
||||
|
||||
func (t *testAttributes) GetName() string {
|
||||
return t.name
|
||||
}
|
||||
|
||||
func errStr(err error) string {
|
||||
if err == nil {
|
||||
return ""
|
||||
}
|
||||
es := err.Error()
|
||||
if len(es) == 0 {
|
||||
panic("invalid empty error")
|
||||
}
|
||||
return es
|
||||
}
|
||||
|
||||
func pemWithGroup(group string) []byte {
|
||||
template := &x509.CertificateRequest{
|
||||
Subject: pkix.Name{
|
||||
Organization: []string{group},
|
||||
},
|
||||
}
|
||||
|
||||
_, key, err := ed25519.GenerateKey(rand.Reader)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
csrDER, err := x509.CreateCertificateRequest(rand.Reader, template, key)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
csrPemBlock := &pem.Block{
|
||||
Type: "CERTIFICATE REQUEST",
|
||||
Bytes: csrDER,
|
||||
}
|
||||
|
||||
p := pem.EncodeToMemory(csrPemBlock)
|
||||
if p == nil {
|
||||
panic("invalid pem block")
|
||||
}
|
||||
|
||||
return p
|
||||
}
|
75
plugin/pkg/admission/certificates/util.go
Normal file
75
plugin/pkg/admission/certificates/util.go
Normal file
@@ -0,0 +1,75 @@
|
||||
/*
|
||||
Copyright 2020 The Kubernetes Authors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package certificates
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strings"
|
||||
|
||||
"k8s.io/apiserver/pkg/authentication/user"
|
||||
"k8s.io/apiserver/pkg/authorization/authorizer"
|
||||
"k8s.io/klog"
|
||||
)
|
||||
|
||||
// IsAuthorizedForSignerName returns true if 'info' is authorized to perform the given
|
||||
// 'verb' on the synthetic 'signers' resource with the given signerName.
|
||||
// If the user does not have permission to perform the 'verb' on the given signerName,
|
||||
// it will also perform an authorization check against {domain portion}/*, for example
|
||||
// `kubernetes.io/*`. This allows an entity to be granted permission to 'verb' on all
|
||||
// signerNames with a given 'domain portion'.
|
||||
func IsAuthorizedForSignerName(ctx context.Context, authz authorizer.Authorizer, info user.Info, verb, signerName string) bool {
|
||||
// First check if the user has explicit permission to 'verb' for the given signerName.
|
||||
attr := buildAttributes(info, verb, signerName)
|
||||
decision, reason, err := authz.Authorize(ctx, attr)
|
||||
switch {
|
||||
case err != nil:
|
||||
klog.V(3).Infof("cannot authorize %q %q for policy: %v,%v", verb, attr.GetName(), reason, err)
|
||||
case decision == authorizer.DecisionAllow:
|
||||
return true
|
||||
}
|
||||
|
||||
// If not, check if the user has wildcard permissions to 'verb' for the domain portion of the signerName, e.g.
|
||||
// 'kubernetes.io/*'.
|
||||
attr = buildWildcardAttributes(info, verb, signerName)
|
||||
decision, reason, err = authz.Authorize(ctx, attr)
|
||||
switch {
|
||||
case err != nil:
|
||||
klog.V(3).Infof("cannot authorize %q %q for policy: %v,%v", verb, attr.GetName(), reason, err)
|
||||
case decision == authorizer.DecisionAllow:
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
func buildAttributes(info user.Info, verb, signerName string) authorizer.Attributes {
|
||||
return authorizer.AttributesRecord{
|
||||
User: info,
|
||||
Verb: verb,
|
||||
Name: signerName,
|
||||
APIGroup: "certificates.k8s.io",
|
||||
APIVersion: "*",
|
||||
Resource: "signers",
|
||||
ResourceRequest: true,
|
||||
}
|
||||
}
|
||||
|
||||
func buildWildcardAttributes(info user.Info, verb, signerName string) authorizer.Attributes {
|
||||
parts := strings.Split(signerName, "/")
|
||||
domain := parts[0]
|
||||
return buildAttributes(info, verb, domain+"/*")
|
||||
}
|
@@ -17,6 +17,7 @@ go_library(
|
||||
deps = [
|
||||
"//pkg/apis/rbac/v1:go_default_library",
|
||||
"//pkg/features:go_default_library",
|
||||
"//staging/src/k8s.io/api/certificates/v1beta1:go_default_library",
|
||||
"//staging/src/k8s.io/api/rbac/v1:go_default_library",
|
||||
"//staging/src/k8s.io/apimachinery/pkg/api/meta:go_default_library",
|
||||
"//staging/src/k8s.io/apimachinery/pkg/apis/meta/v1:go_default_library",
|
||||
|
@@ -21,6 +21,7 @@ import (
|
||||
|
||||
"k8s.io/klog"
|
||||
|
||||
capi "k8s.io/api/certificates/v1beta1"
|
||||
rbacv1 "k8s.io/api/rbac/v1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
utilfeature "k8s.io/apiserver/pkg/util/feature"
|
||||
@@ -337,6 +338,13 @@ func buildControllerRoles() ([]rbacv1.ClusterRole, []rbacv1.ClusterRoleBinding)
|
||||
Rules: []rbacv1.PolicyRule{
|
||||
rbacv1helpers.NewRule("get", "list", "watch", "delete").Groups(certificatesGroup).Resources("certificatesigningrequests").RuleOrDie(),
|
||||
rbacv1helpers.NewRule("update").Groups(certificatesGroup).Resources("certificatesigningrequests/status", "certificatesigningrequests/approval").RuleOrDie(),
|
||||
rbacv1helpers.NewRule("approve").Groups(certificatesGroup).Resources("signers").Names(capi.KubeAPIServerClientKubeletSignerName).RuleOrDie(),
|
||||
rbacv1helpers.NewRule("sign").Groups(certificatesGroup).Resources("signers").Names(
|
||||
capi.LegacyUnknownSignerName,
|
||||
capi.KubeAPIServerClientSignerName,
|
||||
capi.KubeAPIServerClientKubeletSignerName,
|
||||
capi.KubeletServingSignerName,
|
||||
).RuleOrDie(),
|
||||
rbacv1helpers.NewRule("create").Groups(authorizationGroup).Resources("subjectaccessreviews").RuleOrDie(),
|
||||
eventsRule(),
|
||||
},
|
||||
|
@@ -17,6 +17,7 @@ limitations under the License.
|
||||
package bootstrappolicy
|
||||
|
||||
import (
|
||||
capi "k8s.io/api/certificates/v1beta1"
|
||||
rbacv1 "k8s.io/api/rbac/v1"
|
||||
"k8s.io/apimachinery/pkg/api/meta"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
@@ -459,6 +460,30 @@ func ClusterRoles() []rbacv1.ClusterRole {
|
||||
rbacv1helpers.NewRule(ReadUpdate...).Groups(legacyGroup).Resources("persistentvolumeclaims").RuleOrDie(),
|
||||
},
|
||||
},
|
||||
{
|
||||
ObjectMeta: metav1.ObjectMeta{Name: "system:certificates.k8s.io:legacy-unknown-approver"},
|
||||
Rules: []rbacv1.PolicyRule{
|
||||
rbacv1helpers.NewRule("approve").Groups(certificatesGroup).Resources("signers").Names(capi.LegacyUnknownSignerName).RuleOrDie(),
|
||||
},
|
||||
},
|
||||
{
|
||||
ObjectMeta: metav1.ObjectMeta{Name: "system:certificates.k8s.io:kubelet-serving-approver"},
|
||||
Rules: []rbacv1.PolicyRule{
|
||||
rbacv1helpers.NewRule("approve").Groups(certificatesGroup).Resources("signers").Names(capi.KubeletServingSignerName).RuleOrDie(),
|
||||
},
|
||||
},
|
||||
{
|
||||
ObjectMeta: metav1.ObjectMeta{Name: "system:certificates.k8s.io:kube-apiserver-client-approver"},
|
||||
Rules: []rbacv1.PolicyRule{
|
||||
rbacv1helpers.NewRule("approve").Groups(certificatesGroup).Resources("signers").Names(capi.KubeAPIServerClientSignerName).RuleOrDie(),
|
||||
},
|
||||
},
|
||||
{
|
||||
ObjectMeta: metav1.ObjectMeta{Name: "system:certificates.k8s.io:kube-apiserver-client-kubelet-approver"},
|
||||
Rules: []rbacv1.PolicyRule{
|
||||
rbacv1helpers.NewRule("approve").Groups(certificatesGroup).Resources("signers").Names(capi.KubeAPIServerClientKubeletSignerName).RuleOrDie(),
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
if utilfeature.DefaultFeatureGate.Enabled(features.ServiceAccountIssuerDiscovery) {
|
||||
|
@@ -419,6 +419,78 @@ items:
|
||||
- certificatesigningrequests/selfnodeclient
|
||||
verbs:
|
||||
- create
|
||||
- apiVersion: rbac.authorization.k8s.io/v1
|
||||
kind: ClusterRole
|
||||
metadata:
|
||||
annotations:
|
||||
rbac.authorization.kubernetes.io/autoupdate: "true"
|
||||
creationTimestamp: null
|
||||
labels:
|
||||
kubernetes.io/bootstrapping: rbac-defaults
|
||||
name: system:certificates.k8s.io:kube-apiserver-client-approver
|
||||
rules:
|
||||
- apiGroups:
|
||||
- certificates.k8s.io
|
||||
resourceNames:
|
||||
- kubernetes.io/kube-apiserver-client
|
||||
resources:
|
||||
- signers
|
||||
verbs:
|
||||
- approve
|
||||
- apiVersion: rbac.authorization.k8s.io/v1
|
||||
kind: ClusterRole
|
||||
metadata:
|
||||
annotations:
|
||||
rbac.authorization.kubernetes.io/autoupdate: "true"
|
||||
creationTimestamp: null
|
||||
labels:
|
||||
kubernetes.io/bootstrapping: rbac-defaults
|
||||
name: system:certificates.k8s.io:kube-apiserver-client-kubelet-approver
|
||||
rules:
|
||||
- apiGroups:
|
||||
- certificates.k8s.io
|
||||
resourceNames:
|
||||
- kubernetes.io/kube-apiserver-client-kubelet
|
||||
resources:
|
||||
- signers
|
||||
verbs:
|
||||
- approve
|
||||
- apiVersion: rbac.authorization.k8s.io/v1
|
||||
kind: ClusterRole
|
||||
metadata:
|
||||
annotations:
|
||||
rbac.authorization.kubernetes.io/autoupdate: "true"
|
||||
creationTimestamp: null
|
||||
labels:
|
||||
kubernetes.io/bootstrapping: rbac-defaults
|
||||
name: system:certificates.k8s.io:kubelet-serving-approver
|
||||
rules:
|
||||
- apiGroups:
|
||||
- certificates.k8s.io
|
||||
resourceNames:
|
||||
- kubernetes.io/kubelet-serving
|
||||
resources:
|
||||
- signers
|
||||
verbs:
|
||||
- approve
|
||||
- apiVersion: rbac.authorization.k8s.io/v1
|
||||
kind: ClusterRole
|
||||
metadata:
|
||||
annotations:
|
||||
rbac.authorization.kubernetes.io/autoupdate: "true"
|
||||
creationTimestamp: null
|
||||
labels:
|
||||
kubernetes.io/bootstrapping: rbac-defaults
|
||||
name: system:certificates.k8s.io:legacy-unknown-approver
|
||||
rules:
|
||||
- apiGroups:
|
||||
- certificates.k8s.io
|
||||
resourceNames:
|
||||
- kubernetes.io/legacy-unknown
|
||||
resources:
|
||||
- signers
|
||||
verbs:
|
||||
- approve
|
||||
- apiVersion: rbac.authorization.k8s.io/v1
|
||||
kind: ClusterRole
|
||||
metadata:
|
||||
|
@@ -101,6 +101,25 @@ items:
|
||||
- certificatesigningrequests/status
|
||||
verbs:
|
||||
- update
|
||||
- apiGroups:
|
||||
- certificates.k8s.io
|
||||
resourceNames:
|
||||
- kubernetes.io/kube-apiserver-client-kubelet
|
||||
resources:
|
||||
- signers
|
||||
verbs:
|
||||
- approve
|
||||
- apiGroups:
|
||||
- certificates.k8s.io
|
||||
resourceNames:
|
||||
- kubernetes.io/kube-apiserver-client
|
||||
- kubernetes.io/kube-apiserver-client-kubelet
|
||||
- kubernetes.io/kubelet-serving
|
||||
- kubernetes.io/legacy-unknown
|
||||
resources:
|
||||
- signers
|
||||
verbs:
|
||||
- sign
|
||||
- apiGroups:
|
||||
- authorization.k8s.io
|
||||
resources:
|
||||
|
Reference in New Issue
Block a user