mirror of
https://github.com/k3s-io/kubernetes.git
synced 2025-07-19 01:40:13 +00:00
ClusterTrustBundles: Define types
This commit is the main API piece of KEP-3257 (ClusterTrustBundles). This commit: * Adds the certificates.k8s.io/v1alpha1 API group * Adds the ClusterTrustBundle type. * Registers the new type in kube-apiserver. * Implements the type-specfic validation specified for ClusterTrustBundles: - spec.pemTrustAnchors must always be non-empty. - spec.signerName must be either empty or a valid signer name. - Changing spec.signerName is disallowed. * Implements the "attest" admission check to restrict actions on ClusterTrustBundles that include a signer name. Because it wasn't specified in the KEP, I chose to make attempts to update the signer name be validation errors, rather than silently ignored. I have tested this out by launching these changes in kind and manipulating ClusterTrustBundle objects in the resulting cluster using kubectl.
This commit is contained in:
parent
742316ee21
commit
6a75e7c40c
@ -261,6 +261,7 @@ var apiVersionPriorities = map[schema.GroupVersion]priority{
|
||||
{Group: "batch", Version: "v1beta1"}: {group: 17400, version: 9},
|
||||
{Group: "batch", Version: "v2alpha1"}: {group: 17400, version: 9},
|
||||
{Group: "certificates.k8s.io", Version: "v1"}: {group: 17300, version: 15},
|
||||
{Group: "certificates.k8s.io", Version: "v1alpha1"}: {group: 17300, version: 1},
|
||||
{Group: "networking.k8s.io", Version: "v1"}: {group: 17200, version: 15},
|
||||
{Group: "networking.k8s.io", Version: "v1alpha1"}: {group: 17200, version: 1},
|
||||
{Group: "policy", Version: "v1"}: {group: 17100, version: 15},
|
||||
|
@ -88,6 +88,7 @@ batch/v1 \
|
||||
batch/v1beta1 \
|
||||
certificates.k8s.io/v1 \
|
||||
certificates.k8s.io/v1beta1 \
|
||||
certificates.k8s.io/v1alpha1 \
|
||||
coordination.k8s.io/v1beta1 \
|
||||
coordination.k8s.io/v1 \
|
||||
discovery.k8s.io/v1 \
|
||||
|
@ -24,6 +24,7 @@ import (
|
||||
"k8s.io/kubernetes/pkg/api/legacyscheme"
|
||||
"k8s.io/kubernetes/pkg/apis/certificates"
|
||||
v1 "k8s.io/kubernetes/pkg/apis/certificates/v1"
|
||||
"k8s.io/kubernetes/pkg/apis/certificates/v1alpha1"
|
||||
"k8s.io/kubernetes/pkg/apis/certificates/v1beta1"
|
||||
)
|
||||
|
||||
@ -36,5 +37,6 @@ func Install(scheme *runtime.Scheme) {
|
||||
utilruntime.Must(certificates.AddToScheme(scheme))
|
||||
utilruntime.Must(v1.AddToScheme(scheme))
|
||||
utilruntime.Must(v1beta1.AddToScheme(scheme))
|
||||
utilruntime.Must(scheme.SetVersionPriority(v1.SchemeGroupVersion, v1beta1.SchemeGroupVersion))
|
||||
utilruntime.Must(v1alpha1.AddToScheme(scheme))
|
||||
utilruntime.Must(scheme.SetVersionPriority(v1.SchemeGroupVersion, v1beta1.SchemeGroupVersion, v1alpha1.SchemeGroupVersion))
|
||||
}
|
||||
|
@ -47,6 +47,8 @@ func addKnownTypes(scheme *runtime.Scheme) error {
|
||||
scheme.AddKnownTypes(SchemeGroupVersion,
|
||||
&CertificateSigningRequest{},
|
||||
&CertificateSigningRequestList{},
|
||||
&ClusterTrustBundle{},
|
||||
&ClusterTrustBundleList{},
|
||||
)
|
||||
return nil
|
||||
}
|
||||
|
@ -224,3 +224,56 @@ const (
|
||||
UsageMicrosoftSGC KeyUsage = "microsoft sgc"
|
||||
UsageNetscapeSGC KeyUsage = "netscape sgc"
|
||||
)
|
||||
|
||||
// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object
|
||||
|
||||
// ClusterTrustBundle is a cluster-scoped container for X.509 trust anchors
|
||||
// (root certificates).
|
||||
//
|
||||
// ClusterTrustBundle objects are considered to be readable by any authenticated
|
||||
// user in the cluster.
|
||||
//
|
||||
// It can be optionally associated with a particular assigner, in which case it
|
||||
// contains one valid set of trust anchors for that signer. Signers may have
|
||||
// multiple associated ClusterTrustBundles; each is an independent set of trust
|
||||
// anchors for that signer.
|
||||
type ClusterTrustBundle struct {
|
||||
metav1.TypeMeta
|
||||
// +optional
|
||||
metav1.ObjectMeta
|
||||
|
||||
// Spec contains the signer (if any) and trust anchors.
|
||||
// +optional
|
||||
Spec ClusterTrustBundleSpec
|
||||
}
|
||||
|
||||
// ClusterTrustBundleSpec contains the signer and trust anchors.
|
||||
type ClusterTrustBundleSpec struct {
|
||||
// SignerName indicates the associated signer, if any.
|
||||
SignerName string
|
||||
|
||||
// TrustBundle contains the individual X.509 trust anchors for this
|
||||
// bundle, as PEM bundle of PEM-wrapped, DER-formatted X.509 certificates.
|
||||
//
|
||||
// The data must consist only of PEM certificate blocks that parse as valid
|
||||
// X.509 certificates. Each certificate must include a basic constraints
|
||||
// extension with the CA bit set. The API server will reject objects that
|
||||
// contain duplicate certificates, or that use PEM block headers.
|
||||
//
|
||||
// Users of ClusterTrustBundles, including Kubelet, are free to reorder and
|
||||
// deduplicate certificate blocks in this file according to their own logic,
|
||||
// as well as to drop PEM block headers and inter-block data.
|
||||
TrustBundle string
|
||||
}
|
||||
|
||||
// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object
|
||||
|
||||
// ClusterTrustBundleList is a collection of ClusterTrustBundle objects
|
||||
type ClusterTrustBundleList struct {
|
||||
metav1.TypeMeta
|
||||
// +optional
|
||||
metav1.ListMeta
|
||||
|
||||
// Items is a collection of ClusterTrustBundle objects
|
||||
Items []ClusterTrustBundle
|
||||
}
|
||||
|
37
pkg/apis/certificates/v1alpha1/conversion.go
Normal file
37
pkg/apis/certificates/v1alpha1/conversion.go
Normal file
@ -0,0 +1,37 @@
|
||||
/*
|
||||
Copyright 2022 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 v1alpha1
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
)
|
||||
|
||||
func addConversionFuncs(scheme *runtime.Scheme) error {
|
||||
return scheme.AddFieldLabelConversionFunc(
|
||||
SchemeGroupVersion.WithKind("ClusterTrustBundle"),
|
||||
func(label, value string) (string, string, error) {
|
||||
switch label {
|
||||
case "metadata.name", "spec.signerName":
|
||||
return label, value, nil
|
||||
default:
|
||||
return "", "", fmt.Errorf("field label not supported: %s", label)
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
23
pkg/apis/certificates/v1alpha1/defaults.go
Normal file
23
pkg/apis/certificates/v1alpha1/defaults.go
Normal file
@ -0,0 +1,23 @@
|
||||
/*
|
||||
Copyright 2022 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 v1alpha1
|
||||
|
||||
import "k8s.io/apimachinery/pkg/runtime"
|
||||
|
||||
func addDefaultingFuncs(scheme *runtime.Scheme) error {
|
||||
return RegisterDefaults(scheme)
|
||||
}
|
24
pkg/apis/certificates/v1alpha1/doc.go
Normal file
24
pkg/apis/certificates/v1alpha1/doc.go
Normal file
@ -0,0 +1,24 @@
|
||||
/*
|
||||
Copyright 2022 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.
|
||||
*/
|
||||
|
||||
// +k8s:conversion-gen=k8s.io/kubernetes/pkg/apis/certificates
|
||||
// +k8s:conversion-gen-external-types=k8s.io/api/certificates/v1alpha1
|
||||
// +k8s:defaulter-gen=TypeMeta
|
||||
// +k8s:defaulter-gen-input=k8s.io/api/certificates/v1alpha1
|
||||
|
||||
// +groupName=certificates.k8s.io
|
||||
|
||||
package v1alpha1 // import "k8s.io/kubernetes/pkg/apis/certificates/v1alpha1"
|
43
pkg/apis/certificates/v1alpha1/register.go
Normal file
43
pkg/apis/certificates/v1alpha1/register.go
Normal file
@ -0,0 +1,43 @@
|
||||
/*
|
||||
Copyright 2022 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 v1alpha1
|
||||
|
||||
import (
|
||||
certificatesv1alpha1 "k8s.io/api/certificates/v1alpha1"
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
)
|
||||
|
||||
// GroupName is the group name used in this package.
|
||||
const GroupName = "certificates.k8s.io"
|
||||
|
||||
// SchemeGroupVersion is the group and version used in this package.
|
||||
var SchemeGroupVersion = schema.GroupVersion{
|
||||
Group: GroupName,
|
||||
Version: "v1alpha1",
|
||||
}
|
||||
|
||||
var (
|
||||
localSchemeBuilder = &certificatesv1alpha1.SchemeBuilder
|
||||
AddToScheme = localSchemeBuilder.AddToScheme
|
||||
)
|
||||
|
||||
func init() {
|
||||
// We only register manually written functions here. The registration of the
|
||||
// generated functions takes place in the generated files. The separation
|
||||
// makes the code compile even when the generated files are missing.
|
||||
localSchemeBuilder.Register(addDefaultingFuncs, addConversionFuncs)
|
||||
}
|
@ -25,6 +25,7 @@ import (
|
||||
|
||||
v1 "k8s.io/api/core/v1"
|
||||
apiequality "k8s.io/apimachinery/pkg/api/equality"
|
||||
apimachineryvalidation "k8s.io/apimachinery/pkg/api/validation"
|
||||
"k8s.io/apimachinery/pkg/util/diff"
|
||||
"k8s.io/apimachinery/pkg/util/sets"
|
||||
utilvalidation "k8s.io/apimachinery/pkg/util/validation"
|
||||
@ -197,7 +198,7 @@ func validateCertificateSigningRequest(csr *certificates.CertificateSigningReque
|
||||
if !opts.allowLegacySignerName && csr.Spec.SignerName == certificates.LegacyUnknownSignerName {
|
||||
allErrs = append(allErrs, field.Invalid(specPath.Child("signerName"), csr.Spec.SignerName, "the legacy signerName is not allowed via this API version"))
|
||||
} else {
|
||||
allErrs = append(allErrs, ValidateCertificateSigningRequestSignerName(specPath.Child("signerName"), csr.Spec.SignerName)...)
|
||||
allErrs = append(allErrs, ValidateSignerName(specPath.Child("signerName"), csr.Spec.SignerName)...)
|
||||
}
|
||||
if csr.Spec.ExpirationSeconds != nil && *csr.Spec.ExpirationSeconds < 600 {
|
||||
allErrs = append(allErrs, field.Invalid(specPath.Child("expirationSeconds"), *csr.Spec.ExpirationSeconds, "may not specify a duration less than 600 seconds (10 minutes)"))
|
||||
@ -272,7 +273,7 @@ func validateConditions(fldPath *field.Path, csr *certificates.CertificateSignin
|
||||
// The max length of a namespace name is 63 characters (DNS1123Label max length)
|
||||
// The max length of a resource name is 253 characters (DNS1123Subdomain max length)
|
||||
// We then add an additional 2 characters to account for the one '.' and one '/'.
|
||||
func ValidateCertificateSigningRequestSignerName(fldPath *field.Path, signerName string) field.ErrorList {
|
||||
func ValidateSignerName(fldPath *field.Path, signerName string) field.ErrorList {
|
||||
var el field.ErrorList
|
||||
if len(signerName) == 0 {
|
||||
el = append(el, field.Required(fldPath, ""))
|
||||
@ -537,3 +538,129 @@ func hasDuplicateUsage(usages []certificates.KeyUsage) bool {
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// We require your name to be prefixed by .spec.signerName
|
||||
func validateClusterTrustBundleName(signerName string) func(name string, prefix bool) []string {
|
||||
return func(name string, isPrefix bool) []string {
|
||||
if signerName == "" {
|
||||
if strings.Contains(name, ":") {
|
||||
return []string{"ClusterTrustBundle without signer name must not have \":\" in its name"}
|
||||
}
|
||||
return apimachineryvalidation.NameIsDNSSubdomain(name, isPrefix)
|
||||
}
|
||||
|
||||
requiredPrefix := strings.ReplaceAll(signerName, "/", ":") + ":"
|
||||
if !strings.HasPrefix(name, requiredPrefix) {
|
||||
return []string{fmt.Sprintf("ClusterTrustBundle for signerName %s must be named with prefix %s", signerName, requiredPrefix)}
|
||||
}
|
||||
return apimachineryvalidation.NameIsDNSSubdomain(strings.TrimPrefix(name, requiredPrefix), isPrefix)
|
||||
}
|
||||
}
|
||||
|
||||
type ValidateClusterTrustBundleOptions struct {
|
||||
SuppressBundleParsing bool
|
||||
}
|
||||
|
||||
// ValidateClusterTrustBundle runs all validation checks on bundle.
|
||||
func ValidateClusterTrustBundle(bundle *certificates.ClusterTrustBundle, opts ValidateClusterTrustBundleOptions) field.ErrorList {
|
||||
var allErrors field.ErrorList
|
||||
|
||||
metaErrors := apivalidation.ValidateObjectMeta(&bundle.ObjectMeta, false, validateClusterTrustBundleName(bundle.Spec.SignerName), field.NewPath("metadata"))
|
||||
allErrors = append(allErrors, metaErrors...)
|
||||
|
||||
if bundle.Spec.SignerName != "" {
|
||||
signerNameErrors := ValidateSignerName(field.NewPath("spec", "signerName"), bundle.Spec.SignerName)
|
||||
allErrors = append(allErrors, signerNameErrors...)
|
||||
}
|
||||
|
||||
if !opts.SuppressBundleParsing {
|
||||
pemErrors := validateTrustBundle(field.NewPath("spec", "trustBundle"), bundle.Spec.TrustBundle)
|
||||
allErrors = append(allErrors, pemErrors...)
|
||||
}
|
||||
|
||||
return allErrors
|
||||
}
|
||||
|
||||
// ValidateClusterTrustBundleUpdate runs all update validation checks on an
|
||||
// update.
|
||||
func ValidateClusterTrustBundleUpdate(newBundle, oldBundle *certificates.ClusterTrustBundle) field.ErrorList {
|
||||
// If the caller isn't changing the TrustBundle field, don't parse it.
|
||||
// This helps smoothly handle changes in Go's PEM or X.509 parsing
|
||||
// libraries.
|
||||
opts := ValidateClusterTrustBundleOptions{}
|
||||
if newBundle.Spec.TrustBundle == oldBundle.Spec.TrustBundle {
|
||||
opts.SuppressBundleParsing = true
|
||||
}
|
||||
|
||||
var allErrors field.ErrorList
|
||||
allErrors = append(allErrors, ValidateClusterTrustBundle(newBundle, opts)...)
|
||||
allErrors = append(allErrors, apivalidation.ValidateObjectMetaUpdate(&newBundle.ObjectMeta, &oldBundle.ObjectMeta, field.NewPath("metadata"))...)
|
||||
allErrors = append(allErrors, apivalidation.ValidateImmutableField(newBundle.Spec.SignerName, oldBundle.Spec.SignerName, field.NewPath("spec", "signerName"))...)
|
||||
return allErrors
|
||||
}
|
||||
|
||||
// validateTrustBundle rejects intra-block headers, blocks
|
||||
// that don't parse as X.509 CA certificates, and duplicate trust anchors. It
|
||||
// requires that at least one trust anchor is provided.
|
||||
func validateTrustBundle(path *field.Path, in string) field.ErrorList {
|
||||
var allErrors field.ErrorList
|
||||
|
||||
blockDedupe := map[string][]int{}
|
||||
|
||||
rest := []byte(in)
|
||||
var b *pem.Block
|
||||
i := -1
|
||||
for {
|
||||
b, rest = pem.Decode(rest)
|
||||
if b == nil {
|
||||
break
|
||||
}
|
||||
i++
|
||||
|
||||
if b.Type != "CERTIFICATE" {
|
||||
allErrors = append(allErrors, field.Invalid(path, "<value omitted>", fmt.Sprintf("entry %d has bad block type: %v", i, b.Type)))
|
||||
continue
|
||||
}
|
||||
|
||||
if len(b.Headers) != 0 {
|
||||
allErrors = append(allErrors, field.Invalid(path, "<value omitted>", fmt.Sprintf("entry %d has PEM block headers", i)))
|
||||
continue
|
||||
}
|
||||
|
||||
cert, err := x509.ParseCertificate(b.Bytes)
|
||||
if err != nil {
|
||||
allErrors = append(allErrors, field.Invalid(path, "<value omitted>", fmt.Sprintf("entry %d does not parse as X.509", i)))
|
||||
continue
|
||||
}
|
||||
|
||||
if !cert.IsCA {
|
||||
allErrors = append(allErrors, field.Invalid(path, "<value omitted>", fmt.Sprintf("entry %d does not have the CA bit set", i)))
|
||||
continue
|
||||
}
|
||||
|
||||
if !cert.BasicConstraintsValid {
|
||||
allErrors = append(allErrors, field.Invalid(path, "<value omitted>", fmt.Sprintf("entry %d has invalid basic constraints", i)))
|
||||
continue
|
||||
}
|
||||
|
||||
blockDedupe[string(b.Bytes)] = append(blockDedupe[string(b.Bytes)], i)
|
||||
}
|
||||
|
||||
// If we had a malformed block, don't also output potentially-redundant
|
||||
// errors about duplicate or missing trust anchors.
|
||||
if len(allErrors) != 0 {
|
||||
return allErrors
|
||||
}
|
||||
|
||||
if len(blockDedupe) == 0 {
|
||||
allErrors = append(allErrors, field.Invalid(path, "<value omitted>", "at least one trust anchor must be provided"))
|
||||
}
|
||||
|
||||
for _, indices := range blockDedupe {
|
||||
if len(indices) > 1 {
|
||||
allErrors = append(allErrors, field.Invalid(path, "<value omitted>", fmt.Sprintf("duplicate trust anchor (indices %v)", indices)))
|
||||
}
|
||||
}
|
||||
|
||||
return allErrors
|
||||
}
|
||||
|
@ -23,12 +23,15 @@ import (
|
||||
"crypto/x509/pkix"
|
||||
"encoding/pem"
|
||||
"fmt"
|
||||
"math/big"
|
||||
mathrand "math/rand"
|
||||
"reflect"
|
||||
"regexp"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/google/go-cmp/cmp"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/util/sets"
|
||||
"k8s.io/apimachinery/pkg/util/validation/field"
|
||||
@ -1095,6 +1098,471 @@ func Test_validateCertificateSigningRequestOptions(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func mustMakeCertificate(t *testing.T, template *x509.Certificate) []byte {
|
||||
gen := mathrand.New(mathrand.NewSource(12345))
|
||||
|
||||
pub, priv, err := ed25519.GenerateKey(gen)
|
||||
if err != nil {
|
||||
t.Fatalf("Error while generating key: %v", err)
|
||||
}
|
||||
|
||||
cert, err := x509.CreateCertificate(gen, template, template, pub, priv)
|
||||
if err != nil {
|
||||
t.Fatalf("Error while making certificate: %v", err)
|
||||
}
|
||||
|
||||
return cert
|
||||
}
|
||||
|
||||
func mustMakePEMBlock(blockType string, headers map[string]string, data []byte) string {
|
||||
return string(pem.EncodeToMemory(&pem.Block{
|
||||
Type: blockType,
|
||||
Headers: headers,
|
||||
Bytes: data,
|
||||
}))
|
||||
}
|
||||
|
||||
func TestValidateClusterTrustBundle(t *testing.T) {
|
||||
goodCert1 := mustMakeCertificate(t, &x509.Certificate{
|
||||
SerialNumber: big.NewInt(0),
|
||||
Subject: pkix.Name{
|
||||
CommonName: "root1",
|
||||
},
|
||||
IsCA: true,
|
||||
BasicConstraintsValid: true,
|
||||
})
|
||||
|
||||
goodCert2 := mustMakeCertificate(t, &x509.Certificate{
|
||||
SerialNumber: big.NewInt(0),
|
||||
Subject: pkix.Name{
|
||||
CommonName: "root2",
|
||||
},
|
||||
IsCA: true,
|
||||
BasicConstraintsValid: true,
|
||||
})
|
||||
|
||||
badNotCACert := mustMakeCertificate(t, &x509.Certificate{
|
||||
SerialNumber: big.NewInt(0),
|
||||
Subject: pkix.Name{
|
||||
CommonName: "root3",
|
||||
},
|
||||
})
|
||||
|
||||
goodCert1Block := string(mustMakePEMBlock("CERTIFICATE", nil, goodCert1))
|
||||
goodCert2Block := string(mustMakePEMBlock("CERTIFICATE", nil, goodCert2))
|
||||
|
||||
goodCert1AlternateBlock := strings.ReplaceAll(goodCert1Block, "\n", "\n\t\n")
|
||||
|
||||
badNotCACertBlock := string(mustMakePEMBlock("CERTIFICATE", nil, badNotCACert))
|
||||
|
||||
badBlockHeadersBlock := string(mustMakePEMBlock("CERTIFICATE", map[string]string{"key": "value"}, goodCert1))
|
||||
badBlockTypeBlock := string(mustMakePEMBlock("NOTACERTIFICATE", nil, goodCert1))
|
||||
badNonParseableBlock := string(mustMakePEMBlock("CERTIFICATE", nil, []byte("this is not a certificate")))
|
||||
|
||||
testCases := []struct {
|
||||
description string
|
||||
bundle *capi.ClusterTrustBundle
|
||||
opts ValidateClusterTrustBundleOptions
|
||||
wantErrors field.ErrorList
|
||||
}{
|
||||
{
|
||||
description: "valid, no signer name",
|
||||
bundle: &capi.ClusterTrustBundle{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "foo",
|
||||
},
|
||||
Spec: capi.ClusterTrustBundleSpec{
|
||||
TrustBundle: goodCert1Block,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
description: "invalid, no signer name, invalid name",
|
||||
bundle: &capi.ClusterTrustBundle{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "k8s.io:bar:foo",
|
||||
},
|
||||
Spec: capi.ClusterTrustBundleSpec{
|
||||
TrustBundle: goodCert1Block,
|
||||
},
|
||||
},
|
||||
wantErrors: field.ErrorList{
|
||||
field.Invalid(field.NewPath("metadata", "name"), "k8s.io:bar:foo", "ClusterTrustBundle without signer name must not have \":\" in its name"),
|
||||
},
|
||||
},
|
||||
{
|
||||
description: "valid, with signer name",
|
||||
bundle: &capi.ClusterTrustBundle{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "k8s.io:foo:bar",
|
||||
},
|
||||
Spec: capi.ClusterTrustBundleSpec{
|
||||
SignerName: "k8s.io/foo",
|
||||
TrustBundle: goodCert1Block,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
description: "invalid, with signer name, missing name prefix",
|
||||
bundle: &capi.ClusterTrustBundle{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "look-ma-no-prefix",
|
||||
},
|
||||
Spec: capi.ClusterTrustBundleSpec{
|
||||
SignerName: "k8s.io/foo",
|
||||
TrustBundle: goodCert1Block,
|
||||
},
|
||||
},
|
||||
wantErrors: field.ErrorList{
|
||||
field.Invalid(field.NewPath("metadata", "name"), "look-ma-no-prefix", "ClusterTrustBundle for signerName k8s.io/foo must be named with prefix k8s.io:foo:"),
|
||||
},
|
||||
},
|
||||
{
|
||||
description: "invalid, with signer name, empty name suffix",
|
||||
bundle: &capi.ClusterTrustBundle{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "k8s.io:foo:",
|
||||
},
|
||||
Spec: capi.ClusterTrustBundleSpec{
|
||||
SignerName: "k8s.io/foo",
|
||||
TrustBundle: goodCert1Block,
|
||||
},
|
||||
},
|
||||
wantErrors: field.ErrorList{
|
||||
field.Invalid(field.NewPath("metadata", "name"), "k8s.io:foo:", `a lowercase RFC 1123 subdomain must consist of lower case alphanumeric characters, '-' or '.', and must start and end with an alphanumeric character (e.g. 'example.com', regex used for validation is '[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*')`),
|
||||
},
|
||||
},
|
||||
{
|
||||
description: "invalid, with signer name, bad name suffix",
|
||||
bundle: &capi.ClusterTrustBundle{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "k8s.io:foo:123notvalidDNSSubdomain",
|
||||
},
|
||||
Spec: capi.ClusterTrustBundleSpec{
|
||||
SignerName: "k8s.io/foo",
|
||||
TrustBundle: goodCert1Block,
|
||||
},
|
||||
},
|
||||
wantErrors: field.ErrorList{
|
||||
field.Invalid(field.NewPath("metadata", "name"), "k8s.io:foo:123notvalidDNSSubdomain", `a lowercase RFC 1123 subdomain must consist of lower case alphanumeric characters, '-' or '.', and must start and end with an alphanumeric character (e.g. 'example.com', regex used for validation is '[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*')`),
|
||||
},
|
||||
},
|
||||
{
|
||||
description: "valid, with signer name, with inter-block garbage",
|
||||
bundle: &capi.ClusterTrustBundle{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "k8s.io:foo:abc",
|
||||
},
|
||||
Spec: capi.ClusterTrustBundleSpec{
|
||||
SignerName: "k8s.io/foo",
|
||||
TrustBundle: "garbage\n" + goodCert1Block + "\ngarbage\n" + goodCert2Block,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
description: "invalid, no signer name, no trust anchors",
|
||||
bundle: &capi.ClusterTrustBundle{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "foo",
|
||||
},
|
||||
Spec: capi.ClusterTrustBundleSpec{},
|
||||
},
|
||||
wantErrors: field.ErrorList{
|
||||
field.Invalid(field.NewPath("spec", "trustBundle"), "<value omitted>", "at least one trust anchor must be provided"),
|
||||
},
|
||||
},
|
||||
{
|
||||
description: "invalid, no trust anchors",
|
||||
bundle: &capi.ClusterTrustBundle{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "k8s.io:foo:abc",
|
||||
},
|
||||
Spec: capi.ClusterTrustBundleSpec{
|
||||
SignerName: "k8s.io/foo",
|
||||
},
|
||||
},
|
||||
wantErrors: field.ErrorList{
|
||||
field.Invalid(field.NewPath("spec", "trustBundle"), "<value omitted>", "at least one trust anchor must be provided"),
|
||||
},
|
||||
},
|
||||
{
|
||||
description: "invalid, bad signer name",
|
||||
bundle: &capi.ClusterTrustBundle{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "invalid:foo",
|
||||
},
|
||||
Spec: capi.ClusterTrustBundleSpec{
|
||||
SignerName: "invalid",
|
||||
TrustBundle: goodCert1Block,
|
||||
},
|
||||
},
|
||||
wantErrors: field.ErrorList{
|
||||
field.Invalid(field.NewPath("spec", "signerName"), "invalid", "must be a fully qualified domain and path of the form 'example.com/signer-name'"),
|
||||
},
|
||||
},
|
||||
{
|
||||
description: "invalid, no blocks",
|
||||
bundle: &capi.ClusterTrustBundle{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "foo",
|
||||
},
|
||||
Spec: capi.ClusterTrustBundleSpec{
|
||||
TrustBundle: "non block garbage",
|
||||
},
|
||||
},
|
||||
wantErrors: field.ErrorList{
|
||||
field.Invalid(field.NewPath("spec", "trustBundle"), "<value omitted>", "at least one trust anchor must be provided"),
|
||||
},
|
||||
},
|
||||
{
|
||||
description: "invalid, bad block type",
|
||||
bundle: &capi.ClusterTrustBundle{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "foo",
|
||||
},
|
||||
Spec: capi.ClusterTrustBundleSpec{
|
||||
TrustBundle: goodCert1Block + "\n" + badBlockTypeBlock,
|
||||
},
|
||||
},
|
||||
wantErrors: field.ErrorList{
|
||||
field.Invalid(field.NewPath("spec", "trustBundle"), "<value omitted>", "entry 1 has bad block type: NOTACERTIFICATE"),
|
||||
},
|
||||
},
|
||||
{
|
||||
description: "invalid, block with headers",
|
||||
bundle: &capi.ClusterTrustBundle{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "foo",
|
||||
},
|
||||
Spec: capi.ClusterTrustBundleSpec{
|
||||
TrustBundle: goodCert1Block + "\n" + badBlockHeadersBlock,
|
||||
},
|
||||
},
|
||||
wantErrors: field.ErrorList{
|
||||
field.Invalid(field.NewPath("spec", "trustBundle"), "<value omitted>", "entry 1 has PEM block headers"),
|
||||
},
|
||||
},
|
||||
{
|
||||
description: "invalid, cert is not a CA cert",
|
||||
bundle: &capi.ClusterTrustBundle{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "foo",
|
||||
},
|
||||
Spec: capi.ClusterTrustBundleSpec{
|
||||
TrustBundle: badNotCACertBlock,
|
||||
},
|
||||
},
|
||||
wantErrors: field.ErrorList{
|
||||
field.Invalid(field.NewPath("spec", "trustBundle"), "<value omitted>", "entry 0 does not have the CA bit set"),
|
||||
},
|
||||
},
|
||||
{
|
||||
description: "invalid, duplicated blocks",
|
||||
bundle: &capi.ClusterTrustBundle{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "foo",
|
||||
},
|
||||
Spec: capi.ClusterTrustBundleSpec{
|
||||
TrustBundle: goodCert1Block + "\n" + goodCert1AlternateBlock,
|
||||
},
|
||||
},
|
||||
wantErrors: field.ErrorList{
|
||||
field.Invalid(field.NewPath("spec", "trustBundle"), "<value omitted>", "duplicate trust anchor (indices [0 1])"),
|
||||
},
|
||||
},
|
||||
{
|
||||
description: "invalid, non-certificate entry",
|
||||
bundle: &capi.ClusterTrustBundle{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "foo",
|
||||
},
|
||||
Spec: capi.ClusterTrustBundleSpec{
|
||||
TrustBundle: goodCert1Block + "\n" + badNonParseableBlock,
|
||||
},
|
||||
},
|
||||
wantErrors: field.ErrorList{
|
||||
field.Invalid(field.NewPath("spec", "trustBundle"), "<value omitted>", "entry 1 does not parse as X.509"),
|
||||
},
|
||||
},
|
||||
{
|
||||
description: "allow any old garbage in the PEM field if we suppress parsing",
|
||||
bundle: &capi.ClusterTrustBundle{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "foo",
|
||||
},
|
||||
Spec: capi.ClusterTrustBundleSpec{
|
||||
TrustBundle: "garbage",
|
||||
},
|
||||
},
|
||||
opts: ValidateClusterTrustBundleOptions{
|
||||
SuppressBundleParsing: true,
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.description, func(t *testing.T) {
|
||||
gotErrors := ValidateClusterTrustBundle(tc.bundle, tc.opts)
|
||||
if diff := cmp.Diff(gotErrors, tc.wantErrors); diff != "" {
|
||||
t.Fatalf("Unexpected error output from Validate; diff (-got +want)\n%s", diff)
|
||||
}
|
||||
|
||||
// When there are no changes to the object,
|
||||
// ValidateClusterTrustBundleUpdate should not report errors about
|
||||
// the TrustBundle field.
|
||||
tc.bundle.ObjectMeta.ResourceVersion = "1"
|
||||
newBundle := tc.bundle.DeepCopy()
|
||||
newBundle.ObjectMeta.ResourceVersion = "2"
|
||||
gotErrors = ValidateClusterTrustBundleUpdate(newBundle, tc.bundle)
|
||||
|
||||
var filteredWantErrors field.ErrorList
|
||||
for _, err := range tc.wantErrors {
|
||||
if err.Field != "spec.trustBundle" {
|
||||
filteredWantErrors = append(filteredWantErrors, err)
|
||||
}
|
||||
}
|
||||
|
||||
if diff := cmp.Diff(gotErrors, filteredWantErrors); diff != "" {
|
||||
t.Fatalf("Unexpected error output from ValidateUpdate; diff (-got +want)\n%s", diff)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateClusterTrustBundleUpdate(t *testing.T) {
|
||||
goodCert1 := mustMakeCertificate(t, &x509.Certificate{
|
||||
SerialNumber: big.NewInt(0),
|
||||
Subject: pkix.Name{
|
||||
CommonName: "root1",
|
||||
},
|
||||
IsCA: true,
|
||||
BasicConstraintsValid: true,
|
||||
})
|
||||
|
||||
goodCert2 := mustMakeCertificate(t, &x509.Certificate{
|
||||
SerialNumber: big.NewInt(0),
|
||||
Subject: pkix.Name{
|
||||
CommonName: "root2",
|
||||
},
|
||||
IsCA: true,
|
||||
BasicConstraintsValid: true,
|
||||
})
|
||||
|
||||
goodCert1Block := string(mustMakePEMBlock("CERTIFICATE", nil, goodCert1))
|
||||
goodCert2Block := string(mustMakePEMBlock("CERTIFICATE", nil, goodCert2))
|
||||
|
||||
testCases := []struct {
|
||||
description string
|
||||
oldBundle, newBundle *capi.ClusterTrustBundle
|
||||
wantErrors field.ErrorList
|
||||
}{
|
||||
{
|
||||
description: "changing signer name disallowed",
|
||||
oldBundle: &capi.ClusterTrustBundle{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "k8s.io:foo:bar",
|
||||
},
|
||||
Spec: capi.ClusterTrustBundleSpec{
|
||||
SignerName: "k8s.io/foo",
|
||||
TrustBundle: goodCert1Block,
|
||||
},
|
||||
},
|
||||
newBundle: &capi.ClusterTrustBundle{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "k8s.io:foo:bar",
|
||||
},
|
||||
Spec: capi.ClusterTrustBundleSpec{
|
||||
SignerName: "k8s.io/bar",
|
||||
TrustBundle: goodCert1Block,
|
||||
},
|
||||
},
|
||||
wantErrors: field.ErrorList{
|
||||
field.Invalid(field.NewPath("metadata", "name"), "k8s.io:foo:bar", "ClusterTrustBundle for signerName k8s.io/bar must be named with prefix k8s.io:bar:"),
|
||||
field.Invalid(field.NewPath("spec", "signerName"), "k8s.io/bar", "field is immutable"),
|
||||
},
|
||||
},
|
||||
{
|
||||
description: "adding certificate allowed",
|
||||
oldBundle: &capi.ClusterTrustBundle{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "k8s.io:foo:bar",
|
||||
},
|
||||
Spec: capi.ClusterTrustBundleSpec{
|
||||
SignerName: "k8s.io/foo",
|
||||
TrustBundle: goodCert1Block,
|
||||
},
|
||||
},
|
||||
newBundle: &capi.ClusterTrustBundle{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "k8s.io:foo:bar",
|
||||
},
|
||||
Spec: capi.ClusterTrustBundleSpec{
|
||||
SignerName: "k8s.io/foo",
|
||||
TrustBundle: goodCert1Block + "\n" + goodCert2Block,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
description: "emptying trustBundle disallowed",
|
||||
oldBundle: &capi.ClusterTrustBundle{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "k8s.io:foo:bar",
|
||||
},
|
||||
Spec: capi.ClusterTrustBundleSpec{
|
||||
SignerName: "k8s.io/foo",
|
||||
TrustBundle: goodCert1Block,
|
||||
},
|
||||
},
|
||||
newBundle: &capi.ClusterTrustBundle{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "k8s.io:foo:bar",
|
||||
},
|
||||
Spec: capi.ClusterTrustBundleSpec{
|
||||
SignerName: "k8s.io/foo",
|
||||
TrustBundle: "",
|
||||
},
|
||||
},
|
||||
wantErrors: field.ErrorList{
|
||||
field.Invalid(field.NewPath("spec", "trustBundle"), "<value omitted>", "at least one trust anchor must be provided"),
|
||||
},
|
||||
},
|
||||
{
|
||||
description: "emptying trustBundle (replace with non-block garbage) disallowed",
|
||||
oldBundle: &capi.ClusterTrustBundle{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "k8s.io:foo:bar",
|
||||
},
|
||||
Spec: capi.ClusterTrustBundleSpec{
|
||||
SignerName: "k8s.io/foo",
|
||||
TrustBundle: goodCert1Block,
|
||||
},
|
||||
},
|
||||
newBundle: &capi.ClusterTrustBundle{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "k8s.io:foo:bar",
|
||||
},
|
||||
Spec: capi.ClusterTrustBundleSpec{
|
||||
SignerName: "k8s.io/foo",
|
||||
TrustBundle: "non block garbage",
|
||||
},
|
||||
},
|
||||
wantErrors: field.ErrorList{
|
||||
field.Invalid(field.NewPath("spec", "trustBundle"), "<value omitted>", "at least one trust anchor must be provided"),
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.description, func(t *testing.T) {
|
||||
tc.oldBundle.ObjectMeta.ResourceVersion = "1"
|
||||
tc.newBundle.ObjectMeta.ResourceVersion = "2"
|
||||
gotErrors := ValidateClusterTrustBundleUpdate(tc.newBundle, tc.oldBundle)
|
||||
if diff := cmp.Diff(gotErrors, tc.wantErrors); diff != "" {
|
||||
t.Errorf("Unexpected error output from ValidateUpdate; diff (-got +want)\n%s", diff)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
var (
|
||||
validCertificate = []byte(`
|
||||
Leading non-PEM content
|
||||
|
@ -40,6 +40,7 @@ import (
|
||||
batchapiv1 "k8s.io/api/batch/v1"
|
||||
batchapiv1beta1 "k8s.io/api/batch/v1beta1"
|
||||
certificatesapiv1 "k8s.io/api/certificates/v1"
|
||||
certificatesv1alpha1 "k8s.io/api/certificates/v1alpha1"
|
||||
coordinationapiv1 "k8s.io/api/coordination/v1"
|
||||
apiv1 "k8s.io/api/core/v1"
|
||||
discoveryv1 "k8s.io/api/discovery/v1"
|
||||
@ -734,6 +735,7 @@ var (
|
||||
apiserverinternalv1alpha1.SchemeGroupVersion,
|
||||
authenticationv1alpha1.SchemeGroupVersion,
|
||||
resourcev1alpha2.SchemeGroupVersion,
|
||||
certificatesv1alpha1.SchemeGroupVersion,
|
||||
networkingapiv1alpha1.SchemeGroupVersion,
|
||||
storageapiv1alpha1.SchemeGroupVersion,
|
||||
flowcontrolv1alpha1.SchemeGroupVersion,
|
||||
|
@ -67,6 +67,12 @@ const (
|
||||
// Enables dual-stack --node-ip in kubelet with external cloud providers
|
||||
CloudDualStackNodeIPs featuregate.Feature = "CloudDualStackNodeIPs"
|
||||
|
||||
// owner: @ahmedtd
|
||||
// alpha: v1.26
|
||||
//
|
||||
// Enable ClusterTrustBundle objects and Kubelet integration.
|
||||
ClusterTrustBundle featuregate.Feature = "ClusterTrustBundle"
|
||||
|
||||
// owner: @szuecs
|
||||
// alpha: v1.12
|
||||
//
|
||||
@ -934,6 +940,8 @@ var defaultKubernetesFeatureGates = map[featuregate.Feature]featuregate.FeatureS
|
||||
|
||||
CloudDualStackNodeIPs: {Default: false, PreRelease: featuregate.Alpha},
|
||||
|
||||
ClusterTrustBundle: {Default: false, PreRelease: featuregate.Alpha},
|
||||
|
||||
CPUCFSQuotaPeriod: {Default: false, PreRelease: featuregate.Alpha},
|
||||
|
||||
CPUManager: {Default: true, PreRelease: featuregate.GA, LockToDefault: true}, // GA in 1.26
|
||||
|
@ -28,6 +28,7 @@ import (
|
||||
"k8s.io/kubernetes/pkg/api/legacyscheme"
|
||||
"k8s.io/kubernetes/pkg/apis/admissionregistration"
|
||||
"k8s.io/kubernetes/pkg/apis/apps"
|
||||
"k8s.io/kubernetes/pkg/apis/certificates"
|
||||
api "k8s.io/kubernetes/pkg/apis/core"
|
||||
"k8s.io/kubernetes/pkg/apis/events"
|
||||
"k8s.io/kubernetes/pkg/apis/extensions"
|
||||
@ -72,6 +73,7 @@ func NewStorageFactoryConfig() *StorageFactoryConfig {
|
||||
admissionregistration.Resource("validatingadmissionpolicybindings").WithVersion("v1alpha1"),
|
||||
networking.Resource("clustercidrs").WithVersion("v1alpha1"),
|
||||
networking.Resource("ipaddresses").WithVersion("v1alpha1"),
|
||||
certificates.Resource("clustertrustbundles").WithVersion("v1alpha1"),
|
||||
}
|
||||
|
||||
return &StorageFactoryConfig{
|
||||
|
@ -26,6 +26,7 @@ import (
|
||||
"k8s.io/kubernetes/plugin/pkg/admission/alwayspullimages"
|
||||
"k8s.io/kubernetes/plugin/pkg/admission/antiaffinity"
|
||||
certapproval "k8s.io/kubernetes/plugin/pkg/admission/certificates/approval"
|
||||
"k8s.io/kubernetes/plugin/pkg/admission/certificates/ctbattest"
|
||||
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"
|
||||
@ -90,6 +91,7 @@ var AllOrderedPlugins = []string{
|
||||
runtimeclass.PluginName, // RuntimeClass
|
||||
certapproval.PluginName, // CertificateApproval
|
||||
certsigning.PluginName, // CertificateSigning
|
||||
ctbattest.PluginName, // ClusterTrustBundleAttest
|
||||
certsubjectrestriction.PluginName, // CertificateSubjectRestriction
|
||||
defaultingressclass.PluginName, // DefaultIngressClass
|
||||
denyserviceexternalips.PluginName, // DenyServiceExternalIPs
|
||||
@ -137,6 +139,7 @@ func RegisterAllAdmissionPlugins(plugins *admission.Plugins) {
|
||||
storageobjectinuseprotection.Register(plugins)
|
||||
certapproval.Register(plugins)
|
||||
certsigning.Register(plugins)
|
||||
ctbattest.Register(plugins)
|
||||
certsubjectrestriction.Register(plugins)
|
||||
}
|
||||
|
||||
@ -158,6 +161,7 @@ func DefaultOffAdmissionPlugins() sets.String {
|
||||
runtimeclass.PluginName, // RuntimeClass
|
||||
certapproval.PluginName, // CertificateApproval
|
||||
certsigning.PluginName, // CertificateSigning
|
||||
ctbattest.PluginName, // ClusterTrustBundleAttest
|
||||
certsubjectrestriction.PluginName, // CertificateSubjectRestriction
|
||||
defaultingressclass.PluginName, // DefaultIngressClass
|
||||
podsecurity.PluginName, // PodSecurity
|
||||
|
@ -31,6 +31,7 @@ import (
|
||||
autoscalingv2beta1 "k8s.io/api/autoscaling/v2beta1"
|
||||
batchv1 "k8s.io/api/batch/v1"
|
||||
batchv1beta1 "k8s.io/api/batch/v1beta1"
|
||||
certificatesv1alpha1 "k8s.io/api/certificates/v1alpha1"
|
||||
certificatesv1beta1 "k8s.io/api/certificates/v1beta1"
|
||||
coordinationv1 "k8s.io/api/coordination/v1"
|
||||
apiv1 "k8s.io/api/core/v1"
|
||||
@ -407,6 +408,13 @@ func AddHandlers(h printers.PrintHandler) {
|
||||
_ = h.TableHandler(certificateSigningRequestColumnDefinitions, printCertificateSigningRequest)
|
||||
_ = h.TableHandler(certificateSigningRequestColumnDefinitions, printCertificateSigningRequestList)
|
||||
|
||||
clusterTrustBundleColumnDefinitions := []metav1.TableColumnDefinition{
|
||||
{Name: "Name", Type: "string", Format: "name", Description: metav1.ObjectMeta{}.SwaggerDoc()["name"]},
|
||||
{Name: "SignerName", Type: "string", Description: certificatesv1alpha1.ClusterTrustBundleSpec{}.SwaggerDoc()["signerName"]},
|
||||
}
|
||||
h.TableHandler(clusterTrustBundleColumnDefinitions, printClusterTrustBundle)
|
||||
h.TableHandler(clusterTrustBundleColumnDefinitions, printClusterTrustBundleList)
|
||||
|
||||
leaseColumnDefinitions := []metav1.TableColumnDefinition{
|
||||
{Name: "Name", Type: "string", Format: "name", Description: metav1.ObjectMeta{}.SwaggerDoc()["name"]},
|
||||
{Name: "Holder", Type: "string", Description: coordinationv1.LeaseSpec{}.SwaggerDoc()["holderIdentity"]},
|
||||
@ -2095,6 +2103,30 @@ func printCertificateSigningRequestList(list *certificates.CertificateSigningReq
|
||||
return rows, nil
|
||||
}
|
||||
|
||||
func printClusterTrustBundle(obj *certificates.ClusterTrustBundle, options printers.GenerateOptions) ([]metav1.TableRow, error) {
|
||||
row := metav1.TableRow{
|
||||
Object: runtime.RawExtension{Object: obj},
|
||||
}
|
||||
signerName := "<none>"
|
||||
if obj.Spec.SignerName != "" {
|
||||
signerName = obj.Spec.SignerName
|
||||
}
|
||||
row.Cells = append(row.Cells, obj.Name, signerName)
|
||||
return []metav1.TableRow{row}, nil
|
||||
}
|
||||
|
||||
func printClusterTrustBundleList(list *certificates.ClusterTrustBundleList, options printers.GenerateOptions) ([]metav1.TableRow, error) {
|
||||
rows := make([]metav1.TableRow, 0, len(list.Items))
|
||||
for i := range list.Items {
|
||||
r, err := printClusterTrustBundle(&list.Items[i], options)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
rows = append(rows, r...)
|
||||
}
|
||||
return rows, nil
|
||||
}
|
||||
|
||||
func printComponentStatus(obj *api.ComponentStatus, options printers.GenerateOptions) ([]metav1.TableRow, error) {
|
||||
row := metav1.TableRow{
|
||||
Object: runtime.RawExtension{Object: obj},
|
||||
|
@ -0,0 +1,79 @@
|
||||
/*
|
||||
Copyright 2022 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 storage
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"k8s.io/apimachinery/pkg/fields"
|
||||
"k8s.io/apimachinery/pkg/labels"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/apiserver/pkg/registry/generic"
|
||||
genericregistry "k8s.io/apiserver/pkg/registry/generic/registry"
|
||||
"k8s.io/apiserver/pkg/registry/rest"
|
||||
api "k8s.io/kubernetes/pkg/apis/certificates"
|
||||
"k8s.io/kubernetes/pkg/printers"
|
||||
printersinternal "k8s.io/kubernetes/pkg/printers/internalversion"
|
||||
printerstorage "k8s.io/kubernetes/pkg/printers/storage"
|
||||
"k8s.io/kubernetes/pkg/registry/certificates/clustertrustbundle"
|
||||
)
|
||||
|
||||
// REST is a RESTStorage for ClusterTrustBundle.
|
||||
type REST struct {
|
||||
*genericregistry.Store
|
||||
}
|
||||
|
||||
var _ rest.StandardStorage = &REST{}
|
||||
var _ rest.TableConvertor = &REST{}
|
||||
var _ genericregistry.GenericStore = &REST{}
|
||||
|
||||
// NewREST returns a RESTStorage object for ClusterTrustBundle objects.
|
||||
func NewREST(optsGetter generic.RESTOptionsGetter) (*REST, error) {
|
||||
store := &genericregistry.Store{
|
||||
NewFunc: func() runtime.Object { return &api.ClusterTrustBundle{} },
|
||||
NewListFunc: func() runtime.Object { return &api.ClusterTrustBundleList{} },
|
||||
DefaultQualifiedResource: api.Resource("clustertrustbundles"),
|
||||
SingularQualifiedResource: api.Resource("clustertrustbundle"),
|
||||
|
||||
CreateStrategy: clustertrustbundle.Strategy,
|
||||
UpdateStrategy: clustertrustbundle.Strategy,
|
||||
DeleteStrategy: clustertrustbundle.Strategy,
|
||||
|
||||
TableConvertor: printerstorage.TableConvertor{TableGenerator: printers.NewTableGenerator().With(printersinternal.AddHandlers)},
|
||||
}
|
||||
options := &generic.StoreOptions{
|
||||
RESTOptions: optsGetter,
|
||||
AttrFunc: getAttrs,
|
||||
}
|
||||
if err := store.CompleteWithOptions(options); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &REST{store}, nil
|
||||
}
|
||||
|
||||
func getAttrs(obj runtime.Object) (labels.Set, fields.Set, error) {
|
||||
bundle, ok := obj.(*api.ClusterTrustBundle)
|
||||
if !ok {
|
||||
return nil, nil, fmt.Errorf("not a clustertrustbundle")
|
||||
}
|
||||
|
||||
selectableFields := generic.MergeFieldsSets(generic.ObjectMetaFieldsSet(&bundle.ObjectMeta, false), fields.Set{
|
||||
"spec.signerName": bundle.Spec.SignerName,
|
||||
})
|
||||
|
||||
return labels.Set(bundle.Labels), selectableFields, nil
|
||||
}
|
@ -0,0 +1,250 @@
|
||||
/*
|
||||
Copyright 2022 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 storage
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/fields"
|
||||
"k8s.io/apimachinery/pkg/labels"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/apiserver/pkg/registry/generic"
|
||||
genericregistrytest "k8s.io/apiserver/pkg/registry/generic/testing"
|
||||
etcd3testing "k8s.io/apiserver/pkg/storage/etcd3/testing"
|
||||
"k8s.io/kubernetes/pkg/apis/certificates"
|
||||
"k8s.io/kubernetes/pkg/registry/registrytest"
|
||||
)
|
||||
|
||||
const validCert1 = `
|
||||
-----BEGIN CERTIFICATE-----
|
||||
MIIDmTCCAoGgAwIBAgIUUW9bIIsHU61w3yQR6amBuVvRFvcwDQYJKoZIhvcNAQEL
|
||||
BQAwXDELMAkGA1UEBhMCeHgxCjAIBgNVBAgMAXgxCjAIBgNVBAcMAXgxCjAIBgNV
|
||||
BAoMAXgxCjAIBgNVBAsMAXgxCzAJBgNVBAMMAmNhMRAwDgYJKoZIhvcNAQkBFgF4
|
||||
MB4XDTIyMTAxODIzNTIyNFoXDTIzMTAxODIzNTIyNFowXDELMAkGA1UEBhMCeHgx
|
||||
CjAIBgNVBAgMAXgxCjAIBgNVBAcMAXgxCjAIBgNVBAoMAXgxCjAIBgNVBAsMAXgx
|
||||
CzAJBgNVBAMMAmNhMRAwDgYJKoZIhvcNAQkBFgF4MIIBIjANBgkqhkiG9w0BAQEF
|
||||
AAOCAQ8AMIIBCgKCAQEA4PeK4SmlsNwpw97gTtjODQytUfyqhBIwdENwJUbc019Y
|
||||
m3VTCRLCGXjUa22mV6/j7V+mZw114ePFYTiGAH+2dUzWAZOphvtzE5ttPuv6A6Zx
|
||||
k2J69lNFwJ2fPd7XQIH7pEIXjiEBaszxKZKMsN9+jOGu6iFFAwYLMemFYDbZHuqb
|
||||
OwdQcSEsy5wO2ANzFRuYzGXuNcS8jYLHftE8g2P+L0wXnV9eW6/lM2ZFxS/nzDJz
|
||||
qtzrEvQrBsmskTNC8gCRRZ7askp3CVdPKjC90sxAPwhpi8JjJZxSe1Bn/WRHUz82
|
||||
GFytEIJNx9hJY2GI316zkxgTbsxfRQe4QLJN7sRtpwIDAQABo1MwUTAdBgNVHQ4E
|
||||
FgQU9FGsI8t+cu68fGkhtvO9FtUd174wHwYDVR0jBBgwFoAU9FGsI8t+cu68fGkh
|
||||
tvO9FtUd174wDwYDVR0TAQH/BAUwAwEB/zANBgkqhkiG9w0BAQsFAAOCAQEAqDIp
|
||||
In5h2xZfEZcijT3mjfG8Bo6taxM2biy1M7wEpmDrElmrjMLsflZepcjgkSoVz9hP
|
||||
cSX/k9ls1zy1H799gcjs+afSpIa1N0nUIxAKF1RHsFa+dvXpSA8YdhUnbEcBnqx0
|
||||
vN2nDBFpdCSNf+EXNEj12+9ZJm6TLzx22f9vHyRCg4D36X3Rj1FCBWxhf0mSt3ek
|
||||
5px3H53Xu42MqzZCiJc8/m+IqZHaixZS4bsayssaxif2fNxzAIZhgTygo8P8QGjI
|
||||
rUmstMbg4PPq62x1yLAxEo+8XCg05saWZs384JE+K1SDqxobm51EROWVwi8jUrNC
|
||||
9nojtkQ+jDZD+1Stiw==
|
||||
-----END CERTIFICATE-----
|
||||
`
|
||||
|
||||
const validCert2 = `
|
||||
-----BEGIN CERTIFICATE-----
|
||||
MIIC/jCCAeagAwIBAgIBADANBgkqhkiG9w0BAQsFADAVMRMwEQYDVQQDEwprdWJl
|
||||
cm5ldGVzMB4XDTIyMTAxOTIzMTY0MFoXDTMyMTAxNjIzMTY0MFowFTETMBEGA1UE
|
||||
AxMKa3ViZXJuZXRlczCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAO+k
|
||||
zbj35jHIjCd5mxP1FHMwMtvLFPeKUjtaLDP9Bs2jZ97Igmr7NTysn9QZkRP68/XX
|
||||
j993Y8tOLg71N4vRggWiYP+T9Xfo0uHZJmzADKx5XkuC4Gqv79dUdb8IKfAbX9HB
|
||||
ffGmWRnZLLTu8Bv/vfyl0CfE64a57DK+CzNJDwdK46CYYUnEH6Wb9finYrMQ+PLG
|
||||
Oi2c0J4KAYc1WTId5npNwouzf/IMD33PvuXfE7r+/pDbP8u/X03e7U0cc9l7KRxr
|
||||
3gpRQemCG74yRuy1dd3lJ1YCD8q96xVVZimGebnJ0IHi+lORRa2ix/o3OzW3FaP+
|
||||
6kzHU6VnBRDr2rAhMh0CAwEAAaNZMFcwDgYDVR0PAQH/BAQDAgKkMA8GA1UdEwEB
|
||||
/wQFMAMBAf8wHQYDVR0OBBYEFGUVOLM74t1TVoZjifsLl3Rwt1A6MBUGA1UdEQQO
|
||||
MAyCCmt1YmVybmV0ZXMwDQYJKoZIhvcNAQELBQADggEBANHnPVDemZqRybYPN1as
|
||||
Ywxi3iT1I3Wma1rZyxTWeIq8Ik0gnyvbtCD1cFB/5QU1xPW09YnmIFM/E73RIeWT
|
||||
RmCNMgOGmegYxBQRe4UvmwWGJzKNA66c0MBmd2LDHrQlrvdewOCR667Sm9krsGt1
|
||||
tS/t6N/uBXeRSkXKEDXa+jOpYrV3Oq3IntG6zUeCrVbrH2Bs9Ma5fU00TwK3ylw5
|
||||
Ww8KzYdQaxxrLaiRRtFcpM9dFH/vwxl1QUa5vjHcmUjxmZunEmXKplATyLT0FXDw
|
||||
JAo8AuwuuwRh2o+o8SxwzzA+/EBrIREgcv5uIkD352QnfGkEvGu6JOPGZVyd/kVg
|
||||
KA0=
|
||||
-----END CERTIFICATE-----
|
||||
`
|
||||
|
||||
func newStorage(t *testing.T) (*REST, *etcd3testing.EtcdTestServer) {
|
||||
etcdStorage, server := registrytest.NewEtcdStorageForResource(t, certificates.SchemeGroupVersion.WithResource("clustertrustbundles").GroupResource())
|
||||
restOptions := generic.RESTOptions{
|
||||
StorageConfig: etcdStorage,
|
||||
Decorator: generic.UndecoratedStorage,
|
||||
DeleteCollectionWorkers: 1,
|
||||
ResourcePrefix: "clustertrustbundles",
|
||||
}
|
||||
storage, err := NewREST(restOptions)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error from REST storage: %v", err)
|
||||
}
|
||||
return storage, server
|
||||
}
|
||||
|
||||
func TestCreate(t *testing.T) {
|
||||
storage, server := newStorage(t)
|
||||
defer server.Terminate(t)
|
||||
defer storage.Store.DestroyFunc()
|
||||
|
||||
validBundle := &certificates.ClusterTrustBundle{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "ctb1",
|
||||
},
|
||||
Spec: certificates.ClusterTrustBundleSpec{
|
||||
TrustBundle: validCert1,
|
||||
},
|
||||
}
|
||||
|
||||
invalidBundle := &certificates.ClusterTrustBundle{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "ctb1",
|
||||
},
|
||||
Spec: certificates.ClusterTrustBundleSpec{
|
||||
// Empty TrustBundle is invalid.
|
||||
},
|
||||
}
|
||||
|
||||
test := genericregistrytest.New(t, storage.Store)
|
||||
test = test.ClusterScope()
|
||||
|
||||
test.TestCreate(validBundle, invalidBundle)
|
||||
}
|
||||
|
||||
func TestUpdate(t *testing.T) {
|
||||
storage, server := newStorage(t)
|
||||
defer server.Terminate(t)
|
||||
defer storage.Store.DestroyFunc()
|
||||
|
||||
test := genericregistrytest.New(t, storage.Store)
|
||||
test = test.ClusterScope()
|
||||
|
||||
test.TestUpdate(
|
||||
&certificates.ClusterTrustBundle{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "ctb1",
|
||||
},
|
||||
Spec: certificates.ClusterTrustBundleSpec{
|
||||
TrustBundle: validCert1,
|
||||
},
|
||||
},
|
||||
// Valid update
|
||||
func(object runtime.Object) runtime.Object {
|
||||
bundle := object.(*certificates.ClusterTrustBundle)
|
||||
bundle.Spec.TrustBundle = strings.Join([]string{validCert1, validCert2}, "\n")
|
||||
return bundle
|
||||
},
|
||||
// Invalid update
|
||||
func(object runtime.Object) runtime.Object {
|
||||
bundle := object.(*certificates.ClusterTrustBundle)
|
||||
bundle.Spec.TrustBundle = ""
|
||||
return bundle
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
func TestDelete(t *testing.T) {
|
||||
storage, server := newStorage(t)
|
||||
defer server.Terminate(t)
|
||||
defer storage.Store.DestroyFunc()
|
||||
|
||||
test := genericregistrytest.New(t, storage.Store)
|
||||
test = test.ClusterScope()
|
||||
|
||||
test.TestDelete(
|
||||
&certificates.ClusterTrustBundle{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "ctb1",
|
||||
},
|
||||
Spec: certificates.ClusterTrustBundleSpec{
|
||||
TrustBundle: validCert1,
|
||||
},
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
func TestGet(t *testing.T) {
|
||||
storage, server := newStorage(t)
|
||||
defer server.Terminate(t)
|
||||
defer storage.Store.DestroyFunc()
|
||||
|
||||
test := genericregistrytest.New(t, storage.Store)
|
||||
test = test.ClusterScope()
|
||||
|
||||
test.TestGet(
|
||||
&certificates.ClusterTrustBundle{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "ctb1",
|
||||
},
|
||||
Spec: certificates.ClusterTrustBundleSpec{
|
||||
TrustBundle: validCert1,
|
||||
},
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
func TestList(t *testing.T) {
|
||||
storage, server := newStorage(t)
|
||||
defer server.Terminate(t)
|
||||
defer storage.Store.DestroyFunc()
|
||||
|
||||
test := genericregistrytest.New(t, storage.Store)
|
||||
test = test.ClusterScope()
|
||||
|
||||
test.TestList(
|
||||
&certificates.ClusterTrustBundle{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "ctb1",
|
||||
},
|
||||
Spec: certificates.ClusterTrustBundleSpec{
|
||||
TrustBundle: validCert1,
|
||||
},
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
func TestWatch(t *testing.T) {
|
||||
storage, server := newStorage(t)
|
||||
defer server.Terminate(t)
|
||||
defer storage.Store.DestroyFunc()
|
||||
|
||||
test := genericregistrytest.New(t, storage.Store)
|
||||
test = test.ClusterScope()
|
||||
|
||||
test.TestWatch(
|
||||
&certificates.ClusterTrustBundle{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "ctb1",
|
||||
},
|
||||
Spec: certificates.ClusterTrustBundleSpec{
|
||||
SignerName: "k8s.io/foo",
|
||||
TrustBundle: validCert1,
|
||||
},
|
||||
},
|
||||
// matching labels
|
||||
[]labels.Set{},
|
||||
// not matching labels
|
||||
[]labels.Set{
|
||||
{"foo": "bar"},
|
||||
},
|
||||
// matching fields
|
||||
[]fields.Set{
|
||||
{"metadata.name": "ctb1"},
|
||||
},
|
||||
// not matching fields
|
||||
[]fields.Set{
|
||||
{"metadata.name": "bar"},
|
||||
},
|
||||
)
|
||||
}
|
81
pkg/registry/certificates/clustertrustbundle/strategy.go
Normal file
81
pkg/registry/certificates/clustertrustbundle/strategy.go
Normal file
@ -0,0 +1,81 @@
|
||||
/*
|
||||
Copyright 2022 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 clustertrustbundle provides Registry interface and its RESTStorage
|
||||
// implementation for storing ClusterTrustBundle objects.
|
||||
package clustertrustbundle // import "k8s.io/kubernetes/pkg/registry/certificates/clustertrustbundle"
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/apimachinery/pkg/util/validation/field"
|
||||
"k8s.io/apiserver/pkg/registry/rest"
|
||||
"k8s.io/apiserver/pkg/storage/names"
|
||||
"k8s.io/kubernetes/pkg/api/legacyscheme"
|
||||
"k8s.io/kubernetes/pkg/apis/certificates"
|
||||
certvalidation "k8s.io/kubernetes/pkg/apis/certificates/validation"
|
||||
)
|
||||
|
||||
// strategy implements behavior for ClusterTrustBundles.
|
||||
type strategy struct {
|
||||
runtime.ObjectTyper
|
||||
names.NameGenerator
|
||||
}
|
||||
|
||||
// Strategy is the create, update, and delete strategy for ClusterTrustBundles.
|
||||
var Strategy = strategy{legacyscheme.Scheme, names.SimpleNameGenerator}
|
||||
|
||||
var _ rest.RESTCreateStrategy = Strategy
|
||||
var _ rest.RESTUpdateStrategy = Strategy
|
||||
var _ rest.RESTDeleteStrategy = Strategy
|
||||
|
||||
func (strategy) NamespaceScoped() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func (strategy) PrepareForCreate(ctx context.Context, obj runtime.Object) {}
|
||||
|
||||
func (strategy) Validate(ctx context.Context, obj runtime.Object) field.ErrorList {
|
||||
bundle := obj.(*certificates.ClusterTrustBundle)
|
||||
return certvalidation.ValidateClusterTrustBundle(bundle, certvalidation.ValidateClusterTrustBundleOptions{})
|
||||
}
|
||||
|
||||
func (strategy) WarningsOnCreate(ctx context.Context, obj runtime.Object) []string {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (strategy) Canonicalize(obj runtime.Object) {}
|
||||
|
||||
func (strategy) AllowCreateOnUpdate() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func (s strategy) PrepareForUpdate(ctx context.Context, new, old runtime.Object) {}
|
||||
|
||||
func (s strategy) ValidateUpdate(ctx context.Context, new, old runtime.Object) field.ErrorList {
|
||||
newBundle := new.(*certificates.ClusterTrustBundle)
|
||||
oldBundle := old.(*certificates.ClusterTrustBundle)
|
||||
return certvalidation.ValidateClusterTrustBundleUpdate(newBundle, oldBundle)
|
||||
}
|
||||
|
||||
func (strategy) WarningsOnUpdate(ctx context.Context, obj, old runtime.Object) []string {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (strategy) AllowUnconditionalUpdate() bool {
|
||||
return false
|
||||
}
|
@ -0,0 +1,48 @@
|
||||
/*
|
||||
Copyright 2022 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 clustertrustbundle
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"k8s.io/kubernetes/pkg/apis/certificates"
|
||||
)
|
||||
|
||||
func TestWarningsOnCreate(t *testing.T) {
|
||||
if warnings := Strategy.WarningsOnCreate(context.Background(), &certificates.ClusterTrustBundle{}); warnings != nil {
|
||||
t.Errorf("Got %v, want nil", warnings)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAllowCreateOnUpdate(t *testing.T) {
|
||||
if Strategy.AllowCreateOnUpdate() != false {
|
||||
t.Errorf("Got true, want false")
|
||||
}
|
||||
}
|
||||
|
||||
func TestWarningsOnUpdate(t *testing.T) {
|
||||
if warnings := Strategy.WarningsOnUpdate(context.Background(), &certificates.ClusterTrustBundle{}, &certificates.ClusterTrustBundle{}); warnings != nil {
|
||||
t.Errorf("Got %v, want nil", warnings)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAllowUnconditionalUpdate(t *testing.T) {
|
||||
if Strategy.AllowUnconditionalUpdate() != false {
|
||||
t.Errorf("Got true, want false")
|
||||
}
|
||||
}
|
@ -18,13 +18,18 @@ package rest
|
||||
|
||||
import (
|
||||
certificatesapiv1 "k8s.io/api/certificates/v1"
|
||||
certificatesapiv1alpha1 "k8s.io/api/certificates/v1alpha1"
|
||||
"k8s.io/apiserver/pkg/registry/generic"
|
||||
"k8s.io/apiserver/pkg/registry/rest"
|
||||
genericapiserver "k8s.io/apiserver/pkg/server"
|
||||
serverstorage "k8s.io/apiserver/pkg/server/storage"
|
||||
utilfeature "k8s.io/apiserver/pkg/util/feature"
|
||||
"k8s.io/klog/v2"
|
||||
"k8s.io/kubernetes/pkg/api/legacyscheme"
|
||||
"k8s.io/kubernetes/pkg/apis/certificates"
|
||||
"k8s.io/kubernetes/pkg/features"
|
||||
certificatestore "k8s.io/kubernetes/pkg/registry/certificates/certificates/storage"
|
||||
clustertrustbundlestore "k8s.io/kubernetes/pkg/registry/certificates/clustertrustbundle/storage"
|
||||
)
|
||||
|
||||
type RESTStorageProvider struct{}
|
||||
@ -40,17 +45,22 @@ func (p RESTStorageProvider) NewRESTStorage(apiResourceConfigSource serverstorag
|
||||
apiGroupInfo.VersionedResourcesStorageMap[certificatesapiv1.SchemeGroupVersion.Version] = storageMap
|
||||
}
|
||||
|
||||
if storageMap, err := p.v1alpha1Storage(apiResourceConfigSource, restOptionsGetter); err != nil {
|
||||
return genericapiserver.APIGroupInfo{}, err
|
||||
} else if len(storageMap) > 0 {
|
||||
apiGroupInfo.VersionedResourcesStorageMap[certificatesapiv1alpha1.SchemeGroupVersion.Version] = storageMap
|
||||
}
|
||||
|
||||
return apiGroupInfo, nil
|
||||
}
|
||||
|
||||
func (p RESTStorageProvider) v1Storage(apiResourceConfigSource serverstorage.APIResourceConfigSource, restOptionsGetter generic.RESTOptionsGetter) (map[string]rest.Storage, error) {
|
||||
storage := map[string]rest.Storage{}
|
||||
|
||||
// certificatesigningrequests
|
||||
if resource := "certificatesigningrequests"; apiResourceConfigSource.ResourceEnabled(certificatesapiv1.SchemeGroupVersion.WithResource(resource)) {
|
||||
csrStorage, csrStatusStorage, csrApprovalStorage, err := certificatestore.NewREST(restOptionsGetter)
|
||||
if err != nil {
|
||||
return storage, err
|
||||
return nil, err
|
||||
}
|
||||
storage[resource] = csrStorage
|
||||
storage[resource+"/status"] = csrStatusStorage
|
||||
@ -60,6 +70,24 @@ func (p RESTStorageProvider) v1Storage(apiResourceConfigSource serverstorage.API
|
||||
return storage, nil
|
||||
}
|
||||
|
||||
func (p RESTStorageProvider) v1alpha1Storage(apiResourceConfigSource serverstorage.APIResourceConfigSource, restOptionsGetter generic.RESTOptionsGetter) (map[string]rest.Storage, error) {
|
||||
storage := map[string]rest.Storage{}
|
||||
|
||||
if resource := "clustertrustbundles"; apiResourceConfigSource.ResourceEnabled(certificatesapiv1alpha1.SchemeGroupVersion.WithResource(resource)) {
|
||||
if utilfeature.DefaultFeatureGate.Enabled(features.ClusterTrustBundle) {
|
||||
bundleStorage, err := clustertrustbundlestore.NewREST(restOptionsGetter)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
storage[resource] = bundleStorage
|
||||
} else {
|
||||
klog.Warning("ClusterTrustBundle storage is disabled because the ClusterTrustBundle feature gate is disabled")
|
||||
}
|
||||
}
|
||||
|
||||
return storage, nil
|
||||
}
|
||||
|
||||
func (p RESTStorageProvider) GroupName() string {
|
||||
return certificates.GroupName
|
||||
}
|
||||
|
@ -42,11 +42,11 @@ func NewEtcdStorageForResource(t *testing.T, resource schema.GroupResource) (*st
|
||||
completedConfig.APIResourceConfig = serverstorage.NewResourceConfig()
|
||||
factory, err := completedConfig.New()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
t.Fatalf("Error while making storage factory: %v", err)
|
||||
}
|
||||
resourceConfig, err := factory.NewConfig(resource)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
t.Fatalf("Error while finding storage destination: %v", err)
|
||||
}
|
||||
return resourceConfig, server
|
||||
}
|
||||
|
133
plugin/pkg/admission/certificates/ctbattest/admission.go
Normal file
133
plugin/pkg/admission/certificates/ctbattest/admission.go
Normal file
@ -0,0 +1,133 @@
|
||||
/*
|
||||
Copyright 2022 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 ctbattest
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
|
||||
"k8s.io/apiserver/pkg/admission"
|
||||
genericadmissioninit "k8s.io/apiserver/pkg/admission/initializer"
|
||||
"k8s.io/apiserver/pkg/authorization/authorizer"
|
||||
"k8s.io/component-base/featuregate"
|
||||
"k8s.io/klog/v2"
|
||||
api "k8s.io/kubernetes/pkg/apis/certificates"
|
||||
"k8s.io/kubernetes/pkg/features"
|
||||
"k8s.io/kubernetes/plugin/pkg/admission/certificates"
|
||||
)
|
||||
|
||||
const PluginName = "ClusterTrustBundleAttest"
|
||||
|
||||
func Register(plugins *admission.Plugins) {
|
||||
plugins.Register(PluginName, func(config io.Reader) (admission.Interface, error) {
|
||||
return NewPlugin(), nil
|
||||
})
|
||||
}
|
||||
|
||||
// Plugin is the ClusterTrustBundle attest plugin.
|
||||
//
|
||||
// In order to create or update a ClusterTrustBundle that sets signerName,
|
||||
// you must have the following permission: group=certificates.k8s.io
|
||||
// resource=signers resourceName=<the signer name> verb=attest.
|
||||
type Plugin struct {
|
||||
*admission.Handler
|
||||
authz authorizer.Authorizer
|
||||
|
||||
inspectedFeatureGates bool
|
||||
enabled bool
|
||||
}
|
||||
|
||||
var _ admission.ValidationInterface = &Plugin{}
|
||||
var _ admission.InitializationValidator = &Plugin{}
|
||||
var _ genericadmissioninit.WantsAuthorizer = &Plugin{}
|
||||
var _ genericadmissioninit.WantsFeatures = &Plugin{}
|
||||
|
||||
func NewPlugin() *Plugin {
|
||||
return &Plugin{
|
||||
Handler: admission.NewHandler(admission.Create, admission.Update),
|
||||
}
|
||||
}
|
||||
|
||||
// SetAuthorizer sets the plugin's authorizer.
|
||||
func (p *Plugin) SetAuthorizer(authz authorizer.Authorizer) {
|
||||
p.authz = authz
|
||||
}
|
||||
|
||||
// InspectFeatureGates implements WantsFeatures.
|
||||
func (p *Plugin) InspectFeatureGates(featureGates featuregate.FeatureGate) {
|
||||
p.enabled = featureGates.Enabled(features.ClusterTrustBundle)
|
||||
p.inspectedFeatureGates = true
|
||||
}
|
||||
|
||||
// ValidateInitialization checks that the plugin was initialized correctly.
|
||||
func (p *Plugin) ValidateInitialization() error {
|
||||
if p.authz == nil {
|
||||
return fmt.Errorf("%s requires an authorizer", PluginName)
|
||||
}
|
||||
if !p.inspectedFeatureGates {
|
||||
return fmt.Errorf("%s did not see feature gates", PluginName)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
var clusterTrustBundleGroupResource = api.Resource("clustertrustbundles")
|
||||
|
||||
func (p *Plugin) Validate(ctx context.Context, a admission.Attributes, _ admission.ObjectInterfaces) error {
|
||||
if !p.enabled {
|
||||
return nil
|
||||
}
|
||||
if a.GetResource().GroupResource() != clusterTrustBundleGroupResource {
|
||||
return nil
|
||||
}
|
||||
|
||||
newBundle, ok := a.GetObject().(*api.ClusterTrustBundle)
|
||||
if !ok {
|
||||
return admission.NewForbidden(a, fmt.Errorf("expected type ClusterTrustBundle, got: %T", a.GetOldObject()))
|
||||
}
|
||||
|
||||
// Unlike CSRs, it's OK to validate against the *new* object, because
|
||||
// updates to signer name will be rejected during validation. For defense
|
||||
// in depth, reject attempts to change signer at this layer as well.
|
||||
//
|
||||
// We want to use the new object because we also need to perform the signer
|
||||
// name permission check on *create*.
|
||||
|
||||
if a.GetOperation() == admission.Update {
|
||||
oldBundle, ok := a.GetOldObject().(*api.ClusterTrustBundle)
|
||||
if !ok {
|
||||
return admission.NewForbidden(a, fmt.Errorf("expected type ClusterTrustBundle, got: %T", a.GetOldObject()))
|
||||
}
|
||||
|
||||
if oldBundle.Spec.SignerName != newBundle.Spec.SignerName {
|
||||
return admission.NewForbidden(a, fmt.Errorf("changing signerName is forbidden"))
|
||||
}
|
||||
}
|
||||
|
||||
// If signer name isn't specified, we don't need to perform the
|
||||
// attest check.
|
||||
if newBundle.Spec.SignerName == "" {
|
||||
return nil
|
||||
}
|
||||
|
||||
if !certificates.IsAuthorizedForSignerName(ctx, p.authz, a.GetUserInfo(), "attest", newBundle.Spec.SignerName) {
|
||||
klog.V(4).Infof("user not permitted to attest ClusterTrustBundle %q with signerName %q", newBundle.Name, newBundle.Spec.SignerName)
|
||||
return admission.NewForbidden(a, fmt.Errorf("user not permitted to attest for signerName %q", newBundle.Spec.SignerName))
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
306
plugin/pkg/admission/certificates/ctbattest/admission_test.go
Normal file
306
plugin/pkg/admission/certificates/ctbattest/admission_test.go
Normal file
@ -0,0 +1,306 @@
|
||||
/*
|
||||
Copyright 2022 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 ctbattest
|
||||
|
||||
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"
|
||||
"k8s.io/apiserver/pkg/util/feature"
|
||||
featuregatetesting "k8s.io/component-base/featuregate/testing"
|
||||
certificatesapi "k8s.io/kubernetes/pkg/apis/certificates"
|
||||
"k8s.io/kubernetes/pkg/features"
|
||||
)
|
||||
|
||||
func TestPluginValidate(t *testing.T) {
|
||||
tests := []struct {
|
||||
description string
|
||||
clusterTrustBundleFeatureEnabled bool
|
||||
attributes admission.Attributes
|
||||
allowedName string
|
||||
allowed bool
|
||||
authzErr error
|
||||
}{
|
||||
{
|
||||
description: "wrong type on create",
|
||||
clusterTrustBundleFeatureEnabled: true,
|
||||
attributes: &testAttributes{
|
||||
resource: certificatesapi.Resource("clustertrustbundles"),
|
||||
obj: &certificatesapi.ClusterTrustBundleList{},
|
||||
operation: admission.Create,
|
||||
},
|
||||
allowed: false,
|
||||
},
|
||||
{
|
||||
description: "wrong type on update",
|
||||
clusterTrustBundleFeatureEnabled: true,
|
||||
attributes: &testAttributes{
|
||||
resource: certificatesapi.Resource("clustertrustbundles"),
|
||||
obj: &certificatesapi.ClusterTrustBundleList{},
|
||||
operation: admission.Update,
|
||||
},
|
||||
allowed: false,
|
||||
},
|
||||
{
|
||||
description: "reject requests if looking up permissions fails",
|
||||
clusterTrustBundleFeatureEnabled: true,
|
||||
attributes: &testAttributes{
|
||||
resource: certificatesapi.Resource("clustertrustbundles"),
|
||||
obj: &certificatesapi.ClusterTrustBundle{
|
||||
Spec: certificatesapi.ClusterTrustBundleSpec{
|
||||
SignerName: "abc.com/xyz",
|
||||
},
|
||||
},
|
||||
operation: admission.Update,
|
||||
},
|
||||
authzErr: errors.New("forced error"),
|
||||
allowed: false,
|
||||
},
|
||||
{
|
||||
description: "should allow create if no signer name is specified",
|
||||
clusterTrustBundleFeatureEnabled: true,
|
||||
allowedName: "abc.com/xyz",
|
||||
attributes: &testAttributes{
|
||||
resource: certificatesapi.Resource("clustertrustbundles"),
|
||||
obj: &certificatesapi.ClusterTrustBundle{
|
||||
Spec: certificatesapi.ClusterTrustBundleSpec{},
|
||||
},
|
||||
operation: admission.Create,
|
||||
},
|
||||
allowed: true,
|
||||
},
|
||||
{
|
||||
description: "should allow update if no signer name is specified",
|
||||
clusterTrustBundleFeatureEnabled: true,
|
||||
allowedName: "abc.com/xyz",
|
||||
attributes: &testAttributes{
|
||||
resource: certificatesapi.Resource("clustertrustbundles"),
|
||||
oldObj: &certificatesapi.ClusterTrustBundle{
|
||||
Spec: certificatesapi.ClusterTrustBundleSpec{},
|
||||
},
|
||||
obj: &certificatesapi.ClusterTrustBundle{
|
||||
Spec: certificatesapi.ClusterTrustBundleSpec{},
|
||||
},
|
||||
operation: admission.Update,
|
||||
},
|
||||
allowed: true,
|
||||
},
|
||||
{
|
||||
description: "should allow create if user is authorized for specific signerName",
|
||||
clusterTrustBundleFeatureEnabled: true,
|
||||
allowedName: "abc.com/xyz",
|
||||
attributes: &testAttributes{
|
||||
resource: certificatesapi.Resource("clustertrustbundles"),
|
||||
obj: &certificatesapi.ClusterTrustBundle{
|
||||
Spec: certificatesapi.ClusterTrustBundleSpec{
|
||||
SignerName: "abc.com/xyz",
|
||||
},
|
||||
},
|
||||
operation: admission.Create,
|
||||
},
|
||||
allowed: true,
|
||||
},
|
||||
{
|
||||
description: "should allow update if user is authorized for specific signerName",
|
||||
clusterTrustBundleFeatureEnabled: true,
|
||||
allowedName: "abc.com/xyz",
|
||||
attributes: &testAttributes{
|
||||
resource: certificatesapi.Resource("clustertrustbundles"),
|
||||
oldObj: &certificatesapi.ClusterTrustBundle{
|
||||
Spec: certificatesapi.ClusterTrustBundleSpec{
|
||||
SignerName: "abc.com/xyz",
|
||||
},
|
||||
},
|
||||
obj: &certificatesapi.ClusterTrustBundle{
|
||||
Spec: certificatesapi.ClusterTrustBundleSpec{
|
||||
SignerName: "abc.com/xyz",
|
||||
},
|
||||
},
|
||||
operation: admission.Update,
|
||||
},
|
||||
allowed: true,
|
||||
},
|
||||
{
|
||||
description: "should allow create if user is authorized with wildcard",
|
||||
clusterTrustBundleFeatureEnabled: true,
|
||||
allowedName: "abc.com/*",
|
||||
attributes: &testAttributes{
|
||||
resource: certificatesapi.Resource("clustertrustbundles"),
|
||||
obj: &certificatesapi.ClusterTrustBundle{
|
||||
Spec: certificatesapi.ClusterTrustBundleSpec{
|
||||
SignerName: "abc.com/xyz",
|
||||
},
|
||||
},
|
||||
operation: admission.Create,
|
||||
},
|
||||
allowed: true,
|
||||
},
|
||||
{
|
||||
description: "should allow update if user is authorized with wildcard",
|
||||
clusterTrustBundleFeatureEnabled: true,
|
||||
allowedName: "abc.com/*",
|
||||
attributes: &testAttributes{
|
||||
resource: certificatesapi.Resource("clustertrustbundles"),
|
||||
oldObj: &certificatesapi.ClusterTrustBundle{
|
||||
Spec: certificatesapi.ClusterTrustBundleSpec{
|
||||
SignerName: "abc.com/xyz",
|
||||
},
|
||||
},
|
||||
obj: &certificatesapi.ClusterTrustBundle{
|
||||
Spec: certificatesapi.ClusterTrustBundleSpec{
|
||||
SignerName: "abc.com/xyz",
|
||||
},
|
||||
},
|
||||
operation: admission.Update,
|
||||
},
|
||||
allowed: true,
|
||||
},
|
||||
{
|
||||
description: "should deny create if user does not have permission for this signerName",
|
||||
clusterTrustBundleFeatureEnabled: true,
|
||||
allowedName: "notabc.com/xyz",
|
||||
attributes: &testAttributes{
|
||||
resource: certificatesapi.Resource("clustertrustbundles"),
|
||||
obj: &certificatesapi.ClusterTrustBundle{
|
||||
Spec: certificatesapi.ClusterTrustBundleSpec{
|
||||
SignerName: "abc.com/xyz",
|
||||
},
|
||||
},
|
||||
operation: admission.Create,
|
||||
},
|
||||
allowed: false,
|
||||
},
|
||||
{
|
||||
description: "should deny update if user does not have permission for this signerName",
|
||||
clusterTrustBundleFeatureEnabled: true,
|
||||
allowedName: "notabc.com/xyz",
|
||||
attributes: &testAttributes{
|
||||
resource: certificatesapi.Resource("clustertrustbundles"),
|
||||
obj: &certificatesapi.ClusterTrustBundle{
|
||||
Spec: certificatesapi.ClusterTrustBundleSpec{
|
||||
SignerName: "abc.com/xyz",
|
||||
},
|
||||
},
|
||||
operation: admission.Update,
|
||||
},
|
||||
allowed: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.description, func(t *testing.T) {
|
||||
p := Plugin{
|
||||
authz: fakeAuthorizer{
|
||||
t: t,
|
||||
verb: "attest",
|
||||
allowedName: tc.allowedName,
|
||||
decision: authorizer.DecisionAllow,
|
||||
err: tc.authzErr,
|
||||
},
|
||||
}
|
||||
|
||||
defer featuregatetesting.SetFeatureGateDuringTest(t, feature.DefaultFeatureGate, features.ClusterTrustBundle, tc.clusterTrustBundleFeatureEnabled)()
|
||||
p.InspectFeatureGates(feature.DefaultFeatureGate)
|
||||
|
||||
err := p.Validate(context.Background(), tc.attributes, nil)
|
||||
if err == nil && !tc.allowed {
|
||||
t.Errorf("Expected authorization policy to reject ClusterTrustBundle but it was allowed")
|
||||
}
|
||||
if err != nil && tc.allowed {
|
||||
t.Errorf("Expected authorization policy to accept ClusterTrustBundle 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"}
|
||||
}
|
@ -180,6 +180,10 @@ func NodeRules() []rbacv1.PolicyRule {
|
||||
if utilfeature.DefaultFeatureGate.Enabled(features.DynamicResourceAllocation) {
|
||||
nodePolicyRules = append(nodePolicyRules, rbacv1helpers.NewRule("get").Groups(resourceGroup).Resources("resourceclaims").RuleOrDie())
|
||||
}
|
||||
// Kubelet needs access to ClusterTrustBundles to support the pemTrustAnchors volume type.
|
||||
if utilfeature.DefaultFeatureGate.Enabled(features.ClusterTrustBundle) {
|
||||
nodePolicyRules = append(nodePolicyRules, rbacv1helpers.NewRule("get", "list", "watch").Groups(certificatesGroup).Resources("clustertrustbundles").RuleOrDie())
|
||||
}
|
||||
|
||||
return nodePolicyRules
|
||||
}
|
||||
@ -585,6 +589,16 @@ func ClusterRoles() []rbacv1.ClusterRole {
|
||||
Rules: kubeSchedulerRules,
|
||||
})
|
||||
|
||||
// Default ClusterRole to allow reading ClusterTrustBundle objects
|
||||
if utilfeature.DefaultFeatureGate.Enabled(features.ClusterTrustBundle) {
|
||||
roles = append(roles, rbacv1.ClusterRole{
|
||||
ObjectMeta: metav1.ObjectMeta{Name: "system:cluster-trust-bundle-discovery"},
|
||||
Rules: []rbacv1.PolicyRule{
|
||||
rbacv1helpers.NewRule(Read...).Groups(certificatesGroup).Resources("clustertrustbundles").RuleOrDie(),
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
addClusterRoleLabel(roles)
|
||||
return roles
|
||||
}
|
||||
@ -625,6 +639,11 @@ func ClusterRoleBindings() []rbacv1.ClusterRoleBinding {
|
||||
rbacv1helpers.NewClusterBinding("system:service-account-issuer-discovery").Groups(serviceaccount.AllServiceAccountsGroup).BindingOrDie(),
|
||||
)
|
||||
|
||||
// Service accounts can read ClusterTrustBundle objects.
|
||||
if utilfeature.DefaultFeatureGate.Enabled(features.ClusterTrustBundle) {
|
||||
rolebindings = append(rolebindings, rbacv1helpers.NewClusterBinding("system:cluster-trust-bundle-discovery").Groups(serviceaccount.AllServiceAccountsGroup).BindingOrDie())
|
||||
}
|
||||
|
||||
addClusterRoleBindingLabel(rolebindings)
|
||||
|
||||
return rolebindings
|
||||
|
24
staging/src/k8s.io/api/certificates/v1alpha1/doc.go
Normal file
24
staging/src/k8s.io/api/certificates/v1alpha1/doc.go
Normal file
@ -0,0 +1,24 @@
|
||||
/*
|
||||
Copyright 2022 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.
|
||||
*/
|
||||
|
||||
// +k8s:deepcopy-gen=package
|
||||
// +k8s:protobuf-gen=package
|
||||
// +k8s:openapi-gen=true
|
||||
// +k8s:prerelease-lifecycle-gen=true
|
||||
|
||||
// +groupName=certificates.k8s.io
|
||||
|
||||
package v1alpha1 // import "k8s.io/api/certificates/v1alpha1"
|
61
staging/src/k8s.io/api/certificates/v1alpha1/register.go
Normal file
61
staging/src/k8s.io/api/certificates/v1alpha1/register.go
Normal file
@ -0,0 +1,61 @@
|
||||
/*
|
||||
Copyright 2022 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 v1alpha1
|
||||
|
||||
import (
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
)
|
||||
|
||||
// GroupName is the group name use in this package
|
||||
const GroupName = "certificates.k8s.io"
|
||||
|
||||
// SchemeGroupVersion is group version used to register these objects
|
||||
var SchemeGroupVersion = schema.GroupVersion{Group: GroupName, Version: "v1alpha1"}
|
||||
|
||||
// Kind takes an unqualified kind and returns a Group qualified GroupKind
|
||||
func Kind(kind string) schema.GroupKind {
|
||||
return SchemeGroupVersion.WithKind(kind).GroupKind()
|
||||
}
|
||||
|
||||
// Resource takes an unqualified resource and returns a Group qualified GroupResource
|
||||
func Resource(resource string) schema.GroupResource {
|
||||
return SchemeGroupVersion.WithResource(resource).GroupResource()
|
||||
}
|
||||
|
||||
var (
|
||||
// SchemeBuilder is the scheme builder with scheme init functions to run for this API package
|
||||
SchemeBuilder = runtime.NewSchemeBuilder(addKnownTypes)
|
||||
|
||||
localSchemeBuilder = &SchemeBuilder
|
||||
|
||||
// AddToScheme is a global function that registers this API group & version to a scheme
|
||||
AddToScheme = localSchemeBuilder.AddToScheme
|
||||
)
|
||||
|
||||
// Adds the list of known types to the given scheme.
|
||||
func addKnownTypes(scheme *runtime.Scheme) error {
|
||||
scheme.AddKnownTypes(SchemeGroupVersion,
|
||||
&ClusterTrustBundle{},
|
||||
&ClusterTrustBundleList{},
|
||||
)
|
||||
|
||||
// Add the watch version that applies
|
||||
metav1.AddToGroupVersion(scheme, SchemeGroupVersion)
|
||||
return nil
|
||||
}
|
106
staging/src/k8s.io/api/certificates/v1alpha1/types.go
Normal file
106
staging/src/k8s.io/api/certificates/v1alpha1/types.go
Normal file
@ -0,0 +1,106 @@
|
||||
/*
|
||||
Copyright 2023 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 v1alpha1
|
||||
|
||||
import (
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
)
|
||||
|
||||
// +genclient
|
||||
// +genclient:nonNamespaced
|
||||
// +k8s:prerelease-lifecycle-gen:introduced=1.26
|
||||
// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object
|
||||
|
||||
// ClusterTrustBundle is a cluster-scoped container for X.509 trust anchors
|
||||
// (root certificates).
|
||||
//
|
||||
// ClusterTrustBundle objects are considered to be readable by any authenticated
|
||||
// user in the cluster, because they can be mounted by pods using the
|
||||
// `clusterTrustBundle` projection. All service accounts have read access to
|
||||
// ClusterTrustBundles by default. Users who only have namespace-level access
|
||||
// to a cluster can read ClusterTrustBundles by impersonating a serviceaccount
|
||||
// that they have access to.
|
||||
//
|
||||
// It can be optionally associated with a particular assigner, in which case it
|
||||
// contains one valid set of trust anchors for that signer. Signers may have
|
||||
// multiple associated ClusterTrustBundles; each is an independent set of trust
|
||||
// anchors for that signer. Admission control is used to enforce that only users
|
||||
// with permissions on the signer can create or modify the corresponding bundle.
|
||||
type ClusterTrustBundle struct {
|
||||
metav1.TypeMeta `json:",inline"`
|
||||
|
||||
// metadata contains the object metadata.
|
||||
// +optional
|
||||
metav1.ObjectMeta `json:"metadata,omitempty" protobuf:"bytes,1,opt,name=metadata"`
|
||||
|
||||
// spec contains the signer (if any) and trust anchors.
|
||||
Spec ClusterTrustBundleSpec `json:"spec" protobuf:"bytes,2,opt,name=spec"`
|
||||
}
|
||||
|
||||
// ClusterTrustBundleSpec contains the signer and trust anchors.
|
||||
type ClusterTrustBundleSpec struct {
|
||||
// signerName indicates the associated signer, if any.
|
||||
//
|
||||
// In order to create or update a ClusterTrustBundle that sets signerName,
|
||||
// you must have the following cluster-scoped permission:
|
||||
// group=certificates.k8s.io resource=signers resourceName=<the signer name>
|
||||
// verb=attest.
|
||||
//
|
||||
// If signerName is not empty, then the ClusterTrustBundle object must be
|
||||
// named with the signer name as a prefix (translating slashes to colons).
|
||||
// For example, for the signer name `example.com/foo`, valid
|
||||
// ClusterTrustBundle object names include `example.com:foo:abc` and
|
||||
// `example.com:foo:v1`.
|
||||
//
|
||||
// If signerName is empty, then the ClusterTrustBundle object's name must
|
||||
// not have such a prefix.
|
||||
//
|
||||
// List/watch requests for ClusterTrustBundles can filter on this field
|
||||
// using a `spec.signerName=NAME` field selector.
|
||||
//
|
||||
// +optional
|
||||
SignerName string `json:"signerName,omitempty" protobuf:"bytes,1,opt,name=signerName"`
|
||||
|
||||
// trustBundle contains the individual X.509 trust anchors for this
|
||||
// bundle, as PEM bundle of PEM-wrapped, DER-formatted X.509 certificates.
|
||||
//
|
||||
// The data must consist only of PEM certificate blocks that parse as valid
|
||||
// X.509 certificates. Each certificate must include a basic constraints
|
||||
// extension with the CA bit set. The API server will reject objects that
|
||||
// contain duplicate certificates, or that use PEM block headers.
|
||||
//
|
||||
// Users of ClusterTrustBundles, including Kubelet, are free to reorder and
|
||||
// deduplicate certificate blocks in this file according to their own logic,
|
||||
// as well as to drop PEM block headers and inter-block data.
|
||||
TrustBundle string `json:"trustBundle" protobuf:"bytes,2,opt,name=trustBundle"`
|
||||
}
|
||||
|
||||
// +k8s:prerelease-lifecycle-gen:introduced=1.26
|
||||
// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object
|
||||
|
||||
// ClusterTrustBundleList is a collection of ClusterTrustBundle objects
|
||||
type ClusterTrustBundleList struct {
|
||||
metav1.TypeMeta `json:",inline"`
|
||||
|
||||
// metadata contains the list metadata.
|
||||
//
|
||||
// +optional
|
||||
metav1.ListMeta `json:"metadata,omitempty" protobuf:"bytes,1,opt,name=metadata"`
|
||||
|
||||
// items is a collection of ClusterTrustBundle objects
|
||||
Items []ClusterTrustBundle `json:"items" protobuf:"bytes,2,rep,name=items"`
|
||||
}
|
@ -41,6 +41,7 @@ import (
|
||||
batchv1 "k8s.io/api/batch/v1"
|
||||
batchv1beta1 "k8s.io/api/batch/v1beta1"
|
||||
certificatesv1 "k8s.io/api/certificates/v1"
|
||||
certificatesv1alpha1 "k8s.io/api/certificates/v1alpha1"
|
||||
certificatesv1beta1 "k8s.io/api/certificates/v1beta1"
|
||||
coordinationv1 "k8s.io/api/coordination/v1"
|
||||
coordinationv1beta1 "k8s.io/api/coordination/v1beta1"
|
||||
@ -105,6 +106,7 @@ var groups = []runtime.SchemeBuilder{
|
||||
batchv1.SchemeBuilder,
|
||||
certificatesv1.SchemeBuilder,
|
||||
certificatesv1beta1.SchemeBuilder,
|
||||
certificatesv1alpha1.SchemeBuilder,
|
||||
coordinationv1.SchemeBuilder,
|
||||
coordinationv1beta1.SchemeBuilder,
|
||||
corev1.SchemeBuilder,
|
||||
|
6
vendor/modules.txt
vendored
6
vendor/modules.txt
vendored
@ -1234,6 +1234,7 @@ k8s.io/api/autoscaling/v2beta2
|
||||
k8s.io/api/batch/v1
|
||||
k8s.io/api/batch/v1beta1
|
||||
k8s.io/api/certificates/v1
|
||||
k8s.io/api/certificates/v1alpha1
|
||||
k8s.io/api/certificates/v1beta1
|
||||
k8s.io/api/coordination/v1
|
||||
k8s.io/api/coordination/v1beta1
|
||||
@ -1576,6 +1577,7 @@ k8s.io/client-go/applyconfigurations/autoscaling/v2beta2
|
||||
k8s.io/client-go/applyconfigurations/batch/v1
|
||||
k8s.io/client-go/applyconfigurations/batch/v1beta1
|
||||
k8s.io/client-go/applyconfigurations/certificates/v1
|
||||
k8s.io/client-go/applyconfigurations/certificates/v1alpha1
|
||||
k8s.io/client-go/applyconfigurations/certificates/v1beta1
|
||||
k8s.io/client-go/applyconfigurations/coordination/v1
|
||||
k8s.io/client-go/applyconfigurations/coordination/v1beta1
|
||||
@ -1640,6 +1642,7 @@ k8s.io/client-go/informers/batch/v1
|
||||
k8s.io/client-go/informers/batch/v1beta1
|
||||
k8s.io/client-go/informers/certificates
|
||||
k8s.io/client-go/informers/certificates/v1
|
||||
k8s.io/client-go/informers/certificates/v1alpha1
|
||||
k8s.io/client-go/informers/certificates/v1beta1
|
||||
k8s.io/client-go/informers/coordination
|
||||
k8s.io/client-go/informers/coordination/v1
|
||||
@ -1726,6 +1729,8 @@ k8s.io/client-go/kubernetes/typed/batch/v1beta1
|
||||
k8s.io/client-go/kubernetes/typed/batch/v1beta1/fake
|
||||
k8s.io/client-go/kubernetes/typed/certificates/v1
|
||||
k8s.io/client-go/kubernetes/typed/certificates/v1/fake
|
||||
k8s.io/client-go/kubernetes/typed/certificates/v1alpha1
|
||||
k8s.io/client-go/kubernetes/typed/certificates/v1alpha1/fake
|
||||
k8s.io/client-go/kubernetes/typed/certificates/v1beta1
|
||||
k8s.io/client-go/kubernetes/typed/certificates/v1beta1/fake
|
||||
k8s.io/client-go/kubernetes/typed/coordination/v1
|
||||
@ -1802,6 +1807,7 @@ k8s.io/client-go/listers/autoscaling/v2beta2
|
||||
k8s.io/client-go/listers/batch/v1
|
||||
k8s.io/client-go/listers/batch/v1beta1
|
||||
k8s.io/client-go/listers/certificates/v1
|
||||
k8s.io/client-go/listers/certificates/v1alpha1
|
||||
k8s.io/client-go/listers/certificates/v1beta1
|
||||
k8s.io/client-go/listers/coordination/v1
|
||||
k8s.io/client-go/listers/coordination/v1beta1
|
||||
|
Loading…
Reference in New Issue
Block a user