Skip to content

Commit

Permalink
feat: support for identityfiles to select keys from ssh-agent (#355)
Browse files Browse the repository at this point in the history
  • Loading branch information
shreddedbacon authored Jun 27, 2024
1 parent 3dac0e2 commit 2d045bf
Show file tree
Hide file tree
Showing 157 changed files with 2,355 additions and 1,844 deletions.
10 changes: 10 additions & 0 deletions cmd/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -164,6 +164,14 @@ var configAddCmd = &cobra.Command{
if lagoonConfig.SSHKey != "" {
lc.SSHKey = lagoonConfig.SSHKey
}
// check identity files flag
identityFiles, err := cmd.Flags().GetStringSlice("publickey-identityfile")
if err != nil {
return err
}
if identityFiles != nil {
lc.PublicKeyIdentities = identityFiles
}
lagoonCLIConfig.Lagoons[lagoonConfig.Lagoon] = lc
if err := writeLagoonConfig(&lagoonCLIConfig, filepath.Join(configFilePath, configName+configExtension)); err != nil {
return fmt.Errorf("couldn't write config: %v", err)
Expand Down Expand Up @@ -314,6 +322,8 @@ func init() {
"Lagoon Kibana URL (https://logs.amazeeio.cloud)")
configAddCmd.Flags().StringVarP(&lagoonSSHKey, "ssh-key", "", "",
"SSH Key to use for this cluster for generating tokens")
configAddCmd.Flags().StringSliceP("publickey-identityfile", "", []string{},
"Specific public key identity files to use when doing ssh-agent checks (support multiple)")
configLagoonsCmd.Flags().BoolVarP(&fullConfigList, "show-full", "", false,
"Show full config output when listing Lagoon configurations")
configFeatureSwitch.Flags().StringVarP(&updateCheck, "disable-update-check", "", "",
Expand Down
63 changes: 52 additions & 11 deletions cmd/login.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package cmd

import (
"bytes"
"fmt"
"net"
"os"
Expand All @@ -10,7 +11,7 @@ import (
"github.com/spf13/cobra"
"golang.org/x/crypto/ssh"
"golang.org/x/crypto/ssh/agent"
"golang.org/x/crypto/ssh/terminal"
terminal "golang.org/x/term"
)

var loginCmd = &cobra.Command{
Expand All @@ -23,29 +24,71 @@ var loginCmd = &cobra.Command{
},
}

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

if !skipAgent {
// Connect to SSH agent to ask for unencrypted private keys
if sshAgentConn, err := net.Dial("unix", os.Getenv("SSH_AUTH_SOCK")); err == nil {
sshAgent := agent.NewClient(sshAgentConn)

keys, _ := sshAgent.List()
if len(keys) > 0 {
// There are key(s) in the agent
//defer sshAgentConn.Close()
return ssh.PublicKeysCallback(sshAgent.Signers), sshAgentConn.Close
agentSigners, err := sshAgent.Signers()
handleError(err)
// There are key(s) in the agent
if len(agentSigners) > 0 {
identities := make(map[string]ssh.PublicKey)
if publicKeyOverride == "" {
// check for identify files in the current lagoon config context
for _, identityFile := range publicKeyIdentities {
// append to identityfiles
keybytes, err := os.ReadFile(identityFile)
handleError(err)
pubkey, _, _, _, err := ssh.ParseAuthorizedKey(keybytes)
handleError(err)
identities[identityFile] = pubkey
}
} else {
// append to identityfiles
keybytes, err := os.ReadFile(publicKeyOverride)
handleError(err)
pubkey, _, _, _, err := ssh.ParseAuthorizedKey(keybytes)
handleError(err)
identities[publicKeyOverride] = pubkey
}
// check all keys in the agent to see if there is a matching identity file
for _, signer := range agentSigners {
for file, identity := range identities {
if bytes.Equal(signer.PublicKey().Marshal(), identity.Marshal()) {
if verboseOutput {
fmt.Fprintf(os.Stderr, "ssh: attempting connection using identity file public key: %s\n", file)
}
// only provide this matching key back to the ssh client to use
return ssh.PublicKeys(signer), noopCloseFunc
}
}
}
if publicKeyOverride != "" {
handleError(fmt.Errorf("ssh: no key matching %s in agent", publicKeyOverride))
}
// if no matching identity files, just return all agent keys like previous behaviour
if verboseOutput {
fmt.Fprintf(os.Stderr, "ssh: attempting connection using any keys in ssh-agent\n")
}
return ssh.PublicKeysCallback(sshAgent.Signers), noopCloseFunc
}
}
}

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

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

Expand Down Expand Up @@ -98,7 +139,7 @@ func retrieveTokenViaSsh() (string, error) {
privateKey = cmdSSHKey
skipAgent = true
}
authMethod, closeSSHAgent := publicKey(privateKey, skipAgent)
authMethod, closeSSHAgent := publicKey(privateKey, cmdPubkeyIdentity, lagoonCLIConfig.Lagoons[lagoonCLIConfig.Current].PublicKeyIdentities, skipAgent)
config := &ssh.ClientConfig{
User: "lagoon",
Auth: []ssh.AuthMethod{
Expand Down
2 changes: 1 addition & 1 deletion cmd/logs.go
Original file line number Diff line number Diff line change
Expand Up @@ -112,7 +112,7 @@ func getSSHClientConfig(environmentName string) (*ssh.ClientConfig,
return nil, nil, fmt.Errorf("couldn't get ~/.ssh/known_hosts: %v", err)
}
// configure an SSH client session
authMethod, closeSSHAgent := publicKey(privateKey, skipAgent)
authMethod, closeSSHAgent := publicKey(privateKey, cmdPubkeyIdentity, lagoonCLIConfig.Lagoons[lagoonCLIConfig.Current].PublicKeyIdentities, skipAgent)
return &ssh.ClientConfig{
User: cmdProjectName + "-" + environmentName,
Auth: []ssh.AuthMethod{authMethod},
Expand Down
5 changes: 5 additions & 0 deletions cmd/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ var cmdProject app.LagoonProject
var cmdLagoon = ""
var forceAction bool
var cmdSSHKey = ""
var cmdPubkeyIdentity = ""
var inputScanner = bufio.NewScanner(os.Stdin)
var versionFlag bool
var docsFlag bool
Expand All @@ -36,6 +37,7 @@ var createConfig bool
var userPath string
var configFilePath string
var updateDocURL = "https://uselagoon.github.io/lagoon-cli"
var verboseOutput bool

var skipUpdateCheck bool

Expand Down Expand Up @@ -129,6 +131,8 @@ func init() {
rootCmd.PersistentFlags().StringVarP(&cmdLagoon, "lagoon", "l", "", "The Lagoon instance to interact with")
rootCmd.PersistentFlags().BoolVarP(&forceAction, "force", "", false, "Force yes on prompts (if supported)")
rootCmd.PersistentFlags().StringVarP(&cmdSSHKey, "ssh-key", "i", "", "Specify path to a specific SSH key to use for lagoon authentication")
rootCmd.PersistentFlags().StringVarP(&cmdPubkeyIdentity, "ssh-publickey", "", "",
"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")

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

// get config-file from flag
rootCmd.PersistentFlags().StringP("config-file", "", "", "Path to the config file to use (must be *.yml or *.yaml)")
Expand Down
2 changes: 1 addition & 1 deletion cmd/ssh.go
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,7 @@ var sshEnvCmd = &cobra.Command{
} else {

// start an interactive ssh session
authMethod, closeSSHAgent := publicKey(privateKey, skipAgent)
authMethod, closeSSHAgent := publicKey(privateKey, cmdPubkeyIdentity, lagoonCLIConfig.Lagoons[lagoonCLIConfig.Current].PublicKeyIdentities, skipAgent)
config := &ssh.ClientConfig{
User: sshConfig["username"],
Auth: []ssh.AuthMethod{
Expand Down
31 changes: 17 additions & 14 deletions docs/commands/lagoon.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,20 +13,23 @@ lagoon [flags]
### Options

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

### SEE ALSO
Expand Down
27 changes: 15 additions & 12 deletions docs/commands/lagoon_add.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,18 +11,21 @@ Add a project, or add notifications and variables to projects or environments
### Options inherited from parent commands

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

### SEE ALSO
Expand Down
27 changes: 15 additions & 12 deletions docs/commands/lagoon_add_deploytarget-config.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,18 +19,21 @@ lagoon add deploytarget-config [flags]
### Options inherited from parent commands

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

### SEE ALSO
Expand Down
27 changes: 15 additions & 12 deletions docs/commands/lagoon_add_deploytarget.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,18 +30,21 @@ lagoon add deploytarget [flags]
### Options inherited from parent commands

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

### SEE ALSO
Expand Down
27 changes: 15 additions & 12 deletions docs/commands/lagoon_add_group.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,18 +23,21 @@ lagoon add group [flags]
### Options inherited from parent commands

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

### SEE ALSO
Expand Down
Loading

0 comments on commit 2d045bf

Please sign in to comment.