diff --git a/cmd/drone-docker/main.go b/cmd/drone-docker/main.go index d467591d..dd2a6a56 100644 --- a/cmd/drone-docker/main.go +++ b/cmd/drone-docker/main.go @@ -264,6 +264,11 @@ func main() { Usage: "secret key value pairs eg secret_name=/path/to/secret", EnvVar: "PLUGIN_SECRETS_FROM_FILE", }, + cli.StringFlag{ + Name: "ssh-agent", + Usage: "mount ssh agent", + EnvVar: "PLUGIN_SSH_AGENT", + }, cli.StringFlag{ Name: "drone-card-path", Usage: "card path location to write to", @@ -310,6 +315,7 @@ func run(c *cli.Context) error { Secret: c.String("secret"), SecretEnvs: c.StringSlice("secrets-from-env"), SecretFiles: c.StringSlice("secrets-from-file"), + SSHAgent: c.String("ssh-agent"), AddHost: c.StringSlice("add-host"), Quiet: c.Bool("quiet"), }, diff --git a/docker.go b/docker.go index 236065de..fd923e1f 100644 --- a/docker.go +++ b/docker.go @@ -1,8 +1,10 @@ package docker import ( + "encoding/base64" "fmt" "io/ioutil" + "log" "os" "os/exec" "path/filepath" @@ -11,6 +13,11 @@ import ( "time" ) +const ( + SSHAgentSockPath = "/tmp/drone-ssh-agent-sock" + SSHPrivateKeyFromEnv = "SSH_KEY" +) + type ( // Daemon defines Docker daemon parameters. Daemon struct { @@ -63,6 +70,7 @@ type ( SecretFiles []string // Docker build secrets with file as source AddHost []string // Docker build add-host Quiet bool // Docker build quiet + SSHAgent string // Docker build ssh } // Plugin defines the Docker plugin parameters. @@ -105,6 +113,7 @@ type ( // Exec executes the plugin step func (p Plugin) Exec() error { + // start the Docker daemon server if !p.Daemon.Disabled { p.startDaemon() @@ -178,6 +187,16 @@ func (p Plugin) Exec() error { cmds = append(cmds, commandPull(img)) } + // setup for using ssh agent (https://docs.docker.com/develop/develop-images/build_enhancements/#using-ssh-to-access-private-data-in-builds) + if p.Build.SSHAgent != "" { + // TODO check in with one of the drone devs...this should not be necessary. I'm probably doing something + // wrong with the cli framework + p.Build.SSHAgent = strings.TrimSuffix(p.Build.SSHAgent, "]") + p.Build.SSHAgent = strings.TrimPrefix(p.Build.SSHAgent, "[") + fmt.Printf("ssh agent set to \"%s\"\n", p.Build.SSHAgent) + cmds = append(cmds, commandSSHAgentForwardingSetup(p.Build)...) + } + cmds = append(cmds, commandBuild(p.Build)) // docker build for _, tag := range p.Build.Tags { @@ -324,6 +343,9 @@ func commandBuild(build Build) *exec.Cmd { if build.Quiet { args = append(args, "--quiet") } + if build.SSHAgent != "" { + args = append(args, "--ssh", build.SSHAgent) + } if build.AutoLabel { labelSchema := []string{ @@ -349,8 +371,8 @@ func commandBuild(build Build) *exec.Cmd { } } - // we need to enable buildkit, for secret support - if build.Secret != "" || len(build.SecretEnvs) > 0 || len(build.SecretFiles) > 0 { + // we need to enable buildkit, for secret support and ssh agent support + if build.Secret != "" || len(build.SecretEnvs) > 0 || len(build.SecretFiles) > 0 || build.SSHAgent != "" { os.Setenv("DOCKER_BUILDKIT", "1") } return exec.Command(dockerExe, args...) @@ -503,6 +525,40 @@ func commandRmi(tag string) *exec.Cmd { return exec.Command(dockerExe, "rmi", tag) } +func commandSSHAgentForwardingSetup(build Build) []*exec.Cmd { + cmds := make([]*exec.Cmd, 0) + if err := writeSSHPrivateKey(); err != nil { + log.Fatalf("unable to setup ssh agent forwarding: %s", err) + } + os.Setenv("SSH_AUTH_SOCK", SSHAgentSockPath) + cmds = append(cmds, exec.Command("ssh-agent", "-a", SSHAgentSockPath)) + cmds = append(cmds, exec.Command("ssh-add")) + return cmds +} + +func writeSSHPrivateKey() error { + privateKeyBase64 := os.Getenv(SSHPrivateKeyFromEnv) + if privateKeyBase64 == "" { + return fmt.Errorf("%s must be defined and contain the base64 encoded private key to use for ssh agent forwarding", SSHPrivateKeyFromEnv) + } + var err error + privateKey, err := base64.StdEncoding.DecodeString(privateKeyBase64) + if err != nil { + return fmt.Errorf("unable to base64 decode private key") + } + home, err := os.UserHomeDir() + if err != nil { + return fmt.Errorf("unable to determine home directory: %s", err) + } + if err := os.MkdirAll(filepath.Join(home, ".ssh"), 0700); err != nil { + return fmt.Errorf("unable to create .ssh directory: %s", err) + } + if err := os.WriteFile(filepath.Join(home, ".ssh", "id_rsa"), privateKey, 0400); err != nil { + return fmt.Errorf("unable to write ssh key: %s", err) + } + return nil +} + // trace writes each command to stdout with the command wrapped in an xml // tag so that it can be extracted and displayed in the logs. func trace(cmd *exec.Cmd) { diff --git a/docker_test.go b/docker_test.go index ea90181d..91cb4723 100644 --- a/docker_test.go +++ b/docker_test.go @@ -114,6 +114,26 @@ func TestCommandBuild(t *testing.T) { ".", ), }, + { + name: "ssh agent", + build: Build{ + Name: "plugins/drone-docker:latest", + Dockerfile: "Dockerfile", + Context: ".", + SSHAgent: "default", + }, + want: exec.Command( + dockerExe, + "build", + "--rm=true", + "-f", + "Dockerfile", + "-t", + "plugins/drone-docker:latest", + ".", + "--ssh default", + ), + }, } for _, tc := range tcs {