#!/usr/bin/env bash set -Eeuo pipefail SCRIPT_NAME="$(basename "$0")" PATCHMON_SERVER="${PATCHMON_SERVER:-https://patchmon.wheelz.lab}" CADDY_CA_INSTALL_URL="${CADDY_CA_INSTALL_URL:-https://gitea.wheelytho.com/Wheelz/Docker-Install/raw/branch/main/install-caddy-local-ca.sh}" CADDY_CA_CERT_PATH="${CADDY_CA_CERT_PATH:-/usr/local/share/ca-certificates/wheelz-caddy-local-root.crt}" ENROLL_TYPE="${PATCHMON_ENROLL_TYPE:-direct-host}" TOKEN_KEY="${PATCHMON_AUTO_ENROLL_KEY:-}" TOKEN_SECRET="${PATCHMON_AUTO_ENROLL_SECRET:-}" FRIENDLY_NAME_VALUE="${FRIENDLY_NAME:-}" FORCE_INSTALL="false" INSTALL_CADDY_CA="true" PRINT_ONLY="false" YES="false" usage() { cat <<'USAGE' Enroll this Linux host into PatchMon using PatchMon's auto-enrollment script. Usage: ./patchmon-enroll.sh [options] curl -fsSL https://gitea.wheelytho.com/Wheelz/Docker-Install/raw/branch/main/patchmon-enroll.sh | sudo bash -s -- [options] Options: --server URL PatchMon base URL. Default: https://patchmon.wheelz.lab --type TYPE Enrollment type: direct-host or proxmox-lxc. Default: direct-host --token-key KEY PatchMon auto-enrollment token key. Can also use PATCHMON_AUTO_ENROLL_KEY. --token-secret SECRET PatchMon auto-enrollment token secret. Can also use PATCHMON_AUTO_ENROLL_SECRET. --friendly-name NAME Friendly name to report to PatchMon. --force Ask PatchMon's generated installer to force/reinstall when supported. --no-caddy-ca Do not install the Wheelz/Caddy internal CA before enrollment. --print-script Download and print the generated PatchMon script instead of running it. -y, --yes Non-interactive. Fail if token values are missing instead of prompting. -h, --help Show this help. Recommended one-liner, prompts for token if env vars are not set: curl -fsSL https://gitea.wheelytho.com/Wheelz/Docker-Install/raw/branch/main/patchmon-enroll.sh | sudo bash Non-interactive one-liner: PATCHMON_AUTO_ENROLL_KEY='' PATCHMON_AUTO_ENROLL_SECRET='' curl -fsSL https://gitea.wheelytho.com/Wheelz/Docker-Install/raw/branch/main/patchmon-enroll.sh | sudo -E bash -s -- --type direct-host -y If a Proxmox host does not trust patchmon.wheelz.lab during bootstrap, pass the raw-IP server as a script argument, not as an env var before curl: curl -fsSL https://gitea.wheelytho.com/Wheelz/Docker-Install/raw/branch/main/patchmon-enroll.sh | bash -s -- --server http://192.168.20.232:3000 --token-key '' --token-secret '' --type direct-host --force -y USAGE } log() { printf '\n[PatchMon-Enroll] %s\n' "$*"; } warn() { printf '\n[PatchMon-Enroll WARNING] %s\n' "$*" >&2; } fail() { printf '\n[PatchMon-Enroll ERROR] %s\n' "$*" >&2; exit 1; } need_cmd() { command -v "$1" >/dev/null 2>&1 || fail "Required command not found: $1" } urlencode() { # POSIX-ish URL encoding via Python, which is present on the supported Ubuntu/Debian hosts. python3 -c 'import sys, urllib.parse; print(urllib.parse.quote(sys.argv[1], safe=""))' "$1" } prompt_secret() { local prompt="$1" local value="" if [[ "$YES" == "true" ]]; then fail "$prompt is required. Provide it with --token-key/--token-secret or PATCHMON_AUTO_ENROLL_KEY/PATCHMON_AUTO_ENROLL_SECRET." fi if [[ -t 0 ]]; then read -r -s -p "$prompt: " value printf '\n' >&2 else if [[ -r /dev/tty ]]; then read -r -s -p "$prompt: " value &2 else fail "$prompt is required, and no TTY is available for prompting." fi fi printf '%s' "$value" } install_caddy_ca_if_needed() { if [[ "$INSTALL_CADDY_CA" != "true" ]]; then return 0 fi case "$PATCHMON_SERVER" in https://patchmon.wheelz.lab|https://patchmon.wheelz.lab/*) log "Installing/updating Wheelz Caddy local root CA before contacting PatchMon" curl -fsSL "$CADDY_CA_INSTALL_URL" | bash ;; *) log "Skipping Caddy CA install because PatchMon server is not patchmon.wheelz.lab" ;; esac } curl_patchmon() { local -a trust_args=() if [[ -f "$CADDY_CA_CERT_PATH" ]]; then # Use the exact Wheelz/Caddy root CA for PatchMon calls. Some minimal # Proxmox/Debian installs update the system store but curl still fails in # the same one-liner; this makes enrollment deterministic. trust_args=(--cacert "$CADDY_CA_CERT_PATH") fi curl "${trust_args[@]}" "$@" } normalize_agent_config() { local cfg="/etc/patchmon/config.yml" [[ -f "$cfg" ]] || return 0 log "Normalizing local PatchMon agent config to canonical FQDN" python3 - "$cfg" "$PATCHMON_SERVER" <<'PY' from pathlib import Path import re import sys p = Path(sys.argv[1]) server = sys.argv[2].rstrip('/') s = p.read_text() s = re.sub(r'^patchmon_server:.*$', f'patchmon_server: {server}', s, flags=re.M) if 'skip_ssl_verify:' in s: s = re.sub(r'^skip_ssl_verify:.*$', 'skip_ssl_verify: false', s, flags=re.M) else: s += '\nskip_ssl_verify: false\n' p.write_text(s) PY } normalize_generated_script() { local generated_script="$1" log "Normalizing generated PatchMon installer to use: $PATCHMON_SERVER" python3 - "$generated_script" "$PATCHMON_SERVER" <<'PY' from pathlib import Path import re import sys p = Path(sys.argv[1]) server = sys.argv[2].rstrip('/') s = p.read_text() # PatchMon may generate installers using its internal/raw-IP origin even when the # wrapper contacted the canonical FQDN. Force the generated installer itself to # enroll/configure the agent against the FQDN. s = re.sub(r'export PATCHMON_URL="[^"]+"', f'export PATCHMON_URL="{server}"', s) s = s.replace('http://192.168.20.232:3000', server) # Make curl inside PatchMon's generated script trust the Wheelz/Caddy root even # on minimal hosts where the CA bundle refresh is not picked up immediately. if 'CURL_CA_BUNDLE=' not in s: s = s.replace( f'export PATCHMON_URL="{server}"', f'export PATCHMON_URL="{server}"\nexport CURL_CA_BUNDLE="${{CURL_CA_BUNDLE:-/usr/local/share/ca-certificates/wheelz-caddy-local-root.crt}}"\nexport SSL_CERT_FILE="${{SSL_CERT_FILE:-/usr/local/share/ca-certificates/wheelz-caddy-local-root.crt}}"', 1, ) # PatchMon's generated auto-enroll script captures the real agent-install # output, but by default only prints a generic "Failed to install agent" line. # Make failures actionable for one-liner installs. s = s.replace( ' error "Failed to install agent (exit: $install_exit_code)"', ' printf "%s\\n" "$install_output" >&2\n error "Failed to install agent (exit: $install_exit_code)"', ) # Direct-host generated installers and proxmox-lxc generated installers use # different nested install shapes. Normalize both. old_nested = ''' # Download and execute installation script install_exit_code=0 install_output=$(curl $CURL_FLAGS \\ -H "X-API-ID: $api_id" \\ -H "X-API-KEY: $api_key" \\ "$install_url" | sh 2>&1) || install_exit_code=$? ''' new_nested = ''' # Download, normalize, and execute installation script install_exit_code=0 nested_install_script=$(mktemp /tmp/patchmon-agent-install.XXXXXX.sh) if curl $CURL_FLAGS \\ -H "X-API-ID: $api_id" \\ -H "X-API-KEY: $api_key" \\ "$install_url" > "$nested_install_script"; then sed -i "s#http://192.168.20.232:3000#$PATCHMON_URL#g" "$nested_install_script" install_output=$(bash "$nested_install_script" 2>&1) || install_exit_code=$? else install_exit_code=1 install_output="Failed to download nested PatchMon agent installer from $install_url" fi rm -f "$nested_install_script" ''' s = s.replace(old_nested, new_nested) # PatchMon's proxmox-lxc generated installer downloads a nested installer inside # each container with `pct exec ... sh -c`. The containers also need the local # Caddy CA before HTTPS calls to patchmon.wheelz.lab work. Inject a host-side CA # push/update step and normalize the nested script inside the container before # running it. old_lxc_install = ''' # Download and execute in separate steps to avoid stdin issues with piping # Pass CURL_FLAGS as environment variable to container # Use sh -c for POSIX compatibility (Alpine uses ash, not bash) install_output=$(timeout 180 pct exec "$vmid" -- sh -c " export CURL_FLAGS='$CURL_FLAGS' cd /tmp curl \\$CURL_FLAGS \\ -H \\"X-API-ID: $api_id\\" \\ -H \\"X-API-KEY: $api_key\\" \\ -o patchmon-install.sh \\ '$install_url' && \\ sh patchmon-install.sh && \\ rm -f patchmon-install.sh " 2>&1 &1 &1 || true)" ca_push_output="$ca_push_output $(timeout 60 pct exec "$vmid" -- sh -c "command -v update-ca-certificates >/dev/null 2>&1 && update-ca-certificates || true" 2>&1 &1 &2 fail "Unexpected response from PatchMon enrollment endpoint" fi normalize_generated_script "$tmp_script" chmod 700 "$tmp_script" if [[ "$PRINT_ONLY" == "true" ]]; then sed -e "s/${TOKEN_SECRET//\//\\/}//g" -e "s/${TOKEN_KEY//\//\\/}//g" "$tmp_script" exit 0 fi log "Running PatchMon enrollment script as type: $ENROLL_TYPE" # PatchMon's generated scripts use bash-specific features. The proxmox-lxc # installer in particular contains `trap ... ERR`, which fails under Debian's # /bin/sh (dash) with `trap: ERR: bad trap`. Always execute the generated # installer with bash. need_cmd bash if [[ -n "$FRIENDLY_NAME_VALUE" ]]; then FRIENDLY_NAME="$FRIENDLY_NAME_VALUE" bash "$tmp_script" else bash "$tmp_script" fi normalize_agent_config if command -v systemctl >/dev/null 2>&1 && systemctl list-unit-files 'patchmon-agent.service' --no-legend 2>/dev/null | grep -q .; then log "Restarting PatchMon agent after config normalization" systemctl restart patchmon-agent || warn "patchmon-agent restart failed; check service logs" fi log "Verifying local PatchMon agent service" if command -v systemctl >/dev/null 2>&1; then if systemctl list-unit-files 'patchmon*' --no-legend 2>/dev/null | grep -q .; then systemctl --no-pager --full status 'patchmon*' || true else warn "No patchmon systemd unit was found. Check the installer output above." fi else warn "systemctl not available; skipping service verification." fi log "Enrollment wrapper completed"