Repository navigation
feat(cli): erase data written before an IPCrypt rule - #3120
Conversation
|
@claude-fleet review |
|
Review task queued. Task: f32c88c0-bf36-4549-a342-f467182572bf |
There was a problem hiding this comment.
Reviewed the full diff, the code behind its claims, and ran the two revert checks.
Request changes: one should_fix finding.
Findings:
- should_fix, services/ravel-cli/src/load/cli.rs:493 — the ruled-bytes-column warning is wired into the logs arm only; a spans mapping has the same silent gap
- nit, services/ravel-cli/src/erase.rs:376 — a metrics
--plaintext-matcherwritten in the rule's dotted spelling passes the rule check but can never match a stored label
Verified:
- A plaintext matcher is appended to every planned request after the plan, so it never fans out and never changes a derived id — erase.rs:480 with
derived_request_idhashing onlyepochs(erase.rs:213-223). - Plaintext-only proceeds with no marker and writes none:
encrypted_keysis empty, which takes theNone if ... encrypted_keys.is_empty()arm — ipcrypt.rs:174, plus thestored_marker(...) == Noneassertion ina_plaintext_matcher_submits_the_raw_value_and_warns. - A plaintext matcher and a fan-out cannot co-occur, so "added to every request" is always exactly one request:
plans.len() > 1requires an encrypted candidate, which makesencrypted_keysnon-empty and hits the mixed refusal first — erase.rs:456-473 against plan_requests' candidate loop (erase.rs:319-347). - The mixed refusal and the unruled-key refusal both run before
plan_requests/ the marker check, so "Nothing was written" holds — erase.rs:443-473. - Metrics look the key up through
sanitize_rule_key, logs/spans by the raw key —rule_for, erase.rs:228-237. erase statustakes--tenant/--signal/--request-idexactly as the printed line spells them, and reportsUNKNOWNfor an id with no.dreq/.done, which is what docs/deletion-and-gc.md now claims for the base id — main.rs:1606-1613, erase.rs:781.- The conflict branch downcasts the error
submitactually returns (SubmitError::ConflictingRequest.into()) — erase.rs:528-537 against erase.rs:161. - ingest.md's "only spans load an attributes map":
attrs_map_columnis declared on both mappings (mapping.rs:118, 780) but read only in spans.rs:157; nothing in the logs loader reads it. - configuration.md's "tokens carry no ingest or query scope":
TokenCapabilitieshas exactlyddlandipcrypt— crates/ravel-tenant-resolve/src/lib.rs:54-58.
Checks run:
cargo test -p ravel-cli --lib erase::→ 27 passed, 0 failed.cargo test -p ravel-cli --bin ravel-cli erase→erase_submit_needs_a_matcher_or_a_plaintext_matcherpassed.- Disabled the mixed-matcher refusal condition (erase.rs:456) →
a_plaintext_matcher_with_an_encrypted_matcher_is_refused_and_writes_nothingFAILED, 21 others passed. Restored. - Disabled the
SubmitErrorbranch of the error-context arm (erase.rs:530) →a_conflicting_request_in_a_fan_out_gives_no_rerun_hintFAILED, 21 others passed. Restored;git status --porcelainclean apart from an untrackednode_modules.
Smaller things:
ruled_bytes_column_warningsispubbut has only the one in-crate caller;pub(crate)would matchrun_fold_warning_tonext to it.- The mixed-refusal message does not dedupe repeated
--plaintext-matcherkeys, while the stderr warning does (erase.rs:461 vs erase.rs:483). Cosmetic only. - Nothing rejects the same key appearing in both
--matcherand--plaintext-matcherwhen the ordinary one stays plain (empty or non-address value); the predicate then carries the key twice with different values and matches nothing.validate_requestchecks only non-empty keys (crates/ravel-commit/src/erasure.rs:194-204). This is pre-existing for repeated--matcherand not something this PR introduces.
Not checked:
docs/reference/ravel-cli-flags.mdas regenerated output (I read the two changed rows but did not rerun the generator).- The claim in configuration.md that a durable
sys/authtoken resolves on every process reading the bucket, and the per-process OIDC audience argument. - The histogram additions to
ipcrypt_scan_tests.rswere read but not run (the scan suite was outside the test filters I ran).
Not checked:
- docs/reference/ravel-cli-flags.md as generator output (rows read, generator not rerun)
- configuration.md's claims that a durable sys/auth token resolves on every bucket-reading process and that OIDC needs a per-role --oidc-audience
- the classic-histogram cases added to load/ipcrypt_scan_tests.rs were read but not executed
Task f32c88c0-bf36-4549-a342-f467182572bf on pmoust-mbp, model claude-opus-5, effort medium.
| } | ||
| MappingSection::Logs(logs) => logs, | ||
| }; | ||
| for warning in ruled_bytes_column_warnings(&mapping, &rules) { |
There was a problem hiding this comment.
should_fix: ruled_bytes_column_warnings is called only in the MappingSection::Logs arm, so a spans mapping with the same hazard warns nothing.
Failure: a spans mapping with [[spans.attribute]] key = "client.ip" column = "raw_ip" type = "bytes" loads under a policy that rules client.ip; the addresses are stored unencrypted and the operator is told nothing. With the identical logs mapping the load now warns.
Evidence: the call site is inside the logs-only tail of run_fold_warning_to (load/cli.rs:493); run_spans (load/cli.rs:755-780) emits no equivalent. SpansMapping carries the same Vec<AttrMap> with a ColType value_type (load/mapping.rs:768-771, 55-62), and the shared encrypt path only touches string values (encrypt_attr, load/columns.rs:245-250), so bytes columns bypass encryption for spans exactly as for logs. docs/guides/ingest.md's new paragraph is also worded as logs-only.
Fix: take the two Vec<AttrMap> rather than &Mapping and call it from the spans arm too.
| /// [`plan_requests`], then the `sys/ipcrypt/policy` marker check | ||
| /// Refuse a plaintext matcher whose key has no rule for `signal`: an | ||
| /// ordinary matcher already submits that value as written. | ||
| fn check_plaintext_matchers( |
There was a problem hiding this comment.
nit: For metrics, check_plaintext_matchers accepts a key spelled the way the rule is written, but the key is submitted unsanitized, so the request cannot match a stored label.
Failure: erase submit --signal metrics --plaintext-matcher client.ip=192.0.2.1 passes the rule check (rule_for sanitizes to client_ip for the lookup, erase.rs:228-237) and writes a predicate whose key is client.ip, while the loader stores the label as client_ip (load/mapping.rs:588 applies sanitize_label_name). The request is accepted and silently matches nothing.
Evidence: erase.rs:376-397 validates through the sanitized form; erase.rs:480 appends the pair as written; load/mapping.rs:588.
This matches the documented --matcher behaviour ("the matcher key is submitted as written"), so it is pre-existing rather than new, but the new flag's whole purpose is matching stored data and the check is in a position to catch it.
|
Round 1: fix round dispatched, along with the opus checkpoint findings: the status command must carry the store and tenancy flags before "erase", and there are new plaintext-only marker tests.
|
Data stored before a key's IPCrypt rule existed holds raw values, so an ordinary --matcher, which the policy encrypts, can never match it. Add a repeatable `erase submit --plaintext-matcher KEY=VALUE` that submits the value as written, with no encryption and no epoch fan-out. The command warns on stderr naming the keys whose values the erasure log will store in the clear, refuses a key with no rule for --signal (metrics look the key up by its sanitized label name), and refuses a command that also carries an encrypted matcher before anything is written, because matchers in one request are a conjunction and no record can match both. With no policy marker and a non-empty policy, a plaintext-only request proceeds and writes no marker, and the absent-marker refusal now points at the new flag. Erase status takes the derived request ids a fan-out writes, not the base id, which stores nothing. Submit now prints an `erase status` command for each derived id, and its fan-out line names the rule key when it differs from the matcher key. A ConflictingRequest failure part way through a fan-out no longer suggests a rerun that would conflict again. Load setup warns when a mapping column of type bytes carries a key with an IPCrypt rule, since only string values are encrypted. The test and benchmark load entry points that skip the rules and the marker check are documented as such. New tests cover a classic histogram with a dotted label, a FaultStore failure on the Nth put followed by a rerun, and an empty local policy against a policy marker. The ingest guide no longer claims logs load an attributes map, the deletion guide covers plaintext matchers and status ids, and the configuration guide describes the encryption oracle that write access gives. Fixes: #3108 Refs: #2993 Signed-off-by: Panagiotis Moustafellos <pmoust@nofire.ai>
7bab864 to
e42bc67
Compare
|
Round 1 and the opus checkpoint findings are addressed at e42bc67, folded into the single commit.
|
|
@claude-fleet review |
|
Review task queued. Task: cc82aa7d-2c0a-4923-9366-cc1bcbf0030a |
There was a problem hiding this comment.
Previous round at 7bab864: 2 findings.
| Previous finding | Classification | Cited line |
|---|---|---|
services/ravel-cli/src/load/cli.rs:493 (should_fix): ruled_bytes_column_warnings is called only in the MappingSection::Logs arm, so a spans mapping with the same hazard warns nothing. Failure: a spans mapping with [[spans.attribute]] key = "client.ip" column = "raw_ip" type = "bytes" loads under a policy that rules client.ip; the addresses are stored unencrypted and the operator is told nothing. With the identical logs mapping the load now warns. Evidence: the call site is inside the logs-only tail of run_fold_warning_to (load/cli.rs:493); run_spans (load/cli.rs:755-780) emits no equivalent. SpansMapping carries the same Vec<AttrMap> with a ColType value_type (load/mapping.rs:768-771, 55-62), and the shared encrypt path only touches string values (encrypt_attr, load/columns.rs:245-250), so bytes columns bypass encryption for spans exactly as for logs. docs/guides/ingest.md's new paragraph is also worded as logs-only. Fix: take the two Vec<AttrMap> rather than &Mapping and call it from the spans arm too. |
fixed | services/ravel-cli/src/load/cli.rs:473 |
services/ravel-cli/src/erase.rs:376 (nit): For metrics, check_plaintext_matchers accepts a key spelled the way the rule is written, but the key is submitted unsanitized, so the request cannot match a stored label. Failure: erase submit --signal metrics --plaintext-matcher client.ip=192.0.2.1 passes the rule check (rule_for sanitizes to client_ip for the lookup, erase.rs:228-237) and writes a predicate whose key is client.ip, while the loader stores the label as client_ip (load/mapping.rs:588 applies sanitize_label_name). The request is accepted and silently matches nothing. Evidence: erase.rs:376-397 validates through the sanitized form; erase.rs:480 appends the pair as written; load/mapping.rs:588. This matches the documented --matcher behaviour ("the matcher key is submitted as written"), so it is pre-existing rather than new, but the new flag's whole purpose is matching stored data and the check is in a position to catch it. |
open | services/ravel-cli/src/erase.rs:376 |
Re-reviewed at e42bc67. Finding 1 is fixed; finding 2 still stands as a nit.
Approve: no blocking or should_fix finding.
Findings:
- nit (carried over), services/ravel-cli/src/erase.rs:376 — a metrics
--plaintext-matcherkey is validated sanitized and submitted unsanitized
Verified:
- Finding 1 fixed:
ruled_bytes_column_warningsnow takes the attribute iterator and is called from the spans arm too, with its own stored-form wording — load/cli.rs:49-65, 473-479, 504-512; the spans case is pinned bya_spans_bytes_column_under_a_ruled_key_is_stored_unencrypted_and_warns(load/ipcrypt_scan_tests.rs:626-707) - "spans store their lowercase hex, which never parses as an address":
span_attr_stringhex-encodes bytes beforeencrypt_stris applied, on both the attribute and the resource-attribute path — load/spans.rs:549-553, 574-587, 475-494 - "Only spans load an attributes map; the logs
attrs_map_columnis export only": the logs field is write-side only and read nowhere under load/ — load/mapping.rs:112-118; span map entries do go throughencrypt_str— load/spans.rs:804 - The fan-out status line carries the submit's top-level flags, and they are top-level flags (not accepted after
erase) — main.rs:56-74, 101-117; store.rs:388-446; round-tripped byerase_submit_prints_status_commands_with_the_submit_flags(main.rs:5553-5667), which re-parses the printed line into anerase statuswith the same store and tenancy args - Credentials are never echoed; a flag-given credential with its env var unset prints a placeholder — store.rs:433-444, test main.rs:5673-5718
erase statusneeds no--ipcrypt-config, so omitting it from the echoed flags is right — main.rs:1623-1633- "given the base id it reports UNKNOWN" — erase.rs:788
- Conflict errors drop the rerun hint, and
SubmitErrorhas only the conflict variant, so no retriable error loses the hint — erase.rs:62-78, 528-545 - Plaintext-only request proceeds with no marker and writes none:
encrypted_keysis empty, socheck_marker_for_erasurehas nothing to refuse — erase.rs:460-487 - The mixed-matcher refusal happens before the marker check and before any write — erase.rs:465-480
--matcherisrequired_unless_present = "plaintext_matchers", covered byerase_submit_needs_a_matcher_or_a_plaintext_matcher(main.rs:5468-5500)
Smaller things:
- The PR description's "Known limit: the printed status commands carry tenant, signal and id but not the store flags the submit used. The docs say so." is stale: the code now prints them (erase.rs:570-583) and docs/deletion-and-gc.md:772-787 documents them. Drop that paragraph.
- The
<your S3 credential flags>placeholder goes throughshell_word(erase.rs:575-578), so it prints single-quoted as'<your S3 credential flags>'. Harmless, but the operator is meant to replace it, and the quotes read as part of the value. - A key given both as
--plaintext-matcherand as a--matcherwhose value the policy leaves plaintext (not an address) produces two matchers on the same key with different values in one conjunction, accepted and matching nothing (erase.rs:482-484). Pre-existing for duplicate--matcherkeys, narrow, and fails closed.
Not checked:
- No build or test run: this is an aarch64 host and nothing in the review needed a run to settle. CI covers the
ravel-clisuite. - The regenerated
docs/reference/ravel-cli-flags.mdwas read but not regenerated to compare.
Not checked:
- No cargo build or test run (aarch64 host; no finding required one) - CI covers the ravel-cli suite
- docs/reference/ravel-cli-flags.md was read but not regenerated for comparison
Task cc82aa7d-2c0a-4923-9366-cc1bcbf0030a on rp2, model claude-opus-5, effort medium.
| /// [`plan_requests`], then the `sys/ipcrypt/policy` marker check | ||
| /// Refuse a plaintext matcher whose key has no rule for `signal`: an | ||
| /// ordinary matcher already submits that value as written. | ||
| fn check_plaintext_matchers( |
There was a problem hiding this comment.
nit: Still open since 7bab864. For metrics, check_plaintext_matchers accepts a key spelled the way the rule is written, but the key is submitted unsanitized, so the request cannot match a stored label.
Failure: erase submit --signal metrics --plaintext-matcher client.ip=192.0.2.1 passes the rule check (rule_for sanitizes to client_ip for the lookup, erase.rs:228-237) and writes a predicate whose key is client.ip, while the loader stores the label as client_ip (load/mapping.rs:588 applies sanitize_label_name). The request is accepted and silently matches nothing.
Evidence: erase.rs:376-397 validates through the sanitized form; erase.rs:480 appends the pair as written; load/mapping.rs:588.
This matches the documented --matcher behaviour ("the matcher key is submitted as written"), so it is pre-existing rather than new, but the new flag's whole purpose is matching stored data and the check is in a position to catch it.
What
The erasure amendment to ADR-2993 (#3101) and the review follow-ups from #3049.
erase submit --plaintext-matcher KEY=VALUE(repeatable).--signalis refused. For metrics the key is looked up by its sanitized name.--plaintext-matcher.erase statuscommand for each derived id and says the base id stores nothing.docs/deletion-and-gc.mdsays status takes the derived ids. Resolving a base id inside status would need the policy and matchers, or a new stored object.type = "bytes"logs column now prints a warning at setup.load::load*entry points are documented as running with no IPCrypt rules and no marker check, for tests and benchmarks.docs/guides/ingest.mdno longer says logs load an attributes map.sys/authtokens and OIDC cannot.ravel-cliflag reference and a changelog fragment.Tests
client.iplabel through the sanitizer, and the bytes-column warning.Executor gates: clippy and
cargo test -p ravel-cli(441 in the bin suite, 27 erase tests), plus the docs check.Known limit: the printed status commands carry tenant, signal and id but not the store flags the submit used. The docs say so.
Fixes: #3108
Refs: #2993