Skip to content

0.17.0 - 2026-07-26

Latest

Choose a tag to compare

@github-actions github-actions released this 27 Jul 03:54
a8794e4

Release Notes

Fixed

  • Cache reads, refreshes, and clears now share one ownership and freshness
    policy, consistently handling expiration boundaries, clock rollback,
    corrupted SecretSpec entries, and values owned by another project or profile.
  • Vault and OpenBao providers reuse one reqwest::Client per provider
    instance (same OnceLock pattern as Infisical) instead of building a fresh
    client on every get/set/login. Concurrent get_many of many secrets no
    longer opens one TCP(+TLS) handshake per secret against reverse-proxied
    deployments, which was observed to drop part of the burst with
    Failed to connect to Vault.
  • get_each (default Provider::get_many) caps concurrent unique-address
    fetches at 8 by default, overridable with SECRETSPEC_PROVIDER_CONCURRENCY.
    Waves replace a single unbounded thread::scope fan-out.
  • Vault/OpenBao HTTP sends retry up to 3 times on connect/timeout errors only
    (not on HTTP 4xx/5xx), with a short backoff between attempts.

Added

  • SOPS provider (sops://, sops build feature) for reading and writing
    YAML, JSON, dotenv, and INI files through the SOPS CLI, including templated
    per-project/profile paths and provider-credential injection for encryption
    keys and cloud authentication. Writes are serialized and atomically replace
    encrypted files, with secret values passed to SOPS over standard input.

  • Scaleway Secret Manager provider (scaleway://, scaleway build feature) for
    storing secrets in Scaleway's Secret Manager over its v1beta1 REST API.
    Authenticates with an API secret key (secret_key credential or
    SCW_SECRET_KEY), targets a region (URI host or SCW_DEFAULT_REGION, default
    fr-par) and project (?project_id= or SCW_DEFAULT_PROJECT_ID), and stores
    convention secrets under the folder path secretspec/{project}/{profile} with
    the key as the secret name. Native ref references may select a JSON key with
    field and a revision with version, and are read-only.

  • Cached provider aliases with ordered authoritative fallback routes,
    configurable local cache freshness, cache-first reads, automatic refresh
    after reads and writes, and secretspec cache clear invalidation.

    A cache must be a distinct store from the route's own authoritative providers
    (compared by canonical provider URI, so equivalent spellings of one store
    cannot disguise a cache as its own source), must be a store SecretSpec can
    delete from (keyring, pass, gopass, dotenv, or a Vault/OpenBao KV v2 mount) so
    its entries can be invalidated, and must be the only entry in a providers
    list. All three are reported when the route is planned, and an unusable
    max_age when the configuration loads.

    Every entry records the project and profile that own it, and SecretSpec only
    changes an entry it can show is its own: a value it did not write, or one
    belonging to another project or profile, is left alone by reads and refreshes
    and reported by cache clear rather than deleted, since an address alone is not
    proof of ownership when a store is shared. An entry marked as SecretSpec's own
    but unreadable is replaced.

    A cached value never outlives the write that superseded it: a failed refresh, a
    cache that could not be constructed, and a write that bypassed the cache with
    --provider all invalidate the entry. An entry no read can serve — expired, or
    written for a different route — is deleted when found rather than skipped, so an
    expired value does not keep its plaintext in a store that cannot expire
    anything. Where the store can expire a value itself, max_age is applied
    server-side — Vault and OpenBao set the KV v2 path's delete_version_after — so
    a cached copy stops existing at that age even if SecretSpec is never run again,
    and clearing a KV v2 entry destroys its recoverable version history.

    cache clear reports how many entries it actually removed, ignores provider
    overrides, and clears what it can before reporting a cache store it could not.
    Cache writes are audited as cache_refresh rather than set.
    (#199)

  • secretspec config global init --provider <PROVIDER> --profile <PROFILE>
    can save explicitly user-global defaults without interactive prompts,
    including --profile none to clear the default profile. The global
    namespace also supports config inspection and provider-alias commands;
    existing invocations without the namespace remain compatible.
    (#171)

  • Secret scopes: a [scopes] table names membership-only subsets of a
    profile's secrets, so a single service or task resolves only what it declares
    instead of the whole profile. check, run, and export take --scope
    (SECRETSPEC_SCOPE); the consumer-visible set is the intersection of the
    selected profile and the scope's secret list. Scopes are orthogonal to
    profiles and never change a secret's required/default/providers or its
    storage address. A composed secret in a scope still resolves its dependencies
    — even ones the scope leaves out — to build its value, but those dependencies
    are never exposed to the scope, and a provider warning about one calls it "a
    hidden composition input" rather than naming it; a secret that is neither in the scope nor a
    dependency of one is never fetched, and a scope whose intersection with the
    selected profile is empty contacts no provider at all (resolve and report
    results then carry an empty provider). A scope's own list must name at least
    one secret, with no blank or repeated entries.
    run --scope removes every manifest-declared secret the scope does not
    admit from the child environment — across all profiles, even one the parent
    already exported — so no value can leak into the launched process; a secret
    the scope lists is kept even when the selected profile does not declare it.
    export --scope emits the scoped subset but unsets nothing, since no output
    format can express an unset. set, like import, ignores an ambient
    SECRETSPEC_SCOPE entirely, and a blank --scope (or a blank
    SECRETSPEC_SCOPE) clears an inherited scope instead of deferring to it. Under
    project extends, a child scope replaces the parent scope of the same name
    outright rather than unioning their secret lists. Typed SDK loaders ignore an
    ambient SECRETSPEC_SCOPE, since a generated struct always expects the full
    profile; import likewise ignores scope and always copies the whole profile.
    Untyped SDK/FFI builders expose explicit scope selection and return the active
    scope in resolve/report results. Audit events for scoped check, run, and
    export operations record the scope name as well as the keys accessed or
    exposed.

  • age provider (age://) for storing dotenv-style secret sets in an
    age-encrypted file, with ASCII armor by default, team recipient rosters,
    direct X25519 and SSH key support, native tagged recipients, and
    non-interactive age plugins. Hybrid ML-KEM-768 + X25519 keys are recommended
    for new setups to protect stored ciphertext against future quantum attacks.

  • Read-only systemd credential provider (systemd-credential://) for resolving
    secrets and provider authentication credentials from the current service's
    $CREDENTIALS_DIRECTORY, including exact-name references and strict
    filename, file-type, and text validation.

  • KeePass KDBX provider (kdbx:, kdbx build feature) for local encrypted
    databases. It reads KDBX 3 and KDBX 4, writes KDBX 4 with atomic file
    replacement, supports master passwords and key files, and can address
    standard or custom entry fields through secret references.

  • The required field accepts at_least_one and exactly_one group tables,
    supporting overlapping alternative and mutually exclusive credentials
    across check, run, and SDK resolution.

  • OpenBao provider (openbao://, openbao build feature) with its
    own provider identity, documentation, and OpenBao CLI configuration through
    BAO_ADDR, BAO_NAMESPACE, BAO_TOKEN, and BAO_TOKEN_PATH. The
    provider also has OpenBao-prefixed AppRole and JWT inputs; corresponding
    VAULT_* names remain compatibility fallbacks. Compatible KV and standard
    authentication mechanics are shared internally with the Vault provider.
    Vault-compatible addresses accept trailing slashes, and AppRole/JWT login
    exchanges now honor the configured namespace. Reported provider URIs strip
    endpoint credentials while retaining non-secret store and authentication
    attribution.

  • Vault / OpenBao JWT/OIDC authentication (?auth=jwt) logs in through a
    configured Vault role using VAULT_JWT, or requests a short-lived OIDC token
    automatically in GitHub Actions and Forgejo Actions jobs with id-token: write. The role and optional audience can be set in the provider URI or with
    VAULT_JWT_ROLE and VAULT_JWT_AUDIENCE.

  • The Python SDK now publishes a Windows x64 wheel to PyPI, so
    pip install secretspec and uv add secretspec work on Windows.
    (#177)

  • The Ruby SDK now publishes a Windows gem (x64-mingw-ucrt) to RubyGems, so
    gem install secretspec works with RubyInstaller on Windows.

  • The PHP SDK now publishes prebuilt Windows x64 extension binaries
    (secretspec-php-native-<php>-nts-x86_64-pc-windows-msvc.dll) alongside the
    Linux and macOS builds on each release.

Changed

  • The Bitwarden Secrets Manager provider now invokes the separately installed
    official bws CLI instead of linking the Bitwarden SDK. This removes the
    SDK's restricted-license dependency from SecretSpec distributions while
    preserving project-scoped reads, writes, access-token credentials, and
    EU/self-hosted server selection.
  • Secret status output now emphasizes secret names, de-emphasizes descriptions,
    and omits placeholder text when a description is unavailable, making long
    check and import results easier to scan.
    (#139)

Fixed

  • BWS CLI writes preserve secret keys and values that begin with -, and
    hostless BWS provider URIs stay pinned to Bitwarden's default server even
    when ambient BWS profiles or server settings are configured.
  • The dotenv provider rejects variable names its parser cannot read back
    (anything outside [A-Za-z_][A-Za-z0-9_.]*, for example a ref item
    containing a dash) instead of writing a line that made every later read and
    write of the whole file fail to parse. The rejection happens before the CLI
    prompts for a value and names the offending item.

Install secretspec 0.17.0

Install prebuilt binaries via shell script

curl --proto '=https' --tlsv1.2 -LsSf https://github.com/cachix/secretspec/releases/download/v0.17.0/secretspec-installer.sh | sh

Download secretspec 0.17.0

File Platform Checksum
secretspec-aarch64-apple-darwin.tar.xz Apple Silicon macOS checksum
secretspec-x86_64-apple-darwin.tar.xz Intel macOS checksum
secretspec-x86_64-pc-windows-msvc.zip x64 Windows checksum
secretspec-aarch64-unknown-linux-gnu.tar.xz ARM64 Linux checksum
secretspec-x86_64-unknown-linux-gnu.tar.xz x64 Linux checksum