Skip to content

Commit

Permalink
first commit
Browse files Browse the repository at this point in the history
  • Loading branch information
Benton Roberts committed Oct 27, 2015
0 parents commit 912ffa8
Show file tree
Hide file tree
Showing 8 changed files with 452 additions and 0 deletions.
38 changes: 38 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
# program output from databag-env
*.env

# go build output
databag-envdump/databag-envdump
build12/build12
release12/release12
docker-ssh-exec/docker-ssh-exec

# goxc build output and local config
pkg/
*.goxc.local.json

# Compiled Object files, Static and Dynamic libs (Shared Objects)
*.o
*.a
*.so

# Folders
_obj
_test

# Architecture specific extensions/prefixes
*.[568vq]
[568vq].out

*.cgo1.go
*.cgo2.c
_cgo_defun.c
_cgo_gotypes.go
_cgo_export.*

_testmain.go

*.exe
*.test

tmp/
16 changes: 16 additions & 0 deletions .goxc.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
{
"ArtifactsDest": "./pkg",
"Tasks": [
"interpolate-source",
"go-install",
"xc",
"copy-resources",
"archive-zip",
"archive-tar-gz",
"rmbin"
],
"Arch": "amd64",
"BuildConstraints": "linux",
"PackageVersion": "0.5.1",
"ConfigVersion": "0.9"
}
5 changes: 5 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
FROM busybox:latest

ADD pkg/docker-ssh-exec /docker-ssh-exec

ENTRYPOINT ["/docker-ssh-exec"]
88 changes: 88 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
docker-ssh-exec - Secure SSH key injection for Docker builds
================
Allows commands that require an SSH key to be run from within a `Dockerfile`, without leaving the key in the resulting image.

----------------
Overview
----------------
This program runs in two different modes:

* a server mode, run as the Docker image `mdsol/docker-ssh-exec`, which transmits an SSH key on request to the the client; and
* a client mode, invoked from within the `Dockerfile`, that grabs the key from the server, writes it to the filesystem, runs the desired build command, and then *deletes the key* before the filesystem is snapshotted into the build.

----------------
Installation
----------------
To install the server, just pull it like any other Docker image.

To install the client, just grab it from the [releases page][1], uncompress the archive, and copy the binary to somewhere in your `$PATH`. Remember that the client is run during the `docker build...` process, so either install the client just before invoking it, or make sure it's already present in your source image. Here's an example of the code you might run in your source image, to prepare it for SSH cloning from GitHub:

# install Medidata docker-ssh-exec build tool from S3 bucket "mybucket"
curl https://s3.amazonaws.com/mybucket/docker-ssh-exec/\
docker-ssh-exec_0.3.2_linux_amd64.tar.gz | \
tar -xz --strip-components=1 -C /usr/local/bin \
docker-ssh-exec_0.3.2_linux_amd64/docker-ssh-exec
mkdir -p /root/.ssh && chmod 0700 /root/.ssh
ssh-keyscan github.com >/root/.ssh/known_hosts


----------------
Usage
----------------
To run the server component, pass it the private half of your SSH key, either as a shared volume:

docker run -v ~/.ssh/id_rsa:/root/.ssh/id_rsa --name=keyserver -d \
mdsol/docker-ssh-exec -server

or as an ENV var:

docker run -e DOCKER-SSH-KEY="$(cat ~/.ssh/id_rsa)" --name=keyserver -d \
mdsol/docker-ssh-exec -server

Then, run a quick test of the client, to make sure it can get the key:

docker run --rm -it mdsol/docker-ssh-exec cat /root/.ssh/id_rsa

Finally, as long as the source image is set up to trust (or ignore) GitHub's server key, you can clone private repositories from within the `Dockerfile` like this:

docker-exec-ssh git clone [email protected]:my_user/my_private_repo.git

The client first transfers the key from the server, writing it to `$HOME/.ssh/id_rsa` (by default), then executes whatever command you supply as arguments. Before exiting, it deletes the key from the filesystem.

Here's the command-line help:

Usage of docker-ssh-exec:
-key string
path to key file (default "~/.ssh/id_rsa")
-port int
server receiving port (default 1067)
-server
run key server instead of command
-version
print version and exit
-wait int
client timeout, in seconds (default 3)

The software quits with a non-zero exit code (>100) on any error -- except a timeout from the keyserver, in which case it will just ignore the timeout and try to run the build command anyway. If the build command fails, `docker-ssh-exec` returns the exit code of the failed command.


----------------
Known Limitations / Bugs
----------------
The key data is limited to 4096 bytes.


----------------
Contribution / Development
----------------
This software was created by Benton Roberts _([email protected])_

To build it yourself, just `go get` and `go install` as usual:

go get github.com/mdsol/12factor-tools/docker-ssh-exec
cd $GOPATH/src/github.com/mdsol/12factor-tools/docker-ssh-exec
go install


--------
[1]: https://github.com/mdsol/12factor-tools/releases
151 changes: 151 additions & 0 deletions client.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
package main

import (
"flag"
"fmt"
"io/ioutil"
"net"
"os"
"os/exec"
"path/filepath"
"strconv"
"strings"
"syscall"
"time"
)

func client(config Config) {

// open send port
writeSocket := openUDPSocket(`w`, net.UDPAddr{
IP: net.IPv4(255, 255, 255, 255), // (broadcast IPv4)
Port: config.Port,
})
defer writeSocket.Close()

// open receive port on send port + 1
_, porttxt, _ := net.SplitHostPort(writeSocket.LocalAddr().String())
port, _ := strconv.Atoi(porttxt)
readSocket := openUDPSocket(`r`, net.UDPAddr{
IP: net.IPv4(0, 0, 0, 0),
Port: port + 1,
})
defer readSocket.Close()

// listen for reply: first start 2 channels: dataCh, and errCh
data, errors := make(chan []byte), make(chan error)
go func(dataCh chan []byte, errCh chan error) {
keyData := make([]byte, UDP_MSG_SIZE)
n, _, err := readSocket.ReadFromUDP(keyData)
if err != nil {
errCh <- err
}
dataCh <- keyData[0:n]
}(data, errors)

// send key request
fmt.Println("Broadcasting UDP key request...")
_, err := writeSocket.Write([]byte(KEY_REQUEST_TEXT))
if err != nil {
fmt.Println("ERROR sending key request: ", err)
os.Exit(101)
}

// now start the timeout channel
timeout := make(chan bool, 1)
go func() {
time.Sleep(time.Duration(config.Wait) * time.Second)
timeout <- true
}()

// now wait for a reply, an error, or a timeout
reply := ``
select {
case bytes := <-data:
reply = string(bytes)
if strings.HasPrefix(reply, `ERROR`) == true {
fmt.Println("Received error from server:", reply)
os.Exit(102)
}
fmt.Println("Got key from server.")
case err := <-errors:
fmt.Println("Error reading from receive port:", err)
os.Exit(103)
case <-timeout:
fmt.Println("WARNING: timed out waiting for response from key server.")
}

// create key dir and file
keyWritten := false // keep track of whether the key was written
if reply != `` {
fmt.Printf("Writing key to %s\n", config.KeyPath)
err = os.MkdirAll(filepath.Dir(config.KeyPath), 0700)
if err != nil {
fmt.Printf("ERROR creating directory %s: %s\n", config.KeyPath, err)
os.Exit(104)
}
err = ioutil.WriteFile(config.KeyPath, []byte(reply), 0600)
if err != nil {
fmt.Printf("ERROR writing keyfile %s: %s\n", config.KeyPath, err)
os.Exit(105)
}
keyWritten = true
}
// defer close and deletion of keyfile
// from here on, set exitCode and call return instead of os.Exit()
exitCode := 0
defer func() {
if keyWritten == true {
fmt.Printf("Deleting key file %s...\n", config.KeyPath)
if err := os.Remove(config.KeyPath); err != nil {
fmt.Printf("ERROR deleting keyfile '%s': %v\n",
config.KeyPath, err)
exitCode = 106
return
}
}
if exitCode != 0 {
os.Exit(exitCode)
}
}()

// run command
cmd := exec.Command(flag.Arg(0), flag.Args()[1:]...)
cmdText := strings.Join(flag.Args(), " ")
cmd.Stdin = os.Stdin
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
fmt.Println("Running command:", cmdText)
if err := cmd.Start(); err != nil {
fmt.Printf("ERROR starting command '%s': %v\n", cmdText, err)
exitCode = 107
return
}

if err = cmd.Wait(); err != nil {
if exiterr, ok := err.(*exec.ExitError); ok {
// The program has exited with an exit code != 0

// This works on both Unix and Windows. Although package
// syscall is generally platform dependent, WaitStatus is
// defined for both Unix and Windows and in both cases has
// an ExitStatus() method with the same signature.
if status, ok := exiterr.Sys().(syscall.WaitStatus); ok {
exitCode = status.ExitStatus()
fmt.Printf("ERROR: command '%s' exited with status %d\n",
cmdText, exitCode)
} else {
fmt.Printf("ERROR: command '%s' exited with unknown status",
cmdText)
exitCode = 108 // problem getting command's exit status?
}
return
} else {
fmt.Printf("ERROR waiting on command '%s': %v\n", cmdText, err)
exitCode = 109
return
}
}

fmt.Println("Command completed successfully.")
}
54 changes: 54 additions & 0 deletions config.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
package main

import (
"flag"
"fmt"
"os"
"path/filepath"
)

const DEFAULT_KEYPATH = `~/.ssh/id_rsa`

// Represents this app's possible configuration values
type Config struct {
KeyPath string
Server bool
Port int
Wait int
}

// Generates and returns a new Config based on the command-line
func newConfig() Config {
var (
keyArg = flag.String("key", DEFAULT_KEYPATH, "path to key file")
print_v = flag.Bool("version", false, "print version and exit")
server = flag.Bool("server", false, "run key server instead of command")
port = flag.Int("port", SERVER_RECV_PORT, "server receiving port")
wait = flag.Int("wait", CLIENT_TIMEOUT, "client timeout, in seconds")
)
flag.Parse()
if *print_v {
fmt.Printf("docker-ssh-exec version %s, built %s\n", VERSION, SOURCE_DATE)
os.Exit(0)
}
// check arguments for validity
if (len(flag.Args()) < 1) && (*server == false) {
fmt.Println("ERROR: A command to execute is required:",
" docker-ssh-exec [options] [command]")
os.Exit(1)
}
keyPath := *keyArg
if keyPath == DEFAULT_KEYPATH {
home := os.Getenv(`HOME`)
if home == `` {
home = `/root`
}
keyPath = filepath.Join(home, `.ssh`, `id_rsa`)
}
return Config{
Server: *server,
KeyPath: keyPath,
Port: *port,
Wait: *wait,
}
}
Loading

0 comments on commit 912ffa8

Please sign in to comment.