Track tcp/udp dports via deny_port_hits, expose aggregate and per-IP ports in UI; install-link re-run refreshes nft rules. Co-authored-by: Cursor <cursoragent@cursor.com>
314 lines
10 KiB
Bash
314 lines
10 KiB
Bash
#!/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 last_hash), 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; }
|
|
|
|
# JSON parse for enroll; python3 is an acceptable fallback in sync script.
|
|
if ! need_cmd jq && ! need_cmd python3; then
|
|
ensure_pkg jq jq || ensure_pkg python3 python3 || {
|
|
echo "evofw install: need jq or python3" >&2
|
|
exit 1
|
|
}
|
|
fi
|
|
|
|
# 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 (optional companion for iptables)..."
|
|
pkg_install ipset 2>/dev/null || true
|
|
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_SEED:?EVOFW_SEED 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"
|
|
chmod 600 "$CONF_FILE"
|
|
}
|
|
|
|
install_sync_and_uninstall() {
|
|
local sync_tmp=$1
|
|
install -m 755 "$sync_tmp" "$SYNC_SCRIPT"
|
|
if curl -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 <<UNIT
|
|
[Unit]
|
|
Description=EvoFirewall sync timer
|
|
|
|
[Timer]
|
|
OnBootSec=30s
|
|
OnUnitActiveSec=${interval}
|
|
AccuracySec=5s
|
|
Persistent=true
|
|
Unit=evofw-firewall.service
|
|
|
|
[Install]
|
|
WantedBy=timers.target
|
|
UNIT
|
|
systemctl daemon-reload
|
|
systemctl enable --now evofw-firewall.timer
|
|
# First/updated run now (pending → log "pending approval"; after Approve → empty policy is OK).
|
|
systemctl start evofw-firewall.service || true
|
|
else
|
|
(crontab -l 2>/dev/null | grep -v evofw-firewall || true; echo "*/1 * * * * $SYNC_SCRIPT") | crontab -
|
|
"$SYNC_SCRIPT" || true
|
|
fi
|
|
}
|
|
|
|
download_sync_script() {
|
|
local out=$1
|
|
if ! curl -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
|
|
}
|
|
|
|
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"
|
|
# Force one apply after script refresh (nft set upgrade, counters, port hits, etc.).
|
|
rm -f /var/lib/evofw/last_hash
|
|
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 -fsSL ${CP_URL}/v1/agent/uninstall.sh | bash)"
|
|
exit 0
|
|
fi
|
|
|
|
# --- Fresh install (or EVOFW_INSTALL_FORCE=1) ---
|
|
CLIENT_TOKEN="$(gen_token)"
|
|
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
|
|
|
|
if [[ -n "${EVOFW_INSTALL_LINK_ID:-}" ]]; then
|
|
ENROLL_BODY=$(printf '{"name":"%s","hostname":"%s","platform":"%s","token":"%s","client_version":"install.sh/1","install_link_id":"%s"}' \
|
|
"$EVOFW_CLIENT_NAME" "$HOSTNAME" "$PLATFORM" "$CLIENT_TOKEN" "$EVOFW_INSTALL_LINK_ID")
|
|
else
|
|
ENROLL_BODY=$(printf '{"name":"%s","hostname":"%s","platform":"%s","token":"%s","client_version":"install.sh/1"}' \
|
|
"$EVOFW_CLIENT_NAME" "$HOSTNAME" "$PLATFORM" "$CLIENT_TOKEN")
|
|
fi
|
|
|
|
ENROLL_CODE=$(curl -sS -o "$ENROLL_TMP" -w "%{http_code}" -X POST "${CP_URL}/v1/agent/enroll" \
|
|
-H "Content-Type: application/json" \
|
|
-H "X-EvoFW-Seed: ${EVOFW_SEED}" \
|
|
-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"
|
|
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 -fsSL ${CP_URL}/v1/agent/uninstall.sh | bash)"
|