Skip to content

Commit deffa93

Browse files
fix(rate-limiter): production hardening — TLS support for managed Redis (#74)
* fix(rate-limiter): enable TLS in redis client for rediss:// URLs The redis crate was compiled without any TLS feature, so passing a rediss:// URL (e.g. AWS ElastiCache with in-transit encryption, Redis Cloud) failed at backend init with `InvalidClientConfig: can't connect with TLS, the feature is not enabled`. The plugin manager then logged "Failed to load plugin RateLimiterPlugin" and silently skipped the plugin — bindings appeared configured but no rate limiting was applied. Compile the redis dep with rustls-based TLS (pure Rust, no OpenSSL): - tls-rustls - tls-rustls-webpki-roots (Mozilla CA roots, needed for managed Redis services without custom trust setup) - tokio-rustls-comp (async support over rustls) Added a regression test (TestRedisTlsSupport) that constructs a plugin with a rediss:// URL and asserts no InvalidClientConfig is raised. Bumps cpex_rate_limiter 0.0.4 → 0.0.5. Refs: wo-tracker #68217 Signed-off-by: Pratik Gandhi <gandhipratik203@gmail.com> * fix(rate-limiter): bump redis to 0.32 to drop unmaintained transitive dep cargo-deny security-policy CI was red on this branch with two findings: 1. RUSTSEC-2025-0134 — rustls-pemfile 2.2.0 is unmaintained. Pulled in by redis 0.27 → rustls-native-certs 0.7.3. 2. License rejected — webpki-roots is CDLA-Permissive-2.0, which is not in the workspace deny.toml allow-list. Both come from the TLS feature additions in the previous commit. Bumping the redis crate to 0.32 takes us to rustls-native-certs 0.8.x, which migrated to rustls-pki-types and dropped the rustls-pemfile dependency entirely. The advisory is closed at the root rather than suppressed via deny.toml. The license rejection is fixed by adding CDLA-Permissive-2.0 to the workspace allow-list. CDLA-Permissive-2.0 is a Linux-Foundation permissive data license used by webpki-roots for the Mozilla CA bundle — keeping the bundle pinned in the binary keeps deployments self-contained (no host-OS CA-store dependency in containers). No source-code changes — redis 0.27 → 0.32 is API-compatible for the APIs the Rust core uses (Client::open, MultiplexedConnection, cmd builder, redis::Value pattern matching with _ arms, ErrorKind variants we touch). All 47 redis call sites in redis_backend.rs compile unchanged. Local gate green: cargo check-all (57 Rust tests), pytest (112 Python tests). Signed-off-by: Pratik Gandhi <gandhipratik203@gmail.com> * test(rate-limiter): add TLS lazy-handshake regression test Addresses Luca's P2-4 review feedback on PR #74: the construction-only TLS regression test verifies URL parsing but not the lazy handshake. Added test_rediss_url_lazy_handshake_reaches_connectivity_layer that drives a tool_pre_invoke against rediss://127.0.0.1:1/15 so the lazy TLS path actually engages, then asserts the failure is connectivity- shaped (refused / timed out / IO) rather than the wo-tracker #68217 'feature is not enabled' / InvalidClientConfig signature. Signed-off-by: Pratik Gandhi <gandhipratik203@gmail.com> * fix(rate-limiter): install rustls crypto provider for rediss:// runtime path Two related changes that together make `rediss://` URLs actually work end-to-end: 1. Install the rustls ring crypto provider once at Rust core init. Previous TLS commits made `rediss://` URLs parse and the plugin load without the wo-tracker #68217 "feature is not enabled" error, but they left a runtime-time gap: rustls 0.23 dropped its implicit default crypto provider, so the first real TLS handshake panicked with `Call CryptoProvider::install_default()...`. Added a direct dep on rustls 0.23 with the `ring` feature, plus an OnceLock-guarded `ensure_crypto_provider()` in the Rust core that calls `rustls::crypto::ring::default_provider().install_default()` exactly once per process. Called from `RateLimiterPluginCore::new()` before any redis client is constructed. `ring` is the simpler default — pure Rust + small asm, BSD-3 / ISC licensed, builds cleanly across the existing PyPI wheel matrix without extra system toolchains. aws-lc-rs would be needed for FIPS validation; not a requirement for this customer. 2. Drop the `tls-rustls-webpki-roots` feature on the redis crate. That feature pulled `webpki-roots` (Mozilla's CA bundle, licensed CDLA-Permissive-2.0) into the dep tree, which the workspace deny.toml correctly rejected because that license is not in the allow list. Adding the license to the allow list (or as an exception) widens the workspace policy for one transitive crate. Without `tls-rustls-webpki-roots`, the redis crate uses `rustls-native-certs` (Apache-2.0 / MIT / ISC — already allowed) to read CA certificates from the host OS trust store at runtime. mcp-context-forge ships on UBI 10 Minimal, which includes `ca-certificates` by default; AWS ElastiCache uses public CA chains that are in any standard OS bundle. Operationally equivalent for this deployment, with a clean license posture. Reverts the `CDLA-Permissive-2.0` entry from the workspace deny.toml allow list — no longer needed. Test: the existing TLS lazy-handshake regression test (test_rediss_url_lazy_handshake_reaches_connectivity_layer) now reaches the network layer instead of panicking at TLS init. Its assertion is scoped to the wo-tracker #68217 negative signature ("feature is not enabled" / InvalidClientConfig must never appear). A positive "the lazy handshake actually reached the network" assertion would require the Rust core's log_exception() to surface the underlying RedisError text instead of the generic "error; allowing request" message — tracked as a follow-up alongside the real-TLS-Redis-fixture variant. Local gate: cargo fmt-check + clippy clean, 57 Rust unit tests pass, 107 Python integration tests pass. Cargo.lock confirms removal of unmaintained `rustls-pemfile` and license-flagged `webpki-roots`; `rustls-native-certs` 0.8.3 is the only remaining TLS-related dep. Signed-off-by: Pratik Gandhi <gandhipratik203@gmail.com> * chore(rate-limiter): bump 0.0.5 → 0.0.6 for TLS support release Main bumped rate_limiter to 0.0.5 in #73 as part of a workspace-wide dependency refresh, so this PR's release slot moves to 0.0.6. The content of 0.0.6 is the TLS / rediss:// support work in this PR (crypto provider install, redis crate bump for advisory cleanup, TLS regression tests). Signed-off-by: Pratik Gandhi <gandhipratik203@gmail.com> * test(rate-limiter): pin ensure_crypto_provider behaviour with a Rust unit test cargo-mutants flagged an uncovered mutation: replacing ensure_crypto_provider's body with () passed the existing test suite, because nothing in the Rust unit tests directly exercises the crypto provider install (TLS coverage lives on the Python integration side). Added a unit test in plugin.rs's tests module that calls ensure_crypto_provider() and asserts rustls::crypto::CryptoProvider::get_default().is_some(). - Real implementation installs the ring provider via OnceLock → get_default() returns Some → test passes. - Mutated implementation does nothing → no provider installed → get_default() returns None → test fails → mutant killed. OnceLock makes the call idempotent, so the test is order-independent within a cargo-test process. No behavioral change; pure coverage. Signed-off-by: Pratik Gandhi <gandhipratik203@gmail.com> * test(rate-limiter): add real TLS handshake regression test against a TLS-enabled Redis TestRedisTlsSupport only verified URL parsing and the absence of the wo-tracker #68217 InvalidClientConfig signature; nothing in that class drove a real handshake, so a regression in the rustls crypto provider, the cert-chain loader, or rustls-native-certs lookup could pass those tests yet fail at first contact with a real TLS server. This adds an end-to-end fixture that: - generates a self-signed CA + Redis server cert in ~/.cache/ - starts a redis:7 container with --tls-port 6390 (host port 16390) - exports SSL_CERT_FILE so both rustls-native-certs (Rust core) and Python's ssl module (test helpers) trust the test CA without modifying the OS trust store - drives tool_pre_invoke through rediss:// and asserts the counter key materializes in the TLS Redis — only a successful rustls handshake gets you a written key Skips cleanly when openssl or docker is unavailable, so laptops without docker stay green while Linux CI exercises the full path. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> Signed-off-by: Pratik Gandhi <gandhipratik203@gmail.com> * chore(rate-limiter): drop internal-tracker references from code comments Per review feedback: removes references to the internal customer issue tracker ("wo-tracker #68217") from code comments and docstrings. The technical descriptions of the regression are kept intact (e.g. "TLS-feature-not-enabled regression", "managed Redis with in-transit encryption", "InvalidClientConfig signature") so the comments still convey what each test is pinning; just the internal-only tracker ID is removed. Touches: - plugins/rust/python-package/rate_limiter/src/plugin.rs:553 - plugins/tests/rate_limiter/test_redis_integration.py — 6 mentions in TestRedisTlsSupport docstring/comments + TestRedisTlsHandshake docstring No behavioural change. All 59 tests pass. Signed-off-by: Pratik Gandhi <gandhipratik203@gmail.com> --------- Signed-off-by: Pratik Gandhi <gandhipratik203@gmail.com> Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent c3c79e1 commit deffa93

5 files changed

Lines changed: 628 additions & 33 deletions

File tree

Cargo.lock

Lines changed: 146 additions & 29 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

plugins/rust/python-package/rate_limiter/Cargo.toml

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[package]
22
name = "rate_limiter"
3-
version = "0.0.5"
3+
version = "0.0.6"
44
edition.workspace = true
55
authors.workspace = true
66
license.workspace = true
@@ -24,7 +24,21 @@ pyo3-stub-gen = { workspace = true }
2424
pyo3-log = { workspace = true }
2525
parking_lot = "0.12"
2626
thiserror = { workspace = true }
27-
redis = { version = "0.27", features = ["aio", "tokio-comp"] }
27+
redis = { version = "0.32", features = ["aio", "tokio-comp", "tls-rustls", "tokio-rustls-comp"] }
28+
# Direct rustls dep with the `ring` crypto provider feature. redis 0.32
29+
# pulls rustls in transitively but via `default-features = false`, so no
30+
# crypto provider is enabled by default — rustls 0.23 then panics at
31+
# first TLS use ("Call CryptoProvider::install_default()..."). We add
32+
# `ring` here and install it as the default provider once at plugin init.
33+
#
34+
# Note on CA roots: by NOT enabling `tls-rustls-webpki-roots`, the redis
35+
# crate falls back to `rustls-native-certs` (Apache-2.0 / MIT / ISC) which
36+
# reads CA certificates from the host OS trust store at runtime. This
37+
# avoids a transitive dep on `webpki-roots` (CDLA-Permissive-2.0). The
38+
# trade-off: the host container must have a CA bundle installed — true
39+
# for the UBI Minimal image mcp-context-forge ships on, and for any
40+
# distribution that keeps `ca-certificates` in its base.
41+
rustls = { version = "0.23", default-features = false, features = ["ring", "std"] }
2842
tokio = { workspace = true }
2943

3044
[dev-dependencies]

plugins/rust/python-package/rate_limiter/cpex_rate_limiter/plugin-manifest.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
description: "Rate limiting by user/tenant/tool — memory (single-process) or Redis (shared across instances)"
22
author: "ContextForge Contributors"
3-
version: "0.0.5"
3+
version: "0.0.6"
44
kind: "cpex_rate_limiter.rate_limiter.RateLimiterPlugin"
55
available_hooks:
66
- "prompt_pre_fetch"

0 commit comments

Comments
 (0)