Skip to content

fix(oi): four self-contained audit findings - #179

Merged
passcod merged 4 commits into
mainfrom
fix/audit-trivial-oi
Sep 10, 2026
Merged

passcod merged 4 commits into
mainfrom
fix/audit-trivial-oi

Conversation

@passcod

@passcod passcod commented Sep 10, 2026

Copy link
Copy Markdown
Member

🤖 Four trivial findings from the logic-bug audit in the OI handlers, one commit each.

  • /server/status reported a constant active_operations: 0. i[status.get] defines it as the number of lifecycle operations in progress, and state.scheduler was right there and used by other handlers in the same crate. Anything watching the summary saw an idle runtime however busy it was.

  • Key fingerprints that could never authenticate were stored as authorised. The verifier compares byte-for-byte against a lowercase-hex SHA-256, but /keys/authorise stored whatever it was given — uppercase, whitespace-padded, sha256:-prefixed or the wrong length — and then listed it, telling an operator they had granted access they had not. keys::parse_fingerprint normalises what can be and refuses what cannot. /keys/revoke uses it too, so a key authorised from a prefixed paste can be revoked with the string the operator typed.

  • Port 0 was accepted for site-service endpoints and ingress attachments. They arrive as plain u16, so 0 deserialised happily and reached the site-proxy config as listen :0 / dial :0 semantics rather than being refused where the operator could be told which field was wrong. The BSL side has rejected it all along via Port::new.

  • An unknown DNS provider surfaced as a raw constraint failure. set-acme-dns left it to the foreign key, and db_error maps every DB error to not_found, so a typo returned not_found: db error: FOREIGN KEY constraint failed — naming neither the provider nor the remedy, and giving a client the wrong error code. The check runs inside the same DB closure, so nothing can remove the provider in between. The same handler also accepted any hostname, including an empty string, storing a policy that could never match and looked identical in the list to one that works; r[tls.policy.wildcard] defines the shapes and those are what it accepts now.

Two notes on shape. The DNS-name check moved to handler so both the TLS policy and the site-service host validation use one implementation — the localhost rejection stays with site services, since an exact TLS policy may deliberately name it. The port check moved there for the same reason.

The existing key_mgmt tests used "aabbcc" as a stand-in fingerprint, which is one of the values this now refuses; they use a full 64-character digest instead.

One finding from this subsystem is not here: validating /forwards/start's port against the service's declared ports. A Service does not carry its own ports — they come from pods mounting ServicePort — so it needs a traversal that does not exist yet, and getting it wrong rejects forwards that should work. It is on card V4.

`i[status.get]` defines `active_operations` as the number of lifecycle
operations currently in progress; the handler emitted the literal `0`
regardless of scheduler state, while `state.scheduler` was right there
and used by other handlers in the same crate. Anything watching the
summary saw an idle runtime however busy it was.

One operation is active at a time, so this is 0 or 1 — queued operations
are waiting rather than in progress and are not counted.
Fingerprints are lowercase-hex SHA-256 of the client SPKI and the
verifier compares byte-for-byte, but `/keys/authorise` stored whatever it
was given. An uppercase, whitespace-padded, `sha256:`-prefixed or
wrong-length value was accepted and then listed as authorised while no
client could ever match it — telling an operator they had granted access
they had not.

`keys::parse_fingerprint` normalises what can be and rejects what cannot,
with `requirements_invalid` rather than a stored non-key. `/keys/revoke`
uses it too, so a key authorised from a prefixed or uppercase paste can
be revoked with the string the operator actually typed.

The existing key_mgmt tests used `"aabbcc"` as a stand-in fingerprint,
which is one of the values this now refuses; they use a full 64-character
digest instead.
`service_port`, `remote_port` and an attachment's `port` are plain `u16`,
so 0 deserialised happily. It is not a routable listener or backend port,
and it reached the site-proxy config as `listen :0` / dial `:0` semantics
rather than being refused at the interface where the operator could still
be told which field was wrong. The BSL side has rejected it all along via
`Port::new`.

The check lives in `handler` rather than in either module, since both
needed it.
…attern

`set-acme-dns` left an unknown `dns_provider` to the foreign key, and
`db_error` maps every DB error to `not_found`, so an operator typo came
back as `not_found: db error: FOREIGN KEY constraint failed` — naming
neither the provider nor the remedy, and handing a client the wrong error
code to key off. The delete paths in the same file already special-case FK
refusals into clear messages. The existence check runs inside the same DB
closure, so nothing can remove the provider in between.

The handler also accepted any `hostname`, including an empty string,
storing a policy that could never match and that looked identical in the
policy list to one that works. `r[tls.policy.wildcard]` defines the
shapes, and those are what it accepts now.

The DNS-name shape check moves to `handler` so both this and the
site-service host validation use it; the `localhost` rejection stays with
site services, because an exact TLS policy may deliberately name it.
@github-code-quality

github-code-quality Bot commented Sep 10, 2026 •

Copy link
Copy Markdown
Contributor

Code Coverage Overview

Languages: TypeScript, Rust

TypeScript / code-coverage/vitest

The overall line coverage in commit 29fce78 in the fix/audit-trivial-oi branch remains at 66%, unchanged from commit 6cf67ad in the main branch.

Rust / code-coverage/rust

The overall line coverage in commit 29fce78 in the fix/audit-trivial-oi branch is 61%. The line coverage in commit 6cf67ad in the main branch is 60%.

Show a line coverage summary of the most impacted files.
File main 6cf67ad fix/audit-trivial-oi 29fce78 +/-
crates/core/src/oi/server.rs 60% 59% -1%
crates/core/src...ler/services.rs 92% 92% 0%
crates/core/src...er/ingresses.rs 78% 78% 0%
crates/core/src/oi/handler.rs 97% 97% 0%
crates/core/src...ler/key_mgmt.rs 100% 100% 0%
crates/core/src...test_support.rs 100% 100% 0%
crates/core/src.../handler/tls.rs 62% 63% +1%
crates/core/src...me/tls/store.rs 92% 94% +2%
crates/protocol/src/keys.rs 79% 85% +6%
crates/core/src...tls/issuance.rs 21% 36% +15%

Updated September 10, 2026 05:52 UTC

@passcod
passcod added this pull request to the merge queue Sep 10, 2026
Merged via the queue into main with commit a23fa36 Sep 10, 2026
17 checks passed
@passcod
passcod deleted the fix/audit-trivial-oi branch September 10, 2026 11:15
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant