feat(provider): bootstrap credentials sourced from another provider#142
Merged
Conversation
Deploying with
|
| 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 |
444d195 to
8692ce4
Compare
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>
8f4932f to
c18fec1
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Provider aliases can now source their own credentials from another provider. An alias in
[providers]may declare anenvmap binding an environment variable the provider needs (such asBWS_ACCESS_TOKENorVAULT_TOKEN) to a source: a bare provider spec, which reads the value at the convention path, or a table with arefgiving 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 bysecretspec run.Behavior
--provider/SECRETSPEC_PROVIDER, and the default provider.login, is audited with abootstrapmarker naming the variable and the source store.envvariable 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 addgains a repeatable--env VAR=PROVIDERflag.Also in this PR
ProviderAlias::envis a plain map instead of anOption, 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 throughlogin, and plan grouping.lib/secretspec.so, loads in PHP).🤖 Generated with Claude Code