SecretSpec Documentation

repository·main·Indexed 21 days ago

https://github.com/cachix/secretspec

A declarative secrets management tool that separates secret declaration from storage using a `secretspec.toml` manifest. It provides SDKs for Python, Go, Ruby, Node.js, Haskell, C#, PHP, and Swift, allowing developers to resolve secrets from secure backends like system keyrings, 1Password, or cloud secret managers. Features include a cross-language conformance suite, a canonical result format, and Rust-based code generation via the `declare_secrets!` macro.

Tokens
107.7K
Snippets
359
Records
474
Agent score
72%

What's inside SecretSpec

  1. Use configuration inheritance with the extends field

    main

    You can share common secret definitions across multiple projects or services using the extends field in the [project] section. This allows a project to inherit secret requirements from one or more base configuration files.

    For example, a web-api project can inherit common database and authentication requirements from shared configuration files and then add its own service-specific secrets.

    [project]
    name = "web-api"
    revision = "1.0"
    extends = ["../shared/base", "../shared/auth"]
    
    [profiles.default]
    # Inherits DATABASE_URL, INTERNAL_API_KEY from base
    # Inherits JWT_SECRET, SESSION_SECRET from auth
    # Service-specific additions:
    STRIPE_API_KEY = { description = "Stripe payment API", required = true }
    REDIS_URL = { description = "Redis cache connection", required = true }
    PORT = { description = "Server port", required = false, default = "3000" }
  2. Create composed secrets

    main

    A composed secret (available since 0.16) derives a value from other secrets in the effective profile using ${UPPERCASE_NAME} references. Composed secrets are read-only and cannot use default, providers, ref, type, or generate.

    Composition Rules

    • Syntax: Only ${UPPERCASE_NAME} is supported. Ambient environment variables are ignored.
    • No Shell Expansion: Does not support ${NAME:-fallback}, commands, or recursive expansion.
    • Escaping: Use $$ to produce a literal $ (e.g., $${NAME} renders ${NAME}).
    • Dependency Model: Forms a static dependency graph. Cycles and unknown references are rejected.
    • Encoding: Composition is raw string concatenation. It does not perform URL or JSON encoding. You must store components in the format required by the target.
    • as_path interaction: If a dependency uses as_path = true, the composed secret will contain the text of the temporary file path.
    [profiles.default]
    DB_USER = { description = "Database user" }
    DB_PASSWORD = { description = "Database password" }
    DB_HOST = { description = "Database host" }
    DATABASE_URL = { description = "PostgreSQL DSN", composed = "postgres://${DB_USER}:${DB_PASSWORD}@${DB_HOST}/app" }
  3. Specify providers using names, URIs, or aliases

    main

    When configuring secrets or running commands, you can specify a provider in one of three ways:

    1. Provider Name: A simple identifier like keyring or env.
    2. Provider URI: A string that configures a specific instance of a provider, such as dotenv://.env.local or onepassword://Production.
    3. Provider Alias: A meaningful, store-independent name defined in your project or user configuration (e.g., prod_vault).

    Aliases are recommended when a specific URI is shared by multiple secrets or when you want to decouple your secret definitions from the specific storage backend implementation.

    # Using an alias in secretspec.toml
    [providers]
    prod_vault = "onepassword://Production"
    
    [profiles.production]
    DATABASE_URL = { description = "Production database", providers = ["prod_vault"] }
  4. Understand the Starlight project structure

    main

    A standard Starlight project follows this directory structure:

    • src/content/docs/: This is where your documentation lives. Starlight automatically treats .md or .mdx files in this directory as routes based on their filenames.
    • src/assets/: Place images here to embed them in your Markdown using relative links.
    • public/: Place static assets like favicons here.
    • astro.config.ts: The main configuration file for Astro.
    • package.json: Defines project dependencies and scripts.
    .
    ├── public/
    ├── src/
    │   ├── assets/
    │   ├── content/
    │   │   ├── docs/
    │   │   └── content.config.ts
    ├── astro.config.ts
    ├── package.json
    └── tsconfig.json
  5. The SecretSpec design principle: Separating configuration from secrets

    main

    SecretSpec is built on the principle that configuration (behavioral settings) and secrets (credentials, API keys, tokens) should have separate lifecycles and interfaces.

    • Configuration belongs in version control (git), code reviews, and developer machines.
    • Secrets grant authority and require restricted access, independent rotation, and specialized providers.

    Instead of mixing secrets into configuration files (like .dhall, .yaml, or .json), SecretSpec allows you to declare the requirement for a secret in a secretspec.toml file without storing the actual value. This allows the same application to run in different environments (local, CI, production) by using different Providers to resolve those requirements without changing the application's configuration.

  6. Coordinate structure for secret references

    main

    Secret references use provider-independent coordinates to address secrets. This abstraction allows the same reference to work across different providers in a fallback chain. The hierarchy of coordinates is:

    • vault: Which container holds the item (e.g., 1Password only).
    • item: The store's own name for the secret (Required).
    • section: A named group of fields (e.g., 1Password only).
    • field: One component inside the item (for structured stores).
    • version: Which revision to read (e.g., GCSM only).

    Because these are provider-independent, SecretSpec follows the standard provider resolution order (CLI override $\rightarrow$ secret's providers chain $\rightarrow$ profile/global defaults) to find a provider that can interpret the coordinates.

  7. Use the Pass provider for local GPG-encrypted storage

    main

    The pass provider integrates with the Unix password manager pass (password-store) to store secrets using GPG encryption. It is ideal for secure local development. It supports both read and write access.

    Key Details:

    • Provider Name: pass
    • URI Format: pass://[folder_prefix][?store_dir=/path/to/store]
    • Default Storage Path: secretspec/{project}/{profile}/{key}
    • Authentication: Uses the GPG key configured for your pass store.
    pass://[folder_prefix][?store_dir=/path/to/store]
  8. How missing and empty dependencies affect composed secrets

    main

    SecretSpec distinguishes between a dependency being empty and a dependency being missing:

    • Empty dependency: If a dependency resolves to an empty string, the composed secret will contain that empty string in its template.
    • Missing dependency: If a dependency is missing (unresolved by a provider):
      • By default, the composed secret itself becomes missing.
      • If the composed secret has required = false set, the missing dependency causes the composed result to be omitted instead of failing.

    When running secretspec check, the tool will prompt you to resolve the underlying provider-backed dependencies that are causing the composition to fail.

  9. Reference existing Google Cloud secrets

    main

    If you want to use a secret that already exists in Google Cloud Secret Manager rather than letting secretspec manage it, use the ref field. Note that references are read-only.

    • item: The name/ID of the secret in GCP.
    • version: (Optional) Pins a specific version of the secret. Defaults to latest.

    Note: The field option is not supported for references.

    Example Configuration

    [profiles.production]
    # Uses the latest version of 'database-url'
    DATABASE_URL = { description = "DB", ref = { item = "database-url" }, providers = ["gcsm://my-gcp-project"] }
    
    # Uses version '3' of 'signing-key'
    SIGNING_KEY = { description = "Key", ref = { item = "signing-key", version = "3" }, providers = ["gcsm://my-gcp-project"] }
    [profiles.production]
    DATABASE_URL = { description = "DB", ref = { item = "database-url" }, providers = ["gcsm://my-gcp-project"] }
    SIGNING_KEY = { description = "Key", ref = { item = "signing-key", version = "3" }, providers = ["gcsm://my-gcp-project"] }
  10. Use composed secrets to derive values

    main

    Composed secrets allow you to derive a read-only value (like a connection string) from other secrets declared in your secretspec.toml manifest. This is useful when secret stores manage individual components (e.g., username, password, host) but your application requires them assembled into a single string.

    Key properties:

    • Compositions are read-only.
    • They can build on other compositions.
    • SecretSpec checks for missing references and circular dependencies before resolution.
    • The resulting value behaves like any other resolved secret in the CLI and SDKs.
    [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"
    }
  11. Use profile inheritance and file extension in secretspec.toml

    main

    SecretSpec supports hierarchical configuration through inheritance:

    • Profile Inheritance: Every profile inherits from [profiles.default]. Specific profile values take precedence over defaults.
    • File Inheritance: You can use the extends field within the [project] section to inherit configuration from other secretspec.toml files. This is useful for sharing base configurations across multiple projects or directories.
    [project]
    extends = "../base/secretspec.toml"
    
    [profiles.default]
    provider = "aws"
    
    [profiles.dev]
    provider = "local"
    # This overrides the 'aws' provider from [profiles.default]