dcrd/contrib/docker/entrypoint/entrypoint.go
Dave Collins 563e89099e
build/contrib: Improve docker support.
This significantly reworks the docker support to provide build a
lightweight non-root distroless container image based on scratch.  It
employs a multi-stage build that downloads and builds the latest source
code, compresses the resulting binaries, and then produces the final
image based on scratch that only includes the Decred-specific binaries.

It must be noted that there are some still remaining TODO items in the
documentation as well as the Dockerfile that will need to be handled by
a future commit, but the changes are being submitted now to allow
another contributor to finish up those aspects.

The following is an overview of the changes:

- Removes existing Dockerfile and Dockerfile.alpine
- Introduces a new Dockerfile under contrib/docker with the following
  properties:
  - Runs as a non-root user
  - Uses a static UID:GID of 10000:10000
    - Note that using UIDs/GIDs below 10000 for container users is a
      security risk on several systems since a hypothetical attack which
      allows escalation outside of the container might otherwise
      coincide with an existing user's UID or existing group's GID which
      has additional permissions
  - The image is based on scratch image (aka completely empty) and only
    includes the Decred-specific binaries which means there is no shell
    or any other binaries available if an attacker were to somehow
    manage to find a remote execution vulnerability exploit in a Decred
    binary
- Introduces code to build an entrypoint for the image since it is based
  on scratch and thus has no shell for that purpose
- Adds contrib/docker/README.md
- Updates README.md in the main directory to account for changes
  - There is still outstanding work to be done here and thus has several
    TODOs
- Updates contrib/README.md to call out the new addition
2021-09-18 13:17:41 -05:00

114 lines
3.4 KiB
Go

// Copyright (c) 2021 The Decred developers
// Use of this source code is governed by an ISC
// license that can be found in the LICENSE file.
package main
import (
"fmt"
"os"
"os/exec"
"path/filepath"
)
const (
// defaultApp is the default application assumed when either no arguments
// are specified or the first argument starts with a -.
defaultApp = "dcrd"
)
// argN either returns the arguments at the provided position within the given
// args array when it exists or an empty string otherwise.
func argN(args []string, n int) string {
if len(args) > n {
return args[n]
}
return ""
}
// prepend return a new slice that consists of the provided value followed by
// the given args.
func prepend(args []string, val string) []string {
newArgs := make([]string, 0, len(args)+1)
newArgs = append(newArgs, val)
newArgs = append(newArgs, args...)
return newArgs
}
// fileExists reports whether the named file or directory exists.
func fileExists(name string) bool {
if _, err := os.Stat(name); err != nil {
if os.IsNotExist(err) {
return false
}
}
return true
}
func main() {
// Name of the invoking executable. This should typically be "entrypoint".
exeName := filepath.Base(os.Args[0])
// Local copy of supplied arguments without the invoking process. This
// allows the params to be modified independently below as needed.
args := make([]string, len(os.Args)-1)
copy(args, os.Args[1:])
// Assume the provided arguments are for default app when the first
// parameter starts with a dash.
if arg0 := argN(args, 0); arg0 == "" || arg0[0] == '-' {
fmt.Printf("%s: assuming arguments for %s\n", exeName, defaultApp)
args = prepend(args, defaultApp)
}
// Additional setup when running in a container.
arg0 := argN(args, 0)
args = args[1:]
switch arg0 {
case "dcrd":
// Determine the app data directory based on environment variable.
decredData := os.Getenv("DECRED_DATA")
dcrdAppData := filepath.Join(decredData, ".dcrd")
// TODO: Recognize t/true/1, f/false/0
if os.Getenv("DCRD_NO_FILE_LOGGING") != "false" {
args = append(args, "--nofilelogging")
}
args = append(args, fmt.Sprintf("--appdata=%s", dcrdAppData))
args = append(args, "--rpclisten=")
case "dcrctl":
// Determine the app data directories based on environment variable.
decredData := os.Getenv("DECRED_DATA")
dcrdAppData := filepath.Join(decredData, ".dcrd")
rpcCert := filepath.Join(dcrdAppData, "rpc.cert")
dcrctlAppData := filepath.Join(decredData, ".dcrctl")
dcrctlConfig := filepath.Join(dcrctlAppData, "dcrctl.conf")
// TODO: These all unconditionally override config file settings.
// Detect if already there and don't do it?
//
// Prepend the arguments in case the caller wants to override them.
args = prepend(args, fmt.Sprintf("--rpccert=%s", rpcCert))
args = prepend(args, fmt.Sprintf("--configfile=%s", dcrctlConfig))
// Change the home directory to match the data path since dcrctl
// relies in it to discover the dcrd config file in order to extract
// the rpc credentials.
if !fileExists(filepath.Join(dcrctlAppData, "dcrctl.conf")) {
os.Setenv("HOME", decredData)
}
}
// Run the command with the given arguments while redirecting stdin, stdout,
// and stderr to the parent process.
cmd := exec.Command(arg0, args...)
cmd.Stdin = os.Stdin
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
if err := cmd.Run(); err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(cmd.ProcessState.ExitCode())
}
}