#!/bin/bash set -e # Colors GREEN='\033[0;32m' RED='\033[0;31m' YELLOW='\033[1;33m' NC='\033[0m' # No Color log_info() { echo -e "${YELLOW}[INFO]${NC} $1" } log_success() { echo -e "${GREEN}[SUCCESS]${NC} $1" } log_error() { echo -e "${RED}[ERROR]${NC} $1" } # ----------------------------------------------------------------------------- # Input Framework # ----------------------------------------------------------------------------- # Ensure we have a tty for input if [ ! -e /dev/tty ]; then echo "Error: Script must be run in an interactive terminal (cannot find /dev/tty)." >&2 exit 1 fi # Function to prompt for user input # Usage: get_input "VARIABLE_NAME" "Prompt Text" "Default Value" "is_secret(true/false)" get_input() { local var_name="$1" local prompt_text="$2" local default_value="$3" local is_secret="$4" local input_val="" local prompt_full="${GREEN}${prompt_text}${NC}" if [ -n "$default_value" ]; then prompt_full+=" [$default_value]" fi prompt_full+=": " while true; do # Print prompt to stderr so it shows up even if stdout is redirected if [ "$is_secret" == "true" ]; then echo -ne "$prompt_full" >&2 read -s input_val < /dev/tty echo "" >&2 # Newline after secret input else echo -ne "$prompt_full" >&2 read input_val < /dev/tty fi # Use default if input is empty if [ -z "$input_val" ] && [ -n "$default_value" ]; then input_val="$default_value" fi # Validation: Require input if no default exists if [ -z "$input_val" ]; then echo -e "${RED}Error: This value is required.${NC}" >&2 else break fi done # Set the variable dynamically in the parent scope printf -v "$var_name" "%s" "$input_val" export "$var_name=$input_val" } # Function to confirm collected inputs # Usage: confirm_inputs "VAR1" "VAR2" "VAR3" ... confirm_inputs() { echo "" >&2 echo -e "${GREEN}=== Configuration Summary ===${NC}" >&2 for var in "$@"; do local val="${!var}" # Mask secrets in summary if needed, or just show length # For now, just printing value. # To improve: pass a list of secret vars to mask them. echo -e "${YELLOW}$var:${NC} $val" >&2 done echo "" >&2 get_input "CONFIRM" "Is this correct? (y/n)" "y" "false" if [[ "${CONFIRM,,}" != "y" ]]; then echo -e "${RED}Aborted by user.${NC}" >&2 exit 1 fi } install_unit() { local template_url=$1 local filename=$(basename "$template_url") local dest_path=/etc/systemd/system/"$filename" if [ -e "$dest_path" ]; then get_input "CONFIRM_OVERWRITE" "Overwrite $dest_path? (y/n)" "y" "false" if [[ "${CONFIRM_OVERWRITE,,}" != "y" ]]; then echo "Skipping overwrite of ${dest_path}." return fi fi log_info "Installing $filename..." curl -sL $template_url | envsubst > "$dest_path" log_success "$filename installed to $dest_path" } # ----------------------------------------------------------------------------- # Script Logic # ----------------------------------------------------------------------------- echo "Starting Interactive Setup..." echo "-----------------------------" # Verify required external binaries if ! command -v step >/dev/null 2>&1; then # Prompt the user to install the step CLI get_input "INSTALL_STEP" "The 'step' CLI was not found. Install now? (y/n)" "y" "false" if [[ "${INSTALL_STEP,,}" == "y" ]]; then apt-get update && apt-get install -y --no-install-recommends curl vim gpg ca-certificates curl -fsSL https://packages.smallstep.com/keys/apt/repo-signing-key.gpg -o /etc/apt/trusted.gpg.d/smallstep.asc && \ echo 'deb [signed-by=/etc/apt/trusted.gpg.d/smallstep.asc] https://packages.smallstep.com/stable/debian debs main' \ | tee /etc/apt/sources.list.d/smallstep.list apt-get update && apt-get -y install step-cli step-ca else log_error "Cannot continue without 'step'. Aborting." >&2 exit 1 fi else log_success "Step CA installed\n" fi get_input "CERT_DIR" "Enter directory for certificates" "/var/lib/tls" "false" get_input "CERT_FILENAME" "Name for cert file" "cert.pem" "false" get_input "KEY_FILENAME" "Name for private key" "key.pem" "false" if [ ! -e "$CERT_DIR" ]; then (umask 077; mkdir -p "${CERT_DIR}") log_info "Created ${CERT_DIR}" fi # These need to get set so that they get filled into the service correctly. export CERT_LOCATION=$(readlink -f ${CERT_DIR}/$CERT_FILENAME) export KEY_LOCATION=$(readlink -f ${CERT_DIR}/$KEY_FILENAME) confirm_inputs "CERT_LOCATION" "KEY_LOCATION" if [ ! -e "${CERT_DIR}/root_ca.crt" ]; then step ca root "${CERT_DIR}/root_ca.crt" fi if [ ! -f "$CERT_LOCATION" ] || [ ! -f "$KEY_LOCATION" ]; then hostname=$(hostname -s) ip_address=$(ip -4 addr show dev eth0 | awk '/inet /{print $2}' | cut -d/ -f1) step ca certificate "$hostname" \ "${CERT_DIR}/cert.pem" "${CERT_DIR}/key.pem" \ --san "$hostname" \ --san "$hostname.john-stream.com" \ --san "$ip_address" \ --provisioner admin fi echo "" >&2 echo -e "${GREEN}=== Cert information ===${NC}" >&2 openssl x509 -noout -subject -issuer -ext extendedKeyUsage -ext subjectAltName -enddate -in "$CERT_LOCATION" SERVICE_FILE="cert-renewer.service" TIMER_FILE="cert-renewer.timer" REPO_URL_BASE=https://gitea.john-stream.com/john/soteria/raw/branch/main/ SERVICE_TEMPLATE_URL="${REPO_URL_BASE}systemd/${SERVICE_FILE}" TIMER_TEMPLATE_URL="${REPO_URL_BASE}systemd/${TIMER_FILE}" echo "" >&2 echo -e "${GREEN}=== Installing rotation services ===${NC}" >&2 install_unit ${SERVICE_TEMPLATE_URL} install_unit ${TIMER_TEMPLATE_URL} echo "" >&2 echo -e "${GREEN}=== Reloading services ===${NC}" >&2 systemctl daemon-reload systemctl enable --now "${TIMER_FILE}" "${SERVICE_FILE}" systemctl list-unit-files $SERVICE_FILE $TIMER_FILE