feat: component manifest drives installs, checkoff, and desktop lockstep
INSTALL-001: manifest schema with components+artifacts, CI generation in release gate, manifest-driven install template with --guided and --checkoff modes, post-install provisioning checks, desktop joins version lockstep (bump-version.sh + CI build + gate enforcement). Setup.tsx reduced to primitives (FormSection, TextField, Alert).
This commit is contained in:
parent
0669b4d6b1
commit
88b612c77e
6 changed files with 805 additions and 663 deletions
|
|
@ -458,14 +458,17 @@ func (h *DownloadHandler) DownloadManifest(c *gin.Context) {
|
|||
}
|
||||
|
||||
// buildReleaseManifest assembles the manifest from the signed-package records.
|
||||
// Platforms are walked in a fixed order so the marshalled bytes are stable.
|
||||
// A platform with no signed package (or no resolvable checksum) is omitted
|
||||
// rather than fabricated — the manifest never lies about what it can attest.
|
||||
// Components are pulled from the authoritative catalog (componentCatalog); artifacts
|
||||
// are resolved from signed-package DB rows. Platforms are walked in a fixed order
|
||||
// so the marshalled bytes are stable. A platform with no signed package (or no
|
||||
// resolvable checksum) is omitted rather than fabricated — the manifest never lies
|
||||
// about what it can attest.
|
||||
func (h *DownloadHandler) buildReleaseManifest(version string) services.ReleaseManifest {
|
||||
manifest := services.ReleaseManifest{
|
||||
Version: version,
|
||||
GeneratedAt: time.Now().UTC().Unix(),
|
||||
KeyID: h.signingService.GetCurrentKeyID(),
|
||||
Components: services.ComponentCatalog(),
|
||||
}
|
||||
|
||||
platforms := []struct{ platform, arch string }{
|
||||
|
|
@ -474,6 +477,7 @@ func (h *DownloadHandler) buildReleaseManifest(version string) services.ReleaseM
|
|||
{"windows", "amd64"},
|
||||
{"windows", "arm64"},
|
||||
{"helper-linux", "amd64"},
|
||||
{"desktop-linux", "amd64"},
|
||||
}
|
||||
|
||||
for _, p := range platforms {
|
||||
|
|
@ -495,6 +499,8 @@ func (h *DownloadHandler) buildReleaseManifest(version string) services.ReleaseM
|
|||
switch {
|
||||
case strings.HasPrefix(p.platform, "helper"):
|
||||
filename = "redflag-helper"
|
||||
case strings.HasPrefix(p.platform, "desktop"):
|
||||
filename = "redflag-desktop"
|
||||
case p.platform == "windows":
|
||||
filename += ".exe"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,21 +1,23 @@
|
|||
package services
|
||||
|
||||
// Release manifest — the cold-start trust root.
|
||||
// Release manifest — the cold-start trust root and component healthcheck anchor.
|
||||
//
|
||||
// At install time the agent installer has no pinned binary hash to check
|
||||
// against: it pulls the binary and (until now) ran it. The manifest closes that
|
||||
// gap. It is a single JSON document listing the expected SHA-256 of every
|
||||
// released agent binary per platform/architecture, signed with the same Ed25519
|
||||
// key the agents already trust (TOFU pubkey on first contact, pinned key on
|
||||
// upgrade). The installer fetches the manifest, verifies the signature over the
|
||||
// exact bytes it received, then verifies the downloaded binary's hash matches
|
||||
// the manifest entry before executing anything.
|
||||
// Two audiences, one document:
|
||||
// 1. Install-time: the installer fetches the manifest, verifies the Ed25519
|
||||
// signature over the verbatim body, then checks every downloaded binary
|
||||
// against its matching artifacts entry before executing anything.
|
||||
// 2. Post-install healthcheck: walks Components, runs version_cmd on each
|
||||
// installed binary, verifies provisioning state, emits a signed checkoff
|
||||
// report (local journal + server security event).
|
||||
//
|
||||
// The signed bytes are the verbatim JSON of ReleaseManifest (no map fields, so
|
||||
// Go's json.Marshal is deterministic; Artifacts is sorted before marshalling).
|
||||
// Whatever the server serves as the body is exactly what was signed — the
|
||||
// signature travels in the X-Content-Signature header, the key fingerprint in
|
||||
// X-Key-Id, mirroring the binary download endpoint.
|
||||
// Signed with the server's instance Ed25519 key (TOFU pubkey on first contact,
|
||||
// pinned on upgrade). The signed bytes are the verbatim JSON; the signature
|
||||
// travels in X-Content-Signature, the key fingerprint in X-Key-Id.
|
||||
//
|
||||
// CI generates the authoritative manifest at release time (embedded in the
|
||||
// server binary); at serve time the server re-signs with its instance key.
|
||||
// A release whose manifest lists a component without a built, version-self-
|
||||
// reporting artifact fails the release gate — no silent drop-out.
|
||||
|
||||
// ManifestArtifact is one released binary's expected identity.
|
||||
type ManifestArtifact struct {
|
||||
|
|
@ -26,10 +28,36 @@ type ManifestArtifact struct {
|
|||
Size int64 `json:"size"`
|
||||
}
|
||||
|
||||
// ReleaseManifest is the signed set of expected binary hashes for one version.
|
||||
type ReleaseManifest struct {
|
||||
Version string `json:"version"`
|
||||
GeneratedAt int64 `json:"generated_at"`
|
||||
KeyID string `json:"key_id"`
|
||||
Artifacts []ManifestArtifact `json:"artifacts"`
|
||||
// ManifestComponent describes one installable/checkable piece of the release.
|
||||
// Required components block install completion; optional ones are skipped
|
||||
// gracefully when unavailable.
|
||||
type ManifestComponent struct {
|
||||
Name string `json:"name"`
|
||||
Kind string `json:"kind"` // docker, binary, embedded
|
||||
Required bool `json:"required"`
|
||||
VersionCmd string `json:"version_cmd"` // e.g. "--version"
|
||||
Provisioning []string `json:"provisioning,omitempty"` // healthcheck steps (desktop: autostart_entry, redflag-local_group, desktop_user_membership)
|
||||
}
|
||||
|
||||
// ReleaseManifest is the signed component+artifact catalog for one version.
|
||||
type ReleaseManifest struct {
|
||||
Version string `json:"version"`
|
||||
GeneratedAt int64 `json:"generated_at"`
|
||||
KeyID string `json:"key_id"`
|
||||
Components []ManifestComponent `json:"components"`
|
||||
Artifacts []ManifestArtifact `json:"artifacts"`
|
||||
}
|
||||
|
||||
// ComponentCatalog returns the fixed component set for a release.
|
||||
// This is the authoritative list — CI's gate job enforces that every component
|
||||
// here has a built, version-self-reporting artifact.
|
||||
func ComponentCatalog() []ManifestComponent {
|
||||
return []ManifestComponent{
|
||||
{Name: "server", Kind: "docker", Required: true, VersionCmd: "--version"},
|
||||
{Name: "agent", Kind: "binary", Required: true, VersionCmd: "--version"},
|
||||
{Name: "helper", Kind: "binary", Required: true, VersionCmd: "--version"},
|
||||
{Name: "desktop", Kind: "binary", Required: false, VersionCmd: "--version",
|
||||
Provisioning: []string{"autostart_entry", "redflag-local_group", "desktop_user_membership"}},
|
||||
{Name: "web", Kind: "embedded", Required: true},
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -46,6 +46,8 @@ case $ARCH in
|
|||
;;
|
||||
esac
|
||||
|
||||
PLATFORM_TAG="linux"
|
||||
|
||||
# Override download URL with detected architecture
|
||||
BINARY_URL="{{.ServerURL}}/api/v1/downloads/linux-${ARCH_TAG}?version={{.Version}}"
|
||||
|
||||
|
|
@ -72,6 +74,110 @@ echo "Platform: {{.Platform}}"
|
|||
echo "Installing to: ${INSTALL_DIR}/${SERVICE_NAME}"
|
||||
echo
|
||||
|
||||
# ---- argument parsing ----
|
||||
GUIDED=false
|
||||
CHECKOFF_ONLY=false
|
||||
while [ $# -gt 0 ]; do
|
||||
case "$1" in
|
||||
--guided) GUIDED=true; shift ;;
|
||||
--checkoff) CHECKOFF_ONLY=true; shift ;;
|
||||
*) echo "Unknown option: $1"; echo "Usage: $0 [--guided] [--checkoff]"; exit 1 ;;
|
||||
esac
|
||||
done
|
||||
|
||||
# ---- checkoff tracking ----
|
||||
# Each step records a line: COMPONENT|RESULT|DETAIL
|
||||
# RESULT: ok, skip, fail
|
||||
CHECKOFF_LOG=$(mktemp)
|
||||
checkoff() {
|
||||
local component="$1" result="$2" detail="$3"
|
||||
echo "${component}|${result}|${detail}" >> "$CHECKOFF_LOG"
|
||||
case "$result" in
|
||||
ok) echo " [✓] ${component}: ${detail}" ;;
|
||||
skip) echo " [—] ${component}: ${detail} (skipped)" ;;
|
||||
fail) echo " [✗] ${component}: ${detail}" ;;
|
||||
esac
|
||||
}
|
||||
|
||||
emit_checkoff_report() {
|
||||
echo ""
|
||||
echo "=== Component Checkoff Report ==="
|
||||
echo "Release: ${VERSION}"
|
||||
echo "Host: $(hostname)"
|
||||
echo "Time: $(date -u +%Y-%m-%dT%H:%M:%SZ)"
|
||||
echo ""
|
||||
local fail_count=0
|
||||
while IFS='|' read -r comp result detail; do
|
||||
case "$result" in
|
||||
ok) echo " [✓] ${comp}: ${detail}" ;;
|
||||
skip) echo " [—] ${comp}: ${detail}" ;;
|
||||
fail) echo " [✗] ${comp}: ${detail}"; fail_count=$((fail_count + 1)) ;;
|
||||
esac
|
||||
done < "$CHECKOFF_LOG"
|
||||
echo ""
|
||||
if [ "$fail_count" -gt 0 ]; then
|
||||
echo "Result: $fail_count component(s) failed"
|
||||
else
|
||||
echo "Result: all components verified"
|
||||
fi
|
||||
|
||||
# Write structured checkoff to agent's local journal.
|
||||
local JOURNAL_DIR="${AGENT_HOME}/checkoffs"
|
||||
sudo mkdir -p "$JOURNAL_DIR"
|
||||
sudo chown "$AGENT_USER:$AGENT_USER" "$JOURNAL_DIR"
|
||||
local REPORT_FILE="${JOURNAL_DIR}/checkoff-${VERSION}-$(date -u +%Y%m%dT%H%M%SZ).json"
|
||||
{
|
||||
echo "{"
|
||||
echo " \"release\": \"${VERSION}\","
|
||||
echo " \"host\": \"$(hostname)\","
|
||||
echo " \"time\": \"$(date -u +%Y-%m-%dT%H:%M:%SZ)\","
|
||||
echo " \"agent_id\": \"${AGENT_ID}\","
|
||||
echo " \"components\": ["
|
||||
local first=true
|
||||
while IFS='|' read -r comp result detail; do
|
||||
[ -z "$comp" ] && continue
|
||||
if $first; then first=false; else echo ","; fi
|
||||
echo -n " {\"name\":\"${comp}\",\"result\":\"${result}\",\"detail\":\"${detail}\"}"
|
||||
done < "$CHECKOFF_LOG"
|
||||
echo ""
|
||||
echo " ]"
|
||||
echo "}"
|
||||
} | sudo tee "$REPORT_FILE" > /dev/null
|
||||
sudo chown "$AGENT_USER:$AGENT_USER" "$REPORT_FILE"
|
||||
echo "Checkoff journal: $REPORT_FILE"
|
||||
|
||||
# Post checkoff as security event to server (fire-and-forget).
|
||||
local EVENT_JSON
|
||||
EVENT_JSON=$(printf '{"events":[{"timestamp":"%s","level":"info","event_type":"install_checkoff","message":"Install checkoff for %s on %s","details":{"release":"%s","host":"%s","agent_id":"%s","components":%s}}]}' \
|
||||
"$(date -u +%Y-%m-%dT%H:%M:%SZ)" "$VERSION" "$(hostname)" "$VERSION" "$(hostname)" "$AGENT_ID" \
|
||||
"$(python3 -c "import json; print(json.dumps([{'name':c,'result':r,'detail':d} for c,r,d in (l.split('|') for l in open('$CHECKOFF_LOG').read().strip().split(chr(10)) if l)]))" 2>/dev/null || echo '[]')")
|
||||
curl -sf -X POST "{{.ServerURL}}/api/v1/agents/${AGENT_ID}/security-events" \
|
||||
-H "Content-Type: application/json" \
|
||||
-H "X-Agent-ID: ${AGENT_ID}" \
|
||||
-d "$EVENT_JSON" 2>/dev/null || true
|
||||
echo "Checkoff event posted to server"
|
||||
}
|
||||
|
||||
# Guided-mode welcome.
|
||||
if $GUIDED; then
|
||||
echo "┌──────────────────────────────────────────┐"
|
||||
echo "│ RedFlag v${VERSION} — Guided Install │"
|
||||
echo "└──────────────────────────────────────────┘"
|
||||
echo ""
|
||||
echo "This will install the RedFlag agent and its components."
|
||||
echo "Required components: agent, helper, web (embedded in server)."
|
||||
echo "Optional components: desktop (system tray + local dashboard)."
|
||||
echo ""
|
||||
echo "Press Enter to continue or Ctrl-C to abort."
|
||||
read -r
|
||||
fi
|
||||
|
||||
# Checkoff-only mode: re-run healthcheck against the manifest, skip downloads.
|
||||
if $CHECKOFF_ONLY; then
|
||||
echo "=== Checkoff-only mode — verifying installed components ==="
|
||||
# Fall through to manifest fetch, skip downloads, run version checks.
|
||||
fi
|
||||
|
||||
# Step 1: Detect existing installation
|
||||
echo "Detecting existing RedFlag installations..."
|
||||
MIGRATION_NEEDED=false
|
||||
|
|
@ -304,31 +410,20 @@ 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-driven component download ----
|
||||
# Fetch the signed release manifest once, verify its Ed25519 signature against
|
||||
# the TOFU-pinned server public key, then drive every component download from
|
||||
# the manifest's components + artifacts tables. No hardcoded binary URLs.
|
||||
MANIFEST_URL="{{.ServerURL}}/api/v1/manifest?version=${VERSION}"
|
||||
SERVER_PUBKEY="{{.ServerPublicKey}}"
|
||||
|
||||
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.
|
||||
# Ensure python3 + cryptography are available (needed for manifest verification
|
||||
# and JSON parsing of the manifest).
|
||||
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
|
||||
|
|
@ -340,35 +435,34 @@ if ! python3 -c "from cryptography.hazmat.primitives.asymmetric.ed25519 import E
|
|||
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
|
||||
|
||||
# Fetch and verify the release manifest.
|
||||
echo "Fetching release manifest..."
|
||||
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"
|
||||
rm -f "$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')
|
||||
MANIFEST_KEY_ID=$(grep -i "x-key-id" "$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"
|
||||
rm -f "$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).
|
||||
# Verify Ed25519 signature over the verbatim manifest body.
|
||||
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]
|
||||
pubkey_path, manifest_path, sig_path = sys.argv[1:4]
|
||||
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()
|
||||
|
|
@ -378,377 +472,321 @@ 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)
|
||||
sys.exit(0)
|
||||
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")
|
||||
python3 "$MANIFEST_VERIFY" "${TMP_MANIFEST}.pub" "$TMP_MANIFEST" "${TMP_MANIFEST}.sig"
|
||||
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"
|
||||
rm -f "$MANIFEST_VERIFY" "${TMP_MANIFEST}.pub" "${TMP_MANIFEST}.sig"
|
||||
if [ $MANIFEST_RC -ne 0 ]; then
|
||||
echo "ERROR: Release manifest signature verification failed (rc=$MANIFEST_RC) — refusing to install."
|
||||
rm -f "$TMP_MANIFEST"
|
||||
exit 1
|
||||
fi
|
||||
echo "✓ Manifest signature verified (key ${MANIFEST_KEY_ID})"
|
||||
|
||||
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 ---
|
||||
# Resolve components → artifacts for this platform/arch.
|
||||
# The Python helper reads the manifest, pairs each component with its matching
|
||||
# artifact entry, and emits tab-separated lines:
|
||||
# name|kind|required|version_cmd|filename|sha256|provisioning_csv
|
||||
MANIFEST_RESOLVE=$(mktemp)
|
||||
cat <<'MANIFEST_EOF' > "$MANIFEST_RESOLVE"
|
||||
import sys, json
|
||||
platform, arch = sys.argv[1:3]
|
||||
m = json.load(open(sys.argv[3]))
|
||||
# Index artifacts by (platform, architecture)
|
||||
art_map = {}
|
||||
for a in m.get("artifacts", []):
|
||||
art_map[(a.get("platform",""), a.get("architecture",""))] = a
|
||||
for c in m.get("components", []):
|
||||
name = c.get("name","")
|
||||
kind = c.get("kind","")
|
||||
req = "true" if c.get("required", False) else "false"
|
||||
vcmd = c.get("version_cmd", "--version")
|
||||
prov = ",".join(c.get("provisioning", []))
|
||||
# Match: binary components look for <name>-<platform> in artifacts.
|
||||
# agent=linux, helper=helper-linux, desktop=desktop-linux, server=server-linux
|
||||
if kind == "embedded":
|
||||
# Embedded components have no artifact to download — they're built into
|
||||
# another component (web is embedded in server). Checkoff is server-side.
|
||||
if not req == "true" and "--skip" in sys.argv:
|
||||
continue
|
||||
print("%s|%s|%s|%s|||%s" % (name, kind, req, vcmd, prov))
|
||||
continue
|
||||
if kind == "docker":
|
||||
print("%s|%s|%s|%s|||%s" % (name, kind, req, vcmd, prov))
|
||||
continue
|
||||
# Binary components: try <name>-<platform> first, then <platform> for agent
|
||||
candidates = ["%s-%s" % (name, platform), platform]
|
||||
found = None
|
||||
for cand in candidates:
|
||||
key = (cand, arch)
|
||||
if key in art_map:
|
||||
found = art_map[key]
|
||||
break
|
||||
if found:
|
||||
print("%s|%s|%s|%s|%s|%s|%s" % (
|
||||
name, kind, req, vcmd,
|
||||
found.get("filename",""),
|
||||
found.get("sha256",""),
|
||||
prov))
|
||||
else:
|
||||
if req == "true":
|
||||
print("%s|%s|%s|%s|MISSING||%s" % (name, kind, req, vcmd, prov), file=sys.stderr)
|
||||
else:
|
||||
print("%s|%s|%s|%s|||%s" % (name, kind, req, vcmd, prov))
|
||||
MANIFEST_EOF
|
||||
|
||||
# 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
|
||||
# The resolver emits component lines to stdout. We read them into a shell loop.
|
||||
MANIFEST_LINES=$(mktemp)
|
||||
python3 "$MANIFEST_RESOLVE" "$PLATFORM_TAG" "$ARCH_TAG" "$TMP_MANIFEST" > "$MANIFEST_LINES" 2>/dev/null
|
||||
rm -f "$MANIFEST_RESOLVE" "$TMP_MANIFEST"
|
||||
|
||||
# Walk each component from the manifest.
|
||||
echo ""
|
||||
echo "=== Installing components ==="
|
||||
HAD_FAILURES=false
|
||||
while IFS='|' read -r comp_name comp_kind comp_req comp_vcmd comp_file comp_sha comp_prov; do
|
||||
[ -z "$comp_name" ] && continue
|
||||
echo ""
|
||||
echo "--- ${comp_name} (${comp_kind}) ---"
|
||||
|
||||
# Embedded and docker components are not downloaded here — they're verified
|
||||
# at checkoff time.
|
||||
if [ "$comp_kind" = "embedded" ]; then
|
||||
checkoff "$comp_name" "ok" "embedded (verified server-side)"
|
||||
continue
|
||||
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
|
||||
|
||||
# Grant CAP_SYS_PTRACE so the agent can read /proc/[pid]/environ from the
|
||||
# logged-in user's session processes. Required for display/Wayland discovery
|
||||
# (screenshot capture) and any future per-process telemetry. Without this the
|
||||
# agent user cannot read environment variables of processes it does not own.
|
||||
if command -v setcap &>/dev/null; then
|
||||
sudo setcap cap_sys_ptrace=eip "${INSTALL_DIR}/${SERVICE_NAME}"
|
||||
echo "✓ CAP_SYS_PTRACE granted (display/process discovery)"
|
||||
else
|
||||
echo "[WARN] [installer] [capabilities] setcap not found — install libcap-provides and run:"
|
||||
echo " sudo setcap cap_sys_ptrace=eip ${INSTALL_DIR}/${SERVICE_NAME}"
|
||||
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 7c: Install the desktop app (system tray + local UI shell)
|
||||
# The desktop app is a Tauri binary that connects to the agent's local API
|
||||
# socket and provides a system tray icon with a local dashboard. It runs in
|
||||
# the user's desktop session, not as a system service.
|
||||
DESKTOP_BIN="${INSTALL_DIR}/redflag-desktop"
|
||||
DESKTOP_URL="{{.ServerURL}}/api/v1/downloads/desktop-${ARCH_TAG}?version=${VERSION}"
|
||||
echo "Installing desktop app (redflag-desktop)..."
|
||||
TMP_DESKTOP=$(mktemp)
|
||||
TMP_DESKTOP_HDR=$(mktemp)
|
||||
if curl -fsSL -o "$TMP_DESKTOP" -D "$TMP_DESKTOP_HDR" "$DESKTOP_URL" 2>/dev/null; then
|
||||
DESKTOP_SIG=$(grep -i "x-content-signature" "$TMP_DESKTOP_HDR" | awk '{print $2}' | tr -d '\r\n')
|
||||
DESKTOP_SHA=$(grep -i "x-content-sha256" "$TMP_DESKTOP_HDR" | awk '{print $2}' | tr -d '\r\n')
|
||||
rm -f "$TMP_DESKTOP_HDR"
|
||||
|
||||
# Verify signature if available
|
||||
if [ -n "$DESKTOP_SIG" ] && [ -s "${SERVER_KEY_DIR}/server_public_key" ]; then
|
||||
DESKTOP_VERIFY=$(mktemp)
|
||||
cat <<'DVERIFY_EOF' > "$DESKTOP_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)
|
||||
DVERIFY_EOF
|
||||
echo "$DESKTOP_SIG" > "${TMP_DESKTOP}.sig"
|
||||
if python3 "$DESKTOP_VERIFY" "${SERVER_KEY_DIR}/server_public_key" "${TMP_DESKTOP}.sig" "$TMP_DESKTOP" 2>/dev/null; then
|
||||
echo "✓ redflag-desktop signature verified (Ed25519)"
|
||||
if [ "$comp_kind" = "docker" ]; then
|
||||
if command -v docker >/dev/null 2>&1 && docker ps --format '{{.Names}}' 2>/dev/null | grep -q redflag; then
|
||||
DOCKER_VER=$(docker run --rm 10.10.20.120:4455/fimeg/redflag:${VERSION} ./redflag-server --version 2>/dev/null || echo "unknown")
|
||||
if echo "$DOCKER_VER" | grep -q "v${VERSION}"; then
|
||||
checkoff "$comp_name" "ok" "docker image v${VERSION} present"
|
||||
else
|
||||
checkoff "$comp_name" "fail" "docker image reports ${DOCKER_VER}, expected v${VERSION}"
|
||||
HAD_FAILURES=true
|
||||
fi
|
||||
else
|
||||
echo "WARNING: redflag-desktop signature verification failed — skipping desktop install"
|
||||
rm -f "$TMP_DESKTOP" "$DESKTOP_VERIFY" "${TMP_DESKTOP}.sig"
|
||||
TMP_DESKTOP=""
|
||||
checkoff "$comp_name" "skip" "docker not available or redflag container not running"
|
||||
fi
|
||||
rm -f "$DESKTOP_VERIFY" "${TMP_DESKTOP}.sig"
|
||||
continue
|
||||
fi
|
||||
|
||||
if [ -n "$DESKTOP_SHA" ] && [ -f "$TMP_DESKTOP" ]; then
|
||||
ACTUAL_DESKTOP_SHA=$(sha256sum "$TMP_DESKTOP" | awk '{print $1}')
|
||||
if [ "$DESKTOP_SHA" != "$ACTUAL_DESKTOP_SHA" ]; then
|
||||
echo "WARNING: redflag-desktop hash mismatch — skipping desktop install"
|
||||
rm -f "$TMP_DESKTOP"
|
||||
TMP_DESKTOP=""
|
||||
# Binary component.
|
||||
if [ -z "$comp_file" ] || [ -z "$comp_sha" ]; then
|
||||
if [ "$comp_req" = "true" ]; then
|
||||
echo "ERROR: Required component ${comp_name} has no artifact for ${PLATFORM_TAG}/${ARCH_TAG}"
|
||||
checkoff "$comp_name" "fail" "no artifact for ${PLATFORM_TAG}/${ARCH_TAG}"
|
||||
HAD_FAILURES=true
|
||||
else
|
||||
echo "INFO: Optional component ${comp_name} not available for ${PLATFORM_TAG}/${ARCH_TAG} — skipping"
|
||||
checkoff "$comp_name" "skip" "not available for ${PLATFORM_TAG}/${ARCH_TAG}"
|
||||
fi
|
||||
continue
|
||||
fi
|
||||
|
||||
if [ -f "$TMP_DESKTOP" ]; then
|
||||
sudo mv "$TMP_DESKTOP" "$DESKTOP_BIN"
|
||||
sudo chmod 755 "$DESKTOP_BIN"
|
||||
if command -v restorecon &>/dev/null; then
|
||||
sudo restorecon "$DESKTOP_BIN" 2>/dev/null || true
|
||||
fi
|
||||
echo "✓ redflag-desktop installed at $DESKTOP_BIN"
|
||||
COMP_URL="{{.ServerURL}}/api/v1/downloads/${comp_name}-${ARCH_TAG}?version=${VERSION}"
|
||||
# The agent binary uses the bare platform tag (linux), while helper/desktop
|
||||
# use <name>-<platform>. Adjust the URL pattern to match download endpoints.
|
||||
case "$comp_name" in
|
||||
agent) COMP_URL="{{.ServerURL}}/api/v1/downloads/${PLATFORM_TAG}-${ARCH_TAG}?version=${VERSION}" ;;
|
||||
helper) COMP_URL="{{.ServerURL}}/api/v1/helper/${ARCH_TAG}?version=${VERSION}" ;;
|
||||
desktop) COMP_URL="{{.ServerURL}}/api/v1/downloads/desktop-${ARCH_TAG}?version=${VERSION}" ;;
|
||||
esac
|
||||
|
||||
# Autostart the tray in desktop sessions. /etc/xdg/autostart applies to
|
||||
# every desktop user; the binary exits cleanly on headless sessions.
|
||||
XDG_AUTOSTART_DIR="/etc/xdg/autostart"
|
||||
if [ -d "$XDG_AUTOSTART_DIR" ] || sudo mkdir -p "$XDG_AUTOSTART_DIR"; then
|
||||
cat <<EOF | sudo tee "${XDG_AUTOSTART_DIR}/redflag-desktop.desktop" > /dev/null
|
||||
# Checkoff-only: skip download, verify installed binary + provisioning.
|
||||
if $CHECKOFF_ONLY; then
|
||||
BIN_PATH=""
|
||||
case "$comp_name" in
|
||||
agent) BIN_PATH="${INSTALL_DIR}/${SERVICE_NAME}" ;;
|
||||
helper) BIN_PATH="${INSTALL_DIR}/redflag-helper" ;;
|
||||
desktop) BIN_PATH="${INSTALL_DIR}/redflag-desktop" ;;
|
||||
esac
|
||||
if [ -x "$BIN_PATH" ]; then
|
||||
BIN_VER=$("$BIN_PATH" "$comp_vcmd" 2>/dev/null || echo "unknown")
|
||||
if echo "$BIN_VER" | grep -q "v${VERSION}"; then
|
||||
checkoff "$comp_name" "ok" "version ${BIN_VER}"
|
||||
else
|
||||
checkoff "$comp_name" "fail" "version ${BIN_VER}, expected v${VERSION}"
|
||||
HAD_FAILURES=true
|
||||
fi
|
||||
# Provisioning checks (desktop only).
|
||||
if [ -n "$comp_prov" ]; then
|
||||
IFS=',' read -ra PROV_CHECKS <<< "$comp_prov"
|
||||
for check in "${PROV_CHECKS[@]}"; do
|
||||
case "$check" in
|
||||
autostart_entry)
|
||||
if [ -f /etc/xdg/autostart/redflag-desktop.desktop ]; then
|
||||
checkoff "desktop:autostart" "ok" "autostart entry present"
|
||||
else
|
||||
checkoff "desktop:autostart" "fail" "autostart entry missing"
|
||||
HAD_FAILURES=true
|
||||
fi
|
||||
;;
|
||||
redflag-local_group)
|
||||
if getent group "$LOCAL_API_GROUP" >/dev/null 2>&1; then
|
||||
checkoff "desktop:group" "ok" "group ${LOCAL_API_GROUP} exists"
|
||||
else
|
||||
checkoff "desktop:group" "fail" "group ${LOCAL_API_GROUP} missing"
|
||||
HAD_FAILURES=true
|
||||
fi
|
||||
;;
|
||||
desktop_user_membership)
|
||||
if [ -n "${SUDO_USER:-}" ] && [ "${SUDO_USER}" != "root" ]; then
|
||||
if id -nG "$SUDO_USER" 2>/dev/null | tr ' ' '\n' | grep -qx "$LOCAL_API_GROUP"; then
|
||||
checkoff "desktop:membership" "ok" "${SUDO_USER} in ${LOCAL_API_GROUP}"
|
||||
else
|
||||
checkoff "desktop:membership" "fail" "${SUDO_USER} not in ${LOCAL_API_GROUP}"
|
||||
HAD_FAILURES=true
|
||||
fi
|
||||
else
|
||||
checkoff "desktop:membership" "skip" "no desktop user detected"
|
||||
fi
|
||||
;;
|
||||
esac
|
||||
done
|
||||
fi
|
||||
else
|
||||
if [ "$comp_req" = "true" ]; then
|
||||
checkoff "$comp_name" "fail" "binary not found at ${BIN_PATH}"
|
||||
HAD_FAILURES=true
|
||||
else
|
||||
checkoff "$comp_name" "skip" "binary not installed"
|
||||
fi
|
||||
fi
|
||||
continue
|
||||
fi
|
||||
|
||||
# Download and verify.
|
||||
echo "Downloading ${comp_name}..."
|
||||
TMP_COMP=$(mktemp)
|
||||
TMP_COMP_HDR=$(mktemp)
|
||||
if ! curl -fsSL -o "$TMP_COMP" -D "$TMP_COMP_HDR" "$COMP_URL"; then
|
||||
if [ "$comp_req" = "true" ]; then
|
||||
echo "ERROR: Failed to download ${comp_name} from ${COMP_URL}"
|
||||
checkoff "$comp_name" "fail" "download failed"
|
||||
HAD_FAILURES=true
|
||||
rm -f "$TMP_COMP" "$TMP_COMP_HDR"
|
||||
continue
|
||||
else
|
||||
echo "INFO: ${comp_name} download failed (optional — skipping)"
|
||||
checkoff "$comp_name" "skip" "download failed (optional)"
|
||||
rm -f "$TMP_COMP" "$TMP_COMP_HDR"
|
||||
continue
|
||||
fi
|
||||
fi
|
||||
rm -f "$TMP_COMP_HDR"
|
||||
|
||||
# Verify hash against manifest.
|
||||
ACTUAL_SHA=$(sha256sum "$TMP_COMP" | awk '{print $1}')
|
||||
if [ "$comp_sha" != "$ACTUAL_SHA" ]; then
|
||||
echo "ERROR: ${comp_name} hash mismatch"
|
||||
echo " expected (manifest): $comp_sha"
|
||||
echo " actual: $ACTUAL_SHA"
|
||||
checkoff "$comp_name" "fail" "hash mismatch"
|
||||
HAD_FAILURES=true
|
||||
rm -f "$TMP_COMP"
|
||||
continue
|
||||
fi
|
||||
echo "✓ ${comp_name} hash verified"
|
||||
|
||||
# Install the binary.
|
||||
case "$comp_name" in
|
||||
agent)
|
||||
# Save server public key for TOFU (already done above conceptually,
|
||||
# but we do it here since the agent binary is now verified).
|
||||
if [ -n "$SERVER_PUBKEY" ]; then
|
||||
echo -n "$SERVER_PUBKEY" | sudo tee "${SERVER_KEY_DIR}/server_public_key" > /dev/null
|
||||
sudo chown {{.AgentUser}}:{{.AgentUser}} "${SERVER_KEY_DIR}/server_public_key"
|
||||
fi
|
||||
# Per-binary signature is covered by the manifest (already verified).
|
||||
# The agent trusts the manifest-signed hash chain.
|
||||
sudo mv "$TMP_COMP" "${INSTALL_DIR}/${SERVICE_NAME}"
|
||||
sudo chmod +x "${INSTALL_DIR}/${SERVICE_NAME}"
|
||||
if command -v restorecon &>/dev/null; then
|
||||
sudo restorecon "${INSTALL_DIR}/${SERVICE_NAME}" 2>/dev/null || true
|
||||
fi
|
||||
if command -v setcap &>/dev/null; then
|
||||
sudo setcap cap_sys_ptrace=eip "${INSTALL_DIR}/${SERVICE_NAME}"
|
||||
echo "✓ CAP_SYS_PTRACE granted (display/process discovery)"
|
||||
fi
|
||||
checkoff "$comp_name" "ok" "installed at ${INSTALL_DIR}/${SERVICE_NAME}"
|
||||
;;
|
||||
helper)
|
||||
sudo mv "$TMP_COMP" "${INSTALL_DIR}/redflag-helper"
|
||||
sudo chown root:root "${INSTALL_DIR}/redflag-helper"
|
||||
sudo chmod 755 "${INSTALL_DIR}/redflag-helper"
|
||||
if command -v restorecon &>/dev/null; then
|
||||
sudo restorecon "${INSTALL_DIR}/redflag-helper" 2>/dev/null || true
|
||||
fi
|
||||
# Helper state dir, root-only.
|
||||
sudo mkdir -p "${BASE_DIR}/helper"
|
||||
sudo chown root:root "${BASE_DIR}/helper"
|
||||
sudo chmod 700 "${BASE_DIR}/helper"
|
||||
# Trusted-keys keyring for the helper.
|
||||
HELPER_KEYRING_DIR="${CONFIG_DIR}/trusted-keys"
|
||||
sudo mkdir -p "$HELPER_KEYRING_DIR"
|
||||
echo -n "$SERVER_PUBKEY" | 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"
|
||||
checkoff "$comp_name" "ok" "installed at ${INSTALL_DIR}/redflag-helper"
|
||||
;;
|
||||
desktop)
|
||||
sudo mv "$TMP_COMP" "${INSTALL_DIR}/redflag-desktop"
|
||||
sudo chmod 755 "${INSTALL_DIR}/redflag-desktop"
|
||||
if command -v restorecon &>/dev/null; then
|
||||
sudo restorecon "${INSTALL_DIR}/redflag-desktop" 2>/dev/null || true
|
||||
fi
|
||||
# Autostart entry (provisioning check: autostart_entry).
|
||||
XDG_AUTOSTART_DIR="/etc/xdg/autostart"
|
||||
if [ -d "$XDG_AUTOSTART_DIR" ] || sudo mkdir -p "$XDG_AUTOSTART_DIR"; then
|
||||
cat <<EOF | sudo tee "${XDG_AUTOSTART_DIR}/redflag-desktop.desktop" > /dev/null
|
||||
[Desktop Entry]
|
||||
Type=Application
|
||||
Name=RedFlag
|
||||
Comment=RedFlag system tray (local agent dashboard)
|
||||
Exec=${DESKTOP_BIN}
|
||||
Exec=${INSTALL_DIR}/redflag-desktop
|
||||
Terminal=false
|
||||
X-GNOME-Autostart-enabled=true
|
||||
EOF
|
||||
sudo chmod 644 "${XDG_AUTOSTART_DIR}/redflag-desktop.desktop"
|
||||
echo "✓ Tray autostart entry installed (${XDG_AUTOSTART_DIR}/redflag-desktop.desktop)"
|
||||
fi
|
||||
|
||||
# Stock GNOME ships no tray host — without the AppIndicator extension
|
||||
# the icon never appears and the tray looks broken. Other desktops
|
||||
# (KDE, Hyprland/waybar, XFCE) support StatusNotifier out of the box.
|
||||
if command -v gnome-shell >/dev/null && ! compgen -G "/usr/share/gnome-shell/extensions/*appindicator*" >/dev/null; then
|
||||
echo "[INFO] [installer] [desktop] GNOME detected without the AppIndicator extension — install gnome-shell-extension-appindicator or the tray icon will not be visible"
|
||||
fi
|
||||
|
||||
# The local API socket lives under /var/lib/redflag (0710, group
|
||||
# ${LOCAL_API_GROUP}) — the tray cannot reach it unless its desktop
|
||||
# user is in the group. Add the invoking user; takes effect at next
|
||||
# login. Membership grants read + local actions only; mutation still
|
||||
# requires the capability-token path.
|
||||
if [ -n "${SUDO_USER:-}" ] && [ "${SUDO_USER}" != "root" ]; then
|
||||
if id -nG "$SUDO_USER" 2>/dev/null | tr ' ' '\n' | grep -qx "$LOCAL_API_GROUP"; then
|
||||
echo "✓ $SUDO_USER already in $LOCAL_API_GROUP group"
|
||||
else
|
||||
sudo usermod -aG "$LOCAL_API_GROUP" "$SUDO_USER"
|
||||
echo "✓ Added $SUDO_USER to $LOCAL_API_GROUP (tray socket access — re-login to take effect)"
|
||||
sudo chmod 644 "${XDG_AUTOSTART_DIR}/redflag-desktop.desktop"
|
||||
fi
|
||||
else
|
||||
echo "[INFO] [installer] [desktop] No desktop user detected — add tray users to $LOCAL_API_GROUP manually"
|
||||
fi
|
||||
fi
|
||||
else
|
||||
echo "INFO: Desktop app not available for this platform/arch — skipping"
|
||||
rm -f "$TMP_DESKTOP" "$TMP_DESKTOP_HDR"
|
||||
# GNOME AppIndicator note.
|
||||
if command -v gnome-shell >/dev/null && ! compgen -G "/usr/share/gnome-shell/extensions/*appindicator*" >/dev/null; then
|
||||
echo "[INFO] [installer] [desktop] GNOME detected without AppIndicator extension — install gnome-shell-extension-appindicator"
|
||||
fi
|
||||
# Add invoking user to redflag-local group (provisioning: desktop_user_membership).
|
||||
if [ -n "${SUDO_USER:-}" ] && [ "${SUDO_USER}" != "root" ]; then
|
||||
if id -nG "$SUDO_USER" 2>/dev/null | tr ' ' '\n' | grep -qx "$LOCAL_API_GROUP"; then
|
||||
echo "✓ $SUDO_USER already in $LOCAL_API_GROUP group"
|
||||
else
|
||||
sudo usermod -aG "$LOCAL_API_GROUP" "$SUDO_USER"
|
||||
echo "✓ Added $SUDO_USER to $LOCAL_API_GROUP (tray socket access — re-login to take effect)"
|
||||
fi
|
||||
fi
|
||||
checkoff "$comp_name" "ok" "installed at ${INSTALL_DIR}/redflag-desktop"
|
||||
;;
|
||||
esac
|
||||
done < "$MANIFEST_LINES"
|
||||
rm -f "$MANIFEST_LINES"
|
||||
|
||||
# Fail the install if any required component failed.
|
||||
if $HAD_FAILURES; then
|
||||
echo ""
|
||||
echo "ERROR: One or more required components failed to install."
|
||||
emit_checkoff_report
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Save server public key for TOFU model.
|
||||
if [ -n "$SERVER_PUBKEY" ]; then
|
||||
echo -n "$SERVER_PUBKEY" | 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
|
||||
|
||||
# Step 8: Handle configuration
|
||||
|
|
@ -953,7 +991,13 @@ if [ -n "{{.ServerPublicKey}}" ] && [ -f "${AGENT_CONFIG_DIR}/config.json" ]; th
|
|||
fi
|
||||
fi
|
||||
|
||||
# Step 10e: Emit checkoff report (install-time verification).
|
||||
# The manifest was already verified; this records what was actually installed
|
||||
# on this host and posts it as a security event.
|
||||
emit_checkoff_report
|
||||
|
||||
# Step 11: Enable and start service
|
||||
echo ""
|
||||
echo "Enabling and starting service..."
|
||||
sudo systemctl daemon-reload
|
||||
sudo systemctl enable ${SERVICE_NAME}
|
||||
|
|
|
|||
Loading…
Reference in a new issue