tls: automatic certificate renewal

This commit is contained in:
Mikaël Cluseau 2020-04-22 17:36:04 +02:00
parent 5e667295ac
commit 748a028161
5 changed files with 121 additions and 11 deletions

View File

@ -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

View File

@ -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()

View File

@ -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)
}

View File

@ -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"))

View File

@ -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"
)