migration to new secrets nearly complete
This commit is contained in:
parent
3bc20e95cc
commit
11f3c953e2
@ -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) {
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
// }
|
||||
}
|
||||
|
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
|
||||
|
||||
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) {
|
||||
|
@ -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+"/")
|
||||
|
@ -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) {
|
||||
|
@ -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
|
||||
|
@ -1,21 +1,32 @@
|
||||
export default {
|
||||
props: [ 'name', 'href', 'token' ],
|
||||
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: {
|
||||
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)
|
||||
},
|
||||
},
|
||||
}
|
||||
|
@ -145,3 +145,5 @@ header .utils > * {
|
||||
background: black;
|
||||
}
|
||||
}
|
||||
|
||||
.copy { font-size: small; }
|
||||
|
Loading…
Reference in New Issue
Block a user