Security Reference
This page documents the security controls Animus applies at the install boundary and the trust model behind plugin signature verification. For the background design and threat model, see docs/architecture/plugin-signing.md.
Plugin signature policy
animus plugin install verifies that every downloaded plugin binary was signed by a trusted publisher before it is copied into ~/.animus/plugins/. Verification uses cosign keyless signatures against a <asset>.bundle published next to the asset on the GitHub Release. There is no PEM public key to manage: trust is anchored on the signer identity (the Fulcio-issued cert SAN) plus the OIDC issuer (https://token.actions.githubusercontent.com).
Keyless trust model (v0.4.12)
Every launchapp-dev/animus-* release pipeline signs through GitHub Actions OIDC + Sigstore Fulcio + the Rekor transparency log. There is no static signing key; each signature uses a short-lived cert tied to the workflow URI and tag. Animus verifies in three layers:
- Cryptographic validity — cosign checks the signature against the per-signing Fulcio cert, walks the cert chain to the Sigstore Fulcio CA root (built into the cosign binary), and confirms the entry is present in the Rekor transparency log.
- Identity — the cert SAN URI must match the trusted publisher's
identity_regex. Forlaunchapp-dev, that's^https://github\.com/launchapp-dev/[^/]+/\.github/workflows/release\.yml@refs/tags/v.*, which pins every release to the standardizedrelease.ymlworkflow under av*tag in anylaunchapp-devrepo. - OIDC issuer — the cert issuer must equal
https://token.actions.githubusercontent.com, so a non-GitHub Actions signing path is rejected even if the SAN somehow matched.
No baked-in public key is involved. The pre-v0.4.12 key-based path (~/.animus/trusted-keys/<owner>.pem + a baked LAUNCHAPP_DEV_COSIGN_PUBLIC_KEY_PEM constant) was removed because the real release pipeline never produced PEM-verifiable signatures.
Install-time default: warn
The install-time default policy is warn. Verification still runs and the resolved signature_status is recorded in ~/.animus/plugins.yaml, but a missing / invalid / untrusted-signer signature degrades to a stderr warning rather than failing the install.
This default is intentional: it keeps the audit trail intact while avoiding a hard install failure for plugins whose release pipeline does not yet publish a keyless cosign bundle. Source of truth lives in PolicyMode::default_for_install and the CLI fallback in crates/orchestrator-cli/src/services/operations/ops_plugin.rs::effective_policy_mode.
Under warn:
- Verification still runs and
signature_statusis still recorded in~/.animus/plugins.yaml, so the audit trail stays intact. - Unsigned / invalid / untrusted-signer results log a warning to stderr and the install proceeds.
Operators who want fail-closed enforcement opt in per-install:
animus plugin install --signature-policy strict <owner>/<repo>See Recommended posture for production for the deployment-time configuration that pins every install to strict without needing the flag on each invocation.
Policy modes
animus plugin install exposes three enforcement modes via the --signature-policy <MODE> flag. The default is warn.
| Mode | Behavior | When to use |
|---|---|---|
strict | Refuse install if the keyless signature is missing, invalid, or signed by an identity outside the trusted-publisher list. | Production environments — opt in per-install or via deployment config. |
warn | Verify when possible; log a warning to stderr and proceed on any failure. DEFAULT. | Default for interactive use and environments where some plugins lack signatures. |
disabled | Skip verification entirely. | Air-gapped or local-build flows where signing is not feasible. |
The legacy --require-signature and --skip-signature flags are retained for backward compatibility. They map to --signature-policy strict and --signature-policy disabled respectively.
--allow-unsigned is a convenience alias for --signature-policy warn.
Strict mode (opt-in)
animus plugin install --signature-policy strict launchapp-dev/animus-provider-claudeWhen strict mode rejects an install you'll see one of:
signature policy is strict but no cosign signature could be verified: <reason>— no.bundlewas published, or cosign isn't on$PATH.cosign signature verification FAILED; refusing install: <message>— the bundle exists but cosign rejected it cryptographically (bad signature, missing Rekor entry, expired cert, etc.).signature is valid but the signer is not in trusted-signers.yaml— the cert identity isn't on your allowlist (or noTrustedPublisheris configured for the owner).
Each error suggests the appropriate override flag (--allow-unsigned, --signature-policy disabled, or adding the signer to ~/.animus/trusted-signers.yaml).
Warn mode
animus plugin install --signature-policy warn launchapp-dev/animus-subject-experimental
# or equivalently
animus plugin install --allow-unsigned launchapp-dev/animus-subject-experimentalVerification still runs and the result is recorded in ~/.animus/plugins.yaml under signature_status, but the install proceeds even on failure. Use only when migrating publishers onto cosign keyless signing.
Disabled mode
animus plugin install --path ./my-local-build.bin --signature-policy disabledSkips verification entirely. Use this for locally built --path installs or direct --url --sha256 installs that have no upstream signature bundle.
Trusted publishers (built-in)
The keyless model maps GitHub owners to per-publisher identity regex + OIDC issuer combinations. Animus ships one built-in entry:
| Owner | Identity regex | OIDC issuer |
|---|---|---|
launchapp-dev | ^https://github\.com/launchapp-dev/[^/]+/\.github/workflows/release\.yml@refs/tags/v.* | https://token.actions.githubusercontent.com |
This is the trust anchor for every launchapp-dev/animus-* release. The regex is anchored at the start (no prefix-attack surface), escapes literal dots (\.), pins the standardized release.yml workflow, and requires a v* tag — so only release-channel builds are trusted.
Additional publishers can be added programmatically via SignaturePolicy::trusted_publishers (Rust API). A YAML-configurable publisher list is tracked for v0.5.
Operational caveat (v0.4.12): the CLI install path applies the keyless identity regex (<owner>/<repo> → ^https://github\.com/<owner>/<repo>/.+) on a per-install-source basis through verify_with_cosign, and the optional ~/.animus/trusted-signers.yaml glob allowlist is the only way to narrow the trust set further. When trusted-signers.yaml is missing or empty, any release whose cert chain validates and whose SAN matches the per-install identity regex is accepted — pre-populate the file to restrict trust to specific owners (e.g. launchapp-dev/animus-*). The plugin-host TrustedPublisher list is the trust surface used by the in-host verify_plugin_install entry point and by future MCP / daemon-side installs.
Manual cosign verification
To verify a published release artifact by hand:
REPO=animus-transport-graphql
VER=v0.2.3
cd /tmp && mkdir -p cosign-check && cd cosign-check
gh release download $VER --repo launchapp-dev/$REPO --pattern '*.tar.gz' --pattern '*.bundle'
for tar in *.tar.gz; do
cosign verify-blob \
--certificate-identity-regexp "https://github.com/launchapp-dev/$REPO/.github/workflows/release\.yml@refs/tags/v.*" \
--certificate-oidc-issuer "https://token.actions.githubusercontent.com" \
--bundle "$tar.bundle" \
"$tar"
doneCosign prints Verified OK for each artifact when the signature chain holds. This is exactly the command Animus runs internally for keyless verification — the only difference is that Animus pulls the identity regex from the matching TrustedPublisher instead of requiring you to hand-build it.
Cosign binary dependency
Verification shells out to the cosign binary. If cosign is not on $PATH:
- Under
strict: the install fails with an actionableinstall cosign from https://github.com/sigstore/cosignerror. - Under
warn: the install proceeds andsignature_statusis recorded asunsignedwith the same reason. - Under
disabled: cosign is never consulted.
A migration to the in-process sigstore Rust crate is planned for v0.5+. The CLI flag surface will not change.
Signature status field
Every successful install records one of these stable strings under signature_status in ~/.animus/plugins.yaml:
| Value | Meaning |
|---|---|
verified | cosign accepted the keyless bundle against a trusted publisher. |
unsigned | No bundle was published, no trusted publisher matched, or cosign wasn't installed. |
invalid | A bundle was published but cosign rejected it (bad sig, expired cert, missing Rekor). |
untrusted_signer | A bundle verified, but the cert identity isn't on the trusted-publisher list. |
skipped | Verification was bypassed (--signature-policy disabled / --skip-signature). |
animus plugin list surfaces this in the SIG column.
Trusted orgs (TOFU)
Independent of cosign verification, every animus plugin install from a release source also consults a separate allowlist at ~/.animus/trusted-orgs.yaml. Installing from an org not in this list prompts the operator at the TTY (non-suppressible) or fails non-interactively.
Built-in trusted orgs: launchapp-dev.
See docs/architecture/plugin-signing.md for full details.
Manifest name vs repo basename
Install refuses any plugin whose manifest.name does not equal the repo basename. This blocks typosquat attacks where an attacker publishes a near-name release that shadows a legitimate plugin. --force overrides.
Reserved provider tools
Provider plugins that claim one of the in-tree provider tools (claude, codex, gemini, opencode, oai-runner) are refused by default. --allow-shadow-builtin is required to deliberately shadow an in-tree backend.
Configuration files
| File | Purpose |
|---|---|
~/.animus/trusted-signers.yaml | Optional glob allowlist for cosign cert identities. Missing / empty = permissive (any keyless signature whose cert chain validates is accepted, regardless of owner). Populate this file to scope the trust set down. |
~/.animus/trusted-orgs.yaml | TOFU allowlist of GitHub orgs the operator has accepted (orthogonal to cosign trust). |
~/.animus/plugins.yaml | Installed-plugin registry. Records signature_status per entry. |
.animus/plugins.lock (project) or ~/.animus/plugins.lock (global) | Append-only integrity ledger pinning sha256(artifact) + sha256(signature_bundle) for every installed plugin. Project-local takes precedence when <project_root>/.animus/ exists. |
Lockfile fail-closed policy
animus plugin install and animus plugin install-defaults refuse when the resolved plugins.lock exists but cannot be parsed or carries an incompatible schema_version. The error surfaces (a) the exact lockfile path, (b) the underlying loader error chain, and (c) two remediation paths. This is intentional: an unreadable lockfile is an audit-boundary event and silently overwriting it would erase the recorded sha256 history that verify_installed uses to detect tamper on subsequent upgrades.
Remediation paths:
- Restore from version control or backup. Project-local lockfiles live at
<project_root>/.animus/plugins.lockand are intended to be checked in.git checkout HEAD -- .animus/plugins.lockis usually the right call. - Re-run with
--force-rewrite-lockfile. This discards the unreadable file and rebuilds a fresh lockfile from this install onward. The install pipeline emits awarn!at this point that notes integrity history was reset; treat the warning as evidence that subsequent--forceinstalls cannot detect pre-existing tamper until the new lockfile has accumulated enough entries.
The --force-rewrite-lockfile flag is CLI-only: MCP and control-plane install routes default to fail-closed with no override, on the principle that lockfile recovery is an operator decision that should be explicit and synchronous.
~/.animus/trusted-keys/ is no longer consulted as of v0.4.12 — the key-based PEM path it served is gone. Existing directories can be deleted; Animus does not write to it.
Recommended posture for production
- Pass
--signature-policy strictexplicitly on every install. The library default (PolicyMode::default_for_install) iswarnto keep interactive onboarding unblocked; production callers should override per-install (or wrapanimus plugin installin a script that always supplies the flag). - Install
cosignon every machine that runsanimus plugin install. Strict mode fails closed without it. - Pre-populate
~/.animus/trusted-orgs.yamlso non-interactive installs never block on a TOFU prompt. - Audit
signature_statusin~/.animus/plugins.yamlperiodically. Anything other thanverifiedorskippedfor a--path/--urlinstall is a policy violation worth investigating.unsignedrows under the defaultwarnpolicy are the expected signal that the release pipeline for that plugin has not yet adopted keyless cosign bundles; treat them as a reminder to either contact the maintainer or reinstall under--signature-policy strictafter confirming a bundle now ships.
Kill switches
One environment variable disables a plugin-system feature at daemon start for emergency rollback:
ANIMUS_DAEMON_DISABLE_TRIGGERS=1— skip the trigger plugin supervisor.
Requires a daemon restart to take effect and to re-enable. See docs/reference/configuration.md.
ANIMUS_PROVIDER_DISABLE_PLUGIN was removed in v0.4.12 when the in-tree provider backends were extracted to standalone plugins. There is no longer a runtime escape hatch for provider dispatch — uninstall or disable the offending plugin instead.