mirror of
https://github.com/k3s-io/kubernetes.git
synced 2025-07-26 21:17:23 +00:00
certificates: update controllers to understand signerName field
Signed-off-by: James Munnelly <james.munnelly@jetstack.io>
This commit is contained in:
parent
d7e10f9869
commit
d5dae04898
@ -13,6 +13,7 @@ go_library(
|
|||||||
importpath = "k8s.io/kubernetes/pkg/controller/certificates",
|
importpath = "k8s.io/kubernetes/pkg/controller/certificates",
|
||||||
visibility = ["//visibility:public"],
|
visibility = ["//visibility:public"],
|
||||||
deps = [
|
deps = [
|
||||||
|
"//pkg/apis/certificates/v1beta1:go_default_library",
|
||||||
"//pkg/controller:go_default_library",
|
"//pkg/controller:go_default_library",
|
||||||
"//staging/src/k8s.io/api/certificates/v1beta1:go_default_library",
|
"//staging/src/k8s.io/api/certificates/v1beta1:go_default_library",
|
||||||
"//staging/src/k8s.io/apimachinery/pkg/api/errors:go_default_library",
|
"//staging/src/k8s.io/apimachinery/pkg/api/errors:go_default_library",
|
||||||
|
@ -21,14 +21,13 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"crypto/x509"
|
"crypto/x509"
|
||||||
"fmt"
|
"fmt"
|
||||||
"reflect"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
authorization "k8s.io/api/authorization/v1"
|
authorization "k8s.io/api/authorization/v1"
|
||||||
capi "k8s.io/api/certificates/v1beta1"
|
capi "k8s.io/api/certificates/v1beta1"
|
||||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||||
certificatesinformers "k8s.io/client-go/informers/certificates/v1beta1"
|
certificatesinformers "k8s.io/client-go/informers/certificates/v1beta1"
|
||||||
clientset "k8s.io/client-go/kubernetes"
|
clientset "k8s.io/client-go/kubernetes"
|
||||||
|
|
||||||
capihelper "k8s.io/kubernetes/pkg/apis/certificates/v1beta1"
|
capihelper "k8s.io/kubernetes/pkg/apis/certificates/v1beta1"
|
||||||
"k8s.io/kubernetes/pkg/controller/certificates"
|
"k8s.io/kubernetes/pkg/controller/certificates"
|
||||||
)
|
)
|
||||||
@ -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 {
|
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
|
return false
|
||||||
}
|
}
|
||||||
if len(x509cr.DNSNames) > 0 || len(x509cr.EmailAddresses) > 0 || len(x509cr.IPAddresses) > 0 || len(x509cr.URIs) > 0 {
|
return *csr.Spec.SignerName == capi.KubeAPIServerClientKubeletSignerName
|
||||||
return false
|
|
||||||
}
|
|
||||||
if !hasExactUsages(csr, kubeletClientUsages) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
if !strings.HasPrefix(x509cr.Subject.CommonName, "system:node:") {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
return true
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func isSelfNodeClientCert(csr *capi.CertificateSigningRequest, x509cr *x509.CertificateRequest) bool {
|
func isSelfNodeClientCert(csr *capi.CertificateSigningRequest, x509cr *x509.CertificateRequest) bool {
|
||||||
|
@ -36,54 +36,6 @@ import (
|
|||||||
k8s_certificates_v1beta1 "k8s.io/kubernetes/pkg/apis/certificates/v1beta1"
|
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) {
|
func TestHandle(t *testing.T) {
|
||||||
cases := []struct {
|
cases := []struct {
|
||||||
allowed bool
|
allowed bool
|
||||||
@ -208,6 +160,12 @@ func TestRecognizers(t *testing.T) {
|
|||||||
func(b *csrBuilder) {
|
func(b *csrBuilder) {
|
||||||
b.usages = append(b.usages, capi.UsageServerAuth)
|
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)
|
testRecognizer(t, badCases, isNodeClientCert, false)
|
||||||
@ -230,6 +188,7 @@ 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) {
|
func testRecognizer(t *testing.T, cases []func(b *csrBuilder), recognizeFunc func(csr *capi.CertificateSigningRequest, x509cr *x509.CertificateRequest) bool, shouldRecognize bool) {
|
||||||
for _, c := range cases {
|
for _, c := range cases {
|
||||||
b := csrBuilder{
|
b := csrBuilder{
|
||||||
|
signerName: capi.KubeAPIServerClientKubeletSignerName,
|
||||||
cn: "system:node:foo",
|
cn: "system:node:foo",
|
||||||
orgs: []string{"system:nodes"},
|
orgs: []string{"system:nodes"},
|
||||||
requestor: "system:node:foo",
|
requestor: "system:node:foo",
|
||||||
@ -269,6 +228,7 @@ type csrBuilder struct {
|
|||||||
dns []string
|
dns []string
|
||||||
emails []string
|
emails []string
|
||||||
ips []net.IP
|
ips []net.IP
|
||||||
|
signerName string
|
||||||
}
|
}
|
||||||
|
|
||||||
func makeFancyTestCsr(b csrBuilder) *capi.CertificateSigningRequest {
|
func makeFancyTestCsr(b csrBuilder) *capi.CertificateSigningRequest {
|
||||||
@ -292,6 +252,7 @@ func makeFancyTestCsr(b csrBuilder) *capi.CertificateSigningRequest {
|
|||||||
Spec: capi.CertificateSigningRequestSpec{
|
Spec: capi.CertificateSigningRequestSpec{
|
||||||
Username: b.requestor,
|
Username: b.requestor,
|
||||||
Usages: b.usages,
|
Usages: b.usages,
|
||||||
|
SignerName: &b.signerName,
|
||||||
Request: pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE REQUEST", Bytes: csrb}),
|
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/tools/record"
|
||||||
"k8s.io/client-go/util/workqueue"
|
"k8s.io/client-go/util/workqueue"
|
||||||
"k8s.io/klog"
|
"k8s.io/klog"
|
||||||
|
capihelper "k8s.io/kubernetes/pkg/apis/certificates/v1beta1"
|
||||||
"k8s.io/kubernetes/pkg/controller"
|
"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
|
// need to operate on a copy so we don't mutate the csr in the shared cache
|
||||||
csr = csr.DeepCopy()
|
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)
|
return cc.handler(csr)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -16,9 +16,12 @@ go_test(
|
|||||||
],
|
],
|
||||||
embed = [":go_default_library"],
|
embed = [":go_default_library"],
|
||||||
deps = [
|
deps = [
|
||||||
|
"//pkg/apis/certificates/v1beta1:go_default_library",
|
||||||
"//staging/src/k8s.io/api/certificates/v1beta1:go_default_library",
|
"//staging/src/k8s.io/api/certificates/v1beta1:go_default_library",
|
||||||
"//staging/src/k8s.io/apimachinery/pkg/util/clock: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/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",
|
"//staging/src/k8s.io/client-go/util/cert:go_default_library",
|
||||||
"//vendor/github.com/google/go-cmp/cmp:go_default_library",
|
"//vendor/github.com/google/go-cmp/cmp:go_default_library",
|
||||||
],
|
],
|
||||||
|
@ -19,8 +19,10 @@ package signer
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"crypto/x509"
|
||||||
"encoding/pem"
|
"encoding/pem"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
capi "k8s.io/api/certificates/v1beta1"
|
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 {
|
func (s *signer) handle(csr *capi.CertificateSigningRequest) error {
|
||||||
|
// Ignore unapproved requests
|
||||||
if !certificates.IsCertificateRequestApproved(csr) {
|
if !certificates.IsCertificateRequestApproved(csr) {
|
||||||
return nil
|
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 {
|
if err != nil {
|
||||||
return fmt.Errorf("error auto signing csr: %v", err)
|
return fmt.Errorf("error auto signing csr: %v", err)
|
||||||
}
|
}
|
||||||
|
csr.Status.Certificate = cert
|
||||||
_, err = s.client.CertificatesV1beta1().CertificateSigningRequests().UpdateStatus(context.TODO(), csr, metav1.UpdateOptions{})
|
_, err = s.client.CertificatesV1beta1().CertificateSigningRequests().UpdateStatus(context.TODO(), csr, metav1.UpdateOptions{})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("error updating signature for csr: %v", err)
|
return fmt.Errorf("error updating signature for csr: %v", err)
|
||||||
@ -103,23 +123,50 @@ func (s *signer) handle(csr *capi.CertificateSigningRequest) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *signer) sign(csr *capi.CertificateSigningRequest) (*capi.CertificateSigningRequest, error) {
|
func (s *signer) sign(x509cr *x509.CertificateRequest, usages []capi.KeyUsage) ([]byte, error) {
|
||||||
x509cr, err := capihelper.ParseCSR(csr.Spec.Request)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("unable to parse csr %q: %v", csr.Name, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
currCA, err := s.caProvider.currentCA()
|
currCA, err := s.caProvider.currentCA()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
der, err := currCA.Sign(x509cr.Raw, authority.PermissiveSigningPolicy{
|
der, err := currCA.Sign(x509cr.Raw, authority.PermissiveSigningPolicy{
|
||||||
TTL: s.certTTL,
|
TTL: s.certTTL,
|
||||||
Usages: csr.Spec.Usages,
|
Usages: usages,
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
csr.Status.Certificate = pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: der})
|
return pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: der}), nil
|
||||||
return csr, 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
|
package signer
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"crypto/ecdsa"
|
||||||
|
"crypto/elliptic"
|
||||||
"crypto/x509"
|
"crypto/x509"
|
||||||
"crypto/x509/pkix"
|
"crypto/x509/pkix"
|
||||||
|
"encoding/pem"
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
|
"math/rand"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@ -28,7 +32,11 @@ import (
|
|||||||
capi "k8s.io/api/certificates/v1beta1"
|
capi "k8s.io/api/certificates/v1beta1"
|
||||||
"k8s.io/apimachinery/pkg/util/clock"
|
"k8s.io/apimachinery/pkg/util/clock"
|
||||||
"k8s.io/apimachinery/pkg/util/diff"
|
"k8s.io/apimachinery/pkg/util/diff"
|
||||||
|
"k8s.io/client-go/kubernetes/fake"
|
||||||
|
testclient "k8s.io/client-go/testing"
|
||||||
"k8s.io/client-go/util/cert"
|
"k8s.io/client-go/util/cert"
|
||||||
|
|
||||||
|
capihelper "k8s.io/kubernetes/pkg/apis/certificates/v1beta1"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestSigner(t *testing.T) {
|
func TestSigner(t *testing.T) {
|
||||||
@ -50,24 +58,20 @@ func TestSigner(t *testing.T) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("failed to read CSR: %v", err)
|
t.Fatalf("failed to read CSR: %v", err)
|
||||||
}
|
}
|
||||||
|
x509cr, err := capihelper.ParseCSR(csrb)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed to parse CSR: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
csr := &capi.CertificateSigningRequest{
|
certData, err := s.sign(x509cr, []capi.KeyUsage{
|
||||||
Spec: capi.CertificateSigningRequestSpec{
|
|
||||||
Request: []byte(csrb),
|
|
||||||
Usages: []capi.KeyUsage{
|
|
||||||
capi.UsageSigning,
|
capi.UsageSigning,
|
||||||
capi.UsageKeyEncipherment,
|
capi.UsageKeyEncipherment,
|
||||||
capi.UsageServerAuth,
|
capi.UsageServerAuth,
|
||||||
capi.UsageClientAuth,
|
capi.UsageClientAuth,
|
||||||
},
|
})
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
csr, err = s.sign(csr)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("failed to sign CSR: %v", err)
|
t.Fatalf("failed to sign CSR: %v", err)
|
||||||
}
|
}
|
||||||
certData := csr.Status.Certificate
|
|
||||||
if len(certData) == 0 {
|
if len(certData) == 0 {
|
||||||
t.Fatalf("expected a certificate after signing")
|
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()))
|
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
|
||||||
|
}
|
||||||
|
@ -344,7 +344,7 @@ func requestNodeCertificate(client certificatesv1beta1.CertificateSigningRequest
|
|||||||
return nil, err
|
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 {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
@ -107,6 +107,7 @@ func NewKubeletServerCertificateManager(kubeClient clientset.Interface, kubeCfg
|
|||||||
return certSigningRequestClient, nil
|
return certSigningRequestClient, nil
|
||||||
},
|
},
|
||||||
GetTemplate: getTemplate,
|
GetTemplate: getTemplate,
|
||||||
|
SignerName: certificates.KubeletServingSignerName,
|
||||||
Usages: []certificates.KeyUsage{
|
Usages: []certificates.KeyUsage{
|
||||||
// https://tools.ietf.org/html/rfc5280#section-4.2.1.3
|
// https://tools.ietf.org/html/rfc5280#section-4.2.1.3
|
||||||
//
|
//
|
||||||
@ -238,6 +239,7 @@ func NewKubeletClientCertificateManager(
|
|||||||
Organization: []string{"system:nodes"},
|
Organization: []string{"system:nodes"},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
SignerName: certificates.KubeAPIServerClientKubeletSignerName,
|
||||||
Usages: []certificates.KeyUsage{
|
Usages: []certificates.KeyUsage{
|
||||||
// https://tools.ietf.org/html/rfc5280#section-4.2.1.3
|
// https://tools.ietf.org/html/rfc5280#section-4.2.1.3
|
||||||
//
|
//
|
||||||
|
@ -85,6 +85,9 @@ type Config struct {
|
|||||||
// If no template is available, nil may be returned, and no certificate will be requested.
|
// If no template is available, nil may be returned, and no certificate will be requested.
|
||||||
// If specified, takes precedence over Template.
|
// If specified, takes precedence over Template.
|
||||||
GetTemplate func() *x509.CertificateRequest
|
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
|
// Usages is the types of usages that certificates generated by the manager
|
||||||
// can be used for.
|
// can be used for.
|
||||||
Usages []certificates.KeyUsage
|
Usages []certificates.KeyUsage
|
||||||
@ -174,6 +177,7 @@ type manager struct {
|
|||||||
lastRequest *x509.CertificateRequest
|
lastRequest *x509.CertificateRequest
|
||||||
|
|
||||||
dynamicTemplate bool
|
dynamicTemplate bool
|
||||||
|
signerName string
|
||||||
usages []certificates.KeyUsage
|
usages []certificates.KeyUsage
|
||||||
forceRotation bool
|
forceRotation bool
|
||||||
|
|
||||||
@ -219,6 +223,7 @@ func NewManager(config *Config) (Manager, error) {
|
|||||||
clientFn: config.ClientFn,
|
clientFn: config.ClientFn,
|
||||||
getTemplate: getTemplate,
|
getTemplate: getTemplate,
|
||||||
dynamicTemplate: config.GetTemplate != nil,
|
dynamicTemplate: config.GetTemplate != nil,
|
||||||
|
signerName: config.SignerName,
|
||||||
usages: config.Usages,
|
usages: config.Usages,
|
||||||
certStore: config.CertificateStore,
|
certStore: config.CertificateStore,
|
||||||
cert: cert,
|
cert: cert,
|
||||||
@ -424,7 +429,7 @@ func (m *manager) rotateCerts() (bool, error) {
|
|||||||
|
|
||||||
// Call the Certificate Signing Request API to get a certificate for the
|
// Call the Certificate Signing Request API to get a certificate for the
|
||||||
// new private key.
|
// 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 {
|
if err != nil {
|
||||||
utilruntime.HandleError(fmt.Errorf("Failed while requesting a signed certificate from the master: %v", err))
|
utilruntime.HandleError(fmt.Errorf("Failed while requesting a signed certificate from the master: %v", err))
|
||||||
if m.certificateRenewFailure != nil {
|
if m.certificateRenewFailure != nil {
|
||||||
|
@ -46,7 +46,7 @@ import (
|
|||||||
// status, once approved by API server, it will return the API server's issued
|
// 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
|
// certificate (pem-encoded). If there is any errors, or the watch timeouts, it
|
||||||
// will return an error.
|
// 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{
|
csr := &certificates.CertificateSigningRequest{
|
||||||
// Username, UID, Groups will be injected by API server.
|
// Username, UID, Groups will be injected by API server.
|
||||||
TypeMeta: metav1.TypeMeta{Kind: "CertificateSigningRequest"},
|
TypeMeta: metav1.TypeMeta{Kind: "CertificateSigningRequest"},
|
||||||
@ -56,6 +56,7 @@ func RequestCertificate(client certificatesclient.CertificateSigningRequestInter
|
|||||||
Spec: certificates.CertificateSigningRequestSpec{
|
Spec: certificates.CertificateSigningRequestSpec{
|
||||||
Request: csrData,
|
Request: csrData,
|
||||||
Usages: usages,
|
Usages: usages,
|
||||||
|
SignerName: &signerName,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
if len(csr.Name) == 0 {
|
if len(csr.Name) == 0 {
|
||||||
|
@ -7,6 +7,7 @@ go_test(
|
|||||||
"admission_sign_test.go",
|
"admission_sign_test.go",
|
||||||
"admission_subjectrestriction_test.go",
|
"admission_subjectrestriction_test.go",
|
||||||
"admission_test.go",
|
"admission_test.go",
|
||||||
|
"controller_approval_test.go",
|
||||||
"defaulting_test.go",
|
"defaulting_test.go",
|
||||||
"field_selector_test.go",
|
"field_selector_test.go",
|
||||||
"main_test.go",
|
"main_test.go",
|
||||||
@ -14,12 +15,15 @@ go_test(
|
|||||||
tags = ["integration"],
|
tags = ["integration"],
|
||||||
deps = [
|
deps = [
|
||||||
"//cmd/kube-apiserver/app/testing:go_default_library",
|
"//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/authorization/v1:go_default_library",
|
||||||
"//staging/src/k8s.io/api/certificates/v1beta1:go_default_library",
|
"//staging/src/k8s.io/api/certificates/v1beta1:go_default_library",
|
||||||
"//staging/src/k8s.io/api/rbac/v1:go_default_library",
|
"//staging/src/k8s.io/api/rbac/v1:go_default_library",
|
||||||
"//staging/src/k8s.io/apimachinery/pkg/apis/meta/v1:go_default_library",
|
"//staging/src/k8s.io/apimachinery/pkg/apis/meta/v1:go_default_library",
|
||||||
"//staging/src/k8s.io/apimachinery/pkg/runtime/schema:go_default_library",
|
"//staging/src/k8s.io/apimachinery/pkg/runtime/schema:go_default_library",
|
||||||
"//staging/src/k8s.io/apimachinery/pkg/util/wait:go_default_library",
|
"//staging/src/k8s.io/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:go_default_library",
|
||||||
"//staging/src/k8s.io/client-go/kubernetes/typed/authorization/v1:go_default_library",
|
"//staging/src/k8s.io/client-go/kubernetes/typed/authorization/v1:go_default_library",
|
||||||
"//staging/src/k8s.io/client-go/kubernetes/typed/certificates/v1beta1:go_default_library",
|
"//staging/src/k8s.io/client-go/kubernetes/typed/certificates/v1beta1:go_default_library",
|
||||||
|
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},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user