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")
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 {

@ -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{
@ -61,6 +65,8 @@ var (
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 {

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

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