Skip to content

Commit 2d045bf

Browse files
feat: support for identityfiles to select keys from ssh-agent (#355)
1 parent 3dac0e2 commit 2d045bf

157 files changed

Lines changed: 2355 additions & 1844 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

cmd/config.go

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -164,6 +164,14 @@ var configAddCmd = &cobra.Command{
164164
if lagoonConfig.SSHKey != "" {
165165
lc.SSHKey = lagoonConfig.SSHKey
166166
}
167+
// check identity files flag
168+
identityFiles, err := cmd.Flags().GetStringSlice("publickey-identityfile")
169+
if err != nil {
170+
return err
171+
}
172+
if identityFiles != nil {
173+
lc.PublicKeyIdentities = identityFiles
174+
}
167175
lagoonCLIConfig.Lagoons[lagoonConfig.Lagoon] = lc
168176
if err := writeLagoonConfig(&lagoonCLIConfig, filepath.Join(configFilePath, configName+configExtension)); err != nil {
169177
return fmt.Errorf("couldn't write config: %v", err)
@@ -314,6 +322,8 @@ func init() {
314322
"Lagoon Kibana URL (https://logs.amazeeio.cloud)")
315323
configAddCmd.Flags().StringVarP(&lagoonSSHKey, "ssh-key", "", "",
316324
"SSH Key to use for this cluster for generating tokens")
325+
configAddCmd.Flags().StringSliceP("publickey-identityfile", "", []string{},
326+
"Specific public key identity files to use when doing ssh-agent checks (support multiple)")
317327
configLagoonsCmd.Flags().BoolVarP(&fullConfigList, "show-full", "", false,
318328
"Show full config output when listing Lagoon configurations")
319329
configFeatureSwitch.Flags().StringVarP(&updateCheck, "disable-update-check", "", "",

cmd/login.go

Lines changed: 52 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package cmd
22

33
import (
4+
"bytes"
45
"fmt"
56
"net"
67
"os"
@@ -10,7 +11,7 @@ import (
1011
"github.com/spf13/cobra"
1112
"golang.org/x/crypto/ssh"
1213
"golang.org/x/crypto/ssh/agent"
13-
"golang.org/x/crypto/ssh/terminal"
14+
terminal "golang.org/x/term"
1415
)
1516

1617
var loginCmd = &cobra.Command{
@@ -23,29 +24,71 @@ var loginCmd = &cobra.Command{
2324
},
2425
}
2526

26-
func publicKey(path string, skipAgent bool) (ssh.AuthMethod, func() error) {
27+
func publicKey(path, publicKeyOverride string, publicKeyIdentities []string, skipAgent bool) (ssh.AuthMethod, func() error) {
2728
noopCloseFunc := func() error { return nil }
2829

2930
if !skipAgent {
3031
// Connect to SSH agent to ask for unencrypted private keys
3132
if sshAgentConn, err := net.Dial("unix", os.Getenv("SSH_AUTH_SOCK")); err == nil {
3233
sshAgent := agent.NewClient(sshAgentConn)
33-
34-
keys, _ := sshAgent.List()
35-
if len(keys) > 0 {
36-
// There are key(s) in the agent
37-
//defer sshAgentConn.Close()
38-
return ssh.PublicKeysCallback(sshAgent.Signers), sshAgentConn.Close
34+
agentSigners, err := sshAgent.Signers()
35+
handleError(err)
36+
// There are key(s) in the agent
37+
if len(agentSigners) > 0 {
38+
identities := make(map[string]ssh.PublicKey)
39+
if publicKeyOverride == "" {
40+
// check for identify files in the current lagoon config context
41+
for _, identityFile := range publicKeyIdentities {
42+
// append to identityfiles
43+
keybytes, err := os.ReadFile(identityFile)
44+
handleError(err)
45+
pubkey, _, _, _, err := ssh.ParseAuthorizedKey(keybytes)
46+
handleError(err)
47+
identities[identityFile] = pubkey
48+
}
49+
} else {
50+
// append to identityfiles
51+
keybytes, err := os.ReadFile(publicKeyOverride)
52+
handleError(err)
53+
pubkey, _, _, _, err := ssh.ParseAuthorizedKey(keybytes)
54+
handleError(err)
55+
identities[publicKeyOverride] = pubkey
56+
}
57+
// check all keys in the agent to see if there is a matching identity file
58+
for _, signer := range agentSigners {
59+
for file, identity := range identities {
60+
if bytes.Equal(signer.PublicKey().Marshal(), identity.Marshal()) {
61+
if verboseOutput {
62+
fmt.Fprintf(os.Stderr, "ssh: attempting connection using identity file public key: %s\n", file)
63+
}
64+
// only provide this matching key back to the ssh client to use
65+
return ssh.PublicKeys(signer), noopCloseFunc
66+
}
67+
}
68+
}
69+
if publicKeyOverride != "" {
70+
handleError(fmt.Errorf("ssh: no key matching %s in agent", publicKeyOverride))
71+
}
72+
// if no matching identity files, just return all agent keys like previous behaviour
73+
if verboseOutput {
74+
fmt.Fprintf(os.Stderr, "ssh: attempting connection using any keys in ssh-agent\n")
75+
}
76+
return ssh.PublicKeysCallback(sshAgent.Signers), noopCloseFunc
3977
}
4078
}
4179
}
4280

81+
// if no keys in the agent, and a specific private key has been defined, then check the key and use it if possible
82+
if verboseOutput {
83+
fmt.Fprintf(os.Stderr, "ssh: attempting connection using private key: %s\n", path)
84+
}
4385
key, err := os.ReadFile(path)
4486
handleError(err)
4587

4688
// Try to look for an unencrypted private key
4789
signer, err := ssh.ParsePrivateKey(key)
4890
if err != nil {
91+
// if encrypted, prompt for passphrase or error and ask user to add to their agent
4992
fmt.Printf("Enter passphrase for %s:", path)
5093
bytePassword, err := terminal.ReadPassword(int(os.Stdin.Fd()))
5194
if err != nil {
@@ -60,9 +103,7 @@ func publicKey(path string, skipAgent bool) (ssh.AuthMethod, func() error) {
60103
fmt.Println("Lagoon CLI could not decode private key, you will need to add your private key to your ssh-agent.")
61104
os.Exit(1)
62105
}
63-
return ssh.PublicKeys(signer), noopCloseFunc
64106
}
65-
// return unencrypted private key
66107
return ssh.PublicKeys(signer), noopCloseFunc
67108
}
68109

@@ -98,7 +139,7 @@ func retrieveTokenViaSsh() (string, error) {
98139
privateKey = cmdSSHKey
99140
skipAgent = true
100141
}
101-
authMethod, closeSSHAgent := publicKey(privateKey, skipAgent)
142+
authMethod, closeSSHAgent := publicKey(privateKey, cmdPubkeyIdentity, lagoonCLIConfig.Lagoons[lagoonCLIConfig.Current].PublicKeyIdentities, skipAgent)
102143
config := &ssh.ClientConfig{
103144
User: "lagoon",
104145
Auth: []ssh.AuthMethod{

cmd/logs.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -112,7 +112,7 @@ func getSSHClientConfig(environmentName string) (*ssh.ClientConfig,
112112
return nil, nil, fmt.Errorf("couldn't get ~/.ssh/known_hosts: %v", err)
113113
}
114114
// configure an SSH client session
115-
authMethod, closeSSHAgent := publicKey(privateKey, skipAgent)
115+
authMethod, closeSSHAgent := publicKey(privateKey, cmdPubkeyIdentity, lagoonCLIConfig.Lagoons[lagoonCLIConfig.Current].PublicKeyIdentities, skipAgent)
116116
return &ssh.ClientConfig{
117117
User: cmdProjectName + "-" + environmentName,
118118
Auth: []ssh.AuthMethod{authMethod},

cmd/root.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ var cmdProject app.LagoonProject
2626
var cmdLagoon = ""
2727
var forceAction bool
2828
var cmdSSHKey = ""
29+
var cmdPubkeyIdentity = ""
2930
var inputScanner = bufio.NewScanner(os.Stdin)
3031
var versionFlag bool
3132
var docsFlag bool
@@ -36,6 +37,7 @@ var createConfig bool
3637
var userPath string
3738
var configFilePath string
3839
var updateDocURL = "https://uselagoon.github.io/lagoon-cli"
40+
var verboseOutput bool
3941

4042
var skipUpdateCheck bool
4143

@@ -129,6 +131,8 @@ func init() {
129131
rootCmd.PersistentFlags().StringVarP(&cmdLagoon, "lagoon", "l", "", "The Lagoon instance to interact with")
130132
rootCmd.PersistentFlags().BoolVarP(&forceAction, "force", "", false, "Force yes on prompts (if supported)")
131133
rootCmd.PersistentFlags().StringVarP(&cmdSSHKey, "ssh-key", "i", "", "Specify path to a specific SSH key to use for lagoon authentication")
134+
rootCmd.PersistentFlags().StringVarP(&cmdPubkeyIdentity, "ssh-publickey", "", "",
135+
"Specify path to a specific SSH public key to use for lagoon authentication using ssh-agent.\nThis will override any public key identities defined in configuration")
132136

133137
// rootCmd.PersistentFlags().BoolVarP(&listAllProjects, "all-projects", "", false, "All projects (if supported)")
134138
rootCmd.PersistentFlags().BoolVarP(&outputOptions.Header, "no-header", "", false, "No header on table (if supported)")
@@ -137,6 +141,7 @@ func init() {
137141
rootCmd.PersistentFlags().BoolVarP(&outputOptions.Pretty, "pretty", "", false, "Make JSON pretty (if supported)")
138142
rootCmd.PersistentFlags().BoolVarP(&debugEnable, "debug", "", false, "Enable debugging output (if supported)")
139143
rootCmd.PersistentFlags().BoolVarP(&skipUpdateCheck, "skip-update-check", "", false, "Skip checking for updates")
144+
rootCmd.PersistentFlags().BoolVarP(&verboseOutput, "verbose", "v", false, "Enable verbose output to stderr (if supported)")
140145

141146
// get config-file from flag
142147
rootCmd.PersistentFlags().StringP("config-file", "", "", "Path to the config file to use (must be *.yml or *.yaml)")

cmd/ssh.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -95,7 +95,7 @@ var sshEnvCmd = &cobra.Command{
9595
} else {
9696

9797
// start an interactive ssh session
98-
authMethod, closeSSHAgent := publicKey(privateKey, skipAgent)
98+
authMethod, closeSSHAgent := publicKey(privateKey, cmdPubkeyIdentity, lagoonCLIConfig.Lagoons[lagoonCLIConfig.Current].PublicKeyIdentities, skipAgent)
9999
config := &ssh.ClientConfig{
100100
User: sshConfig["username"],
101101
Auth: []ssh.AuthMethod{

docs/commands/lagoon.md

Lines changed: 17 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -13,20 +13,23 @@ lagoon [flags]
1313
### Options
1414

1515
```
16-
--config-file string Path to the config file to use (must be *.yml or *.yaml)
17-
--debug Enable debugging output (if supported)
18-
-e, --environment string Specify an environment to use
19-
--force Force yes on prompts (if supported)
20-
-h, --help help for lagoon
21-
-l, --lagoon string The Lagoon instance to interact with
22-
--no-header No header on table (if supported)
23-
--output-csv Output as CSV (if supported)
24-
--output-json Output as JSON (if supported)
25-
--pretty Make JSON pretty (if supported)
26-
-p, --project string Specify a project to use
27-
--skip-update-check Skip checking for updates
28-
-i, --ssh-key string Specify path to a specific SSH key to use for lagoon authentication
29-
--version Version information
16+
--config-file string Path to the config file to use (must be *.yml or *.yaml)
17+
--debug Enable debugging output (if supported)
18+
-e, --environment string Specify an environment to use
19+
--force Force yes on prompts (if supported)
20+
-h, --help help for lagoon
21+
-l, --lagoon string The Lagoon instance to interact with
22+
--no-header No header on table (if supported)
23+
--output-csv Output as CSV (if supported)
24+
--output-json Output as JSON (if supported)
25+
--pretty Make JSON pretty (if supported)
26+
-p, --project string Specify a project to use
27+
--skip-update-check Skip checking for updates
28+
-i, --ssh-key string Specify path to a specific SSH key to use for lagoon authentication
29+
--ssh-publickey string Specify path to a specific SSH public key to use for lagoon authentication using ssh-agent.
30+
This will override any public key identities defined in configuration
31+
-v, --verbose Enable verbose output to stderr (if supported)
32+
--version Version information
3033
```
3134

3235
### SEE ALSO

docs/commands/lagoon_add.md

Lines changed: 15 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -11,18 +11,21 @@ Add a project, or add notifications and variables to projects or environments
1111
### Options inherited from parent commands
1212

1313
```
14-
--config-file string Path to the config file to use (must be *.yml or *.yaml)
15-
--debug Enable debugging output (if supported)
16-
-e, --environment string Specify an environment to use
17-
--force Force yes on prompts (if supported)
18-
-l, --lagoon string The Lagoon instance to interact with
19-
--no-header No header on table (if supported)
20-
--output-csv Output as CSV (if supported)
21-
--output-json Output as JSON (if supported)
22-
--pretty Make JSON pretty (if supported)
23-
-p, --project string Specify a project to use
24-
--skip-update-check Skip checking for updates
25-
-i, --ssh-key string Specify path to a specific SSH key to use for lagoon authentication
14+
--config-file string Path to the config file to use (must be *.yml or *.yaml)
15+
--debug Enable debugging output (if supported)
16+
-e, --environment string Specify an environment to use
17+
--force Force yes on prompts (if supported)
18+
-l, --lagoon string The Lagoon instance to interact with
19+
--no-header No header on table (if supported)
20+
--output-csv Output as CSV (if supported)
21+
--output-json Output as JSON (if supported)
22+
--pretty Make JSON pretty (if supported)
23+
-p, --project string Specify a project to use
24+
--skip-update-check Skip checking for updates
25+
-i, --ssh-key string Specify path to a specific SSH key to use for lagoon authentication
26+
--ssh-publickey string Specify path to a specific SSH public key to use for lagoon authentication using ssh-agent.
27+
This will override any public key identities defined in configuration
28+
-v, --verbose Enable verbose output to stderr (if supported)
2629
```
2730

2831
### SEE ALSO

docs/commands/lagoon_add_deploytarget-config.md

Lines changed: 15 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -19,18 +19,21 @@ lagoon add deploytarget-config [flags]
1919
### Options inherited from parent commands
2020

2121
```
22-
--config-file string Path to the config file to use (must be *.yml or *.yaml)
23-
--debug Enable debugging output (if supported)
24-
-e, --environment string Specify an environment to use
25-
--force Force yes on prompts (if supported)
26-
-l, --lagoon string The Lagoon instance to interact with
27-
--no-header No header on table (if supported)
28-
--output-csv Output as CSV (if supported)
29-
--output-json Output as JSON (if supported)
30-
--pretty Make JSON pretty (if supported)
31-
-p, --project string Specify a project to use
32-
--skip-update-check Skip checking for updates
33-
-i, --ssh-key string Specify path to a specific SSH key to use for lagoon authentication
22+
--config-file string Path to the config file to use (must be *.yml or *.yaml)
23+
--debug Enable debugging output (if supported)
24+
-e, --environment string Specify an environment to use
25+
--force Force yes on prompts (if supported)
26+
-l, --lagoon string The Lagoon instance to interact with
27+
--no-header No header on table (if supported)
28+
--output-csv Output as CSV (if supported)
29+
--output-json Output as JSON (if supported)
30+
--pretty Make JSON pretty (if supported)
31+
-p, --project string Specify a project to use
32+
--skip-update-check Skip checking for updates
33+
-i, --ssh-key string Specify path to a specific SSH key to use for lagoon authentication
34+
--ssh-publickey string Specify path to a specific SSH public key to use for lagoon authentication using ssh-agent.
35+
This will override any public key identities defined in configuration
36+
-v, --verbose Enable verbose output to stderr (if supported)
3437
```
3538

3639
### SEE ALSO

docs/commands/lagoon_add_deploytarget.md

Lines changed: 15 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -30,18 +30,21 @@ lagoon add deploytarget [flags]
3030
### Options inherited from parent commands
3131

3232
```
33-
--config-file string Path to the config file to use (must be *.yml or *.yaml)
34-
--debug Enable debugging output (if supported)
35-
-e, --environment string Specify an environment to use
36-
--force Force yes on prompts (if supported)
37-
-l, --lagoon string The Lagoon instance to interact with
38-
--no-header No header on table (if supported)
39-
--output-csv Output as CSV (if supported)
40-
--output-json Output as JSON (if supported)
41-
--pretty Make JSON pretty (if supported)
42-
-p, --project string Specify a project to use
43-
--skip-update-check Skip checking for updates
44-
-i, --ssh-key string Specify path to a specific SSH key to use for lagoon authentication
33+
--config-file string Path to the config file to use (must be *.yml or *.yaml)
34+
--debug Enable debugging output (if supported)
35+
-e, --environment string Specify an environment to use
36+
--force Force yes on prompts (if supported)
37+
-l, --lagoon string The Lagoon instance to interact with
38+
--no-header No header on table (if supported)
39+
--output-csv Output as CSV (if supported)
40+
--output-json Output as JSON (if supported)
41+
--pretty Make JSON pretty (if supported)
42+
-p, --project string Specify a project to use
43+
--skip-update-check Skip checking for updates
44+
-i, --ssh-key string Specify path to a specific SSH key to use for lagoon authentication
45+
--ssh-publickey string Specify path to a specific SSH public key to use for lagoon authentication using ssh-agent.
46+
This will override any public key identities defined in configuration
47+
-v, --verbose Enable verbose output to stderr (if supported)
4548
```
4649

4750
### SEE ALSO

docs/commands/lagoon_add_group.md

Lines changed: 15 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -23,18 +23,21 @@ lagoon add group [flags]
2323
### Options inherited from parent commands
2424

2525
```
26-
--config-file string Path to the config file to use (must be *.yml or *.yaml)
27-
--debug Enable debugging output (if supported)
28-
-e, --environment string Specify an environment to use
29-
--force Force yes on prompts (if supported)
30-
-l, --lagoon string The Lagoon instance to interact with
31-
--no-header No header on table (if supported)
32-
--output-csv Output as CSV (if supported)
33-
--output-json Output as JSON (if supported)
34-
--pretty Make JSON pretty (if supported)
35-
-p, --project string Specify a project to use
36-
--skip-update-check Skip checking for updates
37-
-i, --ssh-key string Specify path to a specific SSH key to use for lagoon authentication
26+
--config-file string Path to the config file to use (must be *.yml or *.yaml)
27+
--debug Enable debugging output (if supported)
28+
-e, --environment string Specify an environment to use
29+
--force Force yes on prompts (if supported)
30+
-l, --lagoon string The Lagoon instance to interact with
31+
--no-header No header on table (if supported)
32+
--output-csv Output as CSV (if supported)
33+
--output-json Output as JSON (if supported)
34+
--pretty Make JSON pretty (if supported)
35+
-p, --project string Specify a project to use
36+
--skip-update-check Skip checking for updates
37+
-i, --ssh-key string Specify path to a specific SSH key to use for lagoon authentication
38+
--ssh-publickey string Specify path to a specific SSH public key to use for lagoon authentication using ssh-agent.
39+
This will override any public key identities defined in configuration
40+
-v, --verbose Enable verbose output to stderr (if supported)
3841
```
3942

4043
### SEE ALSO

0 commit comments

Comments
 (0)