Skip to content

Releases: InnerWarden/innerwarden

v0.15.32

Choose a tag to compare

@github-actions github-actions released this 01 Jul 05:42
8d51716

InnerWarden 0.15.32

Fixed

  • macOS retest residuals (a re-test of the fixes below, on the same Mac, caught two more launchd-specific bugs the Linux CI cannot see). (1) system doctor's Services section had its OWN macOS branch that used launchctl list <label> — which only queries the caller's launchd domain, so a non-root doctor could not see the SYSTEM-domain daemons and reported both as "not running" while they were live (same class as the status bug, different code path). It now detects via the same process-presence check as get status. (2) uninstall --purge used dscl . -delete /Users/innerwarden to remove the macOS user, which HANGS on recent macOS (26.x) when driven non-interactively, so the user survived the purge; it now uses sysadminctl -deleteUser (+ dseditgroup -o delete for the group), and every teardown step runs with stdin redirected from /dev/null so no step can ever block on a prompt. Verified live: fresh install → doctor shows both services running → uninstall --purge removes binaries, plists, /usr/local data, and the user/group.
  • macOS support cluster: ctl status/doctor/uninstall + the macos_log collector were Linux/systemd-shaped and misbehaved on macOS (launchd) — found by a full install→test→uninstall pass on a real Mac (v0.15.31, arm64, macOS 26.5.1). The install itself was healthy (signed binaries verify, both launchd daemons load, the dashboard serves HTTPS 200, the Local Warden model downloads + verifies), but eleven platform bugs surfaced. Fixed together:
    • innerwarden uninstall was systemd-only and left macOS half-removed (critical). On macOS it reported services: (none found) while two launchd daemons were running, ran systemctl/userdel (which do not exist there), never launchctl bootout'd the daemons or removed the /Library/LaunchDaemons/com.innerwarden.*.plist files (so after deleting the binaries launchd KeepAlive respawned missing executables), and purged the Linux paths (/etc, /var/lib, /var/log/innerwarden) instead of the real /usr/local/... data — leaving config/data behind. uninstall now has a full launchd teardown (build_plan_macos): bootout supervisors-first, remove the plists, purge the /usr/local config/data/log dirs (plus the legacy /var/lib/innerwarden Local-Warden dir), and dscl . -delete the innerwarden user + group.
    • get status / system doctor reported RUNNING launchd services as "stopped". systemd::service_status called Linux-only systemctl is-active; it is now platform-aware (process-presence via pgrep on macOS) and get status distinguishes running / stopped / unknown so a live daemon is never falsely shown as down.
    • system doctor false-warned "--dashboard flag is missing". It read a nonexistent systemd .service file on macOS; it now reads the launchd plist's ProgramArguments, the message no longer hardcodes innerwarden-agent.service, and the reachability line advertises https:// (the dashboard is TLS).
    • The macos_log collector disabled itself on every modern macOS. Its readiness probe ran log version, which is not a valid subcommand (log: Unknown subcommand 'version', exit 64), so the check always failed and the sensor's primary macOS log source never ran. It now confirms the real Apple log tool by checking its usage output advertises the stream subcommand (ignoring the exit code, since log --help also exits 64).
    • exec-gate failed opaquely without bpftool and misreported an armed gate as inert. On a box lacking linux-tools-$(uname -r) every exec-gate arm/enforce/disarm failed with a bare exit 1, and exec-gate status printed mode=inert (reading None from the unavailable maps) even when the gate was actually enforcing. arm/rehearse/enforce/disarm now preflight and hard-fail with an actionable "install linux-tools-" message; status reports UNKNOWN — cannot read the maps ... may be armed instead of a false "inert"; and on non-Linux status says "not available on this platform (Linux eBPF-LSM only)".
    • Config-change restarts silently no-op'd on macOS. All systemd::restart_service call sites now route through restart_service_auto (launchd launchctl kickstart on macOS, systemd elsewhere), and the sensor's self-monitored config paths + the install-warden restart hint follow the platform install prefix. Also refreshed the stale exec-gate arm help text.
    • Release manifest was Linux-only for macOS users. The release workflow's aggregate SHA256SUMS covered only the Linux binaries; the macOS job now emits a GPG-signed SHA256SUMS-macos so macOS has the same signed-manifest verification path.

Install / upgrade (Linux, toolchain-free, signed binaries)

curl -fsSL https://innerwarden.com/install | sudo bash
# already installed:  sudo innerwarden upgrade --yes

Every binary below is signed (Ed25519 + Sigstore bundle). Docs: https://github.com/InnerWarden/innerwarden/wiki · Site: https://www.innerwarden.com

What's Changed

  • fix(macos): platform-aware ctl service ops, macos_log, and exec-gate (11 findings) by @maiconburn in #1176
  • fix(macos): retest residuals — remove user via sysadminctl + doctor Services via pgrep by @maiconburn in #1177
  • chore(release): 0.15.32 by @maiconburn in #1179

Full Changelog: v0.15.31...v0.15.32

v0.15.31

Choose a tag to compare

@github-actions github-actions released this 01 Jul 01:03
e7e4ceb

InnerWarden 0.15.31

Added

  • innerwarden exec-gate — a free, operator-driven CLI to arm the agent-scoped Execution Gate around your own AI agent (spec 083 product arming). The Execution Gate primitive shipped inert with no in-tree way to populate it — arming lived only in the paid, out-of-tree tooling, so the operator-facing answer to "how do I actually turn this on" was missing. This adds the full free arming path so a personal user can protect their own agent end to end, safe by construction: exec-gate status (live mode / scope / allowlist + scope-cgroup counts), exec-gate arm --pid <P> --observe [--path ...] (resolve the agent process's cgroup-v2 id and arm OBSERVE scoped to it — log what it WOULD block, never deny), exec-gate rehearse --pid <P> [--window N] (count the lsm.exec_gate_would_block events for that cgroup over the window and list the binaries still needing allowlisting), exec-gate enforce --pid <P> [--window N] (flip to deny-unknown-exec, but ONLY after a clean rehearsal — observe-armed, scoped to the pid, and zero would-block in the window — otherwise refused; never a blind flip), and exec-gate disarm (back to inert). The safety brain is in innerwarden_core::execution_gate: a userspace FNV-1a mirror of the in-kernel path hasher (parity-tested byte-for-byte against crates/sensor-ebpf, so an armed enforce gate's keys always match the kernel's), plan_arm (refuses enforce-with-empty-allowlist = the brick, refuses arming with no scope cgroup), an idempotent reconcile (only the diff, never a blind clear-and-rebuild), and a scope-aware divergence monitor (Divergence::ScopeArmedButEmpty) that flags an agent-scoped-but-empty gate in doctor / the slow loop. ctl writes the pinned maps via bpftool (ctl stays aya-free); the agent has the matching aya writer + cgroup-id resolver. Agent-scoped only (LSM_POLICY key 4 = 1 + EXEC_GATE_SCOPE) — the kernel returns allow for any cgroup outside the agent's before any allowlist lookup, so the host and other tenants are never gated (the k7 host-wide-brick lesson, scaled to one pod). Validated end to end on a real kernel (k6.8): observe → real would-block read from the event store → enforce refused while dirty → allowlist → rehearse clean → enforce flips (LSM_POLICY key 3 = 1) → enforce denies a new unknown exec in that one cgroup → disarm, with the operator's own session ungated throughout. No license gate: personal arming (incl. enforce) is free and safe by the rehearsal; the professional/fleet layer is the separate paid product. (Shipped across PRs #1167#1172.)
  • innerwarden agent install-hook --tenant <id> stamps the in-path command guard with its tenant (spec 084 P0 1D — closes P0). In a multi-tenant AI-agent fleet, each managed-agent container's guard now carries the tenant it belongs to: the installed guard script bakes IW_TENANT and sends it to the check-command brain on every check (a tenant body field plus an X-InnerWarden-Tenant header). The agent logs (agent-guard: check-command for tenant) and echoes the tenant in the response, so per-container guard activity is attributable per tenant alongside the innerwarden_incidents_by_tenant{...} metric. The verdict itself stays tenant-agnostic; both /api/agent/check-command and /api/advisor/check-command accept the tenant (body or header). The integration recipe documents baking install-hook --tenant "$TENANT" into the container image / pod template so the reported tenant is bound to the container, not self-asserted by the agent's prompt. Tests: the guard script stamps the tenant (body + header), resolve_tenant prefers body then header, and run_analysis echoes the tenant (and omits it when blank/absent). With this, spec 084 P0 (capture → pod_uid → tenant resolve → observable per-tenant attribution → per-container guard stamp) is complete.
  • Per-tenant attribution is now observable: a per-incident log + a per-tenant incident counter on /metrics (spec 084 P0 1C — closes the observability gap). Phases 1B/1C made the sensor stamp pod_uid and the agent resolve it to a tenant, but the enrichment was in-memory only (the agent enriches a read copy of the sensor-persisted incident), so "incident X → tenant Y" was not surfaced anywhere. Now: (1) tenancy::enrich_incident emits a tenancy: incident attributed to tenant info log (incident_id + tenant + namespace + pod), greppable on the box; (2) the telemetry layer counts incidents per tenant from the tenant:<id> tag and the agent's Prometheus /metrics endpoint exposes innerwarden_incidents_by_tenant{tenant="..."} alongside the existing per-detector / per-action counters (tenant label value escaped). Host / non-k8s incidents carry no tag and so create no per-tenant bucket; entirely inert unless [tenancy] enabled. Validated live on test001 (the deployed agent logged the per-tenant attribution + the counter incremented). Unit-tested: observe_incident buckets per tenant: tag and skips host incidents.
  • eBPF events now carry the Kubernetes pod UID + container runtime, not just the container id (spec 084 P0 phase 1B — the per-tenant attribution anchor). The container-id fix (#1142) gave every event a correct container_id on the systemd cgroup driver, but a multi-tenant AI-agent fleet attributes work to a tenant, and the non-forgeable hop from a container to its owning Kubernetes pod (and thus namespace/tenant) is the pod UID, which the kernel writes into the cgroup path (kubepods-besteffort-pod<uid>.slice) right next to the container id. The eBPF collector's cgroup parser now returns a ContainerIdentity { container_id, pod_uid, runtime } instead of a bare id: pod_uid is parsed from the pod<uuid> slice/segment and normalised to a canonical lowercase dashed UUID (the systemd driver escapes the UUID dashes to underscores; cgroupfs keeps them), and runtime is inferred from the leaf prefix (cri-containerd-→containerd, docker-, crio-, libpod-→podman) or the path. Both new fields are attached to every container-scoped eBPF event (exec, outbound connect, file read/write, privesc, ptrace, setuid, bind, mount, kill, container-drift, LSM-block) via attach_pod_runtime, alongside the existing container_id; host processes and non-k8s containers simply omit them. It is read entirely from the cgroup the kernel already exposes — no CRI/kubelet/API call in the sensor, so the collector stays deterministic. Validated on real data: the parser is gold-tested against live k3s v1.36.2+k3s1 (containerd) cgroups whose extracted pod UID equals kubectl get pod -o jsonpath='{.metadata.uid}' exactly, and a scale measurement over every container process on a two-tenant k3s node attributed 16/16 processes (3 tenants incl. 11 kube-system system pods) through the full cgroup_id → container → pod → tenant chain at 100% (gate: ≥99%). New ContainerIdentity type + parse_container_identity_from_cgroup / extract_pod_uid / detect_runtime / is_pod_uid pure helpers; 6 parser tests incl. the real-k3s gold fixtures. (Sensor half of spec 084 P0; the agent-side pod_uid → namespace/tenant resolver is phase 1C.)
  • The agent now resolves each container-scoped incident to its owning tenant (spec 084 P0 phase 1C — the agent half). Phase 1B made the sensor stamp pod_uid on events; this turns that into a human tenant. A new crates/agent/src/tenancy.rs keeps a refreshed pod_uid → {namespace, pod_name, tenant_id} cache built from the node's own kubeconfig (k3s /etc/rancher/k3s/k3s.yaml by default, or [tenancy] kubeconfig_path): the slow loop calls maybe_refresh (self-rate-limited to [tenancy] refresh_secs, default 60s) which reads the kubeconfig, does the reqwest-rustls client-cert handshake to the API server, and parses /api/v1/pods + /api/v1/namespaces. When a new incident carries a pod_uid (or a 12-char container_id / container entity), a synchronous pre-pass in process_incidents stamps tenant_id / namespace / pod_name into the incident evidence + a tenant:<id> tag, so the knowledge graph, notifications, AI triage and decisions all see the tenant. Tenant id precedence is pod label [tenancy] tenant_label_key (default innerwarden.io/tenant) → namespace label → namespace name, so even a plain kubectl run groups sanely. Sensor stays deterministic — all k8s API I/O lives in the agent, and nothing leaves the host (a node-local read of the cluster the agent already runs in). Inert by default ([tenancy] enabled = false): non-k8s hosts pay nothing. The pure parsers (parse_kubeconfig, parse_pod_list, derive_tenant, resolve, enrich_incident) are unit-tested against real k3s v1.36 kubeconfig + PodList JSON (the same pod UIDs / containerIDs / tenant labels as the 1B gold cgroups); a #[ignore]d live_refresh_against_local_cluster test exercises the real handshake on a k3s node. The same kubeconfig→TLS→/api/v1/pods path was proven live (the node's CA/cert/key authenticated and returned the real PodList), and the full cgroup_id → container → pod → tenant chain measured 100% on the two-tenant node. New [tenancy] agent config section.
  • Official claude-code-protection module — Claude Code is now a first-class registered integration, mirroring OpenClaw's packaging. Claude Code was already marked IntegrationLevel::Official in the agent-guard signature registry (crates/agent-guard/src/signatures.rs) and had a dedicated, enforcing in-path guard (innerwarden agent install-hook, 0.15.30), but it was missing the module/registry/docs that make openclaw-protection discoverable and installable. This adds: a registry.toml entry (so innerwarden module install/enable claude-code-protection resolves and known_module_id recognises it), modules/claude-code-protection/ (module.toml + docs/README.md), and a step-by-step docs/integration-recipes/claude-code-agent-guard.md (linked ...
Read more

v0.15.30

Choose a tag to compare

@github-actions github-actions released this 28 Jun 07:58
79da12c

InnerWarden 0.15.30

Added

  • The agent-guard command inspector now flags attempts to disable InnerWarden itself (in-path self-protection). The check-command brain (POST /api/agent/check-command, the MCP innerwarden_check_command tool, and the agent proxy guard) previously scored commands like systemctl stop innerwarden-*, pkill -f innerwarden, innerwarden uninstall, and rm/truncate of InnerWarden's own binaries, config, data, or pinned eBPF objects as allow / risk 0 - so an AI coding agent wired through the in-path guardrail could be talked into turning the monitor off without the guard objecting. A new security_tooling_tamper signal (score 60 -> deny) in crates/agent-guard (threats::check_security_tamper + SECURITY_TAMPER_INDICATORS / INNERWARDEN_SELF_PATHS) now denies InnerWarden self-disable/removal plus the universal defense-evasion verbs (systemctl stop auditd, setenforce 0, auditctl -e 0, disabling AppArmor; MITRE T1562/T1489). File removal requires a destructive verb AND an InnerWarden path, so status reads and restarts (innerwarden get status, systemctl status/restart innerwarden-agent, grepping a config under /etc/innerwarden) are NOT flagged. Closes the command-layer half of the self-tamper gap surfaced by the 2026-06-27 AI-coding-agent guardrail evaluation (the kernel-side mitre_hunt uid-0 self-stop carve-out is tracked separately). New unit tests pin deny on the tamper set, deny on the host-monitor set, and allow on the benign reads/restart.
  • innerwarden agent install-hook wires the in-path command guard into Claude Code (enforcing, not advisory). agent mcp-serve and POST /api/agent/check-command are advisory - a coding agent running its raw shell tool never asks. The new command writes a fail-closed guard script plus a PreToolUse Bash hook into the agent's settings.json (~/.claude/settings.json by default; --settings/--url/--block-review override), so every shell command the agent proposes is POSTed to the loopback check-command brain and blocked (exit 2) before it runs when the verdict is deny (or review with --block-review), failing CLOSED if the agent is unreachable. The settings merge is idempotent and preserves existing keys/hooks. Currently supports Claude Code. Unit-tested: the JSON merge (empty / idempotent / preserves existing / repairs a non-object root) and the generated script (deny-only vs block-review, the check-command call, fail-closed on error).

Fixed

  • The admin-action audit log was stamped with local time, drifting off the UTC date scheme. append_admin_action named admin-actions-<date>.jsonl from chrono::Local::now(), while every other date-stamped file InnerWarden writes (events-/incidents-/decisions-*.jsonl) and the reader (today_date_string) use UTC. In a non-UTC timezone straddling midnight (e.g. UK/BST after 00:00 local, still the previous UTC day) the audit entry landed on a different date than the rest of the system, splitting the day's audit trail and breaking the reader plus the cmd_tune audit test on that boundary. Now UTC, consistent with the rest of the date-stamped files.
  • The correlation-chain block path bypassed the cloud safelist and banned Canonical on Hetzner (cloud-FP sweep follow-up). Two sibling response paths gate a candidate IP against the cloud/CDN safelist before escalating: the repeat-offender path and the completed-correlation-chain path. The repeat-offender path was switched from identify_provider (a first-octet heuristic that only knows a handful of broad ranges) to cloud_safelist::safelist_label (the real CIDR walk) on 2026-05-08, but the chain path was missed and still used the heuristic. Result: the Data Exfiltration (eBPF Sequence) chain (CL-008-class) banned Canonical 185.125.190.49 (apt/livepatch) on Hetzner, because 185.125.188.0/22 is in the safelist's CIDR table but the first-octet heuristic does not know 185.x. Both paths now route through one shared safelisted_provider helper (a thin wrapper over safelist_label), so the gate cannot drift between them again. Anti-evasion preserved: the safelist is the existing CIDR table (no new IP hardcoded), and a real attacker IP outside every safelist range (203.0.113.45, 45.148.10.121) is still blockable by both paths. A new #[tokio::test] drives both async paths end-to-end with a CIDR-only safelisted IP and asserts each purges it from reputation state instead of escalating.

Install / upgrade (Linux, toolchain-free, signed binaries)

curl -fsSL https://innerwarden.com/install | sudo bash
# already installed:  sudo innerwarden upgrade --yes

Every binary below is signed (Ed25519 + Sigstore bundle). Docs: https://github.com/InnerWarden/innerwarden/wiki · Site: https://www.innerwarden.com

What's Changed

  • fix(agent): correlation-chain block path bypassed cloud_safelist (banned Canonical) by @maiconburn in #1125
  • docs(changelog): record #1125 correlation-chain safelist-bypass under Unreleased by @maiconburn in #1126
  • feat(agent-guard): deny commands that disable InnerWarden itself by @maiconburn in #1127
  • fix(core): stamp admin-action audit filename in UTC, not local time by @maiconburn in #1128
  • feat(ctl): innerwarden agent install-hook — enforce the guard in Claude Code by @maiconburn in #1129
  • release: 0.15.30 — AI-agent guardrail self-protection + audit UTC fix by @maiconburn in #1132

Full Changelog: v0.15.29...v0.15.30

v0.15.29

Choose a tag to compare

@github-actions github-actions released this 27 Jun 08:04
a4e97c0

InnerWarden 0.15.29

Fixed

  • execve events never carried the parent PID in-kernel, leaving the fileless-systemd false-positive gate (0.15.28) inert in production. The 0.15.28 post-deploy re-audit found fileless:systemd still firing on Azure. Root cause: the eBPF execve handler hardcoded event.ppid = 0, so every execve ppid came from a userspace /proc/<pid>/status fallback. That works for long-lived processes (it is why connect events have a parent) but misses short-lived ones, notably systemd's sealed-executor fexecve of /proc/self/fd/N whose /proc entry is gone before the ring reader can read it (the audit measured ppid=0 on 4995/5000 execve events). Because the 0.15.28 fileless-systemd parent-lineage gate needs the parent, it almost never engaged in prod. The fix reads task_struct->real_parent->tgid in-kernel at execve, mirroring the Execution Gate's BPRM_OFFSETS pattern: a new TASK_OFFSETS map (real_parent + tgid byte offsets) is populated by the userspace loader from kernel BTF (member_offset), and the handler does two bounded bpf_probe_read_kernel hops. If BTF is unavailable the offsets stay 0, the handler returns 0, and the /proc fallback applies unchanged (it never reads a guessed offset). Validated live on a 6.x x86_64 kernel: the verifier accepts the program, the offsets resolve from BTF, and a comm=systemd fexecve of /proc/self/fd/N now reports ppid=1, so the gate resolves /proc/1/exe to systemd and suppresses the false positive. aarch64 offsets are BTF-resolved identically.

Install / upgrade (Linux, toolchain-free, signed binaries)

curl -fsSL https://innerwarden.com/install | sudo bash
# already installed:  sudo innerwarden upgrade --yes

Every binary below is signed (Ed25519 + Sigstore bundle). Docs: https://github.com/InnerWarden/innerwarden/wiki · Site: https://www.innerwarden.com

What's Changed

  • fix(sensor): capture execve parent PID in-kernel so the fileless-systemd gate works by @maiconburn in #1123
  • release: 0.15.29 (eBPF execve parent-PID capture) by @maiconburn in #1124

Full Changelog: v0.15.28...v0.15.29

v0.15.28

Choose a tag to compare

@github-actions github-actions released this 26 Jun 23:40
b3a9bd5

InnerWarden 0.15.28

Fixed

Cloud-platform false-positive sweep from a 7-day decision-log audit of an Azure VM (the same method as the clean Oracle audit). A cloud VM's own platform agents and the platform control plane tripped the generic detectors, including a wrong auto-block of the Azure WireServer management IP. Every fix is keyed on a NON-IP signal (no platform IP is hardcoded in the product, by operator policy: "IPs change"); each ships with anti-evasion tests; the detector count is unchanged (82).

  • fileless:systemd flooded Critical on every unit start (Azure: 1206 Critical/week, all comm=systemd). systemd v254+ copies systemd-executor into a sealed memfd and fexecves it via /proc/self/fd/N at the start of every unit, which the fileless detector read as in-memory malware execution. The exec event carries no exe_path, so the fix resolves the launching process's parent via /proc/<ppid>/exe (the kernel symlink, which prctl(PR_SET_NAME)/argv[0] cannot forge) and suppresses only the self-fd fexecve form launched by a systemd manager. Anti-evasion preserved: /memfd:, /dev/fd/, (deleted), /proc/<other-pid>/fd/, and any non-systemd parent (a shell/dropper running exec /proc/self/fd/N) still fire; an unresolvable parent fails safe to firing; the memfd payload is still caught at creation by kernel_promote.
  • Cloud guest agents tripped C2 / flood / IMDS-SSRF detectors, auto-blocking the platform control plane. A cloud VM's management agents (Azure WALinuxAgent, AWS SSM agent / cloud-init, GCP guest agent, OCI cloud agent) poll the platform control plane (WireServer, IMDS) often enough to look like C2 beaconing, connection floods, and IMDS access by an unexpected process. On Azure this fed a cross-layer correlation that auto-blocked the WireServer management IP 168.63.129.16 six times (a block that can sever the VM from its management plane), and produced 869 IMDS needs-review incidents. New crates/sensor/src/cloud_platform.rs (a crate-root helper, not a detector) recognises the platform's agents by non-forgeable process identity: the cloud is auto-detected from DMI/SMBIOS (firmware strings, not anything userspace can forge); a compiled agent is matched by its real /proc/<pid>/exe; an interpreter agent (python3 /usr/sbin/waagent, cloud-init) is trusted only when its script argument is a known agent path that exists on disk as a root-owned file under a trusted system directory; and extension-handler children (relative script path) are matched by walking up to four parent hops to the real agent. is_guest_agent(pid, uid) is gated on a recognised cloud VM AND uid 0, and is used downgrade-only in c2_callback, outbound_anomaly, and imds_ssrf (the interpreter case its exe-prefix list missed). Anti-evasion (tested): an argv that merely names a guest-agent path without the file being root-owned, a trusted interpreter running a /tmp script, an untrusted interpreter, a planted look-alike path, on-prem Hyper-V (not Azure), bare metal, a non-root process, and a webserver runtime hitting IMDS all still fire. The WireServer actor was the Azure guest agent (python3 -u /usr/sbin/waagent), not the co-located AI agent.
  • dns_tunneling flagged Azure platform service DNS as high-entropy tunneling (Azure: 667/667 false positives). The host resolving Azure Storage / SQL / Service Bus / Key Vault FQDNs (<resource>.blob.core.windows.net, <vault>.vault.azure.net) tripped the Shannon-entropy heuristic on the random-looking resource name. The existing DNS_ALLOWED_DOMAINS allowlist (already covering provider-controlled zones like oraclevcn.com, internal.cloudapp.net, azure.com, amazonaws.com, googleapis.com) was simply missing the Azure service zones; windows.net and azure.net are added. This is a domain allowlist, not an IP one: Microsoft controls these zones and does not delegate arbitrary subdomains, so a DNS tunnel cannot be built under them. Anti-evasion preserved: the dot-boundary match means evil-windows.net is not trusted, and tunneling through an attacker-controlled zone still fires.

Install / upgrade (Linux, toolchain-free, signed binaries)

curl -fsSL https://innerwarden.com/install | sudo bash
# already installed:  sudo innerwarden upgrade --yes

Every binary below is signed (Ed25519 + Sigstore bundle). Docs: https://github.com/InnerWarden/innerwarden/wiki · Site: https://www.innerwarden.com

What's Changed

  • fix(sensor): suppress fileless FP from systemd sealed-executor unit launch by @maiconburn in #1119
  • fix(sensor): cloud-platform guest-agent provenance (non-IP) for WireServer/IMDS FPs by @maiconburn in #1120
  • fix(sensor): allowlist Azure platform service DNS zones (windows.net, azure.net) by @maiconburn in #1121
  • release: 0.15.28 (Azure cloud-platform false-positive sweep) by @maiconburn in #1122

Full Changelog: v0.15.27...v0.15.28

v0.15.27

Choose a tag to compare

@github-actions github-actions released this 26 Jun 15:30
ae8d0d6

InnerWarden 0.15.27

Fixed

  • memfd_create fileless-execution false positives on legitimate tools (prod 7-day audit). The kernel:memfd_fileless detector fired 17 High incidents in a week on benign memfd users: fwupdmgr (firmware updater), systemd's (sd-executor), and tokio-rt-worker (a generic Rust async-runtime thread name: InnerWarden's own agent and any other Rust service). The first two are added to the curated comm allowlist, but the Rust-runtime case is fixed the non-forgeable way, not by allowlisting a generic thread name (which any Rust payload could wear): a new third FP layer clears a memfd only when the creating process's kernel-captured exe path (details.exe_path, the execve filename) lives under a package-managed system directory (path_trust::is_trusted_system_path, the single source of truth now shared with host_drift). Anti-evasion preserved: a payload running from /tmp, with a deleted backing file ((deleted)), or memfd-backed exec is explicitly NOT trusted and still promotes, and the fexecve-from-memfd follow-up stays in the recommended checks. New tests pin the FP fixes AND the evasion cases (untrusted/deleted/memfd: exe paths still fire), plus a cross-test that host_drift and kernel_promote agree on what "trusted" means.
  • Cloud-range safelist could free-pass an AbuseIPDB-confirmed attacker (Context Gate blind spot). The 7-day audit found the Warden classifier ignore-ing IPs marked safelist=Google Cloud, abuseipdb=100: a cloud-range safelist was burying a community-confirmed attacker, exactly the free pass an attacker buys by renting cloud. The deterministic Context Gate now reads the existing ip_reputation (AbuseIPDB) and, escalate-only (identical shape to the DShield signal), refuses to passively close (dismiss/ignore) an incident whose IP scores >= 90/100, and blocks the provenance-driven benign-dismiss for such IPs. It can only ever raise a weak passive close to a surface (Monitor / RequestConfirmation), never relax an enforcement verdict, so a noisy shared-cloud IP at this score is at worst Monitored, never auto-blocked. The high floor avoids flooding the operator on borderline scores. New tests pin: confirmed-attacker passive close is surfaced, an enforcement verdict is left intact, a below-floor score is unchanged, the provenance self-dismiss is refused for a confirmed attacker, and the no-reputation path is unchanged.
  • innerwarden upgrade now arch-smoke-tests a new binary before swapping it in. sha256 + signature prove the downloaded bytes are authentic but NOT that they execute on this host's CPU; installing an x86_64 build on the aarch64 prod box took it down on 2026-06-10. The upgrader now stages each verified binary into the install directory (a package-trusted path, so the host's own host_drift detector does not flag the check the way a /tmp exec would, which is what produced the self-inflicted Critical host_drift incidents seen in the audit during the manual aarch64 deploy ritual) and runs --version. It hard-fails and keeps the existing binary only when the binary cannot execute (spawn failure / killed by signal, the wrong-CPU-arch / corruption case); a clean non-zero exit or a cosmetic version-string mismatch is a soft warning that proceeds, so upgrades never break on anything but a genuinely non-runnable asset. This retires the manual "file + ./bin --version in /tmp before swap" procedure by doing it in-product. The smoke-test verdict logic is a pure, fully unit-tested function.

Install / upgrade (Linux, toolchain-free, signed binaries)

curl -fsSL https://innerwarden.com/install | sudo bash
# already installed:  sudo innerwarden upgrade --yes

Every binary below is signed (Ed25519 + Sigstore bundle). Docs: https://github.com/InnerWarden/innerwarden/wiki · Site: https://www.innerwarden.com

What's Changed

  • fix: prod-audit fixes (memfd FP, AbuseIPDB cloud-safelist blind spot, upgrade arch smoke-test) by @maiconburn in #1117
  • release: 0.15.27 by @maiconburn in #1118

Full Changelog: v0.15.26...v0.15.27

v0.15.26

Choose a tag to compare

@github-actions github-actions released this 25 Jun 17:23
a73c93e

InnerWarden 0.15.26

Changed

  • DShield (SANS ISC) reputation is now a real decision signal on the Warden classifier path, not just LLM context. DShield enrichment already attached the community's global attack history to attacker profiles and fed the LLM prompt, but the on-device Warden classifier and its deterministic Context Gate ignored it. The gate now reads a structured ip_dshield_attacker signal (DshieldReputation::is_known_attacker: ISC reports > 0 or active threat-feed membership) and, escalate-only, (1) refuses to passively close (dismiss/ignore) an incident from a DShield-confirmed global attacker, surfacing it instead, and (2) blocks the provenance-driven benign-dismiss for such IPs. It can only ever raise a weak verdict, never relax an enforcement action. The trained classifier's text input is intentionally not changed (novel input is out-of-distribution; enriching the model input is the separate re-distill path), so the model's behaviour is unchanged; DShield acts deterministically in the gate that wraps it. New unit tests pin: a confirmed attacker's confident dismiss is surfaced, an enforcement verdict is left intact, and a non-DShield low-severity dismiss is unchanged.

Fixed

  • Installer no longer sends the telemetry ping from CI / automation. Installer smoke-tests run on ephemeral CI runners (GitHub Actions and friends), each a fresh machine-id from a US x86_64 box, so every run was writing an install row into the opt-out install telemetry, inflating the install count with non-users (most of a given window's "installs" were our own CI). install.sh now detects a CI environment (CI=true/1, or any of GITHUB_ACTIONS/GITLAB_CI/JENKINS_URL/BUILDKITE/CIRCLECI/TF_BUILD/TEAMCITY_VERSION/DRONE) and skips the ping (logging that it did). The install itself still runs and is still verified in CI; only the ping is suppressed, so the telemetry reflects real installs. CI=false (some dev shells) is correctly treated as not-CI.

Install / upgrade (Linux, toolchain-free, signed binaries)

curl -fsSL https://innerwarden.com/install | sudo bash
# already installed:  sudo innerwarden upgrade --yes

Every binary below is signed (Ed25519 + Sigstore bundle). Docs: https://github.com/InnerWarden/innerwarden/wiki · Site: https://www.innerwarden.com

What's Changed

Full Changelog: v0.15.25...v0.15.26

v0.15.25

Choose a tag to compare

@github-actions github-actions released this 24 Jun 18:51
373bea5

InnerWarden 0.15.25

Security

  • quinn-proto 0.11.14 → 0.11.15 (RUSTSEC-2026-0185). Fixes a remote memory-exhaustion advisory (unbounded out-of-order stream reassembly) in the transitive QUIC dependency. The unrelated tract-onnx 0.22 → 0.23 bump was deliberately NOT taken (breaks the build + would need inference-parity revalidation of the Local Warden classifier for zero security benefit).
  • memmap2 0.9.10 → 0.9.11 (RUSTSEC-2026-0186). Clears an "unchecked pointer offset" unsoundness advisory (published 2026-06-20) in a transitive mmap dependency that only enters the tree with the local-classifier feature. Lockfile-only; cargo deny check is clean (advisories/bans/licenses/sources all ok).
  • rustls 0.23.40 → 0.23.41 and bytes 1.11.1 → 1.12.0. Routine backward-compatible dependency maintenance.

Added

  • Execution Gate can now enforce around just the AI agent (agent-scoped mode, spec 083 eBPF primitive). The Execution Gate is a path-exact allowlist enforced in the kernel; host-wide it fits a locked-down appliance, but a general-purpose server constantly execs legitimate new/transient binaries (dpkg/apt maintainer scripts, certbot renewals, dynamic container workloads), so a host-wide allowlist would block them. New opt-in cgroup scoping lets the gate enforce solely inside the AI agent's process tree and allow everything else unconditionally — "zero-trust for the agent" without touching the rest of the machine. New pinned map EXEC_GATE_SCOPE (/sys/fs/bpf/innerwarden/exec_gate_scope, cgroup id → 1) holds the agent's cgroup id(s); the gate consults LSM_POLICY key 4: when 1, try_exec_gate fires only for tasks whose current cgroup id is in EXEC_GATE_SCOPE and allows every other exec. Key 4 absent/0 = host-wide, the original behaviour, so this is opt-in with no regression. The scope map is repin_preserving so it survives sensor restart; empty-while-scoped is fail-open (the gate never fires), so a wipe is not a brick. Free + INERT in the OSS sensor (key 4 unset); the paid config-sign tooling populates the scope and flips key 4. Verifier-cheap: one map read plus, when scoped, one bpf_get_current_cgroup_id lookup, both patterns already used in the gate hook. eBPF program count unchanged (a new map, not a new program).
  • Mesh-VPN persistence detection is now rename-proof (behavioural TUN/WireGuard signal). The previous exec-name detector (tailscale/zerotier/…) could be evaded by renaming the binary. New tunnel_iface collector (collector #31) watches /sys/class/net for a new tun/WireGuard interface appearing at runtime and classifies by the kernel-set TYPE (uevent: DEVTYPE=wireguard or the tun_flags attribute), not the name — so a renamed mesh-VPN binary is still caught, because the tunnel still has to create a tun/wg interface to route traffic. Interfaces present at startup are baselined (the operator's own VPN), so only a tunnel that comes up later fires. The c2_web_tunnel detector promotes the event to a High, allowlistable ([detectors.c2_web_tunnel]) incident with the same dual-use framing ("legitimate if you started a VPN — allowlist it; if not, it is attacker persistence", T1572/T1219). On by default (AlwaysOnCollectorConfig), 30s poll, deduped on the 600s cooldown. New unit tests pin: WireGuard caught by DEVTYPE even under a non-tunnel name, TUN caught by tun_flags, plain interfaces ignored, and the High mesh_vpn_iface incident. Closes the rename-evasion follow-up tracked when the exec-name detector shipped.
  • innerwarden playbook test --insecure. The agent dashboard serves HTTPS with a self-signed certificate, so innerwarden playbook test --url https://127.0.0.1:8787 … failed with invalid peer certificate: UnknownIssuer and the command could not reach its own agent. The new --insecure flag skips TLS verification for the self-signed cert (documented as not-for-untrusted-networks), so the dry-run playbook test works against the live HTTPS dashboard. Unit-tested for both the verifying and insecure agent-construction paths.
  • n8n integration recipe for the Agent Guard API (docs). New
    docs/integration-recipes/n8n-agent-guard.md shows how to drive the existing
    GET /api/agent/security-context (threat assessment) and
    POST /api/agent/check-command (safety validation) endpoints from an n8n
    workflow: HTTP Request node configuration for each endpoint, the request/response shapes
    and recommendation thresholds (allow/review/deny), and a complete importable
    workflow JSON that halts automatically when the server threat level is elevated or a
    command is denied. Documentation-only — no code or behaviour change. Closes the n8n gap
    noted alongside the existing OpenClaw guide; linked from integrations/README.md.
  • Mesh / overlay-VPN remote-access tools are now detected as a persistence channel (Tailscale, ZeroTier, NetBird, Nebula). Closes a real gap: an attacker who lands on a host can install a mesh VPN (tailscale/tailscaled, zerotier-one/zerotier-cli, netbird, nebula) and SSH back in over the encrypted, NAT-traversing tunnel — stable persistent access that looks like ordinary infrastructure (T1572 protocol tunneling / T1219 remote-access software). Ngrok/Cloudflare/bore/frp/chisel were already covered by c2_web_tunnel; the mesh-VPN family was not. The detector now fires on exec of a known mesh-VPN binary. UX-safe by design because these tools are commonly legitimate: fired at High (not Critical, unlike the C2 tunnels) so it never auto-blocks on its own, exec-only (no coordination-DNS matching, which would be noisy on hosts that legitimately run a mesh VPN), deduped on a 600s cooldown, and allowlistable via [detectors.c2_web_tunnel]; the incident text says plainly "LEGITIMATE if you use it for admin access — allowlist it; if you did NOT install it, it is a common attacker-persistence channel." Anti-gap, honestly scoped: the match is on the exact argv0 basename, so a renamed mesh-VPN binary evades exec-name detection — that limitation is documented in the detector and tracked as a behavioural TUN/WireGuard follow-up (the tunnel still has to create a tun/wg interface, which is the rename-proof signal). New unit tests pin: mesh binaries fire High with sub_kind mesh_vpn, the existing tunnel binaries stay Critical, substring/unrelated binaries stay quiet, and repeat execs are deduped. Detector count unchanged (82 — extends the existing c2_web_tunnel).

Fixed

  • KG decide-modifier (spec 043) is no longer inert — it now measures entity tenure with a clock that survives restarts. The Knowledge-Graph confidence modifier was sitting at modifier_raw=0.0 on essentially every incident in production, so it never did its job (suppressing false positives on long-tenured benign IPs) and could never accumulate the "non-zero would_change_action" data its own promotion gate requires. Root cause: its useful benign-suppression bands gate on first_seen_age_days >= 7, but it read first_seen from the in-memory KG IP node, which is rebuilt from a dated, daily graph snapshot and effectively resets across days/restarts — so the age gate was unreachable. Fix: merge_persisted_profile now overlays the persisted attacker-intel profile (loaded from redb on boot, carrying the true first sighting + composite risk) onto the KG features, taking the OLDER age and HIGHER risk. This makes the age-gated benign bands reachable for genuinely long-lived IPs and keeps the repeat-offender band honest, with no detection weakening (the merge only lengthens tenure / raises risk, never the reverse). Still shadow mode by default — it now produces real signal to validate before any operator flips it to enforce. New unit tests pin the unlock and the never-weakens invariant.
  • InnerWarden no longer flags its OWN egress as a reverse shell (self-FP). The eBPF reverse-shell sequence detector (network.outbound_connect + process.fd_redirect/dup2 within a window, per PID) fired Critical ebpf_reverse_shell incidents on the agent's and CLI's own legitimate outbound connections — Telegram notifications (149.154.166.x), the dashboard API, threat-feed polling — because the agent connects out and dup2's fds in the same process. Observed as ~126 Critical self-flags in 30 minutes on a test box (source comm innerwarden-age / innerwarden); pure noise (it did not auto-block) but it spammed incidents and polluted measurements. Now the sequence detector skips a verified InnerWarden self-process, gated by is_verified_infra_process — i.e. the comm matches innerwarden* AND /proc/<pid>/exe resolves to a real system path. No blind spot: a process that merely sets comm=innerwarden-* but whose exe is /tmp (or anywhere non-system) still fires. Verified via the reliable connect-time comm, so skipping the connect also prevents a later corrupted-comm fd_redirect from firing. Regression tests pin both the self-skip and the forged-comm-still-fires case.

Install / upgrade (Linux, toolchain-free, signed binaries)

curl -fsSL https://innerwarden.com/install | sudo bash
# already installed:  sudo innerwarden upgrade --yes

Every binary below is signed (Ed25519 + Sigstore bundle). Docs: https://github.com/InnerWarden/innerwarden/wiki · Site: https://www.innerwarden.com

What's Changed

  • fix(sensor): stop InnerWarden flagging its own egress as a reverse shell by @maiconburn in #1097
  • feat(sensor): detect mesh/overlay-VPN persistence (tailscale/zerotier/netbird/nebula) by @maiconburn in #1098
  • test(sensor): cover journald collector command and cursor branches by @GordonYuanyc in #1096
  • docs(readme): clarity pass for the top screen (virality + comprehension) by...
Read more

v0.15.24

Choose a tag to compare

@github-actions github-actions released this 21 Jun 21:30
a0463f6

InnerWarden 0.15.24

Security

  • spec-081 managed-agent coexistence now works when InnerWarden runs non-root and the agent runs as another user (live FP fix, found 2026-06-21). A co-located AI agent (OpenClaw) doing a routine task — read its own /home/lab/.env, then call its own Azure-OpenAI endpoint — was flagged CRITICAL data-exfiltration and the endpoint was auto-blocked, breaking the agent. Root cause: the managed-agent verifier (evaluate_managed_agent_downgradedecide) fail-closed on two facts a non-root IW agent (innerwarden uid) cannot obtain about a process owned by a different user (lab): (1) readlink /proc/<pid>/exe is EACCES cross-uid → exe_path None → the interpreter-root gate blocked; (2) ProtectHome=yes on the agent unit hid /home, so the own-config stat for the file-owner uid returned None → the own-config gate blocked. Both made spec-081 silently never downgrade for a cross-user agent, even one correctly registered with a matching cmdline fingerprint. Fixes: (a) code — when /proc/exe is unreadable the interpreter-root check falls back to argv[0], safe because the exact registered cmdline-fingerprint match already pins identity (an untrusted argv[0] like /tmp/node still blocks); (b) ops — the example agent unit sets ProtectHome=read-only (so the verifier can read /home to confirm the agent's own config) with an optional CAP_SYS_PTRACE for strict /proc/exe verification. No blind spot: a foreign-secret read (/etc/shadow, another user's ~/.ssh), an unregistered/fingerprint-mismatched process, or a known-bad destination still forces the block. New regression tests pin the cross-uid downgrade + the untrusted-argv0 block.

Install / upgrade (Linux, toolchain-free, signed binaries)

curl -fsSL https://innerwarden.com/install | sudo bash
# already installed:  sudo innerwarden upgrade --yes

Every binary below is signed (Ed25519 + Sigstore bundle). Docs: https://github.com/InnerWarden/innerwarden/wiki · Site: https://www.innerwarden.com

What's Changed

Full Changelog: v0.15.23...v0.15.24

v0.15.23

Choose a tag to compare

@github-actions github-actions released this 21 Jun 18:05
8290203

InnerWarden 0.15.23

Fixed

  • innerwarden upgrade now retries transient asset/sidecar download failures. Right after a release, GitHub's asset CDN intermittently fails individual binary or sidecar (.sha256/.sig) downloads while it propagates the new release. The old code aborted on the first such failure, which made upgrade brittle in exactly that window — deploying 0.15.22, two consecutive runs on one box each failed on a different sidecar before a manual curl --retry deploy succeeded. The binary download and both sidecar fetches now retry (4 attempts, 3s apart); a binary retry re-creates the destination so a partial download is never kept. Retry policy is a pure, unit-tested helper.

Install / upgrade (Linux, toolchain-free, signed binaries)

curl -fsSL https://innerwarden.com/install | sudo bash
# already installed:  sudo innerwarden upgrade --yes

Every binary below is signed (Ed25519 + Sigstore bundle). Docs: https://github.com/InnerWarden/innerwarden/wiki · Site: https://www.innerwarden.com

What's Changed

Full Changelog: v0.15.22...v0.15.23