diff --git a/Dockerfile b/Dockerfile index 706bb86..3f2c1d6 100644 --- a/Dockerfile +++ b/Dockerfile @@ -58,6 +58,36 @@ RUN chmod +x /opt/secy/secy.sh /opt/secy/entrypoint.sh /opt/secy/watch.sh /opt/s # entrypoint.sh copies this into place. COPY agent/conf/srt-settings.json /opt/secy/conf/srt-settings.json +# Generate integrity manifest for security-critical files. +# Verified at container startup by entrypoint.sh. +RUN sha256sum \ + /opt/secy/AGENT.md \ + /opt/secy/WATCH.md \ + /opt/secy/PATROL.md \ + /opt/secy/C2.md \ + /opt/secy/conf/agent.conf \ + /opt/secy/conf/srt-settings.json \ + /opt/secy/entrypoint.sh \ + /opt/secy/secy.sh \ + /opt/secy/watch.sh \ + /opt/secy/patrol.sh \ + /opt/secy/c2.sh \ + /opt/secy/lib/secy-common.sh \ + /opt/secy/lib/agent-common.sh \ + /opt/secy/lib/c2-common.sh \ + /opt/secy/lib/patrol-common.sh \ + /opt/secy/lib/watch-common.sh \ + /opt/secy/lib/inotify-watch.sh \ + /opt/secy/lib/format-stream.sh \ + /usr/local/lib/sread/conf/blocked_paths \ + /usr/local/lib/sread/conf/redact_patterns \ + /usr/local/lib/sread/conf/allowed_mimetypes \ + /usr/local/lib/sread/lib/common.sh \ + /usr/local/lib/sread/lib/blocklist.sh \ + /usr/local/lib/sread/lib/redact.sh \ + /usr/local/bin/sread \ + > /opt/secy/integrity.sha256 + # State directory — mount a volume here for persistent findings RUN mkdir -p /var/lib/secy/state diff --git a/agent/entrypoint.sh b/agent/entrypoint.sh index c20aaed..7e21aa2 100755 --- a/agent/entrypoint.sh +++ b/agent/entrypoint.sh @@ -20,5 +20,27 @@ if [[ -f /mnt/claude-credentials.json ]]; then cp /mnt/claude-credentials.json /root/.claude/.credentials.json fi +# ── Integrity check ────────────────────────────────────────────── +# Verify that security-critical files match the hashes baked at build time. +# If any file was modified (tampered prompts, weakened blocklists, etc.), +# refuse to start. + +INTEGRITY_MANIFEST="/opt/secy/integrity.sha256" + +if [[ -f "$INTEGRITY_MANIFEST" ]]; then + if ! sha256sum --check --strict "$INTEGRITY_MANIFEST" > /dev/null 2>&1; then + echo "INTEGRITY CHECK FAILED — security-critical files have been modified:" >&2 + sha256sum --check "$INTEGRITY_MANIFEST" 2>&1 | grep -v ': OK$' >&2 + echo "" >&2 + echo "This likely means the image was not rebuilt after changing prompts," >&2 + echo "configs, or blocklists. Rebuild with: docker compose build" >&2 + echo "" >&2 + echo "If this is unexpected, the image may have been tampered with." >&2 + exit 1 + fi +else + echo "WARNING: integrity manifest not found — skipping verification" >&2 +fi + # ── Hand off to secy ───────────────────────────────────────────── exec /opt/secy/secy.sh "$@" diff --git a/setup.sh b/setup.sh index 56fc145..67bb4c9 100755 --- a/setup.sh +++ b/setup.sh @@ -177,6 +177,8 @@ do_install() { info "This may take a few minutes on first build..." docker compose -f "${SECY_DIR}/docker-compose.yml" build + # Store image digest for later verification + _save_image_digest info "Image built successfully" # ── 4. Start daemon services ────────────────────────────────── @@ -275,6 +277,9 @@ do_start() { exit 1 fi + # Verify image hasn't been replaced since install + _verify_image_digest + _start_containers # Start notification service @@ -380,6 +385,24 @@ do_status() { warn " data directory not found — run ./setup.sh install" fi + # ── Integrity ───────────────────────────────────────────────── + echo "" + echo -e "${BOLD}Integrity:${RESET}" + if [[ -f "${SECY_DATA_DIR}/.image-digest" ]]; then + local saved current + saved="$(cat "${SECY_DATA_DIR}/.image-digest")" + current="$(docker inspect secy --format '{{.Id}}' 2>/dev/null)" || current="" + if [[ -n "$current" ]] && [[ "$saved" == "$current" ]]; then + info " Image digest: verified" + elif [[ -n "$current" ]]; then + error " Image digest: MISMATCH (rebuilt or tampered since install)" + else + warn " Image digest: saved but image not found" + fi + else + warn " Image digest: not saved (run ./setup.sh install)" + fi + # ── Auth ────────────────────────────────────────────────────── echo "" echo -e "${BOLD}Auth:${RESET}" @@ -405,6 +428,43 @@ _start_containers() { info "Containers started" } +DIGEST_FILE="${SECY_DATA_DIR}/.image-digest" + +_save_image_digest() { + local digest + digest="$(docker inspect secy --format '{{.Id}}' 2>/dev/null)" || return 0 + mkdir -p "$(dirname "$DIGEST_FILE")" + echo "$digest" > "$DIGEST_FILE" + info "Image digest saved" +} + +_verify_image_digest() { + [[ -f "$DIGEST_FILE" ]] || return 0 + + local saved current + saved="$(cat "$DIGEST_FILE")" + current="$(docker inspect secy --format '{{.Id}}' 2>/dev/null)" || return 0 + + if [[ "$saved" != "$current" ]]; then + warn "Image digest mismatch — image was rebuilt or replaced since install" + warn " Expected: ${saved:0:20}..." + warn " Current: ${current:0:20}..." + warn " If you rebuilt intentionally, run: ./setup.sh install" + echo "" + if [[ -t 0 ]]; then + read -rp "Continue anyway? [y/N] " answer + if ! [[ "$answer" =~ ^[Yy]$ ]]; then + error "Aborted. Run ./setup.sh install to update the stored digest." + exit 1 + fi + else + error "Image digest mismatch in non-interactive mode. Aborting." + error "Run ./setup.sh install to update the stored digest." + exit 1 + fi + fi +} + _check_containers() { local containers="secy-secy-watch-1 secy-secy-patrol-1 secy-secy-c2-1" local all_running=true diff --git a/sread/lib/modules/secyhealth.sh b/sread/lib/modules/secyhealth.sh index 4d9d59a..81978fc 100644 --- a/sread/lib/modules/secyhealth.sh +++ b/sread/lib/modules/secyhealth.sh @@ -1,11 +1,31 @@ -# Check secy notification infrastructure: systemd service and cron health check +# Check secy infrastructure: integrity verification, notification service, cron health check # Usage: sread secyhealth run() { local root="" [[ -d "/host/home" ]] && root="/host" - section_header "SECY NOTIFICATION HEALTH" + section_header "SECY INFRASTRUCTURE HEALTH" + + # ── 0. Container integrity ──────────────────────────────────── + echo "--- Container integrity ---" + local integrity_issues=0 + local manifest="/opt/secy/integrity.sha256" + + if [[ -f "$manifest" ]]; then + if sha256sum --check --strict "$manifest" > /dev/null 2>&1; then + echo " Integrity manifest: verified (all files match)" + else + echo " [!] INTEGRITY CHECK FAILED — files modified since build:" + sha256sum --check "$manifest" 2>&1 | grep -v ': OK$' | sed 's/^/ /' + integrity_issues=$((integrity_issues + 1)) + fi + else + echo " [!] Integrity manifest: NOT FOUND at ${manifest}" + echo " Image may have been built without integrity support" + integrity_issues=$((integrity_issues + 1)) + fi + echo "" # Discover the user's home directory local user_home="" @@ -55,7 +75,7 @@ run() { service_issues=$((service_issues + 1)) fi else - echo " [!] Unit file: NOT FOUND at ${service_file#${root}}" + echo " [!] Unit file: NOT FOUND at ${service_file#"${root}"}" echo " Run: ./scripts/setup-notify.sh" service_issues=$((service_issues + 1)) fi @@ -115,7 +135,7 @@ run() { perms="$(stat -c%a "$issues_dir" 2>/dev/null || echo "?")" local issue_count issue_count="$(find "$issues_dir" -maxdepth 1 -name '*.md' -type f 2>/dev/null | wc -l)" - echo " Path: ${issues_dir#${root}}" + echo " Path: ${issues_dir#"${root}"}" echo " Owner: ${owner}, mode: ${perms}" echo " Issues on file: ${issue_count}" @@ -123,7 +143,7 @@ run() { echo " [!] Owned by root — notify.sh may not be able to read new files" fi elif [[ -n "$issues_dir" ]]; then - echo " [!] Issues directory not found: ${issues_dir#${root}}" + echo " [!] Issues directory not found: ${issues_dir#"${root}"}" else echo " (could not determine issues directory path)" fi @@ -131,16 +151,17 @@ run() { # ── Summary ──────────────────────────────────────────────────── echo "--- Summary ---" + echo " Integrity issues: ${integrity_issues}" echo " Service issues: ${service_issues}" echo " Cron issues: ${cron_issues}" - local total=$((service_issues + cron_issues)) + local total=$((integrity_issues + service_issues + cron_issues)) if [[ $total -eq 0 ]]; then - echo " Notification infrastructure: OK" + echo " secy infrastructure: OK" else - echo " [!] Notification infrastructure: DEGRADED (${total} issue(s))" + echo " [!] secy infrastructure: DEGRADED (${total} issue(s))" fi echo "" - log_ok "Notification health check complete (service_issues: ${service_issues}, cron_issues: ${cron_issues})" + log_ok "Health check complete (integrity: ${integrity_issues}, service: ${service_issues}, cron: ${cron_issues})" }