diff --git a/render-context.go b/render-context.go index 4875f45..bb18b69 100644 --- a/render-context.go +++ b/render-context.go @@ -4,10 +4,13 @@ import ( "bytes" "crypto/sha256" "encoding/hex" + "encoding/json" + "errors" "fmt" "log" "path/filepath" + "github.com/cloudflare/cfssl/csr" yaml "gopkg.in/yaml.v2" "novit.nc/direktil/pkg/clustersconfig" "novit.nc/direktil/pkg/config" @@ -60,6 +63,53 @@ func (ctx *renderContext) Config() (ba []byte, cfg *config.Config, err error) { ctxMap := ctx.asMap() + sslCfg, err := sslConfig(ctx.clusterConfig) + if err != nil { + return + } + + secretData, err := loadSecretData(sslCfg) + if err != nil { + return + } + + cluster := ctx.Cluster.Name + + getKeyCert := func(name string) (kc *KeyCert, err error) { + req := ctx.clusterConfig.CSR(name) + if req == nil { + err = errors.New("no such certificate request") + return + } + + if req.CA == "" { + err = errors.New("CA not defined") + return + } + + buf := &bytes.Buffer{} + err = req.Execute(buf, ctxMap, nil) + if err != nil { + return + } + + certReq := &csr.CertificateRequest{ + KeyRequest: csr.NewBasicKeyRequest(), + } + + err = json.Unmarshal(buf.Bytes(), certReq) + if err != nil { + log.Print("unmarshal failed on: ", buf) + return + } + + if req.PerHost { + name = name + "/" + ctx.Host.Name + } + + return secretData.KeyCert(cluster, req.CA, name, req.Profile, req.Label, certReq) + } + extraFuncs := map[string]interface{}{ "static_pods": func(name string) (string, error) { t := ctx.clusterConfig.StaticPodsTemplate(name) @@ -76,6 +126,46 @@ func (ctx *renderContext) Config() (ba []byte, cfg *config.Config, err error) { return buf.String(), nil }, + + "ca_key": func(name string) (s string, err error) { + ca, err := secretData.CA(cluster, name) + if err != nil { + return + } + + s = string(ca.Key) + return + }, + + "ca_crt": func(name string) (s string, err error) { + ca, err := secretData.CA(cluster, name) + if err != nil { + return + } + + s = string(ca.Cert) + return + }, + + "tls_key": func(name string) (s string, err error) { + kc, err := getKeyCert(name) + if err != nil { + return + } + + s = string(kc.Key) + return + }, + + "tls_crt": func(name string) (s string, err error) { + kc, err := getKeyCert(name) + if err != nil { + return + } + + s = string(kc.Cert) + return + }, } buf := bytes.NewBuffer(make([]byte, 0, 4096)) @@ -83,6 +173,13 @@ func (ctx *renderContext) Config() (ba []byte, cfg *config.Config, err error) { return } + if secretData.Changed() { + err = secretData.Save() + if err != nil { + return + } + } + ba = buf.Bytes() cfg = &config.Config{} diff --git a/secrets.go b/secrets.go index 66ccef6..1239f07 100644 --- a/secrets.go +++ b/secrets.go @@ -1,12 +1,21 @@ package main import ( + "encoding/json" + "errors" "fmt" "io/ioutil" "log" "os" + "path/filepath" "strings" + "github.com/cloudflare/cfssl/config" + "github.com/cloudflare/cfssl/csr" + "github.com/cloudflare/cfssl/helpers" + "github.com/cloudflare/cfssl/initca" + "github.com/cloudflare/cfssl/signer" + "github.com/cloudflare/cfssl/signer/local" yaml "gopkg.in/yaml.v2" ) @@ -14,6 +23,180 @@ var ( secrets SecretBackend ) +type SecretData struct { + clusters map[string]*ClusterSecrets + changed bool + config *config.Config +} + +type ClusterSecrets struct { + CAs map[string]*CA +} + +type CA struct { + Key []byte + Cert []byte + + Signed map[string]*KeyCert +} + +type KeyCert struct { + Key []byte + Cert []byte +} + +func loadSecretData(config *config.Config) (*SecretData, error) { + sd := &SecretData{ + clusters: make(map[string]*ClusterSecrets), + changed: false, + config: config, + } + + ba, err := ioutil.ReadFile(filepath.Join(*dataDir, "secret-data.json")) + if err != nil { + if os.IsNotExist(err) { + sd.changed = true + return sd, nil + } + return nil, err + } + + if err := json.Unmarshal(ba, &sd.clusters); err != nil { + return nil, err + } + + return sd, nil +} + +func (sd *SecretData) Changed() bool { + return sd.changed +} + +func (sd *SecretData) Save() error { + ba, err := json.Marshal(sd.clusters) + if err != nil { + return err + } + return ioutil.WriteFile(filepath.Join(*dataDir, "secret-data.json"), ba, 0600) +} + +func (sd *SecretData) cluster(name string) (cs *ClusterSecrets) { + cs, ok := sd.clusters[name] + if ok { + return + } + + cs = &ClusterSecrets{ + CAs: make(map[string]*CA), + } + sd.clusters[name] = cs + 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 { + return + } + + req := &csr.CertificateRequest{ + CN: "Direktil Local Server", + KeyRequest: &csr.BasicKeyRequest{ + A: "ecdsa", + S: 521, // 256, 384, 521 + }, + Names: []csr.Name{ + { + C: "NC", + O: "novit.nc", + }, + }, + } + + cert, _, key, err := initca.New(req) + if err != nil { + return + } + + ca = &CA{ + Key: key, + Cert: cert, + Signed: make(map[string]*KeyCert), + } + + cs.CAs[name] = ca + sd.changed = true + + return +} + +func (sd *SecretData) KeyCert(cluster, caName, name, profile, label string, req *csr.CertificateRequest) (kc *KeyCert, err error) { + if req.CA != nil { + err = errors.New("no CA section allowed here") + return + } + + ca, err := sd.CA(cluster, caName) + if err != nil { + return + } + + kc, ok := ca.Signed[name] + if ok { + return + } + + 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, + } + + 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) +} + type SecretBackend interface { Get(ref string) (string, error) Set(ref, value string) error diff --git a/ssl.go b/ssl.go index 8bbbc90..0f702b4 100644 --- a/ssl.go +++ b/ssl.go @@ -13,6 +13,10 @@ import ( "math/big" "net" "time" + + "github.com/cloudflare/cfssl/config" + + "novit.nc/direktil/pkg/clustersconfig" ) const ( @@ -21,6 +25,10 @@ const ( ECPrivateKeyBlockType = "EC PRIVATE KEY" ) +func sslConfig(cfg *clustersconfig.Config) (*config.Config, error) { + return config.LoadConfig([]byte(cfg.SSLConfig)) +} + func PrivateKeyPEM() (*ecdsa.PrivateKey, []byte) { key, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) if err != nil {