Repository navigation
security: v0.3.6 fixes for secrets, emit subjects, governed mode and http-in - #11
Merged
Merged
Conversation
The daily audit job has failed on master since 2026-10-21: RUSTSEC-2026-0381 and -0382 (certificate paths through distrusted emSign intermediates) landed with no commit here. core and connectors/amqp move to the patched versions, so the HTTP client and MQTT over TLS reject those paths again. The sync nats client pins rustls-webpki 0.102, which has no patched release: -0382 joins the documented ignores with its reason, until the move to async-nats. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_018wXgxT2UoFSZfqhAE1Tb5V
secret() resolved the real value in every path that shows a run instead of performing it: preview, vejas_run_flow, flow-as-tool, shadow-replay, time-travel, canary, golden capture. GET /preview, open with no token, returned any variable holding a secret. In governed mode a vejas_time_travel candidate handed the value to an agent holding only VEJAS_TOKEN, against the S4 invariant in docs/security-audit-scope.md. A candidate is arbitrary code and can transform the value, so this cannot be a filter on the output: the value must not enter the run. Engine::shadow resolves through RedactedStore, which stays fail-closed and yields <secret:path/key>. Engine::live is the only way to a value, and Engine::new is gone so that a new proof path has to choose. vjs-test runs in shadow too, so that a curated golden round-trips. The cost is written down: a branch comparing with a secret takes the not-equal path in shadow. e2e/governed-agent.sh covers each path, a candidate that transforms the value, fail-closed behaviour, and that a live run still gets the value. It fails on v0.3.6. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_018wXgxT2UoFSZfqhAE1Tb5V
… both tokens Replay and purge of dead letters stayed on VEJAS_TOKEN as operational actions (docs/security-audit-scope.md said "by design"). That left them in the agent's hands while ADR-0024 promises that every mutating path is gated: a replay re-runs side effects, a purge destroys what the agent's work is judged on, and a dead letter is untrusted content the agent reads. No proposal kind carries them, so in governed mode they now sit on VEJAS_APPROVAL_TOKEN: refused over MCP, 409 over HTTP without X-Approval-Token. This reverses that written decision, in ADR-0024 Amendment A, and moves the last step of the self-healing loop to the panel. The human door did not work to begin with: the panel holds the approval token and never VEJAS_TOKEN, so with both set, the documented production posture, Approve got a 401 from the write gate. A valid approval token now answers the write gate on the routes that belong to the human. Replays and purges are recorded in VEJAS_AUDIT. The guide said purge was audited; it was not. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_018wXgxT2UoFSZfqhAE1Tb5V
http-in spawned one thread per connection with no bound, and thread::spawn panics when the OS refuses a thread. On the v0.3.6 image with --pids-limit=64, 300 idle connections panicked the accept loop: the connector stopped for good while /healthz said 200 and the topology said running, restarts 0, last_error null. This port has no authentication and the compose file publishes it. http-in now holds at most MAX_CONNECTIONS (default 512) and answers 503 past it, gives a request 30 s to arrive, and drops one connection when no thread is available. The panel port has the same bound (VEJAS_HTTP_MAX_INFLIGHT, default 256). Refusals count in vejas_http_rejected_total. Each unit's supervisor is the thread that runs it, so any panic ended the supervision as well. Attempts now run inside contained(): a panic takes the path of any other failure, restarting with the message in last_error. Supervisor threads carry the unit's name, and start_proc reports a thread it cannot start. The detect reader stops on every way out of an attempt. This is not rate limiting: a client holding every slot still blocks ingestion until it stops, so a hostile network needs the ingress in front. tiny_http's own pool is unbounded and untouched. e2e/http-in-bound.sh holds the bound, and repeats the pids-limit scenario in a container when docker is available. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_018wXgxT2UoFSZfqhAE1Tb5V
A subject built from an event field reached the NATS wire as it was, so a line break in the field became protocol commands, with the permissions of the runtime's NATS user. Through http-in that needed no credential: a flow building a subject from an event field let an anonymous POST publish anywhere or purge a stream. No shipped flow or template does this, but a flow written by an agent can. emit now refuses a subject with whitespace or a control character. It is checked in the interpreter, so the run fails like any other: the message reaches the DLQ after its deliveries, and a shadow run shows the error a live run would get. http-in answers 400 on such a suffix. Its path had no injection of its own, since it cannot carry the CRLF a payload must end with, but it could drop the connection. e2e/emit-subject.sh checks a hostile field, the path to the DLQ, the shadow run, the suffix, and that a clean field still routes. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_018wXgxT2UoFSZfqhAE1Tb5V
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes from a review of v0.3.6. Each problem was reproduced on that binary before the change, and each commit stands alone. This is the content of v0.4.0, a minor release because of the governed-mode change to the HTTP API; the version bump and the release are not in this PR.
emitsubject (e7b0316). A subject built from an event field reached the NATS wire as it was, so throughhttp-inan anonymous POST could publish anywhere or purge a stream. It needs a flow that builds a subject from an event field; no shipped flow or template does.emitnow refuses whitespace and control characters: the run fails and the message reaches the DLQ.http-inanswers 400 on such a suffix.fb3ab18).GET /preview, open with no token, returned variables holding a secret, and in governed mode avejas_time_travelcandidate handed the value to an agent holding onlyVEJAS_TOKEN. Such runs now useEngine::shadow, wheresecret()stays fail-closed and yields<secret:path/key>.Engine::newis gone, so a new proof path has to choose.59e0d9f). Replay and purge of dead letters are human acts onVEJAS_APPROVAL_TOKEN, refused over MCP, and they are now audited. The panel could not approve at all whenVEJAS_TOKENwas set, the documented production posture: a 401 from the write gate. A valid approval token now answers it on the human routes.http-inand supervision (542c876). With a thread limit, a burst of connections panicked the accept loop: the connector stopped for good while/healthzsaid 200 and the topology saidrunning,restarts: 0.http-innow holdsMAX_CONNECTIONS(default 512) and answers 503 past it, the panel port hasVEJAS_HTTP_MAX_INFLIGHT, and a panic in a unit takes the path of any other failure.9b12db7). rustls-webpki 0.103.16 and webpki-roots 1.0.11 for RUSTSEC-2026-0381 and -0382. The daily audit job has been red on master since 2026-10-21.Behaviour changes
vejas_dlq_replayis refused.docs/security-audit-scope.mdcalled replay and purge operational "by design"; ADR-0024 Amendment A reverses that, because an agent reading dead letters reads untrusted content.Verification
e2e/emit-subject.sh,e2e/governed-agent.sh,e2e/http-in-bound.sh(with a real pids limit when docker is there). The first two fail on v0.3.6;http-in-boundwas reproduced there separately, on the released image.Known limits:
http-in's bound is not rate limiting, a client holding every slot still blocks ingestion until it stops, andtiny_http's own pool is unbounded. A child of anexec-*connector inherits the runtime's environment, approval token included; that is unchanged here.🤖 Generated with Claude Code
https://claude.ai/code/session_018wXgxT2UoFSZfqhAE1Tb5V