#!/usr/bin/env bash # EvoFirewall Linux install one-liner # Re-run on an already-installed host updates scripts/timer **and forces nft # rule re-apply** (clears kernel verification), keeping credentials # (unless EVOFW_INSTALL_FORCE=1 → full re-enroll). set -euo pipefail if [[ "${EUID:-$(id -u)}" -ne 0 ]]; then echo "evofw install: run as root" >&2 exit 1 fi need_cmd() { command -v "$1" >/dev/null 2>&1; } pkg_install() { # $@ = package names (distro-specific callers pass the right names) if need_cmd apt-get; then export DEBIAN_FRONTEND=noninteractive apt-get update -qq apt-get install -y -qq "$@" elif need_cmd dnf; then dnf install -y -q "$@" elif need_cmd yum; then yum install -y -q "$@" elif need_cmd apk; then apk add --no-cache "$@" else echo "evofw install: no supported package manager (apt/dnf/yum/apk)" >&2 return 1 fi } ensure_pkg() { # $1 = command to check; remaining = package name(s) to install if missing local bin=$1 shift if need_cmd "$bin"; then return 0 fi echo "evofw install: installing $* (need $bin)..." pkg_install "$@" || { echo "evofw install: failed to install $* — install manually and retry" >&2 return 1 } need_cmd "$bin" } # curl is required to fetch this script in the first place, but re-check for pipe/bash edge cases. ensure_pkg curl curl || exit 1 need_cmd bash || { echo "missing bash" >&2; exit 1; } # Policy validation and nft batch generation require Python; jq alone is not # sufficient. util-linux supplies flock for serialized timer/manual sync runs. ensure_pkg python3 python3 || exit 1 ensure_pkg flock util-linux || exit 1 ensure_pkg timeout coreutils || exit 1 # Firewall tools: never reinstall/replace if already present. if need_cmd nft; then echo "evofw install: nft already present — skip firewall packages" elif need_cmd iptables; then echo "evofw install: iptables already present — skip nftables/iptables install" if ! need_cmd ipset; then echo "evofw install: installing ipset (required companion for iptables)..." ensure_pkg ipset ipset || exit 1 fi else echo "evofw install: no nft/iptables — installing firewall backend..." if need_cmd apt-get; then pkg_install nftables || pkg_install iptables || true elif need_cmd dnf || need_cmd yum; then pkg_install nftables || pkg_install iptables || true elif need_cmd apk; then pkg_install nftables || pkg_install iptables || true fi if need_cmd iptables && ! need_cmd nft && ! need_cmd ipset; then pkg_install ipset 2>/dev/null || true fi fi HAS_SYSTEMD=0 if need_cmd systemctl && [[ -d /run/systemd/system ]]; then HAS_SYSTEMD=1 fi # Cron only when systemd timer is unavailable. if [[ "$HAS_SYSTEMD" -ne 1 ]]; then if ! need_cmd crontab; then echo "evofw install: no systemd — installing cron..." if need_cmd apt-get; then pkg_install cron || pkg_install cronie || true elif need_cmd dnf || need_cmd yum; then pkg_install cronie || true elif need_cmd apk; then pkg_install cronie || pkg_install dcron || true fi fi if ! need_cmd crontab; then echo "evofw install: crontab missing and could not be installed (need systemd or cron)" >&2 exit 1 fi # Ensure cron daemon is running when we fall back to crontab. if need_cmd systemctl; then systemctl enable --now cron 2>/dev/null || \ systemctl enable --now crond 2>/dev/null || true elif need_cmd service; then service cron start 2>/dev/null || service crond start 2>/dev/null || true fi fi : "${EVOFW_CP_URL:?EVOFW_CP_URL required}" : "${EVOFW_ENROLLMENT_CREDENTIAL:?Scoped EVOFW_ENROLLMENT_CREDENTIAL required}" : "${EVOFW_CLIENT_NAME:?EVOFW_CLIENT_NAME required}" CONF_DIR=/etc/evofw CONF_FILE="${CONF_DIR}/agent.conf" SYNC_SCRIPT=/usr/local/sbin/evofw-firewall.sh UNINSTALL_SCRIPT=/usr/local/sbin/evofw-uninstall.sh PLATFORM="${EVOFW_PLATFORM:-linux}" # Capture CP from install-link / env before any `source` of agent.conf. CP_URL="${EVOFW_CP_URL%/}" LINK_CP_URL="$CP_URL" CLIENT_NAME_FROM_LINK="$EVOFW_CLIENT_NAME" # Always single-quote values so names with spaces are safe under `source`. shell_quote() { local s=$1 s=${s//\'/\'\\\'\'} printf "'%s'" "$s" } gen_token() { if command -v openssl >/dev/null 2>&1; then echo -n "evofw_$(openssl rand -base64 32 | tr '+/' '-_' | tr -d '=')" else echo -n "evofw_$(head -c 32 /dev/urandom | base64 | tr '+/' '-_' | tr -d '=\n')" fi } detect_backend() { if command -v nft >/dev/null 2>&1; then echo nft elif command -v ipset >/dev/null 2>&1 && command -v iptables >/dev/null 2>&1; then echo ipset elif command -v iptables >/dev/null 2>&1; then echo iptables else echo "" fi } write_conf() { local client_id=$1 client_token=$2 client_name=$3 backend=$4 mkdir -p "$CONF_DIR" chmod 700 "$CONF_DIR" { printf 'EVOFW_CP_URL=%s\n' "$(shell_quote "$CP_URL")" printf 'CLIENT_ID=%s\n' "$(shell_quote "$client_id")" printf 'CLIENT_TOKEN=%s\n' "$(shell_quote "$client_token")" printf 'CLIENT_NAME=%s\n' "$(shell_quote "$client_name")" printf 'KERNEL_BACKEND=%s\n' "$(shell_quote "$backend")" } >"${CONF_FILE}.new" chmod 600 "${CONF_FILE}.new" python3 - "${CONF_FILE}.new" "$CONF_FILE" <<'PY_CONF_COMMIT' import os, sys with open(sys.argv[1], 'rb') as value: os.fsync(value.fileno()) os.replace(sys.argv[1], sys.argv[2]) fd = os.open(os.path.dirname(sys.argv[2]), os.O_RDONLY) try: os.fsync(fd) finally: os.close(fd) PY_CONF_COMMIT } install_sync_and_uninstall() { local sync_tmp=$1 bash -n "$sync_tmp" || return 1 install -m 755 "$sync_tmp" "${SYNC_SCRIPT}.new" mv -f "${SYNC_SCRIPT}.new" "$SYNC_SCRIPT" if curl --connect-timeout 10 --max-time 60 -fsSL "${CP_URL}/v1/agent/uninstall.sh" -o "$UNINSTALL_SCRIPT" 2>/dev/null; then chmod 755 "$UNINSTALL_SCRIPT" else echo "evofw install: warning — could not download uninstall.sh (optional)" >&2 fi } enable_scheduler_and_run() { local interval="${EVOFW_SYNC_INTERVAL:-1min}" if [[ "$HAS_SYSTEMD" -eq 1 ]]; then cat >/etc/systemd/system/evofw-firewall.service <<'UNIT' [Unit] Description=EvoFirewall sync After=network-online.target Wants=network-online.target [Service] Type=oneshot ExecStart=/usr/local/sbin/evofw-firewall.sh UNIT cat >/etc/systemd/system/evofw-firewall.timer </dev/null | grep -v evofw-firewall || true; echo "*/1 * * * * $SYNC_SCRIPT") | crontab - "$SYNC_SCRIPT" || true fi } download_sync_script() { local out=$1 if ! curl --connect-timeout 10 --max-time 60 -fsSL "${CP_URL}/v1/agent/sync-script" -o "$out"; then echo "failed to download sync script from ${CP_URL}/v1/agent/sync-script" >&2 return 1 fi if ! head -n1 "$out" | grep -q '^#!'; then echo "sync script is not a shell script (CP returned unexpected body)" >&2 return 1 fi bash -n "$out" } SYNC_TMP=$(mktemp) ENROLL_TMP=$(mktemp) trap 'rm -f "$SYNC_TMP" "$ENROLL_TMP"' EXIT # --- Update path: agent already installed --- if [[ -f "$CONF_FILE" && "${EVOFW_INSTALL_FORCE:-}" != "1" ]]; then echo "evofw update: existing install at $CONF_FILE — refreshing scripts (credentials kept)" # shellcheck disable=SC1090 set -a # shellcheck source=/dev/null source "$CONF_FILE" set +a if [[ -z "${CLIENT_ID:-}" || -z "${CLIENT_TOKEN:-}" ]]; then echo "evofw update: $CONF_FILE incomplete (need CLIENT_ID + CLIENT_TOKEN). Set EVOFW_INSTALL_FORCE=1 to re-enroll." >&2 exit 1 fi # Prefer CP URL from this install link / env (stashed before source). CP_URL="${LINK_CP_URL%/}" CLIENT_NAME="${CLIENT_NAME_FROM_LINK:-${CLIENT_NAME:-unknown}}" BACKEND="$(detect_backend)" if [[ -z "$BACKEND" ]]; then echo "no supported firewall backend" >&2 exit 1 fi download_sync_script "$SYNC_TMP" || exit 1 write_conf "$CLIENT_ID" "$CLIENT_TOKEN" "$CLIENT_NAME" "$BACKEND" install_sync_and_uninstall "$SYNC_TMP" # Invalidate structural verification to force reconciliation after refresh. rm -f /var/lib/evofw/kernel_state enable_scheduler_and_run echo "Updated. Client id=${CLIENT_ID}. Sync script + timer refreshed; nft rules re-applied." echo "Force sync: $SYNC_SCRIPT" echo "Uninstall: $UNINSTALL_SCRIPT (or: curl --connect-timeout 10 --max-time 60 -fsSL ${CP_URL}/v1/agent/uninstall.sh | bash)" exit 0 fi # --- Fresh install (or EVOFW_INSTALL_FORCE=1) --- mkdir -p "$CONF_DIR" /var/lib/evofw chmod 700 "$CONF_DIR" exec 8>/var/lib/evofw/sync.lock flock -x 8 ENROLL_PENDING="$CONF_DIR/enrollment-pending.json" CLIENT_TOKEN=$(python3 - "$ENROLL_PENDING" "$CP_URL" "${EVOFW_INSTALL_LINK_ID:-}" <<'PY_ENROLL_PENDING' import json, os, secrets, sys from pathlib import Path path = Path(sys.argv[1]); origin, link = sys.argv[2:] if not link: raise SystemExit('install link required') if path.exists(): state = json.loads(path.read_text()) if state.get('origin') != origin or state.get('link') != link: raise SystemExit('pending enrollment belongs to another link') else: state = {'origin': origin, 'link': link, 'token': 'evofw_' + secrets.token_urlsafe(32)} fd = os.open(str(path) + '.new', os.O_WRONLY | os.O_CREAT | os.O_TRUNC, 0o600) with os.fdopen(fd, 'w') as out: json.dump(state, out); out.flush(); os.fsync(out.fileno()) os.replace(str(path) + '.new', path) fd = os.open(str(path.parent), os.O_RDONLY) try: os.fsync(fd) finally: os.close(fd) print(state['token']) PY_ENROLL_PENDING ) HOSTNAME="$(hostname -f 2>/dev/null || hostname)" # Fail fast: pull sync script before enroll so we never leave a DB agent without a local agent. download_sync_script "$SYNC_TMP" || exit 1 ENROLL_BODY=$(python3 - "$EVOFW_CLIENT_NAME" "$HOSTNAME" "$PLATFORM" "$CLIENT_TOKEN" "$EVOFW_INSTALL_LINK_ID" <<'PY_ENROLL_BODY' import json, sys name, hostname, platform, token, link = sys.argv[1:] print(json.dumps(dict(name=name, hostname=hostname, platform=platform, token=token, client_version='install.sh/2', install_link_id=link))) PY_ENROLL_BODY ) ENROLL_CODE=$(curl --connect-timeout 10 --max-time 60 -sS -o "$ENROLL_TMP" -w "%{http_code}" -X POST "${CP_URL}/v1/agent/enroll" \ -H "Content-Type: application/json" \ -H "X-EvoFW-Enrollment-Credential: ${EVOFW_ENROLLMENT_CREDENTIAL}" \ -d "$ENROLL_BODY") if [[ "$ENROLL_CODE" != "201" && "$ENROLL_CODE" != "200" ]]; then echo "enroll failed: HTTP ${ENROLL_CODE}" >&2 cat "$ENROLL_TMP" >&2 exit 1 fi RESP=$(cat "$ENROLL_TMP") CLIENT_ID="" if command -v jq >/dev/null 2>&1; then CLIENT_ID=$(echo "$RESP" | jq -r '.client_id // .id') else CLIENT_ID=$(echo "$RESP" | sed -n 's/.*"client_id"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p') fi if [[ -z "$CLIENT_ID" || "$CLIENT_ID" == "null" ]]; then echo "enroll response missing client_id" >&2 cat "$ENROLL_TMP" >&2 exit 1 fi BACKEND="$(detect_backend)" if [[ -z "$BACKEND" ]]; then echo "no supported firewall backend" >&2 exit 1 fi write_conf "$CLIENT_ID" "$CLIENT_TOKEN" "$EVOFW_CLIENT_NAME" "$BACKEND" # Config is committed before pending enrollment can be removed. rm -f "$ENROLL_PENDING" flock -u 8 exec 8>&- install_sync_and_uninstall "$SYNC_TMP" enable_scheduler_and_run echo "Installed. Client id=${CLIENT_ID}. Approve in EvoFirewall UI (rules optional — can assign later)." echo "If still offline after Approve, run: $SYNC_SCRIPT" echo "Re-run the same install URL to update scripts without re-enroll." echo "Uninstall: $UNINSTALL_SCRIPT (or: curl --connect-timeout 10 --max-time 60 -fsSL ${CP_URL}/v1/agent/uninstall.sh | bash)"