Skip to content

Blog

SecretSpec 0.17: Scopes, secrets caching, SOPS, age, and systemd credentials

SecretSpec 0.17 ships:

A profile describes how secrets resolve for an environment. A scope now describes which of those secrets one consumer may receive:

secretspec.toml
[profiles.default]
DATABASE_URL = { description = "Database" }
API_KEY = { description = "API key" }
QUEUE_TOKEN = { description = "Queue token" }
[scopes.api]
secrets = ["DATABASE_URL", "API_KEY"]
[scopes.worker]
secrets = ["DATABASE_URL", "QUEUE_TOKEN"]
Terminal window
secretspec run --scope api -- ./api
secretspec run --scope worker -- ./worker

Composed secrets may still read hidden dependencies to build a visible value, but those inputs are not exposed to the child. The same scope selection is available to check, export, and the SDK builders. Scopes minimize secret delivery; they are not an authorization boundary when the child itself holds provider credentials.

Carrying the selected scope through resolver requests and results required a breaking change to secretspec-ffi. All SecretSpec SDKs have been updated for 0.17 to support scopes, so applications should upgrade their SDK package and bundled native resolver together.

Many cloud providers take long enough to resolve a secret that their latency becomes part of every development command.

A single 1Password lookup can take roughly one second.

SecretSpec providers implement get_many so a backend can resolve several values together. Relatively few secret stores and CLIs expose a true bulk-read operation, however, so many providers still have to perform separate lookups.

Waiting on a remote service or its CLI every time makes check, run, and application startup feel slow, especially as a project grows.

A provider alias can now combine its authoritative fallback route with a local cache:

secretspec.toml
[providers]
vault = "vault://vault.example.com:8200/secret"
local = "keyring://secretspec/cache/{project}/{profile}/{key}"
fast_vault = {
fallback = ["vault"],
cache = { provider = "local", max_age = "8h" }
}
[profiles.default.defaults]
providers = ["fast_vault"]

Fresh entries avoid contacting the remote provider. A miss or expired entry falls through to Vault and refreshes the cache; writes update the authoritative provider first and then refresh or invalidate its cached copy. Route changes, reference changes, and writes that bypass the cached alias also invalidate the entry.

The cache is a real copy of the secret, so SecretSpec requires a distinct store that it can delete from and records ownership before changing an entry. secretspec cache clear [NAME] forces the next read back through the authoritative route.

Vault and OpenBao KV v2 caches are the only providers that handle max_age as server-side expiry properly.

None of SecretSpec’s current local providers has strong native support for expiry. They can remove an expired entry the next time SecretSpec sees it, but cannot ensure the local copy disappears at its deadline if SecretSpec never runs again.

Our planned FactorSeal provider in Future work is intended to close that gap with an explicit API for credential eviction among the other goals.

Profiles can now express credential alternatives directly:

secretspec.toml
[profiles.default]
PASSWORD = { description = "Password", required = { at_least_one = "auth" } }
ACCESS_TOKEN = { description = "Token", required = { at_least_one = "auth" } }
GITHUB_TOKEN = { description = "GitHub token", required = { exactly_one = "github_auth" } }
GITHUB_APP_KEY = { description = "GitHub App private key", required = { exactly_one = "github_auth" } }

The auth group accepts a password, an access token, or both. The github_auth group requires exactly one credential and rejects configurations that provide both the token and the app key.

SOPS brings the encrypted-file workflow from our recent SOPS comparison behind SecretSpec’s provider-independent CLI and SDKs. SecretSpec delegates encryption and decryption to the installed SOPS CLI, so existing SOPS key services and .sops.yaml creation rules remain in control.

The provider reads and writes YAML, JSON, dotenv, and INI files, supports a single shared file or {project} / {profile} path templates, and can source sensitive SOPS inputs such as age keys or cloud credentials through provider credentials.

secretspec.toml
[providers]
sops = "sops://secrets/{project}/{profile}.enc.yaml"
[profiles.production.defaults]
providers = ["sops"]

age offers a smaller encrypted-file setup. It stores a dotenv-style secret set for one or more age recipients, including hybrid post-quantum recipients.

KeePass KDBX reads KDBX 3 and 4 databases and writes KDBX 4, with master passwords sourced from another provider rather than embedded in the URI.

OpenBao gets its own openbao:// identity and BAO_* configuration while sharing compatible KV, token, AppRole, and JWT mechanics with Vault. Both Vault and OpenBao can now exchange a JWT for a short-lived token, including an OIDC token minted automatically in GitHub Actions and Forgejo Actions with id-token: write.

Scaleway Secret Manager adds regional, project-aware cloud storage and read-only references to existing secrets and revisions.

systemd credentials is a read-only provider that resolves values from the current service’s $CREDENTIALS_DIRECTORY, including credentials used to bootstrap another provider.

Alongside 0.17, the new cachix/secretspec-action installs SecretSpec, resolves the selected profile, masks every value in the runner log, and adds the secrets to the environment of later job steps:

jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- uses: cachix/secretspec-action@main
with:
profile: production
scope: api
- run: ./deploy.sh

A missing required secret fails the action, so the same step also checks that the deployment environment is complete. See the GitHub Actions guide for provider selection and tokenless Vault or OpenBao authentication through the runner’s OIDC identity.

SecretSpec 0.17 also brings:

  • Bitwarden Secrets Manager — now uses the separately installed official bws CLI instead of linking its SDK.
  • Non-interactive setupsecretspec config global init --provider ... --profile ... configures defaults without prompts.
  • Windows packages — for the Python, Ruby, and PHP SDKs.
  • Clearer status output — plus more controlled concurrency and retry behavior for Vault and OpenBao.
Terminal window
cargo install secretspec

See the full changelog for every change in this release.

The next work brings more control to local secret access:

  • GUI confirmation dialogs — approve or deny a secret request in a native prompt instead of requiring a terminal interaction.
  • A Passbolt provider — bring Passbolt’s open-source, collaboration-focused credential manager behind the same SecretSpec interface for cloud and self-hosted teams.
  • A Bitwarden Password Manager provider — resolve regular Bitwarden vault items, separately from the Bitwarden Secrets Manager provider already available in SecretSpec.
  • A JVM SDK — work is underway to bring the shared SecretSpec resolver to Java, Kotlin, and other JVM languages.
  • A FactorSeal provider — we have started work on a new Linux provider built around mandatory TPM-backed storage and secure defaults. FactorSeal also provides an explicit API for credential expiry, which is crucial for the caching work in this release: local copies can carry a defined eviction deadline instead of living without a retention policy. Still in development.

Questions or feedback? Join us on Discord.

But I Use SOPS

Whenever I show someone SecretSpec, I often hear the same response:

But I use SOPS.

SOPS is good. It encrypts files so they can live in Git without exposing their plaintext values.

But SecretSpec solves a different problem: how applications declare, find, and consume secrets.

Once you have encrypted secrets.yaml, how does your Python service consume it? What about your Go worker or Node.js app?

You still need to decrypt the file, inject its values, select the right file for each environment, validate required keys, and repeat that integration for every language.

And if you release the project as open source, that choice does not stay yours. With SOPS baked into the setup, everyone who runs or contributes to the project must adopt SOPS and its key management, whatever secrets tooling they already use.

SecretSpec starts at the other end. The project declares what the application needs without storing any values:

secretspec.toml
[project]
name = "payments"
revision = "1.0"
[profiles.default]
DATABASE_URL = { description = "Postgres connection string" }
STRIPE_API_KEY = { description = "Stripe secret key" }

The same secret can come from a developer’s system keyring or CI environment variables, while a more sensitive production environment resolves it from Vault. Applications use the same declaration through eight SDKs for Rust, Python, Go, Ruby, Node.js/TypeScript, Haskell, PHP, and C# without knowing the provider.

Encrypted files also make the key workflow a project-wide requirement. Adding a teammate means adding their key to .sops.yaml and re-encrypting every file; removing one means rekeying and rotating the affected secrets, since their key already saw the plaintext. SecretSpec leaves identity and access to the provider: onboarding to Vault or a cloud secrets manager is granting a role, and offboarding is revoking it.

SOPS may be enough today. As your team grows more sensitive to how secrets are handled, you may want Vault’s access policies and centralized audit trail. If applications know about SOPS, each one needs migrating. If they know only SecretSpec, you change the provider configuration; SDK calls and secret names stay the same.

The same resolver provides profiles, required-secret checks, per-secret provider routing and fallback, provider-native references, temporary files, and metadata-only audit logs. You build the integration once, not once per provider and language.

SOPS protects a file. SecretSpec gives applications a provider-independent interface. The selected provider remains responsible for storage, encryption, identity, access control, and availability.

I wrote a fuller SecretSpec comparison showing exactly where SecretSpec ends, where providers begin, and which responsibilities belong to each layer.

Because applications talk to an interface instead of a file, the interface can grow without touching them. Three open proposals point where it is heading:

  • Project security requirements would let a project declare the guarantees a provider must meet, such as encryption at rest or an audit trail, and reject providers that fall short.
  • Lease-aware refresh would let running applications follow key rotation and short-lived credentials instead of restarting for a new value.
  • The SOPS provider (0.17+) brings SOPS itself behind the same SDK interface, making your encrypted files one more place secrets can come from.

With that provider, perhaps “But I use SOPS” just needs two more words:

But I use SOPS with SecretSpec.

If encrypted files fit your workflow, keep using SOPS. Just recognize the boundary: encryption at rest is not an application secrets interface.

Secrets Don’t Belong in Config

Applications should not require passwords, API keys, or tokens in their configuration files.

Configuration describes behavior. It belongs in git, code review, bug reports, and developer machines.

A secret grants authority. It needs restricted access and independent rotation.

Putting both in one file couples different lifecycles and audiences. If rotating a password requires regenerating application configuration, the interface has coupled them too tightly.

We audited all 445 NixOS modules that handle a real secret in nixpkgs at commit 141f212, classifying each by where its secret value ends up.

Where the secret value ends upModulesShare
Merged into a config file at runtime11025%
Inlined into a config in /nix/store429%
Delivered as an environment variable16136%
Left in a dedicated file opened by the app5813%
Loaded through systemd credentials5312%
Passed as a command-line argument194%
Classification uncertain2

The interesting number is 110. A quarter of the modules retrieve a secret safely, then copy it into configuration because that is the only interface the application accepts.

These modules use envsubst, replace-secret, jq, yq, sed, or custom code to assemble a restricted file at startup. The result can be secure, but every module now owns application-specific, security-sensitive glue just to combine two inputs that should have remained separate.

This is not unique to NixOS. The same workaround appears as an entrypoint script, Helm template, init container, or CI interpolation step on other platforms.

As a side note, 42 modules can inline secrets into the world-readable /nix/store. That direct security problem is tracked in nixpkgs issue #24288. The 110 runtime mergers make the broader point: even when deployment authors avoid the leak, the missing separation still creates work.

Applications should accept secret values through a dedicated runtime channel, such as:

  • a password_file or token_file setting;
  • a systemd credential;
  • a narrowly scoped environment variable;
  • or an external secret provider.

These mechanisms are not equally safe: environment variables can be inherited, arguments can appear in process listings, and files still need correct permissions. What separation does guarantee is that the deployer no longer has to manufacture a second, secret-bearing version of the configuration.

The principle is simple; implementing it across environments is not. Local development might use a system keyring, CI environment variables, and production 1Password or Vault. Without a shared abstraction, each environment needs its own naming, lookup, validation, and injection glue.

Cachix historically stored its auth token and per-cache signing keys in ~/.config/cachix/cachix.dhall, alongside cache names and other configuration. It was convenient, but the file had to be treated as a secret even though much of it was ordinary configuration.

A typical file mixed them directly:

~/.config/cachix/cachix.dhall
{ authToken = "XXX-AUTH-TOKEN"
, binaryCaches =
[ { name = "mycache"
, secretKey = "XXX-SIGNING-KEY"
}
]
}

The cache name is configuration; the auth token and signing key are secrets. You could not share the cache configuration without also sharing credentials.

devenv 2.2 separates the token through SecretSpec. The project declares CACHIX_AUTH_TOKEN, devenv resolves it from the configured provider, and the value is passed to Cachix without being added to devenv’s configuration.

Cachix PR #737 brings the same boundary into the client through the SecretSpec Haskell SDK. It resolves CACHIX_AUTH_TOKEN and CACHIX_SIGNING_KEY from SecretSpec and can store them in the user’s chosen provider instead of cachix.dhall. Existing environment variables and config files remain higher-priority fallbacks for compatibility. The PR is still open.

That is the problem SecretSpec is designed to solve: configuration declares the requirement, while each environment chooses where the value lives.

SecretSpec applies that separation by making secretspec.toml a declaration of what an application needs, without storing the values:

secretspec.toml
[project]
name = "myapp"
[profiles.production]
DATABASE_URL = { description = "Postgres connection string" }
STRIPE_API_KEY = { description = "Stripe secret key" }

Providers decide where the values live. A developer can use the system keyring, CI can use environment variables, and production can use 1Password, Vault/OpenBao, or a cloud secret manager without changing the declaration.

An existing application can receive the resolved values at startup:

Terminal window
secretspec run -- ./myapp

Applications can also resolve them directly through the SecretSpec SDKs for Rust, Python, Go, Ruby, Node.js/TypeScript, Haskell, PHP, and C#, all sharing the same resolver so behavior stays consistent across languages.

Providers own where secret values come from. SDKs give applications an idiomatic way to consume them. Configuration remains a shareable declaration of what is required.

If you maintain an application, stop adding passwords and tokens to ordinary configuration schemas. Accept a file reference, credential, environment variable, or provider instead.

For NixOS, SecretSpec issue #65 tracks how an official integration could declare and resolve secrets without per-module substitution glue.

Consistent secret handling across developer machines, CI, and production used to require infrastructure that only dedicated platform teams could build. A project of any size should be able to separate secrets from configuration without building its own secrets platform first.

SecretSpec 0.16: Composed secrets, Infisical, and C# SDK

SecretSpec 0.16 ships:

  • Composed secrets — derive a read-only value, such as a connection string, from other secrets declared in the manifest.
  • Infisical — read and write secrets in Infisical Cloud or a self-hosted instance, with Universal Auth, access-token, and provider-credential authentication.
  • C# SDK — resolve the same manifests from .NET through the shared native resolver, distributed as the Cachix.SecretSpec NuGet package.

Applications often need a connection string while secret stores work better with its independently rotated parts. SecretSpec can now keep those parts separate and assemble the application-facing value when it resolves the manifest:

secretspec.toml
[profiles.default]
DB_USER = { description = "Database user" }
DB_PASSWORD = { description = "Database password" }
DB_HOST = { description = "Database host" }
DATABASE_URL = {
description = "PostgreSQL connection string",
composed = "postgres://${DB_USER}:${DB_PASSWORD}@${DB_HOST}/app"
}

DB_USER, DB_PASSWORD, and DB_HOST still come from their configured providers. DATABASE_URL is assembled in memory and behaves like any other resolved secret in the CLI and SDKs. Compositions are read-only, may build on other compositions, and are checked for missing references and cycles before resolution.

See Composed Secrets for optional values, escaping, profile inheritance, and validation rules.

The new infisical:// provider works with Infisical Cloud, its EU service, and self-hosted instances. Point SecretSpec at an Infisical project and authenticate with Universal Auth:

Terminal window
export INFISICAL_CLIENT_ID=...
export INFISICAL_CLIENT_SECRET=...
secretspec run \
--provider "infisical://app.infisical.com/7e2f1a4c-...?env=prod" \
-- npm start

Access tokens are also supported. Credentials can come from environment variables or SecretSpec’s provider credentials, allowing, for example, an Infisical machine identity to be kept in the system keyring:

secretspec.toml
[providers.infisical]
uri = "infisical://app.infisical.com/7e2f1a4c-..."
[providers.infisical.credentials]
client_id = "keyring"
client_secret = "keyring"

By default, the active SecretSpec profile also names the Infisical environment. A production profile therefore reads from the production environment, while ?env= can select a different one. The provider supports normal SecretSpec reads and writes, as well as references to existing Infisical secrets and versions.

See the Infisical provider guide for self-hosting, authentication, paths, references, and permissions.

The Cachix.SecretSpec NuGet package brings the shared SecretSpec resolver to .NET 8:

Terminal window
dotnet add package Cachix.SecretSpec
using Cachix.SecretSpec;
using var resolved = SecretSpec.Builder()
.WithProvider("keyring://")
.WithProfile("production")
.WithReason("boot web app")
.Load();
Console.WriteLine(resolved.Secrets["DATABASE_URL"].Get());
resolved.SetAsEnv();

It uses the same resolver as the CLI and other language SDKs, so profiles, providers, fallback chains, references, generators, audit reasons, and composed secrets work consistently in .NET. Native resolver builds are included in the NuGet package, with no separate SecretSpec CLI installation required.

See the C# SDK guide for supported platforms, ASP.NET Core integration, preflight reports, error handling, and typed access.

Terminal window
cargo install secretspec

All three additions are opt-in: existing manifests and provider configurations continue to work unchanged. Add composed when a value should be derived, select an infisical:// provider to use Infisical, or install Cachix.SecretSpec in a .NET application.

See the full changelog for every change in this release.

Questions or feedback? Join us on Discord.

SecretSpec 0.15: Provider credentials, Azure Key Vault / Gopass, and PHP SDK

SecretSpec 0.15 ships:

  • Provider credentials — authenticate one secret provider with credentials stored in another, without exporting them to the application environment.
  • Azure Key Vault — store and resolve secrets with service-principal, Azure CLI, managed-identity, or AKS workload-identity authentication.
  • Gopass — use a GPG-encrypted, git-synchronized password store, including multi-user and multi-store setups.
  • PHP SDK — use the shared SecretSpec resolver from PHP-FPM, Laravel, Symfony, and CLI applications through a native extension or ext-ffi.
  • AWS creation guardrails — set a customer-managed KMS key and required tags when SecretSpec creates an AWS Secrets Manager secret.
  • secretspec export — resolve secrets without launching a command, with shell, dotenv, JSON, and GitHub Actions output.
  • Provider and resolution fixes — ordered lazy fallback chains, early ref validation, correctly merged profile overrides, stable output, and broader Node.js Linux compatibility.

Suppose Bitwarden Secrets Manager holds an application’s secrets, but its machine access token is kept in the user’s OS keyring. Declare the relationship on the provider alias:

secretspec.toml
[providers]
keyring = "keyring://"
[providers.bws]
uri = "bws://a9230ec4-5507-4870-b8b5-b3f500587e4c"
[providers.bws.credentials]
access_token = "keyring"

Before SecretSpec connects to Bitwarden, it reads access_token from the keyring at the normal {project}/{profile}/access_token address. The active profile is part of that address, so production and development can authenticate as different machines without changing the alias.

When a credential already has a provider-native address, use a ref. Here a Vault AppRole is kept as two fields of one 1Password item:

secretspec.toml
[providers.vault_prod]
uri = "vault://secret/myapp?auth=approle"
[providers.vault_prod.credentials]
role_id.provider = "onepassword"
role_id.ref.vault = "Infra"
role_id.ref.item = "vault-approle"
role_id.ref.field = "role_id"
secret_id.provider = "onepassword"
secret_id.ref.vault = "Infra"
secret_id.ref.item = "vault-approle"
secret_id.ref.field = "secret_id"

The credential source uses the same ref coordinates as application secrets. The difference is where the value goes: SecretSpec hands it directly to the destination provider in memory. It is not added to the environment of a process started by secretspec run.

Provider credential names are semantic and checked before a source is opened. Bitwarden accepts access_token; Vault accepts token, role_id, and secret_id; 1Password accepts service_account_token; Azure Key Vault accepts tenant_id, client_id, and client_secret. A configured credential is authoritative, while a provider’s usual environment fallback remains available when no credential source is declared.

Credential chains deliberately stop after one hop. The store containing a provider credential cannot itself depend on another provider credential. This keeps the bootstrap path finite and makes dependency mistakes fail before any store is contacted.

Log in once, without an environment variable

Section titled “Log in once, without an environment variable”

The new config provider login command prompts for every credential an alias declares and writes it to the configured source:

Terminal window
$ secretspec config provider login bws
Enter access_token for provider 'bws' (source: keyring): ****
✓ stored access_token in keyring at my-app/default/access_token

A user-level alias and its credential source can also be declared entirely with config provider add:

Terminal window
secretspec config provider add bws "bws://project-uuid" \
--credential access_token=keyring
secretspec config provider login bws

Credentials are fetched once per invocation and profile, then reused for every secret routed through that alias. Each credential read, and each value stored by login, gets an audit event marked with the semantic credential name and source store. As with every SecretSpec audit event, the credential value is never recorded.

See Provider Credentials for the full configuration and resolution rules.

Azure Key Vault joins the provider list with the akv:// scheme:

Terminal window
# Use service-principal credentials, or the current Azure CLI session
secretspec run --provider akv://myvault -- npm start
# Use the platform's managed identity
secretspec check --provider akv://myvault?auth=managed_identity
# Use AKS workload identity federation
secretspec run --provider akv://myvault?auth=workload_identity -- ./deploy

The default authentication mode first looks for the tenant_id, client_id, and client_secret provider credentials introduced above, then their AZURE_TENANT_ID, AZURE_CLIENT_ID, and AZURE_CLIENT_SECRET environment fallbacks. If none are present, it uses the signed-in Azure CLI or Azure Developer CLI session. A partial service principal is an error, rather than a reason to silently switch identities.

That makes a service principal straightforward to keep in the system keyring:

secretspec.toml
[providers.azure]
uri = "akv://myvault"
[providers.azure.credentials]
tenant_id = "keyring"
client_id = "keyring"
client_secret = "keyring"
Terminal window
secretspec config provider login azure
secretspec run --provider azure -- ./deploy

Sovereign clouds can use either a complete vault hostname or an explicit DNS suffix such as akv://myvault?suffix=vault.azure.cn.

Azure restricts secret names to letters, digits, and hyphens and compares them case-insensitively. SecretSpec encodes the project, profile, and key as lowercase, unpadded Base32 components. The encoding keeps names that differ by case or punctuation distinct instead of letting Azure collapse them onto the same secret. Existing Azure secrets can be addressed with a read-only ref.

See the Azure Key Vault provider guide for authentication, naming, references, and required permissions.

The new gopass:// provider reads and writes through the gopass CLI. Gopass builds on the Unix pass provider with multi-user and multi-store support while keeping entries GPG-encrypted and synchronized through git.

Once Gopass is installed and its password store is initialized, select it like any other provider:

Terminal window
secretspec set DATABASE_URL --provider gopass
secretspec run --provider gopass -- npm start

By default, entries live under secretspec/{project}/{profile}/{key}. A custom URI can change that layout, including omitting {project} to share secrets between repositories:

~/.config/secretspec/config.toml
[defaults.providers]
shared = "gopass://secretspec/shared/{profile}/{key}"

An existing Gopass entry can also be addressed directly with a ref, including the mount-point prefix used by a multi-store setup. See the Gopass provider guide for installation, shared-store configuration, references, and current limitations.

The new cachix/secretspec Composer package brings the shared resolver to PHP:

Terminal window
composer require cachix/secretspec
<?php
use Secretspec\SecretSpec;
$resolved = SecretSpec::builder()
->withProfile('production')
->withReason('boot web app')
->load();
echo $resolved->secrets['DATABASE_URL']->get();
$resolved->setAsEnv();

It offers two native backends behind the same PHP API. The recommended native extension embeds the resolver and works under PHP-FPM without ffi.enable, like ext-redis. An ext-ffi fallback loads the shared resolver at runtime for CLI tools and local development. Both use the same Rust core as the CLI and the other language SDKs, so profiles, providers, fallback chains, generators, as_path, audit reasons, and typed missing-secret errors behave the same way.

setAsEnv() updates getenv(), $_ENV, and $_SERVER, which lets Laravel’s env() helper and Symfony’s %env(...)% processors consume resolved secrets during application boot. See the PHP SDK guide for installation and framework examples.

AWS accounts often require a customer-managed KMS key or specific tags in the same CreateSecret request. The AWS Secrets Manager provider now accepts both on its URI:

secretspec.toml
[providers]
prod = "awssm://prod@us-east-1?kms_key_id=alias/my-key&tag.team=platform&tag.env=prod"

kms_key_id and repeatable tag.NAME=VALUE parameters are applied only when SecretSpec creates a secret. Updating an existing secret does not alter the key or tags it was created with. This supports tag-on-create SCP and IAM guardrails without turning routine secret updates into infrastructure changes.

The new export command resolves every secret for the active profile without starting another process. Its default output can be evaluated by a POSIX shell:

Terminal window
eval "$(secretspec export --profile production)"

Use --format dotenv to write dotenv syntax or --format json to pass the resolved values to another tool:

Terminal window
$ secretspec export --profile production --format json
{
"DATABASE_URL": "postgresql://prod.example.com/mydb"
}

GitHub and Forgejo Actions can use --format gha. SecretSpec masks every value in the runner log and appends it to $GITHUB_ENV, making the secrets available to later steps and third-party actions:

- name: Export secrets
run: secretspec export --profile production --format gha
- name: Deploy
run: ./deploy

Like non-interactive check, export never prompts and exits non-zero when a required secret is missing, so it can gate a CI job. Export attempts are also recorded in the audit log. See the export CLI reference for every format and option.

0.15 also tightens the behavior around profiles and fallback chains:

  • Provider chains are now walked strictly in order and resolved lazily. An undefined alias or unreachable fallback is skipped with a warning only when a read reaches it, so a later working provider can still answer.
  • Chain entries accept aliases, bare provider names such as keyring, shorthand such as dotenv:.env, and complete provider URIs.
  • A single destination provider rejects unsupported ref coordinates before contacting the store. Multi-provider chains still validate each destination as they reach it, because an earlier store may support coordinates a later one does not.
  • Profile overrides inherit the base secret’s description and generation type. Validation now uses the effective merged secret while still catching real conflicts, such as combining generate with a profile default.
  • run passes non-UTF-8 environment variables through to the child untouched, and command output that previously depended on map order is now stable.
  • Prebuilt Node.js addons now target glibc 2.28 and statically include libdbus, restoring support for Amazon Linux 2023, RHEL 8/9, and similar distributions.
Terminal window
cargo install secretspec

Existing providers retain their conventional environment authentication when an alias does not declare credentials. Provider credentials are opt-in, and credential dependency chains are limited to one hop.

See the full changelog for every change and fix in this release.

Questions or feedback? Join us on Discord.