#!/usr/bin/env bash # ============================================================================ # Breakwater Bootstrap — public installer entrypoint # # curl -fsSL https://install.bwater.io | sudo bash # # Reads credentials from /dev/tty so the piped bootstrap remains interactive. # The real installer, compose file, and CLI are fetched only after the # install password and activation code are validated. # ============================================================================ set -Eeuo pipefail TTY="/dev/tty" INSTALL_DIR="${BREAKWATER_INSTALL_DIR:-/opt/breakwater}" LICENSE_API="${BREAKWATER_LICENSE_API:-https://license.bwater.io/api/v1}" DEFAULT_REGISTRY_HOST="${BREAKWATER_REGISTRY_HOST:-docker.io}" DEFAULT_REGISTRY_NAMESPACE="${BREAKWATER_REGISTRY_NAMESPACE:-bwaterdemo}" DEFAULT_REGISTRY_USERNAME="${BREAKWATER_REGISTRY_USERNAME:-bwaterdemo}" CURRENT_STEP="bootstrap" if [[ -t 1 && "${TERM:-}" != "dumb" ]]; then BOLD=$'\033[1m' DIM=$'\033[2m' RED=$'\033[0;31m' GREEN=$'\033[0;32m' YELLOW=$'\033[1;33m' CYAN=$'\033[0;36m' NC=$'\033[0m' else BOLD="" DIM="" RED="" GREEN="" YELLOW="" CYAN="" NC="" fi section() { printf "\n%s[%s]%s %s%s%s\n" "$CYAN" "$1" "$NC" "$BOLD" "$2" "$NC" } ok() { printf "%s[ok]%s %s\n" "$GREEN" "$NC" "$1" } warn() { printf "%s[warn]%s %s\n" "$YELLOW" "$NC" "$1" } fail() { printf "%s[error]%s %s\n" "$RED" "$NC" "$1" >&2 } prompt() { local __var_name="$1" local label="$2" local hidden="${3:-0}" local value="" local preset="" case "$__var_name" in INSTALL_PASSWORD) preset="${BREAKWATER_BOOTSTRAP_INSTALL_PASSWORD:-${INSTALL_PASSWORD:-}}" ;; ADMIN_EMAIL) preset="${BREAKWATER_BOOTSTRAP_ADMIN_EMAIL:-${ADMIN_EMAIL:-}}" ;; ADMIN_PASSWORD) preset="${BREAKWATER_BOOTSTRAP_ADMIN_PASSWORD:-${ADMIN_PASSWORD:-}}" ;; ADMIN_PASSWORD_CONFIRM) preset="${BREAKWATER_BOOTSTRAP_ADMIN_PASSWORD_CONFIRM:-}" preset="${preset:-${BREAKWATER_BOOTSTRAP_ADMIN_PASSWORD:-}}" preset="${preset:-${ADMIN_PASSWORD_CONFIRM:-}}" preset="${preset:-${ADMIN_PASSWORD:-}}" ;; ACTIVATION_CODE) preset="${BREAKWATER_BOOTSTRAP_ACTIVATION_CODE:-${ACTIVATION_CODE:-}}" ;; *) preset="${!__var_name:-}" ;; esac if [[ -n "$preset" ]]; then printf -v "$__var_name" '%s' "$preset" return fi if [[ ! -r "$TTY" ]]; then fail "An interactive terminal is required to continue." exit 1 fi if [[ "$hidden" == "1" ]]; then read -r -s -p "$label" value <"$TTY" printf "\n" >"$TTY" else read -r -p "$label" value <"$TTY" fi printf -v "$__var_name" '%s' "$value" } on_error() { local line="$1" local cmd="$2" fail "Installer bootstrap failed during ${CURRENT_STEP}." printf "%sline:%s %s | %scommand:%s %s\n" "$DIM" "$NC" "$line" "$DIM" "$NC" "$cmd" >&2 printf "%snext:%s retry the command or contact support@bwater.io\n" "$DIM" "$NC" >&2 } trap 'on_error "$LINENO" "$BASH_COMMAND"' ERR printf "%s" "$CYAN" cat <<'BANNER' ____ _ _ | __ ) _ __ ___ __ _| | ____ ____ _| |_ ___ _ __ | _ \| '__/ _ \/ _` | |/ /\ \ /\ / / _` | __/ _ \ '__| | |_) | | | __/ (_| | < \ V V / (_| | || __/ | |____/|_| \___|\__,_|_|\_\ \_/\_/ \__,_|\__\___|_| BANNER printf "%sOperational Security Platform Installer%s\n" "$BOLD" "$NC" printf "%s" "$NC" if [[ $EUID -ne 0 ]]; then fail "Run with sudo: curl -fsSL https://install.bwater.io | sudo bash" exit 1 fi if [[ "$(uname -s)" != "Linux" ]]; then fail "Breakwater requires Linux for raw-socket and L2 discovery." exit 1 fi if [[ "$(uname -m)" != "x86_64" && "$(uname -m)" != "amd64" ]]; then fail "Breakwater customer appliances currently support x86_64 Linux only." exit 1 fi for binary in curl python3; do if ! command -v "$binary" >/dev/null 2>&1; then fail "Required command not found: $binary" exit 1 fi done CURRENT_STEP="docker bootstrap" section "1/4" "Preparing the host" if ! command -v docker >/dev/null 2>&1; then printf "%s[info]%s Docker not found. Installing Docker Engine...\n" "$CYAN" "$NC" curl -fsSL https://get.docker.com | sh systemctl enable docker systemctl start docker ok "Docker installed." else ok "Docker already installed." fi if ! docker compose version >/dev/null 2>&1; then fail "Docker Compose v2 is required." exit 1 fi ok "Docker Compose is available." CURRENT_STEP="credential collection" section "2/4" "Collecting install credentials" printf "%sInstall password%s\n" "$BOLD" "$NC" printf "%sProvided by Breakwater sales. Controls access to installer artifacts and image pulls.%s\n" "$DIM" "$NC" prompt INSTALL_PASSWORD "Install password: " 1 printf "\n%sAdmin email%s\n" "$BOLD" "$NC" while true; do prompt ADMIN_EMAIL "Admin email: " if [[ "$ADMIN_EMAIL" =~ ^[^[:space:]@]+@[^[:space:]@]+\.[^[:space:]@]+$ ]]; then break fi warn "Enter a valid email address." done printf "\n%sAdmin password%s\n" "$BOLD" "$NC" while true; do prompt ADMIN_PASSWORD "Admin password (min 12 chars): " 1 if [[ ${#ADMIN_PASSWORD} -lt 12 ]]; then warn "Password must be at least 12 characters." continue fi prompt ADMIN_PASSWORD_CONFIRM "Confirm password: " 1 if [[ "$ADMIN_PASSWORD" == "$ADMIN_PASSWORD_CONFIRM" ]]; then break fi warn "Passwords do not match." done printf "\n%sActivation code%s\n" "$BOLD" "$NC" printf "%sThe signed offline license token issued for this appliance.%s\n" "$DIM" "$NC" prompt ACTIVATION_CODE "30-day activation code: " 1 CURRENT_STEP="license validation" section "3/4" "Validating with the license service" MACHINE_ID="$(cat /etc/machine-id 2>/dev/null || hostname | sha256sum | cut -c1-32)" VALIDATE_RESPONSE="$( curl -sf -X POST "${LICENSE_API}/install/validate" \ -H "Content-Type: application/json" \ -d "{ \"install_password\": \"${INSTALL_PASSWORD}\", \"activation_code\": \"${ACTIVATION_CODE}\", \"machine_id\": \"${MACHINE_ID}\" }" )" parse_json_field() { local field="$1" python3 -c "import json,sys; data=json.load(sys.stdin); print(data.get('${field}',''))" <<<"$VALIDATE_RESPONSE" } INSTALLER_URL="$(parse_json_field installer_url)" COMPOSE_URL="$(parse_json_field compose_url)" CLI_URL="$(parse_json_field cli_url)" REGISTRY_TOKEN="$(parse_json_field registry_token)" REGISTRY_HOST="$(parse_json_field registry_host)" REGISTRY_NAMESPACE="$(parse_json_field registry_namespace)" REGISTRY_USERNAME="$(parse_json_field registry_username)" LICENSE_PUBLIC_KEY="$(parse_json_field license_public_key)" if [[ -z "$INSTALLER_URL" || -z "$COMPOSE_URL" || -z "$CLI_URL" || -z "$LICENSE_PUBLIC_KEY" ]]; then fail "License server response was incomplete." exit 1 fi ok "Credentials validated." REGISTRY_HOST="${REGISTRY_HOST:-$DEFAULT_REGISTRY_HOST}" REGISTRY_NAMESPACE="${REGISTRY_NAMESPACE:-$DEFAULT_REGISTRY_NAMESPACE}" REGISTRY_USERNAME="${REGISTRY_USERNAME:-$DEFAULT_REGISTRY_USERNAME}" REGISTRY_TOKEN="${REGISTRY_TOKEN:-${BREAKWATER_REGISTRY_TOKEN:-}}" CURRENT_STEP="artifact download" section "4/4" "Downloading installer payload" mkdir -p "$INSTALL_DIR" curl -fsSL "$INSTALLER_URL" -o "${INSTALL_DIR}/install.sh" curl -fsSL "$COMPOSE_URL" -o "${INSTALL_DIR}/docker-compose.customer.yml" curl -fsSL "$CLI_URL" -o "${INSTALL_DIR}/breakwater" chmod +x "${INSTALL_DIR}/install.sh" "${INSTALL_DIR}/breakwater" ok "Installer payload downloaded." CURRENT_STEP="registry authentication" if [[ -n "$REGISTRY_TOKEN" ]]; then printf "%s[info]%s Authenticating to the private image registry...\n" "$CYAN" "$NC" if echo "$REGISTRY_TOKEN" | docker login "$REGISTRY_HOST" -u "$REGISTRY_USERNAME" --password-stdin >/dev/null 2>&1; then ok "Registry access granted." else warn "Registry login failed. Image pulls may fail later." fi fi export BREAKWATER_LICENSE_KEY="$ACTIVATION_CODE" export BREAKWATER_ADMIN_EMAIL="$ADMIN_EMAIL" export BREAKWATER_ADMIN_PASSWORD="$ADMIN_PASSWORD" export BREAKWATER_MACHINE_ID="$MACHINE_ID" export BREAKWATER_LICENSE_PUBLIC_KEY="$LICENSE_PUBLIC_KEY" export BREAKWATER_REGISTRY_HOST="$REGISTRY_HOST" export BREAKWATER_REGISTRY_NAMESPACE="$REGISTRY_NAMESPACE" export BREAKWATER_REGISTRY_USERNAME="$REGISTRY_USERNAME" CURRENT_STEP="installer handoff" exec "${INSTALL_DIR}/install.sh"