Skip to content

Support out-of-tree providers via gRPC interface #64

Closed

Description

@guilhem

Propose a standardized interface (e.g., gRPC) to allow implementing out-of-tree (external) providers in secretspec.

Use case: secret stores that are internal, private or closed source, enabled via an independent plugin (not part of upstream/open source).

Example scenarios:

  • secretspec contacts an external provider via a gRPC or similar interface
  • This enables organizations to add secret providers without exposing code
  • Pattern similar to secret management integrations like Vault, 1Password, SOPS, etc.

This would make it easier to integrate proprietary or internal secrets sources without patching secretspec directly.

Thanks!

Activity

  1. domenkozar commented on Mar 11, 2026

    @domenkozar
    Member

    Could be a provider called grpc that has a spec written how it works. Happy to merge that :)

  2. domenkozar commented on Mar 19, 2026

    @domenkozar
    Member

    Could also be something more lightweight like Secretspec Server Protocol using JSONRPC

  3. schickling-assistant commented on May 23, 2026

    @schickling-assistant

    +1 — landed here while working around exactly this gap. Concrete use case + a sketch of an alternative transport that might be lower-friction than gRPC for the most common case.

    Our setup

    We have an internal tool op-proxy that wraps the 1Password CLI with approval gates, audit logging, and a local cache. It's intentionally machine-local — every read is op-proxy read --reason "<text>" --cache <ttl> op://Vault/.../field, and the reason gets surfaced to a desktop approval prompt or recorded in an audit log.

    SECRETSPEC_OPCLI_PATH doesn't fit:

    • op-proxy's CLI shape differs from op (op-proxy read -r <reason> <op://...> vs op item get --format=json <name>).
    • The built-in onepassword:// provider owns its own naming (secretspec/{project}/{profile}/{key}) — we read arbitrary op://Vault/... refs.
    • --reason is required on every call; secretspec can't supply one through the current trait.

    So we patched a real opproxy:// provider into a fork (~250 lines of Rust modeled on env.rs/onepassword.rs, registered through the existing linkme::distributed_slice). Works great as a fork, but we don't want to maintain a Nix overlay rebase against every upstream release, and the provider has no place in upstream because op-proxy is intentionally machine-local.

    On the transport

    gRPC works but introduces a real dependency footprint (tonic, prost, schema versioning, codegen) and a long-lived subprocess lifecycle problem (when does the plugin server start/stop? how does it authenticate to the host? how does it pass --reason-style per-call context cleanly?).

    Two cheaper alternatives worth considering:

    1. WASM component-model plugins (wasmtime + WIT). The host loads .wasm plugins from a config-declared path; each plugin exports a small WIT interface (get(project, profile, key) -> result<option<string>, error>, optionally set, reflect). No transport, no codegen for the plugin author beyond wit-bindgen, no process lifecycle, sandboxed by default. Closest production references: Zellij plugins, Spin, OPA's Rego.

    2. Stdin/stdout subprocess plugins (like git credential helpers / docker credential helpers). Host invokes secretspec-provider-<scheme> with a small JSON request on stdin, reads JSON response. Trivial for plugin authors in any language; no codegen. Closer to the pattern most tools that already do "external provider" use. Less sandboxed than WASM but very simple.

    For our case (single-binary op-proxy), the stdin/stdout pattern is the lightest possible escape hatch and would let us delete the Nix overlay. WASM is the more principled long-term answer if you want plugin distribution as .wasm artifacts.

    Happy to prototype either against provider/mod.rs if there's interest in a direction.

  4. domenkozar commented on May 28, 2026

    @domenkozar
    Member

    Drafted a full v1 spec as a docs page so the wire contract is concrete enough to react to: #98.

    TL;DR of the direction:

    • Stdio subprocess, not gRPC, modeled on git credential helpers
    • URI scheme foo://... resolves to secretspec-provider-foo on $PATH
    • Line-delimited JSON: hello handshake with capability advertisement, then get / set / batch_get / reflect / bye
    • Structured error kinds (not_found, auth_failed, permission_denied, rate_limited, unsupported, unsupported_version, invalid_request, internal)
    • Per-session context map carries metadata like --reason, populated from a secretspec run --context key=value flag, SDK builder, or SECRETSPEC_CONTEXT_<KEY> env vars
    • Streaming / lease-refresh deferred to v2 (Dynamic secrets #11), WASM and gRPC explicitly non-goals for v1

    The page itself carries a draft warning, so merging the PR doesn't commit us to anything beyond having something to react to.

    @schickling-assistant the opproxy:// case is the load-bearing example I designed around: per-call --reason lands as context.reason in hello, missing required context fails the handshake with an invalid_request error so the user sees a clear message. Does the spec as drafted absorb the rest of your fork, or are there shapes (set semantics, multi-vault, anything else) that still need a slot?

  5. venkatd commented on Jul 5, 2026

    @venkatd

    Hi @domenkozar our team is currently evaluating devenv and cachix and one of the roadblocks we are running into is exactly what @schickling-assistant ran into.

    All of our infra references 1Password secrets via granular references op://Vault/.../field and it's not something we would be able to change. If the secretspec 1pw provider supported secret references, we wouldn't even have a desire to extend secretspec in the first place.

    If we go with secretspec's current approach with op now, it would fragment how we manage secrets.

  6. domenkozar commented on Jul 6, 2026

    @domenkozar
    Member

    @venkatd would #121 work?

  7. venkatd commented on Jul 6, 2026

    @venkatd

    Hi @domenkozar the preference would be a direct reference otherwise we'd have a lot of indirection on how we define our secrets. We'd need to declare one provider per secret if I understand right? And would we lose batch loading?

    Our current workaround is to use op run instead of the secretspec commands. We introduced our own ref keyword that we parse out:

    Example in our secretspec.toml

    [profiles.deploy-production]
    ...
    SENTRY_DSN = { description = "Sentry DSN", ref = "op://ST_BACKEND_RUNTIME/ST_BACKEND_SHARED/SENTRY_DSN" }
    ...
    

    I think fnox has a good reference on the ideal too: https://fnox.jdx.dev/providers/1password.html#referencing-secrets

  8. domenkozar commented on Jul 6, 2026

    @domenkozar
    Member

    @schickling-assistant I've added #122 for approval gates and #121 for arbitrary refs. Would that remove the need for your wrapper (we should still ship it, but these things should be part of secretspec anyway).

  9. added 6 commits that reference this issue on Jul 8, 2026
    f7e18f3
    39604dc
    f86e297
    785d552
    468d46b
    9313621
  10. added a commit that references this issue on Jul 12, 2026
    b9795f2
  11. venkatd commented on Jul 14, 2026

    @venkatd

    @domenkozar this is great! We're about to sign up for a cachix plan :)

  12. djbclark commented on Aug 14, 2026

    @djbclark
    Contributor

    Closing #345 in favor of this tracking issue, as this perfectly captures the plugin architecture we need!

    We recently built sudo-secretspec (#345) to solve the security pain point of AI agents running locally as the user and having unfettered read-access to secrets. Our solution uses an isolated service-user vault and a sudo companion binary to enforce a physical boundary. We were about to propose a generic exec:// provider to allow upstream to integrate with us cleanly, but it looks like you are already designing the ideal solution here.

    Regarding the discussion and the proposed spec in #98, here is how it relates to our use case:

    1. Transport (Stdin/stdout subprocess vs WASM vs gRPC): The stdio subprocess model is by far the best option for us. WASM's strict sandboxing would make it extremely difficult (or impossible) for us to invoke sudo to transition to our service-user and read the isolated filesystem vault. The Git Credential Helper model is the perfect amount of flexibility for OS-level privilege boundaries.
    2. Context Map (--reason): Your inclusion of the context.reason in the hello handshake is fantastic. Our entire service-user boundary relies on producing an immutable SQLite ledger of why the AI requested the secret. Having secretspec natively forward the --reason flag via the IPC context map makes our integration seamless.
    3. Distribution (secretspec-provider-foo): The implicit $PATH resolution fits perfectly into our Homebrew packaging. We can simply ship our binary as secretspec-provider-sudo and users can use sudo:// in their manifests.

    This direction fully supports our architecture without requiring any upstream macOS-specific maintenance. We are very excited for this spec to land!

  13. added a commit that references this issue on Aug 14, 2026
  14. domenkozar commented on Aug 16, 2026

    @domenkozar
    Member

    See #362

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions