Replace SSH shell-outs with Go implementation

pull/32/head
Hidde Beydals 5 years ago
parent 25f6291403
commit a332e12338

@ -45,9 +45,6 @@ func runCheckCmd(cmd *cobra.Command, args []string) error {
logAction("checking prerequisites") logAction("checking prerequisites")
checkFailed := false checkFailed := false
if !sshCheck() {
checkFailed = true
}
if !kubectlCheck(ctx, ">=1.18.0") { if !kubectlCheck(ctx, ">=1.18.0") {
checkFailed = true checkFailed = true
@ -76,21 +73,6 @@ func runCheckCmd(cmd *cobra.Command, args []string) error {
return nil 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 { func kubectlCheck(ctx context.Context, version string) bool {
_, err := exec.LookPath("kubectl") _, err := exec.LookPath("kubectl")
if err != nil { if err != nil {

@ -2,20 +2,24 @@ package main
import ( import (
"context" "context"
"crypto/elliptic"
"fmt" "fmt"
"io/ioutil"
"net/url"
"os"
"strings"
sourcev1 "github.com/fluxcd/source-controller/api/v1alpha1" sourcev1 "github.com/fluxcd/source-controller/api/v1alpha1"
"github.com/manifoldco/promptui" "github.com/manifoldco/promptui"
"github.com/spf13/cobra" "github.com/spf13/cobra"
"io/ioutil"
corev1 "k8s.io/api/core/v1" corev1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/api/errors" "k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/types" "k8s.io/apimachinery/pkg/types"
"k8s.io/apimachinery/pkg/util/wait" "k8s.io/apimachinery/pkg/util/wait"
"net/url"
"os"
"sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/client"
"strings"
"github.com/fluxcd/toolkit/internal/ssh"
) )
var createSourceGitCmd = &cobra.Command{ var createSourceGitCmd = &cobra.Command{
@ -55,12 +59,14 @@ For private Git repositories, the basic authentication credentials are stored in
} }
var ( var (
sourceGitURL string sourceGitURL string
sourceGitBranch string sourceGitBranch string
sourceGitTag string sourceGitTag string
sourceGitSemver string sourceGitSemver string
sourceGitUsername string sourceGitUsername string
sourceGitPassword string sourceGitPassword string
sourceGitKeyAlgorithm string
sourceGitRSABits int
) )
func init() { func init() {
@ -70,6 +76,8 @@ func init() {
createSourceGitCmd.Flags().StringVar(&sourceGitSemver, "tag-semver", "", "git tag semver range") createSourceGitCmd.Flags().StringVar(&sourceGitSemver, "tag-semver", "", "git tag semver range")
createSourceGitCmd.Flags().StringVarP(&sourceGitUsername, "username", "u", "", "basic authentication username") createSourceGitCmd.Flags().StringVarP(&sourceGitUsername, "username", "u", "", "basic authentication username")
createSourceGitCmd.Flags().StringVarP(&sourceGitPassword, "password", "p", "", "basic authentication password") 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) createSourceCmd.AddCommand(createSourceGitCmd)
} }
@ -99,8 +107,20 @@ func createSourceGitCmdRun(cmd *cobra.Command, args []string) error {
defer cancel() defer cancel()
withAuth := false withAuth := false
if strings.HasPrefix(sourceGitURL, "ssh") { if u.Scheme == "ssh" {
if err := generateSSH(ctx, name, u.Host, tmpDir); err != nil { 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 return err
} }
withAuth = true withAuth = true
@ -193,27 +213,13 @@ func generateBasicAuth(ctx context.Context, name string) error {
return nil return nil
} }
func generateSSH(ctx context.Context, name, host, tmpDir string) error { func generateSSH(ctx context.Context, generator ssh.KeyPairGenerator, name, host, user 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")
}
logGenerate("generating deploy key") logGenerate("generating deploy key")
kp, err := generator.Generate()
command = fmt.Sprintf("ssh-keygen -b 2048 -t rsa -f %s/identity -q -N \"\"", tmpDir) if err != nil {
if _, err := utils.execCommand(ctx, ModeStderrOS, command); err != nil { return fmt.Errorf("SSH key pair generation failed: %w", err)
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)
} }
fmt.Printf("%s", kp.PublicKey)
prompt := promptui.Prompt{ prompt := promptui.Prompt{
Label: "Have you added the deploy key to your repository", 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") 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") logAction("saving keys")
files := fmt.Sprintf("--from-file=%s/identity --from-file=%s/identity.pub --from-file=%s/known_hosts", files := fmt.Sprintf("--from-literal=identity=\"%s\" --from-literal=identity.pub=\"%s\" --from-literal=known_hosts=\"%s\"",
tmpDir, tmpDir, tmpDir) kp.PublicKey, kp.PrivateKey, serverKey)
secret := fmt.Sprintf("kubectl -n %s create secret generic %s %s --dry-run=client -oyaml | kubectl apply -f-", secret := fmt.Sprintf("kubectl -n %s create secret generic %s %s --dry-run=client -oyaml | kubectl apply -f-",
namespace, name, files) namespace, name, files)
if _, err := utils.execCommand(ctx, ModeOS, secret); err != nil { if _, err := utils.execCommand(ctx, ModeOS, secret); err != nil {

@ -8,6 +8,7 @@ require (
github.com/fluxcd/source-controller v0.0.1-beta.1 github.com/fluxcd/source-controller v0.0.1-beta.1
github.com/manifoldco/promptui v0.7.0 github.com/manifoldco/promptui v0.7.0
github.com/spf13/cobra v1.0.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/api v0.18.2
k8s.io/apimachinery v0.18.2 k8s.io/apimachinery v0.18.2
k8s.io/client-go v0.18.2 k8s.io/client-go v0.18.2

@ -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
}
}

@ -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)
}
Loading…
Cancel
Save