mirror of
https://github.com/k3s-io/kubernetes.git
synced 2025-07-21 10:51:29 +00:00
Merge pull request #88246 from munnerz/csr-signername-controllers
Update CSR controllers & kubelet to respect signerName field
This commit is contained in:
commit
03b7f272c8
4
api/openapi-spec/swagger.json
generated
4
api/openapi-spec/swagger.json
generated
@ -4605,6 +4605,10 @@
|
||||
"format": "byte",
|
||||
"type": "string"
|
||||
},
|
||||
"signerName": {
|
||||
"description": "Requested signer for the request. It is a qualified name in the form: `scope-hostname.io/name`. If empty, it will be defaulted:\n 1. If it's a kubelet client certificate, it is assigned\n \"kubernetes.io/kube-apiserver-client-kubelet\".\n 2. If it's a kubelet serving certificate, it is assigned\n \"kubernetes.io/kubelet-serving\".\n 3. Otherwise, it is assigned \"kubernetes.io/legacy-unknown\".\nDistribution of trust for signers happens out of band. You can select on this field using `spec.signerName`.",
|
||||
"type": "string"
|
||||
},
|
||||
"uid": {
|
||||
"description": "UID information about the requesting user. See user.Info interface for details.",
|
||||
"type": "string"
|
||||
|
@ -300,7 +300,7 @@ func (s *csrSimulator) ServeHTTP(w http.ResponseWriter, req *http.Request) {
|
||||
PrivateKey: s.serverPrivateKey,
|
||||
Backdate: s.backdate,
|
||||
}
|
||||
cr, err := capihelper.ParseCSR(csr)
|
||||
cr, err := capihelper.ParseCSR(csr.Spec.Request)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
@ -29,6 +29,7 @@ var Funcs = func(codecs runtimeserializer.CodecFactory) []interface{} {
|
||||
func(obj *certificates.CertificateSigningRequestSpec, c fuzz.Continue) {
|
||||
c.FuzzNoCustom(obj) // fuzz self without calling this function again
|
||||
obj.Usages = []certificates.KeyUsage{certificates.UsageKeyEncipherment}
|
||||
obj.SignerName = "example.com/custom-sample-signer"
|
||||
},
|
||||
}
|
||||
}
|
||||
|
@ -42,6 +42,12 @@ type CertificateSigningRequestSpec struct {
|
||||
// Base64-encoded PKCS#10 CSR data
|
||||
Request []byte
|
||||
|
||||
// Requested signer for the request. It is a qualified name in the form:
|
||||
// `scope-hostname.io/name`.
|
||||
// Distribution of trust for signers happens out of band.
|
||||
// You can select on this field using `spec.signerName`.
|
||||
SignerName string
|
||||
|
||||
// usages specifies a set of usage contexts the key will be
|
||||
// valid for.
|
||||
// See: https://tools.ietf.org/html/rfc5280#section-4.2.1.3
|
||||
|
@ -3,11 +3,13 @@ package(default_visibility = ["//visibility:public"])
|
||||
load(
|
||||
"@io_bazel_rules_go//go:def.bzl",
|
||||
"go_library",
|
||||
"go_test",
|
||||
)
|
||||
|
||||
go_library(
|
||||
name = "go_default_library",
|
||||
srcs = [
|
||||
"conversion.go",
|
||||
"defaults.go",
|
||||
"doc.go",
|
||||
"helpers.go",
|
||||
@ -19,9 +21,11 @@ go_library(
|
||||
deps = [
|
||||
"//pkg/apis/certificates:go_default_library",
|
||||
"//staging/src/k8s.io/api/certificates/v1beta1:go_default_library",
|
||||
"//staging/src/k8s.io/apimachinery/pkg/apis/meta/v1:go_default_library",
|
||||
"//staging/src/k8s.io/apimachinery/pkg/conversion:go_default_library",
|
||||
"//staging/src/k8s.io/apimachinery/pkg/runtime:go_default_library",
|
||||
"//staging/src/k8s.io/apimachinery/pkg/runtime/schema:go_default_library",
|
||||
"//staging/src/k8s.io/apimachinery/pkg/util/sets:go_default_library",
|
||||
],
|
||||
)
|
||||
|
||||
@ -37,3 +41,10 @@ filegroup(
|
||||
srcs = [":package-srcs"],
|
||||
tags = ["automanaged"],
|
||||
)
|
||||
|
||||
go_test(
|
||||
name = "go_default_test",
|
||||
srcs = ["defaults_test.go"],
|
||||
embed = [":go_default_library"],
|
||||
deps = ["//staging/src/k8s.io/api/certificates/v1beta1:go_default_library"],
|
||||
)
|
||||
|
38
pkg/apis/certificates/v1beta1/conversion.go
Normal file
38
pkg/apis/certificates/v1beta1/conversion.go
Normal file
@ -0,0 +1,38 @@
|
||||
/*
|
||||
Copyright 2020 The Kubernetes Authors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package v1beta1
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
)
|
||||
|
||||
func addConversionFuncs(scheme *runtime.Scheme) error {
|
||||
// Add field conversion funcs.
|
||||
return scheme.AddFieldLabelConversionFunc(SchemeGroupVersion.WithKind("CertificateSigningRequest"),
|
||||
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)
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
@ -17,15 +17,113 @@ limitations under the License.
|
||||
package v1beta1
|
||||
|
||||
import (
|
||||
"crypto/x509"
|
||||
"reflect"
|
||||
"strings"
|
||||
|
||||
certificatesv1beta1 "k8s.io/api/certificates/v1beta1"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/apimachinery/pkg/util/sets"
|
||||
)
|
||||
|
||||
func addDefaultingFuncs(scheme *runtime.Scheme) error {
|
||||
return RegisterDefaults(scheme)
|
||||
}
|
||||
|
||||
func SetDefaults_CertificateSigningRequestSpec(obj *certificatesv1beta1.CertificateSigningRequestSpec) {
|
||||
if obj.Usages == nil {
|
||||
obj.Usages = []certificatesv1beta1.KeyUsage{certificatesv1beta1.UsageDigitalSignature, certificatesv1beta1.UsageKeyEncipherment}
|
||||
}
|
||||
|
||||
if obj.SignerName == nil {
|
||||
signerName := DefaultSignerNameFromSpec(obj)
|
||||
obj.SignerName = &signerName
|
||||
}
|
||||
}
|
||||
|
||||
// DefaultSignerNameFromSpec will determine the signerName that should be set
|
||||
// by attempting to inspect the 'request' content and the spec options.
|
||||
func DefaultSignerNameFromSpec(obj *certificatesv1beta1.CertificateSigningRequestSpec) string {
|
||||
csr, err := ParseCSR(obj.Request)
|
||||
switch {
|
||||
case err != nil:
|
||||
// Set the signerName to 'legacy-unknown' as the CSR could not be
|
||||
// recognised.
|
||||
return certificatesv1beta1.LegacyUnknownSignerName
|
||||
case IsKubeletClientCSR(csr, obj.Usages):
|
||||
return certificatesv1beta1.KubeAPIServerClientKubeletSignerName
|
||||
case IsKubeletServingCSR(csr, obj.Usages):
|
||||
return certificatesv1beta1.KubeletServingSignerName
|
||||
default:
|
||||
return certificatesv1beta1.LegacyUnknownSignerName
|
||||
}
|
||||
}
|
||||
|
||||
func IsKubeletServingCSR(req *x509.CertificateRequest, usages []certificatesv1beta1.KeyUsage) bool {
|
||||
if !reflect.DeepEqual([]string{"system:nodes"}, req.Subject.Organization) {
|
||||
return false
|
||||
}
|
||||
|
||||
// at least one of dnsNames or ipAddresses must be specified
|
||||
if len(req.DNSNames) == 0 && len(req.IPAddresses) == 0 {
|
||||
return false
|
||||
}
|
||||
|
||||
if len(req.EmailAddresses) > 0 || len(req.URIs) > 0 {
|
||||
return false
|
||||
}
|
||||
|
||||
requiredUsages := []certificatesv1beta1.KeyUsage{
|
||||
certificatesv1beta1.UsageDigitalSignature,
|
||||
certificatesv1beta1.UsageKeyEncipherment,
|
||||
certificatesv1beta1.UsageServerAuth,
|
||||
}
|
||||
if !equalUnsorted(requiredUsages, usages) {
|
||||
return false
|
||||
}
|
||||
|
||||
if !strings.HasPrefix(req.Subject.CommonName, "system:node:") {
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
func IsKubeletClientCSR(req *x509.CertificateRequest, usages []certificatesv1beta1.KeyUsage) bool {
|
||||
if !reflect.DeepEqual([]string{"system:nodes"}, req.Subject.Organization) {
|
||||
return false
|
||||
}
|
||||
|
||||
if len(req.DNSNames) > 0 || len(req.EmailAddresses) > 0 || len(req.IPAddresses) > 0 || len(req.URIs) > 0 {
|
||||
return false
|
||||
}
|
||||
|
||||
if !strings.HasPrefix(req.Subject.CommonName, "system:node:") {
|
||||
return false
|
||||
}
|
||||
|
||||
requiredUsages := []certificatesv1beta1.KeyUsage{
|
||||
certificatesv1beta1.UsageDigitalSignature,
|
||||
certificatesv1beta1.UsageKeyEncipherment,
|
||||
certificatesv1beta1.UsageClientAuth,
|
||||
}
|
||||
if !equalUnsorted(requiredUsages, usages) {
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
// equalUnsorted compares two []string for equality of contents regardless of
|
||||
// the order of the elements
|
||||
func equalUnsorted(left, right []certificatesv1beta1.KeyUsage) bool {
|
||||
l := sets.NewString()
|
||||
for _, s := range left {
|
||||
l.Insert(string(s))
|
||||
}
|
||||
r := sets.NewString()
|
||||
for _, s := range right {
|
||||
r.Insert(string(s))
|
||||
}
|
||||
return l.Equal(r)
|
||||
}
|
||||
|
353
pkg/apis/certificates/v1beta1/defaults_test.go
Normal file
353
pkg/apis/certificates/v1beta1/defaults_test.go
Normal file
@ -0,0 +1,353 @@
|
||||
/*
|
||||
Copyright 2020 The Kubernetes Authors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package v1beta1
|
||||
|
||||
import (
|
||||
"crypto/ed25519"
|
||||
"crypto/rand"
|
||||
"crypto/x509"
|
||||
"crypto/x509/pkix"
|
||||
"encoding/pem"
|
||||
"net"
|
||||
"net/url"
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
capi "k8s.io/api/certificates/v1beta1"
|
||||
)
|
||||
|
||||
var (
|
||||
kubeletClientUsages = []capi.KeyUsage{
|
||||
capi.UsageDigitalSignature,
|
||||
capi.UsageKeyEncipherment,
|
||||
capi.UsageClientAuth,
|
||||
}
|
||||
kubeletClientPEMOptions = pemOptions{
|
||||
cn: "system:node:nodename",
|
||||
org: "system:nodes",
|
||||
}
|
||||
|
||||
kubeletServerUsages = []capi.KeyUsage{
|
||||
capi.UsageDigitalSignature,
|
||||
capi.UsageKeyEncipherment,
|
||||
capi.UsageServerAuth,
|
||||
}
|
||||
kubeletServerPEMOptions = pemOptions{
|
||||
cn: "system:node:requester-name",
|
||||
org: "system:nodes",
|
||||
dnsNames: []string{"node-server-name"},
|
||||
ipAddresses: []net.IP{{0, 0, 0, 0}},
|
||||
}
|
||||
)
|
||||
|
||||
func TestSetDefaults_CertificateSigningRequestSpec(t *testing.T) {
|
||||
strPtr := func(s string) *string { return &s }
|
||||
tests := map[string]struct {
|
||||
csr capi.CertificateSigningRequestSpec
|
||||
expectedSignerName string
|
||||
expectedUsages []capi.KeyUsage
|
||||
}{
|
||||
"defaults to legacy-unknown if request is not a CSR": {
|
||||
csr: capi.CertificateSigningRequestSpec{
|
||||
Request: []byte("invalid data"),
|
||||
Usages: kubeletServerUsages,
|
||||
},
|
||||
expectedSignerName: capi.LegacyUnknownSignerName,
|
||||
},
|
||||
"does not default signerName if signerName is already set": {
|
||||
csr: capi.CertificateSigningRequestSpec{
|
||||
Request: csrWithOpts(kubeletServerPEMOptions),
|
||||
Usages: kubeletServerUsages,
|
||||
SignerName: strPtr("example.com/not-kubelet-serving"),
|
||||
},
|
||||
expectedSignerName: "example.com/not-kubelet-serving",
|
||||
},
|
||||
"defaults usages if not set": {
|
||||
csr: capi.CertificateSigningRequestSpec{
|
||||
Request: csrWithOpts(kubeletServerPEMOptions),
|
||||
SignerName: strPtr("example.com/test"),
|
||||
},
|
||||
expectedSignerName: "example.com/test",
|
||||
expectedUsages: []capi.KeyUsage{capi.UsageDigitalSignature, capi.UsageKeyEncipherment},
|
||||
},
|
||||
}
|
||||
for name, test := range tests {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
// create a deepcopy to be sure we don't modify anything in-place
|
||||
csrSpec := test.csr.DeepCopy()
|
||||
SetDefaults_CertificateSigningRequestSpec(csrSpec)
|
||||
if *csrSpec.SignerName != test.expectedSignerName {
|
||||
t.Errorf("expected signerName to be defaulted to %q but it is %q", test.expectedSignerName, *csrSpec.SignerName)
|
||||
}
|
||||
|
||||
// only check expectedUsages if it is non-nil
|
||||
if test.expectedUsages != nil {
|
||||
if !reflect.DeepEqual(test.expectedUsages, csrSpec.Usages) {
|
||||
t.Errorf("expected usages to be defaulted to %v but it is %v", test.expectedUsages, csrSpec.Usages)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestSetDefaults_CertificateSigningRequestSpec_KubeletServing(t *testing.T) {
|
||||
tests := map[string]struct {
|
||||
csr capi.CertificateSigningRequestSpec
|
||||
expectedSignerName string
|
||||
}{
|
||||
"defaults for kubelet-serving": {
|
||||
csr: capi.CertificateSigningRequestSpec{
|
||||
Request: csrWithOpts(kubeletServerPEMOptions),
|
||||
Usages: kubeletServerUsages,
|
||||
Username: kubeletServerPEMOptions.cn,
|
||||
},
|
||||
expectedSignerName: capi.KubeletServingSignerName,
|
||||
},
|
||||
"does not default to kube-apiserver-client-kubelet if org is not 'system:nodes'": {
|
||||
csr: capi.CertificateSigningRequestSpec{
|
||||
Request: csrWithOpts(kubeletServerPEMOptions, pemOptions{org: "not-system:nodes"}),
|
||||
Usages: kubeletServerUsages,
|
||||
Username: "system:node:not-requester-name",
|
||||
},
|
||||
expectedSignerName: capi.LegacyUnknownSignerName,
|
||||
},
|
||||
"does not default to kubelet-serving if CN does not have system:node: prefix": {
|
||||
csr: capi.CertificateSigningRequestSpec{
|
||||
Request: csrWithOpts(kubeletServerPEMOptions, pemOptions{cn: "notprefixed"}),
|
||||
Usages: kubeletServerUsages,
|
||||
Username: "notprefixed",
|
||||
},
|
||||
expectedSignerName: capi.LegacyUnknownSignerName,
|
||||
},
|
||||
"does not default to kubelet-serving if it has an unexpected usage": {
|
||||
csr: capi.CertificateSigningRequestSpec{
|
||||
Request: csrWithOpts(kubeletServerPEMOptions),
|
||||
Usages: append(kubeletServerUsages, capi.UsageClientAuth),
|
||||
Username: kubeletServerPEMOptions.cn,
|
||||
},
|
||||
expectedSignerName: capi.LegacyUnknownSignerName,
|
||||
},
|
||||
"does not default to kubelet-serving if it is missing an expected usage": {
|
||||
csr: capi.CertificateSigningRequestSpec{
|
||||
Request: csrWithOpts(kubeletServerPEMOptions),
|
||||
// Remove the first usage in 'kubeletServerUsages'
|
||||
Usages: kubeletServerUsages[1:],
|
||||
Username: kubeletServerPEMOptions.cn,
|
||||
},
|
||||
expectedSignerName: capi.LegacyUnknownSignerName,
|
||||
},
|
||||
"does not default to kubelet-serving if it does not specify any dnsNames or ipAddresses": {
|
||||
csr: capi.CertificateSigningRequestSpec{
|
||||
Request: csrWithOpts(kubeletServerPEMOptions, pemOptions{ipAddresses: []net.IP{}, dnsNames: []string{}}),
|
||||
Usages: kubeletServerUsages,
|
||||
Username: kubeletServerPEMOptions.cn,
|
||||
},
|
||||
expectedSignerName: capi.LegacyUnknownSignerName,
|
||||
},
|
||||
"does not default to kubelet-serving if it specifies a URI SAN": {
|
||||
csr: capi.CertificateSigningRequestSpec{
|
||||
Request: csrWithOpts(kubeletServerPEMOptions, pemOptions{uris: []string{"http://something"}}),
|
||||
Usages: kubeletServerUsages,
|
||||
Username: kubeletServerPEMOptions.cn,
|
||||
},
|
||||
expectedSignerName: capi.LegacyUnknownSignerName,
|
||||
},
|
||||
"does not default to kubelet-serving if it specifies an emailAddress SAN": {
|
||||
csr: capi.CertificateSigningRequestSpec{
|
||||
Request: csrWithOpts(kubeletServerPEMOptions, pemOptions{emailAddresses: []string{"something"}}),
|
||||
Usages: kubeletServerUsages,
|
||||
Username: kubeletServerPEMOptions.cn,
|
||||
},
|
||||
expectedSignerName: capi.LegacyUnknownSignerName,
|
||||
},
|
||||
}
|
||||
for name, test := range tests {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
// create a deepcopy to be sure we don't modify anything in-place
|
||||
csrSpec := test.csr.DeepCopy()
|
||||
SetDefaults_CertificateSigningRequestSpec(csrSpec)
|
||||
if *csrSpec.SignerName != test.expectedSignerName {
|
||||
t.Errorf("expected signerName to be defaulted to %q but it is %q", test.expectedSignerName, *csrSpec.SignerName)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestSetDefaults_CertificateSigningRequestSpec_KubeletClient(t *testing.T) {
|
||||
tests := map[string]struct {
|
||||
csr capi.CertificateSigningRequestSpec
|
||||
expectedSignerName string
|
||||
}{
|
||||
"defaults for kube-apiserver-client-kubelet": {
|
||||
csr: capi.CertificateSigningRequestSpec{
|
||||
Request: csrWithOpts(kubeletClientPEMOptions),
|
||||
Usages: kubeletClientUsages,
|
||||
},
|
||||
expectedSignerName: capi.KubeAPIServerClientKubeletSignerName,
|
||||
},
|
||||
"does not default to kube-apiserver-client-kubelet if org is not 'system:nodes'": {
|
||||
csr: capi.CertificateSigningRequestSpec{
|
||||
Request: csrWithOpts(kubeletClientPEMOptions, pemOptions{org: "not-system:nodes"}),
|
||||
Usages: kubeletClientUsages,
|
||||
},
|
||||
expectedSignerName: capi.LegacyUnknownSignerName,
|
||||
},
|
||||
"does not default to kube-apiserver-client-kubelet if a dnsName is set": {
|
||||
csr: capi.CertificateSigningRequestSpec{
|
||||
Request: csrWithOpts(kubeletClientPEMOptions, pemOptions{dnsNames: []string{"something"}}),
|
||||
Usages: kubeletClientUsages,
|
||||
},
|
||||
expectedSignerName: capi.LegacyUnknownSignerName,
|
||||
},
|
||||
"does not default to kube-apiserver-client-kubelet if an emailAddress is set": {
|
||||
csr: capi.CertificateSigningRequestSpec{
|
||||
Request: csrWithOpts(kubeletClientPEMOptions, pemOptions{emailAddresses: []string{"something"}}),
|
||||
Usages: kubeletClientUsages,
|
||||
},
|
||||
expectedSignerName: capi.LegacyUnknownSignerName,
|
||||
},
|
||||
"does not default to kube-apiserver-client-kubelet if a uri SAN is set": {
|
||||
csr: capi.CertificateSigningRequestSpec{
|
||||
Request: csrWithOpts(kubeletClientPEMOptions, pemOptions{uris: []string{"http://something"}}),
|
||||
Usages: kubeletClientUsages,
|
||||
},
|
||||
expectedSignerName: capi.LegacyUnknownSignerName,
|
||||
},
|
||||
"does not default to kube-apiserver-client-kubelet if an ipAddress is set": {
|
||||
csr: capi.CertificateSigningRequestSpec{
|
||||
Request: csrWithOpts(kubeletClientPEMOptions, pemOptions{ipAddresses: []net.IP{{0, 0, 0, 0}}}),
|
||||
Usages: kubeletClientUsages,
|
||||
},
|
||||
expectedSignerName: capi.LegacyUnknownSignerName,
|
||||
},
|
||||
"does not default to kube-apiserver-client-kubelet if CN does not have 'system:node:' prefix": {
|
||||
csr: capi.CertificateSigningRequestSpec{
|
||||
Request: csrWithOpts(kubeletClientPEMOptions, pemOptions{cn: "not-prefixed"}),
|
||||
Usages: kubeletClientUsages,
|
||||
},
|
||||
expectedSignerName: capi.LegacyUnknownSignerName,
|
||||
},
|
||||
"does not default to kube-apiserver-client-kubelet if it has an unexpected usage": {
|
||||
csr: capi.CertificateSigningRequestSpec{
|
||||
Request: csrWithOpts(kubeletClientPEMOptions),
|
||||
Usages: append(kubeletClientUsages, capi.UsageServerAuth),
|
||||
},
|
||||
expectedSignerName: capi.LegacyUnknownSignerName,
|
||||
},
|
||||
"does not default to kube-apiserver-client-kubelet if it is missing an expected usage": {
|
||||
csr: capi.CertificateSigningRequestSpec{
|
||||
Request: csrWithOpts(kubeletClientPEMOptions),
|
||||
// Remove the first usage in 'kubeletClientUsages'
|
||||
Usages: kubeletClientUsages[1:],
|
||||
},
|
||||
expectedSignerName: capi.LegacyUnknownSignerName,
|
||||
},
|
||||
}
|
||||
for name, test := range tests {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
// create a deepcopy to be sure we don't modify anything in-place
|
||||
csrSpec := test.csr.DeepCopy()
|
||||
SetDefaults_CertificateSigningRequestSpec(csrSpec)
|
||||
if *csrSpec.SignerName != test.expectedSignerName {
|
||||
t.Errorf("expected signerName to be defaulted to %q but it is %q", test.expectedSignerName, *csrSpec.SignerName)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
type pemOptions struct {
|
||||
cn string
|
||||
org string
|
||||
ipAddresses []net.IP
|
||||
dnsNames []string
|
||||
emailAddresses []string
|
||||
uris []string
|
||||
}
|
||||
|
||||
// overlayPEMOptions overlays one set of pemOptions on top of another to allow
|
||||
// for easily overriding a single field in the options
|
||||
func overlayPEMOptions(opts ...pemOptions) pemOptions {
|
||||
if len(opts) == 0 {
|
||||
return pemOptions{}
|
||||
}
|
||||
base := opts[0]
|
||||
for _, opt := range opts[1:] {
|
||||
if opt.cn != "" {
|
||||
base.cn = opt.cn
|
||||
}
|
||||
if opt.org != "" {
|
||||
base.org = opt.org
|
||||
}
|
||||
if opt.ipAddresses != nil {
|
||||
base.ipAddresses = opt.ipAddresses
|
||||
}
|
||||
if opt.dnsNames != nil {
|
||||
base.dnsNames = opt.dnsNames
|
||||
}
|
||||
if opt.emailAddresses != nil {
|
||||
base.emailAddresses = opt.emailAddresses
|
||||
}
|
||||
if opt.uris != nil {
|
||||
base.uris = opt.uris
|
||||
}
|
||||
}
|
||||
return base
|
||||
}
|
||||
|
||||
func csrWithOpts(base pemOptions, overlays ...pemOptions) []byte {
|
||||
opts := overlayPEMOptions(append([]pemOptions{base}, overlays...)...)
|
||||
uris := make([]*url.URL, len(opts.uris))
|
||||
for i, s := range opts.uris {
|
||||
u, err := url.ParseRequestURI(s)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
uris[i] = u
|
||||
}
|
||||
template := &x509.CertificateRequest{
|
||||
Subject: pkix.Name{
|
||||
CommonName: opts.cn,
|
||||
Organization: []string{opts.org},
|
||||
},
|
||||
IPAddresses: opts.ipAddresses,
|
||||
DNSNames: opts.dnsNames,
|
||||
EmailAddresses: opts.emailAddresses,
|
||||
URIs: uris,
|
||||
}
|
||||
|
||||
_, key, err := ed25519.GenerateKey(rand.Reader)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
csrDER, err := x509.CreateCertificateRequest(rand.Reader, template, key)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
csrPemBlock := &pem.Block{
|
||||
Type: "CERTIFICATE REQUEST",
|
||||
Bytes: csrDER,
|
||||
}
|
||||
|
||||
p := pem.EncodeToMemory(csrPemBlock)
|
||||
if p == nil {
|
||||
panic("invalid pem block")
|
||||
}
|
||||
|
||||
return p
|
||||
}
|
@ -20,14 +20,11 @@ import (
|
||||
"crypto/x509"
|
||||
"encoding/pem"
|
||||
"errors"
|
||||
|
||||
certificatesv1beta1 "k8s.io/api/certificates/v1beta1"
|
||||
)
|
||||
|
||||
// ParseCSR extracts the CSR from the API object and decodes it.
|
||||
func ParseCSR(obj *certificatesv1beta1.CertificateSigningRequest) (*x509.CertificateRequest, error) {
|
||||
// ParseCSR decodes a PEM encoded CSR
|
||||
func ParseCSR(pemBytes []byte) (*x509.CertificateRequest, error) {
|
||||
// extract PEM from request object
|
||||
pemBytes := obj.Spec.Request
|
||||
block, _ := pem.Decode(pemBytes)
|
||||
if block == nil || block.Type != "CERTIFICATE REQUEST" {
|
||||
return nil, errors.New("PEM block type must be CERTIFICATE REQUEST")
|
||||
|
@ -46,5 +46,5 @@ 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)
|
||||
localSchemeBuilder.Register(addDefaultingFuncs, addConversionFuncs)
|
||||
}
|
||||
|
@ -24,6 +24,7 @@ import (
|
||||
unsafe "unsafe"
|
||||
|
||||
v1beta1 "k8s.io/api/certificates/v1beta1"
|
||||
v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
conversion "k8s.io/apimachinery/pkg/conversion"
|
||||
runtime "k8s.io/apimachinery/pkg/runtime"
|
||||
certificates "k8s.io/kubernetes/pkg/apis/certificates"
|
||||
@ -149,7 +150,17 @@ func Convert_certificates_CertificateSigningRequestCondition_To_v1beta1_Certific
|
||||
|
||||
func autoConvert_v1beta1_CertificateSigningRequestList_To_certificates_CertificateSigningRequestList(in *v1beta1.CertificateSigningRequestList, out *certificates.CertificateSigningRequestList, s conversion.Scope) error {
|
||||
out.ListMeta = in.ListMeta
|
||||
out.Items = *(*[]certificates.CertificateSigningRequest)(unsafe.Pointer(&in.Items))
|
||||
if in.Items != nil {
|
||||
in, out := &in.Items, &out.Items
|
||||
*out = make([]certificates.CertificateSigningRequest, len(*in))
|
||||
for i := range *in {
|
||||
if err := Convert_v1beta1_CertificateSigningRequest_To_certificates_CertificateSigningRequest(&(*in)[i], &(*out)[i], s); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
} else {
|
||||
out.Items = nil
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@ -160,7 +171,17 @@ func Convert_v1beta1_CertificateSigningRequestList_To_certificates_CertificateSi
|
||||
|
||||
func autoConvert_certificates_CertificateSigningRequestList_To_v1beta1_CertificateSigningRequestList(in *certificates.CertificateSigningRequestList, out *v1beta1.CertificateSigningRequestList, s conversion.Scope) error {
|
||||
out.ListMeta = in.ListMeta
|
||||
out.Items = *(*[]v1beta1.CertificateSigningRequest)(unsafe.Pointer(&in.Items))
|
||||
if in.Items != nil {
|
||||
in, out := &in.Items, &out.Items
|
||||
*out = make([]v1beta1.CertificateSigningRequest, len(*in))
|
||||
for i := range *in {
|
||||
if err := Convert_certificates_CertificateSigningRequest_To_v1beta1_CertificateSigningRequest(&(*in)[i], &(*out)[i], s); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
} else {
|
||||
out.Items = nil
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@ -171,6 +192,9 @@ func Convert_certificates_CertificateSigningRequestList_To_v1beta1_CertificateSi
|
||||
|
||||
func autoConvert_v1beta1_CertificateSigningRequestSpec_To_certificates_CertificateSigningRequestSpec(in *v1beta1.CertificateSigningRequestSpec, out *certificates.CertificateSigningRequestSpec, s conversion.Scope) error {
|
||||
out.Request = *(*[]byte)(unsafe.Pointer(&in.Request))
|
||||
if err := v1.Convert_Pointer_string_To_string(&in.SignerName, &out.SignerName, s); err != nil {
|
||||
return err
|
||||
}
|
||||
out.Usages = *(*[]certificates.KeyUsage)(unsafe.Pointer(&in.Usages))
|
||||
out.Username = in.Username
|
||||
out.UID = in.UID
|
||||
@ -186,6 +210,9 @@ func Convert_v1beta1_CertificateSigningRequestSpec_To_certificates_CertificateSi
|
||||
|
||||
func autoConvert_certificates_CertificateSigningRequestSpec_To_v1beta1_CertificateSigningRequestSpec(in *certificates.CertificateSigningRequestSpec, out *v1beta1.CertificateSigningRequestSpec, s conversion.Scope) error {
|
||||
out.Request = *(*[]byte)(unsafe.Pointer(&in.Request))
|
||||
if err := v1.Convert_string_To_Pointer_string(&in.SignerName, &out.SignerName, s); err != nil {
|
||||
return err
|
||||
}
|
||||
out.Usages = *(*[]v1beta1.KeyUsage)(unsafe.Pointer(&in.Usages))
|
||||
out.Username = in.Username
|
||||
out.UID = in.UID
|
||||
|
@ -3,6 +3,7 @@ package(default_visibility = ["//visibility:public"])
|
||||
load(
|
||||
"@io_bazel_rules_go//go:def.bzl",
|
||||
"go_library",
|
||||
"go_test",
|
||||
)
|
||||
|
||||
go_library(
|
||||
@ -12,6 +13,7 @@ go_library(
|
||||
deps = [
|
||||
"//pkg/apis/certificates:go_default_library",
|
||||
"//pkg/apis/core/validation:go_default_library",
|
||||
"//staging/src/k8s.io/apimachinery/pkg/util/validation:go_default_library",
|
||||
"//staging/src/k8s.io/apimachinery/pkg/util/validation/field:go_default_library",
|
||||
],
|
||||
)
|
||||
@ -28,3 +30,14 @@ filegroup(
|
||||
srcs = [":package-srcs"],
|
||||
tags = ["automanaged"],
|
||||
)
|
||||
|
||||
go_test(
|
||||
name = "go_default_test",
|
||||
srcs = ["validation_test.go"],
|
||||
embed = [":go_default_library"],
|
||||
deps = [
|
||||
"//pkg/apis/certificates:go_default_library",
|
||||
"//staging/src/k8s.io/apimachinery/pkg/apis/meta/v1:go_default_library",
|
||||
"//staging/src/k8s.io/apimachinery/pkg/util/validation/field:go_default_library",
|
||||
],
|
||||
)
|
||||
|
@ -18,8 +18,11 @@ package validation
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
utilvalidation "k8s.io/apimachinery/pkg/util/validation"
|
||||
"k8s.io/apimachinery/pkg/util/validation/field"
|
||||
|
||||
"k8s.io/kubernetes/pkg/apis/certificates"
|
||||
apivalidation "k8s.io/kubernetes/pkg/apis/core/validation"
|
||||
)
|
||||
@ -48,19 +51,95 @@ func ValidateCertificateRequestName(name string, prefix bool) []string {
|
||||
func ValidateCertificateSigningRequest(csr *certificates.CertificateSigningRequest) field.ErrorList {
|
||||
isNamespaced := false
|
||||
allErrs := apivalidation.ValidateObjectMeta(&csr.ObjectMeta, isNamespaced, ValidateCertificateRequestName, field.NewPath("metadata"))
|
||||
err := validateCSR(csr)
|
||||
|
||||
specPath := field.NewPath("spec")
|
||||
|
||||
err := validateCSR(csr)
|
||||
if err != nil {
|
||||
allErrs = append(allErrs, field.Invalid(specPath.Child("request"), csr.Spec.Request, fmt.Sprintf("%v", err)))
|
||||
}
|
||||
if len(csr.Spec.Usages) == 0 {
|
||||
allErrs = append(allErrs, field.Required(specPath.Child("usages"), "usages must be provided"))
|
||||
}
|
||||
allErrs = append(allErrs, ValidateCertificateSigningRequestSignerName(specPath.Child("signerName"), csr.Spec.SignerName)...)
|
||||
return allErrs
|
||||
}
|
||||
|
||||
// ensure signerName is of the form domain.com/something and up to 571 characters.
|
||||
// This length and format is specified to accommodate signerNames like:
|
||||
// <fqdn>/<resource-namespace>.<resource-name>.
|
||||
// The max length of a FQDN is 253 characters (DNS1123Subdomain max length)
|
||||
// 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 {
|
||||
var el field.ErrorList
|
||||
if len(signerName) == 0 {
|
||||
el = append(el, field.Required(fldPath, "signerName must be provided"))
|
||||
return el
|
||||
}
|
||||
|
||||
segments := strings.Split(signerName, "/")
|
||||
// validate that there is one '/' in the signerName.
|
||||
// we do this after validating the domain segment to provide more info to the user.
|
||||
if len(segments) != 2 {
|
||||
el = append(el, field.Invalid(fldPath, signerName, "must be a fully qualified domain and path of the form 'example.com/signer-name'"))
|
||||
// return early here as we should not continue attempting to validate a missing or malformed path segment
|
||||
// (i.e. one containing multiple or zero `/`)
|
||||
return el
|
||||
}
|
||||
|
||||
// validate that segments[0] is less than 253 characters altogether
|
||||
maxDomainSegmentLength := utilvalidation.DNS1123SubdomainMaxLength
|
||||
if len(segments[0]) > maxDomainSegmentLength {
|
||||
el = append(el, field.TooLong(fldPath, segments[0], maxDomainSegmentLength))
|
||||
}
|
||||
// validate that segments[0] consists of valid DNS1123 labels separated by '.'
|
||||
domainLabels := strings.Split(segments[0], ".")
|
||||
for _, lbl := range domainLabels {
|
||||
// use IsDNS1123Label as we want to ensure the max length of any single label in the domain
|
||||
// is 63 characters
|
||||
if errs := utilvalidation.IsDNS1123Label(lbl); len(errs) > 0 {
|
||||
for _, err := range errs {
|
||||
el = append(el, field.Invalid(fldPath, segments[0], fmt.Sprintf("validating label %q: %s", lbl, err)))
|
||||
}
|
||||
// if we encounter any errors whilst parsing the domain segment, break from
|
||||
// validation as any further error messages will be duplicates, and non-distinguishable
|
||||
// from each other, confusing users.
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// validate that there is at least one '.' in segments[0]
|
||||
if len(domainLabels) < 2 {
|
||||
el = append(el, field.Invalid(fldPath, segments[0], "should be a domain with at least two segments separated by dots"))
|
||||
}
|
||||
|
||||
// validate that segments[1] consists of valid DNS1123 subdomains separated by '.'.
|
||||
pathLabels := strings.Split(segments[1], ".")
|
||||
for _, lbl := range pathLabels {
|
||||
// use IsDNS1123Subdomain because it enforces a length restriction of 253 characters
|
||||
// which is required in order to fit a full resource name into a single 'label'
|
||||
if errs := utilvalidation.IsDNS1123Subdomain(lbl); len(errs) > 0 {
|
||||
for _, err := range errs {
|
||||
el = append(el, field.Invalid(fldPath, segments[1], fmt.Sprintf("validating label %q: %s", lbl, err)))
|
||||
}
|
||||
// if we encounter any errors whilst parsing the path segment, break from
|
||||
// validation as any further error messages will be duplicates, and non-distinguishable
|
||||
// from each other, confusing users.
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// ensure that segments[1] can accommodate a dns label + dns subdomain + '.'
|
||||
maxPathSegmentLength := utilvalidation.DNS1123SubdomainMaxLength + utilvalidation.DNS1123LabelMaxLength + 1
|
||||
maxSignerNameLength := maxDomainSegmentLength + maxPathSegmentLength + 1
|
||||
if len(signerName) > maxSignerNameLength {
|
||||
el = append(el, field.TooLong(fldPath, signerName, maxSignerNameLength))
|
||||
}
|
||||
|
||||
return el
|
||||
}
|
||||
|
||||
func ValidateCertificateSigningRequestUpdate(newCSR, oldCSR *certificates.CertificateSigningRequest) field.ErrorList {
|
||||
validationErrorList := ValidateCertificateSigningRequest(newCSR)
|
||||
metaUpdateErrorList := apivalidation.ValidateObjectMetaUpdate(&newCSR.ObjectMeta, &oldCSR.ObjectMeta, field.NewPath("metadata"))
|
||||
|
308
pkg/apis/certificates/validation/validation_test.go
Normal file
308
pkg/apis/certificates/validation/validation_test.go
Normal file
@ -0,0 +1,308 @@
|
||||
/*
|
||||
Copyright 2020 The Kubernetes Authors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package validation
|
||||
|
||||
import (
|
||||
"crypto/ed25519"
|
||||
"crypto/rand"
|
||||
"crypto/x509"
|
||||
"crypto/x509/pkix"
|
||||
"encoding/pem"
|
||||
"fmt"
|
||||
"reflect"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/util/validation/field"
|
||||
|
||||
capi "k8s.io/kubernetes/pkg/apis/certificates"
|
||||
)
|
||||
|
||||
var (
|
||||
validObjectMeta = metav1.ObjectMeta{Name: "testcsr"}
|
||||
validSignerName = "example.com/valid-name"
|
||||
validUsages = []capi.KeyUsage{capi.UsageKeyEncipherment}
|
||||
)
|
||||
|
||||
func TestValidateCertificateSigningRequest(t *testing.T) {
|
||||
specPath := field.NewPath("spec")
|
||||
// maxLengthSignerName is a signerName that is of maximum length, utilising
|
||||
// the max length specifications defined in validation.go.
|
||||
// It is of the form <fqdn(253)>/<resource-namespace(63)>.<resource-name(253)>
|
||||
maxLengthFQDN := fmt.Sprintf("%s.%s.%s.%s", repeatString("a", 63), repeatString("a", 63), repeatString("a", 63), repeatString("a", 61))
|
||||
maxLengthSignerName := fmt.Sprintf("%s/%s.%s", maxLengthFQDN, repeatString("a", 63), repeatString("a", 253))
|
||||
tests := map[string]struct {
|
||||
csr capi.CertificateSigningRequest
|
||||
errs field.ErrorList
|
||||
}{
|
||||
"CSR with empty request data should fail": {
|
||||
csr: capi.CertificateSigningRequest{
|
||||
ObjectMeta: validObjectMeta,
|
||||
Spec: capi.CertificateSigningRequestSpec{
|
||||
Usages: validUsages,
|
||||
SignerName: validSignerName,
|
||||
},
|
||||
},
|
||||
errs: field.ErrorList{
|
||||
field.Invalid(specPath.Child("request"), []byte(nil), "PEM block type must be CERTIFICATE REQUEST"),
|
||||
},
|
||||
},
|
||||
"CSR with invalid request data should fail": {
|
||||
csr: capi.CertificateSigningRequest{
|
||||
ObjectMeta: validObjectMeta,
|
||||
Spec: capi.CertificateSigningRequestSpec{
|
||||
Usages: validUsages,
|
||||
SignerName: validSignerName,
|
||||
Request: []byte("invalid data"),
|
||||
},
|
||||
},
|
||||
errs: field.ErrorList{
|
||||
field.Invalid(specPath.Child("request"), []byte("invalid data"), "PEM block type must be CERTIFICATE REQUEST"),
|
||||
},
|
||||
},
|
||||
"CSR with no usages should fail": {
|
||||
csr: capi.CertificateSigningRequest{
|
||||
ObjectMeta: validObjectMeta,
|
||||
Spec: capi.CertificateSigningRequestSpec{
|
||||
SignerName: validSignerName,
|
||||
Request: newCSRPEM(t),
|
||||
},
|
||||
},
|
||||
errs: field.ErrorList{
|
||||
field.Required(specPath.Child("usages"), "usages must be provided"),
|
||||
},
|
||||
},
|
||||
"CSR with no signerName set should fail": {
|
||||
csr: capi.CertificateSigningRequest{
|
||||
ObjectMeta: validObjectMeta,
|
||||
Spec: capi.CertificateSigningRequestSpec{
|
||||
Usages: validUsages,
|
||||
Request: newCSRPEM(t),
|
||||
},
|
||||
},
|
||||
errs: field.ErrorList{
|
||||
field.Required(specPath.Child("signerName"), "signerName must be provided"),
|
||||
},
|
||||
},
|
||||
"signerName contains no '/'": {
|
||||
csr: capi.CertificateSigningRequest{
|
||||
ObjectMeta: validObjectMeta,
|
||||
Spec: capi.CertificateSigningRequestSpec{
|
||||
Usages: validUsages,
|
||||
Request: newCSRPEM(t),
|
||||
SignerName: "an-invalid-signer-name",
|
||||
},
|
||||
},
|
||||
errs: field.ErrorList{
|
||||
field.Invalid(specPath.Child("signerName"), "an-invalid-signer-name", "must be a fully qualified domain and path of the form 'example.com/signer-name'"),
|
||||
},
|
||||
},
|
||||
"signerName contains two '/'": {
|
||||
csr: capi.CertificateSigningRequest{
|
||||
ObjectMeta: validObjectMeta,
|
||||
Spec: capi.CertificateSigningRequestSpec{
|
||||
Usages: validUsages,
|
||||
Request: newCSRPEM(t),
|
||||
SignerName: "an-invalid-signer-name.com/something/else",
|
||||
},
|
||||
},
|
||||
errs: field.ErrorList{
|
||||
field.Invalid(specPath.Child("signerName"), "an-invalid-signer-name.com/something/else", "must be a fully qualified domain and path of the form 'example.com/signer-name'"),
|
||||
},
|
||||
},
|
||||
"signerName domain component is not fully qualified": {
|
||||
csr: capi.CertificateSigningRequest{
|
||||
ObjectMeta: validObjectMeta,
|
||||
Spec: capi.CertificateSigningRequestSpec{
|
||||
Usages: validUsages,
|
||||
Request: newCSRPEM(t),
|
||||
SignerName: "example/some-signer-name",
|
||||
},
|
||||
},
|
||||
errs: field.ErrorList{
|
||||
field.Invalid(specPath.Child("signerName"), "example", "should be a domain with at least two segments separated by dots"),
|
||||
},
|
||||
},
|
||||
"signerName path component is empty": {
|
||||
csr: capi.CertificateSigningRequest{
|
||||
ObjectMeta: validObjectMeta,
|
||||
Spec: capi.CertificateSigningRequestSpec{
|
||||
Usages: validUsages,
|
||||
Request: newCSRPEM(t),
|
||||
SignerName: "example.com/",
|
||||
},
|
||||
},
|
||||
errs: field.ErrorList{
|
||||
field.Invalid(specPath.Child("signerName"), "", `validating label "": a DNS-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])?)*')`),
|
||||
},
|
||||
},
|
||||
"signerName path component ends with a symbol": {
|
||||
csr: capi.CertificateSigningRequest{
|
||||
ObjectMeta: validObjectMeta,
|
||||
Spec: capi.CertificateSigningRequestSpec{
|
||||
Usages: validUsages,
|
||||
Request: newCSRPEM(t),
|
||||
SignerName: "example.com/something-",
|
||||
},
|
||||
},
|
||||
errs: field.ErrorList{
|
||||
field.Invalid(specPath.Child("signerName"), "something-", `validating label "something-": a DNS-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])?)*')`),
|
||||
},
|
||||
},
|
||||
"signerName path component is a symbol": {
|
||||
csr: capi.CertificateSigningRequest{
|
||||
ObjectMeta: validObjectMeta,
|
||||
Spec: capi.CertificateSigningRequestSpec{
|
||||
Usages: validUsages,
|
||||
Request: newCSRPEM(t),
|
||||
SignerName: "example.com/-",
|
||||
},
|
||||
},
|
||||
errs: field.ErrorList{
|
||||
field.Invalid(specPath.Child("signerName"), "-", `validating label "-": a DNS-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])?)*')`),
|
||||
},
|
||||
},
|
||||
"signerName path component contains no '.' but is valid": {
|
||||
csr: capi.CertificateSigningRequest{
|
||||
ObjectMeta: validObjectMeta,
|
||||
Spec: capi.CertificateSigningRequestSpec{
|
||||
Usages: validUsages,
|
||||
Request: newCSRPEM(t),
|
||||
SignerName: "example.com/some-signer-name",
|
||||
},
|
||||
},
|
||||
errs: field.ErrorList{},
|
||||
},
|
||||
"signerName with a total length greater than 571 characters should be rejected": {
|
||||
csr: capi.CertificateSigningRequest{
|
||||
ObjectMeta: validObjectMeta,
|
||||
Spec: capi.CertificateSigningRequestSpec{
|
||||
Usages: validUsages,
|
||||
Request: newCSRPEM(t),
|
||||
// this string is longer than the max signerName limit (635 chars)
|
||||
SignerName: maxLengthSignerName + ".toolong",
|
||||
},
|
||||
},
|
||||
errs: field.ErrorList{
|
||||
field.TooLong(specPath.Child("signerName"), maxLengthSignerName+".toolong", len(maxLengthSignerName)),
|
||||
},
|
||||
},
|
||||
"signerName with a fqdn greater than 253 characters should be rejected": {
|
||||
csr: capi.CertificateSigningRequest{
|
||||
ObjectMeta: validObjectMeta,
|
||||
Spec: capi.CertificateSigningRequestSpec{
|
||||
Usages: validUsages,
|
||||
Request: newCSRPEM(t),
|
||||
// this string is longer than the max signerName limit (635 chars)
|
||||
SignerName: fmt.Sprintf("%s.extra/valid-path", maxLengthFQDN),
|
||||
},
|
||||
},
|
||||
errs: field.ErrorList{
|
||||
field.TooLong(specPath.Child("signerName"), fmt.Sprintf("%s.extra", maxLengthFQDN), len(maxLengthFQDN)),
|
||||
},
|
||||
},
|
||||
"signerName can have a longer path if the domain component is less than the max length": {
|
||||
csr: capi.CertificateSigningRequest{
|
||||
ObjectMeta: validObjectMeta,
|
||||
Spec: capi.CertificateSigningRequestSpec{
|
||||
Usages: validUsages,
|
||||
Request: newCSRPEM(t),
|
||||
SignerName: fmt.Sprintf("abc.io/%s.%s", repeatString("a", 253), repeatString("a", 253)),
|
||||
},
|
||||
},
|
||||
errs: field.ErrorList{},
|
||||
},
|
||||
"signerName with a domain label greater than 63 characters will fail": {
|
||||
csr: capi.CertificateSigningRequest{
|
||||
ObjectMeta: validObjectMeta,
|
||||
Spec: capi.CertificateSigningRequestSpec{
|
||||
Usages: validUsages,
|
||||
Request: newCSRPEM(t),
|
||||
SignerName: fmt.Sprintf("%s.example.io/valid-path", repeatString("a", 66)),
|
||||
},
|
||||
},
|
||||
errs: field.ErrorList{
|
||||
field.Invalid(specPath.Child("signerName"), fmt.Sprintf("%s.example.io", repeatString("a", 66)), fmt.Sprintf(`validating label "%s": must be no more than 63 characters`, repeatString("a", 66))),
|
||||
},
|
||||
},
|
||||
"signerName of max length in format <fully-qualified-domain-name>/<resource-namespace>.<resource-name> is valid": {
|
||||
// ensure signerName is of the form domain.com/something and up to 571 characters.
|
||||
// This length and format is specified to accommodate signerNames like:
|
||||
// <fqdn>/<resource-namespace>.<resource-name>.
|
||||
// The max length of a FQDN is 253 characters (DNS1123Subdomain max length)
|
||||
// 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 '/'.
|
||||
csr: capi.CertificateSigningRequest{
|
||||
ObjectMeta: validObjectMeta,
|
||||
Spec: capi.CertificateSigningRequestSpec{
|
||||
Usages: validUsages,
|
||||
Request: newCSRPEM(t),
|
||||
SignerName: maxLengthSignerName,
|
||||
},
|
||||
},
|
||||
errs: field.ErrorList{},
|
||||
},
|
||||
}
|
||||
for name, test := range tests {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
el := ValidateCertificateSigningRequest(&test.csr)
|
||||
if !reflect.DeepEqual(el, test.errs) {
|
||||
t.Errorf("returned and expected errors did not match - expected %v but got %v", test.errs.ToAggregate(), el.ToAggregate())
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func repeatString(s string, num int) string {
|
||||
l := make([]string, num)
|
||||
for i := 0; i < num; i++ {
|
||||
l[i] = s
|
||||
}
|
||||
return strings.Join(l, "")
|
||||
}
|
||||
|
||||
func newCSRPEM(t *testing.T) []byte {
|
||||
template := &x509.CertificateRequest{
|
||||
Subject: pkix.Name{
|
||||
Organization: []string{"testing-org"},
|
||||
},
|
||||
}
|
||||
|
||||
_, key, err := ed25519.GenerateKey(rand.Reader)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
csrDER, err := x509.CreateCertificateRequest(rand.Reader, template, key)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
csrPemBlock := &pem.Block{
|
||||
Type: "CERTIFICATE REQUEST",
|
||||
Bytes: csrDER,
|
||||
}
|
||||
|
||||
p := pem.EncodeToMemory(csrPemBlock)
|
||||
if p == nil {
|
||||
t.Fatal("invalid pem block")
|
||||
}
|
||||
|
||||
return p
|
||||
}
|
@ -13,6 +13,7 @@ go_library(
|
||||
importpath = "k8s.io/kubernetes/pkg/controller/certificates",
|
||||
visibility = ["//visibility:public"],
|
||||
deps = [
|
||||
"//pkg/apis/certificates/v1beta1:go_default_library",
|
||||
"//pkg/controller:go_default_library",
|
||||
"//staging/src/k8s.io/api/certificates/v1beta1:go_default_library",
|
||||
"//staging/src/k8s.io/apimachinery/pkg/api/errors:go_default_library",
|
||||
|
@ -21,14 +21,13 @@ import (
|
||||
"context"
|
||||
"crypto/x509"
|
||||
"fmt"
|
||||
"reflect"
|
||||
"strings"
|
||||
|
||||
authorization "k8s.io/api/authorization/v1"
|
||||
capi "k8s.io/api/certificates/v1beta1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
certificatesinformers "k8s.io/client-go/informers/certificates/v1beta1"
|
||||
clientset "k8s.io/client-go/kubernetes"
|
||||
|
||||
capihelper "k8s.io/kubernetes/pkg/apis/certificates/v1beta1"
|
||||
"k8s.io/kubernetes/pkg/controller/certificates"
|
||||
)
|
||||
@ -81,7 +80,7 @@ func (a *sarApprover) handle(csr *capi.CertificateSigningRequest) error {
|
||||
if approved, denied := certificates.GetCertApprovalCondition(&csr.Status); approved || denied {
|
||||
return nil
|
||||
}
|
||||
x509cr, err := capihelper.ParseCSR(csr)
|
||||
x509cr, err := capihelper.ParseCSR(csr.Spec.Request)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to parse csr %q: %v", csr.Name, err)
|
||||
}
|
||||
@ -146,45 +145,12 @@ func appendApprovalCondition(csr *capi.CertificateSigningRequest, message string
|
||||
})
|
||||
}
|
||||
|
||||
func hasExactUsages(csr *capi.CertificateSigningRequest, usages []capi.KeyUsage) bool {
|
||||
if len(usages) != len(csr.Spec.Usages) {
|
||||
return false
|
||||
}
|
||||
|
||||
usageMap := map[capi.KeyUsage]struct{}{}
|
||||
for _, u := range usages {
|
||||
usageMap[u] = struct{}{}
|
||||
}
|
||||
|
||||
for _, u := range csr.Spec.Usages {
|
||||
if _, ok := usageMap[u]; !ok {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
var kubeletClientUsages = []capi.KeyUsage{
|
||||
capi.UsageKeyEncipherment,
|
||||
capi.UsageDigitalSignature,
|
||||
capi.UsageClientAuth,
|
||||
}
|
||||
|
||||
func isNodeClientCert(csr *capi.CertificateSigningRequest, x509cr *x509.CertificateRequest) bool {
|
||||
if !reflect.DeepEqual([]string{"system:nodes"}, x509cr.Subject.Organization) {
|
||||
isClientCSR := capihelper.IsKubeletClientCSR(x509cr, csr.Spec.Usages)
|
||||
if !isClientCSR {
|
||||
return false
|
||||
}
|
||||
if len(x509cr.DNSNames) > 0 || len(x509cr.EmailAddresses) > 0 || len(x509cr.IPAddresses) > 0 || len(x509cr.URIs) > 0 {
|
||||
return false
|
||||
}
|
||||
if !hasExactUsages(csr, kubeletClientUsages) {
|
||||
return false
|
||||
}
|
||||
if !strings.HasPrefix(x509cr.Subject.CommonName, "system:node:") {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
return *csr.Spec.SignerName == capi.KubeAPIServerClientKubeletSignerName
|
||||
}
|
||||
|
||||
func isSelfNodeClientCert(csr *capi.CertificateSigningRequest, x509cr *x509.CertificateRequest) bool {
|
||||
|
@ -36,54 +36,6 @@ import (
|
||||
k8s_certificates_v1beta1 "k8s.io/kubernetes/pkg/apis/certificates/v1beta1"
|
||||
)
|
||||
|
||||
func TestHasKubeletUsages(t *testing.T) {
|
||||
cases := []struct {
|
||||
usages []capi.KeyUsage
|
||||
expected bool
|
||||
}{
|
||||
{
|
||||
usages: nil,
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
usages: []capi.KeyUsage{},
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
usages: []capi.KeyUsage{
|
||||
capi.UsageKeyEncipherment,
|
||||
capi.UsageDigitalSignature,
|
||||
},
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
usages: []capi.KeyUsage{
|
||||
capi.UsageKeyEncipherment,
|
||||
capi.UsageDigitalSignature,
|
||||
capi.UsageServerAuth,
|
||||
},
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
usages: []capi.KeyUsage{
|
||||
capi.UsageKeyEncipherment,
|
||||
capi.UsageDigitalSignature,
|
||||
capi.UsageClientAuth,
|
||||
},
|
||||
expected: true,
|
||||
},
|
||||
}
|
||||
for _, c := range cases {
|
||||
if hasExactUsages(&capi.CertificateSigningRequest{
|
||||
Spec: capi.CertificateSigningRequestSpec{
|
||||
Usages: c.usages,
|
||||
},
|
||||
}, kubeletClientUsages) != c.expected {
|
||||
t.Errorf("unexpected result of hasKubeletUsages(%v), expecting: %v", c.usages, c.expected)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestHandle(t *testing.T) {
|
||||
cases := []struct {
|
||||
allowed bool
|
||||
@ -208,6 +160,12 @@ func TestRecognizers(t *testing.T) {
|
||||
func(b *csrBuilder) {
|
||||
b.usages = append(b.usages, capi.UsageServerAuth)
|
||||
},
|
||||
func(b *csrBuilder) {
|
||||
b.signerName = "example.com/not-correct"
|
||||
},
|
||||
func(b *csrBuilder) {
|
||||
b.signerName = capi.KubeletServingSignerName
|
||||
},
|
||||
}
|
||||
|
||||
testRecognizer(t, badCases, isNodeClientCert, false)
|
||||
@ -230,9 +188,10 @@ func TestRecognizers(t *testing.T) {
|
||||
func testRecognizer(t *testing.T, cases []func(b *csrBuilder), recognizeFunc func(csr *capi.CertificateSigningRequest, x509cr *x509.CertificateRequest) bool, shouldRecognize bool) {
|
||||
for _, c := range cases {
|
||||
b := csrBuilder{
|
||||
cn: "system:node:foo",
|
||||
orgs: []string{"system:nodes"},
|
||||
requestor: "system:node:foo",
|
||||
signerName: capi.KubeAPIServerClientKubeletSignerName,
|
||||
cn: "system:node:foo",
|
||||
orgs: []string{"system:nodes"},
|
||||
requestor: "system:node:foo",
|
||||
usages: []capi.KeyUsage{
|
||||
capi.UsageKeyEncipherment,
|
||||
capi.UsageDigitalSignature,
|
||||
@ -242,7 +201,7 @@ func testRecognizer(t *testing.T, cases []func(b *csrBuilder), recognizeFunc fun
|
||||
c(&b)
|
||||
t.Run(fmt.Sprintf("csr:%#v", b), func(t *testing.T) {
|
||||
csr := makeFancyTestCsr(b)
|
||||
x509cr, err := k8s_certificates_v1beta1.ParseCSR(csr)
|
||||
x509cr, err := k8s_certificates_v1beta1.ParseCSR(csr.Spec.Request)
|
||||
if err != nil {
|
||||
t.Errorf("unexpected err: %v", err)
|
||||
}
|
||||
@ -262,13 +221,14 @@ func makeTestCsr() *capi.CertificateSigningRequest {
|
||||
}
|
||||
|
||||
type csrBuilder struct {
|
||||
cn string
|
||||
orgs []string
|
||||
requestor string
|
||||
usages []capi.KeyUsage
|
||||
dns []string
|
||||
emails []string
|
||||
ips []net.IP
|
||||
cn string
|
||||
orgs []string
|
||||
requestor string
|
||||
usages []capi.KeyUsage
|
||||
dns []string
|
||||
emails []string
|
||||
ips []net.IP
|
||||
signerName string
|
||||
}
|
||||
|
||||
func makeFancyTestCsr(b csrBuilder) *capi.CertificateSigningRequest {
|
||||
@ -290,9 +250,10 @@ func makeFancyTestCsr(b csrBuilder) *capi.CertificateSigningRequest {
|
||||
}
|
||||
return &capi.CertificateSigningRequest{
|
||||
Spec: capi.CertificateSigningRequestSpec{
|
||||
Username: b.requestor,
|
||||
Usages: b.usages,
|
||||
Request: pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE REQUEST", Bytes: csrb}),
|
||||
Username: b.requestor,
|
||||
Usages: b.usages,
|
||||
SignerName: &b.signerName,
|
||||
Request: pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE REQUEST", Bytes: csrb}),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
@ -36,6 +36,7 @@ import (
|
||||
"k8s.io/client-go/tools/record"
|
||||
"k8s.io/client-go/util/workqueue"
|
||||
"k8s.io/klog"
|
||||
capihelper "k8s.io/kubernetes/pkg/apis/certificates/v1beta1"
|
||||
"k8s.io/kubernetes/pkg/controller"
|
||||
)
|
||||
|
||||
@ -192,7 +193,15 @@ func (cc *CertificateController) syncFunc(key string) error {
|
||||
|
||||
// need to operate on a copy so we don't mutate the csr in the shared cache
|
||||
csr = csr.DeepCopy()
|
||||
|
||||
// If the `signerName` field is not set, we are talking to a pre-1.18 apiserver.
|
||||
// As per the KEP document for the certificates API, this will be defaulted here
|
||||
// in the controller to maintain backwards compatibility.
|
||||
// This should be removed after a deprecation window has passed.
|
||||
// Default here to allow handlers to assume the field is set.
|
||||
if csr.Spec.SignerName == nil {
|
||||
signerName := capihelper.DefaultSignerNameFromSpec(&csr.Spec)
|
||||
csr.Spec.SignerName = &signerName
|
||||
}
|
||||
return cc.handler(csr)
|
||||
}
|
||||
|
||||
|
@ -16,9 +16,12 @@ go_test(
|
||||
],
|
||||
embed = [":go_default_library"],
|
||||
deps = [
|
||||
"//pkg/apis/certificates/v1beta1:go_default_library",
|
||||
"//staging/src/k8s.io/api/certificates/v1beta1:go_default_library",
|
||||
"//staging/src/k8s.io/apimachinery/pkg/util/clock:go_default_library",
|
||||
"//staging/src/k8s.io/apimachinery/pkg/util/diff:go_default_library",
|
||||
"//staging/src/k8s.io/client-go/kubernetes/fake:go_default_library",
|
||||
"//staging/src/k8s.io/client-go/testing:go_default_library",
|
||||
"//staging/src/k8s.io/client-go/util/cert:go_default_library",
|
||||
"//vendor/github.com/google/go-cmp/cmp:go_default_library",
|
||||
],
|
||||
|
@ -19,8 +19,10 @@ package signer
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/x509"
|
||||
"encoding/pem"
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
capi "k8s.io/api/certificates/v1beta1"
|
||||
@ -89,13 +91,31 @@ func newSigner(caFile, caKeyFile string, client clientset.Interface, certificate
|
||||
}
|
||||
|
||||
func (s *signer) handle(csr *capi.CertificateSigningRequest) error {
|
||||
// Ignore unapproved requests
|
||||
if !certificates.IsCertificateRequestApproved(csr) {
|
||||
return nil
|
||||
}
|
||||
csr, err := s.sign(csr)
|
||||
|
||||
// Fast-path to avoid any additional processing if the CSRs signerName does
|
||||
// not have a 'kubernetes.io/' prefix.
|
||||
if !strings.HasPrefix(*csr.Spec.SignerName, "kubernetes.io/") {
|
||||
return nil
|
||||
}
|
||||
|
||||
x509cr, err := capihelper.ParseCSR(csr.Spec.Request)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to parse csr %q: %v", csr.Name, err)
|
||||
}
|
||||
if !requestValidForSignerName(x509cr, csr.Spec.Usages, *csr.Spec.SignerName) {
|
||||
// TODO: mark the CertificateRequest as being in a terminal state and
|
||||
// communicate to the user why the request has been refused.
|
||||
return nil
|
||||
}
|
||||
cert, err := s.sign(x509cr, csr.Spec.Usages)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error auto signing csr: %v", err)
|
||||
}
|
||||
csr.Status.Certificate = cert
|
||||
_, err = s.client.CertificatesV1beta1().CertificateSigningRequests().UpdateStatus(context.TODO(), csr, metav1.UpdateOptions{})
|
||||
if err != nil {
|
||||
return fmt.Errorf("error updating signature for csr: %v", err)
|
||||
@ -103,23 +123,50 @@ func (s *signer) handle(csr *capi.CertificateSigningRequest) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *signer) sign(csr *capi.CertificateSigningRequest) (*capi.CertificateSigningRequest, error) {
|
||||
x509cr, err := capihelper.ParseCSR(csr)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unable to parse csr %q: %v", csr.Name, err)
|
||||
}
|
||||
|
||||
func (s *signer) sign(x509cr *x509.CertificateRequest, usages []capi.KeyUsage) ([]byte, error) {
|
||||
currCA, err := s.caProvider.currentCA()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
der, err := currCA.Sign(x509cr.Raw, authority.PermissiveSigningPolicy{
|
||||
TTL: s.certTTL,
|
||||
Usages: csr.Spec.Usages,
|
||||
Usages: usages,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
csr.Status.Certificate = pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: der})
|
||||
return csr, nil
|
||||
return pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: der}), nil
|
||||
}
|
||||
|
||||
func requestValidForSignerName(req *x509.CertificateRequest, usages []capi.KeyUsage, signerName string) bool {
|
||||
// Only handle CSRs with the specific known signerNames.
|
||||
switch signerName {
|
||||
case capi.KubeletServingSignerName:
|
||||
return capihelper.IsKubeletServingCSR(req, usages)
|
||||
case capi.KubeAPIServerClientKubeletSignerName:
|
||||
return capihelper.IsKubeletClientCSR(req, usages)
|
||||
case capi.KubeAPIServerClientSignerName:
|
||||
return validAPIServerClientUsages(usages)
|
||||
case capi.LegacyUnknownSignerName:
|
||||
// No restrictions are applied to the legacy-unknown signerName to
|
||||
// maintain backward compatibility in v1beta1.
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
func validAPIServerClientUsages(usages []capi.KeyUsage) bool {
|
||||
hasClientAuth := false
|
||||
for _, u := range usages {
|
||||
switch u {
|
||||
// these usages are optional
|
||||
case capi.UsageDigitalSignature, capi.UsageKeyEncipherment:
|
||||
case capi.UsageClientAuth:
|
||||
hasClientAuth = true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
return hasClientAuth
|
||||
}
|
||||
|
@ -17,9 +17,13 @@ limitations under the License.
|
||||
package signer
|
||||
|
||||
import (
|
||||
"crypto/ecdsa"
|
||||
"crypto/elliptic"
|
||||
"crypto/x509"
|
||||
"crypto/x509/pkix"
|
||||
"encoding/pem"
|
||||
"io/ioutil"
|
||||
"math/rand"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
@ -28,7 +32,11 @@ import (
|
||||
capi "k8s.io/api/certificates/v1beta1"
|
||||
"k8s.io/apimachinery/pkg/util/clock"
|
||||
"k8s.io/apimachinery/pkg/util/diff"
|
||||
"k8s.io/client-go/kubernetes/fake"
|
||||
testclient "k8s.io/client-go/testing"
|
||||
"k8s.io/client-go/util/cert"
|
||||
|
||||
capihelper "k8s.io/kubernetes/pkg/apis/certificates/v1beta1"
|
||||
)
|
||||
|
||||
func TestSigner(t *testing.T) {
|
||||
@ -50,24 +58,20 @@ func TestSigner(t *testing.T) {
|
||||
if err != nil {
|
||||
t.Fatalf("failed to read CSR: %v", err)
|
||||
}
|
||||
|
||||
csr := &capi.CertificateSigningRequest{
|
||||
Spec: capi.CertificateSigningRequestSpec{
|
||||
Request: []byte(csrb),
|
||||
Usages: []capi.KeyUsage{
|
||||
capi.UsageSigning,
|
||||
capi.UsageKeyEncipherment,
|
||||
capi.UsageServerAuth,
|
||||
capi.UsageClientAuth,
|
||||
},
|
||||
},
|
||||
x509cr, err := capihelper.ParseCSR(csrb)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to parse CSR: %v", err)
|
||||
}
|
||||
|
||||
csr, err = s.sign(csr)
|
||||
certData, err := s.sign(x509cr, []capi.KeyUsage{
|
||||
capi.UsageSigning,
|
||||
capi.UsageKeyEncipherment,
|
||||
capi.UsageServerAuth,
|
||||
capi.UsageClientAuth,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("failed to sign CSR: %v", err)
|
||||
}
|
||||
certData := csr.Status.Certificate
|
||||
if len(certData) == 0 {
|
||||
t.Fatalf("expected a certificate after signing")
|
||||
}
|
||||
@ -99,3 +103,207 @@ func TestSigner(t *testing.T) {
|
||||
t.Errorf("unexpected diff: %v", cmp.Diff(certs[0], want, diff.IgnoreUnset()))
|
||||
}
|
||||
}
|
||||
|
||||
func TestHandle(t *testing.T) {
|
||||
cases := []struct {
|
||||
name string
|
||||
// parameters to be set on the generated CSR
|
||||
commonName string
|
||||
dnsNames []string
|
||||
org []string
|
||||
usages []capi.KeyUsage
|
||||
// whether the generated CSR should be marked as approved
|
||||
approved bool
|
||||
// the signerName to be set on the generated CSR
|
||||
signerName string
|
||||
// if true, expect an error to be returned
|
||||
err bool
|
||||
// additional verification function
|
||||
verify func(*testing.T, []testclient.Action)
|
||||
}{
|
||||
{
|
||||
name: "should sign if signerName is kubernetes.io/kube-apiserver-client",
|
||||
signerName: "kubernetes.io/kube-apiserver-client",
|
||||
commonName: "hello-world",
|
||||
org: []string{"some-org"},
|
||||
usages: []capi.KeyUsage{capi.UsageClientAuth, capi.UsageDigitalSignature, capi.UsageKeyEncipherment},
|
||||
approved: true,
|
||||
verify: func(t *testing.T, as []testclient.Action) {
|
||||
if len(as) != 1 {
|
||||
t.Errorf("expected one Update action but got %d", len(as))
|
||||
return
|
||||
}
|
||||
csr := as[0].(testclient.UpdateAction).GetObject().(*capi.CertificateSigningRequest)
|
||||
if len(csr.Status.Certificate) == 0 {
|
||||
t.Errorf("expected certificate to be issued but it was not")
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "should refuse to sign if signerName is kubernetes.io/kube-apiserver-client and contains an unexpected usage",
|
||||
signerName: "kubernetes.io/kube-apiserver-client",
|
||||
commonName: "hello-world",
|
||||
org: []string{"some-org"},
|
||||
usages: []capi.KeyUsage{capi.UsageServerAuth, capi.UsageClientAuth, capi.UsageDigitalSignature, capi.UsageKeyEncipherment},
|
||||
approved: true,
|
||||
verify: func(t *testing.T, as []testclient.Action) {
|
||||
if len(as) != 0 {
|
||||
t.Errorf("expected no Update action but got %d", len(as))
|
||||
return
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "should sign if signerName is kubernetes.io/kube-apiserver-client-kubelet",
|
||||
signerName: "kubernetes.io/kube-apiserver-client-kubelet",
|
||||
commonName: "system:node:hello-world",
|
||||
org: []string{"system:nodes"},
|
||||
usages: []capi.KeyUsage{capi.UsageClientAuth, capi.UsageDigitalSignature, capi.UsageKeyEncipherment},
|
||||
approved: true,
|
||||
verify: func(t *testing.T, as []testclient.Action) {
|
||||
if len(as) != 1 {
|
||||
t.Errorf("expected one Update action but got %d", len(as))
|
||||
return
|
||||
}
|
||||
csr := as[0].(testclient.UpdateAction).GetObject().(*capi.CertificateSigningRequest)
|
||||
if len(csr.Status.Certificate) == 0 {
|
||||
t.Errorf("expected certificate to be issued but it was not")
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "should sign if signerName is kubernetes.io/legacy-unknown",
|
||||
signerName: "kubernetes.io/legacy-unknown",
|
||||
approved: true,
|
||||
verify: func(t *testing.T, as []testclient.Action) {
|
||||
if len(as) != 1 {
|
||||
t.Errorf("expected one Update action but got %d", len(as))
|
||||
return
|
||||
}
|
||||
csr := as[0].(testclient.UpdateAction).GetObject().(*capi.CertificateSigningRequest)
|
||||
if len(csr.Status.Certificate) == 0 {
|
||||
t.Errorf("expected certificate to be issued but it was not")
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "should sign if signerName is kubernetes.io/kubelet-serving",
|
||||
signerName: "kubernetes.io/kubelet-serving",
|
||||
commonName: "system:node:testnode",
|
||||
org: []string{"system:nodes"},
|
||||
usages: []capi.KeyUsage{capi.UsageServerAuth, capi.UsageDigitalSignature, capi.UsageKeyEncipherment},
|
||||
dnsNames: []string{"example.com"},
|
||||
approved: true,
|
||||
verify: func(t *testing.T, as []testclient.Action) {
|
||||
if len(as) != 1 {
|
||||
t.Errorf("expected one Update action but got %d", len(as))
|
||||
return
|
||||
}
|
||||
csr := as[0].(testclient.UpdateAction).GetObject().(*capi.CertificateSigningRequest)
|
||||
if len(csr.Status.Certificate) == 0 {
|
||||
t.Errorf("expected certificate to be issued but it was not")
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "should do nothing if an unrecognised signerName is used",
|
||||
signerName: "kubernetes.io/not-recognised",
|
||||
approved: true,
|
||||
verify: func(t *testing.T, as []testclient.Action) {
|
||||
if len(as) != 0 {
|
||||
t.Errorf("expected no action to be taken")
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "should do nothing if not approved",
|
||||
signerName: "kubernetes.io/kubelet-serving",
|
||||
verify: func(t *testing.T, as []testclient.Action) {
|
||||
if len(as) != 0 {
|
||||
t.Errorf("expected no action to be taken")
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "should do nothing if signerName does not start with kubernetes.io",
|
||||
signerName: "example.com/sample-name",
|
||||
approved: true,
|
||||
verify: func(t *testing.T, as []testclient.Action) {
|
||||
if len(as) != 0 {
|
||||
t.Errorf("expected no action to be taken")
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "should do nothing if signerName starts with kubernetes.io but is unrecognised",
|
||||
signerName: "kubernetes.io/not-a-real-signer",
|
||||
approved: true,
|
||||
verify: func(t *testing.T, as []testclient.Action) {
|
||||
if len(as) != 0 {
|
||||
t.Errorf("expected no action to be taken")
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, c := range cases {
|
||||
t.Run(c.name, func(t *testing.T) {
|
||||
client := &fake.Clientset{}
|
||||
s, err := newSigner("./testdata/ca.crt", "./testdata/ca.key", client, 1*time.Hour)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create signer: %v", err)
|
||||
}
|
||||
|
||||
csr := makeTestCSR(csrBuilder{cn: c.commonName, signerName: c.signerName, approved: c.approved, usages: c.usages, org: c.org, dnsNames: c.dnsNames})
|
||||
if err := s.handle(csr); err != nil && !c.err {
|
||||
t.Errorf("unexpected err: %v", err)
|
||||
}
|
||||
c.verify(t, client.Actions())
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// noncryptographic for faster testing
|
||||
// DO NOT COPY THIS CODE
|
||||
var insecureRand = rand.New(rand.NewSource(0))
|
||||
|
||||
type csrBuilder struct {
|
||||
cn string
|
||||
dnsNames []string
|
||||
org []string
|
||||
signerName string
|
||||
approved bool
|
||||
usages []capi.KeyUsage
|
||||
}
|
||||
|
||||
func makeTestCSR(b csrBuilder) *capi.CertificateSigningRequest {
|
||||
pk, err := ecdsa.GenerateKey(elliptic.P256(), insecureRand)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
csrb, err := x509.CreateCertificateRequest(insecureRand, &x509.CertificateRequest{
|
||||
Subject: pkix.Name{
|
||||
CommonName: b.cn,
|
||||
Organization: b.org,
|
||||
},
|
||||
DNSNames: b.dnsNames,
|
||||
}, pk)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
csr := &capi.CertificateSigningRequest{
|
||||
Spec: capi.CertificateSigningRequestSpec{
|
||||
Request: pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE REQUEST", Bytes: csrb}),
|
||||
Usages: b.usages,
|
||||
},
|
||||
}
|
||||
if b.signerName != "" {
|
||||
csr.Spec.SignerName = &b.signerName
|
||||
}
|
||||
if b.approved {
|
||||
csr.Status.Conditions = append(csr.Status.Conditions, capi.CertificateSigningRequestCondition{
|
||||
Type: capi.CertificateApproved,
|
||||
})
|
||||
}
|
||||
return csr
|
||||
}
|
||||
|
@ -26,6 +26,9 @@ go_library(
|
||||
"//plugin/pkg/admission/admit:go_default_library",
|
||||
"//plugin/pkg/admission/alwayspullimages:go_default_library",
|
||||
"//plugin/pkg/admission/antiaffinity:go_default_library",
|
||||
"//plugin/pkg/admission/certificates/approval:go_default_library",
|
||||
"//plugin/pkg/admission/certificates/signing:go_default_library",
|
||||
"//plugin/pkg/admission/certificates/subjectrestriction:go_default_library",
|
||||
"//plugin/pkg/admission/defaulttolerationseconds:go_default_library",
|
||||
"//plugin/pkg/admission/deny:go_default_library",
|
||||
"//plugin/pkg/admission/eventratelimit:go_default_library",
|
||||
|
@ -24,6 +24,9 @@ import (
|
||||
"k8s.io/kubernetes/plugin/pkg/admission/admit"
|
||||
"k8s.io/kubernetes/plugin/pkg/admission/alwayspullimages"
|
||||
"k8s.io/kubernetes/plugin/pkg/admission/antiaffinity"
|
||||
certapproval "k8s.io/kubernetes/plugin/pkg/admission/certificates/approval"
|
||||
certsigning "k8s.io/kubernetes/plugin/pkg/admission/certificates/signing"
|
||||
certsubjectrestriction "k8s.io/kubernetes/plugin/pkg/admission/certificates/subjectrestriction"
|
||||
"k8s.io/kubernetes/plugin/pkg/admission/defaulttolerationseconds"
|
||||
"k8s.io/kubernetes/plugin/pkg/admission/deny"
|
||||
"k8s.io/kubernetes/plugin/pkg/admission/eventratelimit"
|
||||
@ -87,6 +90,9 @@ var AllOrderedPlugins = []string{
|
||||
gc.PluginName, // OwnerReferencesPermissionEnforcement
|
||||
resize.PluginName, // PersistentVolumeClaimResize
|
||||
runtimeclass.PluginName, // RuntimeClass
|
||||
certapproval.PluginName, // CertificateApproval
|
||||
certsigning.PluginName, // CertificateSigning
|
||||
certsubjectrestriction.PluginName, // CertificateSubjectRestriction
|
||||
|
||||
// new admission plugins should generally be inserted above here
|
||||
// webhook, resourcequota, and deny plugins must go at the end
|
||||
@ -128,6 +134,9 @@ func RegisterAllAdmissionPlugins(plugins *admission.Plugins) {
|
||||
setdefault.Register(plugins)
|
||||
resize.Register(plugins)
|
||||
storageobjectinuseprotection.Register(plugins)
|
||||
certapproval.Register(plugins)
|
||||
certsigning.Register(plugins)
|
||||
certsubjectrestriction.Register(plugins)
|
||||
}
|
||||
|
||||
// DefaultOffAdmissionPlugins get admission plugins off by default for kube-apiserver.
|
||||
@ -146,6 +155,9 @@ func DefaultOffAdmissionPlugins() sets.String {
|
||||
podpriority.PluginName, //PodPriority
|
||||
nodetaint.PluginName, //TaintNodesByCondition
|
||||
runtimeclass.PluginName, //RuntimeClass, gates internally on the feature
|
||||
certapproval.PluginName, // CertificateApproval
|
||||
certsigning.PluginName, // CertificateSigning
|
||||
certsubjectrestriction.PluginName, // CertificateSubjectRestriction
|
||||
)
|
||||
|
||||
return sets.NewString(AllOrderedPlugins...).Difference(defaultOnPlugins)
|
||||
|
@ -344,7 +344,7 @@ func requestNodeCertificate(client certificatesv1beta1.CertificateSigningRequest
|
||||
return nil, err
|
||||
}
|
||||
|
||||
req, err := csr.RequestCertificate(client, csrData, name, usages, privateKey)
|
||||
req, err := csr.RequestCertificate(client, csrData, name, certificates.KubeAPIServerClientKubeletSignerName, usages, privateKey)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
@ -107,6 +107,7 @@ func NewKubeletServerCertificateManager(kubeClient clientset.Interface, kubeCfg
|
||||
return certSigningRequestClient, nil
|
||||
},
|
||||
GetTemplate: getTemplate,
|
||||
SignerName: certificates.KubeletServingSignerName,
|
||||
Usages: []certificates.KeyUsage{
|
||||
// https://tools.ietf.org/html/rfc5280#section-4.2.1.3
|
||||
//
|
||||
@ -238,6 +239,7 @@ func NewKubeletClientCertificateManager(
|
||||
Organization: []string{"system:nodes"},
|
||||
},
|
||||
},
|
||||
SignerName: certificates.KubeAPIServerClientKubeletSignerName,
|
||||
Usages: []certificates.KeyUsage{
|
||||
// https://tools.ietf.org/html/rfc5280#section-4.2.1.3
|
||||
//
|
||||
|
@ -397,6 +397,7 @@ func AddHandlers(h printers.PrintHandler) {
|
||||
certificateSigningRequestColumnDefinitions := []metav1.TableColumnDefinition{
|
||||
{Name: "Name", Type: "string", Format: "name", Description: metav1.ObjectMeta{}.SwaggerDoc()["name"]},
|
||||
{Name: "Age", Type: "string", Description: metav1.ObjectMeta{}.SwaggerDoc()["creationTimestamp"]},
|
||||
{Name: "SignerName", Type: "string", Description: certificatesv1beta1.CertificateSigningRequestSpec{}.SwaggerDoc()["signerName"]},
|
||||
{Name: "Requestor", Type: "string", Description: certificatesv1beta1.CertificateSigningRequestSpec{}.SwaggerDoc()["request"]},
|
||||
{Name: "Condition", Type: "string", Description: certificatesv1beta1.CertificateSigningRequestStatus{}.SwaggerDoc()["conditions"]},
|
||||
}
|
||||
@ -1716,7 +1717,11 @@ func printCertificateSigningRequest(obj *certificates.CertificateSigningRequest,
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
row.Cells = append(row.Cells, obj.Name, translateTimestampSince(obj.CreationTimestamp), obj.Spec.Username, status)
|
||||
signerName := "<none>"
|
||||
if obj.Spec.SignerName != "" {
|
||||
signerName = obj.Spec.SignerName
|
||||
}
|
||||
row.Cells = append(row.Cells, obj.Name, translateTimestampSince(obj.CreationTimestamp), signerName, obj.Spec.Username, status)
|
||||
return []metav1.TableRow{row}, nil
|
||||
}
|
||||
|
||||
|
@ -3417,7 +3417,7 @@ func TestPrintCertificateSigningRequest(t *testing.T) {
|
||||
Status: certificates.CertificateSigningRequestStatus{},
|
||||
},
|
||||
// Columns: Name, Age, Requestor, Condition
|
||||
expected: []metav1.TableRow{{Cells: []interface{}{"csr1", "0s", "", "Pending"}}},
|
||||
expected: []metav1.TableRow{{Cells: []interface{}{"csr1", "0s", "<none>", "", "Pending"}}},
|
||||
},
|
||||
// Basic CSR with Spec and Status=Approved.
|
||||
{
|
||||
@ -3438,7 +3438,29 @@ func TestPrintCertificateSigningRequest(t *testing.T) {
|
||||
},
|
||||
},
|
||||
// Columns: Name, Age, Requestor, Condition
|
||||
expected: []metav1.TableRow{{Cells: []interface{}{"csr2", "0s", "CSR Requestor", "Approved"}}},
|
||||
expected: []metav1.TableRow{{Cells: []interface{}{"csr2", "0s", "<none>", "CSR Requestor", "Approved"}}},
|
||||
},
|
||||
// Basic CSR with Spec and SignerName set
|
||||
{
|
||||
csr: certificates.CertificateSigningRequest{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "csr2",
|
||||
CreationTimestamp: metav1.Time{Time: time.Now().Add(1.9e9)},
|
||||
},
|
||||
Spec: certificates.CertificateSigningRequestSpec{
|
||||
Username: "CSR Requestor",
|
||||
SignerName: "example.com/test-signer",
|
||||
},
|
||||
Status: certificates.CertificateSigningRequestStatus{
|
||||
Conditions: []certificates.CertificateSigningRequestCondition{
|
||||
{
|
||||
Type: certificates.CertificateApproved,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
// Columns: Name, Age, Requestor, Condition
|
||||
expected: []metav1.TableRow{{Cells: []interface{}{"csr2", "0s", "example.com/test-signer", "CSR Requestor", "Approved"}}},
|
||||
},
|
||||
// Basic CSR with Spec and Status=Approved; certificate issued.
|
||||
{
|
||||
@ -3460,7 +3482,7 @@ func TestPrintCertificateSigningRequest(t *testing.T) {
|
||||
},
|
||||
},
|
||||
// Columns: Name, Age, Requestor, Condition
|
||||
expected: []metav1.TableRow{{Cells: []interface{}{"csr2", "0s", "CSR Requestor", "Approved,Issued"}}},
|
||||
expected: []metav1.TableRow{{Cells: []interface{}{"csr2", "0s", "<none>", "CSR Requestor", "Approved,Issued"}}},
|
||||
},
|
||||
// Basic CSR with Spec and Status=Denied.
|
||||
{
|
||||
@ -3481,7 +3503,7 @@ func TestPrintCertificateSigningRequest(t *testing.T) {
|
||||
},
|
||||
},
|
||||
// Columns: Name, Age, Requestor, Condition
|
||||
expected: []metav1.TableRow{{Cells: []interface{}{"csr3", "0s", "CSR Requestor", "Denied"}}},
|
||||
expected: []metav1.TableRow{{Cells: []interface{}{"csr3", "0s", "<none>", "CSR Requestor", "Denied"}}},
|
||||
},
|
||||
}
|
||||
|
||||
|
@ -18,9 +18,12 @@ go_library(
|
||||
"//pkg/apis/certificates:go_default_library",
|
||||
"//pkg/apis/certificates/validation:go_default_library",
|
||||
"//staging/src/k8s.io/apimachinery/pkg/apis/meta/v1:go_default_library",
|
||||
"//staging/src/k8s.io/apimachinery/pkg/fields:go_default_library",
|
||||
"//staging/src/k8s.io/apimachinery/pkg/labels:go_default_library",
|
||||
"//staging/src/k8s.io/apimachinery/pkg/runtime:go_default_library",
|
||||
"//staging/src/k8s.io/apimachinery/pkg/util/validation/field:go_default_library",
|
||||
"//staging/src/k8s.io/apiserver/pkg/endpoints/request:go_default_library",
|
||||
"//staging/src/k8s.io/apiserver/pkg/registry/generic:go_default_library",
|
||||
"//staging/src/k8s.io/apiserver/pkg/storage/names:go_default_library",
|
||||
],
|
||||
)
|
||||
|
@ -50,7 +50,7 @@ func NewREST(optsGetter generic.RESTOptionsGetter) (*REST, *StatusREST, *Approva
|
||||
|
||||
TableConvertor: printerstorage.TableConvertor{TableGenerator: printers.NewTableGenerator().With(printersinternal.AddHandlers)},
|
||||
}
|
||||
options := &generic.StoreOptions{RESTOptions: optsGetter}
|
||||
options := &generic.StoreOptions{RESTOptions: optsGetter, AttrFunc: csrregistry.GetAttrs}
|
||||
if err := store.CompleteWithOptions(options); err != nil {
|
||||
return nil, nil, nil, err
|
||||
}
|
||||
|
@ -21,9 +21,12 @@ import (
|
||||
"fmt"
|
||||
|
||||
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/apimachinery/pkg/util/validation/field"
|
||||
genericapirequest "k8s.io/apiserver/pkg/endpoints/request"
|
||||
"k8s.io/apiserver/pkg/registry/generic"
|
||||
"k8s.io/apiserver/pkg/storage/names"
|
||||
"k8s.io/kubernetes/pkg/api/legacyscheme"
|
||||
"k8s.io/kubernetes/pkg/apis/certificates"
|
||||
@ -190,3 +193,21 @@ func (csrApprovalStrategy) PrepareForUpdate(ctx context.Context, obj, old runtim
|
||||
func (csrApprovalStrategy) ValidateUpdate(ctx context.Context, obj, old runtime.Object) field.ErrorList {
|
||||
return validation.ValidateCertificateSigningRequestUpdate(obj.(*certificates.CertificateSigningRequest), old.(*certificates.CertificateSigningRequest))
|
||||
}
|
||||
|
||||
// GetAttrs returns labels and fields of a given object for filtering purposes.
|
||||
func GetAttrs(obj runtime.Object) (labels.Set, fields.Set, error) {
|
||||
csr, ok := obj.(*certificates.CertificateSigningRequest)
|
||||
if !ok {
|
||||
return nil, nil, fmt.Errorf("not a certificatesigningrequest")
|
||||
}
|
||||
return labels.Set(csr.Labels), SelectableFields(csr), nil
|
||||
}
|
||||
|
||||
// SelectableFields returns a field set that can be used for filter selection
|
||||
func SelectableFields(obj *certificates.CertificateSigningRequest) fields.Set {
|
||||
objectMetaFieldsSet := generic.ObjectMetaFieldsSet(&obj.ObjectMeta, false)
|
||||
csrSpecificFieldsSet := fields.Set{
|
||||
"spec.signerName": obj.Spec.SignerName,
|
||||
}
|
||||
return generic.MergeFieldsSets(objectMetaFieldsSet, csrSpecificFieldsSet)
|
||||
}
|
||||
|
@ -14,6 +14,7 @@ filegroup(
|
||||
"//plugin/pkg/admission/admit:all-srcs",
|
||||
"//plugin/pkg/admission/alwayspullimages:all-srcs",
|
||||
"//plugin/pkg/admission/antiaffinity:all-srcs",
|
||||
"//plugin/pkg/admission/certificates:all-srcs",
|
||||
"//plugin/pkg/admission/defaulttolerationseconds:all-srcs",
|
||||
"//plugin/pkg/admission/deny:all-srcs",
|
||||
"//plugin/pkg/admission/eventratelimit:all-srcs",
|
||||
|
32
plugin/pkg/admission/certificates/BUILD
Normal file
32
plugin/pkg/admission/certificates/BUILD
Normal file
@ -0,0 +1,32 @@
|
||||
load("@io_bazel_rules_go//go:def.bzl", "go_library")
|
||||
|
||||
package(default_visibility = ["//visibility:public"])
|
||||
|
||||
filegroup(
|
||||
name = "package-srcs",
|
||||
srcs = glob(["**"]),
|
||||
tags = ["automanaged"],
|
||||
visibility = ["//visibility:private"],
|
||||
)
|
||||
|
||||
filegroup(
|
||||
name = "all-srcs",
|
||||
srcs = [
|
||||
":package-srcs",
|
||||
"//plugin/pkg/admission/certificates/approval:all-srcs",
|
||||
"//plugin/pkg/admission/certificates/signing:all-srcs",
|
||||
"//plugin/pkg/admission/certificates/subjectrestriction:all-srcs",
|
||||
],
|
||||
tags = ["automanaged"],
|
||||
)
|
||||
|
||||
go_library(
|
||||
name = "go_default_library",
|
||||
srcs = ["util.go"],
|
||||
importpath = "k8s.io/kubernetes/plugin/pkg/admission/certificates",
|
||||
deps = [
|
||||
"//staging/src/k8s.io/apiserver/pkg/authentication/user:go_default_library",
|
||||
"//staging/src/k8s.io/apiserver/pkg/authorization/authorizer:go_default_library",
|
||||
"//vendor/k8s.io/klog:go_default_library",
|
||||
],
|
||||
)
|
8
plugin/pkg/admission/certificates/OWNERS
Normal file
8
plugin/pkg/admission/certificates/OWNERS
Normal file
@ -0,0 +1,8 @@
|
||||
# See the OWNERS docs at https://go.k8s.io/owners
|
||||
|
||||
approvers:
|
||||
- sig-auth-certificates-approvers
|
||||
reviewers:
|
||||
- sig-auth-certificates-approvers
|
||||
labels:
|
||||
- sig/auth
|
44
plugin/pkg/admission/certificates/approval/BUILD
Normal file
44
plugin/pkg/admission/certificates/approval/BUILD
Normal file
@ -0,0 +1,44 @@
|
||||
load("@io_bazel_rules_go//go:def.bzl", "go_library", "go_test")
|
||||
|
||||
go_library(
|
||||
name = "go_default_library",
|
||||
srcs = ["admission.go"],
|
||||
importpath = "k8s.io/kubernetes/plugin/pkg/admission/certificates/approval",
|
||||
visibility = ["//visibility:public"],
|
||||
deps = [
|
||||
"//pkg/apis/certificates:go_default_library",
|
||||
"//plugin/pkg/admission/certificates:go_default_library",
|
||||
"//staging/src/k8s.io/apiserver/pkg/admission:go_default_library",
|
||||
"//staging/src/k8s.io/apiserver/pkg/admission/initializer:go_default_library",
|
||||
"//staging/src/k8s.io/apiserver/pkg/authorization/authorizer:go_default_library",
|
||||
"//vendor/k8s.io/klog:go_default_library",
|
||||
],
|
||||
)
|
||||
|
||||
go_test(
|
||||
name = "go_default_test",
|
||||
srcs = ["admission_test.go"],
|
||||
embed = [":go_default_library"],
|
||||
deps = [
|
||||
"//pkg/apis/certificates:go_default_library",
|
||||
"//staging/src/k8s.io/apimachinery/pkg/runtime:go_default_library",
|
||||
"//staging/src/k8s.io/apimachinery/pkg/runtime/schema:go_default_library",
|
||||
"//staging/src/k8s.io/apiserver/pkg/admission:go_default_library",
|
||||
"//staging/src/k8s.io/apiserver/pkg/authentication/user:go_default_library",
|
||||
"//staging/src/k8s.io/apiserver/pkg/authorization/authorizer:go_default_library",
|
||||
],
|
||||
)
|
||||
|
||||
filegroup(
|
||||
name = "package-srcs",
|
||||
srcs = glob(["**"]),
|
||||
tags = ["automanaged"],
|
||||
visibility = ["//visibility:private"],
|
||||
)
|
||||
|
||||
filegroup(
|
||||
name = "all-srcs",
|
||||
srcs = [":package-srcs"],
|
||||
tags = ["automanaged"],
|
||||
visibility = ["//visibility:public"],
|
||||
)
|
99
plugin/pkg/admission/certificates/approval/admission.go
Normal file
99
plugin/pkg/admission/certificates/approval/admission.go
Normal file
@ -0,0 +1,99 @@
|
||||
/*
|
||||
Copyright 2020 The Kubernetes Authors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package approval
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
|
||||
"k8s.io/klog"
|
||||
|
||||
"k8s.io/apiserver/pkg/admission"
|
||||
genericadmissioninit "k8s.io/apiserver/pkg/admission/initializer"
|
||||
"k8s.io/apiserver/pkg/authorization/authorizer"
|
||||
|
||||
api "k8s.io/kubernetes/pkg/apis/certificates"
|
||||
"k8s.io/kubernetes/plugin/pkg/admission/certificates"
|
||||
)
|
||||
|
||||
// PluginName is a string with the name of the plugin
|
||||
const PluginName = "CertificateApproval"
|
||||
|
||||
// Register registers a plugin
|
||||
func Register(plugins *admission.Plugins) {
|
||||
plugins.Register(PluginName, func(config io.Reader) (admission.Interface, error) {
|
||||
return NewPlugin(), nil
|
||||
})
|
||||
}
|
||||
|
||||
// Plugin holds state for and implements the admission plugin.
|
||||
type Plugin struct {
|
||||
*admission.Handler
|
||||
authz authorizer.Authorizer
|
||||
}
|
||||
|
||||
// SetAuthorizer sets the authorizer.
|
||||
func (p *Plugin) SetAuthorizer(authz authorizer.Authorizer) {
|
||||
p.authz = authz
|
||||
}
|
||||
|
||||
// ValidateInitialization ensures an authorizer is set.
|
||||
func (p *Plugin) ValidateInitialization() error {
|
||||
if p.authz == nil {
|
||||
return fmt.Errorf("%s requires an authorizer", PluginName)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
var _ admission.ValidationInterface = &Plugin{}
|
||||
var _ genericadmissioninit.WantsAuthorizer = &Plugin{}
|
||||
|
||||
// NewPlugin creates a new CSR approval admission plugin
|
||||
func NewPlugin() *Plugin {
|
||||
return &Plugin{
|
||||
Handler: admission.NewHandler(admission.Update),
|
||||
}
|
||||
}
|
||||
|
||||
var csrGroupResource = api.Resource("certificatesigningrequests")
|
||||
|
||||
// Validate verifies that the requesting user has permission to approve
|
||||
// CertificateSigningRequests for the specified signerName.
|
||||
func (p *Plugin) Validate(ctx context.Context, a admission.Attributes, _ admission.ObjectInterfaces) error {
|
||||
// Ignore all calls to anything other than 'certificatesigningrequests/approval'.
|
||||
// Ignore all operations other than UPDATE.
|
||||
if a.GetSubresource() != "approval" ||
|
||||
a.GetResource().GroupResource() != csrGroupResource {
|
||||
return nil
|
||||
}
|
||||
|
||||
// We check permissions against the *old* version of the resource, in case
|
||||
// a user is attempting to update the SignerName when calling the approval
|
||||
// endpoint (which is an invalid/not allowed operation)
|
||||
csr, ok := a.GetOldObject().(*api.CertificateSigningRequest)
|
||||
if !ok {
|
||||
return admission.NewForbidden(a, fmt.Errorf("expected type CertificateSigningRequest, got: %T", a.GetOldObject()))
|
||||
}
|
||||
|
||||
if !certificates.IsAuthorizedForSignerName(ctx, p.authz, a.GetUserInfo(), "approve", csr.Spec.SignerName) {
|
||||
klog.V(4).Infof("user not permitted to approve CertificateSigningRequest %q with signerName %q", csr.Name, csr.Spec.SignerName)
|
||||
return admission.NewForbidden(a, fmt.Errorf("user not permitted to approve requests with signerName %q", csr.Spec.SignerName))
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
206
plugin/pkg/admission/certificates/approval/admission_test.go
Normal file
206
plugin/pkg/admission/certificates/approval/admission_test.go
Normal file
@ -0,0 +1,206 @@
|
||||
/*
|
||||
Copyright 2020 The Kubernetes Authors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package approval
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
"k8s.io/apiserver/pkg/admission"
|
||||
"k8s.io/apiserver/pkg/authentication/user"
|
||||
"k8s.io/apiserver/pkg/authorization/authorizer"
|
||||
|
||||
certificatesapi "k8s.io/kubernetes/pkg/apis/certificates"
|
||||
)
|
||||
|
||||
func TestPlugin_Validate(t *testing.T) {
|
||||
tests := map[string]struct {
|
||||
attributes admission.Attributes
|
||||
allowedName string
|
||||
allowed bool
|
||||
authzErr error
|
||||
}{
|
||||
"wrong type": {
|
||||
attributes: &testAttributes{
|
||||
resource: certificatesapi.Resource("certificatesigningrequests"),
|
||||
subresource: "approval",
|
||||
oldObj: &certificatesapi.CertificateSigningRequestList{},
|
||||
operation: admission.Update,
|
||||
},
|
||||
allowed: false,
|
||||
},
|
||||
"reject requests if looking up permissions fails": {
|
||||
attributes: &testAttributes{
|
||||
resource: certificatesapi.Resource("certificatesigningrequests"),
|
||||
subresource: "approval",
|
||||
oldObj: &certificatesapi.CertificateSigningRequest{Spec: certificatesapi.CertificateSigningRequestSpec{
|
||||
SignerName: "abc.com/xyz",
|
||||
}},
|
||||
operation: admission.Update,
|
||||
},
|
||||
authzErr: errors.New("forced error"),
|
||||
allowed: false,
|
||||
},
|
||||
"should allow request if user is authorized for specific signerName": {
|
||||
allowedName: "abc.com/xyz",
|
||||
attributes: &testAttributes{
|
||||
resource: certificatesapi.Resource("certificatesigningrequests"),
|
||||
subresource: "approval",
|
||||
oldObj: &certificatesapi.CertificateSigningRequest{Spec: certificatesapi.CertificateSigningRequestSpec{
|
||||
SignerName: "abc.com/xyz",
|
||||
}},
|
||||
operation: admission.Update,
|
||||
},
|
||||
allowed: true,
|
||||
},
|
||||
"should allow request if user is authorized with wildcard": {
|
||||
allowedName: "abc.com/*",
|
||||
attributes: &testAttributes{
|
||||
resource: certificatesapi.Resource("certificatesigningrequests"),
|
||||
subresource: "approval",
|
||||
oldObj: &certificatesapi.CertificateSigningRequest{Spec: certificatesapi.CertificateSigningRequestSpec{
|
||||
SignerName: "abc.com/xyz",
|
||||
}},
|
||||
operation: admission.Update,
|
||||
},
|
||||
allowed: true,
|
||||
},
|
||||
"should deny request if user does not have permission for this signerName": {
|
||||
allowedName: "notabc.com/xyz",
|
||||
attributes: &testAttributes{
|
||||
resource: certificatesapi.Resource("certificatesigningrequests"),
|
||||
subresource: "approval",
|
||||
oldObj: &certificatesapi.CertificateSigningRequest{Spec: certificatesapi.CertificateSigningRequestSpec{
|
||||
SignerName: "abc.com/xyz",
|
||||
}},
|
||||
operation: admission.Update,
|
||||
},
|
||||
allowed: false,
|
||||
},
|
||||
"should deny request if user attempts to update signerName to a new value they *do* have permission to approve for": {
|
||||
allowedName: "allowed.com/xyz",
|
||||
attributes: &testAttributes{
|
||||
resource: certificatesapi.Resource("certificatesigningrequests"),
|
||||
subresource: "approval",
|
||||
oldObj: &certificatesapi.CertificateSigningRequest{Spec: certificatesapi.CertificateSigningRequestSpec{
|
||||
SignerName: "notallowed.com/xyz",
|
||||
}},
|
||||
obj: &certificatesapi.CertificateSigningRequest{Spec: certificatesapi.CertificateSigningRequestSpec{
|
||||
SignerName: "allowed.com/xyz",
|
||||
}},
|
||||
operation: admission.Update,
|
||||
},
|
||||
allowed: false,
|
||||
},
|
||||
}
|
||||
|
||||
for n, test := range tests {
|
||||
t.Run(n, func(t *testing.T) {
|
||||
p := Plugin{
|
||||
authz: fakeAuthorizer{
|
||||
t: t,
|
||||
verb: "approve",
|
||||
allowedName: test.allowedName,
|
||||
decision: authorizer.DecisionAllow,
|
||||
err: test.authzErr,
|
||||
},
|
||||
}
|
||||
err := p.Validate(context.Background(), test.attributes, nil)
|
||||
if err == nil && !test.allowed {
|
||||
t.Errorf("Expected authorization policy to reject CSR but it was allowed")
|
||||
}
|
||||
if err != nil && test.allowed {
|
||||
t.Errorf("Expected authorization policy to accept CSR but it was rejected: %v", err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
type fakeAuthorizer struct {
|
||||
t *testing.T
|
||||
verb string
|
||||
allowedName string
|
||||
decision authorizer.Decision
|
||||
err error
|
||||
}
|
||||
|
||||
func (f fakeAuthorizer) Authorize(ctx context.Context, a authorizer.Attributes) (authorizer.Decision, string, error) {
|
||||
if f.err != nil {
|
||||
return f.decision, "forced error", f.err
|
||||
}
|
||||
if a.GetVerb() != f.verb {
|
||||
return authorizer.DecisionDeny, fmt.Sprintf("unrecognised verb '%s'", a.GetVerb()), nil
|
||||
}
|
||||
if a.GetAPIGroup() != "certificates.k8s.io" {
|
||||
return authorizer.DecisionDeny, fmt.Sprintf("unrecognised groupName '%s'", a.GetAPIGroup()), nil
|
||||
}
|
||||
if a.GetAPIVersion() != "*" {
|
||||
return authorizer.DecisionDeny, fmt.Sprintf("unrecognised apiVersion '%s'", a.GetAPIVersion()), nil
|
||||
}
|
||||
if a.GetResource() != "signers" {
|
||||
return authorizer.DecisionDeny, fmt.Sprintf("unrecognised resource '%s'", a.GetResource()), nil
|
||||
}
|
||||
if a.GetName() != f.allowedName {
|
||||
return authorizer.DecisionDeny, fmt.Sprintf("unrecognised resource name '%s'", a.GetName()), nil
|
||||
}
|
||||
if !a.IsResourceRequest() {
|
||||
return authorizer.DecisionDeny, fmt.Sprintf("unrecognised IsResourceRequest '%t'", a.IsResourceRequest()), nil
|
||||
}
|
||||
return f.decision, "", nil
|
||||
}
|
||||
|
||||
type testAttributes struct {
|
||||
resource schema.GroupResource
|
||||
subresource string
|
||||
operation admission.Operation
|
||||
obj, oldObj runtime.Object
|
||||
name string
|
||||
|
||||
admission.Attributes // nil panic if any other methods called
|
||||
}
|
||||
|
||||
func (t *testAttributes) GetResource() schema.GroupVersionResource {
|
||||
return t.resource.WithVersion("ignored")
|
||||
}
|
||||
|
||||
func (t *testAttributes) GetSubresource() string {
|
||||
return t.subresource
|
||||
}
|
||||
|
||||
func (t *testAttributes) GetObject() runtime.Object {
|
||||
return t.obj
|
||||
}
|
||||
|
||||
func (t *testAttributes) GetOldObject() runtime.Object {
|
||||
return t.oldObj
|
||||
}
|
||||
|
||||
func (t *testAttributes) GetName() string {
|
||||
return t.name
|
||||
}
|
||||
|
||||
func (t *testAttributes) GetOperation() admission.Operation {
|
||||
return t.operation
|
||||
}
|
||||
|
||||
func (t *testAttributes) GetUserInfo() user.Info {
|
||||
return &user.DefaultInfo{Name: "ignored"}
|
||||
}
|
44
plugin/pkg/admission/certificates/signing/BUILD
Normal file
44
plugin/pkg/admission/certificates/signing/BUILD
Normal file
@ -0,0 +1,44 @@
|
||||
load("@io_bazel_rules_go//go:def.bzl", "go_library", "go_test")
|
||||
|
||||
go_library(
|
||||
name = "go_default_library",
|
||||
srcs = ["admission.go"],
|
||||
importpath = "k8s.io/kubernetes/plugin/pkg/admission/certificates/signing",
|
||||
visibility = ["//visibility:public"],
|
||||
deps = [
|
||||
"//pkg/apis/certificates:go_default_library",
|
||||
"//plugin/pkg/admission/certificates:go_default_library",
|
||||
"//staging/src/k8s.io/apiserver/pkg/admission:go_default_library",
|
||||
"//staging/src/k8s.io/apiserver/pkg/admission/initializer:go_default_library",
|
||||
"//staging/src/k8s.io/apiserver/pkg/authorization/authorizer:go_default_library",
|
||||
"//vendor/k8s.io/klog:go_default_library",
|
||||
],
|
||||
)
|
||||
|
||||
go_test(
|
||||
name = "go_default_test",
|
||||
srcs = ["admission_test.go"],
|
||||
embed = [":go_default_library"],
|
||||
deps = [
|
||||
"//pkg/apis/certificates:go_default_library",
|
||||
"//staging/src/k8s.io/apimachinery/pkg/runtime:go_default_library",
|
||||
"//staging/src/k8s.io/apimachinery/pkg/runtime/schema:go_default_library",
|
||||
"//staging/src/k8s.io/apiserver/pkg/admission:go_default_library",
|
||||
"//staging/src/k8s.io/apiserver/pkg/authentication/user:go_default_library",
|
||||
"//staging/src/k8s.io/apiserver/pkg/authorization/authorizer:go_default_library",
|
||||
],
|
||||
)
|
||||
|
||||
filegroup(
|
||||
name = "package-srcs",
|
||||
srcs = glob(["**"]),
|
||||
tags = ["automanaged"],
|
||||
visibility = ["//visibility:private"],
|
||||
)
|
||||
|
||||
filegroup(
|
||||
name = "all-srcs",
|
||||
srcs = [":package-srcs"],
|
||||
tags = ["automanaged"],
|
||||
visibility = ["//visibility:public"],
|
||||
)
|
106
plugin/pkg/admission/certificates/signing/admission.go
Normal file
106
plugin/pkg/admission/certificates/signing/admission.go
Normal file
@ -0,0 +1,106 @@
|
||||
/*
|
||||
Copyright 2020 The Kubernetes Authors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package signing
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"reflect"
|
||||
|
||||
"k8s.io/klog"
|
||||
|
||||
"k8s.io/apiserver/pkg/admission"
|
||||
genericadmissioninit "k8s.io/apiserver/pkg/admission/initializer"
|
||||
"k8s.io/apiserver/pkg/authorization/authorizer"
|
||||
|
||||
api "k8s.io/kubernetes/pkg/apis/certificates"
|
||||
"k8s.io/kubernetes/plugin/pkg/admission/certificates"
|
||||
)
|
||||
|
||||
// PluginName is a string with the name of the plugin
|
||||
const PluginName = "CertificateSigning"
|
||||
|
||||
// Register registers a plugin
|
||||
func Register(plugins *admission.Plugins) {
|
||||
plugins.Register(PluginName, func(config io.Reader) (admission.Interface, error) {
|
||||
return NewPlugin(), nil
|
||||
})
|
||||
}
|
||||
|
||||
// Plugin holds state for and implements the admission plugin.
|
||||
type Plugin struct {
|
||||
*admission.Handler
|
||||
authz authorizer.Authorizer
|
||||
}
|
||||
|
||||
// SetAuthorizer sets the authorizer.
|
||||
func (p *Plugin) SetAuthorizer(authz authorizer.Authorizer) {
|
||||
p.authz = authz
|
||||
}
|
||||
|
||||
// ValidateInitialization ensures an authorizer is set.
|
||||
func (p *Plugin) ValidateInitialization() error {
|
||||
if p.authz == nil {
|
||||
return fmt.Errorf("%s requires an authorizer", PluginName)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
var _ admission.ValidationInterface = &Plugin{}
|
||||
var _ genericadmissioninit.WantsAuthorizer = &Plugin{}
|
||||
|
||||
// NewPlugin creates a new CSR approval admission plugin
|
||||
func NewPlugin() *Plugin {
|
||||
return &Plugin{
|
||||
Handler: admission.NewHandler(admission.Update),
|
||||
}
|
||||
}
|
||||
|
||||
var csrGroupResource = api.Resource("certificatesigningrequests")
|
||||
|
||||
// Validate verifies that the requesting user has permission to approve
|
||||
// CertificateSigningRequests for the specified signerName.
|
||||
func (p *Plugin) Validate(ctx context.Context, a admission.Attributes, o admission.ObjectInterfaces) error {
|
||||
// Ignore all calls to anything other than 'certificatesigningrequests/approval'.
|
||||
// Ignore all operations other than UPDATE.
|
||||
if a.GetSubresource() != "status" ||
|
||||
a.GetResource().GroupResource() != csrGroupResource {
|
||||
return nil
|
||||
}
|
||||
|
||||
oldCSR, ok := a.GetOldObject().(*api.CertificateSigningRequest)
|
||||
if !ok {
|
||||
return admission.NewForbidden(a, fmt.Errorf("expected type CertificateSigningRequest, got: %T", a.GetOldObject()))
|
||||
}
|
||||
csr, ok := a.GetObject().(*api.CertificateSigningRequest)
|
||||
if !ok {
|
||||
return admission.NewForbidden(a, fmt.Errorf("expected type CertificateSigningRequest, got: %T", a.GetObject()))
|
||||
}
|
||||
|
||||
// only run if the status.certificate field has been changed
|
||||
if reflect.DeepEqual(oldCSR.Status.Certificate, csr.Status.Certificate) {
|
||||
return nil
|
||||
}
|
||||
|
||||
if !certificates.IsAuthorizedForSignerName(ctx, p.authz, a.GetUserInfo(), "sign", oldCSR.Spec.SignerName) {
|
||||
klog.V(4).Infof("user not permitted to sign CertificateSigningRequest %q with signerName %q", oldCSR.Name, oldCSR.Spec.SignerName)
|
||||
return admission.NewForbidden(a, fmt.Errorf("user not permitted to sign requests with signerName %q", oldCSR.Spec.SignerName))
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
260
plugin/pkg/admission/certificates/signing/admission_test.go
Normal file
260
plugin/pkg/admission/certificates/signing/admission_test.go
Normal file
@ -0,0 +1,260 @@
|
||||
/*
|
||||
Copyright 2020 The Kubernetes Authors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package signing
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
"k8s.io/apiserver/pkg/admission"
|
||||
"k8s.io/apiserver/pkg/authentication/user"
|
||||
"k8s.io/apiserver/pkg/authorization/authorizer"
|
||||
|
||||
certificatesapi "k8s.io/kubernetes/pkg/apis/certificates"
|
||||
)
|
||||
|
||||
func TestPlugin_Validate(t *testing.T) {
|
||||
tests := map[string]struct {
|
||||
attributes admission.Attributes
|
||||
allowedName string
|
||||
allowed bool
|
||||
authzErr error
|
||||
}{
|
||||
"wrong type": {
|
||||
attributes: &testAttributes{
|
||||
resource: certificatesapi.Resource("certificatesigningrequests"),
|
||||
subresource: "status",
|
||||
oldObj: &certificatesapi.CertificateSigningRequestList{},
|
||||
obj: &certificatesapi.CertificateSigningRequestList{},
|
||||
operation: admission.Update,
|
||||
},
|
||||
allowed: false,
|
||||
},
|
||||
"allowed if the 'certificate' field has not changed": {
|
||||
attributes: &testAttributes{
|
||||
resource: certificatesapi.Resource("certificatesigningrequests"),
|
||||
subresource: "status",
|
||||
oldObj: &certificatesapi.CertificateSigningRequest{Status: certificatesapi.CertificateSigningRequestStatus{
|
||||
Certificate: []byte("data"),
|
||||
}},
|
||||
obj: &certificatesapi.CertificateSigningRequest{Status: certificatesapi.CertificateSigningRequestStatus{
|
||||
Certificate: []byte("data"),
|
||||
}},
|
||||
operation: admission.Update,
|
||||
},
|
||||
allowed: true,
|
||||
authzErr: errors.New("faked error"),
|
||||
},
|
||||
"deny request if authz lookup fails": {
|
||||
allowedName: "abc.com/xyz",
|
||||
attributes: &testAttributes{
|
||||
resource: certificatesapi.Resource("certificatesigningrequests"),
|
||||
subresource: "status",
|
||||
oldObj: &certificatesapi.CertificateSigningRequest{Spec: certificatesapi.CertificateSigningRequestSpec{
|
||||
SignerName: "abc.com/xyz",
|
||||
}},
|
||||
obj: &certificatesapi.CertificateSigningRequest{
|
||||
Spec: certificatesapi.CertificateSigningRequestSpec{
|
||||
SignerName: "abc.com/xyz",
|
||||
},
|
||||
Status: certificatesapi.CertificateSigningRequestStatus{
|
||||
Certificate: []byte("data"),
|
||||
},
|
||||
},
|
||||
operation: admission.Update,
|
||||
},
|
||||
authzErr: errors.New("test"),
|
||||
allowed: false,
|
||||
},
|
||||
"allow request if user is authorized for specific signerName": {
|
||||
allowedName: "abc.com/xyz",
|
||||
attributes: &testAttributes{
|
||||
resource: certificatesapi.Resource("certificatesigningrequests"),
|
||||
subresource: "status",
|
||||
oldObj: &certificatesapi.CertificateSigningRequest{Spec: certificatesapi.CertificateSigningRequestSpec{
|
||||
SignerName: "abc.com/xyz",
|
||||
}},
|
||||
obj: &certificatesapi.CertificateSigningRequest{
|
||||
Spec: certificatesapi.CertificateSigningRequestSpec{
|
||||
SignerName: "abc.com/xyz",
|
||||
},
|
||||
Status: certificatesapi.CertificateSigningRequestStatus{
|
||||
Certificate: []byte("data"),
|
||||
},
|
||||
},
|
||||
operation: admission.Update,
|
||||
},
|
||||
allowed: true,
|
||||
},
|
||||
"allow request if user is authorized with wildcard": {
|
||||
allowedName: "abc.com/*",
|
||||
attributes: &testAttributes{
|
||||
resource: certificatesapi.Resource("certificatesigningrequests"),
|
||||
subresource: "status",
|
||||
oldObj: &certificatesapi.CertificateSigningRequest{Spec: certificatesapi.CertificateSigningRequestSpec{
|
||||
SignerName: "abc.com/xyz",
|
||||
}},
|
||||
obj: &certificatesapi.CertificateSigningRequest{
|
||||
Spec: certificatesapi.CertificateSigningRequestSpec{
|
||||
SignerName: "abc.com/xyz",
|
||||
},
|
||||
Status: certificatesapi.CertificateSigningRequestStatus{
|
||||
Certificate: []byte("data"),
|
||||
},
|
||||
},
|
||||
operation: admission.Update,
|
||||
},
|
||||
allowed: true,
|
||||
},
|
||||
"should deny request if user does not have permission for this signerName": {
|
||||
allowedName: "notabc.com/xyz",
|
||||
attributes: &testAttributes{
|
||||
resource: certificatesapi.Resource("certificatesigningrequests"),
|
||||
subresource: "status",
|
||||
oldObj: &certificatesapi.CertificateSigningRequest{Spec: certificatesapi.CertificateSigningRequestSpec{
|
||||
SignerName: "abc.com/xyz",
|
||||
}},
|
||||
obj: &certificatesapi.CertificateSigningRequest{
|
||||
Spec: certificatesapi.CertificateSigningRequestSpec{
|
||||
SignerName: "abc.com/xyz",
|
||||
},
|
||||
Status: certificatesapi.CertificateSigningRequestStatus{
|
||||
Certificate: []byte("data"),
|
||||
},
|
||||
},
|
||||
operation: admission.Update,
|
||||
},
|
||||
allowed: false,
|
||||
},
|
||||
"should deny request if user attempts to update signerName to a new value they *do* have permission to sign for": {
|
||||
allowedName: "allowed.com/xyz",
|
||||
attributes: &testAttributes{
|
||||
resource: certificatesapi.Resource("certificatesigningrequests"),
|
||||
subresource: "status",
|
||||
oldObj: &certificatesapi.CertificateSigningRequest{Spec: certificatesapi.CertificateSigningRequestSpec{
|
||||
SignerName: "notallowed.com/xyz",
|
||||
}},
|
||||
obj: &certificatesapi.CertificateSigningRequest{
|
||||
Spec: certificatesapi.CertificateSigningRequestSpec{
|
||||
SignerName: "allowed.com/xyz",
|
||||
},
|
||||
Status: certificatesapi.CertificateSigningRequestStatus{
|
||||
Certificate: []byte("data"),
|
||||
},
|
||||
},
|
||||
operation: admission.Update,
|
||||
},
|
||||
allowed: false,
|
||||
},
|
||||
}
|
||||
|
||||
for n, test := range tests {
|
||||
t.Run(n, func(t *testing.T) {
|
||||
p := Plugin{
|
||||
authz: fakeAuthorizer{
|
||||
t: t,
|
||||
verb: "sign",
|
||||
allowedName: test.allowedName,
|
||||
decision: authorizer.DecisionAllow,
|
||||
err: test.authzErr,
|
||||
},
|
||||
}
|
||||
err := p.Validate(context.Background(), test.attributes, nil)
|
||||
if err == nil && !test.allowed {
|
||||
t.Errorf("Expected authorization policy to reject CSR but it was allowed")
|
||||
}
|
||||
if err != nil && test.allowed {
|
||||
t.Errorf("Expected authorization policy to accept CSR but it was rejected: %v", err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
type fakeAuthorizer struct {
|
||||
t *testing.T
|
||||
verb string
|
||||
allowedName string
|
||||
decision authorizer.Decision
|
||||
err error
|
||||
}
|
||||
|
||||
func (f fakeAuthorizer) Authorize(ctx context.Context, a authorizer.Attributes) (authorizer.Decision, string, error) {
|
||||
if f.err != nil {
|
||||
return f.decision, "forced error", f.err
|
||||
}
|
||||
if a.GetVerb() != f.verb {
|
||||
return authorizer.DecisionDeny, fmt.Sprintf("unrecognised verb '%s'", a.GetVerb()), nil
|
||||
}
|
||||
if a.GetAPIGroup() != "certificates.k8s.io" {
|
||||
return authorizer.DecisionDeny, fmt.Sprintf("unrecognised groupName '%s'", a.GetAPIGroup()), nil
|
||||
}
|
||||
if a.GetAPIVersion() != "*" {
|
||||
return authorizer.DecisionDeny, fmt.Sprintf("unrecognised apiVersion '%s'", a.GetAPIVersion()), nil
|
||||
}
|
||||
if a.GetResource() != "signers" {
|
||||
return authorizer.DecisionDeny, fmt.Sprintf("unrecognised resource '%s'", a.GetResource()), nil
|
||||
}
|
||||
if a.GetName() != f.allowedName {
|
||||
return authorizer.DecisionDeny, fmt.Sprintf("unrecognised resource name '%s'", a.GetName()), nil
|
||||
}
|
||||
if !a.IsResourceRequest() {
|
||||
return authorizer.DecisionDeny, fmt.Sprintf("unrecognised IsResourceRequest '%t'", a.IsResourceRequest()), nil
|
||||
}
|
||||
return f.decision, "", nil
|
||||
}
|
||||
|
||||
type testAttributes struct {
|
||||
resource schema.GroupResource
|
||||
subresource string
|
||||
operation admission.Operation
|
||||
oldObj, obj runtime.Object
|
||||
name string
|
||||
|
||||
admission.Attributes // nil panic if any other methods called
|
||||
}
|
||||
|
||||
func (t *testAttributes) GetResource() schema.GroupVersionResource {
|
||||
return t.resource.WithVersion("ignored")
|
||||
}
|
||||
|
||||
func (t *testAttributes) GetSubresource() string {
|
||||
return t.subresource
|
||||
}
|
||||
|
||||
func (t *testAttributes) GetOldObject() runtime.Object {
|
||||
return t.oldObj
|
||||
}
|
||||
|
||||
func (t *testAttributes) GetObject() runtime.Object {
|
||||
return t.obj
|
||||
}
|
||||
|
||||
func (t *testAttributes) GetName() string {
|
||||
return t.name
|
||||
}
|
||||
|
||||
func (t *testAttributes) GetOperation() admission.Operation {
|
||||
return t.operation
|
||||
}
|
||||
|
||||
func (t *testAttributes) GetUserInfo() user.Info {
|
||||
return &user.DefaultInfo{Name: "ignored"}
|
||||
}
|
41
plugin/pkg/admission/certificates/subjectrestriction/BUILD
Normal file
41
plugin/pkg/admission/certificates/subjectrestriction/BUILD
Normal file
@ -0,0 +1,41 @@
|
||||
load("@io_bazel_rules_go//go:def.bzl", "go_library", "go_test")
|
||||
|
||||
go_library(
|
||||
name = "go_default_library",
|
||||
srcs = ["admission.go"],
|
||||
importpath = "k8s.io/kubernetes/plugin/pkg/admission/certificates/subjectrestriction",
|
||||
visibility = ["//visibility:public"],
|
||||
deps = [
|
||||
"//pkg/apis/certificates:go_default_library",
|
||||
"//staging/src/k8s.io/api/certificates/v1beta1:go_default_library",
|
||||
"//staging/src/k8s.io/apiserver/pkg/admission:go_default_library",
|
||||
"//vendor/k8s.io/klog:go_default_library",
|
||||
],
|
||||
)
|
||||
|
||||
go_test(
|
||||
name = "go_default_test",
|
||||
srcs = ["admission_test.go"],
|
||||
embed = [":go_default_library"],
|
||||
deps = [
|
||||
"//pkg/apis/certificates:go_default_library",
|
||||
"//staging/src/k8s.io/api/certificates/v1beta1:go_default_library",
|
||||
"//staging/src/k8s.io/apimachinery/pkg/runtime:go_default_library",
|
||||
"//staging/src/k8s.io/apimachinery/pkg/runtime/schema:go_default_library",
|
||||
"//staging/src/k8s.io/apiserver/pkg/admission:go_default_library",
|
||||
],
|
||||
)
|
||||
|
||||
filegroup(
|
||||
name = "package-srcs",
|
||||
srcs = glob(["**"]),
|
||||
tags = ["automanaged"],
|
||||
visibility = ["//visibility:private"],
|
||||
)
|
||||
|
||||
filegroup(
|
||||
name = "all-srcs",
|
||||
srcs = [":package-srcs"],
|
||||
tags = ["automanaged"],
|
||||
visibility = ["//visibility:public"],
|
||||
)
|
@ -0,0 +1,93 @@
|
||||
/*
|
||||
Copyright 2020 The Kubernetes Authors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package subjectrestriction
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
|
||||
certificatesv1beta1 "k8s.io/api/certificates/v1beta1"
|
||||
"k8s.io/apiserver/pkg/admission"
|
||||
"k8s.io/klog"
|
||||
certificatesapi "k8s.io/kubernetes/pkg/apis/certificates"
|
||||
)
|
||||
|
||||
// PluginName is a string with the name of the plugin
|
||||
const PluginName = "CertificateSubjectRestriction"
|
||||
|
||||
// Register registers the plugin
|
||||
func Register(plugins *admission.Plugins) {
|
||||
plugins.Register(PluginName, func(config io.Reader) (admission.Interface, error) {
|
||||
return NewPlugin(), nil
|
||||
})
|
||||
}
|
||||
|
||||
// Plugin holds state for and implements the admission plugin.
|
||||
type Plugin struct {
|
||||
*admission.Handler
|
||||
}
|
||||
|
||||
// ValidateInitialization always returns nil.
|
||||
func (p *Plugin) ValidateInitialization() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
var _ admission.ValidationInterface = &Plugin{}
|
||||
|
||||
// NewPlugin constructs a new instance of the CertificateSubjectRestrictions admission interface.
|
||||
func NewPlugin() *Plugin {
|
||||
return &Plugin{
|
||||
Handler: admission.NewHandler(admission.Create),
|
||||
}
|
||||
}
|
||||
|
||||
var csrGroupResource = certificatesapi.Resource("certificatesigningrequests")
|
||||
|
||||
// Validate ensures that if the signerName on a CSR is set to
|
||||
// `kubernetes.io/kube-apiserver-client`, that its organization (group)
|
||||
// attribute is not set to `system:masters`.
|
||||
func (p *Plugin) Validate(_ context.Context, a admission.Attributes, _ admission.ObjectInterfaces) error {
|
||||
if a.GetResource().GroupResource() != csrGroupResource || a.GetSubresource() != "" {
|
||||
return nil
|
||||
}
|
||||
|
||||
csr, ok := a.GetObject().(*certificatesapi.CertificateSigningRequest)
|
||||
if !ok {
|
||||
return admission.NewForbidden(a, fmt.Errorf("expected type CertificateSigningRequest, got: %T", a.GetObject()))
|
||||
}
|
||||
|
||||
if csr.Spec.SignerName != certificatesv1beta1.KubeAPIServerClientSignerName {
|
||||
return nil
|
||||
}
|
||||
|
||||
csrParsed, err := certificatesapi.ParseCSR(csr)
|
||||
if err != nil {
|
||||
return admission.NewForbidden(a, fmt.Errorf("failed to parse CSR: %v", err))
|
||||
}
|
||||
|
||||
for _, group := range csrParsed.Subject.Organization {
|
||||
if group == "system:masters" {
|
||||
klog.V(4).Infof("CSR %s rejected by admission plugin %s for attempting to use signer %s with system:masters group",
|
||||
csr.Name, PluginName, certificatesv1beta1.KubeAPIServerClientSignerName)
|
||||
return admission.NewForbidden(a, fmt.Errorf("use of %s signer with system:masters group is not allowed",
|
||||
certificatesv1beta1.KubeAPIServerClientSignerName))
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
@ -0,0 +1,189 @@
|
||||
/*
|
||||
Copyright 2020 The Kubernetes Authors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package subjectrestriction
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/ed25519"
|
||||
"crypto/rand"
|
||||
"crypto/x509"
|
||||
"crypto/x509/pkix"
|
||||
"encoding/pem"
|
||||
"testing"
|
||||
|
||||
certificatesv1beta1 "k8s.io/api/certificates/v1beta1"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
"k8s.io/apiserver/pkg/admission"
|
||||
certificatesapi "k8s.io/kubernetes/pkg/apis/certificates"
|
||||
)
|
||||
|
||||
func TestPlugin_Validate(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
a admission.Attributes
|
||||
wantErr string
|
||||
}{
|
||||
{
|
||||
name: "ignored resource",
|
||||
a: &testAttributes{
|
||||
resource: schema.GroupResource{
|
||||
Group: "foo",
|
||||
Resource: "bar",
|
||||
},
|
||||
},
|
||||
wantErr: "",
|
||||
},
|
||||
{
|
||||
name: "ignored subresource",
|
||||
a: &testAttributes{
|
||||
resource: certificatesapi.Resource("certificatesigningrequests"),
|
||||
subresource: "approve",
|
||||
},
|
||||
wantErr: "",
|
||||
},
|
||||
{
|
||||
name: "wrong type",
|
||||
a: &testAttributes{
|
||||
resource: certificatesapi.Resource("certificatesigningrequests"),
|
||||
obj: &certificatesapi.CertificateSigningRequestList{},
|
||||
name: "panda",
|
||||
},
|
||||
wantErr: `certificatesigningrequests.certificates.k8s.io "panda" is forbidden: expected type CertificateSigningRequest, got: *certificates.CertificateSigningRequestList`,
|
||||
},
|
||||
{
|
||||
name: "some other signer",
|
||||
a: &testAttributes{
|
||||
resource: certificatesapi.Resource("certificatesigningrequests"),
|
||||
obj: &certificatesapi.CertificateSigningRequest{Spec: certificatesapi.CertificateSigningRequestSpec{
|
||||
Request: pemWithGroup("system:masters"),
|
||||
SignerName: certificatesv1beta1.KubeAPIServerClientKubeletSignerName,
|
||||
}},
|
||||
},
|
||||
wantErr: "",
|
||||
},
|
||||
{
|
||||
name: "invalid request",
|
||||
a: &testAttributes{
|
||||
resource: certificatesapi.Resource("certificatesigningrequests"),
|
||||
obj: &certificatesapi.CertificateSigningRequest{Spec: certificatesapi.CertificateSigningRequestSpec{
|
||||
Request: []byte("this is not a CSR"),
|
||||
SignerName: certificatesv1beta1.KubeAPIServerClientSignerName,
|
||||
}},
|
||||
name: "bear",
|
||||
},
|
||||
wantErr: `certificatesigningrequests.certificates.k8s.io "bear" is forbidden: failed to parse CSR: PEM block type must be CERTIFICATE REQUEST`,
|
||||
},
|
||||
{
|
||||
name: "some other group",
|
||||
a: &testAttributes{
|
||||
resource: certificatesapi.Resource("certificatesigningrequests"),
|
||||
obj: &certificatesapi.CertificateSigningRequest{Spec: certificatesapi.CertificateSigningRequestSpec{
|
||||
Request: pemWithGroup("system:admin"),
|
||||
SignerName: certificatesv1beta1.KubeAPIServerClientSignerName,
|
||||
}},
|
||||
},
|
||||
wantErr: "",
|
||||
},
|
||||
{
|
||||
name: "request for system:masters",
|
||||
a: &testAttributes{
|
||||
resource: certificatesapi.Resource("certificatesigningrequests"),
|
||||
obj: &certificatesapi.CertificateSigningRequest{Spec: certificatesapi.CertificateSigningRequestSpec{
|
||||
Request: pemWithGroup("system:masters"),
|
||||
SignerName: certificatesv1beta1.KubeAPIServerClientSignerName,
|
||||
}},
|
||||
name: "pooh",
|
||||
},
|
||||
wantErr: `certificatesigningrequests.certificates.k8s.io "pooh" is forbidden: use of kubernetes.io/kube-apiserver-client signer with system:masters group is not allowed`,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
p := &Plugin{}
|
||||
if err := p.Validate(context.TODO(), tt.a, nil); errStr(err) != tt.wantErr {
|
||||
t.Errorf("Validate() error = %v, wantErr %v", err, tt.wantErr)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
type testAttributes struct {
|
||||
resource schema.GroupResource
|
||||
subresource string
|
||||
obj runtime.Object
|
||||
name string
|
||||
|
||||
admission.Attributes // nil panic if any other methods called
|
||||
}
|
||||
|
||||
func (t *testAttributes) GetResource() schema.GroupVersionResource {
|
||||
return t.resource.WithVersion("ignored")
|
||||
}
|
||||
|
||||
func (t *testAttributes) GetSubresource() string {
|
||||
return t.subresource
|
||||
}
|
||||
|
||||
func (t *testAttributes) GetObject() runtime.Object {
|
||||
return t.obj
|
||||
}
|
||||
|
||||
func (t *testAttributes) GetName() string {
|
||||
return t.name
|
||||
}
|
||||
|
||||
func errStr(err error) string {
|
||||
if err == nil {
|
||||
return ""
|
||||
}
|
||||
es := err.Error()
|
||||
if len(es) == 0 {
|
||||
panic("invalid empty error")
|
||||
}
|
||||
return es
|
||||
}
|
||||
|
||||
func pemWithGroup(group string) []byte {
|
||||
template := &x509.CertificateRequest{
|
||||
Subject: pkix.Name{
|
||||
Organization: []string{group},
|
||||
},
|
||||
}
|
||||
|
||||
_, key, err := ed25519.GenerateKey(rand.Reader)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
csrDER, err := x509.CreateCertificateRequest(rand.Reader, template, key)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
csrPemBlock := &pem.Block{
|
||||
Type: "CERTIFICATE REQUEST",
|
||||
Bytes: csrDER,
|
||||
}
|
||||
|
||||
p := pem.EncodeToMemory(csrPemBlock)
|
||||
if p == nil {
|
||||
panic("invalid pem block")
|
||||
}
|
||||
|
||||
return p
|
||||
}
|
75
plugin/pkg/admission/certificates/util.go
Normal file
75
plugin/pkg/admission/certificates/util.go
Normal file
@ -0,0 +1,75 @@
|
||||
/*
|
||||
Copyright 2020 The Kubernetes Authors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package certificates
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strings"
|
||||
|
||||
"k8s.io/apiserver/pkg/authentication/user"
|
||||
"k8s.io/apiserver/pkg/authorization/authorizer"
|
||||
"k8s.io/klog"
|
||||
)
|
||||
|
||||
// IsAuthorizedForSignerName returns true if 'info' is authorized to perform the given
|
||||
// 'verb' on the synthetic 'signers' resource with the given signerName.
|
||||
// If the user does not have permission to perform the 'verb' on the given signerName,
|
||||
// it will also perform an authorization check against {domain portion}/*, for example
|
||||
// `kubernetes.io/*`. This allows an entity to be granted permission to 'verb' on all
|
||||
// signerNames with a given 'domain portion'.
|
||||
func IsAuthorizedForSignerName(ctx context.Context, authz authorizer.Authorizer, info user.Info, verb, signerName string) bool {
|
||||
// First check if the user has explicit permission to 'verb' for the given signerName.
|
||||
attr := buildAttributes(info, verb, signerName)
|
||||
decision, reason, err := authz.Authorize(ctx, attr)
|
||||
switch {
|
||||
case err != nil:
|
||||
klog.V(3).Infof("cannot authorize %q %q for policy: %v,%v", verb, attr.GetName(), reason, err)
|
||||
case decision == authorizer.DecisionAllow:
|
||||
return true
|
||||
}
|
||||
|
||||
// If not, check if the user has wildcard permissions to 'verb' for the domain portion of the signerName, e.g.
|
||||
// 'kubernetes.io/*'.
|
||||
attr = buildWildcardAttributes(info, verb, signerName)
|
||||
decision, reason, err = authz.Authorize(ctx, attr)
|
||||
switch {
|
||||
case err != nil:
|
||||
klog.V(3).Infof("cannot authorize %q %q for policy: %v,%v", verb, attr.GetName(), reason, err)
|
||||
case decision == authorizer.DecisionAllow:
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
func buildAttributes(info user.Info, verb, signerName string) authorizer.Attributes {
|
||||
return authorizer.AttributesRecord{
|
||||
User: info,
|
||||
Verb: verb,
|
||||
Name: signerName,
|
||||
APIGroup: "certificates.k8s.io",
|
||||
APIVersion: "*",
|
||||
Resource: "signers",
|
||||
ResourceRequest: true,
|
||||
}
|
||||
}
|
||||
|
||||
func buildWildcardAttributes(info user.Info, verb, signerName string) authorizer.Attributes {
|
||||
parts := strings.Split(signerName, "/")
|
||||
domain := parts[0]
|
||||
return buildAttributes(info, verb, domain+"/*")
|
||||
}
|
@ -17,6 +17,7 @@ go_library(
|
||||
deps = [
|
||||
"//pkg/apis/rbac/v1:go_default_library",
|
||||
"//pkg/features:go_default_library",
|
||||
"//staging/src/k8s.io/api/certificates/v1beta1:go_default_library",
|
||||
"//staging/src/k8s.io/api/rbac/v1:go_default_library",
|
||||
"//staging/src/k8s.io/apimachinery/pkg/api/meta:go_default_library",
|
||||
"//staging/src/k8s.io/apimachinery/pkg/apis/meta/v1:go_default_library",
|
||||
|
@ -21,6 +21,7 @@ import (
|
||||
|
||||
"k8s.io/klog"
|
||||
|
||||
capi "k8s.io/api/certificates/v1beta1"
|
||||
rbacv1 "k8s.io/api/rbac/v1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
utilfeature "k8s.io/apiserver/pkg/util/feature"
|
||||
@ -337,6 +338,13 @@ func buildControllerRoles() ([]rbacv1.ClusterRole, []rbacv1.ClusterRoleBinding)
|
||||
Rules: []rbacv1.PolicyRule{
|
||||
rbacv1helpers.NewRule("get", "list", "watch", "delete").Groups(certificatesGroup).Resources("certificatesigningrequests").RuleOrDie(),
|
||||
rbacv1helpers.NewRule("update").Groups(certificatesGroup).Resources("certificatesigningrequests/status", "certificatesigningrequests/approval").RuleOrDie(),
|
||||
rbacv1helpers.NewRule("approve").Groups(certificatesGroup).Resources("signers").Names(capi.KubeAPIServerClientKubeletSignerName).RuleOrDie(),
|
||||
rbacv1helpers.NewRule("sign").Groups(certificatesGroup).Resources("signers").Names(
|
||||
capi.LegacyUnknownSignerName,
|
||||
capi.KubeAPIServerClientSignerName,
|
||||
capi.KubeAPIServerClientKubeletSignerName,
|
||||
capi.KubeletServingSignerName,
|
||||
).RuleOrDie(),
|
||||
rbacv1helpers.NewRule("create").Groups(authorizationGroup).Resources("subjectaccessreviews").RuleOrDie(),
|
||||
eventsRule(),
|
||||
},
|
||||
|
@ -17,6 +17,7 @@ limitations under the License.
|
||||
package bootstrappolicy
|
||||
|
||||
import (
|
||||
capi "k8s.io/api/certificates/v1beta1"
|
||||
rbacv1 "k8s.io/api/rbac/v1"
|
||||
"k8s.io/apimachinery/pkg/api/meta"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
@ -459,6 +460,30 @@ func ClusterRoles() []rbacv1.ClusterRole {
|
||||
rbacv1helpers.NewRule(ReadUpdate...).Groups(legacyGroup).Resources("persistentvolumeclaims").RuleOrDie(),
|
||||
},
|
||||
},
|
||||
{
|
||||
ObjectMeta: metav1.ObjectMeta{Name: "system:certificates.k8s.io:legacy-unknown-approver"},
|
||||
Rules: []rbacv1.PolicyRule{
|
||||
rbacv1helpers.NewRule("approve").Groups(certificatesGroup).Resources("signers").Names(capi.LegacyUnknownSignerName).RuleOrDie(),
|
||||
},
|
||||
},
|
||||
{
|
||||
ObjectMeta: metav1.ObjectMeta{Name: "system:certificates.k8s.io:kubelet-serving-approver"},
|
||||
Rules: []rbacv1.PolicyRule{
|
||||
rbacv1helpers.NewRule("approve").Groups(certificatesGroup).Resources("signers").Names(capi.KubeletServingSignerName).RuleOrDie(),
|
||||
},
|
||||
},
|
||||
{
|
||||
ObjectMeta: metav1.ObjectMeta{Name: "system:certificates.k8s.io:kube-apiserver-client-approver"},
|
||||
Rules: []rbacv1.PolicyRule{
|
||||
rbacv1helpers.NewRule("approve").Groups(certificatesGroup).Resources("signers").Names(capi.KubeAPIServerClientSignerName).RuleOrDie(),
|
||||
},
|
||||
},
|
||||
{
|
||||
ObjectMeta: metav1.ObjectMeta{Name: "system:certificates.k8s.io:kube-apiserver-client-kubelet-approver"},
|
||||
Rules: []rbacv1.PolicyRule{
|
||||
rbacv1helpers.NewRule("approve").Groups(certificatesGroup).Resources("signers").Names(capi.KubeAPIServerClientKubeletSignerName).RuleOrDie(),
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
if utilfeature.DefaultFeatureGate.Enabled(features.ServiceAccountIssuerDiscovery) {
|
||||
|
@ -419,6 +419,78 @@ items:
|
||||
- certificatesigningrequests/selfnodeclient
|
||||
verbs:
|
||||
- create
|
||||
- apiVersion: rbac.authorization.k8s.io/v1
|
||||
kind: ClusterRole
|
||||
metadata:
|
||||
annotations:
|
||||
rbac.authorization.kubernetes.io/autoupdate: "true"
|
||||
creationTimestamp: null
|
||||
labels:
|
||||
kubernetes.io/bootstrapping: rbac-defaults
|
||||
name: system:certificates.k8s.io:kube-apiserver-client-approver
|
||||
rules:
|
||||
- apiGroups:
|
||||
- certificates.k8s.io
|
||||
resourceNames:
|
||||
- kubernetes.io/kube-apiserver-client
|
||||
resources:
|
||||
- signers
|
||||
verbs:
|
||||
- approve
|
||||
- apiVersion: rbac.authorization.k8s.io/v1
|
||||
kind: ClusterRole
|
||||
metadata:
|
||||
annotations:
|
||||
rbac.authorization.kubernetes.io/autoupdate: "true"
|
||||
creationTimestamp: null
|
||||
labels:
|
||||
kubernetes.io/bootstrapping: rbac-defaults
|
||||
name: system:certificates.k8s.io:kube-apiserver-client-kubelet-approver
|
||||
rules:
|
||||
- apiGroups:
|
||||
- certificates.k8s.io
|
||||
resourceNames:
|
||||
- kubernetes.io/kube-apiserver-client-kubelet
|
||||
resources:
|
||||
- signers
|
||||
verbs:
|
||||
- approve
|
||||
- apiVersion: rbac.authorization.k8s.io/v1
|
||||
kind: ClusterRole
|
||||
metadata:
|
||||
annotations:
|
||||
rbac.authorization.kubernetes.io/autoupdate: "true"
|
||||
creationTimestamp: null
|
||||
labels:
|
||||
kubernetes.io/bootstrapping: rbac-defaults
|
||||
name: system:certificates.k8s.io:kubelet-serving-approver
|
||||
rules:
|
||||
- apiGroups:
|
||||
- certificates.k8s.io
|
||||
resourceNames:
|
||||
- kubernetes.io/kubelet-serving
|
||||
resources:
|
||||
- signers
|
||||
verbs:
|
||||
- approve
|
||||
- apiVersion: rbac.authorization.k8s.io/v1
|
||||
kind: ClusterRole
|
||||
metadata:
|
||||
annotations:
|
||||
rbac.authorization.kubernetes.io/autoupdate: "true"
|
||||
creationTimestamp: null
|
||||
labels:
|
||||
kubernetes.io/bootstrapping: rbac-defaults
|
||||
name: system:certificates.k8s.io:legacy-unknown-approver
|
||||
rules:
|
||||
- apiGroups:
|
||||
- certificates.k8s.io
|
||||
resourceNames:
|
||||
- kubernetes.io/legacy-unknown
|
||||
resources:
|
||||
- signers
|
||||
verbs:
|
||||
- approve
|
||||
- apiVersion: rbac.authorization.k8s.io/v1
|
||||
kind: ClusterRole
|
||||
metadata:
|
||||
|
@ -101,6 +101,25 @@ items:
|
||||
- certificatesigningrequests/status
|
||||
verbs:
|
||||
- update
|
||||
- apiGroups:
|
||||
- certificates.k8s.io
|
||||
resourceNames:
|
||||
- kubernetes.io/kube-apiserver-client-kubelet
|
||||
resources:
|
||||
- signers
|
||||
verbs:
|
||||
- approve
|
||||
- apiGroups:
|
||||
- certificates.k8s.io
|
||||
resourceNames:
|
||||
- kubernetes.io/kube-apiserver-client
|
||||
- kubernetes.io/kube-apiserver-client-kubelet
|
||||
- kubernetes.io/kubelet-serving
|
||||
- kubernetes.io/legacy-unknown
|
||||
resources:
|
||||
- signers
|
||||
verbs:
|
||||
- sign
|
||||
- apiGroups:
|
||||
- authorization.k8s.io
|
||||
resources:
|
||||
|
@ -227,58 +227,59 @@ func init() {
|
||||
}
|
||||
|
||||
var fileDescriptor_09d156762b8218ef = []byte{
|
||||
// 805 bytes of a gzipped FileDescriptorProto
|
||||
0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xa4, 0x54, 0x4b, 0x8f, 0x1b, 0x45,
|
||||
0x10, 0xf6, 0xf8, 0xb5, 0x76, 0x7b, 0xd9, 0x44, 0x2d, 0x14, 0x0d, 0x2b, 0x65, 0x66, 0x35, 0x02,
|
||||
0xb4, 0x3c, 0xd2, 0xc3, 0x46, 0x08, 0x56, 0x7b, 0x40, 0x30, 0x4b, 0x04, 0x2b, 0x12, 0x21, 0x75,
|
||||
0x62, 0x0e, 0x08, 0x89, 0xb4, 0xc7, 0x95, 0x71, 0xc7, 0x99, 0x07, 0xd3, 0x3d, 0x06, 0xdf, 0xf2,
|
||||
0x13, 0x38, 0x72, 0x41, 0xe2, 0x97, 0x70, 0x5e, 0x0e, 0x48, 0x39, 0xe6, 0x80, 0x2c, 0xd6, 0xfc,
|
||||
0x8b, 0x9c, 0x50, 0xf7, 0xb4, 0x3d, 0xc6, 0x2b, 0xe3, 0x28, 0x7b, 0x9b, 0xfa, 0xaa, 0xbe, 0xaf,
|
||||
0x1e, 0x5d, 0x35, 0xe8, 0xcb, 0xf1, 0xb1, 0x20, 0x3c, 0xf5, 0xc7, 0xc5, 0x00, 0xf2, 0x04, 0x24,
|
||||
0x08, 0x7f, 0x02, 0xc9, 0x30, 0xcd, 0x7d, 0xe3, 0x60, 0x19, 0xf7, 0x43, 0xc8, 0x25, 0x7f, 0xc4,
|
||||
0x43, 0xa6, 0xdd, 0x47, 0x03, 0x90, 0xec, 0xc8, 0x8f, 0x20, 0x81, 0x9c, 0x49, 0x18, 0x92, 0x2c,
|
||||
0x4f, 0x65, 0x8a, 0xdd, 0x92, 0x40, 0x58, 0xc6, 0xc9, 0x2a, 0x81, 0x18, 0xc2, 0xfe, 0xad, 0x88,
|
||||
0xcb, 0x51, 0x31, 0x20, 0x61, 0x1a, 0xfb, 0x51, 0x1a, 0xa5, 0xbe, 0xe6, 0x0d, 0x8a, 0x47, 0xda,
|
||||
0xd2, 0x86, 0xfe, 0x2a, 0xf5, 0xf6, 0x3f, 0xac, 0x0a, 0x88, 0x59, 0x38, 0xe2, 0x09, 0xe4, 0x53,
|
||||
0x3f, 0x1b, 0x47, 0x0a, 0x10, 0x7e, 0x0c, 0x92, 0xf9, 0x93, 0x4b, 0x55, 0xec, 0xfb, 0x9b, 0x58,
|
||||
0x79, 0x91, 0x48, 0x1e, 0xc3, 0x25, 0xc2, 0x47, 0xdb, 0x08, 0x22, 0x1c, 0x41, 0xcc, 0xd6, 0x79,
|
||||
0xde, 0x1f, 0x75, 0xf4, 0xc6, 0x69, 0xd5, 0xe6, 0x7d, 0x1e, 0x25, 0x3c, 0x89, 0x28, 0xfc, 0x50,
|
||||
0x80, 0x90, 0xf8, 0x21, 0xea, 0xa8, 0x0a, 0x87, 0x4c, 0x32, 0xdb, 0x3a, 0xb0, 0x0e, 0x7b, 0xb7,
|
||||
0x3f, 0x20, 0xd5, 0x7c, 0x96, 0x89, 0x48, 0x36, 0x8e, 0x14, 0x20, 0x88, 0x8a, 0x26, 0x93, 0x23,
|
||||
0xf2, 0xf5, 0xe0, 0x31, 0x84, 0xf2, 0x1e, 0x48, 0x16, 0xe0, 0xf3, 0x99, 0x5b, 0x9b, 0xcf, 0x5c,
|
||||
0x54, 0x61, 0x74, 0xa9, 0x8a, 0x1f, 0xa2, 0xa6, 0xc8, 0x20, 0xb4, 0xeb, 0x5a, 0xfd, 0x13, 0xb2,
|
||||
0x65, 0xfa, 0x64, 0x63, 0xad, 0xf7, 0x33, 0x08, 0x83, 0x5d, 0x93, 0xab, 0xa9, 0x2c, 0xaa, 0x95,
|
||||
0xf1, 0x08, 0xb5, 0x85, 0x64, 0xb2, 0x10, 0x76, 0x43, 0xe7, 0xf8, 0xf4, 0x0a, 0x39, 0xb4, 0x4e,
|
||||
0xb0, 0x67, 0xb2, 0xb4, 0x4b, 0x9b, 0x1a, 0x7d, 0xef, 0xd7, 0x3a, 0xf2, 0x36, 0x72, 0x4f, 0xd3,
|
||||
0x64, 0xc8, 0x25, 0x4f, 0x13, 0x7c, 0x8c, 0x9a, 0x72, 0x9a, 0x81, 0x1e, 0x68, 0x37, 0x78, 0x73,
|
||||
0x51, 0xf2, 0x83, 0x69, 0x06, 0x2f, 0x66, 0xee, 0xeb, 0xeb, 0xf1, 0x0a, 0xa7, 0x9a, 0x81, 0xdf,
|
||||
0x46, 0xed, 0x1c, 0x98, 0x48, 0x13, 0x3d, 0xae, 0x6e, 0x55, 0x08, 0xd5, 0x28, 0x35, 0x5e, 0xfc,
|
||||
0x0e, 0xda, 0x89, 0x41, 0x08, 0x16, 0x81, 0xee, 0xb9, 0x1b, 0x5c, 0x33, 0x81, 0x3b, 0xf7, 0x4a,
|
||||
0x98, 0x2e, 0xfc, 0xf8, 0x31, 0xda, 0x7b, 0xc2, 0x84, 0xec, 0x67, 0x43, 0x26, 0xe1, 0x01, 0x8f,
|
||||
0xc1, 0x6e, 0xea, 0x29, 0xbd, 0xfb, 0x72, 0xef, 0xac, 0x18, 0xc1, 0x0d, 0xa3, 0xbe, 0x77, 0xf7,
|
||||
0x3f, 0x4a, 0x74, 0x4d, 0xd9, 0x9b, 0x59, 0xe8, 0xe6, 0xc6, 0xf9, 0xdc, 0xe5, 0x42, 0xe2, 0xef,
|
||||
0x2e, 0xed, 0x1b, 0x79, 0xb9, 0x3a, 0x14, 0x5b, 0x6f, 0xdb, 0x75, 0x53, 0x4b, 0x67, 0x81, 0xac,
|
||||
0xec, 0xda, 0xf7, 0xa8, 0xc5, 0x25, 0xc4, 0xc2, 0xae, 0x1f, 0x34, 0x0e, 0x7b, 0xb7, 0x4f, 0x5e,
|
||||
0x7d, 0x11, 0x82, 0xd7, 0x4c, 0x9a, 0xd6, 0x99, 0x12, 0xa4, 0xa5, 0xae, 0xf7, 0x7b, 0xe3, 0x7f,
|
||||
0x1a, 0x54, 0x2b, 0x89, 0xdf, 0x42, 0x3b, 0x79, 0x69, 0xea, 0xfe, 0x76, 0x83, 0x9e, 0x7a, 0x15,
|
||||
0x13, 0x41, 0x17, 0x3e, 0x4c, 0x50, 0xbb, 0x50, 0xcf, 0x23, 0xec, 0xd6, 0x41, 0xe3, 0xb0, 0x1b,
|
||||
0xdc, 0x50, 0x8f, 0xdc, 0xd7, 0xc8, 0x8b, 0x99, 0xdb, 0xf9, 0x0a, 0xa6, 0xda, 0xa0, 0x26, 0x0a,
|
||||
0xbf, 0x8f, 0x3a, 0x85, 0x80, 0x3c, 0x61, 0x31, 0x98, 0xd5, 0x58, 0xce, 0xa1, 0x6f, 0x70, 0xba,
|
||||
0x8c, 0xc0, 0x37, 0x51, 0xa3, 0xe0, 0x43, 0xb3, 0x1a, 0x3d, 0x13, 0xd8, 0xe8, 0x9f, 0x7d, 0x4e,
|
||||
0x15, 0x8e, 0x3d, 0xd4, 0x8e, 0xf2, 0xb4, 0xc8, 0x84, 0xdd, 0xd4, 0xc9, 0x91, 0x4a, 0xfe, 0x85,
|
||||
0x46, 0xa8, 0xf1, 0xe0, 0x04, 0xb5, 0xe0, 0x27, 0x99, 0x33, 0xbb, 0xad, 0x47, 0x79, 0x76, 0xb5,
|
||||
0xbb, 0x25, 0x77, 0x94, 0xd6, 0x9d, 0x44, 0xe6, 0xd3, 0x6a, 0xb2, 0x1a, 0xa3, 0x65, 0x9a, 0x7d,
|
||||
0x40, 0xa8, 0x8a, 0xc1, 0xd7, 0x51, 0x63, 0x0c, 0xd3, 0xf2, 0x80, 0xa8, 0xfa, 0xc4, 0x9f, 0xa1,
|
||||
0xd6, 0x84, 0x3d, 0x29, 0xc0, 0xfc, 0x47, 0xde, 0xdb, 0x5a, 0x8f, 0x56, 0xfb, 0x46, 0x51, 0x68,
|
||||
0xc9, 0x3c, 0xa9, 0x1f, 0x5b, 0xde, 0x9f, 0x16, 0x72, 0xb7, 0x5c, 0x3f, 0xfe, 0x11, 0xa1, 0x70,
|
||||
0x71, 0x9b, 0xc2, 0xb6, 0x74, 0xff, 0xa7, 0xaf, 0xde, 0xff, 0xf2, 0xce, 0xab, 0x1f, 0xe5, 0x12,
|
||||
0x12, 0x74, 0x25, 0x15, 0x3e, 0x42, 0xbd, 0x15, 0x69, 0xdd, 0xe9, 0x6e, 0x70, 0x6d, 0x3e, 0x73,
|
||||
0x7b, 0x2b, 0xe2, 0x74, 0x35, 0xc6, 0xfb, 0xd8, 0x8c, 0x4d, 0x37, 0x8a, 0xdd, 0xc5, 0xfe, 0x5b,
|
||||
0xfa, 0x5d, 0xbb, 0xeb, 0xfb, 0x7b, 0xd2, 0xf9, 0xe5, 0x37, 0xb7, 0xf6, 0xf4, 0xaf, 0x83, 0x5a,
|
||||
0x70, 0xeb, 0xfc, 0xc2, 0xa9, 0x3d, 0xbb, 0x70, 0x6a, 0xcf, 0x2f, 0x9c, 0xda, 0xd3, 0xb9, 0x63,
|
||||
0x9d, 0xcf, 0x1d, 0xeb, 0xd9, 0xdc, 0xb1, 0x9e, 0xcf, 0x1d, 0xeb, 0xef, 0xb9, 0x63, 0xfd, 0xfc,
|
||||
0x8f, 0x53, 0xfb, 0x76, 0xc7, 0x74, 0xf7, 0x6f, 0x00, 0x00, 0x00, 0xff, 0xff, 0x39, 0x0e, 0xb6,
|
||||
0xcd, 0x7f, 0x07, 0x00, 0x00,
|
||||
// 824 bytes of a gzipped FileDescriptorProto
|
||||
0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xa4, 0x54, 0x4d, 0x6f, 0x1b, 0x45,
|
||||
0x18, 0xf6, 0xfa, 0xdb, 0xe3, 0x90, 0x56, 0x23, 0x54, 0x2d, 0x91, 0xba, 0x1b, 0xad, 0x00, 0x85,
|
||||
0x8f, 0xce, 0x92, 0x0a, 0x41, 0x94, 0x03, 0x82, 0x0d, 0x15, 0x44, 0xb4, 0x20, 0x4d, 0x1a, 0x0e,
|
||||
0x08, 0x89, 0x8e, 0xd7, 0x6f, 0x37, 0x53, 0x77, 0x3f, 0xd8, 0x99, 0x35, 0xf8, 0xd6, 0x9f, 0xc0,
|
||||
0x91, 0x0b, 0x12, 0x3f, 0x27, 0x1c, 0x90, 0x7a, 0xec, 0x01, 0x59, 0xc4, 0xdc, 0xf9, 0x01, 0x3d,
|
||||
0xa1, 0x99, 0x1d, 0x7b, 0x8d, 0x23, 0xd7, 0x55, 0x73, 0xdb, 0xf7, 0x79, 0xdf, 0xe7, 0x79, 0x3f,
|
||||
0x67, 0xd1, 0x97, 0xa3, 0x03, 0x41, 0x78, 0xea, 0x8f, 0x8a, 0x01, 0xe4, 0x09, 0x48, 0x10, 0xfe,
|
||||
0x18, 0x92, 0x61, 0x9a, 0xfb, 0xc6, 0xc1, 0x32, 0xee, 0x87, 0x90, 0x4b, 0xfe, 0x90, 0x87, 0x4c,
|
||||
0xbb, 0xf7, 0x07, 0x20, 0xd9, 0xbe, 0x1f, 0x41, 0x02, 0x39, 0x93, 0x30, 0x24, 0x59, 0x9e, 0xca,
|
||||
0x14, 0xbb, 0x25, 0x81, 0xb0, 0x8c, 0x93, 0x65, 0x02, 0x31, 0x84, 0x9d, 0x5b, 0x11, 0x97, 0x67,
|
||||
0xc5, 0x80, 0x84, 0x69, 0xec, 0x47, 0x69, 0x94, 0xfa, 0x9a, 0x37, 0x28, 0x1e, 0x6a, 0x4b, 0x1b,
|
||||
0xfa, 0xab, 0xd4, 0xdb, 0xf9, 0xb0, 0x2a, 0x20, 0x66, 0xe1, 0x19, 0x4f, 0x20, 0x9f, 0xf8, 0xd9,
|
||||
0x28, 0x52, 0x80, 0xf0, 0x63, 0x90, 0xcc, 0x1f, 0x5f, 0xaa, 0x62, 0xc7, 0x5f, 0xc7, 0xca, 0x8b,
|
||||
0x44, 0xf2, 0x18, 0x2e, 0x11, 0x3e, 0xda, 0x44, 0x10, 0xe1, 0x19, 0xc4, 0x6c, 0x95, 0xe7, 0xfd,
|
||||
0x51, 0x47, 0x6f, 0x1c, 0x55, 0x6d, 0x9e, 0xf0, 0x28, 0xe1, 0x49, 0x44, 0xe1, 0xc7, 0x02, 0x84,
|
||||
0xc4, 0x0f, 0x50, 0x57, 0x55, 0x38, 0x64, 0x92, 0xd9, 0xd6, 0xae, 0xb5, 0xd7, 0xbf, 0xfd, 0x01,
|
||||
0xa9, 0xe6, 0xb3, 0x48, 0x44, 0xb2, 0x51, 0xa4, 0x00, 0x41, 0x54, 0x34, 0x19, 0xef, 0x93, 0x6f,
|
||||
0x06, 0x8f, 0x20, 0x94, 0xf7, 0x40, 0xb2, 0x00, 0x9f, 0x4f, 0xdd, 0xda, 0x6c, 0xea, 0xa2, 0x0a,
|
||||
0xa3, 0x0b, 0x55, 0xfc, 0x00, 0x35, 0x45, 0x06, 0xa1, 0x5d, 0xd7, 0xea, 0x9f, 0x90, 0x0d, 0xd3,
|
||||
0x27, 0x6b, 0x6b, 0x3d, 0xc9, 0x20, 0x0c, 0xb6, 0x4c, 0xae, 0xa6, 0xb2, 0xa8, 0x56, 0xc6, 0x67,
|
||||
0xa8, 0x2d, 0x24, 0x93, 0x85, 0xb0, 0x1b, 0x3a, 0xc7, 0xa7, 0x57, 0xc8, 0xa1, 0x75, 0x82, 0x6d,
|
||||
0x93, 0xa5, 0x5d, 0xda, 0xd4, 0xe8, 0x7b, 0xbf, 0xd5, 0x91, 0xb7, 0x96, 0x7b, 0x94, 0x26, 0x43,
|
||||
0x2e, 0x79, 0x9a, 0xe0, 0x03, 0xd4, 0x94, 0x93, 0x0c, 0xf4, 0x40, 0x7b, 0xc1, 0x9b, 0xf3, 0x92,
|
||||
0xef, 0x4f, 0x32, 0x78, 0x3e, 0x75, 0x5f, 0x5f, 0x8d, 0x57, 0x38, 0xd5, 0x0c, 0xfc, 0x36, 0x6a,
|
||||
0xe7, 0xc0, 0x44, 0x9a, 0xe8, 0x71, 0xf5, 0xaa, 0x42, 0xa8, 0x46, 0xa9, 0xf1, 0xe2, 0x77, 0x50,
|
||||
0x27, 0x06, 0x21, 0x58, 0x04, 0xba, 0xe7, 0x5e, 0x70, 0xcd, 0x04, 0x76, 0xee, 0x95, 0x30, 0x9d,
|
||||
0xfb, 0xf1, 0x23, 0xb4, 0xfd, 0x98, 0x09, 0x79, 0x9a, 0x0d, 0x99, 0x84, 0xfb, 0x3c, 0x06, 0xbb,
|
||||
0xa9, 0xa7, 0xf4, 0xee, 0xcb, 0xed, 0x59, 0x31, 0x82, 0x1b, 0x46, 0x7d, 0xfb, 0xee, 0xff, 0x94,
|
||||
0xe8, 0x8a, 0xb2, 0x37, 0xb5, 0xd0, 0xcd, 0xb5, 0xf3, 0xb9, 0xcb, 0x85, 0xc4, 0xdf, 0x5f, 0xba,
|
||||
0x37, 0xf2, 0x72, 0x75, 0x28, 0xb6, 0xbe, 0xb6, 0xeb, 0xa6, 0x96, 0xee, 0x1c, 0x59, 0xba, 0xb5,
|
||||
0x1f, 0x50, 0x8b, 0x4b, 0x88, 0x85, 0x5d, 0xdf, 0x6d, 0xec, 0xf5, 0x6f, 0x1f, 0xbe, 0xfa, 0x21,
|
||||
0x04, 0xaf, 0x99, 0x34, 0xad, 0x63, 0x25, 0x48, 0x4b, 0x5d, 0xef, 0xdf, 0xc6, 0x0b, 0x1a, 0x54,
|
||||
0x27, 0x89, 0xdf, 0x42, 0x9d, 0xbc, 0x34, 0x75, 0x7f, 0x5b, 0x41, 0x5f, 0x6d, 0xc5, 0x44, 0xd0,
|
||||
0xb9, 0x0f, 0x13, 0x84, 0x04, 0x8f, 0x12, 0xc8, 0xbf, 0x66, 0x31, 0xd8, 0x9d, 0x72, 0xd9, 0xea,
|
||||
0x0d, 0x9d, 0x2c, 0x50, 0xba, 0x14, 0x81, 0x09, 0x6a, 0x17, 0x6a, 0x9d, 0xc2, 0x6e, 0xed, 0x36,
|
||||
0xf6, 0x7a, 0xc1, 0x0d, 0x75, 0x14, 0xa7, 0x1a, 0x79, 0x3e, 0x75, 0xbb, 0x5f, 0xc1, 0x44, 0x1b,
|
||||
0xd4, 0x44, 0xe1, 0xf7, 0x51, 0xb7, 0x10, 0x90, 0x27, 0x4a, 0xbd, 0x3c, 0xa5, 0xc5, 0xdc, 0x4e,
|
||||
0x0d, 0x4e, 0x17, 0x11, 0xf8, 0x26, 0x6a, 0x14, 0x7c, 0x68, 0x4e, 0xa9, 0x6f, 0x02, 0x1b, 0xa7,
|
||||
0xc7, 0x9f, 0x53, 0x85, 0x63, 0x0f, 0xb5, 0xa3, 0x3c, 0x2d, 0x32, 0x61, 0x37, 0x75, 0x72, 0xa4,
|
||||
0x92, 0x7f, 0xa1, 0x11, 0x6a, 0x3c, 0x38, 0x41, 0x2d, 0xf8, 0x59, 0xe6, 0xcc, 0x6e, 0xeb, 0xd1,
|
||||
0x1f, 0x5f, 0xed, 0x9d, 0x93, 0x3b, 0x4a, 0xeb, 0x4e, 0x22, 0xf3, 0x49, 0xb5, 0x09, 0x8d, 0xd1,
|
||||
0x32, 0xcd, 0x0e, 0x20, 0x54, 0xc5, 0xe0, 0xeb, 0xa8, 0x31, 0x82, 0x49, 0xf9, 0xe0, 0xa8, 0xfa,
|
||||
0xc4, 0x9f, 0xa1, 0xd6, 0x98, 0x3d, 0x2e, 0xc0, 0xfc, 0x77, 0xde, 0xdb, 0x58, 0x8f, 0x56, 0xfb,
|
||||
0x56, 0x51, 0x68, 0xc9, 0x3c, 0xac, 0x1f, 0x58, 0xde, 0x9f, 0x16, 0x72, 0x37, 0xfc, 0x2d, 0xf0,
|
||||
0x4f, 0x08, 0x85, 0xf3, 0xb7, 0x2c, 0x6c, 0x4b, 0xf7, 0x7f, 0xf4, 0xea, 0xfd, 0x2f, 0xfe, 0x0b,
|
||||
0xd5, 0x8f, 0x75, 0x01, 0x09, 0xba, 0x94, 0x0a, 0xef, 0xa3, 0xfe, 0x92, 0xb4, 0xee, 0x74, 0x2b,
|
||||
0xb8, 0x36, 0x9b, 0xba, 0xfd, 0x25, 0x71, 0xba, 0x1c, 0xe3, 0x7d, 0x6c, 0xc6, 0xa6, 0x1b, 0xc5,
|
||||
0xee, 0xfc, 0xbd, 0x58, 0x7a, 0xaf, 0xbd, 0xd5, 0x7b, 0x3f, 0xec, 0xfe, 0xfa, 0xbb, 0x5b, 0x7b,
|
||||
0xf2, 0xd7, 0x6e, 0x2d, 0xb8, 0x75, 0x7e, 0xe1, 0xd4, 0x9e, 0x5e, 0x38, 0xb5, 0x67, 0x17, 0x4e,
|
||||
0xed, 0xc9, 0xcc, 0xb1, 0xce, 0x67, 0x8e, 0xf5, 0x74, 0xe6, 0x58, 0xcf, 0x66, 0x8e, 0xf5, 0xf7,
|
||||
0xcc, 0xb1, 0x7e, 0xf9, 0xc7, 0xa9, 0x7d, 0xd7, 0x31, 0xdd, 0xfd, 0x17, 0x00, 0x00, 0xff, 0xff,
|
||||
0x69, 0x8d, 0xc8, 0xd3, 0xaf, 0x07, 0x00, 0x00,
|
||||
}
|
||||
|
||||
func (m *CertificateSigningRequest) Marshal() (dAtA []byte, err error) {
|
||||
@ -449,6 +450,13 @@ func (m *CertificateSigningRequestSpec) MarshalToSizedBuffer(dAtA []byte) (int,
|
||||
_ = i
|
||||
var l int
|
||||
_ = l
|
||||
if m.SignerName != nil {
|
||||
i -= len(*m.SignerName)
|
||||
copy(dAtA[i:], *m.SignerName)
|
||||
i = encodeVarintGenerated(dAtA, i, uint64(len(*m.SignerName)))
|
||||
i--
|
||||
dAtA[i] = 0x3a
|
||||
}
|
||||
if len(m.Extra) > 0 {
|
||||
keysForExtra := make([]string, 0, len(m.Extra))
|
||||
for k := range m.Extra {
|
||||
@ -687,6 +695,10 @@ func (m *CertificateSigningRequestSpec) Size() (n int) {
|
||||
n += mapEntrySize + 1 + sovGenerated(uint64(mapEntrySize))
|
||||
}
|
||||
}
|
||||
if m.SignerName != nil {
|
||||
l = len(*m.SignerName)
|
||||
n += 1 + l + sovGenerated(uint64(l))
|
||||
}
|
||||
return n
|
||||
}
|
||||
|
||||
@ -792,6 +804,7 @@ func (this *CertificateSigningRequestSpec) String() string {
|
||||
`Groups:` + fmt.Sprintf("%v", this.Groups) + `,`,
|
||||
`Usages:` + fmt.Sprintf("%v", this.Usages) + `,`,
|
||||
`Extra:` + mapStringForExtra + `,`,
|
||||
`SignerName:` + valueToStringGenerated(this.SignerName) + `,`,
|
||||
`}`,
|
||||
}, "")
|
||||
return s
|
||||
@ -1594,6 +1607,39 @@ func (m *CertificateSigningRequestSpec) Unmarshal(dAtA []byte) error {
|
||||
}
|
||||
m.Extra[mapkey] = *mapvalue
|
||||
iNdEx = postIndex
|
||||
case 7:
|
||||
if wireType != 2 {
|
||||
return fmt.Errorf("proto: wrong wireType = %d for field SignerName", wireType)
|
||||
}
|
||||
var stringLen uint64
|
||||
for shift := uint(0); ; shift += 7 {
|
||||
if shift >= 64 {
|
||||
return ErrIntOverflowGenerated
|
||||
}
|
||||
if iNdEx >= l {
|
||||
return io.ErrUnexpectedEOF
|
||||
}
|
||||
b := dAtA[iNdEx]
|
||||
iNdEx++
|
||||
stringLen |= uint64(b&0x7F) << shift
|
||||
if b < 0x80 {
|
||||
break
|
||||
}
|
||||
}
|
||||
intStringLen := int(stringLen)
|
||||
if intStringLen < 0 {
|
||||
return ErrInvalidLengthGenerated
|
||||
}
|
||||
postIndex := iNdEx + intStringLen
|
||||
if postIndex < 0 {
|
||||
return ErrInvalidLengthGenerated
|
||||
}
|
||||
if postIndex > l {
|
||||
return io.ErrUnexpectedEOF
|
||||
}
|
||||
s := string(dAtA[iNdEx:postIndex])
|
||||
m.SignerName = &s
|
||||
iNdEx = postIndex
|
||||
default:
|
||||
iNdEx = preIndex
|
||||
skippy, err := skipGenerated(dAtA[iNdEx:])
|
||||
|
@ -73,6 +73,19 @@ message CertificateSigningRequestSpec {
|
||||
// Base64-encoded PKCS#10 CSR data
|
||||
optional bytes request = 1;
|
||||
|
||||
// Requested signer for the request. It is a qualified name in the form:
|
||||
// `scope-hostname.io/name`.
|
||||
// If empty, it will be defaulted:
|
||||
// 1. If it's a kubelet client certificate, it is assigned
|
||||
// "kubernetes.io/kube-apiserver-client-kubelet".
|
||||
// 2. If it's a kubelet serving certificate, it is assigned
|
||||
// "kubernetes.io/kubelet-serving".
|
||||
// 3. Otherwise, it is assigned "kubernetes.io/legacy-unknown".
|
||||
// Distribution of trust for signers happens out of band.
|
||||
// You can select on this field using `spec.signerName`.
|
||||
// +optional
|
||||
optional string signerName = 7;
|
||||
|
||||
// allowedUsages specifies a set of usage contexts the key will be
|
||||
// valid for.
|
||||
// See: https://tools.ietf.org/html/rfc5280#section-4.2.1.3
|
||||
|
@ -48,6 +48,19 @@ type CertificateSigningRequestSpec struct {
|
||||
// Base64-encoded PKCS#10 CSR data
|
||||
Request []byte `json:"request" protobuf:"bytes,1,opt,name=request"`
|
||||
|
||||
// Requested signer for the request. It is a qualified name in the form:
|
||||
// `scope-hostname.io/name`.
|
||||
// If empty, it will be defaulted:
|
||||
// 1. If it's a kubelet client certificate, it is assigned
|
||||
// "kubernetes.io/kube-apiserver-client-kubelet".
|
||||
// 2. If it's a kubelet serving certificate, it is assigned
|
||||
// "kubernetes.io/kubelet-serving".
|
||||
// 3. Otherwise, it is assigned "kubernetes.io/legacy-unknown".
|
||||
// Distribution of trust for signers happens out of band.
|
||||
// You can select on this field using `spec.signerName`.
|
||||
// +optional
|
||||
SignerName *string `json:"signerName,omitempty" protobuf:"bytes,7,opt,name=signerName"`
|
||||
|
||||
// allowedUsages specifies a set of usage contexts the key will be
|
||||
// valid for.
|
||||
// See: https://tools.ietf.org/html/rfc5280#section-4.2.1.3
|
||||
@ -72,6 +85,28 @@ type CertificateSigningRequestSpec struct {
|
||||
Extra map[string]ExtraValue `json:"extra,omitempty" protobuf:"bytes,6,rep,name=extra"`
|
||||
}
|
||||
|
||||
// Built in signerName values that are honoured by kube-controller-manager.
|
||||
// None of these usages are related to ServiceAccount token secrets
|
||||
// `.data[ca.crt]` in any way.
|
||||
const (
|
||||
// Signs certificates that will be honored as client-certs by the
|
||||
// kube-apiserver. Never auto-approved by kube-controller-manager.
|
||||
KubeAPIServerClientSignerName = "kubernetes.io/kube-apiserver-client"
|
||||
|
||||
// Signs client certificates that will be honored as client-certs by the
|
||||
// kube-apiserver for a kubelet.
|
||||
// May be auto-approved by kube-controller-manager.
|
||||
KubeAPIServerClientKubeletSignerName = "kubernetes.io/kube-apiserver-client-kubelet"
|
||||
|
||||
// Signs serving certificates that are honored as a valid kubelet serving
|
||||
// certificate by the kube-apiserver, but has no other guarantees.
|
||||
KubeletServingSignerName = "kubernetes.io/kubelet-serving"
|
||||
|
||||
// Has no guarantees for trust at all. Some distributions may honor these
|
||||
// as client certs, but that behavior is not standard kubernetes behavior.
|
||||
LegacyUnknownSignerName = "kubernetes.io/legacy-unknown"
|
||||
)
|
||||
|
||||
// ExtraValue masks the value so protobuf can generate
|
||||
// +protobuf.nullable=true
|
||||
// +protobuf.options.(gogoproto.goproto_stringer)=false
|
||||
|
@ -49,13 +49,14 @@ func (CertificateSigningRequestCondition) SwaggerDoc() map[string]string {
|
||||
}
|
||||
|
||||
var map_CertificateSigningRequestSpec = map[string]string{
|
||||
"": "This information is immutable after the request is created. Only the Request and Usages fields can be set on creation, other fields are derived by Kubernetes and cannot be modified by users.",
|
||||
"request": "Base64-encoded PKCS#10 CSR data",
|
||||
"usages": "allowedUsages specifies a set of usage contexts the key will be valid for. See: https://tools.ietf.org/html/rfc5280#section-4.2.1.3\n https://tools.ietf.org/html/rfc5280#section-4.2.1.12",
|
||||
"username": "Information about the requesting user. See user.Info interface for details.",
|
||||
"uid": "UID information about the requesting user. See user.Info interface for details.",
|
||||
"groups": "Group information about the requesting user. See user.Info interface for details.",
|
||||
"extra": "Extra information about the requesting user. See user.Info interface for details.",
|
||||
"": "This information is immutable after the request is created. Only the Request and Usages fields can be set on creation, other fields are derived by Kubernetes and cannot be modified by users.",
|
||||
"request": "Base64-encoded PKCS#10 CSR data",
|
||||
"signerName": "Requested signer for the request. It is a qualified name in the form: `scope-hostname.io/name`. If empty, it will be defaulted:\n 1. If it's a kubelet client certificate, it is assigned\n \"kubernetes.io/kube-apiserver-client-kubelet\".\n 2. If it's a kubelet serving certificate, it is assigned\n \"kubernetes.io/kubelet-serving\".\n 3. Otherwise, it is assigned \"kubernetes.io/legacy-unknown\".\nDistribution of trust for signers happens out of band. You can select on this field using `spec.signerName`.",
|
||||
"usages": "allowedUsages specifies a set of usage contexts the key will be valid for. See: https://tools.ietf.org/html/rfc5280#section-4.2.1.3\n https://tools.ietf.org/html/rfc5280#section-4.2.1.12",
|
||||
"username": "Information about the requesting user. See user.Info interface for details.",
|
||||
"uid": "UID information about the requesting user. See user.Info interface for details.",
|
||||
"groups": "Group information about the requesting user. See user.Info interface for details.",
|
||||
"extra": "Extra information about the requesting user. See user.Info interface for details.",
|
||||
}
|
||||
|
||||
func (CertificateSigningRequestSpec) SwaggerDoc() map[string]string {
|
||||
|
@ -110,6 +110,11 @@ func (in *CertificateSigningRequestSpec) DeepCopyInto(out *CertificateSigningReq
|
||||
*out = make([]byte, len(*in))
|
||||
copy(*out, *in)
|
||||
}
|
||||
if in.SignerName != nil {
|
||||
in, out := &in.SignerName, &out.SignerName
|
||||
*out = new(string)
|
||||
**out = **in
|
||||
}
|
||||
if in.Usages != nil {
|
||||
in, out := &in.Usages, &out.Usages
|
||||
*out = make([]KeyUsage, len(*in))
|
||||
|
@ -42,17 +42,18 @@
|
||||
},
|
||||
"spec": {
|
||||
"request": "OA==",
|
||||
"signerName": "19",
|
||||
"usages": [
|
||||
"J枊a"
|
||||
],
|
||||
"username": "19",
|
||||
"uid": "20",
|
||||
"username": "20",
|
||||
"uid": "21",
|
||||
"groups": [
|
||||
"21"
|
||||
"22"
|
||||
],
|
||||
"extra": {
|
||||
"22": [
|
||||
"23"
|
||||
"23": [
|
||||
"24"
|
||||
]
|
||||
}
|
||||
},
|
||||
@ -60,8 +61,8 @@
|
||||
"conditions": [
|
||||
{
|
||||
"type": "o,c鮽ort昍řČ扷5Ɨ",
|
||||
"reason": "24",
|
||||
"message": "25",
|
||||
"reason": "25",
|
||||
"message": "26",
|
||||
"lastUpdateTime": "2901-11-14T22:54:07Z"
|
||||
}
|
||||
],
|
||||
|
Binary file not shown.
@ -31,19 +31,20 @@ metadata:
|
||||
uid: "7"
|
||||
spec:
|
||||
extra:
|
||||
"22":
|
||||
- "23"
|
||||
"23":
|
||||
- "24"
|
||||
groups:
|
||||
- "21"
|
||||
- "22"
|
||||
request: OA==
|
||||
uid: "20"
|
||||
signerName: "19"
|
||||
uid: "21"
|
||||
usages:
|
||||
- J枊a
|
||||
username: "19"
|
||||
username: "20"
|
||||
status:
|
||||
certificate: 9Q==
|
||||
conditions:
|
||||
- lastUpdateTime: "2901-11-14T22:54:07Z"
|
||||
message: "25"
|
||||
reason: "24"
|
||||
message: "26"
|
||||
reason: "25"
|
||||
type: o,c鮽ort昍řČ扷5Ɨ
|
||||
|
@ -85,6 +85,9 @@ type Config struct {
|
||||
// If no template is available, nil may be returned, and no certificate will be requested.
|
||||
// If specified, takes precedence over Template.
|
||||
GetTemplate func() *x509.CertificateRequest
|
||||
// SignerName is the name of the certificate signer that should sign certificates
|
||||
// generated by the manager.
|
||||
SignerName string
|
||||
// Usages is the types of usages that certificates generated by the manager
|
||||
// can be used for.
|
||||
Usages []certificates.KeyUsage
|
||||
@ -174,6 +177,7 @@ type manager struct {
|
||||
lastRequest *x509.CertificateRequest
|
||||
|
||||
dynamicTemplate bool
|
||||
signerName string
|
||||
usages []certificates.KeyUsage
|
||||
forceRotation bool
|
||||
|
||||
@ -219,6 +223,7 @@ func NewManager(config *Config) (Manager, error) {
|
||||
clientFn: config.ClientFn,
|
||||
getTemplate: getTemplate,
|
||||
dynamicTemplate: config.GetTemplate != nil,
|
||||
signerName: config.SignerName,
|
||||
usages: config.Usages,
|
||||
certStore: config.CertificateStore,
|
||||
cert: cert,
|
||||
@ -424,7 +429,7 @@ func (m *manager) rotateCerts() (bool, error) {
|
||||
|
||||
// Call the Certificate Signing Request API to get a certificate for the
|
||||
// new private key.
|
||||
req, err := csr.RequestCertificate(client, csrPEM, "", m.usages, privateKey)
|
||||
req, err := csr.RequestCertificate(client, csrPEM, "", m.signerName, m.usages, privateKey)
|
||||
if err != nil {
|
||||
utilruntime.HandleError(fmt.Errorf("Failed while requesting a signed certificate from the master: %v", err))
|
||||
if m.certificateRenewFailure != nil {
|
||||
|
@ -46,7 +46,7 @@ import (
|
||||
// status, once approved by API server, it will return the API server's issued
|
||||
// certificate (pem-encoded). If there is any errors, or the watch timeouts, it
|
||||
// will return an error.
|
||||
func RequestCertificate(client certificatesclient.CertificateSigningRequestInterface, csrData []byte, name string, usages []certificates.KeyUsage, privateKey interface{}) (req *certificates.CertificateSigningRequest, err error) {
|
||||
func RequestCertificate(client certificatesclient.CertificateSigningRequestInterface, csrData []byte, name string, signerName string, usages []certificates.KeyUsage, privateKey interface{}) (req *certificates.CertificateSigningRequest, err error) {
|
||||
csr := &certificates.CertificateSigningRequest{
|
||||
// Username, UID, Groups will be injected by API server.
|
||||
TypeMeta: metav1.TypeMeta{Kind: "CertificateSigningRequest"},
|
||||
@ -54,8 +54,9 @@ func RequestCertificate(client certificatesclient.CertificateSigningRequestInter
|
||||
Name: name,
|
||||
},
|
||||
Spec: certificates.CertificateSigningRequestSpec{
|
||||
Request: csrData,
|
||||
Usages: usages,
|
||||
Request: csrData,
|
||||
Usages: usages,
|
||||
SignerName: &signerName,
|
||||
},
|
||||
}
|
||||
if len(csr.Name) == 0 {
|
||||
|
@ -3239,6 +3239,9 @@ func describeCertificateSigningRequest(csr *certificatesv1beta1.CertificateSigni
|
||||
w.Write(LEVEL_0, "Annotations:\t%s\n", labels.FormatLabels(csr.Annotations))
|
||||
w.Write(LEVEL_0, "CreationTimestamp:\t%s\n", csr.CreationTimestamp.Time.Format(time.RFC1123Z))
|
||||
w.Write(LEVEL_0, "Requesting User:\t%s\n", csr.Spec.Username)
|
||||
if csr.Spec.SignerName != nil {
|
||||
w.Write(LEVEL_0, "Signer:\t%s\n", *csr.Spec.SignerName)
|
||||
}
|
||||
w.Write(LEVEL_0, "Status:\t%s\n", status)
|
||||
|
||||
w.Write(LEVEL_0, "Subject:\n")
|
||||
|
@ -41,6 +41,7 @@ filegroup(
|
||||
"//test/integration/auth:all-srcs",
|
||||
"//test/integration/benchmark/extractlog:all-srcs",
|
||||
"//test/integration/benchmark/jsonify:all-srcs",
|
||||
"//test/integration/certificates:all-srcs",
|
||||
"//test/integration/client:all-srcs",
|
||||
"//test/integration/configmap:all-srcs",
|
||||
"//test/integration/cronjob:all-srcs",
|
||||
|
47
test/integration/certificates/BUILD
Normal file
47
test/integration/certificates/BUILD
Normal file
@ -0,0 +1,47 @@
|
||||
load("@io_bazel_rules_go//go:def.bzl", "go_test")
|
||||
|
||||
go_test(
|
||||
name = "go_default_test",
|
||||
srcs = [
|
||||
"admission_approval_test.go",
|
||||
"admission_sign_test.go",
|
||||
"admission_subjectrestriction_test.go",
|
||||
"admission_test.go",
|
||||
"controller_approval_test.go",
|
||||
"defaulting_test.go",
|
||||
"field_selector_test.go",
|
||||
"main_test.go",
|
||||
],
|
||||
tags = ["integration"],
|
||||
deps = [
|
||||
"//cmd/kube-apiserver/app/testing:go_default_library",
|
||||
"//pkg/controller/certificates:go_default_library",
|
||||
"//pkg/controller/certificates/approver:go_default_library",
|
||||
"//staging/src/k8s.io/api/authorization/v1:go_default_library",
|
||||
"//staging/src/k8s.io/api/certificates/v1beta1:go_default_library",
|
||||
"//staging/src/k8s.io/api/rbac/v1:go_default_library",
|
||||
"//staging/src/k8s.io/apimachinery/pkg/apis/meta/v1:go_default_library",
|
||||
"//staging/src/k8s.io/apimachinery/pkg/runtime/schema:go_default_library",
|
||||
"//staging/src/k8s.io/apimachinery/pkg/util/wait:go_default_library",
|
||||
"//staging/src/k8s.io/client-go/informers:go_default_library",
|
||||
"//staging/src/k8s.io/client-go/kubernetes:go_default_library",
|
||||
"//staging/src/k8s.io/client-go/kubernetes/typed/authorization/v1:go_default_library",
|
||||
"//staging/src/k8s.io/client-go/kubernetes/typed/certificates/v1beta1:go_default_library",
|
||||
"//staging/src/k8s.io/client-go/rest:go_default_library",
|
||||
"//test/integration/framework:go_default_library",
|
||||
],
|
||||
)
|
||||
|
||||
filegroup(
|
||||
name = "package-srcs",
|
||||
srcs = glob(["**"]),
|
||||
tags = ["automanaged"],
|
||||
visibility = ["//visibility:private"],
|
||||
)
|
||||
|
||||
filegroup(
|
||||
name = "all-srcs",
|
||||
srcs = [":package-srcs"],
|
||||
tags = ["automanaged"],
|
||||
visibility = ["//visibility:public"],
|
||||
)
|
147
test/integration/certificates/admission_approval_test.go
Normal file
147
test/integration/certificates/admission_approval_test.go
Normal file
@ -0,0 +1,147 @@
|
||||
/*
|
||||
Copyright 2020 The Kubernetes Authors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package certificates
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
certv1beta1 "k8s.io/api/certificates/v1beta1"
|
||||
rbacv1 "k8s.io/api/rbac/v1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
clientset "k8s.io/client-go/kubernetes"
|
||||
restclient "k8s.io/client-go/rest"
|
||||
|
||||
kubeapiservertesting "k8s.io/kubernetes/cmd/kube-apiserver/app/testing"
|
||||
"k8s.io/kubernetes/test/integration/framework"
|
||||
)
|
||||
|
||||
// Verifies that the CSR approval admission plugin correctly enforces that a
|
||||
// user has permission to approve CSRs for the named signer
|
||||
func TestCSRSignerNameApprovalPlugin(t *testing.T) {
|
||||
tests := map[string]struct {
|
||||
allowedSignerName string
|
||||
signerName string
|
||||
error string
|
||||
}{
|
||||
"should admit when a user has permission for the exact signerName": {
|
||||
allowedSignerName: "example.com/something",
|
||||
signerName: "example.com/something",
|
||||
},
|
||||
"should admit when a user has permission for the wildcard-suffixed signerName": {
|
||||
allowedSignerName: "example.com/*",
|
||||
signerName: "example.com/something",
|
||||
},
|
||||
"should deny if a user does not have permission for the given signerName": {
|
||||
allowedSignerName: "example.com/not-something",
|
||||
signerName: "example.com/something",
|
||||
error: `certificatesigningrequests.certificates.k8s.io "csr" is forbidden: user not permitted to approve requests with signerName "example.com/something"`,
|
||||
},
|
||||
}
|
||||
for name, test := range tests {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
// Run an apiserver with the default configuration options.
|
||||
s := kubeapiservertesting.StartTestServerOrDie(t, kubeapiservertesting.NewDefaultTestServerOptions(), []string{"--authorization-mode=RBAC"}, framework.SharedEtcd())
|
||||
defer s.TearDownFn()
|
||||
client := clientset.NewForConfigOrDie(s.ClientConfig)
|
||||
|
||||
// Grant 'test-user' permission to approve CertificateSigningRequests with the specified signerName.
|
||||
const username = "test-user"
|
||||
grantUserPermissionToApproveFor(t, client, username, test.allowedSignerName)
|
||||
// Create a CSR to attempt to approve.
|
||||
csr := createTestingCSR(t, client.CertificatesV1beta1().CertificateSigningRequests(), "csr", test.signerName, "")
|
||||
|
||||
// Create a second client, impersonating the 'test-user' for us to test with.
|
||||
testuserConfig := restclient.CopyConfig(s.ClientConfig)
|
||||
testuserConfig.Impersonate = restclient.ImpersonationConfig{UserName: username}
|
||||
testuserClient := clientset.NewForConfigOrDie(testuserConfig)
|
||||
|
||||
// Attempt to update the Approved condition.
|
||||
csr.Status.Conditions = append(csr.Status.Conditions, certv1beta1.CertificateSigningRequestCondition{
|
||||
Type: certv1beta1.CertificateApproved,
|
||||
Reason: "AutoApproved",
|
||||
Message: "Approved during integration test",
|
||||
})
|
||||
_, err := testuserClient.CertificatesV1beta1().CertificateSigningRequests().UpdateApproval(csr)
|
||||
if err != nil && test.error != err.Error() {
|
||||
t.Errorf("expected error %q but got: %v", test.error, err)
|
||||
}
|
||||
if err == nil && test.error != "" {
|
||||
t.Errorf("expected to get an error %q but got none", test.error)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func grantUserPermissionToApproveFor(t *testing.T, client clientset.Interface, username string, signerNames ...string) {
|
||||
resourceName := "signername-" + username
|
||||
cr := buildApprovalClusterRoleForSigners(resourceName, signerNames...)
|
||||
crb := buildClusterRoleBindingForUser(resourceName, username, cr.Name)
|
||||
if _, err := client.RbacV1().ClusterRoles().Create(context.TODO(), cr, metav1.CreateOptions{}); err != nil {
|
||||
t.Fatalf("unable to create test fixture RBAC rules: %v", err)
|
||||
}
|
||||
if _, err := client.RbacV1().ClusterRoleBindings().Create(context.TODO(), crb, metav1.CreateOptions{}); err != nil {
|
||||
t.Fatalf("unable to create test fixture RBAC rules: %v", err)
|
||||
}
|
||||
approveRule := cr.Rules[0]
|
||||
updateRule := cr.Rules[1]
|
||||
waitForNamedAuthorizationUpdate(t, client.AuthorizationV1(), username, "", approveRule.Verbs[0], approveRule.ResourceNames[0], schema.GroupResource{Group: approveRule.APIGroups[0], Resource: approveRule.Resources[0]}, true)
|
||||
waitForNamedAuthorizationUpdate(t, client.AuthorizationV1(), username, "", updateRule.Verbs[0], "", schema.GroupResource{Group: updateRule.APIGroups[0], Resource: updateRule.Resources[0]}, true)
|
||||
}
|
||||
|
||||
func buildApprovalClusterRoleForSigners(name string, signerNames ...string) *rbacv1.ClusterRole {
|
||||
return &rbacv1.ClusterRole{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: name,
|
||||
},
|
||||
Rules: []rbacv1.PolicyRule{
|
||||
// must have permission to 'approve' the 'certificatesigners' named
|
||||
// 'signerName' to approve CSRs with the given signerName.
|
||||
{
|
||||
Verbs: []string{"approve"},
|
||||
APIGroups: []string{certv1beta1.SchemeGroupVersion.Group},
|
||||
Resources: []string{"signers"},
|
||||
ResourceNames: signerNames,
|
||||
},
|
||||
{
|
||||
Verbs: []string{"update"},
|
||||
APIGroups: []string{certv1beta1.SchemeGroupVersion.Group},
|
||||
Resources: []string{"certificatesigningrequests/approval"},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func buildClusterRoleBindingForUser(name, username, clusterRoleName string) *rbacv1.ClusterRoleBinding {
|
||||
return &rbacv1.ClusterRoleBinding{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: name,
|
||||
},
|
||||
Subjects: []rbacv1.Subject{
|
||||
{
|
||||
Kind: rbacv1.UserKind,
|
||||
Name: username,
|
||||
},
|
||||
},
|
||||
RoleRef: rbacv1.RoleRef{
|
||||
APIGroup: rbacv1.SchemeGroupVersion.Group,
|
||||
Kind: "ClusterRole",
|
||||
Name: clusterRoleName,
|
||||
},
|
||||
}
|
||||
}
|
124
test/integration/certificates/admission_sign_test.go
Normal file
124
test/integration/certificates/admission_sign_test.go
Normal file
@ -0,0 +1,124 @@
|
||||
/*
|
||||
Copyright 2020 The Kubernetes Authors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package certificates
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
certv1beta1 "k8s.io/api/certificates/v1beta1"
|
||||
rbacv1 "k8s.io/api/rbac/v1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
clientset "k8s.io/client-go/kubernetes"
|
||||
restclient "k8s.io/client-go/rest"
|
||||
|
||||
kubeapiservertesting "k8s.io/kubernetes/cmd/kube-apiserver/app/testing"
|
||||
"k8s.io/kubernetes/test/integration/framework"
|
||||
)
|
||||
|
||||
// Verifies that the CSR approval admission plugin correctly enforces that a
|
||||
// user has permission to sign CSRs for the named signer
|
||||
func TestCSRSignerNameSigningPlugin(t *testing.T) {
|
||||
tests := map[string]struct {
|
||||
allowedSignerName string
|
||||
signerName string
|
||||
error string
|
||||
}{
|
||||
"should admit when a user has permission for the exact signerName": {
|
||||
allowedSignerName: "example.com/something",
|
||||
signerName: "example.com/something",
|
||||
},
|
||||
"should admit when a user has permission for the wildcard-suffixed signerName": {
|
||||
allowedSignerName: "example.com/*",
|
||||
signerName: "example.com/something",
|
||||
},
|
||||
"should deny if a user does not have permission for the given signerName": {
|
||||
allowedSignerName: "example.com/not-something",
|
||||
signerName: "example.com/something",
|
||||
error: `certificatesigningrequests.certificates.k8s.io "csr" is forbidden: user not permitted to sign requests with signerName "example.com/something"`,
|
||||
},
|
||||
}
|
||||
for name, test := range tests {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
// Run an apiserver with the default configuration options.
|
||||
s := kubeapiservertesting.StartTestServerOrDie(t, kubeapiservertesting.NewDefaultTestServerOptions(), []string{"--authorization-mode=RBAC"}, framework.SharedEtcd())
|
||||
defer s.TearDownFn()
|
||||
client := clientset.NewForConfigOrDie(s.ClientConfig)
|
||||
|
||||
// Grant 'test-user' permission to sign CertificateSigningRequests with the specified signerName.
|
||||
const username = "test-user"
|
||||
grantUserPermissionToSignFor(t, client, username, test.allowedSignerName)
|
||||
// Create a CSR to attempt to sign.
|
||||
csr := createTestingCSR(t, client.CertificatesV1beta1().CertificateSigningRequests(), "csr", test.signerName, "")
|
||||
|
||||
// Create a second client, impersonating the 'test-user' for us to test with.
|
||||
testuserConfig := restclient.CopyConfig(s.ClientConfig)
|
||||
testuserConfig.Impersonate = restclient.ImpersonationConfig{UserName: username}
|
||||
testuserClient := clientset.NewForConfigOrDie(testuserConfig)
|
||||
|
||||
// Attempt to 'sign' the certificate.
|
||||
csr.Status.Certificate = []byte("dummy data")
|
||||
_, err := testuserClient.CertificatesV1beta1().CertificateSigningRequests().UpdateStatus(context.TODO(), csr, metav1.UpdateOptions{})
|
||||
if err != nil && test.error != err.Error() {
|
||||
t.Errorf("expected error %q but got: %v", test.error, err)
|
||||
}
|
||||
if err == nil && test.error != "" {
|
||||
t.Errorf("expected to get an error %q but got none", test.error)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func grantUserPermissionToSignFor(t *testing.T, client clientset.Interface, username string, signerNames ...string) {
|
||||
resourceName := "signername-" + username
|
||||
cr := buildSigningClusterRoleForSigners(resourceName, signerNames...)
|
||||
crb := buildClusterRoleBindingForUser(resourceName, username, cr.Name)
|
||||
if _, err := client.RbacV1().ClusterRoles().Create(context.TODO(), cr, metav1.CreateOptions{}); err != nil {
|
||||
t.Fatalf("failed to create test fixtures: %v", err)
|
||||
}
|
||||
if _, err := client.RbacV1().ClusterRoleBindings().Create(context.TODO(), crb, metav1.CreateOptions{}); err != nil {
|
||||
t.Fatalf("failed to create test fixtures: %v", err)
|
||||
}
|
||||
signRule := cr.Rules[0]
|
||||
statusRule := cr.Rules[1]
|
||||
waitForNamedAuthorizationUpdate(t, client.AuthorizationV1(), username, "", signRule.Verbs[0], signRule.ResourceNames[0], schema.GroupResource{Group: signRule.APIGroups[0], Resource: signRule.Resources[0]}, true)
|
||||
waitForNamedAuthorizationUpdate(t, client.AuthorizationV1(), username, "", statusRule.Verbs[0], "", schema.GroupResource{Group: statusRule.APIGroups[0], Resource: statusRule.Resources[0]}, true)
|
||||
}
|
||||
|
||||
func buildSigningClusterRoleForSigners(name string, signerNames ...string) *rbacv1.ClusterRole {
|
||||
return &rbacv1.ClusterRole{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: name,
|
||||
},
|
||||
Rules: []rbacv1.PolicyRule{
|
||||
// must have permission to 'approve' the 'certificatesigners' named
|
||||
// 'signerName' to approve CSRs with the given signerName.
|
||||
{
|
||||
Verbs: []string{"sign"},
|
||||
APIGroups: []string{certv1beta1.SchemeGroupVersion.Group},
|
||||
Resources: []string{"signers"},
|
||||
ResourceNames: signerNames,
|
||||
},
|
||||
{
|
||||
Verbs: []string{"update"},
|
||||
APIGroups: []string{certv1beta1.SchemeGroupVersion.Group},
|
||||
Resources: []string{"certificatesigningrequests/status"},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
@ -0,0 +1,70 @@
|
||||
/*
|
||||
Copyright 2020 The Kubernetes Authors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package certificates
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
certv1beta1 "k8s.io/api/certificates/v1beta1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
clientset "k8s.io/client-go/kubernetes"
|
||||
|
||||
kubeapiservertesting "k8s.io/kubernetes/cmd/kube-apiserver/app/testing"
|
||||
"k8s.io/kubernetes/test/integration/framework"
|
||||
)
|
||||
|
||||
// Verifies that the CertificateSubjectRestriction admission controller works as expected.
|
||||
func TestCertificateSubjectRestrictionPlugin(t *testing.T) {
|
||||
tests := map[string]struct {
|
||||
signerName string
|
||||
group string
|
||||
error string
|
||||
}{
|
||||
"should reject a request if signerName is kube-apiserver-client and group is system:masters": {
|
||||
signerName: certv1beta1.KubeAPIServerClientSignerName,
|
||||
group: "system:masters",
|
||||
error: `certificatesigningrequests.certificates.k8s.io "csr" is forbidden: use of kubernetes.io/kube-apiserver-client signer with system:masters group is not allowed`,
|
||||
},
|
||||
"should admit a request if signerName is NOT kube-apiserver-client and org is system:masters": {
|
||||
signerName: certv1beta1.LegacyUnknownSignerName,
|
||||
group: "system:masters",
|
||||
},
|
||||
"should admit a request if signerName is kube-apiserver-client and group is NOT system:masters": {
|
||||
signerName: certv1beta1.KubeAPIServerClientSignerName,
|
||||
group: "system:notmasters",
|
||||
},
|
||||
}
|
||||
for name, test := range tests {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
// Run an apiserver with the default configuration options.
|
||||
s := kubeapiservertesting.StartTestServerOrDie(t, kubeapiservertesting.NewDefaultTestServerOptions(), []string{""}, framework.SharedEtcd())
|
||||
defer s.TearDownFn()
|
||||
client := clientset.NewForConfigOrDie(s.ClientConfig)
|
||||
|
||||
// Attempt to create the CSR resource.
|
||||
csr := buildTestingCSR("csr", test.signerName, test.group)
|
||||
_, err := client.CertificatesV1beta1().CertificateSigningRequests().Create(context.TODO(), csr, metav1.CreateOptions{})
|
||||
if err != nil && test.error != err.Error() {
|
||||
t.Errorf("expected error %q but got: %v", test.error, err)
|
||||
}
|
||||
if err == nil && test.error != "" {
|
||||
t.Errorf("expected to get an error %q but got none", test.error)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
59
test/integration/certificates/admission_test.go
Normal file
59
test/integration/certificates/admission_test.go
Normal file
@ -0,0 +1,59 @@
|
||||
/*
|
||||
Copyright 2020 The Kubernetes Authors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package certificates
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
authorizationv1 "k8s.io/api/authorization/v1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
"k8s.io/apimachinery/pkg/util/wait"
|
||||
v1authorization "k8s.io/client-go/kubernetes/typed/authorization/v1"
|
||||
)
|
||||
|
||||
// waitForNamedAuthorizationUpdate checks if the given user can perform the named verb and action on the named resource.
|
||||
// Copied from k8s.io/kubernetes/test/e2e/framework/auth.
|
||||
func waitForNamedAuthorizationUpdate(t *testing.T, c v1authorization.SubjectAccessReviewsGetter, user, namespace, verb, resourceName string, resource schema.GroupResource, allowed bool) {
|
||||
review := &authorizationv1.SubjectAccessReview{
|
||||
Spec: authorizationv1.SubjectAccessReviewSpec{
|
||||
ResourceAttributes: &authorizationv1.ResourceAttributes{
|
||||
Group: resource.Group,
|
||||
Verb: verb,
|
||||
Resource: resource.Resource,
|
||||
Namespace: namespace,
|
||||
Name: resourceName,
|
||||
},
|
||||
User: user,
|
||||
},
|
||||
}
|
||||
|
||||
if err := wait.Poll(time.Millisecond*100, time.Second*5, func() (bool, error) {
|
||||
response, err := c.SubjectAccessReviews().Create(context.TODO(), review, metav1.CreateOptions{})
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
if response.Status.Allowed != allowed {
|
||||
return false, nil
|
||||
}
|
||||
return true, nil
|
||||
}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
220
test/integration/certificates/controller_approval_test.go
Normal file
220
test/integration/certificates/controller_approval_test.go
Normal file
@ -0,0 +1,220 @@
|
||||
/*
|
||||
Copyright 2020 The Kubernetes Authors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package certificates
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/x509"
|
||||
"crypto/x509/pkix"
|
||||
"fmt"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
certv1beta1 "k8s.io/api/certificates/v1beta1"
|
||||
rbacv1 "k8s.io/api/rbac/v1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
"k8s.io/apimachinery/pkg/util/wait"
|
||||
"k8s.io/client-go/informers"
|
||||
"k8s.io/client-go/kubernetes"
|
||||
clientset "k8s.io/client-go/kubernetes"
|
||||
restclient "k8s.io/client-go/rest"
|
||||
|
||||
kubeapiservertesting "k8s.io/kubernetes/cmd/kube-apiserver/app/testing"
|
||||
"k8s.io/kubernetes/pkg/controller/certificates"
|
||||
"k8s.io/kubernetes/pkg/controller/certificates/approver"
|
||||
"k8s.io/kubernetes/test/integration/framework"
|
||||
)
|
||||
|
||||
// Integration tests that verify the behaviour of the CSR auto-approving controller.
|
||||
func TestController_AutoApproval(t *testing.T) {
|
||||
validKubeAPIServerClientKubeletUsername := "system:node:abc"
|
||||
validKubeAPIServerClientKubeletCSR := pemWithTemplate(&x509.CertificateRequest{
|
||||
Subject: pkix.Name{
|
||||
CommonName: validKubeAPIServerClientKubeletUsername,
|
||||
Organization: []string{"system:nodes"},
|
||||
},
|
||||
})
|
||||
validKubeAPIServerClientKubeletUsages := []certv1beta1.KeyUsage{
|
||||
certv1beta1.UsageDigitalSignature,
|
||||
certv1beta1.UsageKeyEncipherment,
|
||||
certv1beta1.UsageClientAuth,
|
||||
}
|
||||
tests := map[string]struct {
|
||||
signerName string
|
||||
request []byte
|
||||
usages []certv1beta1.KeyUsage
|
||||
username string
|
||||
autoApproved bool
|
||||
grantNodeClient bool
|
||||
grantSelfNodeClient bool
|
||||
}{
|
||||
"should auto-approve CSR that has kube-apiserver-client-kubelet signerName and matches requirements": {
|
||||
signerName: certv1beta1.KubeAPIServerClientKubeletSignerName,
|
||||
request: validKubeAPIServerClientKubeletCSR,
|
||||
usages: validKubeAPIServerClientKubeletUsages,
|
||||
username: validKubeAPIServerClientKubeletUsername,
|
||||
grantSelfNodeClient: true,
|
||||
autoApproved: true,
|
||||
},
|
||||
"should auto-approve CSR that has kube-apiserver-client-kubelet signerName and matches requirements despite missing username if nodeclient permissions are granted": {
|
||||
signerName: certv1beta1.KubeAPIServerClientKubeletSignerName,
|
||||
request: validKubeAPIServerClientKubeletCSR,
|
||||
usages: validKubeAPIServerClientKubeletUsages,
|
||||
username: "does-not-match-cn",
|
||||
grantNodeClient: true,
|
||||
autoApproved: true,
|
||||
},
|
||||
"should not auto-approve CSR that has kube-apiserver-client-kubelet signerName that does not match requirements": {
|
||||
signerName: certv1beta1.KubeAPIServerClientKubeletSignerName,
|
||||
request: pemWithGroup("system:notnodes"),
|
||||
autoApproved: false,
|
||||
},
|
||||
"should not auto-approve CSR that has kube-apiserver-client signerName that DOES match kubelet CSR requirements": {
|
||||
signerName: certv1beta1.KubeAPIServerClientSignerName,
|
||||
request: validKubeAPIServerClientKubeletCSR,
|
||||
usages: validKubeAPIServerClientKubeletUsages,
|
||||
username: validKubeAPIServerClientKubeletUsername,
|
||||
autoApproved: false,
|
||||
},
|
||||
}
|
||||
for name, test := range tests {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
// Run an apiserver with the default configuration options.
|
||||
s := kubeapiservertesting.StartTestServerOrDie(t, kubeapiservertesting.NewDefaultTestServerOptions(), []string{""}, framework.SharedEtcd())
|
||||
defer s.TearDownFn()
|
||||
client := clientset.NewForConfigOrDie(s.ClientConfig)
|
||||
informers := informers.NewSharedInformerFactory(clientset.NewForConfigOrDie(restclient.AddUserAgent(s.ClientConfig, "certificatesigningrequest-informers")), time.Second)
|
||||
|
||||
// Register the controller
|
||||
c := approver.NewCSRApprovingController(client, informers.Certificates().V1beta1().CertificateSigningRequests())
|
||||
// Start the controller & informers
|
||||
stopCh := make(chan struct{})
|
||||
defer close(stopCh)
|
||||
informers.Start(stopCh)
|
||||
go c.Run(1, stopCh)
|
||||
|
||||
// Configure appropriate permissions
|
||||
if test.grantNodeClient {
|
||||
grantUserNodeClientPermissions(t, client, test.username, false)
|
||||
}
|
||||
if test.grantSelfNodeClient {
|
||||
grantUserNodeClientPermissions(t, client, test.username, true)
|
||||
}
|
||||
|
||||
// Use a client that impersonates the test case 'username' to ensure the `spec.username`
|
||||
// field on the CSR is set correctly.
|
||||
impersonationConfig := restclient.CopyConfig(s.ClientConfig)
|
||||
impersonationConfig.Impersonate.UserName = test.username
|
||||
impersonationClient, err := clientset.NewForConfig(impersonationConfig)
|
||||
if err != nil {
|
||||
t.Fatalf("Error in create clientset: %v", err)
|
||||
}
|
||||
csr := &certv1beta1.CertificateSigningRequest{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "csr",
|
||||
},
|
||||
Spec: certv1beta1.CertificateSigningRequestSpec{
|
||||
Request: test.request,
|
||||
Usages: test.usages,
|
||||
SignerName: &test.signerName,
|
||||
},
|
||||
}
|
||||
_, err = impersonationClient.CertificatesV1beta1().CertificateSigningRequests().Create(context.TODO(), csr, metav1.CreateOptions{})
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create testing CSR: %v", err)
|
||||
}
|
||||
|
||||
if test.autoApproved {
|
||||
if err := waitForCertificateRequestApproved(client, csr.Name); err != nil {
|
||||
t.Errorf("failed to wait for CSR to be auto-approved: %v", err)
|
||||
}
|
||||
} else {
|
||||
if err := ensureCertificateRequestNotApproved(client, csr.Name); err != nil {
|
||||
t.Errorf("failed to ensure that CSR was not auto-approved: %v", err)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const (
|
||||
interval = 100 * time.Millisecond
|
||||
timeout = 5 * time.Second
|
||||
)
|
||||
|
||||
func waitForCertificateRequestApproved(client kubernetes.Interface, name string) error {
|
||||
if err := wait.Poll(interval, timeout, func() (bool, error) {
|
||||
csr, err := client.CertificatesV1beta1().CertificateSigningRequests().Get(context.TODO(), name, metav1.GetOptions{})
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
if certificates.IsCertificateRequestApproved(csr) {
|
||||
return true, nil
|
||||
}
|
||||
return false, nil
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func ensureCertificateRequestNotApproved(client kubernetes.Interface, name string) error {
|
||||
// If waiting for the CSR to be approved times out, we class this as 'not auto approved'.
|
||||
// There is currently no way to explicitly check if the CSR has been rejected for auto-approval.
|
||||
err := waitForCertificateRequestApproved(client, name)
|
||||
switch {
|
||||
case err == wait.ErrWaitTimeout:
|
||||
return nil
|
||||
case err == nil:
|
||||
return fmt.Errorf("CertificateSigningRequest was auto-approved")
|
||||
default:
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
func grantUserNodeClientPermissions(t *testing.T, client clientset.Interface, username string, selfNodeClient bool) {
|
||||
resourceType := "certificatesigningrequests/nodeclient"
|
||||
if selfNodeClient {
|
||||
resourceType = "certificatesigningrequests/selfnodeclient"
|
||||
}
|
||||
cr := buildNodeClientRoleForUser("role", resourceType)
|
||||
crb := buildClusterRoleBindingForUser("rolebinding", username, cr.Name)
|
||||
if _, err := client.RbacV1().ClusterRoles().Create(context.TODO(), cr, metav1.CreateOptions{}); err != nil {
|
||||
t.Fatalf("failed to create test fixtures: %v", err)
|
||||
}
|
||||
if _, err := client.RbacV1().ClusterRoleBindings().Create(context.TODO(), crb, metav1.CreateOptions{}); err != nil {
|
||||
t.Fatalf("failed to create test fixtures: %v", err)
|
||||
}
|
||||
rule := cr.Rules[0]
|
||||
waitForNamedAuthorizationUpdate(t, client.AuthorizationV1(), username, "", rule.Verbs[0], "", schema.GroupResource{Group: rule.APIGroups[0], Resource: rule.Resources[0]}, true)
|
||||
}
|
||||
|
||||
func buildNodeClientRoleForUser(name string, resourceType string) *rbacv1.ClusterRole {
|
||||
return &rbacv1.ClusterRole{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: name,
|
||||
},
|
||||
Rules: []rbacv1.PolicyRule{
|
||||
{
|
||||
Verbs: []string{"create"},
|
||||
APIGroups: []string{certv1beta1.SchemeGroupVersion.Group},
|
||||
Resources: []string{resourceType},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
79
test/integration/certificates/defaulting_test.go
Normal file
79
test/integration/certificates/defaulting_test.go
Normal file
@ -0,0 +1,79 @@
|
||||
/*
|
||||
Copyright 2020 The Kubernetes Authors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package certificates
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
capi "k8s.io/api/certificates/v1beta1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
clientset "k8s.io/client-go/kubernetes"
|
||||
restclient "k8s.io/client-go/rest"
|
||||
|
||||
"k8s.io/kubernetes/test/integration/framework"
|
||||
)
|
||||
|
||||
// Verifies that the signerName field defaulting is wired up correctly.
|
||||
// An exhaustive set of test cases for all permutations of the possible
|
||||
// defaulting cases is written as a unit tests in the
|
||||
// `pkg/apis/certificates/...` directory.
|
||||
// This test cases exists to show that the defaulting function is wired up into
|
||||
// the apiserver correctly.
|
||||
func TestCSRSignerNameDefaulting(t *testing.T) {
|
||||
strPtr := func(s string) *string { return &s }
|
||||
tests := map[string]struct {
|
||||
csr capi.CertificateSigningRequestSpec
|
||||
expectedSignerName string
|
||||
}{
|
||||
"defaults to legacy-unknown if not recognised": {
|
||||
csr: capi.CertificateSigningRequestSpec{
|
||||
Request: pemWithGroup(""),
|
||||
Usages: []capi.KeyUsage{capi.UsageKeyEncipherment, capi.UsageDigitalSignature},
|
||||
},
|
||||
expectedSignerName: capi.LegacyUnknownSignerName,
|
||||
},
|
||||
"does not default signerName if an explicit value is provided": {
|
||||
csr: capi.CertificateSigningRequestSpec{
|
||||
Request: pemWithGroup(""),
|
||||
Usages: []capi.KeyUsage{capi.UsageKeyEncipherment, capi.UsageDigitalSignature},
|
||||
SignerName: strPtr("example.com/my-custom-signer"),
|
||||
},
|
||||
expectedSignerName: "example.com/my-custom-signer",
|
||||
},
|
||||
}
|
||||
for name, test := range tests {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
_, s, closeFn := framework.RunAMaster(nil)
|
||||
defer closeFn()
|
||||
client := clientset.NewForConfigOrDie(&restclient.Config{Host: s.URL, ContentConfig: restclient.ContentConfig{GroupVersion: &schema.GroupVersion{Group: "", Version: "v1"}}})
|
||||
csrClient := client.CertificatesV1beta1().CertificateSigningRequests()
|
||||
csr := &capi.CertificateSigningRequest{
|
||||
ObjectMeta: metav1.ObjectMeta{Name: "testcsr"},
|
||||
Spec: test.csr,
|
||||
}
|
||||
csr, err := csrClient.Create(context.TODO(), csr, metav1.CreateOptions{})
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create CSR resource: %v", err)
|
||||
}
|
||||
if *csr.Spec.SignerName != test.expectedSignerName {
|
||||
t.Errorf("expected CSR signerName to be %q but it was %q", test.expectedSignerName, *csr.Spec.SignerName)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
127
test/integration/certificates/field_selector_test.go
Normal file
127
test/integration/certificates/field_selector_test.go
Normal file
@ -0,0 +1,127 @@
|
||||
/*
|
||||
Copyright 2020 The Kubernetes Authors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package certificates
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/ed25519"
|
||||
"crypto/rand"
|
||||
"crypto/x509"
|
||||
"crypto/x509/pkix"
|
||||
"encoding/pem"
|
||||
"testing"
|
||||
|
||||
capi "k8s.io/api/certificates/v1beta1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
clientset "k8s.io/client-go/kubernetes"
|
||||
certclientset "k8s.io/client-go/kubernetes/typed/certificates/v1beta1"
|
||||
restclient "k8s.io/client-go/rest"
|
||||
|
||||
"k8s.io/kubernetes/test/integration/framework"
|
||||
)
|
||||
|
||||
// Verifies that the 'spec.signerName' field can be correctly used as a field selector on LIST requests
|
||||
func TestCSRSignerNameFieldSelector(t *testing.T) {
|
||||
_, s, closeFn := framework.RunAMaster(nil)
|
||||
defer closeFn()
|
||||
|
||||
client := clientset.NewForConfigOrDie(&restclient.Config{Host: s.URL, ContentConfig: restclient.ContentConfig{GroupVersion: &schema.GroupVersion{Group: "", Version: "v1"}}})
|
||||
csrClient := client.CertificatesV1beta1().CertificateSigningRequests()
|
||||
csr1 := createTestingCSR(t, csrClient, "csr-1", "example.com/signer-name-1", "")
|
||||
csr2 := createTestingCSR(t, csrClient, "csr-2", "example.com/signer-name-2", "")
|
||||
// csr3 has the same signerName as csr2 so we can ensure multiple items are returned when running a filtered
|
||||
// LIST call.
|
||||
csr3 := createTestingCSR(t, csrClient, "csr-3", "example.com/signer-name-2", "")
|
||||
|
||||
signerOneList, err := client.CertificatesV1beta1().CertificateSigningRequests().List(context.TODO(), metav1.ListOptions{FieldSelector: "spec.signerName=example.com/signer-name-1"})
|
||||
if err != nil {
|
||||
t.Errorf("unable to list CSRs with spec.signerName=example.com/signer-name-1")
|
||||
return
|
||||
}
|
||||
if len(signerOneList.Items) != 1 {
|
||||
t.Errorf("expected one CSR to be returned but got %d", len(signerOneList.Items))
|
||||
} else if signerOneList.Items[0].Name != csr1.Name {
|
||||
t.Errorf("expected CSR named 'csr-1' to be returned but got %q", signerOneList.Items[0].Name)
|
||||
}
|
||||
|
||||
signerTwoList, err := client.CertificatesV1beta1().CertificateSigningRequests().List(context.TODO(), metav1.ListOptions{FieldSelector: "spec.signerName=example.com/signer-name-2"})
|
||||
if err != nil {
|
||||
t.Errorf("unable to list CSRs with spec.signerName=example.com/signer-name-2")
|
||||
return
|
||||
}
|
||||
if len(signerTwoList.Items) != 2 {
|
||||
t.Errorf("expected one CSR to be returned but got %d", len(signerTwoList.Items))
|
||||
} else if signerTwoList.Items[0].Name != csr2.Name {
|
||||
t.Errorf("expected CSR named 'csr-2' to be returned but got %q", signerTwoList.Items[0].Name)
|
||||
} else if signerTwoList.Items[1].Name != csr3.Name {
|
||||
t.Errorf("expected CSR named 'csr-3' to be returned but got %q", signerTwoList.Items[1].Name)
|
||||
}
|
||||
}
|
||||
|
||||
func createTestingCSR(t *testing.T, certClient certclientset.CertificateSigningRequestInterface, name, signerName, groupName string) *capi.CertificateSigningRequest {
|
||||
csr, err := certClient.Create(context.TODO(), buildTestingCSR(name, signerName, groupName), metav1.CreateOptions{})
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create testing CSR: %v", err)
|
||||
}
|
||||
return csr
|
||||
}
|
||||
|
||||
func buildTestingCSR(name, signerName, groupName string) *capi.CertificateSigningRequest {
|
||||
return &capi.CertificateSigningRequest{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: name,
|
||||
},
|
||||
Spec: capi.CertificateSigningRequestSpec{
|
||||
SignerName: &signerName,
|
||||
Request: pemWithGroup(groupName),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func pemWithGroup(group string) []byte {
|
||||
template := &x509.CertificateRequest{
|
||||
Subject: pkix.Name{
|
||||
Organization: []string{group},
|
||||
},
|
||||
}
|
||||
return pemWithTemplate(template)
|
||||
}
|
||||
|
||||
func pemWithTemplate(template *x509.CertificateRequest) []byte {
|
||||
_, key, err := ed25519.GenerateKey(rand.Reader)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
csrDER, err := x509.CreateCertificateRequest(rand.Reader, template, key)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
csrPemBlock := &pem.Block{
|
||||
Type: "CERTIFICATE REQUEST",
|
||||
Bytes: csrDER,
|
||||
}
|
||||
|
||||
p := pem.EncodeToMemory(csrPemBlock)
|
||||
if p == nil {
|
||||
panic("invalid pem block")
|
||||
}
|
||||
|
||||
return p
|
||||
}
|
27
test/integration/certificates/main_test.go
Normal file
27
test/integration/certificates/main_test.go
Normal file
@ -0,0 +1,27 @@
|
||||
/*
|
||||
Copyright 2017 The Kubernetes Authors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package certificates
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"k8s.io/kubernetes/test/integration/framework"
|
||||
)
|
||||
|
||||
func TestMain(m *testing.M) {
|
||||
framework.EtcdMain(m.Run)
|
||||
}
|
Loading…
Reference in New Issue
Block a user