Skip to content
This repository was archived by the owner on Mar 24, 2025. It is now read-only.

Commit 912ffa8

Browse files
author
Benton Roberts
committed
first commit
0 parents  commit 912ffa8

File tree

8 files changed

+452
-0
lines changed

8 files changed

+452
-0
lines changed

.gitignore

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
# program output from databag-env
2+
*.env
3+
4+
# go build output
5+
databag-envdump/databag-envdump
6+
build12/build12
7+
release12/release12
8+
docker-ssh-exec/docker-ssh-exec
9+
10+
# goxc build output and local config
11+
pkg/
12+
*.goxc.local.json
13+
14+
# Compiled Object files, Static and Dynamic libs (Shared Objects)
15+
*.o
16+
*.a
17+
*.so
18+
19+
# Folders
20+
_obj
21+
_test
22+
23+
# Architecture specific extensions/prefixes
24+
*.[568vq]
25+
[568vq].out
26+
27+
*.cgo1.go
28+
*.cgo2.c
29+
_cgo_defun.c
30+
_cgo_gotypes.go
31+
_cgo_export.*
32+
33+
_testmain.go
34+
35+
*.exe
36+
*.test
37+
38+
tmp/

.goxc.json

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
{
2+
"ArtifactsDest": "./pkg",
3+
"Tasks": [
4+
"interpolate-source",
5+
"go-install",
6+
"xc",
7+
"copy-resources",
8+
"archive-zip",
9+
"archive-tar-gz",
10+
"rmbin"
11+
],
12+
"Arch": "amd64",
13+
"BuildConstraints": "linux",
14+
"PackageVersion": "0.5.1",
15+
"ConfigVersion": "0.9"
16+
}

Dockerfile

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
FROM busybox:latest
2+
3+
ADD pkg/docker-ssh-exec /docker-ssh-exec
4+
5+
ENTRYPOINT ["/docker-ssh-exec"]

README.md

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
docker-ssh-exec - Secure SSH key injection for Docker builds
2+
================
3+
Allows commands that require an SSH key to be run from within a `Dockerfile`, without leaving the key in the resulting image.
4+
5+
----------------
6+
Overview
7+
----------------
8+
This program runs in two different modes:
9+
10+
* a server mode, run as the Docker image `mdsol/docker-ssh-exec`, which transmits an SSH key on request to the the client; and
11+
* 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.
12+
13+
----------------
14+
Installation
15+
----------------
16+
To install the server, just pull it like any other Docker image.
17+
18+
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:
19+
20+
# install Medidata docker-ssh-exec build tool from S3 bucket "mybucket"
21+
curl https://s3.amazonaws.com/mybucket/docker-ssh-exec/\
22+
docker-ssh-exec_0.3.2_linux_amd64.tar.gz | \
23+
tar -xz --strip-components=1 -C /usr/local/bin \
24+
docker-ssh-exec_0.3.2_linux_amd64/docker-ssh-exec
25+
mkdir -p /root/.ssh && chmod 0700 /root/.ssh
26+
ssh-keyscan github.com >/root/.ssh/known_hosts
27+
28+
29+
----------------
30+
Usage
31+
----------------
32+
To run the server component, pass it the private half of your SSH key, either as a shared volume:
33+
34+
docker run -v ~/.ssh/id_rsa:/root/.ssh/id_rsa --name=keyserver -d \
35+
mdsol/docker-ssh-exec -server
36+
37+
or as an ENV var:
38+
39+
docker run -e DOCKER-SSH-KEY="$(cat ~/.ssh/id_rsa)" --name=keyserver -d \
40+
mdsol/docker-ssh-exec -server
41+
42+
Then, run a quick test of the client, to make sure it can get the key:
43+
44+
docker run --rm -it mdsol/docker-ssh-exec cat /root/.ssh/id_rsa
45+
46+
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:
47+
48+
docker-exec-ssh git clone git@github.com:my_user/my_private_repo.git
49+
50+
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.
51+
52+
Here's the command-line help:
53+
54+
Usage of docker-ssh-exec:
55+
-key string
56+
path to key file (default "~/.ssh/id_rsa")
57+
-port int
58+
server receiving port (default 1067)
59+
-server
60+
run key server instead of command
61+
-version
62+
print version and exit
63+
-wait int
64+
client timeout, in seconds (default 3)
65+
66+
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.
67+
68+
69+
----------------
70+
Known Limitations / Bugs
71+
----------------
72+
The key data is limited to 4096 bytes.
73+
74+
75+
----------------
76+
Contribution / Development
77+
----------------
78+
This software was created by Benton Roberts _(broberts@mdsol.com)_
79+
80+
To build it yourself, just `go get` and `go install` as usual:
81+
82+
go get github.com/mdsol/12factor-tools/docker-ssh-exec
83+
cd $GOPATH/src/github.com/mdsol/12factor-tools/docker-ssh-exec
84+
go install
85+
86+
87+
--------
88+
[1]: https://github.com/mdsol/12factor-tools/releases

client.go

Lines changed: 151 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,151 @@
1+
package main
2+
3+
import (
4+
"flag"
5+
"fmt"
6+
"io/ioutil"
7+
"net"
8+
"os"
9+
"os/exec"
10+
"path/filepath"
11+
"strconv"
12+
"strings"
13+
"syscall"
14+
"time"
15+
)
16+
17+
func client(config Config) {
18+
19+
// open send port
20+
writeSocket := openUDPSocket(`w`, net.UDPAddr{
21+
IP: net.IPv4(255, 255, 255, 255), // (broadcast IPv4)
22+
Port: config.Port,
23+
})
24+
defer writeSocket.Close()
25+
26+
// open receive port on send port + 1
27+
_, porttxt, _ := net.SplitHostPort(writeSocket.LocalAddr().String())
28+
port, _ := strconv.Atoi(porttxt)
29+
readSocket := openUDPSocket(`r`, net.UDPAddr{
30+
IP: net.IPv4(0, 0, 0, 0),
31+
Port: port + 1,
32+
})
33+
defer readSocket.Close()
34+
35+
// listen for reply: first start 2 channels: dataCh, and errCh
36+
data, errors := make(chan []byte), make(chan error)
37+
go func(dataCh chan []byte, errCh chan error) {
38+
keyData := make([]byte, UDP_MSG_SIZE)
39+
n, _, err := readSocket.ReadFromUDP(keyData)
40+
if err != nil {
41+
errCh <- err
42+
}
43+
dataCh <- keyData[0:n]
44+
}(data, errors)
45+
46+
// send key request
47+
fmt.Println("Broadcasting UDP key request...")
48+
_, err := writeSocket.Write([]byte(KEY_REQUEST_TEXT))
49+
if err != nil {
50+
fmt.Println("ERROR sending key request: ", err)
51+
os.Exit(101)
52+
}
53+
54+
// now start the timeout channel
55+
timeout := make(chan bool, 1)
56+
go func() {
57+
time.Sleep(time.Duration(config.Wait) * time.Second)
58+
timeout <- true
59+
}()
60+
61+
// now wait for a reply, an error, or a timeout
62+
reply := ``
63+
select {
64+
case bytes := <-data:
65+
reply = string(bytes)
66+
if strings.HasPrefix(reply, `ERROR`) == true {
67+
fmt.Println("Received error from server:", reply)
68+
os.Exit(102)
69+
}
70+
fmt.Println("Got key from server.")
71+
case err := <-errors:
72+
fmt.Println("Error reading from receive port:", err)
73+
os.Exit(103)
74+
case <-timeout:
75+
fmt.Println("WARNING: timed out waiting for response from key server.")
76+
}
77+
78+
// create key dir and file
79+
keyWritten := false // keep track of whether the key was written
80+
if reply != `` {
81+
fmt.Printf("Writing key to %s\n", config.KeyPath)
82+
err = os.MkdirAll(filepath.Dir(config.KeyPath), 0700)
83+
if err != nil {
84+
fmt.Printf("ERROR creating directory %s: %s\n", config.KeyPath, err)
85+
os.Exit(104)
86+
}
87+
err = ioutil.WriteFile(config.KeyPath, []byte(reply), 0600)
88+
if err != nil {
89+
fmt.Printf("ERROR writing keyfile %s: %s\n", config.KeyPath, err)
90+
os.Exit(105)
91+
}
92+
keyWritten = true
93+
}
94+
// defer close and deletion of keyfile
95+
// from here on, set exitCode and call return instead of os.Exit()
96+
exitCode := 0
97+
defer func() {
98+
if keyWritten == true {
99+
fmt.Printf("Deleting key file %s...\n", config.KeyPath)
100+
if err := os.Remove(config.KeyPath); err != nil {
101+
fmt.Printf("ERROR deleting keyfile '%s': %v\n",
102+
config.KeyPath, err)
103+
exitCode = 106
104+
return
105+
}
106+
}
107+
if exitCode != 0 {
108+
os.Exit(exitCode)
109+
}
110+
}()
111+
112+
// run command
113+
cmd := exec.Command(flag.Arg(0), flag.Args()[1:]...)
114+
cmdText := strings.Join(flag.Args(), " ")
115+
cmd.Stdin = os.Stdin
116+
cmd.Stdout = os.Stdout
117+
cmd.Stderr = os.Stderr
118+
fmt.Println("Running command:", cmdText)
119+
if err := cmd.Start(); err != nil {
120+
fmt.Printf("ERROR starting command '%s': %v\n", cmdText, err)
121+
exitCode = 107
122+
return
123+
}
124+
125+
if err = cmd.Wait(); err != nil {
126+
if exiterr, ok := err.(*exec.ExitError); ok {
127+
// The program has exited with an exit code != 0
128+
129+
// This works on both Unix and Windows. Although package
130+
// syscall is generally platform dependent, WaitStatus is
131+
// defined for both Unix and Windows and in both cases has
132+
// an ExitStatus() method with the same signature.
133+
if status, ok := exiterr.Sys().(syscall.WaitStatus); ok {
134+
exitCode = status.ExitStatus()
135+
fmt.Printf("ERROR: command '%s' exited with status %d\n",
136+
cmdText, exitCode)
137+
} else {
138+
fmt.Printf("ERROR: command '%s' exited with unknown status",
139+
cmdText)
140+
exitCode = 108 // problem getting command's exit status?
141+
}
142+
return
143+
} else {
144+
fmt.Printf("ERROR waiting on command '%s': %v\n", cmdText, err)
145+
exitCode = 109
146+
return
147+
}
148+
}
149+
150+
fmt.Println("Command completed successfully.")
151+
}

config.go

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
package main
2+
3+
import (
4+
"flag"
5+
"fmt"
6+
"os"
7+
"path/filepath"
8+
)
9+
10+
const DEFAULT_KEYPATH = `~/.ssh/id_rsa`
11+
12+
// Represents this app's possible configuration values
13+
type Config struct {
14+
KeyPath string
15+
Server bool
16+
Port int
17+
Wait int
18+
}
19+
20+
// Generates and returns a new Config based on the command-line
21+
func newConfig() Config {
22+
var (
23+
keyArg = flag.String("key", DEFAULT_KEYPATH, "path to key file")
24+
print_v = flag.Bool("version", false, "print version and exit")
25+
server = flag.Bool("server", false, "run key server instead of command")
26+
port = flag.Int("port", SERVER_RECV_PORT, "server receiving port")
27+
wait = flag.Int("wait", CLIENT_TIMEOUT, "client timeout, in seconds")
28+
)
29+
flag.Parse()
30+
if *print_v {
31+
fmt.Printf("docker-ssh-exec version %s, built %s\n", VERSION, SOURCE_DATE)
32+
os.Exit(0)
33+
}
34+
// check arguments for validity
35+
if (len(flag.Args()) < 1) && (*server == false) {
36+
fmt.Println("ERROR: A command to execute is required:",
37+
" docker-ssh-exec [options] [command]")
38+
os.Exit(1)
39+
}
40+
keyPath := *keyArg
41+
if keyPath == DEFAULT_KEYPATH {
42+
home := os.Getenv(`HOME`)
43+
if home == `` {
44+
home = `/root`
45+
}
46+
keyPath = filepath.Join(home, `.ssh`, `id_rsa`)
47+
}
48+
return Config{
49+
Server: *server,
50+
KeyPath: keyPath,
51+
Port: *port,
52+
Wait: *wait,
53+
}
54+
}

0 commit comments

Comments
 (0)