diff --git a/Dockerfile b/Dockerfile deleted file mode 100644 index 04f8646113..0000000000 --- a/Dockerfile +++ /dev/null @@ -1,48 +0,0 @@ -FROM golang:1.17 - -# -# NOTE: The RPC server listens on localhost by default. -# If you require access to the RPC server, -# rpclisten should be set to an empty value. -# -# NOTE: When running simnet, you may not want to preserve -# the data and logs. This can be achieved by specifying -# a location outside the default ~/.dcrd. For example: -# rpclisten= -# simnet=1 -# datadir=~/simnet-data -# logdir=~/simnet-logs -# -# Example testnet instance with RPC server access: -# $ mkdir -p /local/path/dcrd -# -# Place a dcrd.conf into a local directory, i.e. /var/dcrd -# $ mv dcrd.conf /var/dcrd -# -# Verify basic configuration -# $ cat /var/dcrd/dcrd.conf -# rpclisten= -# testnet=1 -# -# Build the docker image -# $ docker build -t user/dcrd . -# -# Run the docker image, mapping the testnet dcrd RPC port. -# $ docker run -d --rm -p 127.0.0.1:19109:19109 -v /var/dcrd:/root/.dcrd user/dcrd -# - -WORKDIR /go/src/github.com/decred/dcrd -COPY . . - -RUN go install . ./cmd/... - -# mainnet -EXPOSE 9108 9109 - -# testnet -EXPOSE 19108 19109 - -# simnet -EXPOSE 18555 19556 - -CMD [ "dcrd" ] diff --git a/Dockerfile.alpine b/Dockerfile.alpine deleted file mode 100644 index cda4c773d5..0000000000 --- a/Dockerfile.alpine +++ /dev/null @@ -1,55 +0,0 @@ -# Build image -FROM golang:1.17 - -# -# NOTE: The RPC server listens on localhost by default. -# If you require access to the RPC server, -# rpclisten should be set to an empty value. -# -# NOTE: When running simnet, you may not want to preserve -# the data and logs. This can be achieved by specifying -# a location outside the default ~/.dcrd. For example: -# rpclisten= -# simnet=1 -# datadir=~/simnet-data -# logdir=~/simnet-logs -# -# Example testnet instance with RPC server access: -# $ mkdir -p /local/path/dcrd -# -# Place a dcrd.conf into a local directory, i.e. /var/dcrd -# $ mv dcrd.conf /var/dcrd -# -# Verify basic configuration -# $ cat /var/dcrd/dcrd.conf -# rpclisten= -# testnet=1 -# -# Build the docker image -# $ docker build -t user/dcrd -f Dockerfile.alpine . -# -# Run the docker image, mapping the testnet dcrd RPC port. -# $ docker run -d --rm -p 127.0.0.1:19109:19109 -v /var/dcrd:/root/.dcrd user/dcrd -# - -WORKDIR /go/src/github.com/decred/dcrd -COPY . . - -RUN CGO_ENABLED=0 GOOS=linux go install . ./cmd/... - -# Production image -FROM alpine:3.14.0 - -RUN apk add --no-cache ca-certificates -COPY --from=0 /go/bin/* /bin/ - -# mainnet -EXPOSE 9108 9109 - -# testnet -EXPOSE 19108 19109 - -# simnet -EXPOSE 18555 19556 - -CMD [ "dcrd" ] diff --git a/README.md b/README.md index cd6f516fb6..406f990e3d 100644 --- a/README.md +++ b/README.md @@ -155,7 +155,6 @@ https://decred.org/downloads/ ```sh $ git version ``` -
Windows Example @@ -184,52 +183,28 @@ https://decred.org/downloads/ Run the `dcrd` executable now installed in `$GOPATH/bin`.
-## Docker - -### Running dcrd - -You can run a decred node from inside a docker container. To build the image -yourself, use the following command: - -``` -docker build -t decred/dcrd . -``` - -Or you can create an alpine based image (requires Docker 17.05 or higher): - -``` -docker build -t decred/dcrd:alpine -f Dockerfile.alpine . -``` - -You can then run the image using: - -``` -docker run decred/dcrd -``` - -You may wish to use an external volume to customize your config and persist the -data in an external volume: - -``` -docker run --rm -v /home/user/dcrdata:/root/.dcrd/data decred/dcrd -``` - -For a minimal image, you can use the decred/dcrd:alpine tag. This is typically -a more secure option while also being a much smaller image. - -You can run `dcrctl` from inside the image. For example, run an image (mounting -your data from externally) with: - -``` -docker run --rm -ti --name=dcrd-1 -v /home/user/.dcrd:/root/.dcrd \ - decred/dcrd:alpine -``` - -And then run `dcrctl` commands against it. For example: - -``` -docker exec -ti dcrd-1 dcrctl getbestblock -``` +## Building and Running OCI Containers (aka Docker/Podman) + +The project does not officially provide container images. However, all of the +necessary files to build your own lightweight non-root container image based on +`scratch` from the latest source code are available in +[contrib/docker](./contrib/docker/README.md). + +It is also worth noting that, to date, most users typically prefer to run `dcrd` +directly, without using a container, for at least a few reasons: + +- `dcrd` is a static binary that does not require root privileges and therefore + does not suffer from the usual deployment issues that typically make + containers attractive +- It is harder and more verbose to run `dcrd` from a container as compared to + normal: + - `dcrd` is designed to automatically create a working default configuration + which means it just works out of the box without the need for additional + configuration for almost all typical users + - The blockchain data and configuration files need to be persistent which + means configuring and managing a docker data volume + - Running non-root containers with `docker` requires special care in regards + to permissions ## Running Tests diff --git a/contrib/README.md b/contrib/README.md index 387c6e4333..0738e0da6d 100644 --- a/contrib/README.md +++ b/contrib/README.md @@ -38,3 +38,10 @@ if desired. See the full [Simulation Network Reference](../docs/simnet_environment.mediawiki) for more details. + +### Building and Running OCI Containers (aka Docker/Podman) + +The project does not officially provide container images. However, all of the +necessary files to build your own lightweight non-root container image based on +`scratch` from the latest source code are available in the docker directory. +See [docker/README.md](./docker/README.md) for more details. diff --git a/contrib/docker/Dockerfile b/contrib/docker/Dockerfile new file mode 100644 index 0000000000..90ddd71963 --- /dev/null +++ b/contrib/docker/Dockerfile @@ -0,0 +1,83 @@ + +# TODO: Needs some documentation here about the RPC server, logging via docker +# logs, mounting a volume, the conf file, etc... + +############### +# Builder Stage +############### + +# Basic Go environment with git, SSL CA certs, and upx. +# golang:1.17.1-alpine (linux/amd64) +FROM golang@sha256:13919fb9091f6667cb375d5fdf016ecd6d3a5d5995603000d422b04583de4ef9 AS builder +RUN apk add --no-cache git ca-certificates upx + +# Empty directory to be copied into place in the production image since it will +# run as a non-root container and thus not have permissions to create +# directories or change ownership of anything outside of the structure already +# created for it. +RUN mkdir /emptydatadir + +# New unprivileged user for use in production image below to improve security. +ENV USER=decred +ENV UID=10000 +RUN adduser \ + --disabled-password \ + --gecos "" \ + --home="/home/${USER}" \ + --shell "/sbin/nologin" \ + --no-create-home \ + --uid "${UID}" \ + "${USER}" + +# Build dcrd and other commands it provides +WORKDIR /go/src/github.com/decred/dcrd +RUN git clone https://github.com/decred/dcrd . && \ + CGO_ENABLED=0 GOOS=linux \ + go install -trimpath -tags safe,netgo,timetzdata \ + -ldflags="-s -w" \ + . ./cmd/gencerts ./cmd/promptsecret + +# Build dcrctl +WORKDIR /go/src/github.com/decred/dcrctl +RUN git clone https://github.com/decred/dcrctl . && \ + CGO_ENABLED=0 GOOS=linux \ + go install -trimpath -tags safe,netgo -ldflags="-s -w" + +# Build entrypoint helper for the production image. +WORKDIR /go/src/github.com/decred/dcrd/contrib/docker/entrypoint +COPY ./contrib/docker/entrypoint/entrypoint.go . +RUN go mod init entrypoint && \ + go mod tidy && \ + CGO_ENABLED=0 GOOS=linux \ + go install -trimpath -tags netgo,timetzdata -ldflags="-s -w" . + +# Compress bins +RUN upx -9 /go/bin/* + +################## +# Production image +################## + +# Minimal scratch-based environment. +FROM scratch +ENV DECRED_DATA=/home/decred +#ENV DCRD_EXPOSE_RPC=false # TODO: Want something like this? +ENV DCRD_NO_FILE_LOGGING=true +COPY --from=builder /etc/passwd /etc/passwd +COPY --from=builder /etc/group /etc/group +COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ +COPY --from=builder /go/bin/* /bin/ +COPY --from=builder --chown=decred /emptydatadir /tmp + +# Use an unprivileged user. +USER decred + +# Ports for the p2p and json-rpc of mainnet, testnet, and simnet, respectively. +EXPOSE 9108 9109 19108 19109 18555 19556 + +ENTRYPOINT [ "/bin/entrypoint" ] + +RUN [ "dcrd", "--version" ] + +# TODO: Want this or not? I've seen conflicting info and I'm not a docker expert... +#VOLUME [ "/home/decred" ] diff --git a/contrib/docker/README.md b/contrib/docker/README.md new file mode 100644 index 0000000000..44d6b631c0 --- /dev/null +++ b/contrib/docker/README.md @@ -0,0 +1,205 @@ +Decred Full Node for Docker +=========================== + +[![ISC License](https://img.shields.io/badge/license-ISC-blue.svg)](http://copyfree.org) + +## Overview + +This provides all of the necessary files to build your own lightweight non-root +container image based on `scratch` that provides `dcrd`, `dcrctl`, +`promptsecret` and `gencerts`. + +The approach used by the primary `Dockerfile` is to employ a multi-stage build +that downloads and builds the latest source code, compresses the resulting +binaries, and then produces a final image based on `scratch` that only includes +the Decred-specific binaries. + +### Container Image Security Properties + +The provided `Dockerfile` places a strong focus on security as follows: + +- 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 + +### Container Environment Variables + +- `DECRED_DATA` (Default: `/home/decred`): + The directory where data is stored inside the container. This typically does + not need to be changed. + +- `DCRD_NO_FILE_LOGGING` (Default: `true`): + Controls whether or not dcrd additionally logs to files under `DECRED_DATA`. + Logging is only done via stdout by default in the container since that is + standard practice for containers. + +- `DCRD_ALT_DNSNAMES`: (Default: None) + Adds alternate server DNS names to the server certificate that is automtically + generated for the RPC server. This is important when attempting to access the + RPC from external sources because TLS is required and clients verify the + server name matches the certificate. + +## Usage + +### Quick Start + +The following are typical commands to get up and going quickly. The remaining +sections describe things more in depth. + +**Note:** These series of commands have you define and use environment variables +in order to help make it clear exactly what every command line argument refers +to. However, this means that if you close the shell, the commands will no +longer work as written because those environment variables will no longer exist. +You may wish to replace all instances of `"${...}"` with the associated concrete +value. + +1. Build the base image with a tag to make it easy ro reference later. These + commands all use `yourusername/dcrd` for the image tag, but you should + replace `yourusername` with your username or something else unique to you so + you can easily identify it as being one of your images: + + ```sh + $ DCRD_IMAGE_NAME="yourusername/dcrd" + $ docker build -t "${DCRD_IMAGE_NAME}" -f contrib/docker/Dockerfile . + ``` + + **NOTE: This MUST be run from the main directory of the dcrd code repo.** + +2. Create a data volume and change its ownership to the user id of the user + inside of the container so it has the necessary permissions to write to it: + + ```sh + $ docker volume create decred-data + $ DECRED_DATA_VOLUME=$(docker volume inspect decred-data -f '{{.Mountpoint}}') + $ sudo chown -R 10000:10000 "${DECRED_DATA_VOLUME}" + ``` + + **NOTE: The data volume only needs to be created once.** + +3. Run `dcrd` on `testnet` in the background using the aforementioned data + volume to store the blockchain and configuration data along with a name to + make it easy to reference later: + + ```sh + $ DCRD_CONTAINER_NAME="dcrd-testnet" + $ docker run -d --read-only \ + --name "${DCRD_CONTAINER_NAME}" \ + -v decred-data:/home/decred \ + "${DCRD_IMAGE_NAME}" --testnet --altdnsnames "${DCRD_CONTAINER_NAME}" + ``` + +4. View the output logs of `dcrd` with the docker logs command: + + ```sh + $ docker logs "${DCRD_CONTAINER_NAME}" + ``` + +### Starting and Stopping the Container + + ```sh + $ docker stop "${DCRD_CONTAINER_NAME}" + $ docker start "${DCRD_CONTAINER_NAME}" + ``` + +### Preliminaries + +TODO: Explain about non-root permissions, network, etc + +### Basics + +TODO: Finish documenting all the details here + +- [Dockerfile](./Dockerfile) + Provides a user-contributed configuration file for building a container image + that consists of `dcrd`, `dcrctl`, `gencerts`, and `promptsecret` along with + exposed ports for dcrd's RPC server. It is based on `scratch` and runs as a + non-root container. + +TODO: It would probably be nice to provide some variants such as: +- `Dockerfile.release` that either grabs the latest release code or checks out the + latest release tag instead of building the master branch +- `Dockerfile.local` that builds an image using the code in the build context + instead of cloning and building the latest master branch + +### Interacting via RPC + +#### TODO: With shared network... + +Assuming the environment variables and configuration matches what was outlined +in the quick start section: + +```sh +$ docker run --rm --network container:"${DCRD_CONTAINER_NAME}" --read-only \ + -v decred-data:/home/decred \ + "${DCRD_IMAGE_NAME}" dcrctl --testnet getblockchaininfo +``` + +#### TODO: With user-defined network... + +Assuming the environment variables and configuration matches what was outlined +in the quick start section: + +TODO: Would need to remove existing container and start new one with `--network decred` as follows... + +**NOTE: The network volume only needs to be created once.** + +```sh +$ docker network create decred +$ docker stop "${DCRD_CONTAINER_NAME}" +$ docker rm "${DCRD_CONTAINER_NAME}" +$ docker run -d --read-only \ + --network decred \ + --name "${DCRD_CONTAINER_NAME}" \ + -v decred-data:/home/decred \ + "${DCRD_IMAGE_NAME}" --testnet --altdnsnames "${DCRD_CONTAINER_NAME}" +$ docker run --rm --read-only \ + --network decred \ + -v decred-data:/home/decred \ + "${DCRD_IMAGE_NAME}" dcrctl --testnet --rpcserver "${DCRD_CONTAINER_NAME}" getblockchaininfo +``` + +#### TODO: Accessing the RPC server from remote services outside of a docker network + +TODO: Needs to be running with port mapped...aka: + +```sh +$ docker run -d --read-only \ + --name "${DCRD_CONTAINER_NAME}" \ + -v decred-data:/home/decred \ + -p 127.0.0.1:19109:19109 \ + "${DCRD_IMAGE_NAME}" --testnet --altdnsnames "${DCRD_CONTAINER_NAME}" +``` + +TODO: From other machine such as the host machine (not inside a docker container)... +TODO: sudo required here because the data volume needs the perms of the uid/gid + inside the container which the local user on the host won't have access too.. + could alternatively use `docker cp` to copy the cert and conf file out of the container... + +```sh +$ dcrdrpcuser=$(sudo cat "${DECRED_DATA_VOLUME}/.dcrd/dcrd.conf" | grep rpcuser= | cut -c9-) +$ dcrdrpcpass=$(sudo cat "${DECRED_DATA_VOLUME}/.dcrd/dcrd.conf" | grep rpcpass= | cut -c9-) +$ sudo curl --cacert "${DECRED_DATA_VOLUME}/.dcrd/rpc.cert" --user "${dcrdrpcuser}:${dcrdrpcpass}" \ + --data-binary '{"jsonrpc":"1.0","id":"1","method":"getbestblock","params":[]}' \ + https://127.0.0.1:19109 +``` + +## Troubleshooting / Common Issues + +TODO + +### TODO: Write permission errors + +TODO + +### TODO: Remote access certificate errors + + +TODO + diff --git a/contrib/docker/entrypoint/entrypoint.go b/contrib/docker/entrypoint/entrypoint.go new file mode 100644 index 0000000000..4ebc8d1ff3 --- /dev/null +++ b/contrib/docker/entrypoint/entrypoint.go @@ -0,0 +1,113 @@ +// 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()) + } +}