From a332e1233895bbcd9ac916c9c8675f3cf6dfe835 Mon Sep 17 00:00:00 2001 From: Hidde Beydals Date: Fri, 5 Jun 2020 22:22:54 +0200 Subject: [PATCH 1/7] Replace SSH shell-outs with Go implementation --- cmd/tk/check.go | 18 -------- cmd/tk/create_source_git.go | 79 ++++++++++++++++++-------------- go.mod | 1 + internal/ssh/host_scan.go | 47 ++++++++++++++++++++ internal/ssh/key_pair.go | 89 +++++++++++++++++++++++++++++++++++++ 5 files changed, 183 insertions(+), 51 deletions(-) create mode 100644 internal/ssh/host_scan.go create mode 100644 internal/ssh/key_pair.go diff --git a/cmd/tk/check.go b/cmd/tk/check.go index a74fee3e..29cf999b 100644 --- a/cmd/tk/check.go +++ b/cmd/tk/check.go @@ -45,9 +45,6 @@ func runCheckCmd(cmd *cobra.Command, args []string) error { logAction("checking prerequisites") checkFailed := false - if !sshCheck() { - checkFailed = true - } if !kubectlCheck(ctx, ">=1.18.0") { checkFailed = true @@ -76,21 +73,6 @@ func runCheckCmd(cmd *cobra.Command, args []string) error { return nil } -func sshCheck() bool { - ok := true - for _, cmd := range []string{"ssh-keygen", "ssh-keyscan"} { - _, err := exec.LookPath(cmd) - if err != nil { - logFailure("%s not found", cmd) - ok = false - } else { - logSuccess("%s found", cmd) - } - } - - return ok -} - func kubectlCheck(ctx context.Context, version string) bool { _, err := exec.LookPath("kubectl") if err != nil { diff --git a/cmd/tk/create_source_git.go b/cmd/tk/create_source_git.go index b5953b27..694e0842 100644 --- a/cmd/tk/create_source_git.go +++ b/cmd/tk/create_source_git.go @@ -2,20 +2,24 @@ package main import ( "context" + "crypto/elliptic" "fmt" + "io/ioutil" + "net/url" + "os" + "strings" + sourcev1 "github.com/fluxcd/source-controller/api/v1alpha1" "github.com/manifoldco/promptui" "github.com/spf13/cobra" - "io/ioutil" corev1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/types" "k8s.io/apimachinery/pkg/util/wait" - "net/url" - "os" "sigs.k8s.io/controller-runtime/pkg/client" - "strings" + + "github.com/fluxcd/toolkit/internal/ssh" ) var createSourceGitCmd = &cobra.Command{ @@ -55,12 +59,14 @@ For private Git repositories, the basic authentication credentials are stored in } var ( - sourceGitURL string - sourceGitBranch string - sourceGitTag string - sourceGitSemver string - sourceGitUsername string - sourceGitPassword string + sourceGitURL string + sourceGitBranch string + sourceGitTag string + sourceGitSemver string + sourceGitUsername string + sourceGitPassword string + sourceGitKeyAlgorithm string + sourceGitRSABits int ) func init() { @@ -70,6 +76,8 @@ func init() { createSourceGitCmd.Flags().StringVar(&sourceGitSemver, "tag-semver", "", "git tag semver range") createSourceGitCmd.Flags().StringVarP(&sourceGitUsername, "username", "u", "", "basic authentication username") createSourceGitCmd.Flags().StringVarP(&sourceGitPassword, "password", "p", "", "basic authentication password") + createSourceGitCmd.Flags().StringVarP(&sourceGitKeyAlgorithm, "ssh-algorithm", "", "rsa", "SSH public key algorithm") + createSourceGitCmd.Flags().IntVarP(&sourceGitRSABits, "ssh-rsa-bits", "", 2048, "SSH RSA public key bit size") createSourceCmd.AddCommand(createSourceGitCmd) } @@ -99,8 +107,20 @@ func createSourceGitCmdRun(cmd *cobra.Command, args []string) error { defer cancel() withAuth := false - if strings.HasPrefix(sourceGitURL, "ssh") { - if err := generateSSH(ctx, name, u.Host, tmpDir); err != nil { + if u.Scheme == "ssh" { + var keyGen ssh.KeyPairGenerator + switch strings.ToLower(sourceGitKeyAlgorithm) { + case "rsa": + keyGen = ssh.NewRSAGenerator(sourceGitRSABits) + case "ecdsa": + // TODO(hidde): make curve configurable by flag + keyGen = ssh.NewECDSAGenerator(elliptic.P521()) + } + host := u.Host + if u.Port() == "" { + host = host + ":22" + } + if err := generateSSH(ctx, keyGen, name, host, tmpDir); err != nil { return err } withAuth = true @@ -193,27 +213,13 @@ func generateBasicAuth(ctx context.Context, name string) error { return nil } -func generateSSH(ctx context.Context, name, host, tmpDir string) error { - logGenerate("generating host key for %s", host) - - command := fmt.Sprintf("ssh-keyscan %s > %s/known_hosts", host, tmpDir) - if _, err := utils.execCommand(ctx, ModeStderrOS, command); err != nil { - return fmt.Errorf("ssh-keyscan failed") - } - +func generateSSH(ctx context.Context, generator ssh.KeyPairGenerator, name, host, user string) error { logGenerate("generating deploy key") - - command = fmt.Sprintf("ssh-keygen -b 2048 -t rsa -f %s/identity -q -N \"\"", tmpDir) - if _, err := utils.execCommand(ctx, ModeStderrOS, command); err != nil { - return fmt.Errorf("ssh-keygen failed") - } - - command = fmt.Sprintf("cat %s/identity.pub", tmpDir) - if deployKey, err := utils.execCommand(ctx, ModeCapture, command); err != nil { - return fmt.Errorf("unable to read identity.pub: %w", err) - } else { - fmt.Print(deployKey) + kp, err := generator.Generate() + if err != nil { + return fmt.Errorf("SSH key pair generation failed: %w", err) } + fmt.Printf("%s", kp.PublicKey) prompt := promptui.Prompt{ Label: "Have you added the deploy key to your repository", @@ -223,9 +229,16 @@ func generateSSH(ctx context.Context, name, host, tmpDir string) error { return fmt.Errorf("aborting") } + logAction("collecting SSH server public key for generated public key algorithm") + serverKey, err := ssh.ScanHostKey(host, user, kp) + if err != nil { + return err + } + logSuccess("collected public key from SSH server") + logAction("saving keys") - files := fmt.Sprintf("--from-file=%s/identity --from-file=%s/identity.pub --from-file=%s/known_hosts", - tmpDir, tmpDir, tmpDir) + files := fmt.Sprintf("--from-literal=identity=\"%s\" --from-literal=identity.pub=\"%s\" --from-literal=known_hosts=\"%s\"", + kp.PublicKey, kp.PrivateKey, serverKey) secret := fmt.Sprintf("kubectl -n %s create secret generic %s %s --dry-run=client -oyaml | kubectl apply -f-", namespace, name, files) if _, err := utils.execCommand(ctx, ModeOS, secret); err != nil { diff --git a/go.mod b/go.mod index 8f051ccb..1d0ca0cc 100644 --- a/go.mod +++ b/go.mod @@ -8,6 +8,7 @@ require ( github.com/fluxcd/source-controller v0.0.1-beta.1 github.com/manifoldco/promptui v0.7.0 github.com/spf13/cobra v1.0.0 + golang.org/x/crypto v0.0.0-20200302210943-78000ba7a073 k8s.io/api v0.18.2 k8s.io/apimachinery v0.18.2 k8s.io/client-go v0.18.2 diff --git a/internal/ssh/host_scan.go b/internal/ssh/host_scan.go new file mode 100644 index 00000000..85d6720c --- /dev/null +++ b/internal/ssh/host_scan.go @@ -0,0 +1,47 @@ +package ssh + +import ( + "encoding/base64" + "fmt" + "net" + + "golang.org/x/crypto/ssh" + "golang.org/x/crypto/ssh/knownhosts" +) + +func ScanHostKey(host string, user string, pair *KeyPair) ([]byte, error) { + signer, err := ssh.ParsePrivateKey(pair.PrivateKey) + if err != nil { + return nil, err + } + col := &collector{} + config := &ssh.ClientConfig{ + User: user, + Auth: []ssh.AuthMethod{ + ssh.PublicKeys(signer), + }, + HostKeyCallback: col.StoreKey(), + } + client, err := ssh.Dial("tcp", host, config) + if err == nil { + defer client.Close() + } + if len(col.knownKeys) > 0 { + return col.knownKeys, nil + } + return col.knownKeys, err +} + +type collector struct { + knownKeys []byte +} + +func (c *collector) StoreKey() ssh.HostKeyCallback { + return func(hostname string, remote net.Addr, key ssh.PublicKey) error { + c.knownKeys = append( + c.knownKeys, + fmt.Sprintf("%s %s %s\n", knownhosts.Normalize(hostname), key.Type(), base64.StdEncoding.EncodeToString(key.Marshal()))..., + ) + return nil + } +} diff --git a/internal/ssh/key_pair.go b/internal/ssh/key_pair.go new file mode 100644 index 00000000..f251c18c --- /dev/null +++ b/internal/ssh/key_pair.go @@ -0,0 +1,89 @@ +package ssh + +import ( + "crypto/ecdsa" + "crypto/elliptic" + "crypto/rand" + "crypto/rsa" + "crypto/x509" + "encoding/pem" + + "golang.org/x/crypto/ssh" +) + +type KeyPair struct { + PublicKey []byte + PrivateKey []byte +} + +type KeyPairGenerator interface { + Generate() (*KeyPair, error) +} + +type RSAGenerator struct { + bits int +} + +func NewRSAGenerator(bits int) KeyPairGenerator { + return &RSAGenerator{bits} +} + +func (g *RSAGenerator) Generate() (*KeyPair, error) { + pk, err := rsa.GenerateKey(rand.Reader, g.bits) + if err != nil { + return nil, err + } + err = pk.Validate() + if err != nil { + return nil, err + } + pub, err := generatePublicKey(&pk.PublicKey) + if err != nil { + return nil, err + } + return &KeyPair{ + PublicKey: pub, + PrivateKey: encodePrivateKeyToPEM(pk), + }, nil +} + +type ECDSAGenerator struct { + c elliptic.Curve +} + +func NewECDSAGenerator(c elliptic.Curve) KeyPairGenerator { + return &ECDSAGenerator{c} +} + +func (g *ECDSAGenerator) Generate() (*KeyPair, error) { + pk, err := ecdsa.GenerateKey(g.c, rand.Reader) + if err != nil { + return nil, err + } + pub, err := generatePublicKey(&pk.PublicKey) + if err != nil { + return nil, err + } + return &KeyPair{ + PublicKey: pub, + PrivateKey: encodePrivateKeyToPEM(pk), + }, nil +} + +func generatePublicKey(pk interface{}) ([]byte, error) { + b, err := ssh.NewPublicKey(pk) + if err != nil { + return nil, err + } + k := ssh.MarshalAuthorizedKey(b) + return k, nil +} + +func encodePrivateKeyToPEM(pk interface{}) []byte { + b, _ := x509.MarshalPKCS8PrivateKey(pk) + block := pem.Block{ + Type: "PRIVATE KEY", + Bytes: b, + } + return pem.EncodeToMemory(&block) +} From 2dfe88b82d558361cc523bd399b2c59857bfd2be Mon Sep 17 00:00:00 2001 From: Hidde Beydals Date: Mon, 8 Jun 2020 13:44:51 +0200 Subject: [PATCH 2/7] Move ssh package from internal to pkg --- cmd/tk/create_source_git.go | 2 +- {internal => pkg}/ssh/host_scan.go | 4 ++++ {internal => pkg}/ssh/key_pair.go | 25 ++++++++++++++++++++----- 3 files changed, 25 insertions(+), 6 deletions(-) rename {internal => pkg}/ssh/host_scan.go (81%) rename {internal => pkg}/ssh/key_pair.go (69%) diff --git a/cmd/tk/create_source_git.go b/cmd/tk/create_source_git.go index 694e0842..929f39f3 100644 --- a/cmd/tk/create_source_git.go +++ b/cmd/tk/create_source_git.go @@ -19,7 +19,7 @@ import ( "k8s.io/apimachinery/pkg/util/wait" "sigs.k8s.io/controller-runtime/pkg/client" - "github.com/fluxcd/toolkit/internal/ssh" + "github.com/fluxcd/toolkit/pkg/ssh" ) var createSourceGitCmd = &cobra.Command{ diff --git a/internal/ssh/host_scan.go b/pkg/ssh/host_scan.go similarity index 81% rename from internal/ssh/host_scan.go rename to pkg/ssh/host_scan.go index 85d6720c..61844608 100644 --- a/internal/ssh/host_scan.go +++ b/pkg/ssh/host_scan.go @@ -9,6 +9,10 @@ import ( "golang.org/x/crypto/ssh/knownhosts" ) +// ScanHostKey collects the given host's preferred public key for the +// algorithm of the given key pair. Any errors (e.g. authentication +// failures) are ignored, except if no key could be collected from the +// host. func ScanHostKey(host string, user string, pair *KeyPair) ([]byte, error) { signer, err := ssh.ParsePrivateKey(pair.PrivateKey) if err != nil { diff --git a/internal/ssh/key_pair.go b/pkg/ssh/key_pair.go similarity index 69% rename from internal/ssh/key_pair.go rename to pkg/ssh/key_pair.go index f251c18c..bad4bf2e 100644 --- a/internal/ssh/key_pair.go +++ b/pkg/ssh/key_pair.go @@ -11,6 +11,7 @@ import ( "golang.org/x/crypto/ssh" ) +// KeyPair holds the public and private key PEM block bytes. type KeyPair struct { PublicKey []byte PrivateKey []byte @@ -41,9 +42,13 @@ func (g *RSAGenerator) Generate() (*KeyPair, error) { if err != nil { return nil, err } + priv, err := encodePrivateKeyToPEM(pk) + if err != nil { + return nil, err + } return &KeyPair{ PublicKey: pub, - PrivateKey: encodePrivateKeyToPEM(pk), + PrivateKey: priv, }, nil } @@ -64,9 +69,13 @@ func (g *ECDSAGenerator) Generate() (*KeyPair, error) { if err != nil { return nil, err } + priv, err := encodePrivateKeyToPEM(pk) + if err != nil { + return nil, err + } return &KeyPair{ PublicKey: pub, - PrivateKey: encodePrivateKeyToPEM(pk), + PrivateKey: priv, }, nil } @@ -79,11 +88,17 @@ func generatePublicKey(pk interface{}) ([]byte, error) { return k, nil } -func encodePrivateKeyToPEM(pk interface{}) []byte { - b, _ := x509.MarshalPKCS8PrivateKey(pk) +// encodePrivateKeyToPEM encodes the given private key to a PEM block. +// The encoded format is PKCS#8 for universal support of the most +// common key types (rsa, ecdsa, ed25519). +func encodePrivateKeyToPEM(pk interface{}) ([]byte, error) { + b, err := x509.MarshalPKCS8PrivateKey(pk) + if err != nil { + return nil, err + } block := pem.Block{ Type: "PRIVATE KEY", Bytes: b, } - return pem.EncodeToMemory(&block) + return pem.EncodeToMemory(&block), nil } From 3a8151bcc0411db1c50ef8c8d21deb08d68a904a Mon Sep 17 00:00:00 2001 From: Hidde Beydals Date: Mon, 8 Jun 2020 20:05:25 +0200 Subject: [PATCH 3/7] Add various custom flags --- cmd/tk/create_source_git.go | 24 ++++----- cmd/tk/flags.go | 101 ++++++++++++++++++++++++++++++++++++ 2 files changed, 113 insertions(+), 12 deletions(-) create mode 100644 cmd/tk/flags.go diff --git a/cmd/tk/create_source_git.go b/cmd/tk/create_source_git.go index 929f39f3..8121a19f 100644 --- a/cmd/tk/create_source_git.go +++ b/cmd/tk/create_source_git.go @@ -2,12 +2,10 @@ package main import ( "context" - "crypto/elliptic" "fmt" "io/ioutil" "net/url" "os" - "strings" sourcev1 "github.com/fluxcd/source-controller/api/v1alpha1" "github.com/manifoldco/promptui" @@ -65,8 +63,9 @@ var ( sourceGitSemver string sourceGitUsername string sourceGitPassword string - sourceGitKeyAlgorithm string - sourceGitRSABits int + sourceGitKeyAlgorithm PublicKeyAlgorithm + sourceGitRSABits RSAKeyBits + sourceGitECDSACurve ECDSACurve ) func init() { @@ -76,8 +75,9 @@ func init() { createSourceGitCmd.Flags().StringVar(&sourceGitSemver, "tag-semver", "", "git tag semver range") createSourceGitCmd.Flags().StringVarP(&sourceGitUsername, "username", "u", "", "basic authentication username") createSourceGitCmd.Flags().StringVarP(&sourceGitPassword, "password", "p", "", "basic authentication password") - createSourceGitCmd.Flags().StringVarP(&sourceGitKeyAlgorithm, "ssh-algorithm", "", "rsa", "SSH public key algorithm") - createSourceGitCmd.Flags().IntVarP(&sourceGitRSABits, "ssh-rsa-bits", "", 2048, "SSH RSA public key bit size") + createSourceGitCmd.Flags().Var(&sourceGitKeyAlgorithm, "ssh-algorithm", "SSH public key algorithm") + createSourceGitCmd.Flags().Var(&sourceGitRSABits, "ssh-rsa-bits", "SSH RSA public key bit size") + createSourceGitCmd.Flags().Var(&sourceGitECDSACurve, "ssh-ecdsa-curve", "SSH ECDSA public key curve") createSourceCmd.AddCommand(createSourceGitCmd) } @@ -109,12 +109,11 @@ func createSourceGitCmdRun(cmd *cobra.Command, args []string) error { withAuth := false if u.Scheme == "ssh" { var keyGen ssh.KeyPairGenerator - switch strings.ToLower(sourceGitKeyAlgorithm) { + switch sourceGitKeyAlgorithm.String() { case "rsa": - keyGen = ssh.NewRSAGenerator(sourceGitRSABits) + keyGen = ssh.NewRSAGenerator(int(sourceGitRSABits)) case "ecdsa": - // TODO(hidde): make curve configurable by flag - keyGen = ssh.NewECDSAGenerator(elliptic.P521()) + keyGen = ssh.NewECDSAGenerator(sourceGitECDSACurve.Curve) } host := u.Host if u.Port() == "" { @@ -230,15 +229,16 @@ func generateSSH(ctx context.Context, generator ssh.KeyPairGenerator, name, host } logAction("collecting SSH server public key for generated public key algorithm") - serverKey, err := ssh.ScanHostKey(host, user, kp) + hostKey, err := ssh.ScanHostKey(host, user, kp) if err != nil { return err } logSuccess("collected public key from SSH server") + fmt.Printf("%s", hostKey) logAction("saving keys") files := fmt.Sprintf("--from-literal=identity=\"%s\" --from-literal=identity.pub=\"%s\" --from-literal=known_hosts=\"%s\"", - kp.PublicKey, kp.PrivateKey, serverKey) + kp.PublicKey, kp.PrivateKey, hostKey) secret := fmt.Sprintf("kubectl -n %s create secret generic %s %s --dry-run=client -oyaml | kubectl apply -f-", namespace, name, files) if _, err := utils.execCommand(ctx, ModeOS, secret); err != nil { diff --git a/cmd/tk/flags.go b/cmd/tk/flags.go new file mode 100644 index 00000000..f91e729c --- /dev/null +++ b/cmd/tk/flags.go @@ -0,0 +1,101 @@ +package main + +import ( + "crypto/elliptic" + "fmt" + "strconv" + "strings" +) + +var supportedPublicKeyAlgorithms = []string{"rsa", "ecdsa"} + +type PublicKeyAlgorithm string + +func (a *PublicKeyAlgorithm) String() string { + return string(*a) +} + +func (a *PublicKeyAlgorithm) Set(str string) error { + if strings.TrimSpace(str) == "" { + *a = PublicKeyAlgorithm(supportedPublicKeyAlgorithms[0]) + return nil + } + for _, v := range supportedPublicKeyAlgorithms { + if str == v { + *a = PublicKeyAlgorithm(str) + return nil + } + } + return fmt.Errorf( + "unsupported public key algorithm '%s', must be one of: %s", + str, + strings.Join(supportedPublicKeyAlgorithms, ", "), + ) +} + +func (a *PublicKeyAlgorithm) Type() string { + return "publicKeyAlgorithm" +} + +var defaultRSAKeyBits = 2048 + +type RSAKeyBits int + +func (b *RSAKeyBits) String() string { + return strconv.Itoa(int(*b)) +} + +func (b *RSAKeyBits) Set(str string) error { + if strings.TrimSpace(str) == "" { + *b = RSAKeyBits(defaultRSAKeyBits) + return nil + } + bits, err := strconv.Atoi(str) + if err != nil { + return err + } + if bits%8 != 0 { + return fmt.Errorf("RSA key bit size should be a multiples of 8") + } + *b = RSAKeyBits(bits) + return nil +} + +func (b *RSAKeyBits) Type() string { + return "rsaKeyBits" +} + +type ECDSACurve struct { + elliptic.Curve +} + +var supportedECDSACurves = map[string]elliptic.Curve{ + "P-256": elliptic.P256(), + "P-384": elliptic.P384(), + "P-521": elliptic.P521(), +} + +func (c *ECDSACurve) String() string { + if c == nil || c.Curve == nil { + return "" + } + return c.Curve.Params().Name +} + +func (c *ECDSACurve) Set(str string) error { + if strings.TrimSpace(str) == "" { + *c = ECDSACurve{supportedECDSACurves["P-384"]} + return nil + } + for k, v := range supportedECDSACurves { + if k == str { + *c = ECDSACurve{v} + return nil + } + } + return fmt.Errorf("unsupported curve '%s', should be one of: P-256, P-384, P-521", str) +} + +func (c *ECDSACurve) Type() string { + return "ecdsaCurve" +} From 60179461446e4ed7a1c7c764444cb7e6dfc1a579 Mon Sep 17 00:00:00 2001 From: Hidde Beydals Date: Tue, 9 Jun 2020 01:11:46 +0200 Subject: [PATCH 4/7] Improve host key scanner, add Ed25519 generator --- cmd/tk/create_source_git.go | 67 +++++++++++++++++++++++-------------- cmd/tk/flags.go | 55 ++++++++++++++++++------------ pkg/ssh/host_scan.go | 23 ++++++------- pkg/ssh/key_pair.go | 26 ++++++++++++++ 4 files changed, 111 insertions(+), 60 deletions(-) diff --git a/cmd/tk/create_source_git.go b/cmd/tk/create_source_git.go index 8121a19f..f017cac2 100644 --- a/cmd/tk/create_source_git.go +++ b/cmd/tk/create_source_git.go @@ -2,10 +2,12 @@ package main import ( "context" + "crypto/elliptic" "fmt" "io/ioutil" "net/url" "os" + "time" sourcev1 "github.com/fluxcd/source-controller/api/v1alpha1" "github.com/manifoldco/promptui" @@ -42,11 +44,19 @@ For private Git repositories, the basic authentication credentials are stored in --url=https://github.com/stefanprodan/podinfo \ --tag-semver=">=3.2.0 <3.3.0" - # Create a source from a Git repository using SSH authentication + # Create a source from a Git repository using SSH authentication create source git podinfo \ --url=ssh://git@github.com/stefanprodan/podinfo \ --branch=master + # Create a source from a Git repository using SSH authentication and an + # ECDSA P-521 curve public key + create source git podinfo \ + --url=ssh://git@github.com/stefanprodan/podinfo \ + --branch=master \ + --ssh-key-algorithm=ecdsa \ + --ssh-ecdsa-curve=p521 + # Create a source from a Git repository using basic authentication create source git podinfo \ --url=https://github.com/stefanprodan/podinfo \ @@ -63,9 +73,9 @@ var ( sourceGitSemver string sourceGitUsername string sourceGitPassword string - sourceGitKeyAlgorithm PublicKeyAlgorithm - sourceGitRSABits RSAKeyBits - sourceGitECDSACurve ECDSACurve + sourceGitKeyAlgorithm PublicKeyAlgorithm = "rsa" + sourceGitRSABits RSAKeyBits = 2048 + sourceGitECDSACurve = ECDSACurve{elliptic.P384()} ) func init() { @@ -75,9 +85,9 @@ func init() { createSourceGitCmd.Flags().StringVar(&sourceGitSemver, "tag-semver", "", "git tag semver range") createSourceGitCmd.Flags().StringVarP(&sourceGitUsername, "username", "u", "", "basic authentication username") createSourceGitCmd.Flags().StringVarP(&sourceGitPassword, "password", "p", "", "basic authentication password") - createSourceGitCmd.Flags().Var(&sourceGitKeyAlgorithm, "ssh-algorithm", "SSH public key algorithm") - createSourceGitCmd.Flags().Var(&sourceGitRSABits, "ssh-rsa-bits", "SSH RSA public key bit size") - createSourceGitCmd.Flags().Var(&sourceGitECDSACurve, "ssh-ecdsa-curve", "SSH ECDSA public key curve") + createSourceGitCmd.Flags().Var(&sourceGitKeyAlgorithm, "ssh-key-algorithm", sourceGitKeyAlgorithm.Description()) + createSourceGitCmd.Flags().Var(&sourceGitRSABits, "ssh-rsa-bits", sourceGitRSABits.Description()) + createSourceGitCmd.Flags().Var(&sourceGitECDSACurve, "ssh-ecdsa-curve", sourceGitECDSACurve.Description()) createSourceCmd.AddCommand(createSourceGitCmd) } @@ -108,18 +118,11 @@ func createSourceGitCmdRun(cmd *cobra.Command, args []string) error { withAuth := false if u.Scheme == "ssh" { - var keyGen ssh.KeyPairGenerator - switch sourceGitKeyAlgorithm.String() { - case "rsa": - keyGen = ssh.NewRSAGenerator(int(sourceGitRSABits)) - case "ecdsa": - keyGen = ssh.NewECDSAGenerator(sourceGitECDSACurve.Curve) - } host := u.Host if u.Port() == "" { host = host + ":22" } - if err := generateSSH(ctx, keyGen, name, host, tmpDir); err != nil { + if err := generateSSH(ctx, name, host); err != nil { return err } withAuth = true @@ -212,13 +215,14 @@ func generateBasicAuth(ctx context.Context, name string) error { return nil } -func generateSSH(ctx context.Context, generator ssh.KeyPairGenerator, name, host, user string) error { - logGenerate("generating deploy key") - kp, err := generator.Generate() +func generateSSH(ctx context.Context, name, host string) error { + gen := getKeyPairGenerator() + logGenerate("generating deploy key pair") + pair, err := gen.Generate() if err != nil { - return fmt.Errorf("SSH key pair generation failed: %w", err) + return fmt.Errorf("key pair generation failed: %w", err) } - fmt.Printf("%s", kp.PublicKey) + fmt.Printf("%s", pair.PublicKey) prompt := promptui.Prompt{ Label: "Have you added the deploy key to your repository", @@ -228,21 +232,21 @@ func generateSSH(ctx context.Context, generator ssh.KeyPairGenerator, name, host return fmt.Errorf("aborting") } - logAction("collecting SSH server public key for generated public key algorithm") - hostKey, err := ssh.ScanHostKey(host, user, kp) + logAction("collecting preferred public key from SSH server") + hostKey, err := ssh.ScanHostKey(host, 30*time.Second) if err != nil { return err } - logSuccess("collected public key from SSH server") + logSuccess("collected public key from SSH server:") fmt.Printf("%s", hostKey) logAction("saving keys") files := fmt.Sprintf("--from-literal=identity=\"%s\" --from-literal=identity.pub=\"%s\" --from-literal=known_hosts=\"%s\"", - kp.PublicKey, kp.PrivateKey, hostKey) + pair.PrivateKey, pair.PublicKey, hostKey) secret := fmt.Sprintf("kubectl -n %s create secret generic %s %s --dry-run=client -oyaml | kubectl apply -f-", namespace, name, files) if _, err := utils.execCommand(ctx, ModeOS, secret); err != nil { - return fmt.Errorf("create secret failed") + return fmt.Errorf("failed to create secret") } return nil } @@ -301,3 +305,16 @@ func isGitRepositoryReady(ctx context.Context, kubeClient client.Client, name, n return false, nil } } + +func getKeyPairGenerator() ssh.KeyPairGenerator { + var keyGen ssh.KeyPairGenerator + switch sourceGitKeyAlgorithm.String() { + case "rsa": + keyGen = ssh.NewRSAGenerator(int(sourceGitRSABits)) + case "ecdsa": + keyGen = ssh.NewECDSAGenerator(sourceGitECDSACurve.Curve) + case "ed25519": + keyGen = ssh.NewEd25519Generator() + } + return keyGen +} diff --git a/cmd/tk/flags.go b/cmd/tk/flags.go index f91e729c..2ae65b9e 100644 --- a/cmd/tk/flags.go +++ b/cmd/tk/flags.go @@ -7,7 +7,7 @@ import ( "strings" ) -var supportedPublicKeyAlgorithms = []string{"rsa", "ecdsa"} +var supportedPublicKeyAlgorithms = []string{"rsa", "ecdsa", "ed25519"} type PublicKeyAlgorithm string @@ -17,8 +17,8 @@ func (a *PublicKeyAlgorithm) String() string { func (a *PublicKeyAlgorithm) Set(str string) error { if strings.TrimSpace(str) == "" { - *a = PublicKeyAlgorithm(supportedPublicKeyAlgorithms[0]) - return nil + return fmt.Errorf("no public key algorithm given, must be one of: %s", + strings.Join(supportedPublicKeyAlgorithms, ", ")) } for _, v := range supportedPublicKeyAlgorithms { if str == v { @@ -26,17 +26,18 @@ func (a *PublicKeyAlgorithm) Set(str string) error { return nil } } - return fmt.Errorf( - "unsupported public key algorithm '%s', must be one of: %s", - str, - strings.Join(supportedPublicKeyAlgorithms, ", "), - ) + return fmt.Errorf("unsupported public key algorithm '%s', must be one of: %s", + str, strings.Join(supportedPublicKeyAlgorithms, ", ")) } func (a *PublicKeyAlgorithm) Type() string { return "publicKeyAlgorithm" } +func (a *PublicKeyAlgorithm) Description() string { + return fmt.Sprintf("SSH public key algorithm (%s)", strings.Join(supportedPublicKeyAlgorithms, ", ")) +} + var defaultRSAKeyBits = 2048 type RSAKeyBits int @@ -65,37 +66,47 @@ func (b *RSAKeyBits) Type() string { return "rsaKeyBits" } +func (b *RSAKeyBits) Description() string { + return "SSH RSA public key bit size (multiplies of 8)" +} + type ECDSACurve struct { elliptic.Curve } var supportedECDSACurves = map[string]elliptic.Curve{ - "P-256": elliptic.P256(), - "P-384": elliptic.P384(), - "P-521": elliptic.P521(), + "p256": elliptic.P256(), + "p384": elliptic.P384(), + "p521": elliptic.P521(), } func (c *ECDSACurve) String() string { - if c == nil || c.Curve == nil { + if c.Curve == nil { return "" } - return c.Curve.Params().Name + return strings.ToLower(strings.Replace(c.Curve.Params().Name, "-", "", 1)) } func (c *ECDSACurve) Set(str string) error { - if strings.TrimSpace(str) == "" { - *c = ECDSACurve{supportedECDSACurves["P-384"]} + if v, ok := supportedECDSACurves[str]; ok { + *c = ECDSACurve{v} return nil } - for k, v := range supportedECDSACurves { - if k == str { - *c = ECDSACurve{v} - return nil - } - } - return fmt.Errorf("unsupported curve '%s', should be one of: P-256, P-384, P-521", str) + return fmt.Errorf("unsupported curve '%s', should be one of: %s", str, strings.Join(ecdsaCurves(), ", ")) } func (c *ECDSACurve) Type() string { return "ecdsaCurve" } + +func (c *ECDSACurve) Description() string { + return fmt.Sprintf("SSH ECDSA public key curve (%s)", strings.Join(ecdsaCurves(), ", ")) +} + +func ecdsaCurves() []string { + keys := make([]string, 0, len(supportedECDSACurves)) + for k := range supportedECDSACurves { + keys = append(keys, k) + } + return keys +} diff --git a/pkg/ssh/host_scan.go b/pkg/ssh/host_scan.go index 61844608..a7a1a20d 100644 --- a/pkg/ssh/host_scan.go +++ b/pkg/ssh/host_scan.go @@ -4,27 +4,20 @@ import ( "encoding/base64" "fmt" "net" + "time" "golang.org/x/crypto/ssh" "golang.org/x/crypto/ssh/knownhosts" ) // ScanHostKey collects the given host's preferred public key for the -// algorithm of the given key pair. Any errors (e.g. authentication -// failures) are ignored, except if no key could be collected from the -// host. -func ScanHostKey(host string, user string, pair *KeyPair) ([]byte, error) { - signer, err := ssh.ParsePrivateKey(pair.PrivateKey) - if err != nil { - return nil, err - } +// Any errors (e.g. authentication failures) are ignored, except if +// no key could be collected from the host. +func ScanHostKey(host string, timeout time.Duration) ([]byte, error) { col := &collector{} config := &ssh.ClientConfig{ - User: user, - Auth: []ssh.AuthMethod{ - ssh.PublicKeys(signer), - }, - HostKeyCallback: col.StoreKey(), + HostKeyCallback: col.StoreKey(), + Timeout: timeout, } client, err := ssh.Dial("tcp", host, config) if err == nil { @@ -40,6 +33,10 @@ type collector struct { knownKeys []byte } +// StoreKey stores the public key in bytes as returned by the host. +// To collect multiple public key types from the host, multiple +// SSH dials need with the ClientConfig HostKeyAlgorithms set to +// the algorithm you want to collect. func (c *collector) StoreKey() ssh.HostKeyCallback { return func(hostname string, remote net.Addr, key ssh.PublicKey) error { c.knownKeys = append( diff --git a/pkg/ssh/key_pair.go b/pkg/ssh/key_pair.go index bad4bf2e..9f8fcd40 100644 --- a/pkg/ssh/key_pair.go +++ b/pkg/ssh/key_pair.go @@ -2,6 +2,7 @@ package ssh import ( "crypto/ecdsa" + "crypto/ed25519" "crypto/elliptic" "crypto/rand" "crypto/rsa" @@ -79,6 +80,31 @@ func (g *ECDSAGenerator) Generate() (*KeyPair, error) { }, nil } +type Ed25519Generator struct{} + +func NewEd25519Generator() KeyPairGenerator { + return &Ed25519Generator{} +} + +func (g *Ed25519Generator) Generate() (*KeyPair, error) { + pk, pv, err := ed25519.GenerateKey(rand.Reader) + if err != nil { + return nil, err + } + pub, err := generatePublicKey(pk) + if err != nil { + return nil, err + } + priv, err := encodePrivateKeyToPEM(pv) + if err != nil { + return nil, err + } + return &KeyPair{ + PublicKey: pub, + PrivateKey: priv, + }, nil +} + func generatePublicKey(pk interface{}) ([]byte, error) { b, err := ssh.NewPublicKey(pk) if err != nil { From 43876b5ab99947a55cfc87cf157f2323aeef0134 Mon Sep 17 00:00:00 2001 From: Hidde Beydals Date: Tue, 9 Jun 2020 11:16:56 +0200 Subject: [PATCH 5/7] Make SSH host key utilities publicly accessible --- pkg/ssh/{host_scan.go => host_key.go} | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) rename pkg/ssh/{host_scan.go => host_key.go} (71%) diff --git a/pkg/ssh/host_scan.go b/pkg/ssh/host_key.go similarity index 71% rename from pkg/ssh/host_scan.go rename to pkg/ssh/host_key.go index a7a1a20d..a02bcaab 100644 --- a/pkg/ssh/host_scan.go +++ b/pkg/ssh/host_key.go @@ -14,10 +14,10 @@ import ( // Any errors (e.g. authentication failures) are ignored, except if // no key could be collected from the host. func ScanHostKey(host string, timeout time.Duration) ([]byte, error) { - col := &collector{} + col := &HostKeyCollector{} config := &ssh.ClientConfig{ - HostKeyCallback: col.StoreKey(), - Timeout: timeout, + HostKeyCallback: col.StoreKey(), + Timeout: timeout, } client, err := ssh.Dial("tcp", host, config) if err == nil { @@ -29,7 +29,9 @@ func ScanHostKey(host string, timeout time.Duration) ([]byte, error) { return col.knownKeys, err } -type collector struct { +// HostKeyCollector offers a StoreKey method which provides an +// HostKeyCallBack to collect public keys from an SSH server. +type HostKeyCollector struct { knownKeys []byte } @@ -37,7 +39,7 @@ type collector struct { // To collect multiple public key types from the host, multiple // SSH dials need with the ClientConfig HostKeyAlgorithms set to // the algorithm you want to collect. -func (c *collector) StoreKey() ssh.HostKeyCallback { +func (c *HostKeyCollector) StoreKey() ssh.HostKeyCallback { return func(hostname string, remote net.Addr, key ssh.PublicKey) error { c.knownKeys = append( c.knownKeys, @@ -46,3 +48,8 @@ func (c *collector) StoreKey() ssh.HostKeyCallback { return nil } } + +// GetKnownKeys returns the collected public keys in bytes. +func (c *HostKeyCollector) GetKnownKeys() []byte { + return c.knownKeys +} From 9099ad3d8ba3a15c1d254f16e5f3439e859e7165 Mon Sep 17 00:00:00 2001 From: Hidde Beydals Date: Tue, 9 Jun 2020 12:25:33 +0200 Subject: [PATCH 6/7] Split cmd SSH/auth functions --- cmd/tk/create_source_git.go | 145 +++++++++++++++++++++++------------- 1 file changed, 92 insertions(+), 53 deletions(-) diff --git a/cmd/tk/create_source_git.go b/cmd/tk/create_source_git.go index f017cac2..9f112af9 100644 --- a/cmd/tk/create_source_git.go +++ b/cmd/tk/create_source_git.go @@ -116,18 +116,70 @@ func createSourceGitCmdRun(cmd *cobra.Command, args []string) error { ctx, cancel := context.WithTimeout(context.Background(), timeout) defer cancel() + kubeClient, err := utils.kubeClient(kubeconfig) + if err != nil { + return err + } + withAuth := false + // TODO(hidde): move all auth prep to separate func? if u.Scheme == "ssh" { + logAction("generating deploy key pair") + pair, err := generateKeyPair(ctx) + if err != nil { + return err + } + + fmt.Printf("%s", pair.PublicKey) + prompt := promptui.Prompt{ + Label: "Have you added the deploy key to your repository", + IsConfirm: true, + } + if _, err := prompt.Run(); err != nil { + return fmt.Errorf("aborting") + } + + logAction("collecting preferred public key from SSH server") host := u.Host if u.Port() == "" { host = host + ":22" } - if err := generateSSH(ctx, name, host); err != nil { + hostKey, err := scanHostKey(ctx, host) + if err != nil { + return err + } + logSuccess("collected public key from SSH server:") + fmt.Printf("%s", hostKey) + + logAction("applying secret with keys") + secret := corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: name, + Namespace: namespace, + }, + StringData: map[string]string{ + "identity": string(pair.PrivateKey), + "identity.pub": string(pair.PublicKey), + "known_hosts": string(hostKey), + }, + } + if err := upsertSecret(ctx, kubeClient, secret); err != nil { return err } withAuth = true } else if sourceGitUsername != "" && sourceGitPassword != "" { - if err := generateBasicAuth(ctx, name); err != nil { + logAction("applying secret with basic auth credentials") + secret := corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: name, + Namespace: namespace, + }, + StringData: map[string]string{ + "username": sourceGitUsername, + "password": sourceGitPassword, + }, + } + if err := upsertSecret(ctx, kubeClient, secret); err != nil { return err } withAuth = true @@ -167,11 +219,6 @@ func createSourceGitCmdRun(cmd *cobra.Command, args []string) error { gitRepository.Spec.Reference.Branch = sourceGitBranch } - kubeClient, err := utils.kubeClient(kubeconfig) - if err != nil { - return err - } - logAction("applying source") if err := upsertGitRepository(ctx, kubeClient, gitRepository); err != nil { return err @@ -203,50 +250,55 @@ func createSourceGitCmdRun(cmd *cobra.Command, args []string) error { return nil } -func generateBasicAuth(ctx context.Context, name string) error { - logAction("saving credentials") - credentials := fmt.Sprintf("--from-literal=username='%s' --from-literal=password='%s'", - sourceGitUsername, sourceGitPassword) - secret := fmt.Sprintf("kubectl -n %s create secret generic %s %s --dry-run=client -oyaml | kubectl apply -f-", - namespace, name, credentials) - if _, err := utils.execCommand(ctx, ModeOS, secret); err != nil { - return fmt.Errorf("kubectl create secret failed") +func generateKeyPair(ctx context.Context) (*ssh.KeyPair, error) { + var keyGen ssh.KeyPairGenerator + switch sourceGitKeyAlgorithm.String() { + case "rsa": + keyGen = ssh.NewRSAGenerator(int(sourceGitRSABits)) + case "ecdsa": + keyGen = ssh.NewECDSAGenerator(sourceGitECDSACurve.Curve) + case "ed25519": + keyGen = ssh.NewEd25519Generator() + default: + return nil, fmt.Errorf("unsupported public key algorithm '%s'", sourceGitKeyAlgorithm.String()) } - return nil + pair, err := keyGen.Generate() + if err != nil { + return nil, fmt.Errorf("key pair generation failed: %w", err) + } + return pair, nil } -func generateSSH(ctx context.Context, name, host string) error { - gen := getKeyPairGenerator() - logGenerate("generating deploy key pair") - pair, err := gen.Generate() +func scanHostKey(ctx context.Context, host string) ([]byte, error) { + hostKey, err := ssh.ScanHostKey(host, 30*time.Second) if err != nil { - return fmt.Errorf("key pair generation failed: %w", err) + return nil, fmt.Errorf("SSH key scan for host '%s' failed: %w", host, err) } - fmt.Printf("%s", pair.PublicKey) + return hostKey, nil +} - prompt := promptui.Prompt{ - Label: "Have you added the deploy key to your repository", - IsConfirm: true, - } - if _, err := prompt.Run(); err != nil { - return fmt.Errorf("aborting") +func upsertSecret(ctx context.Context, kubeClient client.Client, secret corev1.Secret) error { + namespacedName := types.NamespacedName{ + Namespace: secret.GetNamespace(), + Name: secret.GetName(), } - logAction("collecting preferred public key from SSH server") - hostKey, err := ssh.ScanHostKey(host, 30*time.Second) + var existing corev1.Secret + err := kubeClient.Get(ctx, namespacedName, &existing) if err != nil { + if errors.IsNotFound(err) { + if err := kubeClient.Create(ctx, &existing); err != nil { + return err + } else { + return nil + } + } return err } - logSuccess("collected public key from SSH server:") - fmt.Printf("%s", hostKey) - - logAction("saving keys") - files := fmt.Sprintf("--from-literal=identity=\"%s\" --from-literal=identity.pub=\"%s\" --from-literal=known_hosts=\"%s\"", - pair.PrivateKey, pair.PublicKey, hostKey) - secret := fmt.Sprintf("kubectl -n %s create secret generic %s %s --dry-run=client -oyaml | kubectl apply -f-", - namespace, name, files) - if _, err := utils.execCommand(ctx, ModeOS, secret); err != nil { - return fmt.Errorf("failed to create secret") + + existing.StringData = secret.StringData + if err := kubeClient.Update(ctx, &existing); err != nil { + return err } return nil } @@ -305,16 +357,3 @@ func isGitRepositoryReady(ctx context.Context, kubeClient client.Client, name, n return false, nil } } - -func getKeyPairGenerator() ssh.KeyPairGenerator { - var keyGen ssh.KeyPairGenerator - switch sourceGitKeyAlgorithm.String() { - case "rsa": - keyGen = ssh.NewRSAGenerator(int(sourceGitRSABits)) - case "ecdsa": - keyGen = ssh.NewECDSAGenerator(sourceGitECDSACurve.Curve) - case "ed25519": - keyGen = ssh.NewEd25519Generator() - } - return keyGen -} From 65cfa6d3478e19bd7020f6c1ecd31369d8c0d052 Mon Sep 17 00:00:00 2001 From: Hidde Beydals Date: Tue, 9 Jun 2020 12:42:37 +0200 Subject: [PATCH 7/7] Move port check to scanHostKey func --- cmd/tk/create_source_git.go | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/cmd/tk/create_source_git.go b/cmd/tk/create_source_git.go index 9f112af9..97f4934f 100644 --- a/cmd/tk/create_source_git.go +++ b/cmd/tk/create_source_git.go @@ -140,11 +140,7 @@ func createSourceGitCmdRun(cmd *cobra.Command, args []string) error { } logAction("collecting preferred public key from SSH server") - host := u.Host - if u.Port() == "" { - host = host + ":22" - } - hostKey, err := scanHostKey(ctx, host) + hostKey, err := scanHostKey(ctx, u) if err != nil { return err } @@ -269,7 +265,11 @@ func generateKeyPair(ctx context.Context) (*ssh.KeyPair, error) { return pair, nil } -func scanHostKey(ctx context.Context, host string) ([]byte, error) { +func scanHostKey(ctx context.Context, url *url.URL) ([]byte, error) { + host := url.Host + if url.Port() == "" { + host = host + ":22" + } hostKey, err := ssh.ScanHostKey(host, 30*time.Second) if err != nil { return nil, fmt.Errorf("SSH key scan for host '%s' failed: %w", host, err)