-
Notifications
You must be signed in to change notification settings - Fork 13
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Benton Roberts
committed
Oct 27, 2015
0 parents
commit 912ffa8
Showing
8 changed files
with
452 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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/ |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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" | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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"] |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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.") | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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, | ||
} | ||
} |
Oops, something went wrong.