source
Known Limitations
This page distinguishes honest product boundaries from implementation bugs.
This page distinguishes honest product boundaries from implementation bugs.
Research and foundation surfaces not yet broad runtime claims
- semantic judging is advisory unless a specific runtime policy path consumes its verdict
- behavioral templates are the intended deterministic direction, but broad marketing claims still require template coverage and L5 evidence
- streaming reconciliation and active revocation primitives exist, but broader framework-live proof remains a follow-on for some surfaces
- manual reconciliation is implemented at an API-first foundation layer; richer operator UX is follow-on work
- TEE support is not yet a general hardware-rooted production claim until real vendor quote verification and proof-of-possession artifacts exist
- nested attestation primitives exist, but framework-level end-to-end evidence still needs expansion
- fast HMAC is intentionally not a cross-organization non-repudiation tier
Evidence limits
If a delegated tool or gateway can hide all relevant side effects and emits no
evidence, Ardur must classify the result as unknown rather than safe.
Product limits
Ardur is not:
- a sandbox by itself
- a universal semantic-safety engine
- a replacement for identity, workload isolation, or network controls
Those controls still matter around Ardur.
Mission Declaration schema enforcement (2026-04-28 hardening)
After the round-3 hostile re-audit, the MD loader unconditionally
enforces FIVE of the seven audit-flagged v0.1 spec members
(receipt_policy, conformance_profile, tool_manifest_digest,
revocation_ref, governed_memory_stores). The two omitted members
are intentional, not oversights:
approval_policy— absence is treated by the proxy as “no approval gate”, which is a visible operator choice. Including it in the always-required list would force every tool call in deployments that don’t use approvals to carry anoperator_id.probing_rate_limit— round-2 audit flagged validate-but-don’t- enforce theater. The runtime currently has no rate-limiter consuming the value, so requiring it without downstream effect is honesty debt. It returns to the always-required list once a per-mission rate-limiter actually consumes it.
Both members ARE required under strict_schema=True. Full v0.1 schema
validation (including additionalProperties: false at the root) is
opt-in via strict_schema=True on load_mission_declaration /
fetch_mission_declaration. Existing producers that mix legacy fields
(allowed_tools etc.) with v0.1 MDs will fail strict validation; clean
v0.1 producers should set the flag.
The canonical schema doc at docs/specs/mission-declaration-v0.1.schema.json
is mirrored to python/vibap/_specs/mission_declaration_v01.schema.json
so the runtime can validate without depending on the docs tree on disk.
The two files are kept byte-identical by the
spec-schema-sync job in .github/workflows/validate-formats.yml,
which fails the build on drift (FIX-R3-B from round-3 re-audit).
Bounded JWT iat skew at every verifier (2026-04-28 round-3 + round-4 + round-5)
Round-2 hostile re-audit flagged that FIX-6’s bounded-iat-skew gate
only protected receipts; AAT, Mission Declaration, status-list, and
passport JWT loaders all still accepted iat=year_3000. Round-3 lifted
the gate into a shared vibap.passport.assert_iat_in_window helper
applied at every JWT decode call site. Round-4 and round-5 extended the
gate to the parallel-format and Go verifiers the round-3 prompt missed.
The full set of bounded-iat surfaces is now:
Python JWT verifiers (assert_iat_in_window helper):
vibap.passport._decode_passportvibap.aat_adapter.decode_aat_claimsvibap.mission.load_mission_declarationvibap.mission.mission_is_revoked(status list)vibap.receipt.verify_receiptvibap.attestation.verify_attestation(round-4 FIX-R4-3)vibap.spiffe_identity.verify_jwt_svid(round-4 FIX-R4-4)vibap.memory.GovernedMemoryStore.read(round-5 FIX-R5-M3)vibap.tool_response_provenance.verify_tool_response_envelope(round-5 FIX-R5-M4; uses tighter ±60s future window for short-lived tokens)
Python parallel-format / non-JWT verifiers:
vibap.biscuit_passport.verify_biscuit_passport(round-4 FIX-R4-1; round-5 FIX-R5-H5 walks every block, not just leaf)vibap.training_attestation.verify_bundle(round-5 FIX-R5-H6; future-skew now unconditional, was gated onmax_age_s)
Go verifiers (mirrored fail-closed pattern):
go/pkg/credential/verify.go::Verify(SD-JWT-VC; round-3 FIX-R4-2)go/pkg/credential/delegation.go::VerifyPassport(round-5 FIX-R5-H3)go/pkg/credential/status.go::ParseStatusListToken(round-5 FIX-R5-H4)
Default Python window is ±300s future / 30 days past. Each call site
disables PyJWT’s stock verify_iat (which uses zero leeway and clashes
with cross-node clock drift) in favor of the explicit window. Archival
re-verification can pass future_skew_s=None/past_skew_s=None per
call. Go uses a tighter 30s default consistent with the SD-JWT-VC
profile’s clock-drift tolerance.
Operator + webhook /metrics endpoints (deployment hardening required)
The cmd/operator and cmd/webhook binaries expose Prometheus metrics
on :8080/metrics via controller-runtime’s default metricsserver,
without an AuthorizeFunc or FilterProvider. This is a deliberate
controller-runtime convention: production operators are expected to
gate metrics at the deploy layer with one of:
- a
kube-rbac-proxysidecar that requires a KubernetesServiceAccountbearer token with themetrics.k8s.ioAPI group, - a
NetworkPolicythat limits the metrics port to the cluster’s Prometheus operator pod, or - a service-mesh
AuthorizationPolicy(Istio/Linkerd).
The reference deploy/k8s/spire/ manifests do NOT ship a metrics-auth
sidecar today. Production deployments MUST configure one. This is
documented here as a known limitation rather than a code-level fix
because the right answer is deployment-environment-specific.
Bearer-token authentication on Go control-plane services (2026-04-29 round-5)
Round-4 audit flagged that the Go Authority and Governor HTTP services were unauthenticated — anyone with network reach could mint credentials or ingest fabricated governance events. Round-5 closes both:
go/cmd/authority:/signand/statusrequireAuthorization: Bearer <token>matchingARDUR_AUTHORITY_TOKEN(≥32 bytes). The binary refuses to start unless the token is set or--no-require-authis passed for explicit local-dev opt-out. Public endpoints (/attestation,/public-key,/healthz) remain unauthenticated since they advertise the trust anchor.go/pkg/governance.NewHandlerWithAuthwires every/v1/*route through a constant-time bearer-check.cmd/governor/main.goreadsARDUR_GOVERNOR_TOKENfrom env;Validate()refuses to start without it (or without explicitARDUR_GOVERNOR_NO_REQUIRE_AUTH=1opt-out)./healthzand/readyzstay public for K8s probes.
Both services use crypto/subtle.ConstantTimeCompare to defeat timing
side-channel inference of the token. Round-7+ also SHA-256-normalizes
both presented and expected tokens before the constant-time compare
(sha256.Sum256(token) on each side, comparison over the 32-byte
digests) — this defeats the length oracle that
subtle.ConstantTimeCompare short-circuits on length-mismatched
inputs. The Python proxy’s hmac.compare_digest path does the same
SHA-256 normalization. Production deployments SHOULD also front the
services with mTLS at the ingress / service-mesh layer for
defense-in-depth.
Operator-supplied bearer tokens are strings.TrimSpace-ed (Go) /
.strip()-ed (Python) at every entry point — env vars
(ARDUR_AUTHORITY_TOKEN, ARDUR_GOVERNOR_TOKEN, VIBAP_API_TOKEN)
and CLI args (--api-token) — so YAML-quoted secrets with leading
or trailing whitespace authenticate correctly without operator
debugging time. The bearer-scheme parse is RFC 9110-compliant
case-insensitive (Bearer, bearer, BEARER all accepted).
_pinned_urlopen semantics (2026-04-28 round-3)
The pinned-IP fetch path used by the SSRF-resistant Mission Declaration
and status-list fetchers (vibap.mission._pinned_urlopen) explicitly
rejects HTTP redirects (any 3xx → URLError) instead of following.
Following would re-resolve DNS at each hop and bypass the pinned-IP
guard. Producers that need redirects must reconfigure their server to
serve the final URL directly. Non-2xx response status codes (4xx/5xx)
also raise HTTPError, matching the contract urllib.request.urlopen
provides — without this, a 500 body would be passed to the JWT decoder
downstream and fail with a noisy parse error rather than a clean fetch
error.
AAT proof-of-possession default (2026-04-28 hardening)
material_from_aat_grant and GovernanceProxy.start_session_from_aat
default to require_pop=True. A cnf-bearing AAT presented without
holder_public_key + kb_jwt now fails closed. Bearer-mode AATs
(no cnf claim) continue to be accepted; library callers that
legitimately need bearer-style acceptance of a cnf-bearing AAT MUST
opt out explicitly with require_pop=False so the security choice is
visible at the call site.
The HTTP /sessions endpoint plumbs require_pop,
holder_public_key_pem, and kb_jwt through the request body, with
the same fail-closed default.
Revocation fail-closed (2026-04-28 hardening, FIX-1)
go/pkg/credential.Verify no longer fail-opens when a credential
carries a status claim and no StatusClient is configured. Callers
must either provide a StatusClient or set opts.SkipStatusCheck=true
explicitly. The Python loader has always fetched the status list when
present; this fix brings the Go path to the same posture.