Slices 1-3 of the local agent IPC surface: - Read model (local_status.go, loop wired): update counts, scanner status, check-in state, token receipt counts — no token material exposed - Local IPC (localapi/): Unix socket (group=redflag-local, 0660) + Windows named pipe (SDDL: LocalSystem/Admins/RedFlagLocal); five read-only endpoints - `redflag-agent -local-status` CLI probe of the local API surface - Screenshot capture handler (screenshot.go, dispatch wired) - Tauri desktop spine (desktop/): tray icon, left-click window, local IPC reader - Desktop React entry (web/src/desktop/LocalAgentApp.tsx, index.desktop.html, vite.desktop.config.ts) - Installer group provisioning: linux.sh creates redflag-local, sets SupplementaryGroups; windows.ps1 creates RedFlagLocal security group - Server-side: screenshot receipt handler on agents, updates handler additions - web/package.json: @tauri-apps/api + tauri CLI dev dep added
854 lines
36 KiB
Go Template
854 lines
36 KiB
Go Template
#!/bin/bash
|
|
# RedFlag Agent Installer - Linux
|
|
# Generated for agent: {{.AgentID}}
|
|
# Platform: {{.Platform}}
|
|
# Architecture: {{.Architecture}}
|
|
# Version: {{.Version}}
|
|
|
|
set -e
|
|
|
|
# Check if running as root (required for user creation and sudoers)
|
|
if [ "$EUID" -ne 0 ]; then
|
|
echo "ERROR: This script must be run as root for secure installation (use sudo)"
|
|
exit 1
|
|
fi
|
|
|
|
# Variables
|
|
AGENT_ID="{{.AgentID}}"
|
|
AGENT_USER="redflag-agent"
|
|
LOCAL_API_GROUP="redflag-local"
|
|
AGENT_HOME="{{.AgentHome}}"
|
|
BASE_DIR="/var/lib/redflag"
|
|
CONFIG_DIR="/etc/redflag"
|
|
AGENT_CONFIG_DIR="/etc/redflag/agent"
|
|
SERVER_KEY_DIR="{{.ServerKeyDir}}"
|
|
OLD_CONFIG_DIR="/etc/aggregator"
|
|
LOG_DIR="/var/log/redflag"
|
|
AGENT_LOG_DIR="/var/log/redflag/agent"
|
|
INSTALL_DIR="/usr/local/bin"
|
|
SERVICE_NAME="redflag-agent"
|
|
SUDOERS_FILE="/etc/sudoers.d/redflag-agent"
|
|
BINARY_URL="{{.BinaryURL}}"
|
|
CONFIG_URL="{{.ConfigURL}}"
|
|
VERSION="{{.Version}}"
|
|
BACKUP_DIR="${CONFIG_DIR}/backups/backup.$(date +%s)"
|
|
|
|
# Detect architecture
|
|
ARCH=$(uname -m)
|
|
case $ARCH in
|
|
x86_64) ARCH_TAG="amd64" ;;
|
|
aarch64) ARCH_TAG="arm64" ;;
|
|
armv7l) ARCH_TAG="armv7" ;;
|
|
*)
|
|
echo "Unsupported architecture: $ARCH"
|
|
echo "Supported: x86_64 (amd64), aarch64 (arm64), armv7l (armv7)"
|
|
exit 1
|
|
;;
|
|
esac
|
|
|
|
# Override download URL with detected architecture
|
|
BINARY_URL="{{.ServerURL}}/api/v1/downloads/linux-${ARCH_TAG}?version={{.Version}}"
|
|
|
|
# Function to detect package manager
|
|
detect_package_manager() {
|
|
if command -v apt-get &> /dev/null; then
|
|
echo "apt"
|
|
elif command -v dnf &> /dev/null; then
|
|
echo "dnf"
|
|
elif command -v yum &> /dev/null; then
|
|
echo "yum"
|
|
elif command -v pacman &> /dev/null; then
|
|
echo "pacman"
|
|
elif command -v zypper &> /dev/null; then
|
|
echo "zypper"
|
|
else
|
|
echo "unknown"
|
|
fi
|
|
}
|
|
|
|
echo "=== RedFlag Agent v${VERSION} Installation ==="
|
|
echo "Agent ID: ${AGENT_ID}"
|
|
echo "Platform: {{.Platform}}"
|
|
echo "Installing to: ${INSTALL_DIR}/${SERVICE_NAME}"
|
|
echo
|
|
|
|
# Step 1: Detect existing installation
|
|
echo "Detecting existing RedFlag installations..."
|
|
MIGRATION_NEEDED=false
|
|
|
|
if [ -f "${CONFIG_DIR}/config.json" ]; then
|
|
echo "✓ Existing installation detected at ${CONFIG_DIR}"
|
|
MIGRATION_NEEDED=true
|
|
elif [ -f "${OLD_CONFIG_DIR}/config.json" ]; then
|
|
echo "⚠ Old installation detected at ${OLD_CONFIG_DIR} - MIGRATION REQUIRED"
|
|
MIGRATION_NEEDED=true
|
|
else
|
|
echo "✓ Fresh installation"
|
|
fi
|
|
|
|
# Step 2: Create backup if migration needed
|
|
if [ "${MIGRATION_NEEDED}" = true ]; then
|
|
echo
|
|
echo "=== Migration Required ==="
|
|
echo "Agent will migrate on first start. Backing up configuration..."
|
|
sudo mkdir -p "${BACKUP_DIR}"
|
|
|
|
if [ -f "${OLD_CONFIG_DIR}/config.json" ]; then
|
|
echo "Backing up old configuration..."
|
|
sudo cp -r "${OLD_CONFIG_DIR}"/* "${BACKUP_DIR}/" 2>/dev/null || true
|
|
fi
|
|
|
|
if [ -f "${CONFIG_DIR}/config.json" ]; then
|
|
echo "Backing up current configuration..."
|
|
sudo cp "${CONFIG_DIR}/config.json" "${BACKUP_DIR}/config.json.backup" 2>/dev/null || true
|
|
fi
|
|
|
|
echo "Migration will run automatically when agent starts."
|
|
echo "View migration logs with: sudo journalctl -u ${SERVICE_NAME} -f"
|
|
echo
|
|
fi
|
|
|
|
# Step 3: Create system user and home directory
|
|
echo "Creating system user for agent..."
|
|
if id "$AGENT_USER" &>/dev/null; then
|
|
echo "✓ User $AGENT_USER already exists"
|
|
else
|
|
sudo useradd -r -s /bin/false -d "$AGENT_HOME" "$AGENT_USER"
|
|
echo "✓ User $AGENT_USER created"
|
|
fi
|
|
|
|
echo "Creating local API access group..."
|
|
if getent group "$LOCAL_API_GROUP" >/dev/null 2>&1; then
|
|
echo "✓ Group $LOCAL_API_GROUP already exists"
|
|
else
|
|
sudo groupadd --system "$LOCAL_API_GROUP"
|
|
echo "✓ Group $LOCAL_API_GROUP created"
|
|
fi
|
|
|
|
if id -nG "$AGENT_USER" 2>/dev/null | tr ' ' '\n' | grep -qx "$LOCAL_API_GROUP"; then
|
|
echo "✓ $AGENT_USER already in $LOCAL_API_GROUP group"
|
|
else
|
|
sudo usermod -aG "$LOCAL_API_GROUP" "$AGENT_USER"
|
|
echo "✓ Added $AGENT_USER to $LOCAL_API_GROUP group (local API socket ownership)"
|
|
fi
|
|
|
|
# Grant docker socket access so the container scanner can reach the daemon.
|
|
# Sudoers entries below only cover pull/inspect; IsAvailable() pings the
|
|
# unix socket, which is root:docker. Membership is the standard pattern.
|
|
if getent group docker >/dev/null 2>&1; then
|
|
if id -nG "$AGENT_USER" 2>/dev/null | tr ' ' '\n' | grep -qx docker; then
|
|
echo "✓ $AGENT_USER already in docker group"
|
|
else
|
|
sudo usermod -aG docker "$AGENT_USER"
|
|
echo "✓ Added $AGENT_USER to docker group (socket access for container scanner)"
|
|
fi
|
|
else
|
|
echo "[INFO] [installer] [docker] docker group absent — container scanner will report unavailable"
|
|
fi
|
|
|
|
# Create home directory structure
|
|
if [ ! -d "$AGENT_HOME" ]; then
|
|
# Create nested directory structure
|
|
sudo mkdir -p "$BASE_DIR"
|
|
sudo mkdir -p "$AGENT_HOME"
|
|
sudo mkdir -p "$AGENT_HOME/cache"
|
|
sudo mkdir -p "$AGENT_HOME/state"
|
|
sudo mkdir -p "$AGENT_HOME/localapi"
|
|
sudo mkdir -p "$AGENT_CONFIG_DIR"
|
|
sudo mkdir -p "$SERVER_KEY_DIR"
|
|
sudo mkdir -p "$AGENT_LOG_DIR"
|
|
|
|
# Set ownership and permissions
|
|
sudo chown -R "$AGENT_USER:$AGENT_USER" "$BASE_DIR"
|
|
sudo chown "$AGENT_USER:$LOCAL_API_GROUP" "$BASE_DIR"
|
|
sudo chown "$AGENT_USER:$LOCAL_API_GROUP" "$AGENT_HOME"
|
|
sudo chmod 710 "$BASE_DIR"
|
|
sudo chmod 710 "$AGENT_HOME"
|
|
sudo chmod 750 "$AGENT_HOME/cache"
|
|
sudo chmod 750 "$AGENT_HOME/state"
|
|
sudo chown "$AGENT_USER:$LOCAL_API_GROUP" "$AGENT_HOME/localapi"
|
|
sudo chmod 750 "$AGENT_HOME/localapi"
|
|
sudo chmod 755 "$AGENT_CONFIG_DIR"
|
|
sudo chown "$AGENT_USER:$AGENT_USER" "$SERVER_KEY_DIR"
|
|
sudo chmod 755 "$SERVER_KEY_DIR"
|
|
sudo chmod 755 "$AGENT_LOG_DIR"
|
|
|
|
echo "✓ Agent directory structure created:"
|
|
echo " - Agent home: $AGENT_HOME"
|
|
echo " - Config: $AGENT_CONFIG_DIR"
|
|
echo " - Server key cache: $SERVER_KEY_DIR"
|
|
echo " - Logs: $AGENT_LOG_DIR"
|
|
fi
|
|
|
|
# Step 4: Install sudoers configuration with OS-specific commands
|
|
PM=$(detect_package_manager)
|
|
echo "Detected package manager: $PM"
|
|
echo "Installing sudoers configuration..."
|
|
|
|
case "$PM" in
|
|
apt)
|
|
cat <<'EOF' | sudo tee "$SUDOERS_FILE" > /dev/null
|
|
# RedFlag Agent minimal sudo permissions - APT
|
|
# apt discovery needs no sudo — it runs unprivileged with lists/cache/state
|
|
# redirected to an agent-writable temp dir (installer/discovery.go). No apt grants.
|
|
|
|
# Disable lecture (first-time prompt) — agent runs as a service with no TTY
|
|
Defaults:{{.AgentUser}} !lecture
|
|
|
|
# Mutation — ONLY through the capability-token executor
|
|
{{.AgentUser}} ALL=(root) NOPASSWD: /usr/bin/systemd-run --wait --property=ProtectSystem=no -- /usr/local/bin/redflag-helper --token-file /var/lib/redflag/agent/tokens/* --result-file /var/lib/redflag/agent/results/*
|
|
EOF
|
|
;;
|
|
dnf|yum)
|
|
cat <<'EOF' | sudo tee "$SUDOERS_FILE" > /dev/null
|
|
# RedFlag Agent minimal sudo permissions - DNF/YUM
|
|
# DNF discovery needs no sudo — it runs unprivileged with log/cache redirected
|
|
# to an agent-writable temp dir (installer/discovery.go). No dnf discovery grants.
|
|
|
|
# Disable lecture (first-time prompt) — agent runs as a service with no TTY
|
|
Defaults:{{.AgentUser}} !lecture
|
|
|
|
# Mutation — ONLY through the capability-token executor
|
|
{{.AgentUser}} ALL=(root) NOPASSWD: /usr/bin/systemd-run --wait --property=ProtectSystem=no -- /usr/local/bin/redflag-helper --token-file /var/lib/redflag/agent/tokens/* --result-file /var/lib/redflag/agent/results/*
|
|
EOF
|
|
;;
|
|
pacman)
|
|
cat <<'EOF' | sudo tee "$SUDOERS_FILE" > /dev/null
|
|
# RedFlag Agent minimal sudo permissions - Pacman
|
|
# Discovery — non-mutating (read-only)
|
|
{{.AgentUser}} ALL=(root) NOPASSWD: /usr/bin/pacman -Sy
|
|
|
|
# Disable lecture (first-time prompt) — agent runs as a service with no TTY
|
|
Defaults:{{.AgentUser}} !lecture
|
|
|
|
# Mutation — ONLY through the capability-token executor
|
|
{{.AgentUser}} ALL=(root) NOPASSWD: /usr/bin/systemd-run --wait --property=ProtectSystem=no -- /usr/local/bin/redflag-helper --token-file /var/lib/redflag/agent/tokens/* --result-file /var/lib/redflag/agent/results/*
|
|
EOF
|
|
;;
|
|
*)
|
|
cat <<'EOF' | sudo tee "$SUDOERS_FILE" > /dev/null
|
|
# RedFlag Agent minimal sudo permissions - Generic (APT and DNF)
|
|
# Both apt and dnf discovery run unprivileged (lists/log/cache redirected to an
|
|
# agent-writable temp dir, installer/discovery.go). No discovery grants.
|
|
|
|
# Disable lecture (first-time prompt) — agent runs as a service with no TTY
|
|
Defaults:{{.AgentUser}} !lecture
|
|
|
|
# Mutation — ONLY through the capability-token executor
|
|
{{.AgentUser}} ALL=(root) NOPASSWD: /usr/bin/systemd-run --wait --property=ProtectSystem=no -- /usr/local/bin/redflag-helper --token-file /var/lib/redflag/agent/tokens/* --result-file /var/lib/redflag/agent/results/*
|
|
EOF
|
|
;;
|
|
esac
|
|
|
|
# No Docker grants: the agent reaches the docker socket via the docker group
|
|
# (added above), so docker discovery/pull need no sudo.
|
|
# No self-update grants: the binary swap (cp/.bak/chmod/systemctl restart) is
|
|
# performed by the privileged helper under an agent-self capability token, not by
|
|
# the agent. The single systemd-run helper line above is the agent's only sudo.
|
|
|
|
sudo chmod 440 "$SUDOERS_FILE"
|
|
if visudo -c -f "$SUDOERS_FILE" &>/dev/null; then
|
|
echo "✓ Sudoers configuration installed and validated"
|
|
else
|
|
echo "⚠ Sudoers configuration validation failed - using generic version"
|
|
fi
|
|
|
|
# Step 4b: Install polkit rule for transient unit management
|
|
# The agent's only sudo is `systemd-run --pipe ... redflag-helper`. systemd-run
|
|
# spawns a transient unit over the system D-Bus, which polkit gates behind
|
|
# org.freedesktop.systemd1.manage-units (auth_admin). polkit evaluates the
|
|
# original caller ({{.AgentUser}}), so without this rule the helper invocation —
|
|
# and therefore every gated install and the agent self-update — is denied on a
|
|
# TTY-less service. The sudoers line above already pins the exact command; this
|
|
# grants the matching D-Bus permission to the same user, nothing wider.
|
|
POLKIT_RULES_DIR="/etc/polkit-1/rules.d"
|
|
POLKIT_RULE_FILE="${POLKIT_RULES_DIR}/50-redflag-agent.rules"
|
|
if [ -d "$POLKIT_RULES_DIR" ]; then
|
|
cat <<'EOF' | sudo tee "$POLKIT_RULE_FILE" > /dev/null
|
|
// RedFlag Agent — allow the service user to manage transient systemd units only.
|
|
// Scope: manage-transient-units (not manage-units). The agent can only start/stop
|
|
// units it creates via systemd-run, not arbitrary system services. The matching
|
|
// sudoers grant pins the command line to the helper invocation.
|
|
polkit.addRule(function(action, subject) {
|
|
if (action.id == "org.freedesktop.systemd1.manage-transient-units" &&
|
|
subject.user == "{{.AgentUser}}") {
|
|
return polkit.Result.YES;
|
|
}
|
|
});
|
|
EOF
|
|
sudo chmod 644 "$POLKIT_RULE_FILE"
|
|
echo "✓ Polkit rule installed (transient unit management for {{.AgentUser}})"
|
|
else
|
|
echo "⚠ ${POLKIT_RULES_DIR} not found — polkit JS rules unsupported on this host."
|
|
echo " Gated installs and agent self-update will be denied until a polkit rule"
|
|
echo " granting org.freedesktop.systemd1.manage-units to {{.AgentUser}} is added."
|
|
fi
|
|
|
|
# Step 5: Stop existing service
|
|
if systemctl is-active --quiet ${SERVICE_NAME} 2>/dev/null; then
|
|
echo "Stopping existing RedFlag agent service..."
|
|
sudo systemctl stop ${SERVICE_NAME}
|
|
fi
|
|
|
|
# Step 6: Create directories
|
|
echo "Creating directories..."
|
|
sudo mkdir -p "${AGENT_CONFIG_DIR}"
|
|
sudo mkdir -p "${CONFIG_DIR}/backups" # Legacy backup location
|
|
sudo mkdir -p "${SERVER_KEY_DIR}" # Server public key cache (TOFU model)
|
|
sudo mkdir -p "$AGENT_HOME"
|
|
sudo mkdir -p "$AGENT_LOG_DIR"
|
|
|
|
# Step 7: Download agent binary
|
|
echo "Downloading agent binary..."
|
|
TMP_BINARY=$(mktemp)
|
|
TMP_HEADERS=$(mktemp)
|
|
curl -fsSL -o "$TMP_BINARY" -D "$TMP_HEADERS" "${BINARY_URL}"
|
|
|
|
# --- Cold-start trust: verify the binary against the signed release manifest ---
|
|
# The manifest is one Ed25519-signed document listing the expected SHA-256 of
|
|
# every released binary per platform/arch. We verify the manifest signature with
|
|
# the embedded server public key, then confirm the downloaded binary matches its
|
|
# manifest entry BEFORE executing anything. Fail-closed: any failure removes the
|
|
# binary and exits non-zero. This is the install-time counterpart to the
|
|
# self-upgrade hash check — it closes the gap where first-install ran an
|
|
# unverified binary.
|
|
PLATFORM_TAG="linux"
|
|
SERVER_PUBKEY="{{.ServerPublicKey}}"
|
|
MANIFEST_URL="{{.ServerURL}}/api/v1/manifest?version=${VERSION}"
|
|
|
|
if [ -z "$SERVER_PUBKEY" ]; then
|
|
echo "ERROR: No server public key embedded in installer — cannot establish cold-start trust."
|
|
rm -f "$TMP_BINARY" "$TMP_HEADERS"
|
|
exit 1
|
|
fi
|
|
|
|
# Ensure an Ed25519 verifier (python3-cryptography) is available.
|
|
if ! python3 -c "from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PublicKey" 2>/dev/null; then
|
|
echo "Installing python3-cryptography for manifest verification..."
|
|
case "$PM" in
|
|
apt) apt-get install -y python3-cryptography 2>/dev/null || true ;;
|
|
dnf|yum) dnf install -y python3-cryptography 2>/dev/null || true ;;
|
|
pacman) pacman -S --noconfirm python-cryptography 2>/dev/null || true ;;
|
|
*) pip3 install cryptography 2>/dev/null || true ;;
|
|
esac
|
|
fi
|
|
if ! python3 -c "from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PublicKey" 2>/dev/null; then
|
|
echo "ERROR: Could not provide python3-cryptography — cannot verify the release manifest."
|
|
rm -f "$TMP_BINARY" "$TMP_HEADERS"
|
|
exit 1
|
|
fi
|
|
|
|
TMP_MANIFEST=$(mktemp)
|
|
TMP_MANIFEST_HDR=$(mktemp)
|
|
if ! curl -fsSL -o "$TMP_MANIFEST" -D "$TMP_MANIFEST_HDR" "$MANIFEST_URL"; then
|
|
echo "ERROR: Failed to fetch release manifest from ${MANIFEST_URL}"
|
|
rm -f "$TMP_BINARY" "$TMP_HEADERS" "$TMP_MANIFEST" "$TMP_MANIFEST_HDR"
|
|
exit 1
|
|
fi
|
|
MANIFEST_SIG=$(grep -i "x-content-signature" "$TMP_MANIFEST_HDR" | awk '{print $2}' | tr -d '\r\n')
|
|
rm -f "$TMP_MANIFEST_HDR"
|
|
if [ -z "$MANIFEST_SIG" ]; then
|
|
echo "ERROR: Release manifest is unsigned (no X-Content-Signature header) — refusing to install."
|
|
rm -f "$TMP_BINARY" "$TMP_HEADERS" "$TMP_MANIFEST"
|
|
exit 1
|
|
fi
|
|
|
|
# Verifier: checks the Ed25519 signature over the manifest bytes, then prints the
|
|
# expected sha256 for this platform/arch (exit 2 = bad signature, 3 = no entry).
|
|
MANIFEST_VERIFY=$(mktemp)
|
|
cat <<'MANIFEST_EOF' > "$MANIFEST_VERIFY"
|
|
#!/usr/bin/env python3
|
|
import sys, json
|
|
from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PublicKey
|
|
from cryptography.exceptions import InvalidSignature
|
|
|
|
pubkey_path, manifest_path, sig_path, platform, arch = sys.argv[1:6]
|
|
with open(pubkey_path) as f: pub = bytes.fromhex(f.read().strip())
|
|
with open(sig_path) as f: sig = bytes.fromhex(f.read().strip())
|
|
with open(manifest_path, 'rb') as f: body = f.read()
|
|
try:
|
|
Ed25519PublicKey.from_public_bytes(pub).verify(sig, body)
|
|
except InvalidSignature:
|
|
print("manifest signature invalid", file=sys.stderr); sys.exit(2)
|
|
except Exception as e:
|
|
print("manifest verify error: %s" % e, file=sys.stderr); sys.exit(2)
|
|
m = json.loads(body)
|
|
for a in m.get("artifacts", []):
|
|
if a.get("platform") == platform and a.get("architecture") == arch:
|
|
print(a.get("sha256", "")); sys.exit(0)
|
|
print("no manifest entry for %s/%s" % (platform, arch), file=sys.stderr); sys.exit(3)
|
|
MANIFEST_EOF
|
|
|
|
echo "$SERVER_PUBKEY" > "${TMP_MANIFEST}.pub"
|
|
echo "$MANIFEST_SIG" > "${TMP_MANIFEST}.sig"
|
|
set +e
|
|
EXPECTED_HASH=$(python3 "$MANIFEST_VERIFY" "${TMP_MANIFEST}.pub" "$TMP_MANIFEST" "${TMP_MANIFEST}.sig" "$PLATFORM_TAG" "$ARCH_TAG")
|
|
MANIFEST_RC=$?
|
|
set -e
|
|
rm -f "$MANIFEST_VERIFY" "${TMP_MANIFEST}.pub" "${TMP_MANIFEST}.sig" "$TMP_MANIFEST"
|
|
if [ $MANIFEST_RC -ne 0 ] || [ -z "$EXPECTED_HASH" ]; then
|
|
echo "ERROR: Release manifest verification failed (rc=$MANIFEST_RC) — refusing to install."
|
|
rm -f "$TMP_BINARY" "$TMP_HEADERS"
|
|
exit 1
|
|
fi
|
|
|
|
ACTUAL_HASH=$(sha256sum "$TMP_BINARY" | awk '{print $1}')
|
|
if [ "$EXPECTED_HASH" != "$ACTUAL_HASH" ]; then
|
|
echo "ERROR: Binary hash does not match the signed manifest — possible tampering."
|
|
echo " expected (signed manifest): $EXPECTED_HASH"
|
|
echo " actual (downloaded): $ACTUAL_HASH"
|
|
rm -f "$TMP_BINARY" "$TMP_HEADERS"
|
|
exit 1
|
|
fi
|
|
echo "✓ Cold-start trust established: binary matches signed release manifest ($ACTUAL_HASH)"
|
|
# --- end cold-start manifest verification ---
|
|
|
|
# Verify checksum if server provided one
|
|
EXPECTED_CHECKSUM=$(grep -i "x-content-sha256" "$TMP_HEADERS" | awk '{print $2}' | tr -d '\r\n')
|
|
if [ -n "$EXPECTED_CHECKSUM" ]; then
|
|
ACTUAL_CHECKSUM=$(sha256sum "$TMP_BINARY" | awk '{print $1}')
|
|
if [ "$EXPECTED_CHECKSUM" != "$ACTUAL_CHECKSUM" ]; then
|
|
echo "ERROR: Checksum verification failed"
|
|
echo "Expected: $EXPECTED_CHECKSUM"
|
|
echo "Actual: $ACTUAL_CHECKSUM"
|
|
rm -f "$TMP_BINARY" "$TMP_HEADERS"
|
|
exit 1
|
|
fi
|
|
echo "Checksum verified: $ACTUAL_CHECKSUM"
|
|
else
|
|
echo "WARNING: Server did not provide checksum header. Proceeding without verification."
|
|
fi
|
|
|
|
# ISSUE-002: Save signature and public key for agent verification
|
|
EXPECTED_SIGNATURE=$(grep -i "x-content-signature" "$TMP_HEADERS" | awk '{print $2}' | tr -d '\r\n')
|
|
if [ -n "$EXPECTED_SIGNATURE" ]; then
|
|
echo "Signature received, saving for agent verification"
|
|
echo "$EXPECTED_SIGNATURE" | sudo tee "${SERVER_KEY_DIR}/initial_binary.sig" > /dev/null
|
|
sudo chown {{.AgentUser}}:{{.AgentUser}} "${SERVER_KEY_DIR}/initial_binary.sig"
|
|
else
|
|
echo "WARNING: Server did not provide signature header"
|
|
fi
|
|
|
|
# Save server public key for TOFU model (hex-encoded; verify script reads it as hex)
|
|
if [ -n "{{.ServerPublicKey}}" ]; then
|
|
echo -n "{{.ServerPublicKey}}" | sudo tee "${SERVER_KEY_DIR}/server_public_key" > /dev/null
|
|
sudo chown {{.AgentUser}}:{{.AgentUser}} "${SERVER_KEY_DIR}/server_public_key"
|
|
echo "Server public key saved for TOFU verification"
|
|
fi
|
|
|
|
rm -f "$TMP_HEADERS"
|
|
|
|
# ISSUE-002: Verify binary signature before installation (TOFU model)
|
|
if [ -n "$EXPECTED_SIGNATURE" ] && [ -n "{{.ServerPublicKey}}" ]; then
|
|
echo "Verifying binary signature..."
|
|
|
|
# Ensure Ed25519 verification library is available.
|
|
# The ancient 'ed25519' PyPI package is incompatible with Python >=3.12
|
|
# (removed SafeConfigParser). Use 'cryptography' which is the modern,
|
|
# actively maintained alternative.
|
|
if ! python3 -c "from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PublicKey" 2>/dev/null; then
|
|
echo "Installing python3-cryptography for Ed25519 verification..."
|
|
case "$PM" in
|
|
apt)
|
|
apt-get install -y python3-cryptography 2>/dev/null
|
|
;;
|
|
dnf|yum)
|
|
dnf install -y python3-cryptography 2>/dev/null
|
|
;;
|
|
pacman)
|
|
pacman -S --noconfirm python-cryptography 2>/dev/null
|
|
;;
|
|
*)
|
|
pip3 install cryptography 2>/dev/null
|
|
;;
|
|
esac
|
|
# Verify the install succeeded
|
|
if ! python3 -c "from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PublicKey" 2>/dev/null; then
|
|
echo "ERROR: Failed to install python3-cryptography. Cannot verify binary signature."
|
|
echo "Install manually: pip3 install cryptography"
|
|
rm -f "$TMP_BINARY" "$TMP_HEADERS" "${SERVER_KEY_DIR}/initial_binary.sig"
|
|
exit 1
|
|
fi
|
|
echo "✓ python3-cryptography installed"
|
|
fi
|
|
|
|
# Create temporary verification script
|
|
VERIFY_SCRIPT=$(mktemp)
|
|
cat <<'VERIFY_EOF' > "$VERIFY_SCRIPT"
|
|
#!/usr/bin/env python3
|
|
"""Verify an Ed25519 signature over a binary using the cryptography library."""
|
|
import sys
|
|
|
|
try:
|
|
from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PublicKey
|
|
from cryptography.exceptions import InvalidSignature
|
|
except ImportError:
|
|
print("Ed25519 verification requires 'cryptography' library", file=sys.stderr)
|
|
print("Install: pip3 install cryptography", file=sys.stderr)
|
|
sys.exit(1)
|
|
|
|
try:
|
|
# Read public key (raw hex-encoded Ed25519 public key)
|
|
with open(sys.argv[1], 'r') as f:
|
|
pubkey_hex = f.read().strip()
|
|
pubkey = bytes.fromhex(pubkey_hex)
|
|
|
|
# Read signature (raw hex-encoded Ed25519 signature)
|
|
with open(sys.argv[2], 'r') as f:
|
|
sig_hex = f.read().strip()
|
|
signature = bytes.fromhex(sig_hex)
|
|
|
|
# Read binary
|
|
with open(sys.argv[3], 'rb') as f:
|
|
binary = f.read()
|
|
|
|
# Verify
|
|
verifying_key = Ed25519PublicKey.from_public_bytes(pubkey)
|
|
verifying_key.verify(signature, binary)
|
|
sys.exit(0)
|
|
except Exception as e:
|
|
print(f"Signature verification failed: {e}", file=sys.stderr)
|
|
sys.exit(1)
|
|
VERIFY_EOF
|
|
chmod +x "$VERIFY_SCRIPT"
|
|
|
|
if python3 "$VERIFY_SCRIPT" "${SERVER_KEY_DIR}/server_public_key" "${SERVER_KEY_DIR}/initial_binary.sig" "$TMP_BINARY"; then
|
|
echo "✓ Binary signature verified (Ed25519)"
|
|
rm -f "$VERIFY_SCRIPT"
|
|
else
|
|
echo "ERROR: Binary signature verification failed - possible tampering"
|
|
echo "Expected: Trust on First Use (TOFU) verification failed"
|
|
rm -f "$TMP_BINARY" "$VERIFY_SCRIPT" "${SERVER_KEY_DIR}/initial_binary.sig"
|
|
exit 1
|
|
fi
|
|
else
|
|
echo "WARNING: Cannot verify signature - missing public key or signature"
|
|
echo "This is a security risk. Ensure server is trusted."
|
|
fi
|
|
|
|
sudo mv "$TMP_BINARY" "${INSTALL_DIR}/${SERVICE_NAME}"
|
|
sudo chmod +x "${INSTALL_DIR}/${SERVICE_NAME}"
|
|
|
|
# Fix SELinux context if restorecon is available (RHEL/Fedora/CentOS)
|
|
if command -v restorecon &>/dev/null; then
|
|
sudo restorecon "${INSTALL_DIR}/${SERVICE_NAME}" 2>/dev/null || true
|
|
fi
|
|
|
|
# Step 7b: Install the capability-gate executor (redflag-helper)
|
|
# The helper runs as root via systemd-run and changes package state only with a
|
|
# server-signed capability token. It is tamper-verified at install exactly like
|
|
# the agent: download, verify the server's Ed25519 signature over the binary
|
|
# against the TOFU-pinned public key, then install root-owned and NOT writable
|
|
# by the agent user — the agent must never be able to replace its own privileged
|
|
# executor. The whole block is gated on signing being enabled (no server key →
|
|
# no tokens are ever minted → the helper is never invoked), so a non-signing
|
|
# install degrades cleanly to Tier-1 update management. Fail-closed otherwise.
|
|
if [ -n "{{.ServerPublicKey}}" ]; then
|
|
HELPER_BIN="${INSTALL_DIR}/redflag-helper"
|
|
HELPER_URL="{{.ServerURL}}/api/v1/helper/${ARCH_TAG}?version=${VERSION}"
|
|
echo "Installing capability-gate executor (redflag-helper)..."
|
|
TMP_HELPER=$(mktemp)
|
|
TMP_HELPER_HDR=$(mktemp)
|
|
if ! curl -fsSL -o "$TMP_HELPER" -D "$TMP_HELPER_HDR" "$HELPER_URL"; then
|
|
echo "ERROR: Failed to download redflag-helper from ${HELPER_URL}"
|
|
rm -f "$TMP_HELPER" "$TMP_HELPER_HDR"
|
|
exit 1
|
|
fi
|
|
HELPER_SIG=$(grep -i "x-content-signature" "$TMP_HELPER_HDR" | awk '{print $2}' | tr -d '\r\n')
|
|
HELPER_SHA=$(grep -i "x-content-sha256" "$TMP_HELPER_HDR" | awk '{print $2}' | tr -d '\r\n')
|
|
rm -f "$TMP_HELPER_HDR"
|
|
|
|
if [ -z "$HELPER_SIG" ] || [ ! -s "${SERVER_KEY_DIR}/server_public_key" ]; then
|
|
echo "ERROR: redflag-helper is unsigned or no pinned server key — refusing to install."
|
|
rm -f "$TMP_HELPER"
|
|
exit 1
|
|
fi
|
|
|
|
# Verify the per-binary Ed25519 signature against the TOFU-pinned key (hex).
|
|
HELPER_VERIFY=$(mktemp)
|
|
cat <<'HVERIFY_EOF' > "$HELPER_VERIFY"
|
|
#!/usr/bin/env python3
|
|
import sys
|
|
from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PublicKey
|
|
with open(sys.argv[1]) as f: pub = bytes.fromhex(f.read().strip())
|
|
with open(sys.argv[2]) as f: sig = bytes.fromhex(f.read().strip())
|
|
with open(sys.argv[3], 'rb') as f: body = f.read()
|
|
Ed25519PublicKey.from_public_bytes(pub).verify(sig, body)
|
|
HVERIFY_EOF
|
|
echo "$HELPER_SIG" > "${TMP_HELPER}.sig"
|
|
if python3 "$HELPER_VERIFY" "${SERVER_KEY_DIR}/server_public_key" "${TMP_HELPER}.sig" "$TMP_HELPER"; then
|
|
echo "✓ redflag-helper signature verified (Ed25519)"
|
|
else
|
|
echo "ERROR: redflag-helper signature verification failed — possible tampering."
|
|
rm -f "$TMP_HELPER" "$HELPER_VERIFY" "${TMP_HELPER}.sig"
|
|
exit 1
|
|
fi
|
|
rm -f "$HELPER_VERIFY" "${TMP_HELPER}.sig"
|
|
|
|
if [ -n "$HELPER_SHA" ]; then
|
|
ACTUAL_HELPER_SHA=$(sha256sum "$TMP_HELPER" | awk '{print $1}')
|
|
if [ "$HELPER_SHA" != "$ACTUAL_HELPER_SHA" ]; then
|
|
echo "ERROR: redflag-helper hash mismatch (expected $HELPER_SHA, got $ACTUAL_HELPER_SHA)."
|
|
rm -f "$TMP_HELPER"
|
|
exit 1
|
|
fi
|
|
fi
|
|
|
|
sudo mv "$TMP_HELPER" "$HELPER_BIN"
|
|
sudo chown root:root "$HELPER_BIN"
|
|
sudo chmod 755 "$HELPER_BIN"
|
|
if command -v restorecon &>/dev/null; then
|
|
sudo restorecon "$HELPER_BIN" 2>/dev/null || true
|
|
fi
|
|
echo "✓ redflag-helper installed at $HELPER_BIN"
|
|
|
|
# Helper state + staging dir, root-only. The helper copies the agent-supplied
|
|
# upgrade binary here, hashes it, then installs from this copy — so the dir
|
|
# MUST NOT be writable by the agent user, or a compromised agent could swap
|
|
# the verified bytes between hash-check and install (TOCTOU) and get an
|
|
# arbitrary binary installed as root. 0700 root:root closes that window; the
|
|
# helper otherwise creates it at runtime with an inherited umask, which is not
|
|
# a guarantee. Holds upgrade-staging.bin* and consumed-tokens (replay state).
|
|
sudo mkdir -p "${BASE_DIR}/helper"
|
|
sudo chown root:root "${BASE_DIR}/helper"
|
|
sudo chmod 700 "${BASE_DIR}/helper"
|
|
echo "✓ helper state dir secured at ${BASE_DIR}/helper (root:root 0700)"
|
|
|
|
# Trusted-keys keyring: the helper verifies capability-token signatures
|
|
# against the server's Ed25519 signing key, resolved by key_id from *.pub
|
|
# hex files here. Same key the agent pinned for TOFU; the helper reads it
|
|
# independently as root (its systemd-run unit has a clean environment).
|
|
HELPER_KEYRING_DIR="${CONFIG_DIR}/trusted-keys"
|
|
sudo mkdir -p "$HELPER_KEYRING_DIR"
|
|
echo -n "{{.ServerPublicKey}}" | sudo tee "${HELPER_KEYRING_DIR}/server.pub" > /dev/null
|
|
sudo chown -R root:root "$HELPER_KEYRING_DIR"
|
|
sudo chmod 755 "$HELPER_KEYRING_DIR"
|
|
sudo chmod 644 "${HELPER_KEYRING_DIR}/server.pub"
|
|
echo "✓ Helper trusted-keys keyring provisioned"
|
|
|
|
# Replay-guard state dir (consumed token ids). Root-only.
|
|
HELPER_STATE_DIR="${BASE_DIR}/helper"
|
|
sudo mkdir -p "$HELPER_STATE_DIR"
|
|
sudo chown root:root "$HELPER_STATE_DIR"
|
|
sudo chmod 700 "$HELPER_STATE_DIR"
|
|
echo "✓ Helper replay-guard state directory created"
|
|
fi
|
|
|
|
# Step 8: Handle configuration
|
|
# IMPORTANT: The agent handles its own migration on first start.
|
|
# We either preserve existing config OR create a minimal template.
|
|
if [ -f "${AGENT_CONFIG_DIR}/config.json" ]; then
|
|
echo "[CONFIG] Upgrade detected - preserving existing configuration"
|
|
echo "[CONFIG] Agent will handle migration automatically on first start"
|
|
echo "[CONFIG] Backup created at: ${BACKUP_DIR}"
|
|
else
|
|
echo "[CONFIG] Fresh install - generating minimal configuration with registration token"
|
|
# Create minimal config template - agent will populate missing fields on first start
|
|
sudo tee "${AGENT_CONFIG_DIR}/config.json" > /dev/null <<EOF
|
|
{
|
|
"version": 5,
|
|
"agent_version": "${VERSION}",
|
|
"agent_id": "",
|
|
"token": "",
|
|
"refresh_token": "",
|
|
"registration_token": "{{.RegistrationToken}}",
|
|
"machine_id": "",
|
|
"check_in_interval": 300,
|
|
"server_url": "{{.ServerURL}}",
|
|
"network": {
|
|
"timeout": 30000000000,
|
|
"retry_count": 3,
|
|
"retry_delay": 5000000000,
|
|
"max_idle_conn": 10
|
|
},
|
|
"proxy": {
|
|
"enabled": false
|
|
},
|
|
"tls": {
|
|
"enabled": false,
|
|
"insecure_skip_verify": false
|
|
},
|
|
"logging": {
|
|
"level": "info",
|
|
"max_size": 100,
|
|
"max_backups": 3,
|
|
"max_age": 28
|
|
},
|
|
"subsystems": {
|
|
"system": {"enabled": true, "timeout": 10000000000, "circuit_breaker": {"enabled": true, "failure_threshold": 3, "failure_window": 600000000000, "open_duration": 1800000000000, "half_open_attempts": 2}},
|
|
"filesystem": {"enabled": true, "timeout": 10000000000, "circuit_breaker": {"enabled": true, "failure_threshold": 3, "failure_window": 600000000000, "open_duration": 1800000000000, "half_open_attempts": 2}},
|
|
"network": {"enabled": true, "timeout": 30000000000, "circuit_breaker": {"enabled": true, "failure_threshold": 3, "failure_window": 600000000000, "open_duration": 1800000000000, "half_open_attempts": 2}},
|
|
"processes": {"enabled": true, "timeout": 30000000000, "circuit_breaker": {"enabled": true, "failure_threshold": 3, "failure_window": 600000000000, "open_duration": 1800000000000, "half_open_attempts": 2}},
|
|
"updates": {"enabled": true, "timeout": 30000000000, "circuit_breaker": {"enabled": false, "failure_threshold": 0, "failure_window": 0, "open_duration": 0, "half_open_attempts": 0}},
|
|
"storage": {"enabled": true, "timeout": 10000000000, "circuit_breaker": {"enabled": true, "failure_threshold": 3, "failure_window": 600000000000, "open_duration": 1800000000000, "half_open_attempts": 2}}
|
|
},
|
|
"security": {
|
|
"ed25519_verification": true,
|
|
"nonce_validation": true,
|
|
"machine_id_binding": true
|
|
}
|
|
}
|
|
EOF
|
|
fi
|
|
|
|
# Step 9: Set permissions on config file
|
|
sudo chmod 600 "${AGENT_CONFIG_DIR}/config.json"
|
|
|
|
# Step 10: Create systemd service with security hardening
|
|
echo "Creating systemd service with security configuration..."
|
|
cat <<EOF | sudo tee /etc/systemd/system/${SERVICE_NAME}.service
|
|
[Unit]
|
|
Description=RedFlag Security Agent
|
|
After=network.target
|
|
StartLimitBurst=5
|
|
StartLimitIntervalSec=60
|
|
|
|
[Service]
|
|
Type=simple
|
|
User={{.AgentUser}}
|
|
Group={{.AgentUser}}
|
|
SupplementaryGroups=${LOCAL_API_GROUP}
|
|
WorkingDirectory={{.AgentHome}}
|
|
ExecStart=${INSTALL_DIR}/${SERVICE_NAME}
|
|
Restart=always
|
|
RestartSec=30
|
|
RestartPreventExitStatus=255
|
|
|
|
# Security hardening
|
|
# Note: NoNewPrivileges disabled to allow sudo for package management
|
|
# INSTALL_DIR is in ReadWritePaths so the sudo'd cp during agent self-upgrade
|
|
# can write the new binary and .bak alongside it — ProtectSystem=strict
|
|
# otherwise makes the mount RO inside the unit's namespace and sudo does
|
|
# not bypass that (sudo inherits the mount namespace).
|
|
ProtectSystem=strict
|
|
ProtectHome=true
|
|
ReadWritePaths={{.AgentHome}} {{.AgentHome}}/cache {{.AgentHome}}/state {{.AgentHome}}/migration_backups {{.AgentConfigDir}} {{.AgentLogDir}} {{.ServerKeyDir}} ${INSTALL_DIR}
|
|
PrivateTmp=true
|
|
ProtectKernelTunables=true
|
|
ProtectKernelModules=true
|
|
# ProtectControlGroups left at default (false) — the agent invokes
|
|
# systemd-run to spawn the helper as a transient unit, which requires
|
|
# cgroup creation.
|
|
|
|
RestrictRealtime=true
|
|
RestrictSUIDSGID=true
|
|
RemoveIPC=true
|
|
|
|
# Logging
|
|
StandardOutput=journal
|
|
StandardError=journal
|
|
SyslogIdentifier=${SERVICE_NAME}
|
|
|
|
[Install]
|
|
WantedBy=multi-user.target
|
|
EOF
|
|
|
|
# Set proper permissions on directories
|
|
echo "Setting directory permissions..."
|
|
sudo chown -R {{.AgentUser}}:{{.AgentUser}} "{{.AgentConfigDir}}"
|
|
sudo chown {{.AgentUser}}:{{.AgentUser}} "{{.AgentConfigDir}}/config.json"
|
|
sudo chmod 600 "{{.AgentConfigDir}}/config.json"
|
|
sudo chown -R {{.AgentUser}}:{{.AgentUser}} "{{.AgentHome}}"
|
|
sudo chown {{.AgentUser}}:${LOCAL_API_GROUP} "${BASE_DIR}"
|
|
sudo chown {{.AgentUser}}:${LOCAL_API_GROUP} "{{.AgentHome}}"
|
|
sudo chmod 710 "${BASE_DIR}"
|
|
sudo chmod 710 "{{.AgentHome}}"
|
|
sudo mkdir -p "{{.AgentHome}}/localapi"
|
|
sudo chown {{.AgentUser}}:${LOCAL_API_GROUP} "{{.AgentHome}}/localapi"
|
|
sudo chmod 750 "{{.AgentHome}}/localapi"
|
|
sudo chown -R {{.AgentUser}}:{{.AgentUser}} "{{.AgentLogDir}}"
|
|
sudo chmod 750 "{{.AgentLogDir}}"
|
|
# Server public key directory - agent needs to write the TOFU cached key here
|
|
sudo chown {{.AgentUser}}:{{.AgentUser}} "${SERVER_KEY_DIR}"
|
|
sudo chmod 755 "${SERVER_KEY_DIR}"
|
|
|
|
# Decide install flow per docs/AGENT_LIFECYCLE.md:
|
|
# Fresh Install → no usable local config → call --register
|
|
# Upgrade In Place → local config has non-empty refresh_token → skip --register
|
|
# Token in URL is ignored on the upgrade path; refresh_token authenticates.
|
|
EXISTING_REFRESH_TOKEN=""
|
|
if [ -f "${AGENT_CONFIG_DIR}/config.json" ]; then
|
|
if command -v jq &>/dev/null; then
|
|
EXISTING_REFRESH_TOKEN="$(sudo jq -r '.refresh_token // ""' "${AGENT_CONFIG_DIR}/config.json" 2>/dev/null || echo "")"
|
|
else
|
|
# jq absent — fall back to a tolerant grep. The JSON is written by us
|
|
# at install time; the field is on its own line. This is intentionally
|
|
# tight, not a general JSON parser.
|
|
EXISTING_REFRESH_TOKEN="$(sudo grep -oE '"refresh_token"[[:space:]]*:[[:space:]]*"[^"]+"' "${AGENT_CONFIG_DIR}/config.json" 2>/dev/null | sed -E 's/.*"refresh_token"[[:space:]]*:[[:space:]]*"([^"]+)".*/\1/')"
|
|
fi
|
|
fi
|
|
|
|
if [ -n "${EXISTING_REFRESH_TOKEN}" ]; then
|
|
echo "[INFO] [installer] [register] Upgrade in place — existing credentials detected, skipping registration"
|
|
echo "[INFO] [installer] [register] Token in URL is ignored on the upgrade path; refresh_token authenticates"
|
|
elif [ -n "{{.RegistrationToken}}" ]; then
|
|
echo "[INFO] [installer] [register] Registering agent with server..."
|
|
if sudo -u "{{.AgentUser}}" "${INSTALL_DIR}/${SERVICE_NAME}" --server "{{.ServerURL}}" --token "{{.RegistrationToken}}" --register; then
|
|
echo "[SUCCESS] [installer] [register] Agent registered successfully"
|
|
echo "[INFO] [installer] [register] Agent ID assigned, configuration updated"
|
|
else
|
|
echo "[ERROR] [installer] [register] Registration failed - check token validity and server connectivity"
|
|
echo "[WARN] [installer] [register] Agent installed but not registered. Service will not start."
|
|
echo ""
|
|
echo "[INFO] [installer] [register] To retry registration manually:"
|
|
echo "[INFO] [installer] [register] sudo -u {{.AgentUser}} ${INSTALL_DIR}/${SERVICE_NAME} --server {{.ServerURL}} --token YOUR_TOKEN --register"
|
|
echo "[INFO] [installer] [register] Then start service:"
|
|
echo "[INFO] [installer] [register] sudo systemctl start ${SERVICE_NAME}"
|
|
exit 1
|
|
fi
|
|
else
|
|
echo "[INFO] [installer] [register] No registration token provided - skipping registration"
|
|
echo "[INFO] [installer] [register] Service will start but agent will exit until registered"
|
|
echo "[INFO] [installer] [register] To register manually:"
|
|
echo "[INFO] [installer] [register] sudo -u {{.AgentUser}} ${INSTALL_DIR}/${SERVICE_NAME} --server {{.ServerURL}} --token YOUR_TOKEN --register"
|
|
fi
|
|
|
|
# Step 10b: Provision the host agent_id for the capability-gate helper.
|
|
# The helper runs as root with a clean environment (systemd-run), so it cannot
|
|
# inherit the id from the agent — it reads /etc/redflag/agent_id and refuses any
|
|
# token whose agent_id does not match this host. Written after registration,
|
|
# when the agent has populated its config with the server-assigned id.
|
|
if [ -n "{{.ServerPublicKey}}" ] && [ -f "${AGENT_CONFIG_DIR}/config.json" ]; then
|
|
PROVISIONED_AGENT_ID=""
|
|
if command -v jq &>/dev/null; then
|
|
PROVISIONED_AGENT_ID="$(sudo jq -r '.agent_id // ""' "${AGENT_CONFIG_DIR}/config.json" 2>/dev/null || echo "")"
|
|
else
|
|
PROVISIONED_AGENT_ID="$(sudo grep -oE '"agent_id"[[:space:]]*:[[:space:]]*"[^"]+"' "${AGENT_CONFIG_DIR}/config.json" 2>/dev/null | sed -E 's/.*"agent_id"[[:space:]]*:[[:space:]]*"([^"]+)".*/\1/')"
|
|
fi
|
|
if [ -n "$PROVISIONED_AGENT_ID" ]; then
|
|
echo -n "$PROVISIONED_AGENT_ID" | sudo tee "${CONFIG_DIR}/agent_id" > /dev/null
|
|
sudo chown root:root "${CONFIG_DIR}/agent_id"
|
|
sudo chmod 644 "${CONFIG_DIR}/agent_id"
|
|
echo "✓ Helper agent_id provisioned (${PROVISIONED_AGENT_ID})"
|
|
else
|
|
echo "[WARN] [installer] [helper] agent_id not found in config — helper bind-check will fail until provisioned"
|
|
fi
|
|
fi
|
|
|
|
# Step 11: Enable and start service
|
|
echo "Enabling and starting service..."
|
|
sudo systemctl daemon-reload
|
|
sudo systemctl enable ${SERVICE_NAME}
|
|
sudo systemctl start ${SERVICE_NAME}
|
|
|
|
echo
|
|
if systemctl is-active --quiet ${SERVICE_NAME}; then
|
|
echo "✓ Installation complete!"
|
|
echo ""
|
|
echo "=== Security Information ==="
|
|
echo "Agent is running with security hardening:"
|
|
echo " ✓ Dedicated system user: {{.AgentUser}}"
|
|
echo " ✓ Local API group: ${LOCAL_API_GROUP}"
|
|
echo " ✓ Limited sudo access for package management only"
|
|
echo " ✓ Systemd service with security restrictions"
|
|
echo " ✓ Protected configuration directory"
|
|
echo ""
|
|
echo "Check status: sudo systemctl status ${SERVICE_NAME}"
|
|
echo "View logs: sudo journalctl -u ${SERVICE_NAME} -f"
|
|
else
|
|
echo "⚠ Installation complete but service not started"
|
|
echo " This may be normal for fresh installs awaiting registration"
|
|
echo ""
|
|
echo "To start after registration:"
|
|
echo " sudo systemctl start ${SERVICE_NAME}"
|
|
fi
|