Files
matrix/scripts/setup-local-communication.sh

300 lines
9.3 KiB
Bash

#!/usr/bin/env bash
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
PROJECT_ROOT="$(cd "${SCRIPT_DIR}/.." && pwd)"
CONFIG_DIR="${PROJECT_ROOT}/config"
KEY_DIR="${CONFIG_DIR}/keys"
CLIENT_ROOT="${CONFIG_DIR}/clients"
CLIENT_ID=""
INSTALLATION_ID=""
CLOUD_BASE_URL=""
TOKEN_URL=""
CLIENT_DIR=""
CLIENT_CONFIG="${CONFIG_DIR}/clients.yml"
CLIENT_PRIVATE_KEY=""
CLIENT_PUBLIC_KEY=""
MACHINE_ASSERTION=""
SKIP_BUILD_CONFIGURED="${CYGNUS_SETUP_SKIP_BUILD+x}"
SKIP_BUILD="${CYGNUS_SETUP_SKIP_BUILD:-false}"
FORCE_ASSERTION="${CYGNUS_SETUP_FORCE_ASSERTION:-false}"
NON_INTERACTIVE="${CYGNUS_SETUP_NON_INTERACTIVE:-false}"
log() {
printf '\n==> %s\n' "$1"
}
fail() {
printf '\nERROR: %s\n' "$1" >&2
exit 1
}
require_command() {
command -v "$1" >/dev/null 2>&1 || fail "Required command '$1' was not found."
}
prompt_value() {
local prompt="$1"
local default_value="$2"
local value=""
if [[ "${NON_INTERACTIVE}" == "true" ]] || [[ ! -t 0 ]]; then
printf '%s' "${default_value}"
return
fi
read -r -p "${prompt} [${default_value}]: " value
printf '%s' "${value:-${default_value}}"
}
prompt_yes_no() {
local prompt="$1"
local default_value="$2"
local answer=""
local hint="y/N"
[[ "${default_value}" == "true" ]] && hint="Y/n"
if [[ "${NON_INTERACTIVE}" == "true" ]] || [[ ! -t 0 ]]; then
printf '%s' "${default_value}"
return
fi
read -r -p "${prompt} [${hint}]: " answer
case "${answer}" in
y|Y|yes|YES) printf '%s' "true" ;;
n|N|no|NO) printf '%s' "false" ;;
"") printf '%s' "${default_value}" ;;
*) fail "Please answer yes or no." ;;
esac
}
validate_identifier() {
local label="$1"
local value="$2"
[[ "${value}" =~ ^[A-Za-z0-9][A-Za-z0-9_-]*$ ]] || fail \
"${label} must contain no spaces and use only letters, numbers, '-' or '_'."
}
collect_client_inputs() {
local default_client="${CYGNUS_SETUP_CLIENT_ID:-customer-a}"
local default_installation="${CYGNUS_SETUP_INSTALLATION_ID:-site-01}"
local default_url="${CYGNUS_SETUP_CLOUD_URL:-http://localhost:8090}"
log "Client provisioning details"
CLIENT_ID="$(prompt_value \
"Customer identifier (no spaces; used in key and directory names)" \
"${default_client}")"
validate_identifier "Customer identifier" "${CLIENT_ID}"
INSTALLATION_ID="$(prompt_value \
"Installation identifier (no spaces)" \
"${default_installation}")"
validate_identifier "Installation identifier" "${INSTALLATION_ID}"
CLOUD_BASE_URL="$(prompt_value "Cloud service base URL" "${default_url}")"
[[ "${CLOUD_BASE_URL}" =~ ^https?://[^[:space:]]+$ ]] || fail \
"Cloud service base URL must be a valid http:// or https:// URL."
if [[ -z "${SKIP_BUILD_CONFIGURED}" ]]; then
if [[ "$(prompt_yes_no "Run the complete Maven verification" "true")" == "true" ]]; then
SKIP_BUILD="false"
else
SKIP_BUILD="true"
fi
fi
TOKEN_URL="${CLOUD_BASE_URL%/}/oauth2/token"
CLIENT_DIR="${CLIENT_ROOT}/${CLIENT_ID}"
CLIENT_PRIVATE_KEY="${CLIENT_DIR}/${CLIENT_ID}-signing-private.pem"
CLIENT_PUBLIC_KEY="${CLIENT_DIR}/${CLIENT_ID}-signing-public.pem"
MACHINE_ASSERTION="${CLIENT_DIR}/${CLIENT_ID}-${INSTALLATION_ID}-assertion.jwt"
printf '\nProvisioning summary:\n'
printf ' Customer: %s\n' "${CLIENT_ID}"
printf ' Installation: %s\n' "${INSTALLATION_ID}"
printf ' Cloud URL: %s\n' "${CLOUD_BASE_URL}"
printf ' Token audience: %s\n' "${TOKEN_URL}"
printf ' Verify build: %s\n' "$([[ "${SKIP_BUILD}" == "true" ]] && printf no || printf yes)"
if [[ "$(prompt_yes_no "Continue with these values" "true")" != "true" ]]; then
fail "Provisioning cancelled."
fi
}
generate_private_key() {
local path="$1"
if [[ -f "${path}" ]]; then
printf 'Keeping existing private key: %s\n' "${path}"
return
fi
openssl genpkey -algorithm RSA -pkeyopt rsa_keygen_bits:3072 -out "${path}"
chmod 600 "${path}"
}
generate_public_key() {
local private_path="$1"
local public_path="$2"
if [[ -f "${public_path}" ]]; then
printf 'Keeping existing public key: %s\n' "${public_path}"
return
fi
openssl pkey -in "${private_path}" -pubout -out "${public_path}"
chmod 644 "${public_path}"
}
configure_java_21() {
local java_major
if [[ "$(uname -s)" == "Darwin" ]] && [[ -x /usr/libexec/java_home ]]; then
if java_home_21="$(/usr/libexec/java_home -v 21 2>/dev/null)"; then
export JAVA_HOME="${java_home_21}"
export PATH="${JAVA_HOME}/bin:${PATH}"
fi
fi
require_command java
java_major="$(java -version 2>&1 | awk -F '[".]' '/version/ {print $2; exit}')"
[[ "${java_major}" == "21" ]] || fail \
"JDK 21 is required. Current java is: $(java -version 2>&1 | head -n 1)"
}
write_client_configuration() {
local public_key_uri="file:${CLIENT_PUBLIC_KEY}"
local existing_installation=""
if [[ ! -f "${CLIENT_CONFIG}" ]]; then
{
printf '%s\n' "cygnus:"
printf '%s\n' " security:"
printf '%s\n' " clients:"
} > "${CLIENT_CONFIG}"
elif grep -Fq " ${CLIENT_ID}:" "${CLIENT_CONFIG}"; then
existing_installation="$(awk \
-v client=" ${CLIENT_ID}:" \
'$0 == client { found = 1; next }
found && /installation-id:/ {
sub(/^.*installation-id:[[:space:]]*/, "");
print;
exit
}
found && /^ [^[:space:]]/ { exit }' \
"${CLIENT_CONFIG}")"
if [[ "${existing_installation}" != "${INSTALLATION_ID}" ]]; then
fail "Client '${CLIENT_ID}' already uses installation '${existing_installation}' in ${CLIENT_CONFIG}; requested '${INSTALLATION_ID}'. Use the existing installation ID or provision a different customer identifier."
fi
printf 'Keeping existing configuration for client: %s\n' "${CLIENT_ID}"
return
fi
{
printf ' %s:\n' "${CLIENT_ID}"
printf '%s\n' " enabled: true"
printf ' installation-id: %s\n' "${INSTALLATION_ID}"
printf ' assertion-public-key: %s\n' "${public_key_uri}"
printf '%s\n' " scopes:"
printf '%s\n' " - identity.login"
} >> "${CLIENT_CONFIG}"
chmod 600 "${CLIENT_CONFIG}"
printf 'Added client %s to: %s\n' "${CLIENT_ID}" "${CLIENT_CONFIG}"
}
generate_machine_assertion() {
if [[ -f "${MACHINE_ASSERTION}" ]] \
&& [[ "${FORCE_ASSERTION}" != "true" ]]; then
if [[ "$(prompt_yes_no \
"Machine assertion already exists. Regenerate it" \
"false")" == "true" ]]; then
FORCE_ASSERTION="true"
else
printf 'Keeping existing machine assertion: %s\n' "${MACHINE_ASSERTION}"
return
fi
fi
[[ -f "${CLIENT_PRIVATE_KEY}" ]] || fail \
"Customer signing key was not found: ${CLIENT_PRIVATE_KEY}"
[[ -f "${KEY_DIR}/assertion-decryption-public.pem" ]] || fail \
"Cloud assertion public key was not found: ${KEY_DIR}/assertion-decryption-public.pem"
rm -f "${MACHINE_ASSERTION}"
mvn -q -pl cygnus-cloud-client exec:java \
-Dexec.mainClass=com.cygnus.client.provisioning.MachineAssertionGenerator \
-Dexec.args="${CLIENT_ID} ${INSTALLATION_ID} ${TOKEN_URL} ${CLIENT_PRIVATE_KEY} ${KEY_DIR}/assertion-decryption-public.pem ${MACHINE_ASSERTION}"
[[ -s "${MACHINE_ASSERTION}" ]] || fail \
"Machine assertion generation did not create: ${MACHINE_ASSERTION}"
chmod 600 "${MACHINE_ASSERTION}"
}
main() {
cd "${PROJECT_ROOT}"
collect_client_inputs
log "Checking JDK 21, Maven, and OpenSSL"
require_command awk
require_command openssl
require_command mvn
configure_java_21
java -version
mvn -version
openssl version
if [[ "${SKIP_BUILD}" != "true" ]]; then
log "Building and verifying all modules"
mvn clean verify
else
log "Skipping Maven verification because CYGNUS_SETUP_SKIP_BUILD=true"
mvn -q -pl cygnus-cloud-client -am compile
fi
log "Creating protected local configuration directories"
mkdir -p "${KEY_DIR}" "${CLIENT_DIR}"
chmod 700 "${CONFIG_DIR}" "${KEY_DIR}" "${CLIENT_ROOT}" "${CLIENT_DIR}"
log "Generating cloud assertion-encryption keys"
generate_private_key "${KEY_DIR}/assertion-decryption-private.pem"
generate_public_key \
"${KEY_DIR}/assertion-decryption-private.pem" \
"${KEY_DIR}/assertion-decryption-public.pem"
log "Generating cloud access-token signing keys"
generate_private_key "${KEY_DIR}/access-token-private.pem"
generate_public_key \
"${KEY_DIR}/access-token-private.pem" \
"${KEY_DIR}/access-token-public.pem"
log "Generating cloud login-payload encryption keys"
generate_private_key "${KEY_DIR}/login-private.pem"
generate_public_key \
"${KEY_DIR}/login-private.pem" \
"${KEY_DIR}/login-public.pem"
log "Generating on-premises installation signing keys"
generate_private_key "${CLIENT_PRIVATE_KEY}"
generate_public_key \
"${CLIENT_PRIVATE_KEY}" \
"${CLIENT_PUBLIC_KEY}"
log "Writing cloud machine-client configuration"
write_client_configuration
log "Generating encrypted machine assertion"
generate_machine_assertion
log "Local communication security setup completed"
printf '%s\n' \
"Client ID: ${CLIENT_ID}" \
"Installation ID: ${INSTALLATION_ID}" \
"Token audience: ${TOKEN_URL}" \
"Client config: ${CLIENT_CONFIG}" \
"Assertion file: ${MACHINE_ASSERTION}" \
"Login public key: ${KEY_DIR}/login-public.pem" \
"" \
"Next: configure the cloud and on-prem environment variables, then start" \
"the cloud service on ${CLOUD_BASE_URL} before starting the on-prem app."
}
main "$@"