425 lines
7.5 KiB
Go
425 lines
7.5 KiB
Go
package main
|
|
|
|
import (
|
|
"crypto"
|
|
"crypto/rand"
|
|
"encoding/base32"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"io/ioutil"
|
|
"net"
|
|
"os"
|
|
"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"
|
|
"github.com/cloudflare/cfssl/initca"
|
|
"github.com/cloudflare/cfssl/log"
|
|
"github.com/cloudflare/cfssl/signer"
|
|
"github.com/cloudflare/cfssl/signer/local"
|
|
"k8s.io/apimachinery/pkg/util/validation"
|
|
"k8s.io/apimachinery/pkg/util/validation/field"
|
|
)
|
|
|
|
var (
|
|
secretData *SecretData
|
|
DontSave = false
|
|
)
|
|
|
|
type SecretData struct {
|
|
l sync.Mutex
|
|
|
|
prevHash uint64
|
|
|
|
clusters map[string]*ClusterSecrets
|
|
changed bool
|
|
config *config.Config
|
|
}
|
|
|
|
type ClusterSecrets struct {
|
|
CAs map[string]*CA
|
|
Tokens map[string]string
|
|
Passwords map[string]string
|
|
SSHKeyPairs map[string][]SSHKeyPair
|
|
}
|
|
|
|
type CA struct {
|
|
Key []byte
|
|
Cert []byte
|
|
|
|
Signed map[string]*KeyCert
|
|
}
|
|
|
|
type KeyCert struct {
|
|
Key []byte
|
|
Cert []byte
|
|
ReqHash string
|
|
}
|
|
|
|
func secretDataPath() string {
|
|
return filepath.Join(*dataDir, "secret-data.json")
|
|
}
|
|
|
|
func loadSecretData(config *config.Config) (err error) {
|
|
log.Info("Loading secret data")
|
|
|
|
sd := &SecretData{
|
|
clusters: make(map[string]*ClusterSecrets),
|
|
changed: false,
|
|
config: config,
|
|
}
|
|
|
|
ba, err := ioutil.ReadFile(secretDataPath())
|
|
if err != nil {
|
|
if os.IsNotExist(err) {
|
|
sd.changed = true
|
|
err = nil
|
|
secretData = sd
|
|
return
|
|
}
|
|
return
|
|
}
|
|
|
|
if err = json.Unmarshal(ba, &sd.clusters); err != nil {
|
|
return
|
|
}
|
|
|
|
sd.prevHash = xxhash.Sum64(ba)
|
|
|
|
secretData = sd
|
|
return
|
|
}
|
|
|
|
func (sd *SecretData) Changed() bool {
|
|
return sd.changed
|
|
}
|
|
|
|
func (sd *SecretData) Save() (err error) {
|
|
if DontSave {
|
|
return
|
|
}
|
|
|
|
sd.l.Lock()
|
|
defer sd.l.Unlock()
|
|
|
|
ba, err := json.Marshal(sd.clusters)
|
|
if err != nil {
|
|
return
|
|
}
|
|
|
|
h := xxhash.Sum64(ba)
|
|
if h == sd.prevHash {
|
|
return
|
|
}
|
|
|
|
log.Info("Saving secret data")
|
|
err = ioutil.WriteFile(secretDataPath(), ba, 0600)
|
|
|
|
if err == nil {
|
|
sd.prevHash = h
|
|
}
|
|
|
|
return
|
|
}
|
|
|
|
func newClusterSecrets() *ClusterSecrets {
|
|
return &ClusterSecrets{
|
|
CAs: make(map[string]*CA),
|
|
Tokens: make(map[string]string),
|
|
Passwords: make(map[string]string),
|
|
}
|
|
}
|
|
|
|
func (sd *SecretData) cluster(name string) (cs *ClusterSecrets) {
|
|
cs, ok := sd.clusters[name]
|
|
if ok {
|
|
return
|
|
}
|
|
|
|
sd.l.Lock()
|
|
defer sd.l.Unlock()
|
|
|
|
log.Info("secret-data: new cluster: ", name)
|
|
|
|
cs = newClusterSecrets()
|
|
sd.clusters[name] = cs
|
|
sd.changed = true
|
|
return
|
|
}
|
|
|
|
func (sd *SecretData) Passwords(cluster string) (passwords []string) {
|
|
cs := sd.cluster(cluster)
|
|
|
|
passwords = make([]string, 0, len(cs.Passwords))
|
|
for name := range cs.Passwords {
|
|
passwords = append(passwords, name)
|
|
}
|
|
|
|
sort.Strings(passwords)
|
|
|
|
return
|
|
}
|
|
|
|
func (sd *SecretData) Password(cluster, name string) (password string) {
|
|
cs := sd.cluster(cluster)
|
|
|
|
if cs.Passwords == nil {
|
|
cs.Passwords = make(map[string]string)
|
|
}
|
|
|
|
password = cs.Passwords[name]
|
|
return
|
|
}
|
|
|
|
func (sd *SecretData) SetPassword(cluster, name, password string) {
|
|
cs := sd.cluster(cluster)
|
|
|
|
if cs.Passwords == nil {
|
|
cs.Passwords = make(map[string]string)
|
|
}
|
|
|
|
cs.Passwords[name] = password
|
|
sd.changed = true
|
|
}
|
|
|
|
func (sd *SecretData) Token(cluster, name string) (token string, err error) {
|
|
cs := sd.cluster(cluster)
|
|
|
|
token = cs.Tokens[name]
|
|
if token != "" {
|
|
return
|
|
}
|
|
|
|
sd.l.Lock()
|
|
defer sd.l.Unlock()
|
|
|
|
log.Info("secret-data: new token in cluster ", cluster, ": ", name)
|
|
|
|
b := make([]byte, 16)
|
|
_, err = rand.Read(b)
|
|
if err != nil {
|
|
return
|
|
}
|
|
|
|
token = base32.StdEncoding.WithPadding(base32.NoPadding).EncodeToString(b)
|
|
|
|
cs.Tokens[name] = token
|
|
sd.changed = true
|
|
return
|
|
}
|
|
|
|
func (sd *SecretData) RenewCACert(cluster, name string) (err error) {
|
|
cs := sd.cluster(cluster)
|
|
|
|
ca := cs.CAs[name]
|
|
|
|
var signer crypto.Signer
|
|
signer, err = helpers.ParsePrivateKeyPEM(ca.Key)
|
|
if err != nil {
|
|
return
|
|
}
|
|
|
|
newCert, _, err := initca.NewFromSigner(newCACertReq(), signer)
|
|
if err != nil {
|
|
return
|
|
}
|
|
|
|
sd.l.Lock()
|
|
defer sd.l.Unlock()
|
|
|
|
cs.CAs[name].Cert = newCert
|
|
sd.changed = true
|
|
|
|
return
|
|
}
|
|
|
|
func newCACertReq() *csr.CertificateRequest {
|
|
return &csr.CertificateRequest{
|
|
CN: "Direktil Local Server",
|
|
KeyRequest: &csr.KeyRequest{
|
|
A: "ecdsa",
|
|
S: 521, // 256, 384, 521
|
|
},
|
|
Names: []csr.Name{
|
|
{
|
|
C: "NC",
|
|
O: "novit.nc",
|
|
},
|
|
},
|
|
}
|
|
}
|
|
|
|
func (sd *SecretData) CA(cluster, name string) (ca *CA, err error) {
|
|
|
|
defer func() {
|
|
if err != nil {
|
|
err = fmt.Errorf("cluster %s CA %s: %w", cluster, name, err)
|
|
}
|
|
}()
|
|
|
|
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)
|
|
if err != nil {
|
|
err = fmt.Errorf("renew: %w", err)
|
|
}
|
|
}
|
|
|
|
return
|
|
}
|
|
|
|
sd.l.Lock()
|
|
defer sd.l.Unlock()
|
|
|
|
log.Info("secret-data: new CA in cluster ", cluster, ": ", name)
|
|
|
|
req := newCACertReq()
|
|
|
|
cert, _, key, err := initca.New(req)
|
|
if err != nil {
|
|
err = fmt.Errorf("initca: %w", err)
|
|
return
|
|
}
|
|
|
|
ca = &CA{
|
|
Key: key,
|
|
Cert: cert,
|
|
Signed: make(map[string]*KeyCert),
|
|
}
|
|
|
|
cs.CAs[name] = ca
|
|
sd.changed = true
|
|
|
|
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 {
|
|
// valid IP (v4 or v6)
|
|
continue
|
|
}
|
|
|
|
if host == "*" {
|
|
continue
|
|
}
|
|
|
|
if errs := validation.IsDNS1123Subdomain(host); len(errs) == 0 {
|
|
continue
|
|
}
|
|
if errs := validation.IsWildcardDNS1123Subdomain(host); len(errs) == 0 {
|
|
continue
|
|
}
|
|
|
|
path := field.NewPath(cluster, name, "hosts").Index(idx)
|
|
return nil, fmt.Errorf("%v: %q is not an IP or FQDN", path, host)
|
|
}
|
|
|
|
if req.CA != nil {
|
|
err = errors.New("no CA section allowed here")
|
|
return
|
|
}
|
|
|
|
ca, err := sd.CA(cluster, caName)
|
|
if err != nil {
|
|
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 {
|
|
err = checkCertUsable(kc.Cert)
|
|
if err == nil {
|
|
return
|
|
}
|
|
log.Infof("%s regenerating certificate: ", err)
|
|
|
|
} else if ok {
|
|
log.Infof("%s CSR changed for %s: hash=%q previous=%q", name, rh, kc.ReqHash)
|
|
} else {
|
|
log.Infof("%s new CSR for %s", logPrefix, name)
|
|
}
|
|
|
|
sd.l.Lock()
|
|
defer sd.l.Unlock()
|
|
|
|
sgr, err := ca.Signer(sd.config.Signing)
|
|
if err != nil {
|
|
return
|
|
}
|
|
|
|
generator := &csr.Generator{Validator: func(_ *csr.CertificateRequest) error { return nil }}
|
|
|
|
csr, key, err := generator.ProcessRequest(req)
|
|
if err != nil {
|
|
return
|
|
}
|
|
|
|
signReq := signer.SignRequest{
|
|
Request: string(csr),
|
|
Profile: profile,
|
|
Label: label,
|
|
}
|
|
|
|
cert, err := sgr.Sign(signReq)
|
|
if err != nil {
|
|
return
|
|
}
|
|
|
|
kc = &KeyCert{
|
|
Key: key,
|
|
Cert: cert,
|
|
ReqHash: rh,
|
|
}
|
|
|
|
ca.Signed[name] = kc
|
|
sd.changed = true
|
|
|
|
return
|
|
}
|
|
|
|
func (ca *CA) Signer(policy *config.Signing) (result *local.Signer, err error) {
|
|
caCert, err := helpers.ParseCertificatePEM(ca.Cert)
|
|
if err != nil {
|
|
return
|
|
}
|
|
|
|
caKey, err := helpers.ParsePrivateKeyPEM(ca.Key)
|
|
if err != nil {
|
|
return
|
|
}
|
|
|
|
return local.NewSigner(caKey, caCert, signer.DefaultSigAlgo(caKey), policy)
|
|
}
|