mirror of
https://github.com/rustfs/rustfs.git
synced 2026-07-26 08:18:18 +00:00
test(security): wire negative-auth suites into e2e-smoke with a count-floor guard (#4815)
The header-SigV4 (sec-1), presigned-URL (sec-2), and admin-gate (sec-4) negative auth-rejection e2e suites merged earlier but only presigned_negative was actually selected by any CI profile; negative_sigv4_test and admin_auth_test compiled and sat unrun. Add both to the e2e-smoke default-filter so all three attacker-facing S3 auth-rejection suites execute on every PR. They already meet the smoke admission criteria (RustFSTestEnvironment, random ports, parallel-safe, no #[ignore], no feature gates), so this is a pure filterset change — the single e2e-in-CI wiring mechanism (backlog#1149 ci-4), not a new job. Because the filter selects by module name, a rename or deletion could silently drop a suite out of the security gate with no CI signal. Add scripts/check_security_smoke_count.sh (infra-12 count-floor mechanism): it lists what the e2e-smoke profile selects and fails if the count of security auth-rejection tests drops below the committed floor in .config/security-smoke-floor.txt (16). Invoked from the e2e-tests job, before the smoke run, so the nextest list compiles the binaries the run reuses. The GHSA-3p3x FTPS/WebDAV constant-time e2e (protocols::test_protocol_core_suite) stays out by topology: it binds fixed ports, needs the ftps,webdav features, and is #[serial], so it cannot join the random-port default-feature smoke profile as a filterset change (global ruling G5). Its GHSA-r5qv sibling is a unit test that already runs in the default CI pass. docs/testing/security-regressions.md now carries the full CI-execution map and flags the GHSA-3p3x e2e CI-lane gap as a ci-domain follow-up. Refs: backlog#1151 (sec-5)
This commit is contained in:
+19
-1
@@ -144,10 +144,28 @@ test-group = 'e2e-reliability'
|
|||||||
# the guard now honours an off-by-default opt-in and this suite's source servers
|
# the guard now honours an off-by-default opt-in and this suite's source servers
|
||||||
# set it (RUSTFS_REPLICATION_ALLOW_LOOPBACK_TARGET) — so the allowlist below is
|
# set it (RUSTFS_REPLICATION_ALLOW_LOOPBACK_TARGET) — so the allowlist below is
|
||||||
# restored.
|
# restored.
|
||||||
|
#
|
||||||
|
# Security negative-auth subset (backlog#1151 sec-5): the three attacker-facing
|
||||||
|
# S3 auth-rejection suites join the first clause above by module name —
|
||||||
|
# presigned_negative (sec-2), negative_sigv4 (sec-1, header SigV4), and
|
||||||
|
# admin_auth (sec-4, admin gate + root-credential lifecycle). All three use
|
||||||
|
# RustFSTestEnvironment on a random port and are parallel-safe, so they meet the
|
||||||
|
# smoke admission criteria unchanged. This is the wiring step that makes those
|
||||||
|
# merged suites actually execute on every PR (they were dead until listed here).
|
||||||
|
# A rename that drops any of them out of this filter would silently thin the
|
||||||
|
# security gate with no CI signal, so scripts/check_security_smoke_count.sh owns
|
||||||
|
# a count-floor guard over exactly this subset (infra-12 mechanism, floor in
|
||||||
|
# .config/security-smoke-floor.txt), invoked from the e2e-tests job in ci.yml.
|
||||||
|
# NOT here by topology: the GHSA-3p3x FTPS/WebDAV constant-time e2e
|
||||||
|
# (protocols::test_protocol_core_suite) binds fixed ports and needs the
|
||||||
|
# ftps,webdav features, so it cannot join this random-port, default-feature
|
||||||
|
# profile; its GHSA-r5qv sibling is a unit test that already runs in the
|
||||||
|
# test-and-lint `--all --exclude e2e_test` pass. See
|
||||||
|
# docs/testing/security-regressions.md for the full CI-execution map.
|
||||||
[profile.e2e-smoke]
|
[profile.e2e-smoke]
|
||||||
default-filter = """
|
default-filter = """
|
||||||
package(e2e_test) & (
|
package(e2e_test) & (
|
||||||
test(/^(delete_marker_migration_semantics|version_id_regression|list_objects_v2_pagination|list_object_versions_regression|list_objects_duplicates|list_buckets_double_slash|leading_slash_key|special_chars|create_bucket_region|delete_objects_versioning|head_object_consistency|head_object_range|copy_object_metadata|copy_source_invalid_date|content_encoding|anonymous_access|bucket_policy_check|presigned_negative)_test::/)
|
test(/^(delete_marker_migration_semantics|version_id_regression|list_objects_v2_pagination|list_object_versions_regression|list_objects_duplicates|list_buckets_double_slash|leading_slash_key|special_chars|create_bucket_region|delete_objects_versioning|head_object_consistency|head_object_range|copy_object_metadata|copy_source_invalid_date|content_encoding|anonymous_access|bucket_policy_check|presigned_negative|negative_sigv4|admin_auth)_test::/)
|
||||||
| test(/^replication_extension_test::(test_replication_check_succeeds_with_remote_target|test_replication_check_rejects_target_without_object_lock|test_set_remote_target_rejects_unversioned_source_bucket|test_replication_check_rejects_unversioned_source_bucket|test_replication_check_rejects_missing_replication_config|test_replication_check_rejects_invalid_bucket|test_set_remote_target_rejects_same_bucket_on_same_deployment|test_set_remote_target_rejects_unversioned_target_bucket|test_set_remote_target_update_requires_arn|test_set_remote_target_update_rejects_missing_target|test_set_remote_target_rejects_invalid_target_url|test_set_remote_target_rejects_self_signed_https_target_without_skip_tls_verify|test_set_remote_target_rejects_private_ca_https_target_without_ca_cert_pem|test_list_remote_targets_rejects_empty_bucket|test_list_remote_targets_rejects_invalid_bucket|test_remove_remote_target_rejects_missing_target|test_remove_remote_target_rejects_missing_arn|test_remove_remote_target_rejects_invalid_bucket|test_remove_remote_target_rejects_target_used_by_replication|test_delete_bucket_replication_removes_remote_target)$/)
|
| test(/^replication_extension_test::(test_replication_check_succeeds_with_remote_target|test_replication_check_rejects_target_without_object_lock|test_set_remote_target_rejects_unversioned_source_bucket|test_replication_check_rejects_unversioned_source_bucket|test_replication_check_rejects_missing_replication_config|test_replication_check_rejects_invalid_bucket|test_set_remote_target_rejects_same_bucket_on_same_deployment|test_set_remote_target_rejects_unversioned_target_bucket|test_set_remote_target_update_requires_arn|test_set_remote_target_update_rejects_missing_target|test_set_remote_target_rejects_invalid_target_url|test_set_remote_target_rejects_self_signed_https_target_without_skip_tls_verify|test_set_remote_target_rejects_private_ca_https_target_without_ca_cert_pem|test_list_remote_targets_rejects_empty_bucket|test_list_remote_targets_rejects_invalid_bucket|test_remove_remote_target_rejects_missing_target|test_remove_remote_target_rejects_missing_arn|test_remove_remote_target_rejects_invalid_bucket|test_remove_remote_target_rejects_target_used_by_replication|test_delete_bucket_replication_removes_remote_target)$/)
|
||||||
| test(/^reliant::lifecycle::/)
|
| test(/^reliant::lifecycle::/)
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -0,0 +1,12 @@
|
|||||||
|
# Committed floor for the number of security negative-auth tests selected by the
|
||||||
|
# e2e-smoke PR profile (see scripts/check_security_smoke_count.sh, backlog#1151
|
||||||
|
# sec-5).
|
||||||
|
#
|
||||||
|
# The floor equals the exact count of e2e_test cases whose name starts with a
|
||||||
|
# security module prefix (negative_sigv4_test, presigned_negative_test,
|
||||||
|
# admin_auth_test) that the [profile.e2e-smoke] default-filter in
|
||||||
|
# .config/nextest.toml selects, at the time this file was last updated. CI fails
|
||||||
|
# if the selected count drops below this number, so a rename or removal that
|
||||||
|
# thins the security smoke gate must update this file in the same PR.
|
||||||
|
# Adding tests does not require a bump, but bumping keeps the guard tight.
|
||||||
|
16
|
||||||
@@ -435,6 +435,16 @@ jobs:
|
|||||||
- name: Make binary executable
|
- name: Make binary executable
|
||||||
run: chmod +x ./target/debug/rustfs
|
run: chmod +x ./target/debug/rustfs
|
||||||
|
|
||||||
|
# Guard the security negative-auth smoke subset (backlog#1151 sec-5)
|
||||||
|
# against a rename or deletion silently dropping it out of the e2e-smoke
|
||||||
|
# filter. The script lists what the profile selects and fails if the count
|
||||||
|
# of security auth-rejection tests falls below the committed floor in
|
||||||
|
# .config/security-smoke-floor.txt (infra-12 count-floor mechanism). Run
|
||||||
|
# before the smoke suite so a thinned gate fails fast; the `nextest list`
|
||||||
|
# here compiles the e2e_test binaries the run below reuses.
|
||||||
|
- name: Check security smoke subset count floor
|
||||||
|
run: ./scripts/check_security_smoke_count.sh check
|
||||||
|
|
||||||
# PR smoke subset of the in-repo e2e suite (backlog#1149 ci-4). The
|
# PR smoke subset of the in-repo e2e suite (backlog#1149 ci-4). The
|
||||||
# profile.e2e-smoke default-filter in .config/nextest.toml is the single
|
# profile.e2e-smoke default-filter in .config/nextest.toml is the single
|
||||||
# wiring mechanism for e2e tests in CI — extend that filter instead of
|
# wiring mechanism for e2e tests in CI — extend that filter instead of
|
||||||
|
|||||||
@@ -22,21 +22,45 @@ forced to update its pinned test (red -> green).
|
|||||||
| [GHSA-r5qv-rc46-hv8q](https://github.com/rustfs/rustfs/security/advisories/GHSA-r5qv-rc46-hv8q) | Internode RPC authentication must fail closed | rustfs/rustfs#4402 | `ghsa_r5qv_resolve_shared_secret_rejects_default_fallback`, `ghsa_r5qv_verify_rpc_signature_fails_closed_on_missing_or_invalid_auth` (`crates/ecstore/src/cluster/rpc/http_auth.rs`) | unit |
|
| [GHSA-r5qv-rc46-hv8q](https://github.com/rustfs/rustfs/security/advisories/GHSA-r5qv-rc46-hv8q) | Internode RPC authentication must fail closed | rustfs/rustfs#4402 | `ghsa_r5qv_resolve_shared_secret_rejects_default_fallback`, `ghsa_r5qv_verify_rpc_signature_fails_closed_on_missing_or_invalid_auth` (`crates/ecstore/src/cluster/rpc/http_auth.rs`) | unit |
|
||||||
| [GHSA-m77q-r63m-pj89](https://github.com/rustfs/rustfs/security/advisories/GHSA-m77q-r63m-pj89) | STS JWTs signed with shared root secret (intentionally unfixed) | n/a | `test_created_sts_credentials_authorize_with_session_token_claims` (`crates/iam/src/sys.rs`) — pins current behavior; sec-7 adds GHSA naming | unit |
|
| [GHSA-m77q-r63m-pj89](https://github.com/rustfs/rustfs/security/advisories/GHSA-m77q-r63m-pj89) | STS JWTs signed with shared root secret (intentionally unfixed) | n/a | `test_created_sts_credentials_authorize_with_session_token_claims` (`crates/iam/src/sys.rs`) — pins current behavior; sec-7 adds GHSA naming | unit |
|
||||||
|
|
||||||
## Where these run
|
## Where these run (CI-execution map)
|
||||||
|
|
||||||
- **Unit tests** (`ghsa_r5qv_*` in `crates/ecstore`) run automatically in the
|
Every security regression must land where CI actually runs it — a named test in
|
||||||
default CI pass via `cargo nextest run` / `cargo test` — no special wiring.
|
an unexecuted suite is theater. The suites split across three execution paths by
|
||||||
- **Protocol e2e tests** (WebDAV/FTPS constant-time login rejection) live in the
|
topology:
|
||||||
`e2e_test` protocols suite, which binds **fixed ports** and requires
|
|
||||||
`--test-threads=1`. Because of the fixed ports they **cannot** join the
|
- **Unit tests** — `ghsa_r5qv_*` (`crates/ecstore`) and the GHSA-m77q STS
|
||||||
`e2e-smoke` nextest profile (parallel, dynamic ports). They run manually today:
|
pinning (`crates/iam`) run automatically in the default CI pass
|
||||||
|
(`cargo nextest run --profile ci --all --exclude e2e_test`) — no special
|
||||||
|
wiring. This is the CI-executed regression for the RPC fail-closed (r5qv) and
|
||||||
|
STS-signing (m77q) advisories.
|
||||||
|
- **S3-API negative-auth e2e (e2e-smoke, PR-gated)** — the attacker-facing S3
|
||||||
|
auth-rejection suites run on every PR via the `e2e-smoke` nextest profile
|
||||||
|
(`.config/nextest.toml`), which each spawns its own server on a random port
|
||||||
|
and is parallel-safe:
|
||||||
|
- `negative_sigv4_test` — tampered/wrong-key/skewed header SigV4 (sec-1)
|
||||||
|
- `presigned_negative_test` — expired/tampered/wrong-key presigned URLs (sec-2)
|
||||||
|
- `admin_auth_test` — non-admin denial + root-credential lifecycle (sec-4)
|
||||||
|
|
||||||
|
A count-floor guard (`scripts/check_security_smoke_count.sh`, floor in
|
||||||
|
`.config/security-smoke-floor.txt`) runs in the `e2e-tests` CI job and fails if
|
||||||
|
a rename drops any of these out of the smoke filter (infra-12 mechanism). This
|
||||||
|
is the sec-5 wiring: those merged suites were dead weight until listed here.
|
||||||
|
- **Protocol e2e (WebDAV/FTPS constant-time login, GHSA-3p3x)** — lives in the
|
||||||
|
`e2e_test` protocols suite (`test_protocol_core_suite`), which binds **fixed
|
||||||
|
ports**, needs the `ftps,webdav` build features, and is `#[serial]`. Those
|
||||||
|
three properties make it **structurally incompatible** with the random-port,
|
||||||
|
default-feature, parallel `e2e-smoke` profile: it cannot be a filterset change,
|
||||||
|
so sec-5 (scoped to a filterset change, global ruling G5) does not wire it.
|
||||||
|
It runs manually today:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
RUSTFS_BUILD_FEATURES=ftps,webdav cargo test --package e2e_test \
|
RUSTFS_BUILD_FEATURES=ftps,webdav cargo test --package e2e_test \
|
||||||
test_protocol_core_suite -- --test-threads=1 --nocapture
|
test_protocol_core_suite -- --test-threads=1 --nocapture
|
||||||
```
|
```
|
||||||
|
|
||||||
Wiring the security e2e lane into CI is tracked separately (sec-5, backlog#1151).
|
**Open gap:** GHSA-3p3x's *e2e* layer has no PR-gated CI execution. A protocols
|
||||||
|
e2e CI lane is a ci-domain concern (a new profile/job, not a filterset change);
|
||||||
|
it is deliberately out of sec-5's boundary and left as a follow-up.
|
||||||
|
|
||||||
## Adding a new advisory guard
|
## Adding a new advisory guard
|
||||||
|
|
||||||
@@ -44,5 +68,8 @@ forced to update its pinned test (red -> green).
|
|||||||
2. Name the test (or the helper/assertion) `ghsa_<id>_*`, or attach a
|
2. Name the test (or the helper/assertion) `ghsa_<id>_*`, or attach a
|
||||||
`GHSA-<id>` doc comment with the advisory URL and fix PR.
|
`GHSA-<id>` doc comment with the advisory URL and fix PR.
|
||||||
3. Add a row to the table above.
|
3. Add a row to the table above.
|
||||||
4. Land the unit-level guard where the default CI pass runs it; land any e2e
|
4. Land it where CI runs it (see the map above): a unit guard in the default CI
|
||||||
guard in the protocols suite (and register it with sec-5's filterset seam).
|
pass; an S3-API negative-auth e2e in the `e2e-smoke` filter (add the module to
|
||||||
|
`.config/nextest.toml` and bump `.config/security-smoke-floor.txt`); a
|
||||||
|
fixed-port protocol e2e in the protocols suite (still manual — see the open
|
||||||
|
gap above).
|
||||||
|
|||||||
Executable
+85
@@ -0,0 +1,85 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# Count-floor guard for the security negative-auth smoke subset (backlog#1151
|
||||||
|
# sec-5), built on the infra-12 mechanism (scripts/check_migration_gate_count.sh,
|
||||||
|
# backlog#1153).
|
||||||
|
#
|
||||||
|
# The e2e-smoke profile in .config/nextest.toml selects tests BY MODULE NAME.
|
||||||
|
# The three attacker-facing S3 auth-rejection suites — negative_sigv4_test
|
||||||
|
# (header SigV4, sec-1), presigned_negative_test (presigned URL, sec-2), and
|
||||||
|
# admin_auth_test (admin gate + root-credential lifecycle, sec-4) — are wired
|
||||||
|
# into that filter so they run on every PR. A rename that drops a suite out of
|
||||||
|
# the filter (or a deletion) silently thins the security gate to nothing with no
|
||||||
|
# CI signal, exactly the failure mode the migration-gate guard was built for.
|
||||||
|
#
|
||||||
|
# This script lists what the e2e-smoke profile ACTUALLY selects and counts the
|
||||||
|
# security suites among them, then fails when that number drops below the
|
||||||
|
# committed floor in .config/security-smoke-floor.txt. Because it lists via the
|
||||||
|
# profile (not a standalone name filter), it verifies real smoke MEMBERSHIP: a
|
||||||
|
# suite renamed without updating the nextest.toml filter stops being selected,
|
||||||
|
# its tests vanish from the listing, and the count falls below the floor. To
|
||||||
|
# shrink the subset intentionally, update the floor file in the same PR so the
|
||||||
|
# reduction is explicit and reviewable (adding tests never requires a bump —
|
||||||
|
# the floor is a lower bound — but bumping keeps the guard tight).
|
||||||
|
#
|
||||||
|
# NAMING CONVENTION DEPENDENCY: the security suites are matched by these
|
||||||
|
# reserved module-name prefixes:
|
||||||
|
#
|
||||||
|
# negative_sigv4_test, presigned_negative_test, admin_auth_test
|
||||||
|
#
|
||||||
|
# Renaming a suite away from all of these prefixes removes it from the count.
|
||||||
|
# Keep this regex in sync with the module names in the e2e-smoke filter.
|
||||||
|
#
|
||||||
|
# Usage:
|
||||||
|
# scripts/check_security_smoke_count.sh # count check (alias of check)
|
||||||
|
# scripts/check_security_smoke_count.sh check # count check only
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
cd "$(dirname "$0")/.."
|
||||||
|
|
||||||
|
# Anchored module-name prefixes of the security negative-auth smoke subset.
|
||||||
|
# Kept deliberately in lockstep with the e2e-smoke filter in .config/nextest.toml.
|
||||||
|
SECURITY_SMOKE_REGEX='^(negative_sigv4|presigned_negative|admin_auth)_test::'
|
||||||
|
FLOOR_FILE=".config/security-smoke-floor.txt"
|
||||||
|
|
||||||
|
mode="${1:-check}"
|
||||||
|
case "$mode" in
|
||||||
|
all | check) ;;
|
||||||
|
*)
|
||||||
|
echo "usage: $0 [check]" >&2
|
||||||
|
exit 2
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
floor="$(grep -Ev '^[[:space:]]*(#|$)' "$FLOOR_FILE" | head -n1 | tr -d '[:space:]')"
|
||||||
|
if ! [[ "$floor" =~ ^[0-9]+$ ]]; then
|
||||||
|
echo "error: $FLOOR_FILE does not contain a numeric floor (got: '$floor')" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# List via the e2e-smoke PROFILE so the default-filter is applied: only the
|
||||||
|
# tests the PR smoke suite would actually run appear in the output. Count the
|
||||||
|
# ones whose test name starts with a security module prefix. The structured JSON
|
||||||
|
# listing is used (not the human format) for the same reason infra-12 does: the
|
||||||
|
# human format's indentation varies across nextest versions; a JSON schema change
|
||||||
|
# makes jq fail loudly rather than silently collapsing the count to zero.
|
||||||
|
count="$(cargo nextest list --profile e2e-smoke -p e2e_test --message-format json \
|
||||||
|
| jq --arg re "$SECURITY_SMOKE_REGEX" \
|
||||||
|
'[."rust-suites"[].testcases | to_entries[] | select(.key | test($re))] | length')"
|
||||||
|
if ! [[ "$count" =~ ^[0-9]+$ ]]; then
|
||||||
|
echo "error: could not parse nextest JSON listing (got count: '$count')" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if ((count < floor)); then
|
||||||
|
echo "error: e2e-smoke selects $count security auth-rejection tests, below the committed floor of $floor." >&2
|
||||||
|
echo "" >&2
|
||||||
|
echo "The security smoke subset is matched by module name (negative_sigv4_test /" >&2
|
||||||
|
echo "presigned_negative_test / admin_auth_test) in the e2e-smoke default-filter" >&2
|
||||||
|
echo "(.config/nextest.toml). A rename or removal has dropped one out of the smoke" >&2
|
||||||
|
echo "suite. Either restore the module names (and this script's regex), or — if the" >&2
|
||||||
|
echo "reduction is intentional — update $FLOOR_FILE in this PR so the" >&2
|
||||||
|
echo "security-gate change is explicit and reviewable." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
echo "security smoke count OK: $count selected auth-rejection tests (floor: $floor)"
|
||||||
Reference in New Issue
Block a user