Skip to content

fix(security): AWS SigV4 headers are credential slots; full-version image tags only; count unscannable-body bypasses - #1126

Merged
jarvis9443 merged 5 commits into
mainfrom
ci/drop-semver-abbrev-tags
Sep 3, 2026
Merged

jarvis9443 merged 5 commits into
mainfrom
ci/drop-semver-abbrev-tags

Conversation

@jarvis9443

@jarvis9443 jarvis9443 commented Sep 3, 2026 •

Copy link
Copy Markdown
Contributor

Three independent changes on the 1.0.0 critical path, batched into one PR per this repo's one-PR-per-repository rule.


1. Security: the AWS SigV4 headers are credential slots again

This is the important one. x-amz-security-token, x-amz-date and x-amz-content-sha256 were blocked outright by RESERVED_UPSTREAM_HEADERS in config.rs. When the forwarding rules moved into crates/aisix-core/src/forwarded_headers.rs, six of that constant's names became CREDENTIAL_SLOT_HEADERS — the set a glob cannot reach — and the three x-amz-* names landed in no list at all: not the credential slots, not NON_FORWARDABLE_HEADERS, not NEVER_FORWARD_FROM_CLIENT.

A provider key configured forward_client_headers: ["x-*"] therefore relayed the caller's AWS session token to the upstream. That contradicts the module's own stated rule — a glob is a statement about the operator's own headers, not consent to hand a third party the caller's credential.

Bedrock upstreams were never exposed: the SigV4 signer owns all three names and drops any supplied value before signing. The reach was every other upstream, which is exactly where an AWS credential has no business arriving.

The fix restores the credential-slot rule, not the old absolute block: a glob does not reach the three, and an operator who names one exactly still forwards it — identical to how the other six behave, and strictly more capable than the pre-split behaviour, which allowed neither.

provider_key and passthrough_route enumerate the credential slots in the field descriptions the control plane and the published API reference read, so both gain the three names and the schemas are regenerated. mcp_server refers to "a credential slot" without enumerating and needed no change.

Scope note. No released build ever shipped the glob-reachable state — it existed only between the refactor and this commit, both after v0.13.0. This is a defect fixed before release, not an advisory: no user could have observed it, and the release notes should not describe it as a behaviour change.

Testing. The tests name the three headers explicitly rather than iterating the constant. A loop over CREDENTIAL_SLOT_HEADERS can only confirm that whatever is in the list behaves and can never notice a name that dropped out of it — which is precisely how this went unseen. Both new tests go red without the fix; the gateway-level one fails by showing all three headers arriving at the upstream under ["x-*"], and also pins the positive direction (an exactly-named x-amz-security-token still forwards).


2. Image tags: publish the full version only, no :X.Y or :X


A release tag push published :X.Y.Z, :X.Y, :X and :latest. The two abbreviations are snapshots dressed as lines: :1 published at 1.0.0 stops moving the day 1.1.0 ships, so anyone who pinned it silently keeps an old image under a name that suggests it tracks the major. The full version and :latest are the two honest answers.

The project is about to cut its first non-0.x version, which is the first time a :1 would be created — and the workflow that runs on a tag push is the one at the tagged commit, so this has to be on main before that tag point is chosen.

.github/workflows/docker-image.yml — the metadata-action tags: block drops:

type=semver,pattern={{major}}.{{minor}}
type=semver,pattern={{major}}

type=semver,pattern={{version}}, type=raw,value=latest, type=raw,value=dev, type=sha,prefix=sha- and type=ref,event=pr are unchanged. The tag-strategy comment at the top of the file and the comment above the block are updated to match.

Two places that described or advertised the removed tags:

  • .github/workflows/release-draft.yml stamps a rolling-tags sentence into every stable release body. It named the :X.Y tag, which would otherwise be advertised on the release page while never being pushed. It now names latest only; the -rc branch is unchanged.
  • RELEASING.md listed the published tag set.

Publish paths swept. docker/metadata-action is invoked in exactly one place (docker-image.yml); it is the only workflow that builds or pushes an image, and it covers both the GHCR publish and the Docker Hub mirror. release-draft.yml mentions the action only in prose. No other tag-publishing path exists in the repo, and nothing in the repo pins an abbreviated tag.

Behavior change. A release tag now publishes :X.Y.Z, :latest and :sha-<short> to GHCR and the Docker Hub mirror — no :X.Y, no :X. Already-published abbreviations are untouched; this only stops new ones being created. Anything pinning an abbreviated tag must move to the full version or :latest.

Testing. CI-only with no runtime behavior, so there is nothing for a fail-before/pass-after test to bite on, and no test or linter in the repo asserts the published tag set. Verified by inspection instead: both workflows still parse, the edited release-draft.yml shell block still passes bash -n, and the downstream steps consuming steps.meta.outputs.tags are unaffected by there being fewer tags — the release-path steps address the image by digest, the PR-path steps take head -n1, and the cosign step dedups per repository and signs the digest.


3. aisix_guardrail_bypasses_total counts an unscannable-body bypass

aisix_guardrail_bypasses_total only ever counted bypasses a guardrail member reported: the increment hangs off the per-execution record, and an unscannable body has no execution by construction — the body could not be decoded, so no member ran. The chain recorded that pass-through to the audit log (and so to UsageEvent.guardrail_bypassed_reason) and to nothing else.

The sibling counter is the other way round: aisix_guardrail_blocks_total deliberately includes fail-closed paths that happen before a member executes. So one situation — the body could not be scanned — was counted when the chain refused and counted nowhere when it let the request through, which is the direction an operator reads bypasses_total to find. #1115 widened exactly that fail-open half in this release, so the traffic newly passing through was the traffic the counter could not see.

GuardrailMetricsSink gains record_guardrail_bypass(reason), and GuardrailChain::record_bypass now feeds both receivers. A second trait method rather than a synthetic execution, deliberately: there is no member, kind or duration to report, and inventing them would put a phantom row in the per-execution latency histogram. Required rather than defaulted, so a sink cannot silently drop these.

Which pass-throughs count is unchanged and still decided in one place. record_unevaluable_{input,output}_bypass already distinguishes a chain whose members all fail open on that side (a bypass) from one where no member reads that side (never offered to screen it, so not a bypass), read off the same member set the refusal gate uses. The counter reads that decision rather than deriving its own.

Behavior change

aisix_guardrail_bypasses_total now also counts bypasses that happen before any guardrail member executes — today that is reason="unscannable_body", a body the gateway could not give the guardrails at all, which a fail-open chain lets through. The reason label gains that value; it is a bounded DP-internal tag and is clamped on the way in, so cardinality is unchanged in kind.

Operators are affected. The reason="unscannable_body" series was always zero for this cause and starts being non-zero, and the unlabelled total rises accordingly on any deployment that receives bodies the scanner cannot read. An alert threshold on aisix_guardrail_bypasses_total may fire on traffic that was always passing unscreened and simply was not counted. Nothing about which traffic is allowed or refused changes — this is a counting fix only.

Two limits are now stated where the counter is declared, because the change makes both easier to get wrong:

  • blocks_total and bypasses_total are not the same unit and must not be summed or compared. The first fires once per request, off the terminal usage event; the second fires once per bypass event, so one outage on a chain with two fail-open members on both hooks adds 4 for a single request.
  • The new producer reaches this counter only. With no member execution there is no aisix_guardrail_latency_seconds row, so that family's result="bypassed" slice stays execution-only and the two no longer agree on a total.
  • The counter still does not cover every unscannable pass-through: jobs::scan_output_blob and passthrough_route scan a lossy copy with no failure policy involved and record nothing by design, so a zero is not proof that nothing went unread.

Follow-up owed in this effort

For the credential-slot fix (change 1). The credential-slot names are enumerated verbatim in six places in AISIX-Cloud: openapi/cp-admin.yaml (provider_key default_headers + forward_client_headers, mcp_server create/update, passthrough_route ×2) and in four pages in each docs repo (models/upstream-request-headers.md, mcp-gateway/upstream-authentication.md, reference/resources-file.md ×2). All still list six names. This is description-only drift — the CP's internal/cpapi/resources/forward_client_headers.go mirrors the transport block lists and deliberately keeps no credential names, so the CP accepts x-amz-security-token in forward_client_headers, projects it, and the DP forwards it on an exact match. Nothing is functionally broken; the published contract just under-describes which names a glob no longer reaches. Tracked outside this PR.

The published metric reference and the guardrail behaviour guide contradict this change and need the paired EN + ZH docs PRs (api7/docs and api7/docs.apiseven.com): docs/ai-gateway/traffic-controls/guardrails/behavior.md states that a proxy-raised unscannable_body pass "reaches the usage record only", and src/components/MetricReference/data.ts describes the counter as timed fail-open executions and enumerates a closed reason list that omits unscannable_body. Tracked outside this PR.

Testing

The chain unit test that pins which pass-throughs are bypasses now asserts both sinks, so the usage field and the counter cannot disagree. All three assertions were verified to go red under mutation: dropping the sink forwarding, counting every pass-through, and letting the counter derive its own predicate off a separate member set.

The bypass-reason e2e additionally asserts the reason="unscannable_body" delta in GET /metrics after driving a real unscannable request through a real DP — the positive direction only; the discrimination between a bypass and a never-offered pass-through is pinned in the unit test, where an output-only chain can be built without seeding a second route.

Summary by CodeRabbit

  • Changes
    • Docker releases now publish full semantic version tags, along with latest and SHA-based tags.
    • Major and minor version aliases are no longer published; release messaging and documentation reflect this policy.
    • Guardrail bypasses now record bounded reasons in audit logs and metrics, including unevaluable request bodies.
    • Bypass metrics are verified alongside successful request handling and usage events.
    • AWS SigV4 credential headers require explicit, exact-name forwarding and are no longer forwarded through broad wildcard patterns.

A release tag published `:X.Y.Z`, `:X.Y`, `:X` and `:latest`. The two
abbreviations are removed; `:latest`, `:dev`, `:sha-<short>` and PR refs
are unchanged.

An abbreviated tag reads like a line but is a snapshot: `:1` published at
1.0.0 stops moving the day 1.1.0 ships, so anyone who pinned it silently
keeps an old image under a name that suggests otherwise. The full version
and `:latest` are the two honest answers, and the project is about to cut
its first non-0.x version, which is when a `:1` would first be created.

Also removes the `:X.Y` sentence the release-draft workflow stamps into
every stable release body, and the tag list in RELEASING.md — both would
otherwise advertise tags that are no longer published.

Existing published abbreviations are unaffected; this only stops new ones
being created.
Copilot AI lite review requested due to automatic review settings September 3, 2026 11:44
@coderabbitai

coderabbitai Bot commented Sep 3, 2026 •

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Essentials

Run ID: 151bcfb3-0518-4465-8c5b-fe8535ca37ec

📥 Commits

Reviewing files that changed from the base of the PR and between c42a191 and 5708a19.

📒 Files selected for processing (9)
  • crates/aisix-core/src/forwarded_headers.rs
  • crates/aisix-core/src/models/passthrough_route.rs
  • crates/aisix-core/src/models/provider_key.rs
  • crates/aisix-gateway/src/upstream_headers.rs
  • crates/aisix-guardrails/src/chain.rs
  • crates/aisix-obs/src/metrics.rs
  • schemas/resources/passthrough_route.schema.json
  • schemas/resources/provider_key.schema.json
  • tests/e2e/src/cases/forward-client-headers-e2e.test.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • crates/aisix-guardrails/src/chain.rs

Included review availability: Your plan provides up to 5 included reviews per hour; 0 remain after this review.


📝 Walkthrough

Walkthrough

The release configuration removes Docker X and X.Y alias tags. Stable releases publish full X.Y.Z, latest, and SHA tags. Guardrail bypasses now record bounded reasons in audit logs and metrics. AWS SigV4 headers require exact forwarding patterns.

Changes

Docker tag strategy

Layer / File(s) Summary
Image metadata tag generation
.github/workflows/docker-image.yml
Docker metadata now publishes only the full semantic version. The workflow documentation reflects this tag strategy.
Release messaging and documentation
.github/workflows/release-draft.yml, RELEASING.md
Stable release messages and release documentation now advertise full-version, latest, and SHA tags. Prerelease messaging remains unchanged.

Guardrail bypass metrics

Layer / File(s) Summary
Bypass metrics contract and recording
crates/aisix-core/src/models/guardrail.rs, crates/aisix-guardrails/src/chain.rs, crates/aisix-obs/src/metrics.rs
GuardrailMetricsSink now accepts proxy-level bypass reasons. Guardrail chains record bounded reasons in audit logs and metrics. Observability uses one counter helper for execution and pre-execution bypasses.
Bypass metrics validation
crates/aisix-guardrails/src/build.rs, crates/aisix-guardrails/src/chain.rs, tests/e2e/src/cases/guardrail-bypass-reason-e2e.test.ts
Test sinks support bypass events. Unit tests verify fail-open and fail-closed behavior. The end-to-end test verifies the unscannable_body metric increment.

AWS SigV4 header forwarding

Layer / File(s) Summary
Credential-slot contract
crates/aisix-core/src/forwarded_headers.rs, crates/aisix-core/src/models/*.rs, schemas/resources/*.json
AWS SigV4 credential headers are documented as credential slots that require exact forwarding patterns.
SigV4 forwarding validation
crates/aisix-core/src/forwarded_headers.rs, crates/aisix-gateway/src/upstream_headers.rs, tests/e2e/src/cases/forward-client-headers-e2e.test.ts
Tests verify that broad x-* patterns do not forward AWS SigV4 headers, while exact names permit forwarding.

Estimated code review effort: 3 (Moderate) | ~30 minutes

Merge Risk: ⚪ Minimal · up to 5708a

This change protects AWS SigV4 credential headers from wildcard forwarding while retaining explicit forwarding, records unscannable-body bypasses in metrics, and removes abbreviated release image tags. No current merge-blocking risk remains.

Suggested reviewers: moonming

🚥 Pre-merge checks | ✅ 6
✅ Passed checks (6 passed)
Check name Status Explanation
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
E2e Test Quality Review ✅ Passed PASS — The changed runtime behavior has end-to-end coverage. The guardrail test drives a real gateway with etcd, a real local upstream, and an SLS exporter, then checks successful responses, usage-eve…
Security Check ✅ Passed PASS — the diff introduces no finding under the seven stated security categories. 1. Sensitive data exposure: No new logging, response serialization, audit serialization, or secret-bearing object form…
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes all three primary changes: AWS SigV4 credential-slot handling, removal of abbreviated image tags, and counting unscannable-body bypasses.
Full details: E2e Test Quality Review

Explanation

PASS — The changed runtime behavior has end-to-end coverage. The guardrail test drives a real gateway with etcd, a real local upstream, and an SLS exporter, then checks successful responses, usage-event reasons, and the aisix_guardrail_bypasses_total delta. The header test drives real gateway requests and inspects headers received by the upstream, including all three AWS SigV4 headers under x-*. Assertions use structured values and clear test names. Fixtures use unique models and explicit readiness gates, so tests do not rely on execution order. Local upstream/SLS servers are scoped test dependencies and are justified for deterministic external-service inspection. Docker tag changes are CI workflow changes, so API E2E coverage is not applicable.

Full details: Security Check

Explanation

PASS — the diff introduces no finding under the seven stated security categories. 1. Sensitive data exposure: No new logging, response serialization, audit serialization, or secret-bearing object formatting was added. AWS SigV4 headers are blocked for wildcard forwarding and exact forwarding requires explicit operator configuration. Forwarded values are marked sensitive. 2. Unencrypted database secrets: No database model, migration, or persistence code changed. 3. Authorization bypass: No endpoint, permission check, or error-response control flow changed. 4. Cross-resource access: No resource lookup, ownership validation, or parent-scope query changed. 5. TLS/cryptographic errors: No TLS configuration or certificate handling changed. 6. Shared-resource safety: No shared-resource binding or cascade operation changed. 7. Secret reference resolution: No configuration resolution path changed. The guardrail metric addition passes bounded reasons to audit and metrics. The Docker and release workflow changes alter image-tag prose and tag patterns only; permissions and secret handling are unchanged. The changed-file diff confirms that the AWS header list and guardrail telemetry paths are the only security-relevant implementation changes, and both changes reduce uncontrolled credential forwarding or add bounded observability rather than introduce a listed vulnerability.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch ci/drop-semver-abbrev-tags

Comment @coderabbitai help to get the list of available commands.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟢 Approval recommended

The changes are limited to CI/workflow/docs, and the updated workflows no longer reference or depend on the removed abbreviated image tags.

Pull request overview

This PR updates the container image publishing strategy so releases only publish “honest” moving/stable tags: the full version (X.Y.Z) plus latest (stable only) and sha-<short>, removing the misleading abbreviated semver tags (:X.Y, :X) from the release workflow.

Changes:

  • Remove docker/metadata-action semver tag patterns for {{major}}.{{minor}} and {{major}}, leaving only {{version}} (plus existing latest, dev, sha-, and PR/ref tags).
  • Update workflow comments and release text generation to stop advertising :X.Y / :X tags.
  • Update release process documentation to reflect the new published tag set.
File summaries
File Description
RELEASING.md Updates release documentation to reflect that only full version + latest + sha-<short> are published.
.github/workflows/release-draft.yml Adjusts the generated rolling-tags sentence so stable releases advertise latest only (no X.Y).
.github/workflows/docker-image.yml Removes major/minor semver tag publishing and updates tag-strategy documentation/comments accordingly.
Review details
  • Files reviewed: 3/3 changed files
  • Comments generated: 0
  • Review effort level: Lite

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

…s_total

`aisix_guardrail_bypasses_total` only ever counted bypasses a guardrail
member reported: the increment hangs off the per-execution record, and an
unscannable body has no execution by construction — the body could not be
decoded, so no member ran. The chain recorded that pass-through to the
audit log (and so to `UsageEvent.guardrail_bypassed_reason`) and to
nothing else.

The sibling counter is the other way round: `aisix_guardrail_blocks_total`
deliberately includes the fail-closed paths that happen before a member
executes. So one situation — the body could not be scanned — was counted
when the chain refused and counted nowhere when it let the request
through, which is the direction an operator reads `bypasses_total` to
find. #1115 widened exactly that fail-open half, so the traffic newly
passing through was the traffic the counter could not see.

`GuardrailMetricsSink` gains `record_guardrail_bypass(reason)` and
`GuardrailChain::record_bypass` now feeds both receivers. Deliberately a
second method rather than a synthetic execution: there is no member, kind
or duration to report, and inventing them would put a phantom row in the
per-execution latency histogram. It is required rather than defaulted so
a sink cannot silently drop these.

Which pass-throughs count is unchanged and is still decided in one place.
`record_unevaluable_{input,output}_bypass` already distinguishes a chain
whose members all fail open on that side (a bypass) from one where no
member reads that side (never offered to screen it, so not a bypass), off
the same member set the refusal gate uses. The counter reads that
decision rather than deriving its own.

Tests: the chain unit test that pins the distinction now asserts both
sinks, so the usage field and the counter cannot disagree about which
pass-through was a bypass. All three assertions were verified to go red
under mutation — dropping the sink forwarding, counting every
pass-through, and letting the counter derive its own predicate. The
bypass-reason e2e additionally asserts the `reason="unscannable_body"`
delta in `GET /metrics` after driving a real unscannable request.
@jarvis9443 jarvis9443 changed the title ci: publish the full version image tag only, no :X.Y or :X ci: publish the full version image tag only; count unscannable-body bypasses Sep 3, 2026
@nic-6443
nic-6443 requested a lite review from Copilot September 3, 2026 12:01
@jarvis9443

Copy link
Copy Markdown
Contributor Author

@coderabbitai the E2e Test Quality Review warning is looking at the wrong base. 7297b004 is this branch's merge base — it is origin/main's current HEAD, not a commit in this PR — so the CLAUDE.md change it names was already on main before the branch existed and is not part of this diff:

$ git merge-base origin/main HEAD
7297b004ba42c7ba51b457c98d444c2e84b72fe8
$ git log --oneline origin/main -1
7297b004 docs: an absent optional config block means the feature is off (#1124)
$ git diff --name-only origin/main...HEAD
.github/workflows/docker-image.yml
.github/workflows/release-draft.yml
RELEASING.md
crates/aisix-core/src/models/guardrail.rs
crates/aisix-guardrails/src/build.rs
crates/aisix-guardrails/src/chain.rs
crates/aisix-obs/src/metrics.rs
tests/e2e/src/cases/guardrail-bypass-reason-e2e.test.ts

There is nothing to remove or revert — the suggested resolution would drop a commit off main. The two changes actually in the PR are batched deliberately: this repo's contributing rules ask for one PR per repository per effort rather than splitting related work across several bot-reviewed PRs.

@coderabbitai

coderabbitai Bot commented Sep 3, 2026

Copy link
Copy Markdown

Tip

For best results, initiate chat on the files or code changes.

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '--- merge base ---'
git merge-base origin/main HEAD

printf '%s\n' '--- origin/main HEAD ---'
git rev-parse origin/main
git log -1 --format='%H%n%s' origin/main

printf '%s\n' '--- files in origin/main...HEAD ---'
git diff --name-only origin/main...HEAD

printf '%s\n' '--- CLAUDE.md status in the merge-base diff ---'
git diff --name-status origin/main...HEAD -- CLAUDE.md

printf '%s\n' '--- commits introduced by HEAD relative to origin/main ---'
git log --oneline origin/main..HEAD

Length of output: 1491


@jarvis9443: Verified. 7297b004 is both the merge base and origin/main HEAD. CLAUDE.md is not in the origin/main...HEAD diff.

The E2E Test Quality Review warning used a change from main as a PR change. I withdraw the warning. The two commits in this PR are the actual PR changes.

🐇🔍


✏️ Learnings added
Learnt from: jarvis9443
URL: https://github.com/api7/aisix/pull/1126

Timestamp: 2026-09-03T12:02:42.509Z
Learning: In the `api7/aisix` repository, related changes for one repository effort can be batched in one pull request. Do not require splitting such changes into separate PRs solely because they affect multiple related areas.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.

You are interacting with an AI system.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟢 Approval recommended

The changes are cohesive with the stated behavior changes, the new required trait method is implemented at all in-repo call sites, and unit/e2e coverage was extended to prevent audit-vs-metrics drift.

Review details
  • Files reviewed: 8/8 changed files
  • Comments generated: 0 new
  • Review effort level: Lite

Review findings on the doc comments added by the previous commit. All
three were prose that overstated what the counter says; no behaviour
changes.

- `blocks_total` and `bypasses_total` are not the same unit, so calling
  them counterparts invited summing or comparing them. The first fires
  once per REQUEST off the terminal usage event; the second fires once per
  bypass EVENT, so one outage on a chain with two fail-open members on
  both hooks adds 4 for a single request.
- "a body the gateway could not give the guardrails at all" claimed the
  counter covers every unscannable pass-through. It does not:
  `jobs::scan_output_blob` and `passthrough_route` scan a lossy copy with
  no failure policy involved and record nothing by design
  (crates/aisix-proxy/AGENTS.md), so a zero here is not proof that
  nothing went unread.
- The pre-execution producer reaches this counter only. With no execution
  there is no `aisix_guardrail_latency_seconds` row, so that family's
  `result="bypassed"` slice stays execution-only and the two no longer
  agree on a total — worth saying where both are documented together.

Also corrects the claim in `GuardrailChain::record_bypass` that the two
producers cannot both fire on one request. No EVENT is counted twice, but
a request can legitimately produce both: `audio.rs` records an
undecodable transcript tail and then scans the decodable remainder, whose
members may themselves fail open.
Three headers became reachable by an ordinary glob when the forwarding
rules moved out of `config.rs`. `RESERVED_UPSTREAM_HEADERS` used to block
`x-amz-security-token`, `x-amz-date` and `x-amz-content-sha256` outright.
The split into `forwarded_headers` kept six of that constant's names as
`CREDENTIAL_SLOT_HEADERS`, which a glob cannot reach, and the three
`x-amz-*` names landed in no list at all — not the credential slots, not
`NON_FORWARDABLE_HEADERS`, not `NEVER_FORWARD_FROM_CLIENT`.

So a provider key configured `forward_client_headers: ["x-*"]` relayed
the caller's AWS session token to the upstream. That contradicts the
module's own rule, that a glob is a statement about the operator's own
headers and not consent to hand a third party the caller's credential.
Bedrock upstreams were never exposed — the SigV4 signer owns all three
names and drops any supplied value before signing — so the reach was
every OTHER upstream, which is where an AWS credential has no business
arriving.

The fix restores the credential-SLOT rule rather than the old absolute
block: a glob does not reach the three, and an operator who names one
exactly still forwards it, exactly as the other six behave. That keeps
the capability available to an upstream that genuinely reads the caller's
SigV4 headers, which the pre-split behaviour did not allow.

`provider_key` and `passthrough_route` enumerate the credential slots in
the field descriptions the control plane and the public API reference
read, so both gain the three names and the schemas are regenerated.
`mcp_server` says "a credential slot" without enumerating and needed no
change.

Tests name the three headers explicitly rather than iterating the
constant — a loop over `CREDENTIAL_SLOT_HEADERS` can only confirm that
whatever is in the list behaves, and can never notice a name that dropped
out of it, which is exactly how this went unseen. Both go red without the
fix, the gateway-level one by showing all three arriving at the upstream
under `["x-*"]`.
@jarvis9443 jarvis9443 changed the title ci: publish the full version image tag only; count unscannable-body bypasses fix(security): AWS SigV4 headers are credential slots; full-version image tags only; count unscannable-body bypasses Sep 3, 2026
@nic-6443
nic-6443 requested a lite review from Copilot September 3, 2026 12:37

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟢 Approval recommended

The changes are cohesive, fully covered by targeted unit/e2e tests for the security and metrics behavior, and the workflow/tag policy updates are consistent across all repo publish/documentation touchpoints.

Review details
  • Files reviewed: 14/14 changed files
  • Comments generated: 0 new
  • Review effort level: Lite

Review findings on the previous commit.

The SigV4 fix was pinned only at the unit layer, while the bug's
observable form is an upstream actually receiving the caller's AWS
session token. `forward-client-headers-e2e` already drives a ProviderKey
configured `X-*` against a real upstream, so the three names join the
request it already makes and the assertion says they must not arrive.
Verified to fail without the fix, reporting the token's value at the
upstream.

That fixture had no coverage of a credential slot being refused by a glob
at all — it asserted only the positive direction, an exactly-named
`x-api-key` forwarding — so this closes a gap wider than the one commit.

Also corrects two counts the previous commit left stale: the module
header still enumerated six credential slots, and `exact_match_only_with`
said "the two" shared names a passthrough route's own slots may take,
which is five once the trio is on the shared list. The claim around it is
unaffected — those names are all on the shared list, so the rule stays
complete — only the number was wrong.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 Needs a closer look

It combines security-sensitive header-forwarding behavior changes with release publishing workflow changes and core observability plumbing, warranting final human review despite strong test coverage.

Review details
  • Files reviewed: 15/15 changed files
  • Comments generated: 0 new
  • Review effort level: Lite

@jarvis9443
jarvis9443 merged commit d211774 into main Sep 3, 2026
16 checks passed
@jarvis9443
jarvis9443 deleted the ci/drop-semver-abbrev-tags branch September 3, 2026 13:22
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.

2 participants