From 748a0281617f6605637473b2956c97fda0b2f29a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mika=C3=ABl=20Cluseau?= Date: Wed, 22 Apr 2020 17:36:04 +0200 Subject: [PATCH] tls: automatic certificate renewal --- Dockerfile | 2 +- cmd/dkl-local-server/secrets.go | 72 +++++++++++++++++++++++++++-- cmd/dkl-local-server/ws-clusters.go | 36 +++++++++++++++ cmd/dkl-local-server/ws.go | 8 ++++ pkg/mime/mime.go | 14 +++--- 5 files changed, 121 insertions(+), 11 deletions(-) diff --git a/Dockerfile b/Dockerfile index 2cbfb3d..f4b69ae 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,5 +1,5 @@ # ------------------------------------------------------------------------ -from mcluseau/golang-builder:1.14.0 as build +from mcluseau/golang-builder:1.14.1 as build # ------------------------------------------------------------------------ from debian:stretch diff --git a/cmd/dkl-local-server/secrets.go b/cmd/dkl-local-server/secrets.go index c7af48c..08fb343 100644 --- a/cmd/dkl-local-server/secrets.go +++ b/cmd/dkl-local-server/secrets.go @@ -1,7 +1,9 @@ package main import ( + "crypto" "crypto/rand" + "crypto/x509" "encoding/base32" "encoding/json" "errors" @@ -12,8 +14,10 @@ import ( "path/filepath" "sort" "sync" + "time" "github.com/cespare/xxhash" + "github.com/cloudflare/cfssl/certinfo" "github.com/cloudflare/cfssl/config" "github.com/cloudflare/cfssl/csr" "github.com/cloudflare/cfssl/helpers" @@ -212,11 +216,49 @@ func (sd *SecretData) Token(cluster, name string) (token string, err error) { return } +func (sd *SecretData) RenewCACert(cluster, name string) (err error) { + cs := sd.cluster(cluster) + + ca := cs.CAs[name] + + var cert *x509.Certificate + cert, err = helpers.ParseCertificatePEM(ca.Cert) + if err != nil { + return + } + + var signer crypto.Signer + signer, err = helpers.ParsePrivateKeyPEM(ca.Key) + if err != nil { + return + } + + newCert, err := initca.RenewFromSigner(cert, signer) + if err != nil { + return + } + + sd.l.Lock() + defer sd.l.Unlock() + + cs.CAs[name].Cert = newCert + sd.changed = true + + return +} + func (sd *SecretData) CA(cluster, name string) (ca *CA, err error) { cs := sd.cluster(cluster) ca, ok := cs.CAs[name] if ok { + checkErr := checkCertUsable(ca.Cert) + if checkErr == nil { + log.Infof("secret-data cluster %s: CA %s: regenerating certificate: %v", cluster, name, checkErr) + + err = sd.RenewCACert(cluster, name) + } + return } @@ -256,6 +298,22 @@ func (sd *SecretData) CA(cluster, name string) (ca *CA, err error) { return } +func checkCertUsable(certPEM []byte) error { + cert, err := certinfo.ParseCertificatePEM(certPEM) + if err != nil { + return err + } + + certDuration := cert.NotAfter.Sub(cert.NotBefore) + delayBeforeRegen := certDuration / 3 // TODO allow configuration + + if cert.NotAfter.Sub(time.Now()) < delayBeforeRegen { + return errors.New("too old") + } + + return nil +} + func (sd *SecretData) KeyCert(cluster, caName, name, profile, label string, req *csr.CertificateRequest) (kc *KeyCert, err error) { for idx, host := range req.Hosts { if ip := net.ParseIP(host); ip != nil { @@ -288,15 +346,21 @@ func (sd *SecretData) KeyCert(cluster, caName, name, profile, label string, req return } + logPrefix := fmt.Sprintf("secret-data: cluster %s: CA %s:", cluster, caName) + rh := hash(req) kc, ok := ca.Signed[name] if ok && rh == kc.ReqHash { - return + err = checkCertUsable(kc.Cert) + if err == nil { + return + } + log.Infof("%s regenerating certificate: ", err) + } else if ok { - log.Infof("secret-data: cluster %s: CA %s: CSR changed for %s: hash=%q previous=%q", - cluster, caName, name, rh, kc.ReqHash) + log.Infof("%s CSR changed for %s: hash=%q previous=%q", name, rh, kc.ReqHash) } else { - log.Infof("secret-data: cluster %s: CA %s: new CSR for %s", cluster, caName, name) + log.Infof("%s new CSR for %s", logPrefix, name) } sd.l.Lock() diff --git a/cmd/dkl-local-server/ws-clusters.go b/cmd/dkl-local-server/ws-clusters.go index 763debf..0f39ba7 100644 --- a/cmd/dkl-local-server/ws-clusters.go +++ b/cmd/dkl-local-server/ws-clusters.go @@ -135,3 +135,39 @@ func wsClusterBootstrapPods(req *restful.Request, resp *restful.Response) { wsRender(resp, cluster.BootstrapPods, cluster) } + +func wsClusterCACert(req *restful.Request, resp *restful.Response) { + cluster := wsReadCluster(req, resp) + if cluster == nil { + return + } + + ca, err := secretData.CA(req.PathParameter("cluster"), req.PathParameter("ca-name")) + if err != nil { + wsError(resp, err) + return + } + + resp.Write(ca.Cert) +} + +func wsClusterSignedCert(req *restful.Request, resp *restful.Response) { + cluster := wsReadCluster(req, resp) + if cluster == nil { + return + } + + ca, err := secretData.CA(req.PathParameter("cluster"), req.PathParameter("ca-name")) + if err != nil { + wsError(resp, err) + return + } + + kc := ca.Signed[req.QueryParameter("name")] + if kc == nil { + wsNotFound(req, resp) + return + } + + resp.Write(kc.Cert) +} diff --git a/cmd/dkl-local-server/ws.go b/cmd/dkl-local-server/ws.go index 2c248ee..2b88ade 100644 --- a/cmd/dkl-local-server/ws.go +++ b/cmd/dkl-local-server/ws.go @@ -49,6 +49,14 @@ func registerWS(rest *restful.Container) { ws.Route(ws.PUT("/clusters/{cluster-name}/passwords/{password-name}").To(wsClusterSetPassword). Doc("Set cluster's password")) + ws.Route(ws.GET("/clusters/{cluster-name}/ca/{ca-name}/certificate").To(wsClusterCACert). + Produces(mime.CACERT). + Doc("Get cluster CA's certificate")) + ws.Route(ws.GET("/clusters/{cluster-name}/ca/{ca-name}/signed").To(wsClusterSignedCert). + Produces(mime.CERT). + Param(ws.QueryParameter("name", "signed reference name").Required(true)). + Doc("Get cluster's certificate signed by the CA")) + ws.Route(ws.GET("/clusters/{cluster-name}/tokens/{token-name}").To(wsClusterToken). Doc("Get cluster's token")) diff --git a/pkg/mime/mime.go b/pkg/mime/mime.go index 24dee7f..c196462 100644 --- a/pkg/mime/mime.go +++ b/pkg/mime/mime.go @@ -1,10 +1,12 @@ package mime const ( - YAML = "text/vnd.yaml" - TAR = "application/tar" - DISK = "application/x-diskimage" - ISO = "application/x-iso9660-image" - IPXE = "text/x-ipxe" - OCTET = "application/octet-stream" + YAML = "text/vnd.yaml" + TAR = "application/tar" + DISK = "application/x-diskimage" + ISO = "application/x-iso9660-image" + IPXE = "text/x-ipxe" + OCTET = "application/octet-stream" + CERT = "application/x-x509-user-cert" + CACERT = "application/x-x509-ca-cert" )