add ssh user CA support
This commit is contained in:
@ -275,6 +275,10 @@ func (ctx *renderContext) templateFuncs(ctxMap map[string]any) map[string]interf
|
||||
return getKeyCert(name, "tls_dir")
|
||||
},
|
||||
|
||||
"ssh_user_ca": func(path string) (s string) {
|
||||
return fmt.Sprintf("{{ ssh_user_ca %q %q}}",
|
||||
path, cluster)
|
||||
},
|
||||
"ssh_host_keys": func(dir string) (s string) {
|
||||
return fmt.Sprintf("{{ ssh_host_keys %q %q \"\"}}",
|
||||
dir, cluster)
|
||||
|
@ -69,11 +69,19 @@ func buildInitrd(out io.Writer, ctx *renderContext) (err error) {
|
||||
cat.AppendBytes(cfgBytes, "config.yaml", 0600)
|
||||
|
||||
// ssh keys
|
||||
// FIXME we want a bootstrap-stage key instead of the real host key
|
||||
// XXX do we want a bootstrap-stage key instead of the real host key?
|
||||
for _, format := range []string{"rsa", "dsa", "ecdsa", "ed25519"} {
|
||||
cat.AppendBytes(cfg.FileContent("/etc/ssh/ssh_host_"+format+"_key"), "id_"+format, 0600)
|
||||
}
|
||||
|
||||
// ssh user CA
|
||||
userCA, err := sshCAPubKey(ctx.Host.ClusterName)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get SSH user CA: %w", err)
|
||||
}
|
||||
|
||||
cat.AppendBytes(userCA, "user_ca.pub", 0600)
|
||||
|
||||
return cat.Close()
|
||||
}
|
||||
|
||||
|
@ -148,7 +148,7 @@ func (ctx *renderContext) Tag() (string, error) {
|
||||
|
||||
enc := yaml.NewEncoder(h)
|
||||
|
||||
for _, o := range []interface{}{cfg, ctx} {
|
||||
for _, o := range []any{cfg, ctx} {
|
||||
if err := enc.Encode(o); err != nil {
|
||||
return "", err
|
||||
}
|
||||
@ -157,21 +157,6 @@ func (ctx *renderContext) Tag() (string, error) {
|
||||
return hex.EncodeToString(h.Sum(nil)), nil
|
||||
}
|
||||
|
||||
func asMap(v interface{}) map[string]interface{} {
|
||||
ba, err := yaml.Marshal(v)
|
||||
if err != nil {
|
||||
panic(err) // shouldn't happen
|
||||
}
|
||||
|
||||
result := make(map[string]interface{})
|
||||
|
||||
if err := yaml.Unmarshal(ba, result); err != nil {
|
||||
panic(err) // shouldn't happen
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
func (ctx *renderContext) TemplateFuncs() map[string]any {
|
||||
funcs := templateFuncs(ctx.SSLConfig)
|
||||
|
||||
@ -187,6 +172,14 @@ func (ctx *renderContext) TemplateFuncs() map[string]any {
|
||||
return hex.EncodeToString(ba[:])
|
||||
},
|
||||
|
||||
"ssh_user_ca": func(path, cluster string) (s string, err error) {
|
||||
userCA, err := sshCAPubKey(cluster)
|
||||
return asYaml([]config.FileDef{{
|
||||
Path: path,
|
||||
Mode: 0644,
|
||||
Content: string(userCA),
|
||||
}})
|
||||
},
|
||||
"ssh_host_keys": func(dir, cluster, host string) (s string, err error) {
|
||||
if host == "" {
|
||||
host = ctx.Host.Name
|
||||
|
@ -1,17 +1,19 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"crypto/dsa"
|
||||
"crypto/ecdsa"
|
||||
"bytes"
|
||||
"crypto"
|
||||
"crypto/ed25519"
|
||||
"crypto/elliptic"
|
||||
"crypto/rand"
|
||||
"crypto/rsa"
|
||||
"crypto/x509"
|
||||
"encoding/asn1"
|
||||
"encoding/pem"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"os/exec"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"golang.org/x/crypto/ssh"
|
||||
)
|
||||
|
||||
var sshHostKeys = KVSecrets[[]SSHKeyPair]{"hosts/ssh-host-keys"}
|
||||
@ -104,64 +106,106 @@ genLoop:
|
||||
return
|
||||
}
|
||||
|
||||
func sshKeyGenDSA() (data []byte, pubKey interface{}, err error) {
|
||||
privKey := &dsa.PrivateKey{}
|
||||
var sshCAKeys = KVSecrets[string]{"ssh-ca-keys"}
|
||||
|
||||
err = dsa.GenerateParameters(&privKey.Parameters, rand.Reader, dsa.L1024N160)
|
||||
func sshCAKey(cluster string) (caKeyPem string, err error) {
|
||||
storeKey := "clusters/" + cluster
|
||||
caKeyPem, _, err = sshCAKeys.Get(storeKey)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
err = dsa.GenerateKey(privKey, rand.Reader)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
if caKeyPem == "" {
|
||||
_, pk, err := ed25519.GenerateKey(nil)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
data, err = asn1.Marshal(*privKey)
|
||||
//data, err = x509.MarshalPKCS8PrivateKey(privKey)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
pemBlock, err := ssh.MarshalPrivateKey(crypto.PrivateKey(pk), "")
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
pubKey = privKey.PublicKey
|
||||
return
|
||||
}
|
||||
|
||||
func sshKeyGenRSA() (data []byte, pubKey interface{}, err error) {
|
||||
privKey, err := rsa.GenerateKey(rand.Reader, 2048)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
data = x509.MarshalPKCS1PrivateKey(privKey)
|
||||
pubKey = privKey.Public()
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func sshKeyGenECDSA() (data []byte, pubKey interface{}, err error) {
|
||||
privKey, err := ecdsa.GenerateKey(elliptic.P521(), rand.Reader)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
data, err = x509.MarshalPKCS8PrivateKey(privKey)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
pubKey = privKey.Public()
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func sshKeyGenED25519() (data []byte, pubKey interface{}, err error) {
|
||||
pubKey, privKey, err := ed25519.GenerateKey(rand.Reader)
|
||||
|
||||
data, err = x509.MarshalPKCS8PrivateKey(privKey)
|
||||
if err != nil {
|
||||
return
|
||||
caKeyPem = string(pem.EncodeToMemory(pemBlock))
|
||||
sshCAKeys.Put(storeKey, caKeyPem)
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func sshCAPubKey(cluster string) (pubKey []byte, err error) {
|
||||
keyPem, err := sshCAKey(cluster)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
k, err := ssh.ParsePrivateKey([]byte(keyPem))
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
pubKey = ssh.MarshalAuthorizedKey(k.PublicKey())
|
||||
return
|
||||
}
|
||||
|
||||
// principal: user (login) to allow (ie: "root")
|
||||
// validity: ssh-keygen validity string (ie: "+1h", "202506280811:202506281011", ""=forever)
|
||||
// options: ssh-keygen options (ie: "force-command=/bin/date +\"%F %T\"", "source-address=192.168.1.0/24,192.168.42.0/24"
|
||||
func sshCASign(cluster string, userPubKey []byte, principal, validity string, options ...string) (cert []byte, err error) {
|
||||
caKey, err := sshCAKey(cluster)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
_, identity, _, _, err := ssh.ParseAuthorizedKey(userPubKey)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
userPubKeyFile, err := os.CreateTemp("/tmp", "user.pub")
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
defer os.Remove(userPubKeyFile.Name())
|
||||
|
||||
_, err = io.Copy(userPubKeyFile, bytes.NewBuffer(userPubKey))
|
||||
userPubKeyFile.Close()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
err = os.WriteFile(userPubKeyFile.Name(), userPubKey, 0600)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
serial := strconv.FormatInt(time.Now().Unix(), 10)
|
||||
cmd := exec.Command("ssh-keygen", "-q", "-s", "/dev/stdin", "-I", identity, "-z", serial, "-n", principal)
|
||||
|
||||
if validity != "" {
|
||||
cmd.Args = append(cmd.Args, "-V", validity)
|
||||
}
|
||||
|
||||
for _, opt := range options {
|
||||
cmd.Args = append(cmd.Args, "-O", opt)
|
||||
}
|
||||
|
||||
cmd.Args = append(cmd.Args, userPubKeyFile.Name())
|
||||
|
||||
stderr := new(bytes.Buffer)
|
||||
cmd.Stdin = bytes.NewBuffer([]byte(caKey))
|
||||
cmd.Stderr = stderr
|
||||
|
||||
err = cmd.Run()
|
||||
if err != nil {
|
||||
err = fmt.Errorf("ssh-keygen sign failed: %s", strings.TrimSpace(stderr.String()))
|
||||
return
|
||||
}
|
||||
|
||||
certFile := userPubKeyFile.Name() + "-cert.pub"
|
||||
cert, err = os.ReadFile(certFile)
|
||||
|
||||
os.Remove(certFile)
|
||||
|
||||
return
|
||||
}
|
||||
|
@ -121,3 +121,41 @@ func wsClusterSignedCert(req *restful.Request, resp *restful.Response) {
|
||||
resp.AddHeader("Content-Disposition", "attachment; filename="+strconv.Quote(clusterName+"_"+caName+"_"+url.PathEscape(name)+".crt"))
|
||||
resp.Write(kc.Cert)
|
||||
}
|
||||
|
||||
type SSHSignReq struct {
|
||||
PubKey string
|
||||
Principal string
|
||||
Validity string
|
||||
Options []string
|
||||
}
|
||||
|
||||
func wsClusterSSHUserCAPubKey(req *restful.Request, resp *restful.Response) {
|
||||
clusterName := req.PathParameter("cluster-name")
|
||||
|
||||
pubkey, err := sshCAPubKey(clusterName)
|
||||
if err != nil {
|
||||
wsError(resp, err)
|
||||
return
|
||||
}
|
||||
|
||||
resp.Write(pubkey)
|
||||
}
|
||||
|
||||
func wsClusterSSHUserCASign(req *restful.Request, resp *restful.Response) {
|
||||
clusterName := req.PathParameter("cluster-name")
|
||||
|
||||
signReq := SSHSignReq{}
|
||||
err := req.ReadEntity(&signReq)
|
||||
if err != nil {
|
||||
wsError(resp, err)
|
||||
return
|
||||
}
|
||||
|
||||
cert, err := sshCASign(clusterName, []byte(signReq.PubKey), signReq.Principal, signReq.Validity, signReq.Options...)
|
||||
if err != nil {
|
||||
wsError(resp, err)
|
||||
return
|
||||
}
|
||||
|
||||
resp.Write(cert)
|
||||
}
|
||||
|
@ -86,8 +86,9 @@ func registerWS(rest *restful.Container) {
|
||||
Doc("Delete a host template instance"))
|
||||
|
||||
const (
|
||||
GET = http.MethodGet
|
||||
PUT = http.MethodPut
|
||||
GET = http.MethodGet
|
||||
PUT = http.MethodPut
|
||||
POST = http.MethodPost
|
||||
)
|
||||
|
||||
cluster := func(method, subPath string) *restful.RouteBuilder {
|
||||
@ -126,6 +127,13 @@ func registerWS(rest *restful.Container) {
|
||||
Produces(mime.CERT).
|
||||
Param(ws.QueryParameter("name", "signed reference name").Required(true)).
|
||||
Doc("Get cluster's certificate signed by the CA"),
|
||||
|
||||
cluster(GET, "/ssh/user-ca").To(wsClusterSSHUserCAPubKey).
|
||||
Produces(mime.OCTET).
|
||||
Doc("User CA public key for this cluster"),
|
||||
cluster(POST, "/ssh/user-ca/sign").To(wsClusterSSHUserCASign).
|
||||
Produces(mime.OCTET).
|
||||
Doc("Sign a user's SSH public key for this cluster"),
|
||||
} {
|
||||
ws.Route(builder)
|
||||
}
|
||||
|
Reference in New Issue
Block a user