From 11f3c953e292b3d2ddb91bec0a7258d59f518f94 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mika=C3=ABl=20Cluseau?= Date: Sun, 12 Feb 2023 15:18:42 +0100 Subject: [PATCH] migration to new secrets nearly complete --- .../cluster-render-context.go | 248 ++++++------- cmd/dkl-local-server/render-context.go | 42 +-- cmd/dkl-local-server/secrets.go | 341 ------------------ cmd/dkl-local-server/ssh-secrets.go | 102 +++--- cmd/dkl-local-server/ssh-secrets_test.go | 11 +- cmd/dkl-local-server/tls-ca.go | 178 +++++++++ cmd/dkl-local-server/ws-cluster-cas.go | 48 +++ cmd/dkl-local-server/ws-cluster-tokens.go | 24 ++ cmd/dkl-local-server/ws-clusters.go | 13 +- cmd/dkl-local-server/ws.go | 5 +- html/ui/js/GetCopy.js | 27 +- html/ui/style.css | 2 + 12 files changed, 482 insertions(+), 559 deletions(-) create mode 100644 cmd/dkl-local-server/tls-ca.go diff --git a/cmd/dkl-local-server/cluster-render-context.go b/cmd/dkl-local-server/cluster-render-context.go index 37d1652..988115c 100644 --- a/cmd/dkl-local-server/cluster-render-context.go +++ b/cmd/dkl-local-server/cluster-render-context.go @@ -6,157 +6,161 @@ import ( "log" "path" + cfsslconfig "github.com/cloudflare/cfssl/config" "github.com/cloudflare/cfssl/csr" yaml "gopkg.in/yaml.v2" "novit.tech/direktil/pkg/config" ) -var templateFuncs = map[string]interface{}{ - "password": func(cluster, name string) (password string, err error) { - password = secretData.Password(cluster, name) - if len(password) == 0 { - err = fmt.Errorf("password %q not defined for cluster %q", name, cluster) +func templateFuncs(sslCfg *cfsslconfig.Config) map[string]any { + getKeyCert := func(cluster, caName, name, profile, label, reqJson string) (kc KeyCert, err error) { + certReq := &csr.CertificateRequest{ + KeyRequest: csr.NewKeyRequest(), } - return - }, - "token": func(cluster, name string) (s string, err error) { - return secretData.Token(cluster, name) - }, - - "ca_key": func(cluster, name string) (s string, err error) { - ca, err := secretData.CA(cluster, name) + err = json.Unmarshal([]byte(reqJson), certReq) if err != nil { + log.Print("CSR unmarshal failed on: ", reqJson) return } - s = string(ca.Key) - return - }, + return getUsableKeyCert(cluster, caName, name, profile, label, certReq, sslCfg) + } - "ca_crt": func(cluster, name string) (s string, err error) { - ca, err := secretData.CA(cluster, name) - if err != nil { + return map[string]any{ + "password": func(cluster, name string) (password string, err error) { + password, _, err = clusterPasswords.Get(cluster + "/" + name) + if err != nil { + return + } + if len(password) == 0 { + err = fmt.Errorf("password %q not defined for cluster %q", name, cluster) + } return - } + }, - s = string(ca.Cert) - return - }, + "token": getOrCreateClusterToken, - "ca_dir": func(cluster, name string) (s string, err error) { - ca, err := secretData.CA(cluster, name) - if err != nil { + "ca_key": func(cluster, name string) (s string, err error) { + ca, err := getUsableClusterCA(cluster, name) + if err != nil { + return + } + + s = string(ca.Key) return - } + }, - dir := "/etc/tls-ca/" + name + "ca_crt": func(cluster, name string) (s string, err error) { + ca, err := getUsableClusterCA(cluster, name) + if err != nil { + return + } - return asYaml([]config.FileDef{ - { - Path: path.Join(dir, "ca.crt"), - Mode: 0644, - Content: string(ca.Cert), - }, - { - Path: path.Join(dir, "ca.key"), - Mode: 0600, - Content: string(ca.Key), - }, - }) - }, - - "tls_key": func(cluster, caName, name, profile, label, reqJson string) (s string, err error) { - kc, err := getKeyCert(cluster, caName, name, profile, label, reqJson) - if err != nil { + s = string(ca.Cert) return - } + }, - s = string(kc.Key) - return - }, + "ca_dir": func(cluster, name string) (s string, err error) { + ca, err := getUsableClusterCA(cluster, name) + if err != nil { + return + } - "tls_crt": func(cluster, caName, name, profile, label, reqJson string) (s string, err error) { - kc, err := getKeyCert(cluster, caName, name, profile, label, reqJson) - if err != nil { - return - } + dir := "/etc/tls-ca/" + name - s = string(kc.Cert) - return - }, - - "tls_dir": func(dir, cluster, caName, name, profile, label, reqJson string) (s string, err error) { - ca, err := secretData.CA(cluster, caName) - if err != nil { - return - } - - kc, err := getKeyCert(cluster, caName, name, profile, label, reqJson) - if err != nil { - return - } - - return asYaml([]config.FileDef{ - { - Path: path.Join(dir, "ca.crt"), - Mode: 0644, - Content: string(ca.Cert), - }, - { - Path: path.Join(dir, "tls.crt"), - Mode: 0644, - Content: string(kc.Cert), - }, - { - Path: path.Join(dir, "tls.key"), - Mode: 0600, - Content: string(kc.Key), - }, - }) - }, - - "ssh_host_keys": func(dir, cluster, host string) (s string, err error) { - pairs, err := secretData.SSHKeyPairs(cluster, host) - if err != nil { - return - } - - files := make([]config.FileDef, 0, len(pairs)*2) - - for _, pair := range pairs { - basePath := path.Join(dir, "ssh_host_"+pair.Type+"_key") - files = append(files, []config.FileDef{ + return asYaml([]config.FileDef{ { - Path: basePath, - Mode: 0600, - Content: pair.Private, - }, - { - Path: basePath + ".pub", + Path: path.Join(dir, "ca.crt"), Mode: 0644, - Content: pair.Public, + Content: string(ca.Cert), }, - }...) - } + { + Path: path.Join(dir, "ca.key"), + Mode: 0600, + Content: string(ca.Key), + }, + }) + }, - return asYaml(files) - }, -} + "tls_key": func(cluster, caName, name, profile, label, reqJson string) (s string, err error) { + kc, err := getKeyCert(cluster, caName, name, profile, label, reqJson) + if err != nil { + return + } -func getKeyCert(cluster, caName, name, profile, label, reqJson string) (kc *KeyCert, err error) { - certReq := &csr.CertificateRequest{ - KeyRequest: csr.NewKeyRequest(), + s = string(kc.Key) + return + }, + + "tls_crt": func(cluster, caName, name, profile, label, reqJson string) (s string, err error) { + kc, err := getKeyCert(cluster, caName, name, profile, label, reqJson) + if err != nil { + return + } + + s = string(kc.Cert) + return + }, + + "tls_dir": func(dir, cluster, caName, name, profile, label, reqJson string) (s string, err error) { + ca, err := getUsableClusterCA(cluster, caName) + if err != nil { + return + } + + kc, err := getKeyCert(cluster, caName, name, profile, label, reqJson) + if err != nil { + return + } + + return asYaml([]config.FileDef{ + { + Path: path.Join(dir, "ca.crt"), + Mode: 0644, + Content: string(ca.Cert), + }, + { + Path: path.Join(dir, "tls.crt"), + Mode: 0644, + Content: string(kc.Cert), + }, + { + Path: path.Join(dir, "tls.key"), + Mode: 0600, + Content: string(kc.Key), + }, + }) + }, + + "ssh_host_keys": func(dir, cluster, host string) (s string, err error) { + pairs, err := getSSHKeyPairs(host) + if err != nil { + return + } + + files := make([]config.FileDef, 0, len(pairs)*2) + + for _, pair := range pairs { + basePath := path.Join(dir, "ssh_host_"+pair.Type+"_key") + files = append(files, []config.FileDef{ + { + Path: basePath, + Mode: 0600, + Content: pair.Private, + }, + { + Path: basePath + ".pub", + Mode: 0644, + Content: pair.Public, + }, + }...) + } + + return asYaml(files) + }, } - - err = json.Unmarshal([]byte(reqJson), certReq) - if err != nil { - log.Print("CSR unmarshal failed on: ", reqJson) - return - } - - return secretData.KeyCert(cluster, caName, name, profile, label, certReq) } func asYaml(v interface{}) (string, error) { diff --git a/cmd/dkl-local-server/render-context.go b/cmd/dkl-local-server/render-context.go index 1a8eb14..05ea51f 100644 --- a/cmd/dkl-local-server/render-context.go +++ b/cmd/dkl-local-server/render-context.go @@ -25,7 +25,7 @@ var cmdlineParam = restful.QueryParameter("cmdline", "Linux kernel cmdline addit type renderContext struct { Host *localconfig.Host - SSLConfig string + SSLConfig *cfsslconfig.Config // Linux kernel extra cmdline CmdLine string `yaml:"-"` @@ -61,32 +61,27 @@ func renderCtx(w http.ResponseWriter, r *http.Request, ctx *renderContext, what return nil } -var prevSSLConfig = "-" - -func newRenderContext(host *localconfig.Host, cfg *localconfig.Config) (ctx *renderContext, err error) { - if prevSSLConfig != cfg.SSLConfig { - var sslCfg *cfsslconfig.Config - - if len(cfg.SSLConfig) == 0 { - sslCfg = &cfsslconfig.Config{} - } else { - sslCfg, err = cfsslconfig.LoadConfig([]byte(cfg.SSLConfig)) - if err != nil { - return - } - } - - err = loadSecretData(sslCfg) +func sslConfigFromLocalConfig(cfg *localconfig.Config) (sslCfg *cfsslconfig.Config, err error) { + if len(cfg.SSLConfig) == 0 { + sslCfg = &cfsslconfig.Config{} + } else { + sslCfg, err = cfsslconfig.LoadConfig([]byte(cfg.SSLConfig)) if err != nil { return } + } + return +} - prevSSLConfig = cfg.SSLConfig +func newRenderContext(host *localconfig.Host, cfg *localconfig.Config) (ctx *renderContext, err error) { + sslCfg, err := sslConfigFromLocalConfig(cfg) + if err != nil { + return } return &renderContext{ - SSLConfig: cfg.SSLConfig, Host: host, + SSLConfig: sslCfg, }, nil } @@ -120,7 +115,7 @@ func (ctx *renderContext) BootstrapConfig() (ba []byte, cfg *bsconfig.Config, er func (ctx *renderContext) render(templateText string) (ba []byte, err error) { tmpl, err := template.New(ctx.Host.Name + "/config"). - Funcs(templateFuncs). + Funcs(templateFuncs(ctx.SSLConfig)). Parse(templateText) if err != nil { @@ -132,13 +127,6 @@ func (ctx *renderContext) render(templateText string) (ba []byte, err error) { return } - if secretData.Changed() { - err = secretData.Save() - if err != nil { - return - } - } - ba = buf.Bytes() return } diff --git a/cmd/dkl-local-server/secrets.go b/cmd/dkl-local-server/secrets.go index fb0836c..b5b1f40 100644 --- a/cmd/dkl-local-server/secrets.go +++ b/cmd/dkl-local-server/secrets.go @@ -1,31 +1,16 @@ 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 ( @@ -34,12 +19,7 @@ var ( ) type SecretData struct { - l sync.Mutex - - prevHash uint64 - clusters map[string]*ClusterSecrets - changed bool config *config.Config } @@ -50,13 +30,6 @@ type ClusterSecrets struct { SSHKeyPairs map[string][]SSHKeyPair } -type CA struct { - Key []byte - Cert []byte - - Signed map[string]*KeyCert -} - type KeyCert struct { Key []byte Cert []byte @@ -72,14 +45,12 @@ func loadSecretData(config *config.Config) (err error) { 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 @@ -91,221 +62,10 @@ func loadSecretData(config *config.Config) (err error) { 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 { @@ -321,104 +81,3 @@ func checkCertUsable(certPEM []byte) error { 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) -} diff --git a/cmd/dkl-local-server/ssh-secrets.go b/cmd/dkl-local-server/ssh-secrets.go index 5267472..cbf6d0b 100644 --- a/cmd/dkl-local-server/ssh-secrets.go +++ b/cmd/dkl-local-server/ssh-secrets.go @@ -15,34 +15,16 @@ import ( "os/exec" ) +var sshHostKeys = KVSecrets[[]SSHKeyPair]{"hosts/ssh-host-keys"} + type SSHKeyPair struct { Type string Public string Private string } -func (sd *SecretData) SSHKeyPairs(cluster, host string) (pairs []SSHKeyPair, err error) { - cs := sd.cluster(cluster) - - if cs.SSHKeyPairs == nil { - cs.SSHKeyPairs = map[string][]SSHKeyPair{} - } - - outFile, err := ioutil.TempFile("/tmp", "dls-key.") - if err != nil { - return - } - - outPath := outFile.Name() - - removeTemp := func() { - os.Remove(outPath) - os.Remove(outPath + ".pub") - } - - defer removeTemp() - - pairs = cs.SSHKeyPairs[host] +func getSSHKeyPairs(host string) (pairs []SSHKeyPair, err error) { + pairs, _, err = sshHostKeys.Get(host) didGenerate := false @@ -59,46 +41,64 @@ genLoop: } } - didGenerate = true + err = func() (err error) { + outFile, err := ioutil.TempFile("/tmp", "dls-key.") + if err != nil { + return + } - removeTemp() + outPath := outFile.Name() - var out, privKey, pubKey []byte + removeTemp := func() { + os.Remove(outPath) + os.Remove(outPath + ".pub") + } + + removeTemp() + defer removeTemp() + + var out, privKey, pubKey []byte + + out, err = exec.Command("ssh-keygen", + "-N", "", + "-C", "root@"+host, + "-f", outPath, + "-t", keyType).CombinedOutput() + if err != nil { + err = fmt.Errorf("ssh-keygen failed: %v: %s", err, string(out)) + return + } + + privKey, err = ioutil.ReadFile(outPath) + if err != nil { + return + } + + pubKey, err = ioutil.ReadFile(outPath + ".pub") + if err != nil { + return + } + + pairs = append(pairs, SSHKeyPair{ + Type: keyType, + Public: string(pubKey), + Private: string(privKey), + }) + didGenerate = true - out, err = exec.Command("ssh-keygen", - "-N", "", - "-C", "root@"+host, - "-f", outPath, - "-t", keyType).CombinedOutput() - if err != nil { - err = fmt.Errorf("ssh-keygen failed: %v: %s", err, string(out)) return - } + }() - privKey, err = ioutil.ReadFile(outPath) if err != nil { return } - - os.Remove(outPath) - - pubKey, err = ioutil.ReadFile(outPath + ".pub") - if err != nil { - return - } - - os.Remove(outPath + ".pub") - - pairs = append(pairs, SSHKeyPair{ - Type: keyType, - Public: string(pubKey), - Private: string(privKey), - }) } if didGenerate { - cs.SSHKeyPairs[host] = pairs - err = sd.Save() + err = sshHostKeys.Put(host, pairs) + if err != nil { + return + } } return diff --git a/cmd/dkl-local-server/ssh-secrets_test.go b/cmd/dkl-local-server/ssh-secrets_test.go index 9b14186..1cc3a57 100644 --- a/cmd/dkl-local-server/ssh-secrets_test.go +++ b/cmd/dkl-local-server/ssh-secrets_test.go @@ -7,11 +7,8 @@ func init() { } func TestSSHKeyGet(t *testing.T) { - sd := &SecretData{ - clusters: make(map[string]*ClusterSecrets), - } - - if _, err := sd.SSHKeyPairs("test", "host"); err != nil { - t.Error(err) - } + // TODO needs fake secret store + // if _, err := getSSHKeyPairs("host"); err != nil { + // t.Error(err) + // } } diff --git a/cmd/dkl-local-server/tls-ca.go b/cmd/dkl-local-server/tls-ca.go new file mode 100644 index 0000000..c5f101f --- /dev/null +++ b/cmd/dkl-local-server/tls-ca.go @@ -0,0 +1,178 @@ +package main + +import ( + "crypto" + "errors" + "fmt" + "log" + "net" + + "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" + "k8s.io/apimachinery/pkg/util/validation" +) + +type CA struct { + Key []byte + Cert []byte + + Signed map[string]*KeyCert +} + +func (ca *CA) Init() (err error) { + req := ca.newReq() + + cert, _, key, err := initca.New(req) + if err != nil { + err = fmt.Errorf("initca: %w", err) + return + } + + ca.Key = key + ca.Cert = cert + + return +} + +func (ca *CA) RenewCert() (err error) { + var signer crypto.Signer + signer, err = helpers.ParsePrivateKeyPEM(ca.Key) + if err != nil { + return + } + + newCert, _, err := initca.NewFromSigner(ca.newReq(), signer) + if err != nil { + return + } + + ca.Cert = newCert + + return +} + +func (_ CA) newReq() *csr.CertificateRequest { + return &csr.CertificateRequest{ + CN: "Direktil Local Server", + KeyRequest: &csr.KeyRequest{ + A: "ecdsa", + S: 521, // 256, 384, 521 + }, + Names: []csr.Name{ + { + O: "novit.io", + }, + }, + } +} + +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) +} + +func getUsableKeyCert(cluster, caName, name, profile, label string, req *csr.CertificateRequest, cfg *config.Config) (kc KeyCert, err error) { + log := log.New(log.Default().Writer(), cluster+": CA "+caName+": ", log.Flags()|log.Lmsgprefix) + + ca, err := getUsableClusterCA(cluster, caName) + if err != nil { + return + } + + for _, 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 + } + + err = fmt.Errorf("%q is not an IP or FQDN", host) + return + } + + if req.CA != nil { + err = errors.New("no CA section allowed here") + return + } + + rh := hash(req) + + key := cluster + "/" + caName + "/" + name + + kc, found, err := clusterCASignedKeys.Get(key) + if err != nil { + return + } + + if found { + if rh == kc.ReqHash { + err = checkCertUsable(kc.Cert) + if err == nil { + return // all good, no need to create or renew + } + + log.Print("regenerating certificate: ", err) + + } else { + log.Printf("CSR changed for %s: hash=%q previous=%q", name, rh, kc.ReqHash) + } + } else { + log.Print("new CSR for ", name) + } + + sgr, err := ca.Signer(cfg.Signing) + if err != nil { + return + } + + generator := &csr.Generator{Validator: func(_ *csr.CertificateRequest) error { return nil }} + + csr, tlsKey, 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: tlsKey, + Cert: cert, + ReqHash: rh, + } + + err = clusterCASignedKeys.Put(key, kc) + + return +} diff --git a/cmd/dkl-local-server/ws-cluster-cas.go b/cmd/dkl-local-server/ws-cluster-cas.go index 08ca6f9..7e08c95 100644 --- a/cmd/dkl-local-server/ws-cluster-cas.go +++ b/cmd/dkl-local-server/ws-cluster-cas.go @@ -1,6 +1,9 @@ package main import ( + "fmt" + + "github.com/cloudflare/cfssl/log" restful "github.com/emicklei/go-restful" ) @@ -18,6 +21,51 @@ func wsClusterCA(req *restful.Request, resp *restful.Response) { clusterCAs.WsGet(resp, clusterName+"/"+name) } +func getUsableClusterCA(cluster, name string) (ca CA, err error) { + defer func() { + if err != nil { + err = fmt.Errorf("cluster %s CA %s: %w", cluster, name, err) + } + }() + + key := cluster + "/" + name + + ca, found, err := clusterCAs.Get(key) + if err != nil { + return + } + + if !found { + log.Info("new CA in cluster ", cluster, ": ", name) + + err = ca.Init() + if err != nil { + return + } + + err = clusterCAs.Put(key, ca) + if err != nil { + return + } + + return + } + + checkErr := checkCertUsable(ca.Cert) + if checkErr != nil { + log.Infof("cluster %s: CA %s: regenerating certificate: %v", cluster, name, checkErr) + + err = ca.RenewCert() + if err != nil { + err = fmt.Errorf("renew: %w", err) + } + + err = clusterCAs.Put(key, ca) + } + + return +} + var clusterCASignedKeys = newClusterSecretKV[KeyCert]("CA-signed-keys") func wsClusterCASignedKeys(req *restful.Request, resp *restful.Response) { diff --git a/cmd/dkl-local-server/ws-cluster-tokens.go b/cmd/dkl-local-server/ws-cluster-tokens.go index 1354c68..8157c90 100644 --- a/cmd/dkl-local-server/ws-cluster-tokens.go +++ b/cmd/dkl-local-server/ws-cluster-tokens.go @@ -1,11 +1,35 @@ package main import ( + "crypto/rand" + "encoding/base32" + restful "github.com/emicklei/go-restful" ) var clusterTokens = newClusterSecretKV[string]("tokens") +func getOrCreateClusterToken(cluster, name string) (token string, err error) { + key := cluster + "/" + name + + token, found, err := clusterTokens.Get(key) + + if err != nil || found { + return + } + + b := make([]byte, 16) + _, err = rand.Read(b) + if err != nil { + return + } + + token = base32.StdEncoding.WithPadding(base32.NoPadding).EncodeToString(b) + + err = clusterTokens.Put(key, token) + return +} + func wsClusterTokens(req *restful.Request, resp *restful.Response) { clusterName := req.PathParameter("cluster-name") clusterTokens.WsList(resp, clusterName+"/") diff --git a/cmd/dkl-local-server/ws-clusters.go b/cmd/dkl-local-server/ws-clusters.go index 391d750..65bd09f 100644 --- a/cmd/dkl-local-server/ws-clusters.go +++ b/cmd/dkl-local-server/ws-clusters.go @@ -68,7 +68,18 @@ func wsClusterAddons(req *restful.Request, resp *restful.Response) { return } - wsRender(resp, cluster.Addons, cluster) + cfg := wsReadConfig(resp) + if cfg == nil { + return + } + + sslCfg, err := sslConfigFromLocalConfig(cfg) + if err != nil { + wsError(resp, err) + return + } + + wsRender(resp, sslCfg, cluster.Addons, cluster) } func wsClusterCACert(req *restful.Request, resp *restful.Response) { diff --git a/cmd/dkl-local-server/ws.go b/cmd/dkl-local-server/ws.go index d681e09..503fa9d 100644 --- a/cmd/dkl-local-server/ws.go +++ b/cmd/dkl-local-server/ws.go @@ -8,6 +8,7 @@ import ( "strings" "text/template" + cfsslconfig "github.com/cloudflare/cfssl/config" "github.com/emicklei/go-restful" "novit.tech/direktil/pkg/localconfig" @@ -186,8 +187,8 @@ func wsError(resp *restful.Response, err error) { http.StatusText(http.StatusInternalServerError)) } -func wsRender(resp *restful.Response, tmplStr string, value interface{}) { - tmpl, err := template.New("wsRender").Funcs(templateFuncs).Parse(tmplStr) +func wsRender(resp *restful.Response, sslCfg *cfsslconfig.Config, tmplStr string, value interface{}) { + tmpl, err := template.New("wsRender").Funcs(templateFuncs(sslCfg)).Parse(tmplStr) if err != nil { wsError(resp, err) return diff --git a/html/ui/js/GetCopy.js b/html/ui/js/GetCopy.js index b2a6bd5..4c3b0be 100644 --- a/html/ui/js/GetCopy.js +++ b/html/ui/js/GetCopy.js @@ -1,21 +1,32 @@ export default { props: [ 'name', 'href', 'token' ], data() { return {showCopied: false} }, - template: `
copied!
{{name}} 🗐
`, + template: `
copied!
{{name}} 🗐
`, methods: { - fetchAndCopy() { + fetch() { event.preventDefault() - - fetch(this.href, { + return fetch(this.href, { method: 'GET', headers: { 'Authorization': 'Bearer ' + this.token }, - }).then((resp) => resp.headers.get("content-type") == "application/json" ? resp.json() : resp.text()) - .then((value) => { + }) + }, + handleFetchError(e) { + console.log("failed to get value:", e) + alert('failed to get value') + }, + fetchAndSave() { + this.fetch().then(resp => resp.blob()).then((value) => { + window.open(URL.createObjectURL(value), "_blank") + }).catch(this.handleFetchError) + }, + fetchAndCopy() { + this.fetch() + .then((resp) => resp.headers.get("content-type") == "application/json" ? resp.json() : resp.text()) + .then((value) => { window.navigator.clipboard.writeText(value) this.showCopied = true setTimeout(() => { this.showCopied = false }, 1000) - }) - .catch((e) => { console.log("failed to get value:", e); alert('failed to get value') }) + }).catch(this.handleFetchError) }, }, } diff --git a/html/ui/style.css b/html/ui/style.css index c040380..368ea0e 100644 --- a/html/ui/style.css +++ b/html/ui/style.css @@ -145,3 +145,5 @@ header .utils > * { background: black; } } + +.copy { font-size: small; }