2019-12-03 11:03:20 +01:00
|
|
|
package main
|
|
|
|
|
|
|
|
import (
|
2025-06-28 11:04:44 +02:00
|
|
|
"bytes"
|
|
|
|
"crypto"
|
2019-12-03 11:03:20 +01:00
|
|
|
"crypto/ed25519"
|
2025-06-28 11:04:44 +02:00
|
|
|
"encoding/pem"
|
2019-12-03 11:03:20 +01:00
|
|
|
"fmt"
|
2025-06-28 11:04:44 +02:00
|
|
|
"io"
|
2019-12-03 11:03:20 +01:00
|
|
|
"os"
|
|
|
|
"os/exec"
|
2025-06-28 11:04:44 +02:00
|
|
|
"strconv"
|
|
|
|
"strings"
|
|
|
|
"time"
|
|
|
|
|
|
|
|
"golang.org/x/crypto/ssh"
|
2019-12-03 11:03:20 +01:00
|
|
|
)
|
|
|
|
|
2023-02-12 15:18:42 +01:00
|
|
|
var sshHostKeys = KVSecrets[[]SSHKeyPair]{"hosts/ssh-host-keys"}
|
|
|
|
|
2019-12-03 11:03:20 +01:00
|
|
|
type SSHKeyPair struct {
|
|
|
|
Type string
|
|
|
|
Public string
|
|
|
|
Private string
|
|
|
|
}
|
|
|
|
|
2023-02-12 15:18:42 +01:00
|
|
|
func getSSHKeyPairs(host string) (pairs []SSHKeyPair, err error) {
|
|
|
|
pairs, _, err = sshHostKeys.Get(host)
|
2019-12-03 11:03:20 +01:00
|
|
|
|
|
|
|
didGenerate := false
|
|
|
|
|
|
|
|
genLoop:
|
|
|
|
for _, keyType := range []string{
|
|
|
|
"rsa",
|
|
|
|
"dsa",
|
|
|
|
"ecdsa",
|
|
|
|
"ed25519",
|
|
|
|
} {
|
|
|
|
for _, pair := range pairs {
|
|
|
|
if pair.Type == keyType {
|
|
|
|
continue genLoop
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-02-12 15:18:42 +01:00
|
|
|
err = func() (err error) {
|
2025-01-26 11:31:04 +01:00
|
|
|
outFile, err := os.CreateTemp("/tmp", "dls-key.")
|
2023-02-12 15:18:42 +01:00
|
|
|
if err != nil {
|
|
|
|
return
|
|
|
|
}
|
2019-12-03 11:03:20 +01:00
|
|
|
|
2023-02-12 15:18:42 +01:00
|
|
|
outPath := outFile.Name()
|
2019-12-03 11:03:20 +01:00
|
|
|
|
2023-02-12 15:18:42 +01:00
|
|
|
removeTemp := func() {
|
|
|
|
os.Remove(outPath)
|
|
|
|
os.Remove(outPath + ".pub")
|
|
|
|
}
|
2019-12-03 11:03:20 +01:00
|
|
|
|
2023-02-12 18:59:14 +01:00
|
|
|
removeTemp()
|
2023-02-12 15:18:42 +01:00
|
|
|
defer removeTemp()
|
2019-12-03 11:03:20 +01:00
|
|
|
|
2023-02-12 15:18:42 +01:00
|
|
|
var out, privKey, pubKey []byte
|
|
|
|
|
2023-02-12 18:59:14 +01:00
|
|
|
cmd := exec.Command("ssh-keygen",
|
2023-02-12 15:18:42 +01:00
|
|
|
"-N", "",
|
|
|
|
"-C", "root@"+host,
|
|
|
|
"-f", outPath,
|
2023-02-12 18:59:14 +01:00
|
|
|
"-t", keyType)
|
|
|
|
out, err = cmd.CombinedOutput()
|
2023-02-12 15:18:42 +01:00
|
|
|
if err != nil {
|
|
|
|
err = fmt.Errorf("ssh-keygen failed: %v: %s", err, string(out))
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2025-01-26 11:31:04 +01:00
|
|
|
privKey, err = os.ReadFile(outPath)
|
2023-02-12 15:18:42 +01:00
|
|
|
if err != nil {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2025-01-26 11:31:04 +01:00
|
|
|
pubKey, err = os.ReadFile(outPath + ".pub")
|
2023-02-12 15:18:42 +01:00
|
|
|
if err != nil {
|
|
|
|
return
|
|
|
|
}
|
2019-12-03 11:03:20 +01:00
|
|
|
|
2023-02-12 15:18:42 +01:00
|
|
|
pairs = append(pairs, SSHKeyPair{
|
|
|
|
Type: keyType,
|
|
|
|
Public: string(pubKey),
|
|
|
|
Private: string(privKey),
|
|
|
|
})
|
|
|
|
didGenerate = true
|
|
|
|
|
|
|
|
return
|
|
|
|
}()
|
2019-12-03 11:03:20 +01:00
|
|
|
|
|
|
|
if err != nil {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if didGenerate {
|
2023-02-12 15:18:42 +01:00
|
|
|
err = sshHostKeys.Put(host, pairs)
|
|
|
|
if err != nil {
|
|
|
|
return
|
|
|
|
}
|
2019-12-03 11:03:20 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2025-06-28 11:04:44 +02:00
|
|
|
var sshCAKeys = KVSecrets[string]{"ssh-ca-keys"}
|
2019-12-03 11:03:20 +01:00
|
|
|
|
2025-06-28 11:04:44 +02:00
|
|
|
func sshCAKey(cluster string) (caKeyPem string, err error) {
|
|
|
|
storeKey := "clusters/" + cluster
|
|
|
|
caKeyPem, _, err = sshCAKeys.Get(storeKey)
|
2019-12-03 11:03:20 +01:00
|
|
|
if err != nil {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2025-06-28 11:04:44 +02:00
|
|
|
if caKeyPem == "" {
|
|
|
|
_, pk, err := ed25519.GenerateKey(nil)
|
|
|
|
if err != nil {
|
|
|
|
return "", err
|
|
|
|
}
|
|
|
|
|
|
|
|
pemBlock, err := ssh.MarshalPrivateKey(crypto.PrivateKey(pk), "")
|
|
|
|
if err != nil {
|
|
|
|
return "", err
|
|
|
|
}
|
|
|
|
|
|
|
|
caKeyPem = string(pem.EncodeToMemory(pemBlock))
|
|
|
|
sshCAKeys.Put(storeKey, caKeyPem)
|
|
|
|
}
|
|
|
|
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
func sshCAPubKey(cluster string) (pubKey []byte, err error) {
|
|
|
|
keyPem, err := sshCAKey(cluster)
|
2019-12-03 11:03:20 +01:00
|
|
|
if err != nil {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2025-06-28 11:04:44 +02:00
|
|
|
k, err := ssh.ParsePrivateKey([]byte(keyPem))
|
2019-12-03 11:03:20 +01:00
|
|
|
if err != nil {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2025-06-28 11:04:44 +02:00
|
|
|
pubKey = ssh.MarshalAuthorizedKey(k.PublicKey())
|
2019-12-03 11:03:20 +01:00
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2025-06-28 11:04:44 +02:00
|
|
|
// 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)
|
2019-12-03 11:03:20 +01:00
|
|
|
if err != nil {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2025-06-28 11:04:44 +02:00
|
|
|
_, identity, _, _, err := ssh.ParseAuthorizedKey(userPubKey)
|
|
|
|
if err != nil {
|
|
|
|
return
|
|
|
|
}
|
2019-12-03 11:03:20 +01:00
|
|
|
|
2025-06-28 11:04:44 +02:00
|
|
|
userPubKeyFile, err := os.CreateTemp("/tmp", "user.pub")
|
|
|
|
if err != nil {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
defer os.Remove(userPubKeyFile.Name())
|
2019-12-03 11:03:20 +01:00
|
|
|
|
2025-06-28 11:04:44 +02:00
|
|
|
_, err = io.Copy(userPubKeyFile, bytes.NewBuffer(userPubKey))
|
|
|
|
userPubKeyFile.Close()
|
2019-12-03 11:03:20 +01:00
|
|
|
if err != nil {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2025-06-28 11:04:44 +02:00
|
|
|
err = os.WriteFile(userPubKeyFile.Name(), userPubKey, 0600)
|
2019-12-03 11:03:20 +01:00
|
|
|
if err != nil {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2025-06-28 11:04:44 +02:00
|
|
|
serial := strconv.FormatInt(time.Now().Unix(), 10)
|
|
|
|
cmd := exec.Command("ssh-keygen", "-q", "-s", "/dev/stdin", "-I", identity, "-z", serial, "-n", principal)
|
2019-12-03 11:03:20 +01:00
|
|
|
|
2025-06-28 11:04:44 +02:00
|
|
|
if validity != "" {
|
|
|
|
cmd.Args = append(cmd.Args, "-V", validity)
|
|
|
|
}
|
|
|
|
|
|
|
|
for _, opt := range options {
|
|
|
|
cmd.Args = append(cmd.Args, "-O", opt)
|
|
|
|
}
|
2019-12-03 11:03:20 +01:00
|
|
|
|
2025-06-28 11:04:44 +02:00
|
|
|
cmd.Args = append(cmd.Args, userPubKeyFile.Name())
|
2019-12-03 11:03:20 +01:00
|
|
|
|
2025-06-28 11:04:44 +02:00
|
|
|
stderr := new(bytes.Buffer)
|
|
|
|
cmd.Stdin = bytes.NewBuffer([]byte(caKey))
|
|
|
|
cmd.Stderr = stderr
|
|
|
|
|
|
|
|
err = cmd.Run()
|
2019-12-03 11:03:20 +01:00
|
|
|
if err != nil {
|
2025-06-28 11:04:44 +02:00
|
|
|
err = fmt.Errorf("ssh-keygen sign failed: %s", strings.TrimSpace(stderr.String()))
|
2019-12-03 11:03:20 +01:00
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2025-06-28 11:04:44 +02:00
|
|
|
certFile := userPubKeyFile.Name() + "-cert.pub"
|
|
|
|
cert, err = os.ReadFile(certFile)
|
|
|
|
|
|
|
|
os.Remove(certFile)
|
|
|
|
|
2019-12-03 11:03:20 +01:00
|
|
|
return
|
|
|
|
}
|