migration to new secrets nearly complete
This commit is contained in:
parent
3bc20e95cc
commit
11f3c953e2
@ -6,27 +6,44 @@ import (
|
|||||||
"log"
|
"log"
|
||||||
"path"
|
"path"
|
||||||
|
|
||||||
|
cfsslconfig "github.com/cloudflare/cfssl/config"
|
||||||
"github.com/cloudflare/cfssl/csr"
|
"github.com/cloudflare/cfssl/csr"
|
||||||
yaml "gopkg.in/yaml.v2"
|
yaml "gopkg.in/yaml.v2"
|
||||||
|
|
||||||
"novit.tech/direktil/pkg/config"
|
"novit.tech/direktil/pkg/config"
|
||||||
)
|
)
|
||||||
|
|
||||||
var templateFuncs = map[string]interface{}{
|
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(),
|
||||||
|
}
|
||||||
|
|
||||||
|
err = json.Unmarshal([]byte(reqJson), certReq)
|
||||||
|
if err != nil {
|
||||||
|
log.Print("CSR unmarshal failed on: ", reqJson)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
return getUsableKeyCert(cluster, caName, name, profile, label, certReq, sslCfg)
|
||||||
|
}
|
||||||
|
|
||||||
|
return map[string]any{
|
||||||
"password": func(cluster, name string) (password string, err error) {
|
"password": func(cluster, name string) (password string, err error) {
|
||||||
password = secretData.Password(cluster, name)
|
password, _, err = clusterPasswords.Get(cluster + "/" + name)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
if len(password) == 0 {
|
if len(password) == 0 {
|
||||||
err = fmt.Errorf("password %q not defined for cluster %q", name, cluster)
|
err = fmt.Errorf("password %q not defined for cluster %q", name, cluster)
|
||||||
}
|
}
|
||||||
return
|
return
|
||||||
},
|
},
|
||||||
|
|
||||||
"token": func(cluster, name string) (s string, err error) {
|
"token": getOrCreateClusterToken,
|
||||||
return secretData.Token(cluster, name)
|
|
||||||
},
|
|
||||||
|
|
||||||
"ca_key": func(cluster, name string) (s string, err error) {
|
"ca_key": func(cluster, name string) (s string, err error) {
|
||||||
ca, err := secretData.CA(cluster, name)
|
ca, err := getUsableClusterCA(cluster, name)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -36,7 +53,7 @@ var templateFuncs = map[string]interface{}{
|
|||||||
},
|
},
|
||||||
|
|
||||||
"ca_crt": func(cluster, name string) (s string, err error) {
|
"ca_crt": func(cluster, name string) (s string, err error) {
|
||||||
ca, err := secretData.CA(cluster, name)
|
ca, err := getUsableClusterCA(cluster, name)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -46,7 +63,7 @@ var templateFuncs = map[string]interface{}{
|
|||||||
},
|
},
|
||||||
|
|
||||||
"ca_dir": func(cluster, name string) (s string, err error) {
|
"ca_dir": func(cluster, name string) (s string, err error) {
|
||||||
ca, err := secretData.CA(cluster, name)
|
ca, err := getUsableClusterCA(cluster, name)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -88,7 +105,7 @@ var templateFuncs = map[string]interface{}{
|
|||||||
},
|
},
|
||||||
|
|
||||||
"tls_dir": func(dir, cluster, caName, name, profile, label, reqJson string) (s string, err error) {
|
"tls_dir": func(dir, cluster, caName, name, profile, label, reqJson string) (s string, err error) {
|
||||||
ca, err := secretData.CA(cluster, caName)
|
ca, err := getUsableClusterCA(cluster, caName)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -118,7 +135,7 @@ var templateFuncs = map[string]interface{}{
|
|||||||
},
|
},
|
||||||
|
|
||||||
"ssh_host_keys": func(dir, cluster, host string) (s string, err error) {
|
"ssh_host_keys": func(dir, cluster, host string) (s string, err error) {
|
||||||
pairs, err := secretData.SSHKeyPairs(cluster, host)
|
pairs, err := getSSHKeyPairs(host)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -143,20 +160,7 @@ var templateFuncs = map[string]interface{}{
|
|||||||
|
|
||||||
return asYaml(files)
|
return asYaml(files)
|
||||||
},
|
},
|
||||||
}
|
|
||||||
|
|
||||||
func getKeyCert(cluster, caName, name, profile, label, reqJson string) (kc *KeyCert, err error) {
|
|
||||||
certReq := &csr.CertificateRequest{
|
|
||||||
KeyRequest: csr.NewKeyRequest(),
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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) {
|
func asYaml(v interface{}) (string, error) {
|
||||||
|
@ -25,7 +25,7 @@ var cmdlineParam = restful.QueryParameter("cmdline", "Linux kernel cmdline addit
|
|||||||
|
|
||||||
type renderContext struct {
|
type renderContext struct {
|
||||||
Host *localconfig.Host
|
Host *localconfig.Host
|
||||||
SSLConfig string
|
SSLConfig *cfsslconfig.Config
|
||||||
|
|
||||||
// Linux kernel extra cmdline
|
// Linux kernel extra cmdline
|
||||||
CmdLine string `yaml:"-"`
|
CmdLine string `yaml:"-"`
|
||||||
@ -61,12 +61,7 @@ func renderCtx(w http.ResponseWriter, r *http.Request, ctx *renderContext, what
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
var prevSSLConfig = "-"
|
func sslConfigFromLocalConfig(cfg *localconfig.Config) (sslCfg *cfsslconfig.Config, err error) {
|
||||||
|
|
||||||
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 {
|
if len(cfg.SSLConfig) == 0 {
|
||||||
sslCfg = &cfsslconfig.Config{}
|
sslCfg = &cfsslconfig.Config{}
|
||||||
} else {
|
} else {
|
||||||
@ -75,18 +70,18 @@ func newRenderContext(host *localconfig.Host, cfg *localconfig.Config) (ctx *ren
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
err = loadSecretData(sslCfg)
|
func newRenderContext(host *localconfig.Host, cfg *localconfig.Config) (ctx *renderContext, err error) {
|
||||||
|
sslCfg, err := sslConfigFromLocalConfig(cfg)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
prevSSLConfig = cfg.SSLConfig
|
|
||||||
}
|
|
||||||
|
|
||||||
return &renderContext{
|
return &renderContext{
|
||||||
SSLConfig: cfg.SSLConfig,
|
|
||||||
Host: host,
|
Host: host,
|
||||||
|
SSLConfig: sslCfg,
|
||||||
}, nil
|
}, 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) {
|
func (ctx *renderContext) render(templateText string) (ba []byte, err error) {
|
||||||
tmpl, err := template.New(ctx.Host.Name + "/config").
|
tmpl, err := template.New(ctx.Host.Name + "/config").
|
||||||
Funcs(templateFuncs).
|
Funcs(templateFuncs(ctx.SSLConfig)).
|
||||||
Parse(templateText)
|
Parse(templateText)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -132,13 +127,6 @@ func (ctx *renderContext) render(templateText string) (ba []byte, err error) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if secretData.Changed() {
|
|
||||||
err = secretData.Save()
|
|
||||||
if err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
ba = buf.Bytes()
|
ba = buf.Bytes()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@ -1,31 +1,16 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"crypto"
|
|
||||||
"crypto/rand"
|
|
||||||
"encoding/base32"
|
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
"net"
|
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"sort"
|
|
||||||
"sync"
|
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/cespare/xxhash"
|
|
||||||
"github.com/cloudflare/cfssl/certinfo"
|
"github.com/cloudflare/cfssl/certinfo"
|
||||||
"github.com/cloudflare/cfssl/config"
|
"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/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 (
|
var (
|
||||||
@ -34,12 +19,7 @@ var (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type SecretData struct {
|
type SecretData struct {
|
||||||
l sync.Mutex
|
|
||||||
|
|
||||||
prevHash uint64
|
|
||||||
|
|
||||||
clusters map[string]*ClusterSecrets
|
clusters map[string]*ClusterSecrets
|
||||||
changed bool
|
|
||||||
config *config.Config
|
config *config.Config
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -50,13 +30,6 @@ type ClusterSecrets struct {
|
|||||||
SSHKeyPairs map[string][]SSHKeyPair
|
SSHKeyPairs map[string][]SSHKeyPair
|
||||||
}
|
}
|
||||||
|
|
||||||
type CA struct {
|
|
||||||
Key []byte
|
|
||||||
Cert []byte
|
|
||||||
|
|
||||||
Signed map[string]*KeyCert
|
|
||||||
}
|
|
||||||
|
|
||||||
type KeyCert struct {
|
type KeyCert struct {
|
||||||
Key []byte
|
Key []byte
|
||||||
Cert []byte
|
Cert []byte
|
||||||
@ -72,14 +45,12 @@ func loadSecretData(config *config.Config) (err error) {
|
|||||||
|
|
||||||
sd := &SecretData{
|
sd := &SecretData{
|
||||||
clusters: make(map[string]*ClusterSecrets),
|
clusters: make(map[string]*ClusterSecrets),
|
||||||
changed: false,
|
|
||||||
config: config,
|
config: config,
|
||||||
}
|
}
|
||||||
|
|
||||||
ba, err := ioutil.ReadFile(secretDataPath())
|
ba, err := ioutil.ReadFile(secretDataPath())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if os.IsNotExist(err) {
|
if os.IsNotExist(err) {
|
||||||
sd.changed = true
|
|
||||||
err = nil
|
err = nil
|
||||||
secretData = sd
|
secretData = sd
|
||||||
return
|
return
|
||||||
@ -91,221 +62,10 @@ func loadSecretData(config *config.Config) (err error) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
sd.prevHash = xxhash.Sum64(ba)
|
|
||||||
|
|
||||||
secretData = sd
|
secretData = sd
|
||||||
return
|
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 {
|
func checkCertUsable(certPEM []byte) error {
|
||||||
cert, err := certinfo.ParseCertificatePEM(certPEM)
|
cert, err := certinfo.ParseCertificatePEM(certPEM)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -321,104 +81,3 @@ func checkCertUsable(certPEM []byte) error {
|
|||||||
|
|
||||||
return nil
|
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)
|
|
||||||
}
|
|
||||||
|
@ -15,34 +15,16 @@ import (
|
|||||||
"os/exec"
|
"os/exec"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
var sshHostKeys = KVSecrets[[]SSHKeyPair]{"hosts/ssh-host-keys"}
|
||||||
|
|
||||||
type SSHKeyPair struct {
|
type SSHKeyPair struct {
|
||||||
Type string
|
Type string
|
||||||
Public string
|
Public string
|
||||||
Private string
|
Private string
|
||||||
}
|
}
|
||||||
|
|
||||||
func (sd *SecretData) SSHKeyPairs(cluster, host string) (pairs []SSHKeyPair, err error) {
|
func getSSHKeyPairs(host string) (pairs []SSHKeyPair, err error) {
|
||||||
cs := sd.cluster(cluster)
|
pairs, _, err = sshHostKeys.Get(host)
|
||||||
|
|
||||||
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]
|
|
||||||
|
|
||||||
didGenerate := false
|
didGenerate := false
|
||||||
|
|
||||||
@ -59,9 +41,21 @@ genLoop:
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
didGenerate = true
|
err = func() (err error) {
|
||||||
|
outFile, err := ioutil.TempFile("/tmp", "dls-key.")
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
outPath := outFile.Name()
|
||||||
|
|
||||||
|
removeTemp := func() {
|
||||||
|
os.Remove(outPath)
|
||||||
|
os.Remove(outPath + ".pub")
|
||||||
|
}
|
||||||
|
|
||||||
removeTemp()
|
removeTemp()
|
||||||
|
defer removeTemp()
|
||||||
|
|
||||||
var out, privKey, pubKey []byte
|
var out, privKey, pubKey []byte
|
||||||
|
|
||||||
@ -80,25 +74,31 @@ genLoop:
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
os.Remove(outPath)
|
|
||||||
|
|
||||||
pubKey, err = ioutil.ReadFile(outPath + ".pub")
|
pubKey, err = ioutil.ReadFile(outPath + ".pub")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
os.Remove(outPath + ".pub")
|
|
||||||
|
|
||||||
pairs = append(pairs, SSHKeyPair{
|
pairs = append(pairs, SSHKeyPair{
|
||||||
Type: keyType,
|
Type: keyType,
|
||||||
Public: string(pubKey),
|
Public: string(pubKey),
|
||||||
Private: string(privKey),
|
Private: string(privKey),
|
||||||
})
|
})
|
||||||
|
didGenerate = true
|
||||||
|
|
||||||
|
return
|
||||||
|
}()
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if didGenerate {
|
if didGenerate {
|
||||||
cs.SSHKeyPairs[host] = pairs
|
err = sshHostKeys.Put(host, pairs)
|
||||||
err = sd.Save()
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return
|
return
|
||||||
|
@ -7,11 +7,8 @@ func init() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestSSHKeyGet(t *testing.T) {
|
func TestSSHKeyGet(t *testing.T) {
|
||||||
sd := &SecretData{
|
// TODO needs fake secret store
|
||||||
clusters: make(map[string]*ClusterSecrets),
|
// if _, err := getSSHKeyPairs("host"); err != nil {
|
||||||
}
|
// t.Error(err)
|
||||||
|
// }
|
||||||
if _, err := sd.SSHKeyPairs("test", "host"); err != nil {
|
|
||||||
t.Error(err)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
178
cmd/dkl-local-server/tls-ca.go
Normal file
178
cmd/dkl-local-server/tls-ca.go
Normal file
@ -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
|
||||||
|
}
|
@ -1,6 +1,9 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/cloudflare/cfssl/log"
|
||||||
restful "github.com/emicklei/go-restful"
|
restful "github.com/emicklei/go-restful"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -18,6 +21,51 @@ func wsClusterCA(req *restful.Request, resp *restful.Response) {
|
|||||||
clusterCAs.WsGet(resp, clusterName+"/"+name)
|
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")
|
var clusterCASignedKeys = newClusterSecretKV[KeyCert]("CA-signed-keys")
|
||||||
|
|
||||||
func wsClusterCASignedKeys(req *restful.Request, resp *restful.Response) {
|
func wsClusterCASignedKeys(req *restful.Request, resp *restful.Response) {
|
||||||
|
@ -1,11 +1,35 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"crypto/rand"
|
||||||
|
"encoding/base32"
|
||||||
|
|
||||||
restful "github.com/emicklei/go-restful"
|
restful "github.com/emicklei/go-restful"
|
||||||
)
|
)
|
||||||
|
|
||||||
var clusterTokens = newClusterSecretKV[string]("tokens")
|
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) {
|
func wsClusterTokens(req *restful.Request, resp *restful.Response) {
|
||||||
clusterName := req.PathParameter("cluster-name")
|
clusterName := req.PathParameter("cluster-name")
|
||||||
clusterTokens.WsList(resp, clusterName+"/")
|
clusterTokens.WsList(resp, clusterName+"/")
|
||||||
|
@ -68,7 +68,18 @@ func wsClusterAddons(req *restful.Request, resp *restful.Response) {
|
|||||||
return
|
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) {
|
func wsClusterCACert(req *restful.Request, resp *restful.Response) {
|
||||||
|
@ -8,6 +8,7 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"text/template"
|
"text/template"
|
||||||
|
|
||||||
|
cfsslconfig "github.com/cloudflare/cfssl/config"
|
||||||
"github.com/emicklei/go-restful"
|
"github.com/emicklei/go-restful"
|
||||||
|
|
||||||
"novit.tech/direktil/pkg/localconfig"
|
"novit.tech/direktil/pkg/localconfig"
|
||||||
@ -186,8 +187,8 @@ func wsError(resp *restful.Response, err error) {
|
|||||||
http.StatusText(http.StatusInternalServerError))
|
http.StatusText(http.StatusInternalServerError))
|
||||||
}
|
}
|
||||||
|
|
||||||
func wsRender(resp *restful.Response, tmplStr string, value interface{}) {
|
func wsRender(resp *restful.Response, sslCfg *cfsslconfig.Config, tmplStr string, value interface{}) {
|
||||||
tmpl, err := template.New("wsRender").Funcs(templateFuncs).Parse(tmplStr)
|
tmpl, err := template.New("wsRender").Funcs(templateFuncs(sslCfg)).Parse(tmplStr)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
wsError(resp, err)
|
wsError(resp, err)
|
||||||
return
|
return
|
||||||
|
@ -1,21 +1,32 @@
|
|||||||
export default {
|
export default {
|
||||||
props: [ 'name', 'href', 'token' ],
|
props: [ 'name', 'href', 'token' ],
|
||||||
data() { return {showCopied: false} },
|
data() { return {showCopied: false} },
|
||||||
template: `<span class="notif"><div v-if="showCopied">copied!</div><a :href="href" @click="fetchAndCopy()">{{name}}<small> 🗐</small></a></span>`,
|
template: `<span class="notif"><div v-if="showCopied">copied!</div><a :href="href" @click="fetchAndSave()">{{name}}</a> <a href="#" class="copy" @click="fetchAndCopy()">🗐</a></span>`,
|
||||||
methods: {
|
methods: {
|
||||||
fetchAndCopy() {
|
fetch() {
|
||||||
event.preventDefault()
|
event.preventDefault()
|
||||||
|
return fetch(this.href, {
|
||||||
fetch(this.href, {
|
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
headers: { 'Authorization': 'Bearer ' + this.token },
|
headers: { 'Authorization': 'Bearer ' + this.token },
|
||||||
}).then((resp) => resp.headers.get("content-type") == "application/json" ? resp.json() : resp.text())
|
})
|
||||||
|
},
|
||||||
|
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) => {
|
.then((value) => {
|
||||||
window.navigator.clipboard.writeText(value)
|
window.navigator.clipboard.writeText(value)
|
||||||
this.showCopied = true
|
this.showCopied = true
|
||||||
setTimeout(() => { this.showCopied = false }, 1000)
|
setTimeout(() => { this.showCopied = false }, 1000)
|
||||||
})
|
}).catch(this.handleFetchError)
|
||||||
.catch((e) => { console.log("failed to get value:", e); alert('failed to get value') })
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
@ -145,3 +145,5 @@ header .utils > * {
|
|||||||
background: black;
|
background: black;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.copy { font-size: small; }
|
||||||
|
Loading…
Reference in New Issue
Block a user