Releases: InnerWarden/innerwarden
Release list
v0.15.32
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 usedlaunchctl list <label>— which only queries the caller's launchd domain, so a non-rootdoctorcould 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 asget status. (2)uninstall --purgeuseddscl . -delete /Users/innerwardento remove the macOS user, which HANGS on recent macOS (26.x) when driven non-interactively, so the user survived the purge; it now usessysadminctl -deleteUser(+dseditgroup -o deletefor the group), and every teardown step runs with stdin redirected from/dev/nullso no step can ever block on a prompt. Verified live: fresh install →doctorshows both services running →uninstall --purgeremoves binaries, plists,/usr/localdata, and the user/group. - macOS support cluster:
ctlstatus/doctor/uninstall + themacos_logcollector 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 uninstallwas systemd-only and left macOS half-removed (critical). On macOS it reportedservices: (none found)while two launchd daemons were running, ransystemctl/userdel(which do not exist there), neverlaunchctl bootout'd the daemons or removed the/Library/LaunchDaemons/com.innerwarden.*.plistfiles (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.uninstallnow has a full launchd teardown (build_plan_macos): bootout supervisors-first, remove the plists, purge the/usr/localconfig/data/log dirs (plus the legacy/var/lib/innerwardenLocal-Warden dir), anddscl . -deletetheinnerwardenuser + group.get status/system doctorreported RUNNING launchd services as "stopped".systemd::service_statuscalled Linux-onlysystemctl is-active; it is now platform-aware (process-presence viapgrepon macOS) andget statusdistinguishes running / stopped / unknown so a live daemon is never falsely shown as down.system doctorfalse-warned "--dashboard flag is missing". It read a nonexistent systemd.servicefile on macOS; it now reads the launchd plist'sProgramArguments, the message no longer hardcodesinnerwarden-agent.service, and the reachability line advertiseshttps://(the dashboard is TLS).- The
macos_logcollector disabled itself on every modern macOS. Its readiness probe ranlog 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 Applelogtool by checking its usage output advertises thestreamsubcommand (ignoring the exit code, sincelog --helpalso exits 64). exec-gatefailed opaquely withoutbpftooland misreported an armed gate as inert. On a box lackinglinux-tools-$(uname -r)everyexec-gate arm/enforce/disarmfailed with a bare exit 1, andexec-gate statusprintedmode=inert(readingNonefrom the unavailable maps) even when the gate was actually enforcing.arm/rehearse/enforce/disarmnow preflight and hard-fail with an actionable "install linux-tools-" message;statusreportsUNKNOWN — cannot read the maps ... may be armedinstead of a false "inert"; and on non-Linuxstatussays "not available on this platform (Linux eBPF-LSM only)".- Config-change restarts silently no-op'd on macOS. All
systemd::restart_servicecall sites now route throughrestart_service_auto(launchdlaunchctl kickstarton macOS, systemd elsewhere), and the sensor's self-monitored config paths + theinstall-wardenrestart hint follow the platform install prefix. Also refreshed the staleexec-gate armhelp text. - Release manifest was Linux-only for macOS users. The release workflow's aggregate
SHA256SUMScovered only the Linux binaries; the macOS job now emits a GPG-signedSHA256SUMS-macosso 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 --yesEvery 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
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 thelsm.exec_gate_would_blockevents 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), andexec-gate disarm(back to inert). The safety brain is ininnerwarden_core::execution_gate: a userspace FNV-1a mirror of the in-kernel path hasher (parity-tested byte-for-byte againstcrates/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 indoctor/ the slow loop. ctl writes the pinned maps viabpftool(ctl stays aya-free); the agent has the matching aya writer + cgroup-id resolver. Agent-scoped only (LSM_POLICYkey 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_POLICYkey 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 bakesIW_TENANTand sends it to thecheck-commandbrain on every check (atenantbody field plus anX-InnerWarden-Tenantheader). 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 theinnerwarden_incidents_by_tenant{...}metric. The verdict itself stays tenant-agnostic; both/api/agent/check-commandand/api/advisor/check-commandaccept the tenant (body or header). The integration recipe documents bakinginstall-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_tenantprefers body then header, andrun_analysisechoes 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 stamppod_uidand 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_incidentemits atenancy: incident attributed to tenantinfo log (incident_id + tenant + namespace + pod), greppable on the box; (2) the telemetry layer counts incidents per tenant from thetenant:<id>tag and the agent's Prometheus/metricsendpoint exposesinnerwarden_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_incidentbuckets pertenant: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_idon 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 aContainerIdentity { container_id, pod_uid, runtime }instead of a bare id:pod_uidis parsed from thepod<uuid>slice/segment and normalised to a canonical lowercase dashed UUID (the systemd driver escapes the UUID dashes to underscores; cgroupfs keeps them), andruntimeis 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) viaattach_pod_runtime, alongside the existingcontainer_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 equalskubectl 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 fullcgroup_id → container → pod → tenantchain at 100% (gate: ≥99%). NewContainerIdentitytype +parse_container_identity_from_cgroup/extract_pod_uid/detect_runtime/is_pod_uidpure 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_uidon events; this turns that into a human tenant. A newcrates/agent/src/tenancy.rskeeps a refreshedpod_uid → {namespace, pod_name, tenant_id}cache built from the node's own kubeconfig (k3s/etc/rancher/k3s/k3s.yamlby default, or[tenancy] kubeconfig_path): the slow loop callsmaybe_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 apod_uid(or a 12-charcontainer_id/ container entity), a synchronous pre-pass inprocess_incidentsstampstenant_id/namespace/pod_nameinto the incident evidence + atenant:<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(defaultinnerwarden.io/tenant) → namespace label → namespace name, so even a plainkubectl rungroups 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]dlive_refresh_against_local_clustertest exercises the real handshake on a k3s node. The same kubeconfig→TLS→/api/v1/podspath was proven live (the node's CA/cert/key authenticated and returned the real PodList), and the fullcgroup_id → container → pod → tenantchain measured 100% on the two-tenant node. New[tenancy]agent config section. - Official
claude-code-protectionmodule — Claude Code is now a first-class registered integration, mirroring OpenClaw's packaging. Claude Code was already markedIntegrationLevel::Officialin 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 makeopenclaw-protectiondiscoverable and installable. This adds: aregistry.tomlentry (soinnerwarden module install/enable claude-code-protectionresolves andknown_module_idrecognises it),modules/claude-code-protection/(module.toml+docs/README.md), and a step-by-stepdocs/integration-recipes/claude-code-agent-guard.md(linked ...
v0.15.30
InnerWarden 0.15.30
Added
- The agent-guard command inspector now flags attempts to disable InnerWarden itself (in-path self-protection). The
check-commandbrain (POST /api/agent/check-command, the MCPinnerwarden_check_commandtool, and theagent proxyguard) previously scored commands likesystemctl stop innerwarden-*,pkill -f innerwarden,innerwarden uninstall, andrm/truncateof InnerWarden's own binaries, config, data, or pinned eBPF objects asallow/ 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 newsecurity_tooling_tampersignal (score 60 ->deny) incrates/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-sidemitre_huntuid-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-hookwires the in-path command guard into Claude Code (enforcing, not advisory).agent mcp-serveandPOST /api/agent/check-commandare advisory - a coding agent running its raw shell tool never asks. The new command writes a fail-closed guard script plus a PreToolUseBashhook into the agent'ssettings.json(~/.claude/settings.jsonby default;--settings/--url/--block-reviewoverride), so every shell command the agent proposes is POSTed to the loopbackcheck-commandbrain and blocked (exit 2) before it runs when the verdict isdeny(orreviewwith--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_actionnamedadmin-actions-<date>.jsonlfromchrono::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 thecmd_tuneaudit 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) tocloud_safelist::safelist_label(the real CIDR walk) on 2026-05-08, but the chain path was missed and still used the heuristic. Result: theData Exfiltration (eBPF Sequence)chain (CL-008-class) banned Canonical185.125.190.49(apt/livepatch) on Hetzner, because185.125.188.0/22is in the safelist's CIDR table but the first-octet heuristic does not know185.x. Both paths now route through one sharedsafelisted_providerhelper (a thin wrapper oversafelist_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 --yesEvery 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
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:systemdstill firing on Azure. Root cause: the eBPF execve handler hardcodedevent.ppid = 0, so every execveppidcame from a userspace/proc/<pid>/statusfallback. That works for long-lived processes (it is whyconnectevents have a parent) but misses short-lived ones, notably systemd's sealed-executorfexecveof/proc/self/fd/Nwhose/procentry is gone before the ring reader can read it (the audit measuredppid=0on 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 readstask_struct->real_parent->tgidin-kernel at execve, mirroring the Execution Gate'sBPRM_OFFSETSpattern: a newTASK_OFFSETSmap (real_parent+tgidbyte offsets) is populated by the userspace loader from kernel BTF (member_offset), and the handler does two boundedbpf_probe_read_kernelhops. If BTF is unavailable the offsets stay 0, the handler returns 0, and the/procfallback 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 acomm=systemdfexecveof/proc/self/fd/Nnow reportsppid=1, so the gate resolves/proc/1/exeto 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 --yesEvery 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
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:systemdflooded Critical on every unit start (Azure: 1206 Critical/week, allcomm=systemd). systemd v254+ copiessystemd-executorinto a sealed memfd andfexecves it via/proc/self/fd/Nat the start of every unit, which the fileless detector read as in-memory malware execution. The exec event carries noexe_path, so the fix resolves the launching process's parent via/proc/<ppid>/exe(the kernel symlink, whichprctl(PR_SET_NAME)/argv[0]cannot forge) and suppresses only the self-fdfexecveform launched by a systemd manager. Anti-evasion preserved:/memfd:,/dev/fd/,(deleted),/proc/<other-pid>/fd/, and any non-systemd parent (a shell/dropper runningexec /proc/self/fd/N) still fire; an unresolvable parent fails safe to firing; the memfd payload is still caught at creation bykernel_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.16six times (a block that can sever the VM from its management plane), and produced 869 IMDS needs-review incidents. Newcrates/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 ANDuid 0, and is used downgrade-only inc2_callback,outbound_anomaly, andimds_ssrf(the interpreter case its exe-prefix list missed). Anti-evasion (tested): anargvthat merely names a guest-agent path without the file being root-owned, a trusted interpreter running a/tmpscript, 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_tunnelingflagged 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 existingDNS_ALLOWED_DOMAINSallowlist (already covering provider-controlled zones likeoraclevcn.com,internal.cloudapp.net,azure.com,amazonaws.com,googleapis.com) was simply missing the Azure service zones;windows.netandazure.netare 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 meansevil-windows.netis 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 --yesEvery 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
InnerWarden 0.15.27
Fixed
memfd_createfileless-execution false positives on legitimate tools (prod 7-day audit). Thekernel:memfd_filelessdetector fired 17 High incidents in a week on benign memfd users:fwupdmgr(firmware updater), systemd's(sd-executor), andtokio-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 curatedcommallowlist, 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, theexecvefilename) lives under a package-managed system directory (path_trust::is_trusted_system_path, the single source of truth now shared withhost_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 thefexecve-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 thathost_driftandkernel_promoteagree 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 markedsafelist=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 existingip_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 upgradenow 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 ownhost_driftdetector does not flag the check the way a/tmpexec would, which is what produced the self-inflicted Criticalhost_driftincidents 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 --versionin /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 --yesEvery 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
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_attackersignal (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
installrow 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.shnow detects a CI environment (CI=true/1, or any ofGITHUB_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 --yesEvery binary below is signed (Ed25519 + Sigstore bundle). Docs: https://github.com/InnerWarden/innerwarden/wiki · Site: https://www.innerwarden.com
What's Changed
- fix(install): skip telemetry ping in CI / automation by @maiconburn in #1114
- feat(agent): DShield reputation as a decision signal in the Context Gate by @maiconburn in #1115
- release: 0.15.26 by @maiconburn in #1116
Full Changelog: v0.15.25...v0.15.26
v0.15.25
InnerWarden 0.15.25
Security
quinn-proto0.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 unrelatedtract-onnx0.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).memmap20.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 thelocal-classifierfeature. Lockfile-only;cargo deny checkis clean (advisories/bans/licenses/sources all ok).rustls0.23.40 → 0.23.41 andbytes1.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/aptmaintainer scripts,certbotrenewals, 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 mapEXEC_GATE_SCOPE(/sys/fs/bpf/innerwarden/exec_gate_scope, cgroup id → 1) holds the agent's cgroup id(s); the gate consultsLSM_POLICYkey 4: when1,try_exec_gatefires only for tasks whose current cgroup id is inEXEC_GATE_SCOPEand allows every other exec. Key 4 absent/0 = host-wide, the original behaviour, so this is opt-in with no regression. The scope map isrepin_preservingso 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 paidconfig-signtooling populates the scope and flips key 4. Verifier-cheap: one map read plus, when scoped, onebpf_get_current_cgroup_idlookup, 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. Newtunnel_ifacecollector (collector #31) watches/sys/class/netfor a new tun/WireGuard interface appearing at runtime and classifies by the kernel-set TYPE (uevent: DEVTYPE=wireguardor thetun_flagsattribute), not the name — so a renamed mesh-VPN binary is still caught, because the tunnel still has to create atun/wginterface to route traffic. Interfaces present at startup are baselined (the operator's own VPN), so only a tunnel that comes up later fires. Thec2_web_tunneldetector 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 byDEVTYPEeven under a non-tunnel name, TUN caught bytun_flags, plain interfaces ignored, and the Highmesh_vpn_ifaceincident. 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, soinnerwarden playbook test --url https://127.0.0.1:8787 …failed withinvalid peer certificate: UnknownIssuerand the command could not reach its own agent. The new--insecureflag 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.mdshows 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 fromintegrations/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 byc2_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 atun/wginterface, which is the rename-proof signal). New unit tests pin: mesh binaries fire High with sub_kindmesh_vpn, the existing tunnel binaries stay Critical, substring/unrelated binaries stay quiet, and repeat execs are deduped. Detector count unchanged (82 — extends the existingc2_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.0on 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-zerowould_change_action" data its own promotion gate requires. Root cause: its useful benign-suppression bands gate onfirst_seen_age_days >= 7, but it readfirst_seenfrom 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_profilenow 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 toenforce. 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 Criticalebpf_reverse_shellincidents 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 comminnerwarden-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 byis_verified_infra_process— i.e. the comm matchesinnerwarden*AND/proc/<pid>/exeresolves to a real system path. No blind spot: a process that merely setscomm=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 --yesEvery 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...
v0.15.24
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_downgrade→decide) fail-closed on two facts a non-root IW agent (innerwardenuid) cannot obtain about a process owned by a different user (lab): (1)readlink /proc/<pid>/exeis EACCES cross-uid →exe_pathNone → the interpreter-root gate blocked; (2)ProtectHome=yeson the agent unit hid/home, so the own-configstatfor 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/exeis unreadable the interpreter-root check falls back toargv[0], safe because the exact registered cmdline-fingerprint match already pins identity (an untrustedargv[0]like/tmp/nodestill blocks); (b) ops — the example agent unit setsProtectHome=read-only(so the verifier can read /home to confirm the agent's own config) with an optionalCAP_SYS_PTRACEfor strict/proc/exeverification. 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 --yesEvery binary below is signed (Ed25519 + Sigstore bundle). Docs: https://github.com/InnerWarden/innerwarden/wiki · Site: https://www.innerwarden.com
What's Changed
- security(agent): fix spec-081 managed-agent coexistence for cross-uid / non-root IW by @maiconburn in #1094
- release: 0.15.24 by @maiconburn in #1095
Full Changelog: v0.15.23...v0.15.24
v0.15.23
InnerWarden 0.15.23
Fixed
innerwarden upgradenow 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 madeupgradebrittle in exactly that window — deploying 0.15.22, two consecutive runs on one box each failed on a different sidecar before a manualcurl --retrydeploy 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 --yesEvery binary below is signed (Ed25519 + Sigstore bundle). Docs: https://github.com/InnerWarden/innerwarden/wiki · Site: https://www.innerwarden.com
What's Changed
- ci: auto-announce releases to your own channels on publish by @maiconburn in #1091
- fix(ctl): retry transient asset/sidecar downloads in innerwarden upgrade by @maiconburn in #1092
- release: 0.15.23 by @maiconburn in #1093
Full Changelog: v0.15.22...v0.15.23