Skip to content

What's not done / known gaps - #5

Closed
ArksherX wants to merge 33 commits into
securitylevel5:mainfrom
ArksherX:feature/per-identity-rate-limit
Closed

What's not done / known gaps#5
ArksherX wants to merge 33 commits into
securitylevel5:mainfrom
ArksherX:feature/per-identity-rate-limit

Conversation

@ArksherX

@ArksherX ArksherX commented May 16, 2026

Copy link
Copy Markdown

Summary

Adds per-identity byte-rate limiting to the tunnelled data proxied through the gateway. Each agent identity (extracted from the mTLS client certificate extension) gets its own token bucket rate limiter. If an identity exceeds its configured throughput limit, the copy loop is throttled automatically.

Components of the Gateway (task item 1)

The gateway is composed of the following parts:

  • main.rs — Entry point. Loads config, sets up TLS, starts the TCP listener, and dispatches connections to MakeProxyService.
  • proxy.rs — Core request handling. Implements the Service trait for hyper, extracts the destination from the CONNECT request, calls the policy engine, opens the upstream TCP connection, and spawns the bidirectional tunnel.
  • policy.rs — Authorization logic. Extracts the agent identity from the custom X.509 certificate extension, queries PostgreSQL to check for a valid signed permission row, and returns Allow or Deny.
  • config.rs — Typed configuration structs deserialized from config.toml.
  • tls.rs — Sets up the rustls server config for mTLS, requiring and verifying client certificates.
  • rate_limit.rs (new) — Per-identity token bucket rate limiters backed by governor and stored in a DashMap.

What the feature does

  • Reads [rate_limit] from config.tomlbytes_per_second and burst_bytes
  • Maintains an in-memory DashMap<String, Arc<RateLimiter>> keyed by agent identity
  • When a tunnel is spawned, if bytes_per_second > 0, the bidirectional copy is wrapped with rate limiter checks per identity
  • Logs when rate limiting is active for a connection

Design choices and tradeoffs

In-memory over persistent storage
State lives in a DashMap on the heap. This means limits reset on gateway restart and are not shared across multiple gateway instances. This is the right tradeoff for a single-node SL5 weight enclave deployment — restarts are controlled events, and the SL5 threat model assumes a single-facility enclave. Adding distributed state (e.g. a Postgres counter per identity per time window) would be the correct next step for multi-node deployments, at the cost of a database roundtrip per data chunk.

Global config, not per-identity config
All identities share the same bytes_per_second and burst_bytes values from config. Per-identity limits would require either a config map keyed by identity string or a new database column — straightforward to add but out of scope for this implementation given the simplicity priority.

What it protects against and what it doesn't
The rate limit addresses sustained bulk exfiltration — an agent continuously streaming large volumes of data will be throttled. It does not address short bursts below the window duration, and it does not address an adversary who controls multiple distinct identities. It is a bandwidth control, not a session control.

Crate choice: governor
governor provides a well-tested token bucket implementation with no_std support and minimal dependencies. RateLimiter::direct with a Quota::per_second is the simplest correct primitive for bytes-per-second limiting.

Implementation process and tools used

  • Read the upstream codebase to understand where the bidirectional copy happens (spawn_tunnel in proxy.rs)
  • Used Claude (Anthropic) extensively for Rust-specific guidance: fixing lifetime errors with tokio::spawn, resolving clippy pedantic lints (needless_pass_by_value, clone_on_copy), and deriving Copy on RateLimitConfig to satisfy both the borrow checker and clippy simultaneously
  • All design decisions were made independently; Claude was used as a Rust reference, not an architect

What's not done / known gaps

  • No per-identity config (all identities share the same limit)
  • No integration test verifying throughput is measurably capped (the rate
    limit code path is exercised and all 61 tests pass, but a timing-based
    throughput assertion was not added to avoid flakiness in CI)

sl5-guy added 30 commits April 13, 2026 12:09
Proxy accepts HTTP/2 CONNECT requests over mTLS, extracts a custom X.509
extension from the client certificate, and evaluates it against a TOML
policy to authorize connections to requested destinations. Allowed
connections are tunneled as opaque TCP via bidirectional byte copy.

Includes structured logging (tracing), OpenTelemetry export, Prometheus
metrics, and a comprehensive test suite (38 integration + 8 e2e tests
covering tunnel success, policy denial, extension enforcement, mTLS
fail-closed, unreachable destinations, and method rejection).
Tested end-to-end: client connects to proxy over mTLS h2, sends CONNECT,
tunnels TLS to the destination, and receives an echoed JSON response.

The client uses h2 + ssl.MemoryBIO to perform TLS-over-h2-tunnel
(validate_outbound_headers disabled to work around h2's incorrect
CONNECT pseudo-header validation).
generate-certs.sh creates the certs/ directory with all certificates
needed to run the proxy locally. README quick-start section documents
the workflow.

Sample client hardened against livelock (bounded fast-recv loops),
SSLWantWriteError during handshake/send/recv, and unexpected
ConnectionTerminated frames from the proxy.
Replace all wildcard ("*") dependency versions with semver-compatible
pins: "MAJOR" for 1.x+ crates, "0.MINOR" for 0.x crates.
OpenTelemetry tracing is sufficient. Drop the metrics and
metrics-exporter-prometheus crates, the metrics_bind config field,
the PrometheusBuilder listener, and the corresponding docs. Add a
regression test ensuring metrics_bind is rejected as unknown.
Record source identity, source peer, destination authority, gateway endpoint, and tunnel outcomes directly on gateway and sidecar events so logs are self-contained.
Support disabling stdout logging while keeping OTLP export active, and document the TPM cert reissue behavior in the sidecar connection flow.
Store only the signing key public DER and enforce uniqueness on that source value instead of carrying a derived hash that can drift.
Create principal signing keys through tpm2-pkcs11 so enrollment stores only the TPM public key while keeping the private key on the principal machine.
Let admins grant principal signing keys destination delegation authority without binding grants to individual agent identities.
Bind authorization to the presented client certificate key in Postgres so client CA bundles are no longer a trust root.
Keep registry-backed policy details behind the policy construction boundary and trim unused dependency features so the gateway exposes less non-core surface.
Move the former examples into purpose-specific homes so demo scaffolding and registry mutation commands are not presented as sample code.
Change cargo clippy to pedantic and switch sqlx to use query macros for compile-time type checks
@ArksherX ArksherX changed the title feat: add per-identity data rate limiting What's not done / known gaps May 16, 2026
@luiscosio luiscosio closed this Jul 10, 2026
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.

3 participants