Skip to content

Commit

Permalink
feat: allow files and symlinks in ddev auth ssh, fixes ddev#5465, f…
Browse files Browse the repository at this point in the history
  • Loading branch information
stasadev authored Nov 13, 2024
1 parent e6bdf00 commit 6c258b7
Show file tree
Hide file tree
Showing 2 changed files with 140 additions and 28 deletions.
161 changes: 133 additions & 28 deletions cmd/ddev/cmd/auth-ssh.go
Original file line number Diff line number Diff line change
@@ -1,27 +1,35 @@
package cmd

import (
"os"
"path/filepath"

"fmt"
"github.com/ddev/ddev/pkg/ddevapp"
"github.com/ddev/ddev/pkg/docker"
"github.com/ddev/ddev/pkg/exec"
"github.com/ddev/ddev/pkg/fileutil"
"github.com/ddev/ddev/pkg/globalconfig"
"github.com/ddev/ddev/pkg/heredoc"
"github.com/ddev/ddev/pkg/nodeps"
"github.com/ddev/ddev/pkg/util"
"github.com/ddev/ddev/pkg/versionconstants"
"github.com/spf13/cobra"
"os"
"path/filepath"
"slices"
"strings"
)

// sshKeyPath is the full path to the *directory* containing SSH keys.
var sshKeyPath string
var sshKeyFiles, sshKeyDirs []string

// AuthSSHCommand implements the "ddev auth ssh" command
var AuthSSHCommand = &cobra.Command{
Use: "ssh",
Short: "Add SSH key authentication to the ddev-ssh-agent container",
Long: `Use this command to provide the password to your SSH key to the ddev-ssh-agent container, where it can be used by other containers. Normal usage is "ddev auth ssh", or if your key is not in ~/.ssh, ddev auth ssh --ssh-key-path=/some/path/.ssh"`,
Example: `ddev auth ssh`,
Use: "ssh",
Short: "Add SSH key authentication to the ddev-ssh-agent container",
Long: `Use this command to provide the password to your SSH key to the ddev-ssh-agent container, where it can be used by other containers.`,
Example: heredoc.DocI2S(`
ddev auth ssh
ddev auth ssh -d ~/custom/path/to/ssh
ddev auth ssh -f ~/.ssh/id_ed25519 -f ~/.ssh/id_rsa
ddev auth ssh -d ~/.ssh -f ~/custom/path/to/ssh/id_ed25519
`),
Run: func(_ *cobra.Command, args []string) {
var err error
if len(args) > 0 {
Expand All @@ -30,28 +38,32 @@ var AuthSSHCommand = &cobra.Command{

uidStr, _, _ := util.GetContainerUIDGid()

if sshKeyPath == "" {
// Use ~/.ssh if nothing is provided
if sshKeyFiles == nil && sshKeyDirs == nil {
homeDir, err := os.UserHomeDir()
if err != nil {
util.Failed("Unable to determine home directory: %v", err)
}
sshKeyPath = filepath.Join(homeDir, ".ssh")
sshKeyDirs = append(sshKeyDirs, filepath.Join(homeDir, ".ssh"))
}
if !filepath.IsAbs(sshKeyPath) {
sshKeyPath, err = filepath.Abs(sshKeyPath)

files := getSSHKeyPaths(sshKeyDirs, true, false)
files = append(files, getSSHKeyPaths(sshKeyFiles, false, true)...)

var keys []string
// Get real paths to key files in case they are symlinks
for _, file := range files {
key, err := filepath.EvalSymlinks(file)
if err != nil {
util.Failed("Failed to derive absolute path for SSH key path %s: %v", sshKeyPath, err)
util.Warning("Unable to read %s file: %v", file, err)
continue
}
if !slices.Contains(keys, key) && fileIsPrivateKey(key) {
keys = append(keys, key)
}
}
fi, err := os.Stat(sshKeyPath)
if os.IsNotExist(err) {
util.Failed("The SSH key directory %s was not found", sshKeyPath)
}
if err != nil {
util.Failed("Failed to check status of SSH key directory %s: %v", sshKeyPath, err)
}
if !fi.IsDir() {
util.Failed("The SSH key directory (%s) must be a directory", sshKeyPath)
if len(keys) == 0 {
util.Failed("No SSH keys found in %s", strings.Join(append(sshKeyDirs, sshKeyFiles...), ", "))
}

app, err := ddevapp.GetActiveApp("")
Expand All @@ -68,20 +80,113 @@ var AuthSSHCommand = &cobra.Command{
if err != nil {
util.Failed("Failed to start ddev-ssh-agent container: %v", err)
}
sshKeyPath = util.WindowsPathToCygwinPath(sshKeyPath)

dockerCmd := []string{"run", "-it", "--rm", "--volumes-from=" + ddevapp.SSHAuthName, "--user=" + uidStr, "--entrypoint=", "--mount=type=bind,src=" + sshKeyPath + ",dst=/tmp/sshtmp", versionconstants.SSHAuthImage + ":" + versionconstants.SSHAuthTag + "-built", "bash", "-c", `cp -r /tmp/sshtmp ~/.ssh && chmod -R go-rwx ~/.ssh && cd ~/.ssh && grep -l '^-----BEGIN .* PRIVATE KEY-----' * | xargs -d '\n' ssh-add`}
var mounts []string
// Map to track already added keys
addedKeys := make(map[string]struct{})
for i, keyPath := range keys {
filename := filepath.Base(keyPath)
// If it has the same name, change it to avoid conflicts
// This can happen if you have symlinks to the same key
if _, exists := addedKeys[filename]; exists {
filename = fmt.Sprintf("%s_%d", filename, i)
}
addedKeys[filename] = struct{}{}
keyPath = util.WindowsPathToCygwinPath(keyPath)
mounts = append(mounts, "--mount=type=bind,src="+keyPath+",dst=/tmp/sshtmp/"+filename)
}

dockerCmd := []string{"run", "-it", "--rm", "--volumes-from=" + ddevapp.SSHAuthName, "--user=" + uidStr, "--entrypoint="}
dockerCmd = append(dockerCmd, mounts...)
dockerCmd = append(dockerCmd, docker.GetSSHAuthImage()+"-built", "bash", "-c", `cp -r /tmp/sshtmp ~/.ssh && chmod -R go-rwx ~/.ssh && cd ~/.ssh && grep -l '^-----BEGIN .* PRIVATE KEY-----' * | xargs -d '\n' ssh-add`)

err = exec.RunInteractiveCommand("docker", dockerCmd)

if err != nil {
util.Failed("Docker command 'docker %v' failed: %v", dockerCmd, err)
helpMessage := ""
// Add more helpful message to the obscure error from Docker
// Can be triggered if the key is in /tmp on macOS
if strings.Contains(err.Error(), "bind source path does not exist") {
helpMessage = "\n\nThe specified SSH key path is not shared with your Docker provider."
}
util.Failed("Docker command 'docker %v' failed: %v %v", echoDockerCmd(dockerCmd), err, helpMessage)
}
},
}

// getSSHKeyPaths returns an array of full paths to SSH keys
// with checks to ensure they are valid.
func getSSHKeyPaths(sshKeyPathArray []string, acceptsDirsOnly bool, acceptsFilesOnly bool) []string {
var files []string
for _, sshKeyPath := range sshKeyPathArray {
if !filepath.IsAbs(sshKeyPath) {
cwd, err := os.Getwd()
if err != nil {
util.Failed("Failed to get current working directory: %v", err)
}
fullPath, err := filepath.Abs(filepath.Join(cwd, sshKeyPath))
if err != nil {
util.Failed("Failed to derive absolute path for SSH key path %s: %v", sshKeyPath, err)
}
sshKeyPath = fullPath
}
fi, err := os.Stat(sshKeyPath)
if os.IsNotExist(err) {
util.Failed("The SSH key path %s was not found", sshKeyPath)
}
if err != nil {
util.Failed("Failed to check status of SSH key path %s: %v", sshKeyPath, err)
}
if !fi.IsDir() {
if acceptsDirsOnly {
util.Failed("SSH key path %s is not a directory", sshKeyPath)
}
files = append(files, sshKeyPath)
} else {
if acceptsFilesOnly {
util.Failed("SSH key path %s is not a file", sshKeyPath)
}
files, err = fileutil.ListFilesInDirFullPath(sshKeyPath, true)
if err != nil {
util.Failed("Failed to list files in %s: %v", sshKeyPath, err)
}
}
}
return files
}

// fileIsPrivateKey checks if a file is readable and that it is a private key.
// Regex isn't used here because files can be huge.
// The full check if it's really a private key is done with grep -l '^-----BEGIN .* PRIVATE KEY-----'.
func fileIsPrivateKey(filePath string) bool {
file, err := os.Open(filePath)
if err != nil {
return false
}
// nolint: errcheck
defer file.Close()
prefix := []byte("-----BEGIN")
buffer := make([]byte, len(prefix))
_, err = file.Read(buffer)
if err != nil {
return false
}
return string(buffer) == string(prefix)
}

// echoDockerCmd formats the Docker command to be more readable.
func echoDockerCmd(dockerCmd []string) string {
for i, arg := range dockerCmd {
if strings.Contains(arg, " ") {
dockerCmd[i] = `"` + arg + `"`
}
}
return strings.Join(dockerCmd, " ")
}

func init() {
AuthSSHCommand.Flags().StringVarP(&sshKeyPath, "ssh-key-path", "d", "", "full path to SSH key directory")
AuthSSHCommand.Flags().StringArrayVarP(&sshKeyFiles, "ssh-key-file", "f", nil, "full path to SSH key file")
AuthSSHCommand.Flags().StringArrayVarP(&sshKeyDirs, "ssh-key-path", "d", nil, "full path to SSH key directory")

AuthCmd.AddCommand(AuthSSHCommand)
}
7 changes: 7 additions & 0 deletions docs/content/users/usage/commands.md
Original file line number Diff line number Diff line change
Expand Up @@ -191,10 +191,17 @@ Example:
```shell
# Add your SSH keys to the SSH agent container
ddev auth ssh
# Add your SSH keys from ~/custom/path/to/ssh
ddev auth ssh -d ~/custom/path/to/ssh
# Add your SSH keys from ~/.ssh/id_ed25519 and ~/.ssh/id_rsa
ddev auth ssh -f ~/.ssh/id_ed25519 -f ~/.ssh/id_rsa
# Add your SSH keys from ~/.ssh and ~/custom/path/to/ssh/id_ed25519
ddev auth ssh -d ~/.ssh -f ~/custom/path/to/ssh/id_ed25519
```

Flags:

* `--ssh-key-file`, `-f`: Full path to SSH key file.
* `--ssh-key-path`, `-d`: Full path to SSH key directory.

## `blackfire`
Expand Down

0 comments on commit 6c258b7

Please sign in to comment.