Files
EvoFirewall/apps/api/src/agent-scripts/install.sh
T
Denozordec 9fa6269174
quality / changes (push) Successful in 7s
quality / commitlint (push) Skipped
quality / openapi (push) Successful in 30s
quality / web (push) Successful in 1m3s
quality / api (push) Successful in 1m15s
quality / linux-agent-smoke (push) Failing after 27s
quality / docker-check (push) Failing after 2m28s
CD / quality (push) Failing after 5m52s
CD / publish (push) Skipped
feat: harden firewall policies telemetry and operations
Implement reliable agent apply, telemetry v2, analytics retention, guarded rollouts, enrollment, labels and webhooks. Improve ReUI frontend data safety and bounded list loading, and add Docker smoke, recovery and resource checks. Document local verification and pending native acceptance.
2026-10-26 13:10:58 +07:00

351 lines
12 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 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 <<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 --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)"