Merge pull request #32 from fluxcd/go-native-ssh

pull/33/head
Hidde Beydals 5 years ago committed by GitHub
commit 9e31bbe716
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -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"
"time"
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/pkg/ssh"
) )
var createSourceGitCmd = &cobra.Command{ var createSourceGitCmd = &cobra.Command{
@ -40,11 +44,19 @@ For private Git repositories, the basic authentication credentials are stored in
--url=https://github.com/stefanprodan/podinfo \ --url=https://github.com/stefanprodan/podinfo \
--tag-semver=">=3.2.0 <3.3.0" --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 \ create source git podinfo \
--url=ssh://git@github.com/stefanprodan/podinfo \ --url=ssh://git@github.com/stefanprodan/podinfo \
--branch=master --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 a source from a Git repository using basic authentication
create source git podinfo \ create source git podinfo \
--url=https://github.com/stefanprodan/podinfo \ --url=https://github.com/stefanprodan/podinfo \
@ -55,12 +67,15 @@ 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 PublicKeyAlgorithm = "rsa"
sourceGitRSABits RSAKeyBits = 2048
sourceGitECDSACurve = ECDSACurve{elliptic.P384()}
) )
func init() { func init() {
@ -70,6 +85,9 @@ 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().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) createSourceCmd.AddCommand(createSourceGitCmd)
} }
@ -98,14 +116,66 @@ func createSourceGitCmdRun(cmd *cobra.Command, args []string) error {
ctx, cancel := context.WithTimeout(context.Background(), timeout) ctx, cancel := context.WithTimeout(context.Background(), timeout)
defer cancel() defer cancel()
kubeClient, err := utils.kubeClient(kubeconfig)
if err != nil {
return err
}
withAuth := false withAuth := false
if strings.HasPrefix(sourceGitURL, "ssh") { // TODO(hidde): move all auth prep to separate func?
if err := generateSSH(ctx, name, u.Host, tmpDir); err != nil { 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")
hostKey, err := scanHostKey(ctx, u)
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 return err
} }
withAuth = true withAuth = true
} else if sourceGitUsername != "" && sourceGitPassword != "" { } 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 return err
} }
withAuth = true withAuth = true
@ -145,11 +215,6 @@ func createSourceGitCmdRun(cmd *cobra.Command, args []string) error {
gitRepository.Spec.Reference.Branch = sourceGitBranch gitRepository.Spec.Reference.Branch = sourceGitBranch
} }
kubeClient, err := utils.kubeClient(kubeconfig)
if err != nil {
return err
}
logAction("applying source") logAction("applying source")
if err := upsertGitRepository(ctx, kubeClient, gitRepository); err != nil { if err := upsertGitRepository(ctx, kubeClient, gitRepository); err != nil {
return err return err
@ -181,55 +246,59 @@ func createSourceGitCmdRun(cmd *cobra.Command, args []string) error {
return nil return nil
} }
func generateBasicAuth(ctx context.Context, name string) error { func generateKeyPair(ctx context.Context) (*ssh.KeyPair, error) {
logAction("saving credentials") var keyGen ssh.KeyPairGenerator
credentials := fmt.Sprintf("--from-literal=username='%s' --from-literal=password='%s'", switch sourceGitKeyAlgorithm.String() {
sourceGitUsername, sourceGitPassword) case "rsa":
secret := fmt.Sprintf("kubectl -n %s create secret generic %s %s --dry-run=client -oyaml | kubectl apply -f-", keyGen = ssh.NewRSAGenerator(int(sourceGitRSABits))
namespace, name, credentials) case "ecdsa":
if _, err := utils.execCommand(ctx, ModeOS, secret); err != nil { keyGen = ssh.NewECDSAGenerator(sourceGitECDSACurve.Curve)
return fmt.Errorf("kubectl create secret failed") 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, tmpDir string) error { func scanHostKey(ctx context.Context, url *url.URL) ([]byte, error) {
logGenerate("generating host key for %s", host) host := url.Host
if url.Port() == "" {
command := fmt.Sprintf("ssh-keyscan %s > %s/known_hosts", host, tmpDir) host = host + ":22"
if _, err := utils.execCommand(ctx, ModeStderrOS, command); err != nil {
return fmt.Errorf("ssh-keyscan failed")
} }
hostKey, err := ssh.ScanHostKey(host, 30*time.Second)
logGenerate("generating deploy key") if err != nil {
return nil, fmt.Errorf("SSH key scan for host '%s' failed: %w", host, err)
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")
} }
return hostKey, nil
}
command = fmt.Sprintf("cat %s/identity.pub", tmpDir) func upsertSecret(ctx context.Context, kubeClient client.Client, secret corev1.Secret) error {
if deployKey, err := utils.execCommand(ctx, ModeCapture, command); err != nil { namespacedName := types.NamespacedName{
return fmt.Errorf("unable to read identity.pub: %w", err) Namespace: secret.GetNamespace(),
} else { Name: secret.GetName(),
fmt.Print(deployKey)
} }
prompt := promptui.Prompt{ var existing corev1.Secret
Label: "Have you added the deploy key to your repository", err := kubeClient.Get(ctx, namespacedName, &existing)
IsConfirm: true, if err != nil {
} if errors.IsNotFound(err) {
if _, err := prompt.Run(); err != nil { if err := kubeClient.Create(ctx, &existing); err != nil {
return fmt.Errorf("aborting") return err
} else {
return nil
}
}
return err
} }
logAction("saving keys") existing.StringData = secret.StringData
files := fmt.Sprintf("--from-file=%s/identity --from-file=%s/identity.pub --from-file=%s/known_hosts", if err := kubeClient.Update(ctx, &existing); err != nil {
tmpDir, tmpDir, tmpDir) return err
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 nil return nil
} }

@ -0,0 +1,112 @@
package main
import (
"crypto/elliptic"
"fmt"
"strconv"
"strings"
)
var supportedPublicKeyAlgorithms = []string{"rsa", "ecdsa", "ed25519"}
type PublicKeyAlgorithm string
func (a *PublicKeyAlgorithm) String() string {
return string(*a)
}
func (a *PublicKeyAlgorithm) Set(str string) error {
if strings.TrimSpace(str) == "" {
return fmt.Errorf("no public key algorithm given, must be one of: %s",
strings.Join(supportedPublicKeyAlgorithms, ", "))
}
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"
}
func (a *PublicKeyAlgorithm) Description() string {
return fmt.Sprintf("SSH public key algorithm (%s)", strings.Join(supportedPublicKeyAlgorithms, ", "))
}
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"
}
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{
"p256": elliptic.P256(),
"p384": elliptic.P384(),
"p521": elliptic.P521(),
}
func (c *ECDSACurve) String() string {
if c.Curve == nil {
return ""
}
return strings.ToLower(strings.Replace(c.Curve.Params().Name, "-", "", 1))
}
func (c *ECDSACurve) Set(str string) error {
if v, ok := supportedECDSACurves[str]; ok {
*c = ECDSACurve{v}
return nil
}
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
}

@ -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,55 @@
package ssh
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
// 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 := &HostKeyCollector{}
config := &ssh.ClientConfig{
HostKeyCallback: col.StoreKey(),
Timeout: timeout,
}
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
}
// HostKeyCollector offers a StoreKey method which provides an
// HostKeyCallBack to collect public keys from an SSH server.
type HostKeyCollector 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 *HostKeyCollector) 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
}
}
// GetKnownKeys returns the collected public keys in bytes.
func (c *HostKeyCollector) GetKnownKeys() []byte {
return c.knownKeys
}

@ -0,0 +1,130 @@
package ssh
import (
"crypto/ecdsa"
"crypto/ed25519"
"crypto/elliptic"
"crypto/rand"
"crypto/rsa"
"crypto/x509"
"encoding/pem"
"golang.org/x/crypto/ssh"
)
// KeyPair holds the public and private key PEM block bytes.
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
}
priv, err := encodePrivateKeyToPEM(pk)
if err != nil {
return nil, err
}
return &KeyPair{
PublicKey: pub,
PrivateKey: priv,
}, 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
}
priv, err := encodePrivateKeyToPEM(pk)
if err != nil {
return nil, err
}
return &KeyPair{
PublicKey: pub,
PrivateKey: priv,
}, 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 {
return nil, err
}
k := ssh.MarshalAuthorizedKey(b)
return k, nil
}
// 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), nil
}
Loading…
Cancel
Save