mirror of
https://github.com/k3s-io/kubernetes.git
synced 2025-07-29 14:37:00 +00:00
Two implmentations of cert renewal
This commit is contained in:
parent
816f2a4868
commit
a53f478d21
151
cmd/kubeadm/app/phases/certs/renewal/certsapi.go
Normal file
151
cmd/kubeadm/app/phases/certs/renewal/certsapi.go
Normal file
@ -0,0 +1,151 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2018 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 renewal
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto"
|
||||||
|
"crypto/rand"
|
||||||
|
"crypto/x509"
|
||||||
|
"crypto/x509/pkix"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
certsapi "k8s.io/api/certificates/v1beta1"
|
||||||
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||||
|
"k8s.io/apimachinery/pkg/fields"
|
||||||
|
"k8s.io/apimachinery/pkg/watch"
|
||||||
|
"k8s.io/client-go/kubernetes"
|
||||||
|
certstype "k8s.io/client-go/kubernetes/typed/certificates/v1beta1"
|
||||||
|
certutil "k8s.io/client-go/util/cert"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
certAPIPrefixName = "kubeadm-cert"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
watchTimeout = 5 * time.Minute
|
||||||
|
)
|
||||||
|
|
||||||
|
// CertsAPIRenewal creates new certificates using the certs API
|
||||||
|
type CertsAPIRenewal struct {
|
||||||
|
client certstype.CertificatesV1beta1Interface
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewCertsAPIRenawal takes a certificate pair to construct the Interface.
|
||||||
|
func NewCertsAPIRenawal(client kubernetes.Interface) Interface {
|
||||||
|
return &CertsAPIRenewal{
|
||||||
|
client: client.CertificatesV1beta1(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Renew takes a certificate using the cert and key.
|
||||||
|
func (r *CertsAPIRenewal) Renew(cfg *certutil.Config) (*x509.Certificate, crypto.PrivateKey, error) {
|
||||||
|
reqTmp := &x509.CertificateRequest{
|
||||||
|
Subject: pkix.Name{
|
||||||
|
CommonName: cfg.CommonName,
|
||||||
|
Organization: cfg.Organization,
|
||||||
|
},
|
||||||
|
DNSNames: cfg.AltNames.DNSNames,
|
||||||
|
IPAddresses: cfg.AltNames.IPs,
|
||||||
|
}
|
||||||
|
|
||||||
|
key, err := certutil.NewPrivateKey()
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, fmt.Errorf("Couldn't create new private key: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
csr, err := x509.CreateCertificateRequest(rand.Reader, reqTmp, key)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, fmt.Errorf("Couldn't create csr: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
usages := make([]certsapi.KeyUsage, len(cfg.Usages))
|
||||||
|
for i, usage := range cfg.Usages {
|
||||||
|
certsAPIUsage, ok := usageMap[usage]
|
||||||
|
if !ok {
|
||||||
|
return nil, nil, fmt.Errorf("unknown key usage %v", usage)
|
||||||
|
}
|
||||||
|
usages[i] = certsAPIUsage
|
||||||
|
}
|
||||||
|
|
||||||
|
k8sCSR := &certsapi.CertificateSigningRequest{
|
||||||
|
ObjectMeta: metav1.ObjectMeta{
|
||||||
|
GenerateName: certAPIPrefixName,
|
||||||
|
},
|
||||||
|
Spec: certsapi.CertificateSigningRequestSpec{
|
||||||
|
Request: csr,
|
||||||
|
Usages: usages,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
req, err := r.client.CertificateSigningRequests().Create(k8sCSR)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, fmt.Errorf("couldn't create certificate signing request: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
watcher, err := r.client.CertificateSigningRequests().Watch(metav1.ListOptions{
|
||||||
|
Watch: true,
|
||||||
|
FieldSelector: fields.Set{"metadata.name": req.Name}.String(),
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, fmt.Errorf("couldn't watch for certificate creation: %v", err)
|
||||||
|
}
|
||||||
|
defer watcher.Stop()
|
||||||
|
|
||||||
|
select {
|
||||||
|
case ev := <-watcher.ResultChan():
|
||||||
|
if ev.Type != watch.Modified {
|
||||||
|
return nil, nil, fmt.Errorf("unexpected event receieved: %q", ev.Type)
|
||||||
|
}
|
||||||
|
case <-time.After(watchTimeout):
|
||||||
|
return nil, nil, errors.New("timeout trying to sign certificate")
|
||||||
|
}
|
||||||
|
|
||||||
|
req, err = r.client.CertificateSigningRequests().Get(req.Name, metav1.GetOptions{})
|
||||||
|
if len(req.Status.Conditions) < 1 {
|
||||||
|
return nil, nil, errors.New("certificate signing request has no statuses")
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: under what circumstances are there more than one?
|
||||||
|
if status := req.Status.Conditions[0].Type; status != certsapi.CertificateApproved {
|
||||||
|
return nil, nil, fmt.Errorf("Unexpected certificate status %v", status)
|
||||||
|
}
|
||||||
|
|
||||||
|
cert, err := x509.ParseCertificate(req.Status.Certificate)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, fmt.Errorf("couldn't parse issued certificate: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return cert, key, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var usageMap = map[x509.ExtKeyUsage]certsapi.KeyUsage{
|
||||||
|
x509.ExtKeyUsageAny: certsapi.UsageAny,
|
||||||
|
x509.ExtKeyUsageServerAuth: certsapi.UsageServerAuth,
|
||||||
|
x509.ExtKeyUsageClientAuth: certsapi.UsageClientAuth,
|
||||||
|
x509.ExtKeyUsageCodeSigning: certsapi.UsageCodeSigning,
|
||||||
|
x509.ExtKeyUsageEmailProtection: certsapi.UsageEmailProtection,
|
||||||
|
x509.ExtKeyUsageIPSECEndSystem: certsapi.UsageIPsecEndSystem,
|
||||||
|
x509.ExtKeyUsageIPSECTunnel: certsapi.UsageIPsecTunnel,
|
||||||
|
x509.ExtKeyUsageIPSECUser: certsapi.UsageIPsecUser,
|
||||||
|
x509.ExtKeyUsageTimeStamping: certsapi.UsageTimestamping,
|
||||||
|
x509.ExtKeyUsageOCSPSigning: certsapi.UsageOCSPSigning,
|
||||||
|
x509.ExtKeyUsageMicrosoftServerGatedCrypto: certsapi.UsageMicrosoftSGC,
|
||||||
|
x509.ExtKeyUsageNetscapeServerGatedCrypto: certsapi.UsageNetscapSGC,
|
||||||
|
}
|
51
cmd/kubeadm/app/phases/certs/renewal/filerenewal.go
Normal file
51
cmd/kubeadm/app/phases/certs/renewal/filerenewal.go
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2018 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 renewal
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto"
|
||||||
|
"crypto/rsa"
|
||||||
|
"crypto/x509"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
certutil "k8s.io/client-go/util/cert"
|
||||||
|
"k8s.io/kubernetes/cmd/kubeadm/app/phases/certs/pkiutil"
|
||||||
|
)
|
||||||
|
|
||||||
|
// FileRenewal renews a certificate using local certs
|
||||||
|
type FileRenewal struct {
|
||||||
|
caCert *x509.Certificate
|
||||||
|
caKey crypto.PrivateKey
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewFileRenewal takes a certificate pair to construct the Interface.
|
||||||
|
func NewFileRenewal(caCert *x509.Certificate, caKey crypto.PrivateKey) Interface {
|
||||||
|
return &FileRenewal{
|
||||||
|
caCert: caCert,
|
||||||
|
caKey: caKey,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Renew takes a certificate using the cert and key
|
||||||
|
func (r *FileRenewal) Renew(cfg *certutil.Config) (*x509.Certificate, crypto.PrivateKey, error) {
|
||||||
|
caKey, ok := r.caKey.(*rsa.PrivateKey)
|
||||||
|
if !ok {
|
||||||
|
return nil, nil, fmt.Errorf("unsupported private key type %t", r.caKey)
|
||||||
|
}
|
||||||
|
|
||||||
|
return pkiutil.NewCertAndKey(r.caCert, caKey, cfg)
|
||||||
|
}
|
61
cmd/kubeadm/app/phases/certs/renewal/filerenewal_test.go
Normal file
61
cmd/kubeadm/app/phases/certs/renewal/filerenewal_test.go
Normal file
@ -0,0 +1,61 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2018 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 renewal
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/x509"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
certutil "k8s.io/client-go/util/cert"
|
||||||
|
"k8s.io/kubernetes/cmd/kubeadm/app/phases/certs"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestFileRenew(t *testing.T) {
|
||||||
|
caCertCfg := &certutil.Config{CommonName: "kubernetes"}
|
||||||
|
caCert, caKey, err := certs.NewCACertAndKey(caCertCfg)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("couldn't create CA: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
fr := NewFileRenewal(caCert, caKey)
|
||||||
|
|
||||||
|
certCfg := &certutil.Config{
|
||||||
|
CommonName: "test-certs",
|
||||||
|
AltNames: certutil.AltNames{
|
||||||
|
DNSNames: []string{"test-domain.space"},
|
||||||
|
},
|
||||||
|
Usages: []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth},
|
||||||
|
}
|
||||||
|
|
||||||
|
cert, _, err := fr.Renew(certCfg)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unexpected error renewing cert: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
pool := x509.NewCertPool()
|
||||||
|
pool.AddCert(caCert)
|
||||||
|
|
||||||
|
_, err = cert.Verify(x509.VerifyOptions{
|
||||||
|
DNSName: "test-domain.space",
|
||||||
|
Roots: pool,
|
||||||
|
KeyUsages: []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth},
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("couldn't verify new cert: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
29
cmd/kubeadm/app/phases/certs/renewal/interface.go
Normal file
29
cmd/kubeadm/app/phases/certs/renewal/interface.go
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2018 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 renewal
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto"
|
||||||
|
"crypto/x509"
|
||||||
|
|
||||||
|
certutil "k8s.io/client-go/util/cert"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Interface represents a standard way to renew a certificate.
|
||||||
|
type Interface interface {
|
||||||
|
Renew(*certutil.Config) (*x509.Certificate, crypto.PrivateKey, error)
|
||||||
|
}
|
133
cmd/kubeadm/app/phases/certs/renewal/renewal_test.go
Normal file
133
cmd/kubeadm/app/phases/certs/renewal/renewal_test.go
Normal file
@ -0,0 +1,133 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2018 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 renewal
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/rsa"
|
||||||
|
"crypto/x509"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
certsapi "k8s.io/api/certificates/v1beta1"
|
||||||
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||||
|
"k8s.io/apimachinery/pkg/runtime"
|
||||||
|
"k8s.io/apimachinery/pkg/watch"
|
||||||
|
fakecerts "k8s.io/client-go/kubernetes/typed/certificates/v1beta1/fake"
|
||||||
|
k8stesting "k8s.io/client-go/testing"
|
||||||
|
certutil "k8s.io/client-go/util/cert"
|
||||||
|
"k8s.io/kubernetes/cmd/kubeadm/app/phases/certs"
|
||||||
|
"k8s.io/kubernetes/cmd/kubeadm/app/phases/certs/pkiutil"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestRenewImplementations(t *testing.T) {
|
||||||
|
caCertCfg := &certutil.Config{CommonName: "kubernetes"}
|
||||||
|
caCert, caKey, err := certs.NewCACertAndKey(caCertCfg)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("couldn't create CA: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
client := &fakecerts.FakeCertificatesV1beta1{
|
||||||
|
Fake: &k8stesting.Fake{},
|
||||||
|
}
|
||||||
|
certReq := getCertReq(t, caCert, caKey)
|
||||||
|
client.AddReactor("get", "certificatesigningrequests", defaultReactionFunc(certReq))
|
||||||
|
watcher := watch.NewFakeWithChanSize(1, false)
|
||||||
|
watcher.Modify(certReq)
|
||||||
|
client.AddWatchReactor("certificatesigningrequests", k8stesting.DefaultWatchReactor(watcher, nil))
|
||||||
|
|
||||||
|
// override the timeout so tests are faster
|
||||||
|
watchTimeout = time.Second
|
||||||
|
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
impl Interface
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "filerenewal",
|
||||||
|
impl: NewFileRenewal(caCert, caKey),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "certs api",
|
||||||
|
impl: &CertsAPIRenewal{
|
||||||
|
client: client,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, test := range tests {
|
||||||
|
t.Run(test.name, func(t *testing.T) {
|
||||||
|
|
||||||
|
certCfg := &certutil.Config{
|
||||||
|
CommonName: "test-certs",
|
||||||
|
AltNames: certutil.AltNames{
|
||||||
|
DNSNames: []string{"test-domain.space"},
|
||||||
|
},
|
||||||
|
Usages: []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth},
|
||||||
|
}
|
||||||
|
|
||||||
|
cert, _, err := test.impl.Renew(certCfg)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unexpected error renewing cert: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
pool := x509.NewCertPool()
|
||||||
|
pool.AddCert(caCert)
|
||||||
|
|
||||||
|
_, err = cert.Verify(x509.VerifyOptions{
|
||||||
|
DNSName: "test-domain.space",
|
||||||
|
Roots: pool,
|
||||||
|
KeyUsages: []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth},
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("couldn't verify new cert: %v", err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func defaultReactionFunc(obj runtime.Object) k8stesting.ReactionFunc {
|
||||||
|
return func(act k8stesting.Action) (bool, runtime.Object, error) {
|
||||||
|
return true, obj, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func getCertReq(t *testing.T, caCert *x509.Certificate, caKey *rsa.PrivateKey) *certsapi.CertificateSigningRequest {
|
||||||
|
cert, _, err := pkiutil.NewCertAndKey(caCert, caKey, &certutil.Config{
|
||||||
|
CommonName: "testcert",
|
||||||
|
AltNames: certutil.AltNames{
|
||||||
|
DNSNames: []string{"test-domain.space"},
|
||||||
|
},
|
||||||
|
Usages: []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth},
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("couldn't generate cert: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return &certsapi.CertificateSigningRequest{
|
||||||
|
ObjectMeta: metav1.ObjectMeta{
|
||||||
|
Name: "testcert",
|
||||||
|
},
|
||||||
|
Status: certsapi.CertificateSigningRequestStatus{
|
||||||
|
Conditions: []certsapi.CertificateSigningRequestCondition{
|
||||||
|
certsapi.CertificateSigningRequestCondition{
|
||||||
|
Type: certsapi.CertificateApproved,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Certificate: cert.Raw,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user