Skip to content

feat(provider): bootstrap credentials sourced from another provider#142

Merged
domenkozar merged 14 commits into
mainfrom
feat/provider-bootstrap-overlay
Jul 16, 2026
Merged

feat(provider): bootstrap credentials sourced from another provider#142
domenkozar merged 14 commits into
mainfrom
feat/provider-bootstrap-overlay

Conversation

@domenkozar

@domenkozar domenkozar commented Jul 15, 2026

Copy link
Copy Markdown
Member

Summary

Provider aliases can now source their own credentials from another provider. An alias in [providers] may declare an env map binding an environment variable the provider needs (such as BWS_ACCESS_TOKEN or VAULT_TOKEN) to a source: a bare provider spec, which reads the value at the convention path, or a table with a ref giving exact coordinates. The credential is fetched from the source and handed to the store, so a machine token can live in the OS keyring instead of a plaintext environment variable, and it is never written into the environment of processes started by secretspec run.

[providers]
bws = { uri = "bws://project-uuid", env = { BWS_ACCESS_TOKEN = "keyring" } }
vault = { uri = "vault://kv/app?auth=approle", credentials = {
  VAULT_ROLE_ID   = { provider = "onepassword", ref = { vault = "Infra", item = "approle", field = "role_id" } },
  VAULT_SECRET_ID = { provider = "onepassword", ref = { vault = "Infra", item = "approle", field = "secret_id" } },
} }

Behavior

  • An environment variable that is already set (non empty) wins, so CI keeps working with no extra configuration.
  • Bootstrap chains are limited to one hop, enforced on every path the alias can appear on: chain primary, chain fallback, explicit --provider/SECRETSPEC_PROVIDER, and the default provider.
  • Credentials are fetched once per invocation and profile, then reused across all secrets routed at the alias; convention path credentials are scoped per profile.
  • Every source read, and every credential stored through login, is audited with a bootstrap marker naming the variable and the source store.
  • Declaring an env variable the target provider never reads prints a warning naming the variables it does read.

CLI

  • secretspec config provider login <alias> prompts for each declared bootstrap credential and stores it at its source, at exactly the location resolution later reads it from.
  • secretspec config provider add gains a repeatable --env VAR=PROVIDER flag.

Also in this PR

  • Docs for the feature (concepts, provider pages, CLI and configuration reference).
  • A cleanup pass over the branch (reuse/simplification/efficiency review): ProviderAlias::env is a plain map instead of an Option, single registry scheme lookup, one ordering rule for bootstrap entries, bootstrap writes audit through the shared write audit helper, per provider bootstrap variable consts, and deduplicated CI release/build scripts.

Testing

  • cargo test --package secretspec: 424 tests pass, including new coverage for overlay resolution, env wins, one hop validation, profile scoping, round trips through login, and plan grouping.
  • PHP extension build script smoke tested (stages lib/secretspec.so, loads in PHP).

🤖 Generated with Claude Code

@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Jul 15, 2026

Copy link
Copy Markdown

Deploying with  Cloudflare Workers  Cloudflare Workers

The latest updates on your project. Learn more about integrating Git with Workers.

Status Name Latest Commit Preview URL Updated (UTC)
✅ Deployment successful!
View logs
secretspec 0472279 Commit Preview URL

Branch Preview URL
Jul 16 2026, 01:00 AM

@domenkozar domenkozar force-pushed the feat/provider-bootstrap-overlay branch from 444d195 to 8692ce4 Compare July 15, 2026 20:31
domenkozar and others added 13 commits July 15, 2026 16:53
Introduce a per-provider "bootstrap env overlay" — an in-memory
HashMap<String, SecretString> injected at construction — that credential
reads consult after the process environment. This is the delivery
mechanism for letting a provider's own credentials come from another
provider, without ever calling std::env::set_var (which would leak them
into the child environment of `secretspec run`).

The overlay is handed to the concrete provider value inside the
registration factory, before any Arc/Box wrapping, because a &mut self
hook cannot be forwarded through the blanket impl Provider for Arc<T> —
a preflight provider wrapped as Box<Arc<P>> would otherwise silently
receive the default no-op.

bws and vault consult the overlay lazily via a shared env-wins helper;
onepassword fills its existing service_account_token field when unset.
The overlay is empty everywhere in production for now, so every path
behaves exactly as before; a test proves factory injection reaches the
preflight-wrapped onepassword provider.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Change the provider-alias map value from a bare String to a ProviderAlias
struct carrying the URI plus an optional bootstrap-credential `env` map.
This lets an alias declare which environment variables the provider needs
(e.g. an access token) and where to source them, the config surface for
letting a provider's credentials come from another provider:

    [providers]
    keyring = "keyring://"
    bws = { uri = "bws://project-uuid", env = { BWS_ACCESS_TOKEN = "keyring" } }

Deserialization accepts both the bare-string and table forms via a manual
Visitor (deny_unknown_fields inside the table arm for precise errors,
rather than an untagged enum). Serialization emits an env-less alias back
as a bare string, so existing configs round-trip unchanged.

The `env` map is parsed and stored but not yet consulted; wiring it into
resolution follows. Consumers still read the alias URI as before.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Make each alias `env` entry a BootstrapSource rather than a bare provider
string, so a bootstrap credential is located the same way any secret is:
a provider spec plus an optional `ref` giving native coordinates. This
replaces the earlier idea of a dedicated `providers/{alias}/{VAR}` storage
convention with the mechanism the rest of the tool already uses.

    [providers]
    # bare string: read from the provider at the convention path
    bws = { uri = "bws://proj", env = { BWS_ACCESS_TOKEN = "keyring" } }
    # table: pin the exact location with `ref`
    vault = { uri = "vault://kv?auth=approle", env = {
      VAULT_ROLE_ID = { provider = "onepassword", ref = { vault = "Infra", item = "approle", field = "role_id" } },
    } }

A ref-less source round-trips back to the bare string form. The `env` map
is still parsed and stored but not yet consulted; resolution follows.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Make an alias's `env` map take effect: when a provider is built from an
alias that declares bootstrap credentials, each variable is fetched from
its source provider (at a `ref` location or the convention path) and
handed to the store as an overlay it reads before the environment. A
variable already set in the environment wins and is not fetched; a
declared credential that cannot be found is a hard error naming how to fix
it. Bootstrap chains are validated at plan time and limited to one hop,
which also makes cycles impossible.

To keep an alias's `env` reachable at construction, routing and grouping
now key on the primary spec rather than its resolved URI, so two aliases
that share a URI but declare different credentials no longer merge into one
group. The string `TryFrom` for a provider gains a shared
`provider_from_spec` body so construction can carry the overlay.

Applies to per-secret `providers` chains (primary and fallback) and the
default provider. An explicit `--provider` override is resolved to a URI
before routing, so it reads credentials from the environment rather than a
chain.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add the onboarding commands for provider bootstrap credentials:

- `config provider login <alias>` prompts (hidden input) for each bootstrap
  credential the alias declares in `env` and stores it in the source
  provider at the exact location resolution reads it from, so the next
  operation can authenticate. Reports where each was stored and suggests
  `check` to verify. A read-only source is rejected up front.
- `config provider add` gains a repeatable `--env VAR=PROVIDER` flag to
  declare bare-string bootstrap sources from the command line (use `ref`
  by editing the config).

Backed by two library methods on the resolver — `bootstrap_credentials`
(what an alias needs) and `store_bootstrap_credential` (write one to its
source) — so the store and read paths share their addressing.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add a "Bootstrap Credentials" section to the providers concept page
explaining how an alias's `env` map sources a provider's own credentials
from another provider, the env-wins/no-leak/one-hop behavior, and the
`login` flow. Document the alias table form and `env` sources in the
configuration reference, and `config provider login` plus `add --env` in
the CLI reference. Cross-link the bws, vault, and onepassword provider
pages to it.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
An explicit --provider <alias> or SECRETSPEC_PROVIDER=<alias> override was
resolved to a URI before routing, losing the alias's bootstrap env, so the
very command `config provider login` recommends (`check --provider <alias>`)
skipped the stored credentials. Routes now carry the raw spec as the build
key and keep the resolved URI for display and the report.

Also tightens the rest of the bootstrap plumbing:

- validate bootstrap sources (known provider, one hop) on every construction
  path via resolve_bootstrap_overlay, not just plan time chain primaries
- memoize resolved overlays per spec and reuse one source provider per
  distinct source, so credentials are fetched once per invocation
- resolve the 1Password bootstrap token where it is consumed instead of
  mutating the config, so uri() keeps the scheme the user configured
- share one env wins predicate between the resolver and providers; a set
  but empty environment variable now counts as unset on both sides
- extract shared provider construction and BootstrapSource address and
  location helpers so the read and write paths cannot drift
- simplify the CLI alias construction and dedupe test alias maps

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…r access

- The bootstrap overlay memo is keyed by (profile, spec) and every provider
  build is addressed under the operation's profile, so convention-path
  credentials at {project}/{profile}/{VAR} never bleed across profiles.
- Bootstrap source reads and login stores are audited with a bootstrap
  marker; stores pass the require_reason gate and clear the memo, so a
  rotated credential takes effect immediately.
- Providers statically declare the variables they read through the overlay
  (bootstrap_vars in register_provider!); an alias declaring a variable its
  provider never reads warns instead of fetching a silently ignored value.
- Bootstrap source specs are redacted in prompts and errors, and
  onepassword folds a hash of the effective token (not its plaintext) into
  the preflight cache key; the factory-injection test asserts injection via
  scope-key differences accordingly.
- The prompt-for-missing header names the default provider from the
  registry without constructing it, so a bootstrapped default alias no
  missing secret routes to cannot fail or fetch during display.
- Bootstrap source validation composes the underlying resolution error
  (alias listings, the onepassword spelling hint) and rejects unknown
  schemes at validation time; env = {} normalizes to "no bootstrap env".

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Cleanups from a reuse/simplification/efficiency/altitude review of the
branch, with no behavior change:

- ProviderAlias.env is a plain map (empty means no bootstrap
  credentials), removing the Option normalization and the Some/None
  handling at every consumer
- one registry scheme lookup (registration_for_scheme) shared by spec
  checks, bootstrap_vars, display names, and provider construction
- resolve_bootstrap_overlay looks the alias up once and passes env down;
  sorted_bootstrap_entries is the single ordering rule for fetches,
  validation errors, and login prompts
- store_bootstrap_credential audits through audit_write_result like
  every other write path
- provider bootstrap variable names are consts shared between the
  registration and the read sites so the two cannot drift
- both release workflows publish assets via
  scripts/upload-release-asset.sh; the PHP extension build/stage mapping
  lives only in secretspec-php/scripts/build-ext.sh (now honoring
  CARGO_TARGET_DIR), called by php-ext.yml and ci-sdks.sh

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@domenkozar domenkozar force-pushed the feat/provider-bootstrap-overlay branch from 8f4932f to c18fec1 Compare July 16, 2026 00:43
@domenkozar domenkozar merged commit 409e3d2 into main Jul 16, 2026
23 of 28 checks passed
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.

1 participant