Problem/Motivation

Split out of #3613761, where the design was settled. Dual-venue record for GitHub #30.

The shared secret is a bearer credential whose scope is everything gated. Anyone holding it can mint a working download URL for any gated file whose UUID they can obtain, regardless of who they are. Mint authorizes nothing — that is deliberate, but it means the secret's blast radius is the whole corpus.

Proposed resolution

Secrets stay out of configuration. Config holds secret_id → scope; settings.php supplies secret_id → value. Preserving "the secret never appears in exported configuration" is the constraint not to trade away — it is why the current single secret is environment-injected.

Scope by field, not by media bundle. Gating is declared on the field storage, and FileGateResolver::getGateForFile() already keys on entity_type.field_name. Bundle scoping needs a second lookup and does not match how gating is declared.

Identify the secret by basic-auth username. SharedSecretAuthTrait already reads basic auth, so username = secret id and password = secret. No new header, and it avoids a constant-time comparison against every configured secret on every request.

The signed URL needs a key id. The mint credential and the HMAC signing key are the same value today (GrantSigner::compute()). With several secrets, redemption must know which to verify against — so k=<id> in the query, like a JWT kid. Absent k falls back to the legacy single secret, which is the compatible path for existing installs. Keep the ids opaque: an id naming the tier that holds it tells an attacker where to aim.

Enforce scope at redemption, not only at mint. This is the load-bearing decision. Checked only at mint, narrowing a secret leaves every outstanding grant valid until it expires. Checked at redemption, narrowing behaves like revocation — which is what an operator narrowing scope actually wants, particularly if they are narrowing because they suspect a leak.

Rotation then falls out for free: add a new secret, keep the old one for verification only, drop it later.

Remaining tasks

  • Decide what happens to a grant signed by a secret since deleted rather than narrowed. Suggest treating it as unverifiable and therefore denied — deleting a secret should be at least as strong as narrowing one.
  • Decide whether a named secret with no scope entry means "everything" or "nothing". Leaning: "everything" only for the legacy no-k path, and "nothing" for any named secret, so adding a secret without scoping it is an obvious mistake rather than a silent grant of the whole corpus.

API changes

Additive. The URL gains an optional k parameter; existing single-secret installs keep working unchanged.

Comments

jmcerda created an issue. See original summary.

jmcerda’s picture

Both open questions in the summary are now settled.

**A grant signed by a since-deleted secret is unverifiable, and therefore denied.** Deleting a secret is at least as strong as narrowing one: an operator who deletes a secret has decided it should stop working, and leaving its outstanding grants live until expiry would contradict that. It also keeps deletion usable as an emergency response.

**A named secret with no scope entry grants nothing.** Only the legacy no-\`k\` path means "everything", and only for backward compatibility with the single-secret installs that exist today.

The asymmetry is deliberate. Adding a secret and forgetting to scope it is exactly the mistake this feature exists to prevent, so it must fail closed and visibly rather than silently handing over the whole corpus. The legacy path is different because those installs never opted into scoping at all — changing their behaviour would be the breaking change, not the safe default.

Worth surfacing an unscoped named secret on the status report too, so "grants nothing" reads as a configuration error rather than as a mysteriously dead credential.

jmcerda’s picture

Version: 1.0.2 » 1.x-dev
Status: Active » Fixed

Fixed in 1.2.0.

https://www.drupal.org/project/file_gate/releases/1.2.0

Named secrets + field scopes, k= on grants, scope at mint and redeem, unscoped named secrets ERROR on the status report. Legacy download_secret without k= remains whole-corpus.

Now that this issue is closed, review the contribution record.

As a contributor, attribute any organization that helped you, or if you volunteered your own time.

Maintainers, credit people who helped resolve this issue.

jmcerda’s picture

Version: 1.x-dev » 1.2.0

Fixed in 1.2.0.

https://www.drupal.org/project/file_gate/releases/1.2.0

Named secrets + field scopes, k= on grants, scope at mint and redeem, unscoped named secrets ERROR on the status report. Legacy download_secret without k= remains whole-corpus.

jmcerda’s picture

Status: Fixed » Closed (fixed)

Closing as shipped in 1.2.0. (Was Fixed; closed now that the release is public.)