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