diff --git a/README.md b/README.md index c688f0c..d627feb 100644 --- a/README.md +++ b/README.md @@ -1 +1,60 @@ -# local-ca +# Local ACME Certificate authority + +This repo builds a container that can be used in a docker-compose environment to create a disposable CA and issue/update certificates using certbot. + +It is heavily informed by the smallstep authors via https://github.com/smallstep/certificates/blob/master/docker/entrypoint.sh + +## Accessing the root cert + +The easiest way to obtain the cert for use validating other certs within the environment is to download the pem from the smallstep ca at the well known url: `https://step-ca:9000/roots.pem`. + +The next easiest way to obtain the cert is through mounting a docker volume which contains the certificate. See the docker-compose example below or follow the OpenCHAMI quickstart. + + +## Docker Compose Usage + +This container can be used with docker compose following this example: + +``` + step-ca: + container_name: step-ca + hostname: step-ca + image: ghcr.io/openchami/local-ca:v0.1.0 + ports: + - "9000:9000" + networks: + - openchami-certs + volumes: + - ./configs/step-ca/:/home/step + # Keeping the database in a volume improves performance. I don't understand why. + - step-ca-db:/home/step/db + # Keeping the root CA in a volume allows us to back it up and restore it. + - step-root-ca:/root-ca/ + environment: + # To initialize your CA, modify these environment variables + - STEPPATH=/home/step + - DOCKER_STEPCA_INIT_NAME=OpenCHAMI + - DOCKER_STEPCA_INIT_DNS_NAMES=localhost,step-ca + - DOCKER_STEPCA_INIT_ACME=true + healthcheck: + test: ["CMD", "step", "ca", "health", "--ca-url", "https://step-ca:9000", "--root", "/root-ca/root_ca.crt"] + interval: 10s + timeout: 10s + retries: 5 + certbot-issue-cert: + container_name: certbot + hostname: certbot + image: certbot/certbot:v2.10.0 + depends_on: + step-ca: + condition: service_healthy + environment: + - REQUESTS_CA_BUNDLE=/root-ca/root_ca.crt # This is the root CA certificate that we use to verify the local CA. + command: [ "certonly", "--webroot", "--server", "https://step-ca:9000/acme/acme/directory", "--webroot-path", "/var/www/html", "--agree-tos", "--email", "docker-compose@example.com", "-d", "openchami.bikeshack.dev", "-n" ] + networks: + - openchami-certs + volumes: + - local-certs:/etc/letsencrypt + - certbot-challenges:/var/www/html/ + - step-root-ca:/root-ca:ro +``` \ No newline at end of file diff --git a/local-ca/Dockerfile b/local-ca/Dockerfile index 8333ed4..2c2ee55 100644 --- a/local-ca/Dockerfile +++ b/local-ca/Dockerfile @@ -1,8 +1,23 @@ FROM cgr.dev/chainguard/wolfi-base #install step dependencies -RUN apk add wget step step-ca -#need a mountpoint on /mnt, with files password1 and password2 within the base dir -ENV STEPPATH=/mnt/ -COPY --chmod=555 init.sh /bin/init.sh -ENTRYPOINT init.sh +RUN apk add --no-cache wget step step-ca bash +ENV CONFIGPATH="/home/step/config/ca.json" +ENV PWDPATH="/home/step/secrets/password" +ENV STEPPATH="/home/step" + +RUN mkdir /root-ca + +VOLUME ["/home/step", "/root-ca"] + +# The entrypoint script will generate the certificate and export the root cert to the /root-ca volume + +STOPSIGNAL SIGTERM +HEALTHCHECK CMD step ca health 2>/dev/null | grep "^ok" >/dev/null + +COPY entrypoint.sh /entrypoint.sh + +EXPOSE 9000/TCP + +ENTRYPOINT ["/bin/bash", "/entrypoint.sh"] +CMD exec /usr/bin/step-ca --password-file $PWDPATH $CONFIGPATH diff --git a/local-ca/entrypoint.sh b/local-ca/entrypoint.sh new file mode 100644 index 0000000..a5c63da --- /dev/null +++ b/local-ca/entrypoint.sh @@ -0,0 +1,105 @@ +#!/bin/bash +set -eo pipefail + +# Adapted from the smallstep example entrypoint at: https://github.com/smallstep/certificates/blob/master/docker/entrypoint.sh + +# Paraphrased from: +# https://github.com/influxdata/influxdata-docker/blob/0d341f18067c4652dfa8df7dcb24d69bf707363d/influxdb/2.0/entrypoint.sh +# (a repo with no LICENSE.md) + +export STEPPATH=$(step path) + +# List of env vars required for step ca init +declare -ra REQUIRED_INIT_VARS=(DOCKER_STEPCA_INIT_NAME DOCKER_STEPCA_INIT_DNS_NAMES) + +# Ensure all env vars required to run step ca init are set. +function init_if_possible () { + local missing_vars=0 + for var in "${REQUIRED_INIT_VARS[@]}"; do + if [ -z "${!var}" ]; then + missing_vars=1 + fi + done + if [ ${missing_vars} = 1 ]; then + >&2 echo "there is no ca.json config file; please run step ca init, or provide config parameters via DOCKER_STEPCA_INIT_ vars" + else + step_ca_init "${@}" + fi +} + +function generate_password () { + set +o pipefail + < /dev/urandom tr -dc A-Za-z0-9 | head -c40 + echo + set -o pipefail +} + +# Initialize a CA if not already initialized +function step_ca_init () { + DOCKER_STEPCA_INIT_PROVISIONER_NAME="${DOCKER_STEPCA_INIT_PROVISIONER_NAME:-admin}" + DOCKER_STEPCA_INIT_ADMIN_SUBJECT="${DOCKER_STEPCA_INIT_ADMIN_SUBJECT:-step}" + DOCKER_STEPCA_INIT_ADDRESS="${DOCKER_STEPCA_INIT_ADDRESS:-:9000}" + + local -a setup_args=( + --name "${DOCKER_STEPCA_INIT_NAME}" + --dns "${DOCKER_STEPCA_INIT_DNS_NAMES}" + --provisioner "${DOCKER_STEPCA_INIT_PROVISIONER_NAME}" + --password-file "${STEPPATH}/password" + --provisioner-password-file "${STEPPATH}/provisioner_password" + --address "${DOCKER_STEPCA_INIT_ADDRESS}" + ) + if [ -n "${DOCKER_STEPCA_INIT_PASSWORD_FILE}" ]; then + cat < "${DOCKER_STEPCA_INIT_PASSWORD_FILE}" > "${STEPPATH}/password" + cat < "${DOCKER_STEPCA_INIT_PASSWORD_FILE}" > "${STEPPATH}/provisioner_password" + elif [ -n "${DOCKER_STEPCA_INIT_PASSWORD}" ]; then + echo "${DOCKER_STEPCA_INIT_PASSWORD}" > "${STEPPATH}/password" + echo "${DOCKER_STEPCA_INIT_PASSWORD}" > "${STEPPATH}/provisioner_password" + else + generate_password > "${STEPPATH}/password" + generate_password > "${STEPPATH}/provisioner_password" + fi + if [ "${DOCKER_STEPCA_INIT_SSH}" == "true" ]; then + setup_args=("${setup_args[@]}" --ssh) + fi + if [ "${DOCKER_STEPCA_INIT_ACME}" == "true" ]; then + setup_args=("${setup_args[@]}" --acme) + fi + if [ "${DOCKER_STEPCA_INIT_REMOTE_MANAGEMENT}" == "true" ]; then + setup_args=("${setup_args[@]}" --remote-management + --admin-subject "${DOCKER_STEPCA_INIT_ADMIN_SUBJECT}" + ) + fi + step ca init "${setup_args[@]}" + echo "" + if [ "${DOCKER_STEPCA_INIT_REMOTE_MANAGEMENT}" == "true" ]; then + echo "👉 Your CA administrative username is: ${DOCKER_STEPCA_INIT_ADMIN_SUBJECT}" + fi + echo "👉 Your CA administrative password is: $(< $STEPPATH/provisioner_password )" + echo "🤫 This will only be displayed once." + shred -u $STEPPATH/provisioner_password + mv $STEPPATH/password $PWDPATH + + # Copy the CA certificates to a volume that can be shared for future interaction with the CA + # First we put the root ca cert and intermediate cert in the easiest place to find it in the volume + cp /home/step/certs/root_ca.crt /root-ca/root_ca.crt + cp /home/step/certs/intermediate_ca.crt /root-ca/intermediate_ca.crt + # Then we set up the files in the right place for the step client to find them + mkdir -p /root-ca/step/certs + cp /home/step/certs/root_ca.crt /root-ca/step/certs/root_ca.crt + cp /home/step/certs/intermediate_ca.crt /root-ca/step/certs/intermediate_ca.crt + # Finally, we copy the step config files to the volume without exposing any secrets + mkdir -p /root-ca/step/config + cp /home/step/config/ca.json /root-ca/step/config/ca.json + cp /home/step/config/defaults.json /root-ca/step/config/defaults.json + echo "🔒 Your CA is ready to go!" +} + +if [ -f /usr/sbin/pcscd ]; then + /usr/sbin/pcscd +fi + +if [ ! -f "${STEPPATH}/config/ca.json" ]; then + init_if_possible +fi + +exec "${@}" \ No newline at end of file diff --git a/local-ca/init.sh b/local-ca/init.sh deleted file mode 100644 index 5061673..0000000 --- a/local-ca/init.sh +++ /dev/null @@ -1,8 +0,0 @@ -if ! [ -f /mnt/password1 -a -f /mnt/config/ca.json ] -then - echo $(tr -dc 'A-Za-z0-9!"#$%&'\''()*+,-./:;<=>?@[\]^_`{|}~' /mnt/password1 - echo $(tr -dc 'A-Za-z0-9!"#$%&'\''()*+,-./:;<=>?@[\]^_`{|}~' /mnt/password2 - step ca init --name="SI" --deployment-type standalone --password-file=/mnt/password1 --provisioner-password-file=/mnt/password2 --dns="SI.ca" --address=":443" --provisioner="admin" --remote-management --acme -fi -step-ca --password-file=/mnt/password1 /mnt/config/ca.json -