Skip to content

Latest commit

 

History

History
421 lines (320 loc) · 17.3 KB

README.md

File metadata and controls

421 lines (320 loc) · 17.3 KB

Decred Full Node for Docker

ISC License

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.

In a hurry? Skip to the Quick Start Guide.

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 (See further details)
  • The image is based on scratch (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

Quick Reference

The following are typical commands to get up and going quickly. The remaining sections describe things more in depth.

TIP: The commands throughout this document have you define and use shell 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 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 to 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:

    IMPORTANT: This MUST be run from the main directory of the dcrd code repo.

    $ DCRD_IMAGE_NAME="yourusername/dcrd"
    $ docker build -t "${DCRD_IMAGE_NAME}" -f contrib/docker/Dockerfile .

    To build a specific git tag or branch, the DCRD_BUILD_TAG argument can be specified. The argument is optional and defaults to master.

     $ docker build --build-arg DCRD_BUILD_TAG=release-v1.x.x \
         -t "${DCRD_IMAGE_NAME}" -f contrib/docker/Dockerfile .
    

    By default, the DCRD_BUILD_TAG will also specify the tag or branch of the dcrctl utility to be built, as well. It is feasible that dcrctl and dcrd don't use the same tags; if that's the case then the DCRCTL_BUILD_TAG can be used (on its own or alongside DCRD_BUILD_TAG):

     $ docker build --build-arg DCRCTL_BUILD_TAG=release-v1.x.x \
         -t "${DCRD_IMAGE_NAME}" -f contrib/docker/Dockerfile .
    
  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:

    NOTE: The data volume only needs to be created once.

    $ docker volume create decred-data
    $ DECRED_DATA_VOLUME=$(docker volume inspect decred-data -f '{{.Mountpoint}}')
    $ sudo chown -R 10000:10000 "${DECRED_DATA_VOLUME}"
  3. Run dcrd on mainnet 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 and exposing its peer-to-peer port:

    $ DCRD_MAINNET_P2P_PORT=9108
    $ DCRD_CONTAINER_NAME="dcrd"
    $ docker run -d --read-only \
      --name "${DCRD_CONTAINER_NAME}" \
      -v decred-data:/home/decred \
      -p ${DCRD_MAINNET_P2P_PORT}:${DCRD_MAINNET_P2P_PORT} \
      "${DCRD_IMAGE_NAME}" --altdnsnames "${DCRD_CONTAINER_NAME}"
  4. View the output logs of dcrd with the docker logs command:

    $ docker logs "${DCRD_CONTAINER_NAME}"
  5. Don't forget to configure any host and network firewalls to allow access to the peer-to-peer port and potentially setup port forwarding if the host is using Network Address Translation (NAT) if you want to allow inbound connections to contribute to network decentralization.

Querying dcrd with dcrctl Inside the Running Container

$ docker exec "${DCRD_CONTAINER_NAME}" dcrctl getblockchaininfo

Showing available dcrctl Commands Inside the Running Container

$ docker exec "${DCRD_CONTAINER_NAME}" dcrctl -l

TIP: The dcrctl utility interfaces with both dcrd and dcrwallet. Since the container only provides dcrd, which acts as a chain server, only the commands listed under "Chain Server Commands" are available.

Starting and Stopping the Container

$ docker stop -t 60 "${DCRD_CONTAINER_NAME}"
$ docker start "${DCRD_CONTAINER_NAME}"

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 Preliminaries

By default, Docker containers run as root which poses a security threat when many applications are deployed since any unknown vulerabilities in one application could potentially lead to an attacker gaining access to other applications. Morever, compromise of root priveleges inside a container that is part of a shared network can put the entire network at risk.

Further, containers with users that have user ids (UIDs) or group ids (GIDs) below 10000 is a security risk on several systems since a hypothetical attack which allows escalation of the container might otherwise coincide with an existing user's UID or existing group's GID which has additional permissions.

In order to avoid these types of security risks, this image runs as the non-root user decred with a static UID:GID of 10000:10000. This is important to keep in mind when creating and binding a volume to house the data since said volume will need to ensure the owner and group permissions are assigned to that UID and GID, respectively. Failure to assign the proper permissions will lead to write errors since the non-root user will not be able to write to the volume.

The primary method of interacting with a running instance of dcrd is accomplished by means of authenticated and encrypted remote procedure calls (RPCs). TLS is used to provide confidentiality, integrity, and authenticity.

By default, dcrd, and by extension this image, automatically configures its RPC server to use basic access authentication with a random username (rpcuser) and password (rpcpass) and generates a self-signed X.509 certificate, also known as the RPC certificate (rpccert), for TLS.

These credentials may or may not be needed depending on how you intend to use the image.

Another detail to be aware of is that most TLS clients verify the target server name of the running dcrd instance matches one of the DNS names listed in the certificate to help prevent man-in-the-middle attacks. The certificate that is automatically generated is populated by default with localhost entries along with the container ID of the container that generated it and its IP address at the time it was generated. Note that this means local authentication will always work without issue, but, since container IDs and docker IP addresses are ephemeral, this can lead to authentication failures for remote clients.

IMPORTANT: For this reason, it is HIGHLY recommended to start the container with a stable name and to provide that container name via either the --altdnsnames CLI parameter or the DCRD_ALT_DNSNAMES environment variable to prevent authentication failures from remote clients.

For example, assuming the environment variables and configuration matches what was outlined in the quick start section, running the container with the --altdnsnames CLI parameter:

$ docker run -d --read-only \
  --name "${DCRD_CONTAINER_NAME}" \
  -v decred-data:/home/decred \
  -p ${DCRD_MAINNET_P2P_PORT}:${DCRD_MAINNET_P2P_PORT} \
  "${DCRD_IMAGE_NAME}" --altdnsnames "${DCRD_CONTAINER_NAME}"

Usage

Interacting via RPC with dcrctl Using Local Authentication

The image provides the dcrctl utility for querying and controlling various aspects of the running instance of dcrd and automatically configures it to read the authentication credentials and TLS certificate from DECRED_DATA.

In other words, when dcrctl is running inside a container built with this image, no additional configuration is required to query the local dcrd instance. This is referred to as local authentication.

Assuming the environment variables and configuration matches what was outlined in the quick start section, the following example allows obtaining information about the state of the blockchain:

$ docker exec "${DCRD_CONTAINER_NAME}" dcrctl getblockchaininfo

A list of available dcrctl commands may be obtained as follows:

$ docker exec "${DCRD_CONTAINER_NAME}" dcrctl -l

TIP: The dcrctl utility interfaces with both dcrd and dcrwallet. Since the container only provides dcrd, which acts as a chain server, only the commands listed under "Chain Server Commands" are available.

Interacting via RPC with a Joined Docker Network

Applications running in a separate container that wish to interact with the RPC server may wish to join the network of the running dcrd container instance which effectively makes it as if both containers are running on the same host for the purposes of the network and thus can communicate via localhost.

For example, assuming the environment variables and configuration matches what was outlined in the quick start section, the following illustrates this technique by running dcrctl in a separate container while joining the network of the running dcrd container instance:

$ docker run --rm --network container:"${DCRD_CONTAINER_NAME}" --read-only \
  -v decred-data:/home/decred \
  "${DCRD_IMAGE_NAME}" dcrctl getblockchaininfo

Interacting via RPC with a User-Defined Docker Network

Another approach for running multiple applications in separate containers that wish to interact with the RPC server is by creating a user-defined Docker network and configuring all containers to use that network.

Note that all containers on the user-defined network will have their own IP addresses and thus from the point of view of the RPC server, the connections will appear as though they are coming from a remote machine.

This is important since, as described in the RPC Server Authentication section, most TLS clients verify the target server name of the running dcrd instance matches the DNS names listed in the certificate to help prevent man-in-the-middle attacks, so be sure to follow the instructions in that section to avoid authentication failures when using this approach.

For example, assuming the environment variables and configuration matches what was outlined in the quick start section, the following creating a user-defined Docker network. running a dcrd container attached to the user-defined network, and then running dcrctl in a separate container also attached to the user-defined network configured to talk to the remote dcrd RPC server:

NOTE: The network volume only needs to be created once.

$ docker network create decred
$ docker run -d --read-only \
  --network decred \
  --name "${DCRD_CONTAINER_NAME}" \
  -v decred-data:/home/decred \
  -p ${DCRD_MAINNET_P2P_PORT}:${DCRD_MAINNET_P2P_PORT} \
  "${DCRD_IMAGE_NAME}" --altdnsnames "${DCRD_CONTAINER_NAME}"
$ docker run --rm --read-only \
  --network decred \
  -v decred-data:/home/decred \
  "${DCRD_IMAGE_NAME}" dcrctl --rpcserver "${DCRD_CONTAINER_NAME}" getblockchaininfo

Accessing the RPC Server from Remote Services Outside of a Docker Network

The previously described techniques for interacting with the dcrd RPC server all make use of Docker's networking capabilities and rely on having access to the data volume in order to read the authentication credentials.

Any external applications that do not read the local authenticaion credentials or are not running in a Docker container will need to specify the RPC username (rpcuser) and password (rpcpass) as well as the RPC certificate (rpccert) for TLS.

Further, the RPC port will need to exposed from the container to be accessible outside of a Docker network.

For example, assuming the environment variables and configuration matches what was outlined in the quick start section, the following illustrates running a dcrd container that exposes the RPC port from the container as a port listening on localhost of the host machine, obtaining the authentication credentials and RPC certificate from the data volume, and then making a call via curl from the host machine:

# Run a dcrd container with the RPC port in the container mapped to localhost
# on the host machine.
#
# Note that you would need to map the port to the external interface of the
# host machine in order to access it from machines other than the host
# machine.  In other words, without the '127.0.0.1:' prefix that binds it
# to localhost.
$ DCRD_MAINNET_RPC_PORT=9109
$ docker run -d --read-only \
  --name "${DCRD_CONTAINER_NAME}" \
  -v decred-data:/home/decred \
  -p ${DCRD_MAINNET_P2P_PORT}:${DCRD_MAINNET_P2P_PORT} \
  -p 127.0.0.1:${DCRD_MAINNET_RPC_PORT}:${DCRD_MAINNET_RPC_PORT} \
  "${DCRD_IMAGE_NAME}" --altdnsnames "${DCRD_CONTAINER_NAME}"

# Acquire credentials from the data volume and issue RPC via curl.
#
# Notice that sudo is required here because the data volume must be configured
# with the permissions of the UID/GID inside the container which the local user
# on the host won't have access to.
$ 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:${DCRD_MAINNET_RPC_PORT}

Troubleshooting / Common Issues

Permission Denied Errors

Write permission issues will typically look similar to:

Error creating a default config file: mkdir /home/decred/.dcrd: permission denied
loadConfig: failed to create home directory: mkdir /home/decred/.dcrd: permission denied
exit status 1

As described in Non-Root User Permissions, this is the result of the non-root user inside of the container not having permissions to write to the data volume.

This can be resolved by changing the owner and group of the data volume bound to the container to match the non-root user inside the container.

For example:

$ DECRED_DATA_VOLUME=$(docker volume inspect decred-data -f '{{.Mountpoint}}')
$ sudo chown -R 10000:10000 "${DECRED_DATA_VOLUME}"

Remote Access Certificate Errors

Issues related to RPC certificate server verification will typically look similar to:

Post "https://dcrd:9109": x509: certificate is valid for a84fb1e0aa46, localhost, not dcrd
exit status 1

As described in RPC Server Authentication, most TLS clients verify the target server name of the running dcrd instance matches one of the DNS names listed in the certificate to help prevent man-in-the-middle attacks.

This issue means that the certificate does not have the target server name (or external IP address) listed as one of the authorized names.

In order to resolve the issue, the RPC certificate pair will need to be recreated with the appropriate authorized IP addresses and/or DNS names.

The easiest way to accomplish this is to delete the certificate pair from the data volume and run a new container instance of dcrd with either the --altdnsnames CLI parameter or the DCRD_ALT_DNSNAMES environment variable so a new certificate pair is automatically generated with the new values.

For example:

$ DECRED_DATA_VOLUME=$(docker volume inspect decred-data -f '{{.Mountpoint}}')
$ sudo rm "${DECRED_DATA_VOLUME}"/.dcrd/rpc.{cert,key}
$ docker run -d --read-only \
     --name "${DCRD_CONTAINER_NAME}" \
     -v decred-data:/home/decred \
     -p ${DCRD_MAINNET_P2P_PORT}:${DCRD_MAINNET_P2P_PORT} \
     "${DCRD_IMAGE_NAME}" --altdnsnames "${DCRD_CONTAINER_NAME}" \
     --altdnsnames example.com

Potential Future Work for Contributors

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