From 764ce711b67fdec01be885387ff6793275ccc996 Mon Sep 17 00:00:00 2001 From: Andrew Keesler Date: Tue, 18 Jan 2022 12:24:05 -0500 Subject: [PATCH 1/3] distroless: rewrite docker-entrypoint.sh in go See go doc ./cmd/docker-entrypoint for why. Signed-off-by: Andrew Keesler --- Dockerfile | 5 +- Makefile | 1 + cmd/docker-entrypoint/main.go | 92 +++++++++++++++++++++++ cmd/docker-entrypoint/main_test.go | 113 +++++++++++++++++++++++++++++ docker-entrypoint.sh | 32 -------- 5 files changed, 208 insertions(+), 35 deletions(-) create mode 100644 cmd/docker-entrypoint/main.go create mode 100644 cmd/docker-entrypoint/main_test.go delete mode 100755 docker-entrypoint.sh diff --git a/Dockerfile b/Dockerfile index f00e0456..68049867 100644 --- a/Dockerfile +++ b/Dockerfile @@ -54,13 +54,12 @@ COPY --from=builder /usr/local/src/dex/go.mod /usr/local/src/dex/go.sum /usr/loc COPY --from=builder /usr/local/src/dex/api/v2/go.mod /usr/local/src/dex/api/v2/go.sum /usr/local/src/dex/api/v2/ COPY --from=builder /go/bin/dex /usr/local/bin/dex +COPY --from=builder /go/bin/docker-entrypoint /usr/local/bin/docker-entrypoint COPY --from=builder /usr/local/src/dex/web /srv/dex/web COPY --from=gomplate /usr/local/bin/gomplate /usr/local/bin/gomplate USER 1001:1001 -COPY docker-entrypoint.sh /entrypoint.sh - -ENTRYPOINT ["/entrypoint.sh"] +ENTRYPOINT ["/usr/local/bin/docker-entrypoint"] CMD ["dex", "serve", "/etc/dex/config.docker.yaml"] diff --git a/Makefile b/Makefile index b92aee98..775e3316 100644 --- a/Makefile +++ b/Makefile @@ -48,6 +48,7 @@ bin/example-app: .PHONY: release-binary release-binary: generate @go build -o /go/bin/dex -v -ldflags $(LD_FLAGS) $(REPO_PATH)/cmd/dex + @go build -o /go/bin/docker-entrypoint -v -ldflags $(LD_FLAGS) $(REPO_PATH)/cmd/docker-entrypoint docker-compose.override.yaml: cp docker-compose.override.yaml.dist docker-compose.override.yaml diff --git a/cmd/docker-entrypoint/main.go b/cmd/docker-entrypoint/main.go new file mode 100644 index 00000000..0c507d17 --- /dev/null +++ b/cmd/docker-entrypoint/main.go @@ -0,0 +1,92 @@ +// Package main provides a utility program to launch the Dex container process with an optional +// templating step (provided by gomplate). +// +// This was originally written as a shell script, but we rewrote it as a Go program so that it could +// run as a raw binary in a distroless container. +package main + +import ( + "fmt" + "os" + "os/exec" + "strings" + "syscall" +) + +func main() { + // Note that this docker-entrypoint program is args[0], and it is provided with the true process + // args. + args := os.Args[1:] + + if err := run(args, realExec, realWhich); err != nil { + fmt.Println("error:", err.Error()) + os.Exit(1) + } +} + +func realExec(fork bool, args ...string) error { + if fork { + if output, err := exec.Command(args[0], args[1:]...).CombinedOutput(); err != nil { + return fmt.Errorf("cannot fork/exec command %s: %w (output: %q)", args, err, string(output)) + } + return nil + } + + argv0, err := exec.LookPath(args[0]) + if err != nil { + return fmt.Errorf("cannot lookup path for command %s: %w", args[0], err) + } + + if err := syscall.Exec(argv0, args, os.Environ()); err != nil { + return fmt.Errorf("cannot exec command %s (%q): %w", args, argv0, err) + } + + return nil +} + +func realWhich(path string) string { + fullPath, err := exec.LookPath(path) + if err != nil { + return "" + } + return fullPath +} + +func run(args []string, execFunc func(bool, ...string) error, whichFunc func(string) string) error { + if args[0] != "dex" && args[0] != whichFunc("dex") { + return execFunc(false, args...) + } + + if args[1] != "serve" { + return execFunc(false, args...) + } + + newArgs := []string{} + for _, tplCandidate := range args { + if hasSuffixes(tplCandidate, ".tpl", ".tmpl", ".yaml") { + tmpFile, err := os.CreateTemp("/tmp", "dex.config.yaml-*") + if err != nil { + return fmt.Errorf("cannot create temp file: %w", err) + } + + if err := execFunc(true, "gomplate", "-f", tplCandidate, "-o", tmpFile.Name()); err != nil { + return err + } + + newArgs = append(newArgs, tmpFile.Name()) + } else { + newArgs = append(newArgs, tplCandidate) + } + } + + return execFunc(false, newArgs...) +} + +func hasSuffixes(s string, suffixes ...string) bool { + for _, suffix := range suffixes { + if strings.HasSuffix(s, suffix) { + return true + } + } + return false +} diff --git a/cmd/docker-entrypoint/main_test.go b/cmd/docker-entrypoint/main_test.go new file mode 100644 index 00000000..c8aef169 --- /dev/null +++ b/cmd/docker-entrypoint/main_test.go @@ -0,0 +1,113 @@ +package main + +import ( + "strings" + "testing" +) + +type execArgs struct { + fork bool + argPrefixes []string +} + +func TestRun(t *testing.T) { + tests := []struct { + name string + args []string + execReturns error + whichReturns string + wantExecArgs []execArgs + wantErr error + }{ + { + name: "executable not dex", + args: []string{"tuna", "fish"}, + wantExecArgs: []execArgs{{fork: false, argPrefixes: []string{"tuna", "fish"}}}, + }, + { + name: "executable is full path to dex", + args: []string{"/usr/local/bin/dex", "marshmallow", "zelda"}, + whichReturns: "/usr/local/bin/dex", + wantExecArgs: []execArgs{{fork: false, argPrefixes: []string{"/usr/local/bin/dex", "marshmallow", "zelda"}}}, + }, + { + name: "command is not serve", + args: []string{"dex", "marshmallow", "zelda"}, + wantExecArgs: []execArgs{{fork: false, argPrefixes: []string{"dex", "marshmallow", "zelda"}}}, + }, + { + name: "no templates", + args: []string{"dex", "serve", "config.yaml.not-a-template"}, + wantExecArgs: []execArgs{{fork: false, argPrefixes: []string{"dex", "serve", "config.yaml.not-a-template"}}}, + }, + { + name: "no templates", + args: []string{"dex", "serve", "config.yaml.not-a-template"}, + wantExecArgs: []execArgs{{fork: false, argPrefixes: []string{"dex", "serve", "config.yaml.not-a-template"}}}, + }, + { + name: ".tpl template", + args: []string{"dex", "serve", "config.tpl"}, + wantExecArgs: []execArgs{ + {fork: true, argPrefixes: []string{"gomplate", "-f", "config.tpl", "-o", "/tmp/dex.config.yaml-"}}, + {fork: false, argPrefixes: []string{"dex", "serve", "/tmp/dex.config.yaml-"}}, + }, + }, + { + name: ".tmpl template", + args: []string{"dex", "serve", "config.tmpl"}, + wantExecArgs: []execArgs{ + {fork: true, argPrefixes: []string{"gomplate", "-f", "config.tmpl", "-o", "/tmp/dex.config.yaml-"}}, + {fork: false, argPrefixes: []string{"dex", "serve", "/tmp/dex.config.yaml-"}}, + }, + }, + { + name: ".yaml template", + args: []string{"dex", "serve", "some/path/config.yaml"}, + wantExecArgs: []execArgs{ + {fork: true, argPrefixes: []string{"gomplate", "-f", "some/path/config.yaml", "-o", "/tmp/dex.config.yaml-"}}, + {fork: false, argPrefixes: []string{"dex", "serve", "/tmp/dex.config.yaml-"}}, + }, + }, + } + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + var gotExecForks []bool + var gotExecArgs [][]string + fakeExec := func(fork bool, args ...string) error { + gotExecForks = append(gotExecForks, fork) + gotExecArgs = append(gotExecArgs, args) + return test.execReturns + } + + fakeWhich := func(_ string) string { return test.whichReturns } + + gotErr := run(test.args, fakeExec, fakeWhich) + if (test.wantErr == nil) != (gotErr == nil) { + t.Errorf("wanted error %s, got %s", test.wantErr, gotErr) + } + if !execArgsMatch(test.wantExecArgs, gotExecForks, gotExecArgs) { + t.Errorf("wanted exec args %+v, got %+v %+v", test.wantExecArgs, gotExecForks, gotExecArgs) + } + }) + } +} + +func execArgsMatch(wantExecArgs []execArgs, gotForks []bool, gotExecArgs [][]string) bool { + if len(wantExecArgs) != len(gotForks) { + return false + } + + for i := range wantExecArgs { + if wantExecArgs[i].fork != gotForks[i] { + return false + } + for j := range wantExecArgs[i].argPrefixes { + if !strings.HasPrefix(gotExecArgs[i][j], wantExecArgs[i].argPrefixes[j]) { + return false + } + } + } + + return true +} diff --git a/docker-entrypoint.sh b/docker-entrypoint.sh deleted file mode 100755 index bb12d313..00000000 --- a/docker-entrypoint.sh +++ /dev/null @@ -1,32 +0,0 @@ -#!/bin/sh -e - -### Usage: /docker-entrypoint.sh -function main() { - executable=$1 - command=$2 - - if [[ "$executable" != "dex" ]] && [[ "$executable" != "$(which dex)" ]]; then - exec $@ - fi - - if [[ "$command" != "serve" ]]; then - exec $@ - fi - - for tpl_candidate in $@ ; do - case "$tpl_candidate" in - *.tpl|*.tmpl|*.yaml) - tmp_file=$(mktemp /tmp/dex.config.yaml-XXXXXX) - gomplate -f "$tpl_candidate" -o "$tmp_file" - - args="${args} ${tmp_file}" - ;; - *) - args="${args} ${tpl_candidate}" - ;; - esac - done - exec $args -} - -main $@ From a672ff92882223601ceb20edde54778fc4545a92 Mon Sep 17 00:00:00 2001 From: Andrew Keesler Date: Tue, 18 Jan 2022 12:40:27 -0500 Subject: [PATCH 2/3] distroless: fetch CA certificates in builder stage ...so that we don't rely on a package manager to bring these down into the runner stage. Signed-off-by: Andrew Keesler --- Dockerfile | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Dockerfile b/Dockerfile index 68049867..7ce70100 100644 --- a/Dockerfile +++ b/Dockerfile @@ -2,7 +2,7 @@ FROM golang:1.17.6-alpine3.14 AS builder WORKDIR /usr/local/src/dex -RUN apk add --no-cache --update alpine-sdk +RUN apk add --no-cache --update alpine-sdk ca-certificates openssl ARG TARGETOS ARG TARGETARCH @@ -39,8 +39,8 @@ FROM alpine:3.15.0 # Proper installations should manage those certificates, but it's a bad user # experience when this doesn't work out of the box. # -# OpenSSL is required so wget can query HTTPS endpoints for health checking. -RUN apk add --no-cache --update ca-certificates openssl +# See https://go.dev/src/crypto/x509/root_linux.go for Go root CA bundle locations. +COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ca-certificates.crt RUN mkdir -p /var/dex RUN chown -R 1001:1001 /var/dex From 0394bf8ceab3806140b8645f89742552d4b53cda Mon Sep 17 00:00:00 2001 From: Andrew Keesler Date: Tue, 18 Jan 2022 19:40:28 -0500 Subject: [PATCH 3/3] distroless: Dockerfile works with distroless base image I can build this via: docker build --build-arg BASEIMAGE=gcr.io/distroless/static:latest -t andrew:distroless . Signed-off-by: Andrew Keesler --- Dockerfile | 18 +++++++++++------- Makefile | 2 +- 2 files changed, 12 insertions(+), 8 deletions(-) diff --git a/Dockerfile b/Dockerfile index 7ce70100..1a3117d4 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,3 +1,5 @@ +ARG BASEIMAGE=alpine:3.15.0 + FROM golang:1.17.6-alpine3.14 AS builder WORKDIR /usr/local/src/dex @@ -20,6 +22,12 @@ COPY . . RUN make release-binary +FROM alpine:3.15.0 AS stager + +RUN mkdir -p /var/dex +RUN mkdir -p /etc/dex +COPY config.docker.yaml /etc/dex/ + FROM alpine:3.15.0 AS gomplate ARG TARGETOS @@ -33,7 +41,7 @@ RUN wget -O /usr/local/bin/gomplate \ && chmod +x /usr/local/bin/gomplate -FROM alpine:3.15.0 +FROM $BASEIMAGE # Dex connectors, such as GitHub and Google logins require root certificates. # Proper installations should manage those certificates, but it's a bad user @@ -42,12 +50,8 @@ FROM alpine:3.15.0 # See https://go.dev/src/crypto/x509/root_linux.go for Go root CA bundle locations. COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ca-certificates.crt -RUN mkdir -p /var/dex -RUN chown -R 1001:1001 /var/dex - -RUN mkdir -p /etc/dex -COPY config.docker.yaml /etc/dex/ -RUN chown -R 1001:1001 /etc/dex +COPY --from=stager --chown=1001:1001 /var/dex /var/dex +COPY --from=stager --chown=1001:1001 /etc/dex /etc/dex # Copy module files for CVE scanning / dependency analysis. COPY --from=builder /usr/local/src/dex/go.mod /usr/local/src/dex/go.sum /usr/local/src/dex/ diff --git a/Makefile b/Makefile index 775e3316..8572091e 100644 --- a/Makefile +++ b/Makefile @@ -18,7 +18,7 @@ group=$(shell id -g -n) export GOBIN=$(PWD)/bin -LD_FLAGS="-w -X main.version=$(VERSION)" +LD_FLAGS="-w -X main.version=$(VERSION) -extldflags \"-static\"" # Dependency versions