Skip to content

Commit 4ccb6c6

Browse files
author
tom
committed
Merge branch 'develop' into 'main'
Promote develop → main Closes #10 See merge request pub-projects/hort!93
2 parents 9c834e7 + 751f8e3 commit 4ccb6c6

19 files changed

Lines changed: 656 additions & 333 deletions

File tree

.gitlab-ci.yml

Lines changed: 33 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -130,6 +130,17 @@ variables:
130130
- apt-get install -y -qq --no-install-recommends
131131
protobuf-compiler pkg-config libssl-dev ca-certificates curl git
132132

133+
# Fetch the PREBUILT cargo-binstall (a single static binary) into
134+
# $CARGO_HOME/bin, so tool installs (`cargo binstall <tool>`) download a
135+
# prebuilt release instead of compiling the tool from source every pipeline.
136+
# Best-effort (`|| true`): if the fetch is unreachable, the per-tool install
137+
# lines fall back to `cargo install` (source), so binstall is a pure
138+
# accelerator and never a hard dependency of a gate. Requires curl on PATH.
139+
.install-binstall: &install-binstall
140+
- curl -L --proto '=https' --tlsv1.2 -sSf
141+
https://raw.githubusercontent.com/cargo-bins/cargo-binstall/main/install-from-binstall-release.sh
142+
| bash || true
143+
133144
# Conditional internal-PKI CA trust. Two trust-store flavours: RHEL-family
134145
# (the buildah image) and Debian/Alpine (rust:slim, alpine/helm). Both are
135146
# no-ops when PLATFORM_CA_PATH is unset or the file is absent, so a public-CA
@@ -335,7 +346,9 @@ test:coverage:
335346
- *platform-ca-deb
336347
- *hort-auth-steps
337348
- rustup component add llvm-tools-preview
338-
- cargo install --locked cargo-llvm-cov || true
349+
- *install-binstall
350+
- cargo binstall --no-confirm --locked cargo-llvm-cov
351+
|| cargo install --locked cargo-llvm-cov || true
339352
script:
340353
- cargo llvm-cov --workspace --lib
341354
--lcov --output-path lcov.info
@@ -405,13 +418,16 @@ test:integration:
405418
security:cargo-audit:
406419
stage: security
407420
image: ${RUST_IMAGE}
408-
needs:
409-
- job: test:unit
410-
artifacts: false
421+
# No workspace compile — `cargo audit` reads Cargo.lock and the tool is a
422+
# prebuilt binstall download — so this runs from pipeline start rather than
423+
# queuing behind the serialized test-compile chain.
424+
needs: []
411425
before_script:
412426
- apt-get update -qq
413-
- apt-get install -y -qq --no-install-recommends ca-certificates git pkg-config libssl-dev
414-
- cargo install --locked cargo-audit || true
427+
- apt-get install -y -qq --no-install-recommends ca-certificates git pkg-config libssl-dev curl
428+
- *install-binstall
429+
- cargo binstall --no-confirm --locked cargo-audit
430+
|| cargo install --locked cargo-audit || true
415431
script:
416432
# Diagnostic JSON artefact captured first; failure-tolerant (||
417433
# true) so a non-zero exit still produces the upload below.
@@ -489,13 +505,16 @@ quality:no-plans-on-default-branch:
489505
security:cargo-deny:
490506
stage: security
491507
image: ${RUST_IMAGE}
492-
needs:
493-
- job: test:unit
494-
artifacts: false
508+
# No workspace compile — `cargo deny check` reads `cargo metadata` and the
509+
# tool is a prebuilt binstall download — so this runs from pipeline start
510+
# rather than queuing behind the serialized test-compile chain.
511+
needs: []
495512
before_script:
496513
- apt-get update -qq
497-
- apt-get install -y -qq --no-install-recommends ca-certificates git pkg-config libssl-dev
498-
- cargo install --locked cargo-deny || true
514+
- apt-get install -y -qq --no-install-recommends ca-certificates git pkg-config libssl-dev curl
515+
- *install-binstall
516+
- cargo binstall --no-confirm --locked cargo-deny
517+
|| cargo install --locked cargo-deny || true
499518
script:
500519
- cargo deny --workspace check
501520

@@ -986,7 +1005,9 @@ release:sbom:
9861005
# already pulls curl + ca-certificates; add jq explicitly so the
9871006
# Vault-token + key fetch in cosign-setup.sh succeeds.
9881007
- apt-get install -y -qq --no-install-recommends jq
989-
- cargo install --locked cargo-cyclonedx || true
1008+
- *install-binstall
1009+
- cargo binstall --no-confirm --locked cargo-cyclonedx
1010+
|| cargo install --locked cargo-cyclonedx || true
9901011
# Trust the platform CA (internal PKI) BEFORE cosign-setup.sh runs: in
9911012
# vault-key mode its HTTPS call to ${VAULT_ADDR} (OpenBao) must verify
9921013
# against an internal CA not in the public bundle. No-op when

CHANGELOG.md

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,29 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
77

88
## [Unreleased]
99

10+
### Added
11+
12+
- **`HORT_RATELIMIT_EXEMPT_CIDRS`** (Helm `rateLimitExemptCidrs`) — source CIDRs
13+
whose resolved client IP bypasses both rate-limit buckets. For first-party CI
14+
that shares one egress IP (or sits behind one ingress) and would otherwise
15+
collapse into a single per-IP bucket and `429` on legitimate publish bursts.
16+
Keyed on the trust-resolved client IP (not a spoofable header). Note: this
17+
also removes the per-IP anti-credential-stuffing limit on the token-mint
18+
paths for that range — list only fully trusted CI egress ranges.
19+
20+
### Fixed
21+
22+
- **The inbound rate limiter now sustains its configured per-minute rate.** The
23+
auth (`HORT_RATELIMIT_AUTH_PER_MIN`, default 60) and write
24+
(`HORT_RATELIMIT_WRITE_PER_MIN`, default 300) token buckets replenished one
25+
token per minute regardless of the cap, so after the initial burst, sustained
26+
traffic was throttled to ~1 request/minute — roughly 60×/300× tighter than
27+
documented — which persistently `429`'d automated writers such as CI pushing
28+
multi-layer images. Tokens now replenish at the configured per-minute rate.
29+
- **Rate-limit `429` responses no longer advertise `Retry-After: 0`.** A
30+
sub-second wait is rounded up to at least 1 second, so a throttled client
31+
backs off for a beat instead of hot-looping on an immediate retry.
32+
1033
## [0.9.8] - 2026-07-05
1134

1235
Headlines: OCI **image-index / manifest-list (multi-arch) push**; **working

Cargo.lock

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

crates/hort-app/src/use_cases/pat_validation_use_case.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -154,7 +154,7 @@ struct ApiTokenUsedGate {
154154
///
155155
/// Defaults: **30 misses / 5 min triggers a 15-min
156156
/// lockout**. Argon2id at OWASP-2024 cost is ~50–80 ms per verify, so
157-
/// an attacker botnet of N IPs × 60/min through `tower_governor`
157+
/// an attacker botnet of N IPs × 60/min through the per-IP rate limiter
158158
/// would otherwise pin N×80 CPU-cores at no auth cost — this
159159
/// gate caps the verify rate per IP-bucket regardless of where the
160160
/// global rate-limiter is.

crates/hort-http-core/Cargo.toml

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,6 @@ hort-adapters-ephemeral-memory = { path = "../hort-adapters-ephemeral-memory", o
3636
axum = { workspace = true }
3737
tower = { workspace = true }
3838
tower-http = { workspace = true }
39-
tower_governor = { version = "=0.5.0", default-features = false, features = ["axum"] }
4039
governor = "0.8"
4140
tokio = { workspace = true }
4241
tokio-util = { workspace = true }

crates/hort-http-core/src/middleware/load_shed.rs

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,9 @@
1515
//! in-flight per IP (`HORT_MAX_INFLIGHT_PER_IP`). Reads
1616
//! [`RequestTrust::client_ip`] from request extensions (populated by
1717
//! the request trust layer); a missing trust extension surfaces as a 500
18-
//! rather than silently bypassing the cap (mirrors
19-
//! [`crate::middleware::rate_limit::TrustAwareKeyExtractor`]).
18+
//! rather than silently bypassing the cap (same "missing trust → 500,
19+
//! never bucket-bypass" invariant the rate-limit middleware enforces —
20+
//! see [`crate::middleware::rate_limit`]).
2021
//!
2122
//! # Threat model
2223
//!
@@ -319,7 +320,8 @@ pub async fn global_load_shed_middleware(
319320
///
320321
/// Reads `client_ip` from [`RequestTrust`] (populated by the request
321322
/// trust layer). On a missing extension surfaces 500 — same conservative
322-
/// failure mode as [`crate::middleware::rate_limit::TrustAwareKeyExtractor`].
323+
/// failure mode the rate-limit middleware uses (see
324+
/// [`crate::middleware::rate_limit`]).
323325
/// Get-or-inserts an `Arc<Semaphore>` for that IP and try-acquires one
324326
/// permit; failure sheds with 503 and the
325327
/// `result=per_ip_shed` metric label.

0 commit comments

Comments
 (0)