diff --git a/cmd/flux/bootstrap_git.go b/cmd/flux/bootstrap_git.go index f906dae2..536fa7de 100644 --- a/cmd/flux/bootstrap_git.go +++ b/cmd/flux/bootstrap_git.go @@ -56,6 +56,9 @@ command will perform an upgrade if needed.`, # Run bootstrap for a Git repository with a passwordless private key flux bootstrap git --url=ssh://git@example.com/repository.git --private-key-file= + + # Run bootstrap for a Git repository with a private key and password + flux bootstrap git --url=ssh://git@example.com/repository.git --private-key-file= --password= `, RunE: bootstrapGitCmdRun, } @@ -163,6 +166,7 @@ func bootstrapGitCmdRun(cmd *cobra.Command, args []string) error { repositoryURL.Host = repositoryURL.Hostname() } else { secretOpts.PrivateKeyAlgorithm = sourcesecret.PrivateKeyAlgorithm(bootstrapArgs.keyAlgorithm) + secretOpts.Password = gitArgs.password secretOpts.RSAKeyBits = int(bootstrapArgs.keyRSABits) secretOpts.ECDSACurve = bootstrapArgs.keyECDSACurve.Curve @@ -229,7 +233,7 @@ func transportForURL(u *url.URL) (transport.AuthMethod, error) { }, nil case "ssh": if bootstrapArgs.privateKeyFile != "" { - return ssh.NewPublicKeysFromFile(u.User.Username(), bootstrapArgs.privateKeyFile, "") + return ssh.NewPublicKeysFromFile(u.User.Username(), bootstrapArgs.privateKeyFile, gitArgs.password) } return nil, nil default: diff --git a/cmd/flux/create_source_git.go b/cmd/flux/create_source_git.go index b864132f..b3090737 100644 --- a/cmd/flux/create_source_git.go +++ b/cmd/flux/create_source_git.go @@ -236,6 +236,7 @@ func createSourceGitCmdRun(cmd *cobra.Command, args []string) error { secretOpts.PrivateKeyAlgorithm = sourcesecret.PrivateKeyAlgorithm(sourceGitArgs.keyAlgorithm) secretOpts.RSAKeyBits = int(sourceGitArgs.keyRSABits) secretOpts.ECDSACurve = sourceGitArgs.keyECDSACurve.Curve + secretOpts.Password = sourceGitArgs.password case "https": secretOpts.Username = sourceGitArgs.username secretOpts.Password = sourceGitArgs.password diff --git a/docs/cmd/flux_bootstrap_git.md b/docs/cmd/flux_bootstrap_git.md index 0ba7ebf9..70e8628d 100644 --- a/docs/cmd/flux_bootstrap_git.md +++ b/docs/cmd/flux_bootstrap_git.md @@ -28,6 +28,9 @@ flux bootstrap git [flags] # Run bootstrap for a Git repository with a passwordless private key flux bootstrap git --url=ssh://git@example.com/repository.git --private-key-file= + # Run bootstrap for a Git repository with a private key and password + flux bootstrap git --url=ssh://git@example.com/repository.git --private-key-file= --password= + ``` ### Options diff --git a/pkg/manifestgen/sourcesecret/sourcesecret.go b/pkg/manifestgen/sourcesecret/sourcesecret.go index 19342319..8d7fb865 100644 --- a/pkg/manifestgen/sourcesecret/sourcesecret.go +++ b/pkg/manifestgen/sourcesecret/sourcesecret.go @@ -41,10 +41,10 @@ func Generate(options Options) (*manifestgen.Manifest, error) { var keypair *ssh.KeyPair switch { - case options.Username != "", options.Password != "": + case options.Username != "" && options.Password != "": // noop case len(options.PrivateKeyPath) > 0: - if keypair, err = loadKeyPair(options.PrivateKeyPath); err != nil { + if keypair, err = loadKeyPair(options.PrivateKeyPath, options.Password); err != nil { return nil, err } case len(options.PrivateKeyAlgorithm) > 0: @@ -101,7 +101,7 @@ func buildSecret(keypair *ssh.KeyPair, hostKey, caFile, certFile, keyFile []byte secret.Labels = options.Labels secret.StringData = map[string]string{} - if options.Username != "" || options.Password != "" { + if options.Username != "" && options.Password != "" { secret.StringData[UsernameSecretKey] = options.Username secret.StringData[PasswordSecretKey] = options.Password } @@ -119,18 +119,28 @@ func buildSecret(keypair *ssh.KeyPair, hostKey, caFile, certFile, keyFile []byte secret.StringData[PrivateKeySecretKey] = string(keypair.PrivateKey) secret.StringData[PublicKeySecretKey] = string(keypair.PublicKey) secret.StringData[KnownHostsSecretKey] = string(hostKey) + // set password if present + if options.Password != "" { + secret.StringData[PasswordSecretKey] = string(options.Password) + } } return } -func loadKeyPair(path string) (*ssh.KeyPair, error) { +func loadKeyPair(path string, password string) (*ssh.KeyPair, error) { b, err := ioutil.ReadFile(path) if err != nil { return nil, fmt.Errorf("failed to open private key file: %w", err) } - ppk, err := cryptssh.ParsePrivateKey(b) + var ppk cryptssh.Signer + if password != "" { + ppk, err = cryptssh.ParsePrivateKeyWithPassphrase(b, []byte(password)) + } else { + ppk, err = cryptssh.ParsePrivateKey(b) + } + if err != nil { return nil, err } diff --git a/pkg/manifestgen/sourcesecret/sourcesecret_test.go b/pkg/manifestgen/sourcesecret/sourcesecret_test.go index 89402425..618b86cf 100644 --- a/pkg/manifestgen/sourcesecret/sourcesecret_test.go +++ b/pkg/manifestgen/sourcesecret/sourcesecret_test.go @@ -18,23 +18,82 @@ package sourcesecret import ( "io/ioutil" + "os" "reflect" "testing" -) -func Test_loadKeyPair(t *testing.T) { - pk, _ := ioutil.ReadFile("testdata/rsa") - ppk, _ := ioutil.ReadFile("testdata/rsa.pub") + "golang.org/x/crypto/ssh" + "golang.org/x/crypto/ssh/testdata" +) - got, err := loadKeyPair("testdata/rsa") - if err != nil { - t.Errorf("loadKeyPair() error = %v", err) - return +func Test_passwordLoadKeyPair(t *testing.T) { + tests := []struct { + name string + privateKeyPath string + publicKeyPath string + password string + }{ + { + name: "private key pair with password", + privateKeyPath: "testdata/password_rsa", + publicKeyPath: "testdata/password_rsa.pub", + password: "password", + }, } - if !reflect.DeepEqual(got.PrivateKey, pk) { - t.Errorf("PrivateKey %s != %s", got.PrivateKey, pk) + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + pk, _ := ioutil.ReadFile(tt.privateKeyPath) + ppk, _ := ioutil.ReadFile(tt.publicKeyPath) + + got, err := loadKeyPair(tt.privateKeyPath, tt.password) + if err != nil { + t.Errorf("loadKeyPair() error = %v", err) + return + } + + if !reflect.DeepEqual(got.PrivateKey, pk) { + t.Errorf("PrivateKey %s != %s", got.PrivateKey, pk) + } + if !reflect.DeepEqual(got.PublicKey, ppk) { + t.Errorf("PublicKey %s != %s", got.PublicKey, ppk) + } + }) } - if !reflect.DeepEqual(got.PublicKey, ppk) { - t.Errorf("PublicKey %s != %s", got.PublicKey, ppk) +} + +func Test_PasswordlessLoadKeyPair(t *testing.T) { + for algo, privateKey := range testdata.PEMBytes { + t.Run(algo, func(t *testing.T) { + f, err := ioutil.TempFile("", "test-private-key-") + if err != nil { + t.Fatalf("unable to create temporary file. err: %s", err) + } + defer os.Remove(f.Name()) + + if _, err = f.Write(privateKey); err != nil { + t.Fatalf("unable to write private key to file. err: %s", err) + } + + got, err := loadKeyPair(f.Name(), "") + if err != nil { + t.Errorf("loadKeyPair() error = %v", err) + return + } + + pk, _ := ioutil.ReadFile(f.Name()) + if !reflect.DeepEqual(got.PrivateKey, pk) { + t.Errorf("PrivateKey %s != %s", got.PrivateKey, string(privateKey)) + } + + signer, err := ssh.ParsePrivateKey(privateKey) + if err != nil { + t.Errorf("unexpected error: %s", err) + } + + if !reflect.DeepEqual(got.PublicKey, ssh.MarshalAuthorizedKey(signer.PublicKey())) { + t.Errorf("PublicKey %s != %s", got.PublicKey, ssh.MarshalAuthorizedKey(signer.PublicKey())) + } + }) } } diff --git a/pkg/manifestgen/sourcesecret/testdata/password_rsa b/pkg/manifestgen/sourcesecret/testdata/password_rsa new file mode 100644 index 00000000..dd2ef91c --- /dev/null +++ b/pkg/manifestgen/sourcesecret/testdata/password_rsa @@ -0,0 +1,17 @@ +-----BEGIN OPENSSH PRIVATE KEY----- +b3BlbnNzaC1rZXktdjEAAAAACmFlczI1Ni1jdHIAAAAGYmNyeXB0AAAAGAAAABB18lOcbN +Q7pk768hyQUymCAAAAEAAAAAEAAACXAAAAB3NzaC1yc2EAAAADAQABAAAAgQCi/p5KtpRl +tu3yxEsc+NtR9kRBuYhsh55609dNiTonLlL7K00pLCEWrtEIVM7FzD8IA6Q1NMNBFijje3 +6gnqMlo6M/cJObkGfZFC025diLI5/ND5R/l60XLtRZfh92K9nCsQKsJaW+R8LTtBdhil/q +M/+u07sFHzzk6/36FStg/QAAAiAQAjBJ6TYZuu4iHrrQgbhmlQem18Xjm8f2M9M0BrYgJV +F5OVL7mL/bsFTp0IA92HXnxGs0gF4ue5ujCE7SWOyr4SpJfgijExnmDPkZ9nhCG06MsCQ9 +uU7tTtEnrYcZ5/mmEe8E/O74Mo8xBqI8Unv95eF4p/tAMcDctVX39/lSjP1UZYL6vrLk2L +SoEWK2DmZ2ZYdtBJ20AGDJ7MRIX3X/+qZkuYcy7GfPTuDKPIyrr7UEIYH//x8RaGuvOUuu +P5bXGOBCTayZgGeWwNDePVITxMhpqTYjy7hqJ1ppBEv1svvbax5ksbwTRzU6quN75o+tbX +hf5v0HbiRl3w6LtuwciiQtGsgijxt1noViZvpMLam5EJ3+eTnKnfEPxBMaCx7qepqPT9yI +GJ/myyB/+FMkVe9epBeO2wyBTPPzr9O8co8SbcFkFEpcmxKk8toPf4F4XGs7lnibsDXGWE +s2+WPmf17WLgpTMLutSVFIxw/V6ajnVrTTTE9AOwC9TYBx152YTWGILUlMMQ4AdNri8H80 +k/RMv18Tut6k4v6I1aKfUSfXRaaxwagt6zMJ2TvYJFsskfTboM/FjzcDvl4jWQvfcoKJJa +da6L6TwnfL03qI+tdT84R4a9oYgZ27WB0ekPnmDpWgIl5jN/F2mHz2/KpuAzC7JgpPdzty +271M16OmGvgerrvc/57d0bh4x/E9gj3ZMpkoXBH/lfs5c7LbvRxQ +-----END OPENSSH PRIVATE KEY----- diff --git a/pkg/manifestgen/sourcesecret/testdata/password_rsa.pub b/pkg/manifestgen/sourcesecret/testdata/password_rsa.pub new file mode 100644 index 00000000..d9b15e1d --- /dev/null +++ b/pkg/manifestgen/sourcesecret/testdata/password_rsa.pub @@ -0,0 +1 @@ +ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAAAgQCi/p5KtpRltu3yxEsc+NtR9kRBuYhsh55609dNiTonLlL7K00pLCEWrtEIVM7FzD8IA6Q1NMNBFijje36gnqMlo6M/cJObkGfZFC025diLI5/ND5R/l60XLtRZfh92K9nCsQKsJaW+R8LTtBdhil/qM/+u07sFHzzk6/36FStg/Q==