Enable tls-rustls-insecure to support --insecure flag - #8
Conversation
--tls upgrades the connection to rediss://, but there was no way to skip certificate verification, so the tool couldn't connect to servers with self-signed or private-CA certs (the normal case for test/staging/ephemeral benchmark deployments). The documented redis-rs escape hatch (rediss://host:port/#insecure) was silently a no-op here: the fragment parser that sets the internal `insecure` flag is gated only on the `tls-rustls` feature (already enabled transitively), but the code that actually relaxes verification is gated on the separate `tls-rustls-insecure` feature, which wasn't compiled in. Fixes: - Cargo.toml: add `tls-rustls-insecure` to the redis dependency's features. - New `--insecure` / `REDIS_TLS_INSECURE` CLI flag, wired into build_redis_url() in src/main.rs: when set, it requires the URL scheme to already be `rediss` (via --tls or a rediss:// --url) and appends the `#insecure` fragment, which now takes effect end-to-end. - README: document the new flag and the TLS cert-verification caveat. Fixes #7
|
🤖 Automated first-pass review — a human maintainer's review is still required before merge. There's already an earlier automated comment on this PR covering the direct- 1. The last commit's correction didn't land in all three places. 2. The new 3. On the parts I did check and think are solid: the For the record on process: this repo has no human-authored review comments anywhere in its history, so where the description says "per review" or "round-N review" there's no recorded reviewer position for me to check any of this against — everything above is reasoned from the diff and from AGENTS.md/CONTRIBUTING.md. AGENTS.md's "do not introduce new dependencies without checking with the maintainer" applies to the new direct |
…cs, env-bool tests
- Parse build_redis_url()'s generated URL through redis-rs's own
IntoConnectionInfo and assert ConnectionAddr::TcpTls { insecure: true, .. },
not just the URL string — catches an upstream fragment format change.
- Add a CI step asserting `tls-rustls-insecure` stays in the redis crate's
enabled feature set (via `cargo tree -e features -i redis`), so a future
dependency edit dropping it can't silently turn --insecure into a no-op.
- README: document that tls-rustls-insecure is a crate-wide feature, not
gated behind --insecure — a pre-existing --url with a #insecure fragment
now skips verification automatically where it previously didn't.
- Add unit tests proving REDIS_TLS_INSECURE=false (and the pre-existing
REDIS_TLS=false) resolve to false rather than "present therefore true",
and that REDIS_TLS_INSECURE=0 is a clean parse error, never silent true.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
redis-rs's rustls integration enables no crypto backend of its own (its `rustls` dependency is default-features = false, only rustls/std), so rustls 0.23 has no process-level CryptoProvider and every TLS connection attempt panics at connect time with "Could not automatically determine the process-level CryptoProvider from Rustls crate features" — on both the verifying and --insecure paths. This was found in the sibling celery-benchmark repo via a live TLS handshake test and reproduced identically here (same redis feature set, no install_default call anywhere in the codebase). Add a direct `rustls` dependency with the `ring` feature (matching `cargo tree -i rustls`, which shows ring pulled in via rustls-webpki — no aws-lc-rs in the tree) purely to make Cargo resolve that feature, and call `rustls::crypto::ring::default_provider().install_default()` once at the top of main(), before any Redis connection is attempted. Verified with a real TLS handshake against a self-signed-cert redis-server: without --insecure, connection now fails cleanly with a certificate error instead of panicking; with --insecure, a full benchmark run completes end-to-end (INFO fetch, job drain across all worker trials, idle-poll phase, JSON report written).
… gap, real TLS CI test Three follow-ups to the CryptoProvider fix (5d9cb63), matching the identical pattern applied to celery-benchmark/bullmq-benchmark/sidekiq-benchmark: - The startup warning was keyed off cli.insecure, not the actual built URL, so a --url/REDIS_URL that already carried a #insecure fragment silently disabled certificate verification with no warning printed. Add url_disables_cert_verification(), checked against the final built URL, and key the warning off that instead. - Move install_default() out of main() into a reusable, idempotent resque_bench::tls::install_crypto_provider() so both main() and any test can call it safely without a double-install panic. - Add a CI step that spins up a real TLS-only redis-server with a throwaway self-signed cert and drives the built binary against it: without --insecure it must fail cleanly (no panic) on the untrusted cert; with --insecure it must complete a full run and print the new warning. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…sertion, README/env-var fix, scheme-gated warning, CryptoProvider assertion - CI: replace pidfile+sleep TLS readiness check with a real redis-cli --tls ping retry loop, so the negative-case assertion can't pass on a server that isn't listening yet. Also assert the exact "invalid peer certificate" text on stderr for the without-`--insecure` case, not just non-zero exit + no panic. - README: `REDIS_TLS_INSECURE=1` is actually a parse error (clap's env-backed bool only accepts the literal strings "true"/"false", verified against the built binary and matching the existing --tls/REDIS_TLS behavior) — fix the docs to say `=true`, not `=1`. - url_disables_cert_verification: gate on scheme == "rediss" in addition to the #insecure fragment, so a plain redis:// URL with a stray #insecure fragment no longer triggers the misleading "TLS certificate verification is DISABLED" warning. Add a test for that case. - install_crypto_provider_is_idempotent: assert a CryptoProvider is actually installed after the idempotent calls, not just that they don't panic. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Without tls-rustls-insecure, a rediss://...#insecure URL doesn't silently
fall back to full certificate verification — redis-rs still parses the
fragment (URL fragment parsing isn't feature-gated) but then hard-fails
the connection attempt with ErrorKind::InvalidClientConfig ("Cannot create
insecure client without tls-rustls-insecure feature"), per
redis-1.5.0/src/connection.rs:1255-1279. Correct the Cargo.toml comment and
the README's "Behavior change / blast radius" section, which both
previously described this as a silent no-op with verification intact.
Root cause
--tlsupgrades the connection URL torediss://, but there was no way to disable certificate verification, so the tool could not connect to any server with a self-signed or private-CA cert — the normal case for test/staging/ephemeral benchmark deployments.The documented redis-rs escape hatch
rediss://host:port/#insecurealso silently did nothing here:redis = { features = ["tokio-comp", "tokio-rustls-comp", "tls-rustls-webpki-roots"] }tls-rustls-webpki-rootsandtokio-rustls-comptransitively enabletls-rustls#insecureURL fragment is parsed under thetls-rustlsgate (present) and sets an internalinsecure: truetls-rustls-insecurefeature, which was not enabledSo the flag was silently accepted by the URL parser and had zero effect on the actual TLS handshake.
Fix
tls-rustls-insecureto theredisdependency's feature list.--insecure/REDIS_TLS_INSECURECLI flag. Wired intobuild_redis_url()— the same function that already applies--host/--port/--db/--password/--tlsas URL mutations. When--insecureis set:rediss(via--tlsor arediss://--url), returning a clear error otherwise instead of a silent no-op;#insecurefragment viaurl::Url::set_fragment, which now actually disables verification end-to-end since the crate feature is compiled in.build_redis_url_insecure_appends_fragment_with_tls,build_redis_url_insecure_works_with_rediss_url_without_tls_flag,build_redis_url_insecure_without_tls_is_rejected.Review follow-ups (addressed in the second commit)
build_redis_url_insecure_appends_fragment_with_tlsnow also parses the generated URL through redis-rs's ownIntoConnectionInfo(the same pathredis::Client::openuses) and asserts the resultingConnectionAddrisTcpTls { insecure: true, .. }, not just a string-suffix check on#insecure. This catches an upstream fragment-name/format change. It does not catch thetls-rustls-insecureCargo feature itself being dropped — that's a separate failure mode, covered by (2)..github/workflows/ci.ymlrunningcargo tree -e features -i redis --workspace | grep -q tls-rustls-insecure, failing the build if the feature is no longer enabled on therediscrate — so a future dependency edit can't silently turn--insecureback into a no-op while all other tests stay green.tls-rustls-insecureis a crate-wide, build-time Cargo feature — it is not gated behind--insecureat runtime. Before this PR, a--url/REDIS_URLthat already carried a#insecurefragment (e.g.rediss://host:6379/0#insecure) was silently getting full certificate verification anyway (the feature wasn't compiled in, so the fragment was inert). After this PR, that same pre-existing fragment takes effect automatically and skips verification — even for callers who never pass--insecurethemselves. This is the intended fix, but is a real behavior change for anyone already using that fragment, so it's now called out explicitly in the README's TLS section.tls-rustls-insecureis a pure feature gate on top of the already-presenttls-rustls/ringdependency tree — diffedcargo treebefore/after the Cargo.toml change and the only difference is the workspace member's own path; no new crate is pulled in. Also did an actualcargo build --release --target x86_64-unknown-linux-musl, which succeeds and produces a working--version-reporting static-pie binary, confirming the reasoning holds in practice (not just by inspection).Cargo.lock): the derived bool+env flag only recognizes the literals"true"/"false"—"0"is neither and produces a clean parse error, never a silenttruefrom "variable is merely present". Addedinsecure_env_var_false_resolves_to_false_not_presence_flag,insecure_env_var_true_resolves_to_true,insecure_env_var_zero_is_a_clean_parse_error_not_silent_true, and the same check against the pre-existing--tls/REDIS_TLSpattern (tls_env_var_false_resolves_to_false_not_presence_flag) to lock this in as a regression test.Test plan (rounds 1-3)
cargo buildsucceedscargo check --all-featuressucceedscargo test— 36 unit tests insrc/main.rs(32 + 4 new) + 15 lib tests + 4 integration tests, all passcargo fmt --check— cleancargo clippy --all-targets -- -D warnings— cleancargo tree -e features -i redis --workspace | grep tls-rustls-insecure— present (the new CI guard)cargo build --release --target x86_64-unknown-linux-musl— succeeds; binary runs and reports--version--insecurereachesbuild_redis_url()and the#insecurefragment, which flows intoredis::Client::open(url)(the actual connection construction), not just argument parsingFollow-up: --tls was actually non-functional end-to-end (CryptoProvider panic)
While reviewing this fix, a sibling repo in the org (
celery-benchmark, samerediscrateversion and identical TLS feature set:
tokio-comp,tokio-rustls-comp,tls-rustls-webpki-roots,tls-rustls-insecure) was found via a live TLS handshake testto have a real runtime bug that goes beyond the
tls-rustls-insecurefeature-flag issue above:nothing in the codebase ever installed a process-level
rustlsCryptoProvider. redis-rs's ownrustlsdependency isdefault-features = false(onlyrustls/std), so it never enablesrustls's
ringoraws-lc-rscrypto backend feature. rustls 0.23 requires the application toinstall a provider once before any TLS connection, or every connection panics — on both the
verifying and
--insecurepaths alike. That means--tls(and therefore--insecure) was not a"missing feature flag" bug, it was completely non-functional.
This repo has the identical
redisfeature list and, on inspection(
grep -rn "CryptoProvider|install_default" src/), the identical gap — not yet caught herebecause the round-2 review above verified with unit tests, a musl release build, and env-var
parsing tests, but never a real TLS handshake against a live server.
Root cause confirmed and reproduced:
ring(notaws-lc-rs) is the resolved crypto backend, pulled in transitively viarustls-webpki. Building the pre-fix binary and running it with--tlsagainst a realTLS-terminated
redis-server(self-signed cert) reproduced the panic exactly as predicted:Fix (this PR, same shape as the
celery-benchmarkfix):Cargo.toml: added a directrustls = { version = "0.23", default-features = false, features = ["ring"] }dependency — not used directly in code, it exists purely so Cargo resolves rustls'sringfeature for the single resolvedrustlspackage in the tree.src/main.rs, top ofmain():let _ = rustls::crypto::ring::default_provider().install_default();— called once beforeCli::parse()/any Redis connection, result ignored since a second call in-process would returnErrharmlessly.Verified with a real handshake against a self-signed-cert
redis-serveron a--tls-port(cert generated with
openssl req -x509 -newkey rsa:2048 -nodes -keyout key.pem -out cert.pem -days 1 -subj /CN=localhost):Run 1 —
--tlswithout--insecure(expect a clean certificate error, not a panic):Run 2 —
--tls --insecure(expect success, real end-to-end work):Both runs confirm the fix: verified TLS now fails cleanly on an untrusted cert (no panic) instead
of crashing, and
--insecurecompletes a full real benchmark (INFO fetch, all four worker-counttrials, idle-poll phase, JSON report) over an actual TLS connection.
Re-verified full suite after the fix:
cargo build,cargo check --all-features,cargo clippy --all-targets -- -D warnings,cargo fmt --check, andcargo test(15 lib + 36 bin + 4integration tests) all pass.
Fixes #7
Round 4: reusable CryptoProvider helper, close the
#insecurefragment warning gap, and a real CI TLS handshake testSame root-cause pattern fixed in this round across sibling repos in the org
(
celery-benchmark,bullmq-benchmark,sidekiq-benchmark) — three follow-ups to theCryptoProvider fix above:
The startup warning had a gap: it keyed off
cli.insecure, not the actual URL.A user-supplied
--url rediss://host:6380/0#insecure(or aREDIS_URLenv varcarrying that fragment) disabled certificate verification with zero indication
in the output, since nothing checked for it — only
--insecurebeing explicitlypassed would have. Fixed by adding
url_disables_cert_verification(), which checksthe final built URL's fragment directly, and keying the warning off that instead
of
cli.insecure:Verified locally:
--url "rediss://127.0.0.1:16391/0#insecure"with no--insecureflag now prints the warning and connects insecurely, matching what
--insecureitself does. Added 5 tests:
url_disables_cert_verification_false_by_default,_true_via_insecure_flag,_true_via_url_supplied_fragment_without_flag(thespecific gap this closes),
_false_for_plain_tls_without_insecure, and_false_for_unparsable_url.install_default()was only reachable frommain(), not from tests. Pulled itout into a new
src/tls.rsmodule (resque_bench::tls::install_crypto_provider()),registered via
pub mod tls;insrc/lib.rs. It's idempotent (let _ = ...on the"already installed"
Err, never.expect()/.unwrap()), so bothmain()and anypresent/future integration test that opens a real
rediss://connection can call itsafely without risking a double-install panic. Added
tls::tests::install_crypto_provider_is_idempotent, which calls it three times in arow in-process.
New CI step: a real TLS handshake, not just unit tests.
.github/workflows/ci.ymlnow spins up a dedicated TLS-only
redis-serverwith a throwaway self-signed cert(
openssl req -x509 -newkey rsa:2048 -nodes -keyout key.pem -out cert.pem -days 1 -subj /CN=localhost,--tls-port,--tls-cluster no --tls-replication no --tls-auth-clients no) and drives the actual built release binary against it:--insecure: must exit non-zero with a certificate error, and the stepgreps stderr for "panic" and fails if found (guards against the exact
CryptoProvider regression this PR fixes recurring silently);
--insecure: must complete a full run and print the insecure-mode warningfrom (1) on stderr.
Dry-ran the exact commands used in the CI step locally against a real
redis-server(self-signed cert, TLS-only on--tls-port 16391,--port 0):--insecure: exit code 1, stderr =invalid peer certificate: Other(OtherError(CaUsedAsEndEntity)), no panic. PASS.--insecure: exit code 0, ran 200 jobs across 2 workers (22k+ jobs/s),errors: 0, printed the warning from (1),redis-cli --tls --insecure shutdown nosavestopped the server cleanly afterward. PASS.Re-verified full suite after these changes:
cargo build,cargo check --all-features,cargo clippy --all-targets -- -D warnings(clean),cargo fmt --check(clean), and
cargo test— now 16 lib tests (+1) + 41src/main.rstests (+5) + 4integration tests = 61 total, all pass.
Round 6: real musl re-verification after the
rustls/ringdependency landed, plus a doc-accuracy fixTwo independent-reviewer findings addressed in this round.
1. The musl verification in the "Test plan (rounds 1-3)" checklist above predates the
ringdependency and was never redoneConfirmed via
git log: thecargo build --release --target x86_64-unknown-linux-muslrecorded above (round 2/3, "musl release build" bullet) ran against the tree as it stood
before the CryptoProvider fix. That fix (commit
5d9cb63, "Follow-up: --tls wasactually non-functional end-to-end (CryptoProvider panic)" section above) is what added
the direct
rustls = { ..., features = ["ring"] }dependency toCargo.toml— beforethat commit,
ringwas only a transitive dependency pulled in viarustls-webpki, andnothing in this crate's own
Cargo.tomlforced Cargo to resolve rustls'sringfeatureexplicitly.
ringcompiles C and assembly via thecccrate, which needs a working musl Ctoolchain — a requirement that didn't exist yet at the time of the original musl
verification. So the musl cross-build had never actually been exercised against the
current, final dependency graph, unlike sibling repos in the org (
sidekiq-benchmark,celery-benchmark,bullmq-benchmark), which all got a dedicated final muslre-verification round after their own
ringdependency landed.Re-verified now, on this branch's current HEAD (after the CryptoProvider fix, the
round-4/5 follow-ups, and this round's doc fix):
Confirmed the resulting binary is genuinely static, not just "happens to run here":
ring's C/assembly compiled cleanly under the musl toolchain, zeroGLIBC_*symbolsanywhere in the binary,
lddreports "statically linked", and the binary actually executes— matching exactly what
.github/workflows/release.yml's own "Verify binary architecture"step checks (
fileoutput + a real--version/--helpsmoke run). No gap found:release.yml's existing "Install musl toolchain" step (sudo apt-get install -y musl-tools, with a comment already noting "rustlspulls inring, which compiles C andassembly, so the musl target needs a musl C toolchain") already correctly anticipates this
requirement — it just hadn't been re-exercised locally against the final dependency graph
until now. No changes needed to
release.yml.2. Doc-accuracy nit: the
#insecure-without-the-feature framing was backwardsThe "Behavior change / blast radius" framing above (and the matching
Cargo.tomlcomment)described a pre-existing
#insecure-fragment URL as, before this PR, being "silentlyignored" with the connection getting full certificate verification anyway. That's not what
actually happens: per
redis-1.5.0/src/connection.rs(~lines 1255-1279), URL fragmentparsing itself isn't feature-gated (a
#insecurefragment always sets an internalinsecure: true), but building the TLS config withinsecure: truewhiletls-rustls-insecureis not compiled in hits an explicitfail!(...)arm and returnsErrorKind::InvalidClientConfig("Cannot create insecure client withouttls-rustls-insecure feature") — a hard connection failure, not a silent success with
verification intact. There was never a working prior configuration that carried that
fragment; it simply refused to connect. Corrected the wording in
Cargo.toml's comment andboth affected passages in the README's TLS section (including "Behavior change / blast
radius") to describe the hard-fail accurately instead.
Re-verified full suite after this round:
cargo build,cargo check --all-features,cargo clippy --all-targets -- -D warnings(clean),cargo fmt --check(clean), andcargo test(16 lib + 42src/main.rs+ 4 integration tests = 62 total, all pass).