Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: support for identityfiles to select keys from ssh-agent #355

Merged
merged 1 commit into from
Jun 27, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading