diff --git a/.env b/.env index 1cfb9d5287..0544281d29 100644 --- a/.env +++ b/.env @@ -153,8 +153,22 @@ MF_CERTS_SIGN_RSA_BITS=2048 MF_CERTS_VAULT_HOST= MF_CERTS_VAULT_PKI_PATH=pki_int MF_CERTS_VAULT_ROLE=agent -MF_CERTS_VAULT_TOKEN=s.nArgw6xn3uIOfA7nfKk8LFaW +MF_CERTS_VAULT_TOKEN= +### Vault +MF_VAULT_HOST=vault +MF_VAULT_PORT=8200 +MF_VAULT_UNSEAL_KEY_1= +MF_VAULT_UNSEAL_KEY_2= +MF_VAULT_UNSEAL_KEY_3= +MF_VAULT_TOKEN= +MF_VAULT_CA_NAME=mainflux +MF_VAULT_CA_ROLE_NAME=mainflux +MF_VAULT_CA_DOMAIN_NAME=mainflux.com +MF_VAULT_CA_OU='Mainflux Cloud' +MF_VAULT_CA_ORG='Mainflux Company' +MF_VAULT_CA_COUNTRY=Serbia +MF_VAULT_CA_LOC=BG ### LoRa MF_LORA_ADAPTER_LOG_LEVEL=debug diff --git a/docker/addons/vault/.gitignore b/docker/addons/vault/.gitignore new file mode 100644 index 0000000000..1269488f7f --- /dev/null +++ b/docker/addons/vault/.gitignore @@ -0,0 +1 @@ +data diff --git a/docker/addons/vault/README.md b/docker/addons/vault/README.md new file mode 100644 index 0000000000..02b400cf50 --- /dev/null +++ b/docker/addons/vault/README.md @@ -0,0 +1,100 @@ +This is Vault service deployment to be used with Mainflux. + +When the Vault service is started, some initialization steps need to be done to set things up. + +## Setup + +The following scripts are provided, which work on the running Vault service in Docker. + +1. `vault-init.sh` + +Calls `vault operator init` to perform the initial vault initialization and generates +a `data/secrets` file which contains the Vault unseal keys and root tokens. + +After this step, the corresponding Vault environment variables (`MF_VAULT_TOKEN`, `MF_VAULT_UNSEAL_KEY_1`, +`MF_VAULT_UNSEAL_KEY_2`, `MF_VAULT_UNSEAL_KEY_3`) should be updated in `.env` file. + +Example contents for `data/secrets`: + +``` +Unseal Key 1: Ay0YZecYJ2HVtNtXfPootXK5LtF+JZoDmBb7IbbYdLBI +Unseal Key 2: P6hb7x2cglv0p61jdLyNE3+d44cJUOFaDt9jHFDfr8Df +Unseal Key 3: zSBfDHzUiWoOzXKY1pnnBqKO8UD2MDLuy8DNTxNtEBFy +Unseal Key 4: 5oJuDDuMI0I8snaw/n4VLNpvndvvKi6JlkgOxuWXqMSz +Unseal Key 5: ZhsUkk2tXBYEcWgz4WUCHH9rocoW6qZoiARWlkE5Epi5 + +Initial Root Token: s.V2hdd00P4bHtUQnoWZK2hSaS + +Vault initialized with 5 key shares and a key threshold of 3. Please securely +distribute the key shares printed above. When the Vault is re-sealed, +restarted, or stopped, you must supply at least 3 of these keys to unseal it +before it can start servicing requests. + +Vault does not store the generated master key. Without at least 3 key to +reconstruct the master key, Vault will remain permanently sealed! + +It is possible to generate new unseal keys, provided you have a quorum of +existing unseal keys shares. See "vault operator rekey" for more information. +bash-4.4 + +Use 3 out of five keys presented and put it into .env file and than start the composition again Vault should be in unsealed state ( take a note that this is not recommended in terms of security, this is deployment for development) A real production deployment can use Vault auto unseal mode where vault gets unseal keys from some 3rd party KMS ( on AWS for example) +``` + +2. `vault-unseal.sh` + +This can be run after the initialization to unseal Vault, which is necessary for it to be used to store and/or get +secrets. + +The unseal environment variables need to be set in `.env` for the script to work. + +This script should not be necessary to run after the initial setup, since the Vault service unseals itself when +starting the container. + +3. `vault-set-pki.sh` + +This script is used to generate the root certificate, intermediate certificate and HTTPS server certificate. +After it runs, it copes the necessary certificates and keys to the `docker/ssl/certs` folder. + +The CA parameters are obtained from the environment variables starting with `MF_VAULT_CA` in `.env` file. + +## Vault CLI + +It can also be useful to run the Vault CLI for inspection and administration work. + +This can be done directly using the Vault image in Docker: `docker run -it mainflux/vault:latest vault` + +``` +Usage: vault [args] + +Common commands: + read Read data and retrieves secrets + write Write data, configuration, and secrets + delete Delete secrets and configuration + list List data or secrets + login Authenticate locally + agent Start a Vault agent + server Start a Vault server + status Print seal and HA status + unwrap Unwrap a wrapped secret + +Other commands: + audit Interact with audit devices + auth Interact with auth methods + debug Runs the debug command + kv Interact with Vault's Key-Value storage + lease Interact with leases + monitor Stream log messages from a Vault server + namespace Interact with namespaces + operator Perform operator-specific tasks + path-help Retrieve API help for paths + plugin Interact with Vault plugins and catalog + policy Interact with policies + print Prints runtime configurations + secrets Interact with secrets engines + ssh Initiate an SSH session + token Interact with tokens +``` + +### Vault Web UI + +The Vault Web UI is accessible by default on `http://localhost:8200/ui`. diff --git a/docker/addons/vault/config.hcl b/docker/addons/vault/config.hcl new file mode 100644 index 0000000000..192dd5af24 --- /dev/null +++ b/docker/addons/vault/config.hcl @@ -0,0 +1,10 @@ +storage "file" { + path = "/vault/file" +} + +listener "tcp" { + address = "0.0.0.0:8200" + tls_disable = 1 +} + +ui = true diff --git a/docker/addons/vault/docker-compose.yml b/docker/addons/vault/docker-compose.yml new file mode 100644 index 0000000000..e5b5be99e4 --- /dev/null +++ b/docker/addons/vault/docker-compose.yml @@ -0,0 +1,42 @@ +# Copyright (c) Mainflux +# SPDX-License-Identifier: Apache-2.0 + +# This docker-compose file contains optional Vault service for Mainflux platform. +# Since this is optional, this file is dependent of docker-compose file +# from /docker. In order to run these services, execute command: +# docker-compose -f docker/docker-compose.yml -f docker/addons/vault/docker-compose.yml up +# from project root. Vault default port (8200) is exposed, so you can use Vault CLI tool for +# vault inspection and administration, as well as access the UI. + +version: '3.7' + +networks: + docker_mainflux-base-net: + external: true + +volumes: + mainflux-vault-volume: + +services: + vault: + image: vault:latest + container_name: mainflux-vault + ports: + - ${MF_VAULT_PORT}:8200 + networks: + - docker_mainflux-base-net + volumes: + - mainflux-vault-volume:/vault/file + - mainflux-vault-volume:/vault/logs + - ./config.hcl:/vault/config/config.hcl + - ./entrypoint.sh:/entrypoint.sh + environment: + VAULT_ADDR: http://127.0.0.1:${MF_VAULT_PORT} + MF_VAULT_PORT: ${MF_VAULT_PORT} + MF_VAULT_UNSEAL_KEY_1: ${MF_VAULT_UNSEAL_KEY_1} + MF_VAULT_UNSEAL_KEY_2: ${MF_VAULT_UNSEAL_KEY_2} + MF_VAULT_UNSEAL_KEY_3: ${MF_VAULT_UNSEAL_KEY_3} + entrypoint: /bin/sh + command: /entrypoint.sh + cap_add: + - IPC_LOCK diff --git a/docker/addons/vault/entrypoint.sh b/docker/addons/vault/entrypoint.sh new file mode 100644 index 0000000000..69da9a7432 --- /dev/null +++ b/docker/addons/vault/entrypoint.sh @@ -0,0 +1,23 @@ +#!/usr/bin/dumb-init /bin/sh + +VAULT_CONFIG_DIR=/vault/config + +docker-entrypoint.sh server & +VAULT_PID=$! + +sleep 2 + +echo $MF_VAULT_UNSEAL_KEY_1 +echo $MF_VAULT_UNSEAL_KEY_2 +echo $MF_VAULT_UNSEAL_KEY_3 + +if [[ ! -z "${MF_VAULT_UNSEAL_KEY_1}" ]] && + [[ ! -z "${MF_VAULT_UNSEAL_KEY_2}" ]] && + [[ ! -z "${MF_VAULT_UNSEAL_KEY_3}" ]]; then + echo "Unsealing Vault" + vault operator unseal ${MF_VAULT_UNSEAL_KEY_1} + vault operator unseal ${MF_VAULT_UNSEAL_KEY_2} + vault operator unseal ${MF_VAULT_UNSEAL_KEY_3} +fi + +wait $VAULT_PID \ No newline at end of file diff --git a/docker/addons/vault/vault-init.sh b/docker/addons/vault/vault-init.sh new file mode 100755 index 0000000000..3143840722 --- /dev/null +++ b/docker/addons/vault/vault-init.sh @@ -0,0 +1,10 @@ +#!/bin/bash +set -euo pipefail + +vault() { + docker exec -it mainflux-vault vault "$@" +} + +mkdir -p data + +vault operator init 2>&1 | tee >(sed -r 's/\x1b\[[0-9;]*m//g' > data/secrets) diff --git a/docker/addons/vault/vault-set-pki.sh b/docker/addons/vault/vault-set-pki.sh new file mode 100755 index 0000000000..3a0cf3fe37 --- /dev/null +++ b/docker/addons/vault/vault-set-pki.sh @@ -0,0 +1,142 @@ +#!/bin/bash +set -euo pipefail + +scriptdir="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd )" +export MAINFLUX_DIR=$scriptdir/../../../ + +cd $scriptdir + +readDotEnv() { + set -o allexport + source $MAINFLUX_DIR/.env + set +o allexport +} + +vault() { + docker exec -it mainflux-vault vault "$@" +} + +vaultEnablePKI() { + vault secrets enable -path pki_${MF_VAULT_CA_NAME} pki + vault secrets tune -max-lease-ttl=87600h pki_${MF_VAULT_CA_NAME} +} + +vaultAddRoleToSecret() { + vault write pki_${MF_VAULT_CA_NAME}/roles/${MF_VAULT_CA_NAME} \ + allow_any_name=true \ + max_ttl="4300h" \ + default_ttl="4300h" \ + generate_lease=true +} + +vaultGenerateRootCACertificate() { + echo "Generate root CA certificate" + vault write -format=json pki_${MF_VAULT_CA_NAME}/root/generate/exported \ + common_name="\"$MF_VAULT_CA_DOMAIN_NAME CA Root\"" \ + ou="\"$MF_VAULT_CA_OU\""\ + organization="\"$MF_VAULT_CA_ORG\"" \ + country="\"$MF_VAULT_CA_COUNTRY\"" \ + locality="\"$MF_VAULT_CA_LOC\"" \ + ttl=87600h | tee >(jq -r .data.certificate >data/${MF_VAULT_CA_NAME}_ca.crt) \ + >(jq -r .data.issuing_ca >data/${MF_VAULT_CA_NAME}_issuing_ca.crt) \ + >(jq -r .data.private_key >data/${MF_VAULT_CA_NAME}_ca.key) +} + +vaultGenerateIntermediateCAPKI() { + echo "Generate Intermediate CA PKI" + export NAME_PKI_INT_PATH="pki_int_$MF_VAULT_CA_NAME" + vault secrets enable -path=${NAME_PKI_INT_PATH} pki + vault secrets tune -max-lease-ttl=43800h ${NAME_PKI_INT_PATH} +} + +vaultGenerateIntermediateCSR() { + echo "Generate intermediate CSR" + vault write -format=json ${NAME_PKI_INT_PATH}/intermediate/generate/exported \ + common_name="$MF_VAULT_CA_DOMAIN_NAME Intermediate Authority" \ + | tee >(jq -r .data.csr >data/${MF_VAULT_CA_NAME}_int.csr) \ + >(jq -r .data.private_key >data/${MF_VAULT_CA_NAME}_int.key) +} + +vaultSignIntermediateCSR() { + echo "Sign intermediate CSR" + docker cp data/${MF_VAULT_CA_NAME}_int.csr mainflux-vault:/vault/${MF_VAULT_CA_NAME}_int.csr + vault write -format=json pki_${MF_VAULT_CA_NAME}/root/sign-intermediate \ + csr=@/vault/${MF_VAULT_CA_NAME}_int.csr \ + | tee >(jq -r .data.certificate >data/${MF_VAULT_CA_NAME}_int.crt) \ + >(jq -r .data.issuing_ca >data/${MF_VAULT_CA_NAME}_int_issuing_ca.crt) +} + +vaultInjectIntermediateCertificate() { + echo "Inject Intermediate Certificate" + docker cp data/${MF_VAULT_CA_NAME}_int.crt mainflux-vault:/vault/${MF_VAULT_CA_NAME}_int.crt + vault write ${NAME_PKI_INT_PATH}/intermediate/set-signed certificate=@/vault/${MF_VAULT_CA_NAME}_int.crt +} + +vaultGenerateIntermediateCertificateBundle() { + echo "Generate intermediate certificate bundle" + cat data/${MF_VAULT_CA_NAME}_int.crt data/${MF_VAULT_CA_NAME}_ca.crt \ + > data/${MF_VAULT_CA_NAME}_int_bundle.crt +} + +vaultSetupIssuingURLs() { + echo "Setup URLs for CRL and issuing" + VAULT_ADDR=http://$MF_VAULT_HOST:$MF_VAULT_PORT + vault write ${NAME_PKI_INT_PATH}/config/urls \ + issuing_certificates="$VAULT_ADDR/v1/${NAME_PKI_INT_PATH}/ca" \ + crl_distribution_points="$VAULT_ADDR/v1/${NAME_PKI_INT_PATH}/crl" +} + +vaultSetupCARole() { + echo "Setup CA role" + vault write ${NAME_PKI_INT_PATH}/roles/${MF_VAULT_CA_ROLE_NAME} \ + allow_subdomains=true \ + allow_any_name=true \ + max_ttl="720h" +} + +vaultGenerateServerCertificate() { + echo "Generate server certificate" + vault write -format=json ${NAME_PKI_INT_PATH}/issue/${MF_VAULT_CA_ROLE_NAME} \ + common_name="$MF_VAULT_CA_DOMAIN_NAME" ttl="8670h" \ + | tee >(jq -r .data.certificate >data/${MF_VAULT_CA_DOMAIN_NAME}.crt) \ + >(jq -r .data.private_key >data/${MF_VAULT_CA_DOMAIN_NAME}.key) +} + +vaultCleanupFiles() { + docker exec mainflux-vault sh -c 'rm -rf /vault/*.{crt,csr}' +} + +if ! command -v jq &> /dev/null +then + echo "jq command could not be found, please install it and try again." + exit +fi + +readDotEnv + +mkdir -p data + +vault login ${MF_VAULT_TOKEN} + +vaultEnablePKI +vaultAddRoleToSecret +vaultGenerateRootCACertificate +vaultGenerateIntermediateCAPKI +vaultGenerateIntermediateCSR +vaultSignIntermediateCSR +vaultInjectIntermediateCertificate +vaultGenerateIntermediateCertificateBundle +vaultSetupIssuingURLs +vaultSetupCARole +vaultGenerateServerCertificate +vaultCleanupFiles + +echo "Copying certificate files" + +cp -v data/${MF_VAULT_CA_DOMAIN_NAME}.crt ${MAINFLUX_DIR}/docker/ssl/certs/mainflux-server.crt +cp -v data/${MF_VAULT_CA_DOMAIN_NAME}.key ${MAINFLUX_DIR}/docker/ssl/certs/mainflux-server.key +cp -v data/${MF_VAULT_CA_NAME}_int.key ${MAINFLUX_DIR}/docker/ssl/certs/ca.key +cp -v data/${MF_VAULT_CA_NAME}_int.crt ${MAINFLUX_DIR}/docker/ssl/certs/ca.crt +cp -v data/${MF_VAULT_CA_NAME}_int_bundle.crt ${MAINFLUX_DIR}/docker/ssl/bundle.pem + +exit 0 diff --git a/docker/addons/vault/vault-unseal.sh b/docker/addons/vault/vault-unseal.sh new file mode 100755 index 0000000000..9c8c069fbb --- /dev/null +++ b/docker/addons/vault/vault-unseal.sh @@ -0,0 +1,22 @@ + +#!/bin/bash +set -euo pipefail + +scriptdir="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd )" +export MAINFLUX_DIR=$scriptdir/../../../ + +readDotEnv() { + set -o allexport + source $MAINFLUX_DIR/.env + set +o allexport +} + +vault() { + docker exec -it mainflux-vault vault "$@" +} + +readDotEnv + +vault operator unseal ${MF_VAULT_UNSEAL_KEY_1} +vault operator unseal ${MF_VAULT_UNSEAL_KEY_2} +vault operator unseal ${MF_VAULT_UNSEAL_KEY_3}