diff --git a/compose/services.yml b/compose/services.yml index 74f5db6..6c8feba 100644 --- a/compose/services.yml +++ b/compose/services.yml @@ -134,6 +134,10 @@ services: SENTRY_DSN: VPN_HAPROXY_USEPROXYPROTOCOL: 'true' VPN_SERVICE_API_KEY: ${OPENBALENA_VPN_SERVICE_API_KEY} + VPN_OPENVPN_CA_CRT: ${OPENBALENA_VPN_CA} + VPN_OPENVPN_SERVER_CRT: ${OPENBALENA_VPN_SERVER_CRT} + VPN_OPENVPN_SERVER_KEY: ${OPENBALENA_VPN_SERVER_KEY} + VPN_OPENVPN_SERVER_DH: ${OPENBALENA_VPN_SERVER_DH} # FIXME: remove all of the following API_SERVICE_API_KEY: __unused__ PROXY_SERVICE_API_KEY: __unused__5 @@ -176,6 +180,8 @@ services: - img.${OPENBALENA_HOST_NAME} environment: BALENA_ROOT_CA: ${OPENBALENA_ROOT_CA} + BALENA_HAPROXY_CRT: ${OPENBALENA_ROOT_CRT} + BALENA_HAPROXY_KEY: ${OPENBALENA_ROOT_KEY} HAPROXY_HOSTNAME: ${OPENBALENA_HOST_NAME} # FIXME: remove the following diff --git a/haproxy/entry.sh b/haproxy/entry.sh index 62b49bb..443fced 100755 --- a/haproxy/entry.sh +++ b/haproxy/entry.sh @@ -1,9 +1,10 @@ -#!/bin/sh +#!/bin/bash -eu -CA_B64="$BALENA_ROOT_CA" -CA_FILE=/etc/ssl/private/root.chain.pem - -mkdir -p $(dirname "$CA_FILE") -echo "$CA_B64" | base64 -d >"$CA_FILE" - -exec haproxy -f /usr/local/etc/haproxy/haproxy.cfg +HAPROXY_CHAIN=/etc/ssl/private/open-balena.pem +mkdir -p "$(dirname "${HAPROXY_CHAIN}")" +( + echo "${BALENA_HAPROXY_CRT}" | base64 -d + echo "${BALENA_HAPROXY_KEY}" | base64 -d + echo "${BALENA_ROOT_CA}" | base64 -d +) > "${HAPROXY_CHAIN}" +exec haproxy -f /usr/local/etc/haproxy/haproxy.cfg \ No newline at end of file diff --git a/haproxy/haproxy.cfg b/haproxy/haproxy.cfg index d84190f..5e0bed9 100644 --- a/haproxy/haproxy.cfg +++ b/haproxy/haproxy.cfg @@ -47,7 +47,7 @@ backend redirect-to-https-in frontend https-in mode http option forwardfor - bind 127.0.0.1:444 ssl crt /etc/ssl/private/root.chain.pem accept-proxy + bind 127.0.0.1:444 ssl crt /etc/ssl/private/open-balena.pem accept-proxy reqadd X-Forwarded-Proto:\ https acl host_api hdr_dom(host) -i "api.${HAPROXY_HOSTNAME}" diff --git a/scripts/_keyid.js b/scripts/_keyid.js deleted file mode 100644 index 61ee419..0000000 --- a/scripts/_keyid.js +++ /dev/null @@ -1,79 +0,0 @@ -'use strict'; - -var crypto = require('crypto'); -var fs = require('fs'); - -var base32 = (function() { - // Extracted from https://github.com/chrisumbel/thirty-two - // to avoid having to install packages for this script. - var charTable = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567"; - var byteTable = [ - 0xff, 0xff, 0x1a, 0x1b, 0x1c, 0x1d, 0x1e, 0x1f, - 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, - 0xff, 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, - 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, - 0x0f, 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, - 0x17, 0x18, 0x19, 0xff, 0xff, 0xff, 0xff, 0xff, - 0xff, 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, - 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, - 0x0f, 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, - 0x17, 0x18, 0x19, 0xff, 0xff, 0xff, 0xff, 0xff - ]; - - function quintetCount(buff) { - var quintets = Math.floor(buff.length / 5); - return buff.length % 5 == 0 ? quintets: quintets + 1; - } - - return function(plain) { - if (!Buffer.isBuffer(plain)) { - plain = new Buffer(plain); - } - var i = 0; - var j = 0; - var shiftIndex = 0; - var digit = 0; - var encoded = new Buffer(quintetCount(plain) * 8); - - /* byte by byte isn't as pretty as quintet by quintet but tests a bit - faster. will have to revisit. */ - while(i < plain.length) { - var current = plain[i]; - - if(shiftIndex > 3) { - digit = current & (0xff >> shiftIndex); - shiftIndex = (shiftIndex + 5) % 8; - digit = (digit << shiftIndex) | ((i + 1 < plain.length) ? - plain[i + 1] : 0) >> (8 - shiftIndex); - i++; - } else { - digit = (current >> (8 - (shiftIndex + 5))) & 0x1f; - shiftIndex = (shiftIndex + 5) % 8; - if(shiftIndex == 0) i++; - } - - encoded[j] = charTable.charCodeAt(digit); - j++; - } - - for (i = j; i < encoded.length; i++) { - encoded[i] = 0x3d; //'='.charCodeAt(0) - } - return encoded; - } -})(); - -function joseKeyId(der) { - var hasher = crypto.createHash('sha256'); - hasher.update(der); - var b32 = base32(hasher.digest().slice(0, 30)).toString('ascii'); - var chunks = []; - for (var i = 0; i < b32.length; i += 4) { - chunks.push(b32.substr(i, 4)); - } - return chunks.join(':'); -} - -var derFilePath = process.argv[2]; -var der = fs.readFileSync(derFilePath); -process.stdout.write(joseKeyId(der)); \ No newline at end of file diff --git a/scripts/gen-root-ca b/scripts/gen-root-ca new file mode 100755 index 0000000..fba233b --- /dev/null +++ b/scripts/gen-root-ca @@ -0,0 +1,32 @@ +#!/bin/bash -eu + +usage() { + echo "usage: $0 COMMON_NAME [OUT]" + echo + echo " COMMON_NAME the domain name the certificate is valid for, eg. example.com" + echo " OUT path to output directory generated files will be placed in" + echo +} + +if [ -z "$1" ]; then + usage + exit 1 +fi + +CMD="$(realpath "$0")" +DIR="$(dirname "${CMD}")" + +CN="$1" +OUT="$(realpath "${2:-.}")" + +source "${DIR}/ssl-common.sh" + +# Create a secret key and CA file for the self-signed CA +"$easyrsa_bin" --pki-dir="${ROOT_PKI}" init-pki 2>/dev/null +"$easyrsa_bin" --pki-dir="${ROOT_PKI}" --days="${CA_EXPIRY_DAYS}" --req-cn="ca.${CN}" build-ca nopass 2>/dev/null +ROOT_CA="${ROOT_PKI}/ca.crt" +echo "ROOT_CA=${ROOT_CA//$OUT/\$OUT}" + +# update indexes and generate CRLs +"$easyrsa_bin" --pki-dir="${ROOT_PKI}" update-db 2>/dev/null +"$easyrsa_bin" --pki-dir="${ROOT_PKI}" gen-crl 2>/dev/null \ No newline at end of file diff --git a/scripts/gen-root-ca-cert b/scripts/gen-root-ca-cert deleted file mode 100755 index d4086ef..0000000 --- a/scripts/gen-root-ca-cert +++ /dev/null @@ -1,55 +0,0 @@ -#!/bin/sh - -set -e - -CN=$1 -OUT=${2:-.} - -CA="${CN}-ca" -CA_FILE="${OUT}/ca" -CN_FILE="${OUT}/root" - -CN_EXPIRY_DAYS=730 -CA_EXPIRY_DAYS=3650 - -usage() { - echo "usage: $0 HOST_NAME [OUT]" - echo - echo " HOST_NAME the domain name the certificate is valid for, eg. example.com" - echo " OUT path to output directory generated files will be placed in" - echo -} - -if [ -z "$CN" ]; then - usage - exit 1 -fi - -cat > "${CN_FILE}.v3.ext" </dev/null -openssl req -x509 -new -nodes -sha256 -days $CA_EXPIRY_DAYS -key "${CA_FILE}.key" -subj "/CN=${CA}" -out "${CA_FILE}.pem" 2>/dev/null - -# Create a secret key and Certificate Signing Request (CSR) for the domain -openssl genrsa -out "${CN_FILE}.key" 2048 2>/dev/null -openssl req -new -key "${CN_FILE}.key" -subj "/CN=${CN}" -out "${CN_FILE}.csr" 2>/dev/null - -# Sign the request with the self-signed CA and extension file -openssl x509 -req -sha256 -days $CN_EXPIRY_DAYS -in "${CN_FILE}.csr" -CA "${CA_FILE}.pem" -CAkey "${CA_FILE}.key" -CAcreateserial -out "${CN_FILE}.pem" -extfile "${CN_FILE}.v3.ext" 2>/dev/null - -# Create the custom certificate chain file -cat "${CN_FILE}.pem" "${CA_FILE}.pem" "${CN_FILE}.key" >"${CN_FILE}.chain.pem" - -# Cleanup -rm "${CA_FILE}.key" "${CA_FILE}.pem" "${CN_FILE}.key" "${CN_FILE}.pem" "${CN_FILE}.csr" "${CN_FILE}.v3.ext" - -echo "ROOT_CA=${CN_FILE}.chain.pem" diff --git a/scripts/gen-root-cert b/scripts/gen-root-cert new file mode 100755 index 0000000..f310a60 --- /dev/null +++ b/scripts/gen-root-cert @@ -0,0 +1,33 @@ +#!/bin/bash -eu + +usage() { + echo "usage: $0 COMMON_NAME [OUT]" + echo + echo " COMMON_NAME the domain name the certificate is valid for, eg. example.com" + echo " OUT path to output directory generated files will be placed in" + echo +} + +if [ -z "$1" ]; then + usage + exit 1 +fi + +CMD="$(realpath "$0")" +DIR="$(dirname "${CMD}")" + +CN="$1" +OUT="$(realpath "${2:-.}")" + +source "${DIR}/ssl-common.sh" + +# generate default CSR and sign (root + wildcard) +"$easyrsa_bin" --pki-dir="${ROOT_PKI}" --days="${CRT_EXPIRY_DAYS}" --subject-alt-name="DNS:${CN}" build-server-full "*.${CN}" nopass 2>/dev/null +ROOT_CRT="${ROOT_PKI}"'/issued/*.'"${CN}"'.crt' +ROOT_KEY="${ROOT_PKI}"'/private/*.'"${CN}"'.key' +echo "ROOT_CRT=${ROOT_CRT//$OUT/\$OUT}" +echo "ROOT_KEY=${ROOT_KEY//$OUT/\$OUT}" + +# update indexes and generate CRLs +"$easyrsa_bin" --pki-dir="${ROOT_PKI}" update-db 2>/dev/null +"$easyrsa_bin" --pki-dir="${ROOT_PKI}" gen-crl 2>/dev/null \ No newline at end of file diff --git a/scripts/gen-token-auth-cert b/scripts/gen-token-auth-cert index 29197c2..60ae2a9 100755 --- a/scripts/gen-token-auth-cert +++ b/scripts/gen-token-auth-cert @@ -1,42 +1,42 @@ -#!/bin/sh - -set -e - -CMD=$0 -DIR=$(dirname "$CMD") - -CN=$1 -OUT=${2:-.} - -CERT_FILE="${OUT}/token-auth" -EXPIRY_DAYS=730 +#!/bin/bash -eu usage() { - echo "usage: $0 HOST_NAME [OUT]" + echo "usage: $0 COMMON_NAME [OUT]" echo - echo " HOST_NAME the domain name the certificate is valid for, eg. example.com" - echo " OUT path to output directory generated files will be placed in" + echo " COMMON_NAME the domain name the certificate is valid for, eg. example.com" + echo " OUT path to output directory generated files will be placed in" echo } -keyid() { - # FIXME: do this in bash or python, not node - nodejs "${DIR}/_keyid.js" "$1" -} - -if [ -z "$CN" ]; then +if [ -z "$1" ]; then usage exit 1 fi -openssl ecparam -name prime256v1 -genkey -noout -out "${CERT_FILE}.pem" 2>/dev/null -openssl req -x509 -new -nodes -days "${EXPIRY_DAYS}" -key "${CERT_FILE}.pem" -subj "/CN=api.${CN}" -out "${CERT_FILE}.crt" 2>/dev/null -openssl ec -in "${CERT_FILE}.pem" -pubout -outform DER -out "${CERT_FILE}".der 2>/dev/null -keyid "${CERT_FILE}".der >"${CERT_FILE}".kid +CMD="$(realpath "$0")" +DIR="$(dirname "${CMD}")" -# Cleanup -rm "${CERT_FILE}.der" +CN="$1" +OUT="$(realpath "${2:-.}")" -echo "PUB=${CERT_FILE}.crt" -echo "KEY=${CERT_FILE}.pem" -echo "KID=${CERT_FILE}.kid" +source "${DIR}/ssl-common.sh" + +keyid() { + local der="$(openssl ec -in "$1" -pubout -outform DER 2>/dev/null)" + python -c "import sys as S; from base64 import b32encode as B; import hashlib as H; h = H.sha256(); h.update(S.argv[1].encode('ascii')); s = B(h.digest()[:30]).decode('ascii'); S.stdout.write(':'.join([s[i:i+4] for i in range(0, len(s), 4)]))" "${der}" +} + +# generate api CSR and sign +"$easyrsa_bin" --pki-dir="${ROOT_PKI}" --days=730 --use-algo=ec --curve=prime256v1 build-server-full "api.${CN}" nopass 2>/dev/null +JWT_CRT="${ROOT_PKI}/issued/api.${CN}.crt" +JWT_KEY="${ROOT_PKI}/private/api.${CN}.key" +echo "JWT_CRT=${JWT_CRT//$OUT/\$OUT}" +echo "JWT_KEY=${JWT_KEY//$OUT/\$OUT}" + +# update indexes and generate CRLs +"$easyrsa_bin" --pki-dir="${ROOT_PKI}" update-db 2>/dev/null +"$easyrsa_bin" --pki-dir="${ROOT_PKI}" gen-crl 2>/dev/null + +# generate key ID +JWT_KID="$(keyid "${JWT_CRT}")" +echo "JWT_KID=${JWT_KID//$OUT/\$OUT}" diff --git a/scripts/gen-vpn-certs b/scripts/gen-vpn-certs new file mode 100755 index 0000000..af21735 --- /dev/null +++ b/scripts/gen-vpn-certs @@ -0,0 +1,52 @@ +#!/bin/bash -eu + +usage() { + echo "usage: $0 COMMON_NAME [OUT]" + echo + echo " COMMON_NAME the domain name the certificate is valid for, eg. example.com" + echo " OUT path to output directory generated files will be placed in" + echo +} + +if [ -z "$1" ]; then + usage + exit 1 +fi + +CMD="$(realpath "$0")" +DIR="$(dirname "${CMD}")" + +CN="$1" +OUT="$(realpath "${2:-.}")" + +source "${DIR}/ssl-common.sh" +VPN_PKI="$(realpath "${OUT}/vpn")" + +# generate VPN sub-CA +"$easyrsa_bin" --pki-dir="${VPN_PKI}" init-pki 2>/dev/null +"$easyrsa_bin" --pki-dir="${VPN_PKI}" --days="${CA_EXPIRY_DAYS}" --req-cn="vpn-ca.${CN}" build-ca nopass subca 2>/dev/null + +# import sub-CA CSR into root PKI, sign, and copy back to vpn PKI +"$easyrsa_bin" --pki-dir="${ROOT_PKI}" import-req "${VPN_PKI}/reqs/ca.req" "vpn-ca" 2>/dev/null +"$easyrsa_bin" --pki-dir="${ROOT_PKI}" sign-req ca "vpn-ca" 2>/dev/null +cp "${ROOT_PKI}/issued/vpn-ca.crt" "${VPN_PKI}/ca.crt" +VPN_CA="${VPN_PKI}/ca.crt" +echo "VPN_CA=${VPN_CA//$OUT/\$OUT}" + +# generate and sign vpn server certificate +"$easyrsa_bin" --pki-dir="${VPN_PKI}" --days="${CRT_EXPIRY_DAYS}" build-server-full "vpn.${CN}" nopass 2>/dev/null +VPN_CRT="${VPN_PKI}/issued/vpn.${CN}.crt" +VPN_KEY="${VPN_PKI}/private/vpn.${CN}.key" +echo "VPN_CRT=${VPN_CRT//$OUT/\$OUT}" +echo "VPN_KEY=${VPN_KEY//$OUT/\$OUT}" + +# generate vpn dhparams (keysize of 2048 will do, 4096 can wind up taking hours to generate) +"$easyrsa_bin" --pki-dir="${VPN_PKI}" --keysize=2048 gen-dh 2>/dev/null +VPN_DH="${VPN_PKI}/dh.pem" +echo "VPN_DH=${VPN_DH//$OUT/\$OUT}" + +# update indexes and generate CRLs +"$easyrsa_bin" --pki-dir="${ROOT_PKI}" update-db 2>/dev/null +"$easyrsa_bin" --pki-dir="${VPN_PKI}" update-db 2>/dev/null +"$easyrsa_bin" --pki-dir="${ROOT_PKI}" gen-crl 2>/dev/null +"$easyrsa_bin" --pki-dir="${VPN_PKI}" gen-crl 2>/dev/null \ No newline at end of file diff --git a/scripts/make-env b/scripts/make-env index 8f303f4..9de2c2d 100755 --- a/scripts/make-env +++ b/scripts/make-env @@ -1,30 +1,42 @@ -#!/bin/sh - -set -e - -HOST_NAME="$1" -ROOT_CA="$2" -TOKEN_AUTH_PUB="$3" -TOKEN_AUTH_KEY="$4" -TOKEN_AUTH_KID="$5" +#!/bin/bash -eu usage() { - echo "usage: $0 HOST_NAME ROOT_CA TOKEN_AUTH_PUB TOKEN_AUTH_KEY TOKEN_AUTH_KID" + echo "usage: $0" + echo + echo "Required Variables:" + echo + echo " HOST_NAME" + echo " ROOT_CA Path to root CA certificate" + echo " ROOT_CRT Path to root/wildcard certificate" + echo " ROOT_KEY Path to root/wildcard private key" + echo " JWT_CRT Path to Token Auth certificate" + echo " JWT_KEY Path to Token Auth private key" + echo " JWT_KID The KeyID for the Token Auth certificate" + echo " VPN_CA Path to the VPN sub-CA certificate" + echo " VPN_CRT Path to the VPN server certificate" + echo " VPN_KEY Path to the VPN server private key" + echo " VPN_DH Path to the VPN server Diffie Hellman parameters" echo } +for var in HOST_NAME ROOT_CA ROOT_CRT ROOT_KEY JWT_CRT JWT_KEY JWT_KID VPN_CA VPN_CRT VPN_KEY VPN_DH; do + if [ -z "${!var-}" ]; then + usage + exit 1 + fi +done + randstr() { - LC_CTYPE=C tr -dc A-Za-z0-9 < /dev/urandom | fold -w ${1:-32} | head -n 1 + LC_CTYPE=C tr -dc A-Za-z0-9 < /dev/urandom | fold -w "${1:-32}" | head -n 1 } b64encode() { - cat "$1" | base64 --wrap=0 2>/dev/null || cat "$1" | base64 --break=0 + cat "$@" | base64 --wrap=0 2>/dev/null || cat "$@" | base64 --break=0 } -if [ -z "$HOST_NAME" ] || [ -z "$ROOT_CA" ] || [ -z "$TOKEN_AUTH_PUB" ] || [ -z "$TOKEN_AUTH_KEY" ] || [ -z "$TOKEN_AUTH_KID" ]; then - usage - exit 1 -fi +b64encode_str() { + echo -n "$@" | base64 --wrap=0 - 2>/dev/null || echo -n "$@" | base64 --break=0 - +} cat < Creating new project at: $PROJECT_DIR" mkdir -p "$PROJECT_DIR" "$CERTS_DIR" -echo_bold "==> Creating root CA cert..." -"${DIR}/gen-root-ca-cert" $HOST_NAME "$CERTS_DIR" +echo_bold "==> Generating root CA cert..." +source "${DIR}/gen-root-ca" "${HOST_NAME}" "${CERTS_DIR}" -echo_bold "==> Creating token auth cert..." -"${DIR}/gen-token-auth-cert" $HOST_NAME "$CERTS_DIR" +echo_bold "==> Generating root cert chain for haproxy..." +source "${DIR}/gen-root-cert" "${HOST_NAME}" "${CERTS_DIR}" + +echo_bold "==> Generating token auth cert..." +source "${DIR}/gen-token-auth-cert" "${HOST_NAME}" "${CERTS_DIR}" + +echo_bold "==> Generating VPN CA, cert and dhparam (this may take a while)..." +source "${DIR}/gen-vpn-certs" "${HOST_NAME}" "${CERTS_DIR}" echo_bold "==> Setting up environment..." -cat >"${PROJECT_DIR}/activate" <"${PROJECT_DIR}/activate" <(source "${DIR}/make-env") echo_bold "==> Adding default compose file..." cp "${BASE_DIR}/compose/template.yml" "${PROJECT_DIR}/docker-compose.yml" @@ -56,4 +53,4 @@ echo_bold "==> Patching /etc/hosts..." "${DIR}/patch-hosts" $HOST_NAME echo_bold "==> Activating project..." -"${DIR}/select-project" "$PROJECT_DIR" +"${DIR}/select-project" "${PROJECT_DIR}" \ No newline at end of file