1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
[]
# Published to crates.io as `eggrd` ("edgeguard" is taken by an unrelated crate). The binary and
# library keep the name `edgeguard` (see [lib]/[[bin]] below), so the CLI, env vars (EDGEGUARD_*),
# the /__edgeguard/* namespace, and `use edgeguard::` in tests/benches are all unchanged — only the
# crates.io package id differs. Install with `cargo install eggrd` (installs the `edgeguard` binary).
= "eggrd"
= "0.4.0"
= "2021"
= "A drop-in Rust edge proxy that gives any app a secure front door: auth, rate limiting, and hardened response headers, with zero changes to the upstream app."
= "Apache-2.0"
= "https://github.com/lucheeseng827/eggrd"
= "README.md"
= ["proxy", "reverse-proxy", "security", "waf", "rate-limit"]
= ["web-programming::http-server", "network-programming", "command-line-utilities"]
# Keep the published package lean: ship the crate + its config reference + benches, not the load-test
# rig, the detached wasm worker, docs, or deploy examples.
# `ee/` is the PRIVATE control plane (its own workspace) — it must never be packaged into the
# public `eggrd` crate. Keep it first; the rest just trims the published tarball.
= ["ee/", "edgeguard-ner/", "loadtest/", "worker/", "docs/", "examples/", "tests/", ".github/", ".ossync.yaml"]
# Package id is `eggrd`, but the compiled artifacts stay `edgeguard` so nothing downstream renames.
[]
= "edgeguard"
= "src/lib.rs"
[[]]
= "edgeguard"
= "src/main.rs"
[]
= { = "1", = ["full"] }
= "0.7"
= { = "1", = ["client", "http1", "server"] }
= { = "0.1", = ["client", "client-legacy", "http1", "server", "server-auto", "tokio"] }
= "0.1"
= "1"
= "0.6"
= "0.22"
= { = "1", = ["derive"] }
= "1"
= "0.8"
# WAF-lite input inspection: the built-in SQLi/XSS/path-traversal heuristics and the
# operator-supplied `[[waf.rules]]` deny patterns. The `regex` crate matches in linear time
# and rejects backreferences/lookaround, so a user-supplied pattern can't trigger catastrophic
# backtracking (ReDoS) — a safety property we rely on for the configurable rules.
= "1"
# Edge-DLP gazetteer: a single linear-time Aho-Corasick automaton over the operator's deny-list
# terms (known names / codenames / identifiers). Pulled in directly (the `regex` crate already
# depends on it transitively, so this adds no new leaf to the tree) — see src/dlp.rs.
= "1"
# JWT verification (HS*/RS*/ES*/PS*/EdDSA) and JWKS parsing.
= "9"
# JWKS fetch over HTTPS. `rustls-tls` avoids a system OpenSSL dependency; default features
# (native-tls, gzip, …) are off — a plain GET is all the JWKS client needs.
# Already ring-only, no change needed: `rustls-tls` => `rustls-tls-webpki-roots` => `__rustls-ring`,
# and reqwest declares rustls/tokio-rustls/hyper-rustls with `default-features = false`.
= { = "0.12", = false, = ["rustls-tls", "json"] }
# Distributed (shared-store) rate limiter for multi-replica deployments: a Redis-backed GCRA
# store (`ratelimit.store = "redis"`). `tokio-comp` for async, `connection-manager` for
# auto-reconnect, `tokio-rustls-comp` so `rediss://` TLS uses rustls (no system OpenSSL),
# matching the rest of the crate. Only used when the distributed limiter is enabled.
# Also already clean: `tls-rustls` only asks for `rustls/std`, and redis declares rustls and
# tokio-rustls with `default-features = false` — it inherits whichever provider the graph picks.
= { = "1", = ["tokio-comp", "connection-manager", "tokio-rustls-comp"] }
# Lock-free atomic swap of the live policy, so config hot-reload never blocks the request
# path nor drops in-flight connections.
= "1"
# Watch the config file for changes (hot-reload).
= "6"
# TLS termination. `ring` is the ONLY crypto provider this crate links, so the rustls config is
# deterministic and `CryptoProvider::from_crate_features()` resolves unambiguously.
#
# `default-features = false` is load-bearing, not tidiness. rustls's default set is
# ["aws_lc_rs", "logging", "prefer-post-quantum", "std", "tls12"]; adding "ring" on top of it
# links BOTH providers, which:
# * drags in aws-lc-sys — the graph's only cmake/bindgen C build, and by a wide margin its
# most expensive crate. It is also the part most likely to break on a new toolchain or a
# musl/cross target, i.e. exactly the static-binary/distroless promise. (Measurements in
# CHANGELOG.md under 0.3.1..Unreleased; they are not repeated here to drift.) And
# * makes `rustls::ClientConfig::builder()` PANIC ("Could not automatically determine the
# process-level CryptoProvider") in any crate that relies on feature-based provider
# selection, unless `tls::init_crypto()` has already run. It only runs when `[tls] enabled`,
# so `ratelimit.store = "redis"` over `rediss://` with TLS termination off used to abort the
# process on the first rate-limited request (the `redis` crate calls that builder).
# So: keep every rustls-consuming dep below on an explicit ring path. `prefer-post-quantum` is
# dropped because it *is* an aws_lc_rs alias; nothing is lost, since src/tls.rs already builds
# the ServerConfig with an explicit ring provider and ring has no ML-KEM — the listener
# negotiates the same TLS1.3 / X25519 / TLS_AES_256_GCM_SHA384 before and after.
= { = "0.23", = false, = ["ring", "logging", "std", "tls12"] }
# Same story: tokio-rustls's defaults are ["logging", "tls12", "aws_lc_rs"], and its `aws_lc_rs`
# turns `rustls/aws_lc_rs` back on for the whole graph via feature unification.
= { = "0.26", = false, = ["ring", "logging", "tls12"] }
= "2"
# Adapt the axum router onto a manual hyper connection when terminating TLS ourselves.
= { = "0.5", = ["util"] }
# Optional response compression (gzip), applied only when `validation.compress_responses` is on
# and never to streamed `text/event-stream` responses.
= { = "0.6", = ["compression-gzip"] }
# ACME / Let's Encrypt automatic certificates (HTTP-01), and CSR/key generation for it.
# 0.7.2 (Oct 2024) could not parse Let's Encrypt's current authorization payload —
# `missing field \`token\`` — so ACME issuance was broken in the field. 0.8 also
# generates the key + CSR in `finalize()`, which is why rcgen was dropped in 0.3.0:
# nothing in this crate constructed a certificate itself any more.
# Its default features are ["aws-lc-rs", "hyper-rustls"], and `aws-lc-rs` also forces
# `rcgen/aws_lc_rs` + `hyper-rustls/aws-lc-rs`. The `ring` feature is the drop-in swap for it
# (`ring` => hyper-rustls?/ring + rcgen/ring); `hyper-rustls` is re-listed because it is a
# default we still need for the ACME HTTP client. hyper-rustls and rustls-platform-verifier
# come in transitively from here and need no entry of their own — this covers both.
= { = "0.8", = false, = ["ring", "hyper-rustls"] }
# Self-signed certificate generation for `[tls] self_signed` / `edgeguard cert`, so TLS can be
# turned on without obtaining a certificate first. The 0.3.0 notes say rcgen was "dropped" — true
# of DIRECT use (instant-acme 0.8 builds its own key and CSR in `finalize()`), but instant-acme
# depends on rcgen itself, so it was still compiled on every build. Declaring it here is a direct
# edge to a node already in the graph: no new crate is compiled and no transitive dependency is
# added. Default features are off — `x509-parser` and `aws_lc_rs` are not needed; `ring` matches
# the provider the rest of the crate pins, and `pem` is the output format the TLS loader reads.
= { = "0.14", = false, = ["pem", "ring"] }
# Validity dates on the generated certificate. Already in the tree (rcgen and the ACME stack pull
# it); named here because this crate uses it directly.
= { = "0.3", = false, = ["std"] }
= "0.1"
= { = "0.3", = ["json", "env-filter"] }
# Generate a request id (UUID v4) when an inbound request doesn't carry one, for end-to-end log
# correlation (`X-Request-Id`).
= { = "1", = ["v4"] }
= "1"
# `std` pulls in `password-hash`'s getrandom-backed `OsRng` for the `--hash` helper's
# salt generation (verification works without it; salting a fresh hash needs an RNG).
= { = "0.5", = ["std"] }
# Optional ML NER layer for edge DLP (gateway L3): the pure-Rust `edgeguard-ner` crate (tract-onnx +
# tokenizers). It is an OPTIONAL dependency gated behind the `ner` feature below, and its own heavy
# ONNX dep graph is gated behind `edgeguard-ner/onnx`, so the DEFAULT build pulls neither — the
# single-static-binary / distroless / static-musl promise is unchanged unless you build `--features ner`.
# A `version` is required alongside `path` so the published `eggrd` manifest is valid (Cargo rejects
# a path-only dep when packaging). The default build never pulls it (optional, off by default), so
# `cargo package`/`publish` of eggrd succeeds; the `ner` feature is a build-from-source opt-in.
= { = "edgeguard-ner", = "0.1.1", = true }
[]
# Default build is the deterministic-only edge DLP (regex + gazetteer + entropy): no ML, no ONNX,
# byte-for-byte the lean proxy. `ner` adds the ONNX NER family (person/address/org) by turning on the
# optional crate AND its `onnx` feature (tract + tokenizers). Keep `default` empty.
= []
= ["dep:edgeguard-ner", "edgeguard-ner/onnx"]
# Process-group signaling in the supervisor is Unix-only.
[]
= "0.2"
# Micro-benchmark harness for the request-path hot spots (auth gate, WAF regex eval, response
# hardening, config parsing). Macro/throughput load testing lives out-of-process under
# `loadtest/` (k6 + docker-compose); these criterion benches isolate the per-call cost of the
# pure-Rust pipeline stages that the macro test can only measure end-to-end. See docs/TESTPLAN.md.
[]
= { = "0.5", = ["html_reports"] }
[[]]
= "auth"
= false
[[]]
= "waf"
= false
[[]]
= "response"
= false
# Release profile — GOVERNS EVERY BUILD THAT SHIPS, none of which is the monorepo workspace build.
# eggrd is distributed as a standalone crate, and all three paths make this file the workspace root,
# so all three get exactly the settings below:
# - `cargo install eggrd` — crates.io, the package is its own root;
# - `mancube/eggrd` — `Dockerfile`'s build context is this directory (`COPY . .`), so nothing
# above it is copied into the image;
# - the public `lucheeseng827/eggrd` mirror — `sync-eggrd-oss.yml` stages the cut OUTSIDE the
# monorepo checkout specifically "so the cut has no parent workspace", and gates on that tree.
#
# Only `cargo build --release` from the MONOREPO root differs: cargo profiles are workspace-global,
# so there the root manifest wins (`opt-level = "z"`, `lto = true`, `codegen-units = 1`) and cargo
# prints "profiles for the non root package will be ignored". That warning is about the monorepo
# build, which ships nothing — it is not evidence this block is dead. Deleting it would silently
# drop `lto = "thin"` and `strip` from the published crate, the image and the mirror, on a crate
# whose pitch is a tiny static binary on distroless.
#
# One consequence worth knowing: `[profile.bench]` inherits `[profile.release]`, so running the
# three benches above from the monorepo root measures `opt-level = "z"` + fat LTO — a configuration
# nothing ships. Bench from a standalone cut (or the mirror) for numbers that describe the real
# binary. No CI does this: no workflow runs `cargo bench`. Unlike `panic`, `opt-level` IS legal in a
# `[profile.release.package.eggrd]` override at the monorepo root if the workspace-build gap ever
# needs closing — though `lto` is not, so that would only narrow the difference, not remove it.
[]
= 3
= "thin"
= true