From 73e30a757fdb1bbb76f3923edc813f25c4505f80 Mon Sep 17 00:00:00 2001 From: Justin Cormack Date: Thu, 16 Mar 2017 11:29:21 +0000 Subject: [PATCH] Add a metadata handler for GCP which allows ssh login - this needs improvements to make it more "platform native", in particular GCP supports multiple users and more ssh key mangement options. - at present you can login as root with any platform ssh key - add support for uts=host and ipc=host - set the hostname from the metadata as well Signed-off-by: Justin Cormack --- base/metadata-gcp/.gitignore | 4 ++ base/metadata-gcp/Dockerfile | 3 ++ base/metadata-gcp/Makefile | 44 +++++++++++++++++++++ base/metadata-gcp/gcp.go | 75 ++++++++++++++++++++++++++++++++++++ config.go | 7 +++- examples/gcp.yaml | 25 ++++++++++++ examples/sshd.yaml | 1 + tools/riddler/riddler.sh | 10 ++++- 8 files changed, 167 insertions(+), 2 deletions(-) create mode 100644 base/metadata-gcp/.gitignore create mode 100644 base/metadata-gcp/Dockerfile create mode 100644 base/metadata-gcp/Makefile create mode 100644 base/metadata-gcp/gcp.go diff --git a/base/metadata-gcp/.gitignore b/base/metadata-gcp/.gitignore new file mode 100644 index 000000000..db2b4ca32 --- /dev/null +++ b/base/metadata-gcp/.gitignore @@ -0,0 +1,4 @@ +dev +proc +sys +usr diff --git a/base/metadata-gcp/Dockerfile b/base/metadata-gcp/Dockerfile new file mode 100644 index 000000000..f646e349c --- /dev/null +++ b/base/metadata-gcp/Dockerfile @@ -0,0 +1,3 @@ +FROM scratch +COPY . ./ +CMD ["/usr/bin/metadata-gcp"] diff --git a/base/metadata-gcp/Makefile b/base/metadata-gcp/Makefile new file mode 100644 index 000000000..a583a794b --- /dev/null +++ b/base/metadata-gcp/Makefile @@ -0,0 +1,44 @@ +GO_COMPILE=mobylinux/go-compile:3afebc59c5cde31024493c3f91e6102d584a30b9@sha256:e0786141ea7df8ba5735b63f2a24b4ade9eae5a02b0e04c4fca33b425ec69b0a + +SHA_IMAGE=alpine:3.5@sha256:dfbd4a3a8ebca874ebd2474f044a0b33600d4523d03b0df76e5c5986cb02d7e8 + +METADATA_BINARY=usr/bin/metadata-gcp + +IMAGE=metadata-gcp + +.PHONY: tag push clean container +default: push + +$(METADATA_BINARY): gcp.go + mkdir -p $(dir $@) + tar cf - $^ | docker run --rm --net=none --log-driver=none -i $(GO_COMPILE) -o $@ | tar xf - + +DIRS=dev proc sys +$(DIRS): + mkdir -p $@ + +DEPS=$(DIRS) $(METADATA_BINARY) + +container: Dockerfile $(DEPS) + tar cf - $^ | docker build --no-cache -t $(IMAGE):build - + +hash: Dockerfile $(DEPS) + find $^ -type f | xargs cat | docker run --rm -i $(SHA_IMAGE) sha1sum - | sed 's/ .*//' > hash + +push: hash container + docker pull mobylinux/$(IMAGE):$(shell cat hash) || \ + (docker tag $(IMAGE):build mobylinux/$(IMAGE):$(shell cat hash) && \ + docker push mobylinux/$(IMAGE):$(shell cat hash)) + docker rmi $(IMAGE):build + rm -f hash + +tag: hash container + docker pull mobylinux/$(IMAGE):$(shell cat hash) || \ + docker tag $(IMAGE):build mobylinux/$(IMAGE):$(shell cat hash) + docker rmi $(IMAGE):build + rm -f hash + +clean: + rm -rf hash $(DIRS) usr + +.DELETE_ON_ERROR: diff --git a/base/metadata-gcp/gcp.go b/base/metadata-gcp/gcp.go new file mode 100644 index 000000000..7a1e4ad80 --- /dev/null +++ b/base/metadata-gcp/gcp.go @@ -0,0 +1,75 @@ +package main + +import ( + "io/ioutil" + "log" + "net/http" + "strings" + "syscall" + "time" +) + +const ( + project = "http://metadata.google.internal/computeMetadata/v1/project/" + instance = "http://metadata.google.internal/computeMetadata/v1/instance/" +) + +// If optional not set, will panic. Optional will allow 404 +// We assume most failure cases are that this code is included in a non Google Cloud +// environment, which should generally be ok, so just fail fast. +func metadata(url string, optional bool) []byte { + var client = &http.Client{ + Timeout: time.Second * 2, + } + + req, err := http.NewRequest("", url, nil) + if err != nil { + log.Fatalf("http NewRequest failed: %v", err) + } + req.Header.Set("Metadata-Flavor", "Google") + + resp, err := client.Do(req) + if err != nil { + // Probably not running on Google Cloud but this package included + log.Fatalf("Could not contact Google Cloud Metadata service: %v", err) + } + if optional && resp.StatusCode == 404 { + return []byte{} + } + if resp.StatusCode != 200 { + // Probably not running on Google Cloud but this package included + log.Fatalf("Google Cloud Metadata Server http error: %s", resp.Status) + } + body, err := ioutil.ReadAll(resp.Body) + if err != nil { + log.Fatalf("Failed to read http response: %v", err) + } + return body +} + +func main() { + hostname := metadata(instance+"hostname", false) + err := syscall.Sethostname(hostname) + if err != nil { + log.Printf("Failed to set hostname: %v", err) + } + sshKeys := metadata(project+"attributes/sshKeys", true) + // TODO also retrieve the instance keys and respect block project keys see https://cloud.google.com/compute/docs/instances/ssh-keys + + // the keys have usernames attached, but as a simplification we are going to add them all to one root file + // TODO split them into individual user files and make the ssh container construct those users + + rootKeys := "" + for _, line := range strings.Split(string(sshKeys), "\n") { + parts := strings.SplitN(line, ":", 2) + // ignoring username for now + if len(parts) == 2 { + rootKeys = rootKeys + parts[1] + "\n" + } + } + + err = ioutil.WriteFile("/etc/ssh/authorized_keys", []byte(rootKeys), 0600) + if err != nil { + log.Printf("Failed to write ssh keys: %v", err) + } +} diff --git a/config.go b/config.go index 2a32c1002..db004a29c 100644 --- a/config.go +++ b/config.go @@ -46,10 +46,11 @@ type MobyImage struct { NetworkMode string `yaml:"network_mode"` Pid string Ipc string + Uts string ReadOnly bool `yaml:"read_only"` } -const riddler = "mobylinux/riddler:7d4545d8b8ac2700971a83f12a3446a76db28c14@sha256:11b7310df6482fc38aa52b419c2ef1065d7b9207c633d47554e13aa99f6c0b72" +const riddler = "mobylinux/riddler:8fe62ff02b9d28767554105b55d4613db3e77429@sha256:42b7f5c81fb85f8afb17548e00e5b81cfa4818c192e1199d61850491a178b0da" // NewConfig parses a config file func NewConfig(config []byte) (*Moby, error) { @@ -91,6 +92,10 @@ func ConfigToRun(order int, path string, image *MobyImage) []string { // TODO only "host" supported args = append(args, "--ipc="+image.Ipc) } + if image.Uts != "" { + // TODO only "host" supported + args = append(args, "--uts="+image.Uts) + } for _, bind := range image.Binds { args = append(args, "-v", bind) } diff --git a/examples/gcp.yaml b/examples/gcp.yaml index 7eec38df3..152f3acd3 100644 --- a/examples/gcp.yaml +++ b/examples/gcp.yaml @@ -17,6 +17,16 @@ system: - /proc/sys/fs/binfmt_misc:/binfmt_misc read_only: true command: [/usr/bin/binfmt, -dir, /etc/binfmt.d/, -mount, /binfmt_misc] + - name: metadata-gcp + image: "mobylinux/metadata-gcp:7fc3dd5ef92e0408fb3f76048bbaae88bbb55ad9" + binds: + - /tmp:/etc/ssh + - /etc/resolv.conf:/etc/resolv.conf + read_only: true + network_mode: host + uts: host + capabilities: + - CAP_SYS_ADMIN daemon: - name: rngd image: "mobylinux/rngd:3dad6dd43270fa632ac031e99d1947f20b22eec9@sha256:1c93c1db7196f6f71f8e300bc1d15f0376dd18e8891c8789d77c8ff19f3a9a92" @@ -25,6 +35,21 @@ daemon: oom_score_adj: -800 read_only: true command: [/bin/tini, /usr/sbin/rngd, -f] + - name: sshd + image: "mobylinux/sshd:3940f3fa07da1b6adab975112894b103b3c491f1" + capabilities: + - CAP_NET_BIND_SERVICE + - CAP_CHOWN + - CAP_SETUID + - CAP_SETGID + - CAP_DAC_OVERRIDE + - CAP_SYS_CHROOT + - CAP_KILL + network_mode: host + binds: + - /tmp/authorized_keys:/root/.ssh/authorized_keys + - /etc/resolv.conf:/etc/resolv.conf + pid: host - name: nginx image: "nginx:alpine" capabilities: diff --git a/examples/sshd.yaml b/examples/sshd.yaml index c17a06141..a16679470 100644 --- a/examples/sshd.yaml +++ b/examples/sshd.yaml @@ -31,6 +31,7 @@ daemon: - CAP_SETGID - CAP_DAC_OVERRIDE - CAP_SYS_CHROOT + - CAP_KILL network_mode: host binds: - /root/.ssh:/root/.ssh diff --git a/tools/riddler/riddler.sh b/tools/riddler/riddler.sh index 2557af53a..a8375ccae 100755 --- a/tools/riddler/riddler.sh +++ b/tools/riddler/riddler.sh @@ -30,9 +30,17 @@ docker rm $CONTAINER > /dev/null # remove user namespaces # --read-only sets /dev ro # /sysfs ro unless privileged - cannot detect so will do if grant all caps -# +# ipc, uts namespaces always isolated + +UTS="." +IPC="." +echo $ARGS | grep -q uts=host && UTS=".linux.namespaces = (.linux.namespaces|map(select(.type!=\"uts\")))" +echo $ARGS | grep -q ipc=host && IPC=".linux.namespaces = (.linux.namespaces|map(select(.type!=\"ipc\")))" + mv config.json config.json.orig cat config.json.orig | \ + jq "$UTS" | \ + jq "$IPC" | \ jq 'del(.process.rlimits)' | \ jq 'del (.linux.resources.memory.swappiness)' | \ jq 'del(.linux.uidMappings) | del(.linux.gidMappings) | .linux.namespaces = (.linux.namespaces|map(select(.type!="user")))' | \