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; }