secretspec

package module
v0.20.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Aug 31, 2026 License: Apache-2.0 Imports: 14 Imported by: 0

README

secretspec (Go SDK)

Go bindings for SecretSpec, a declarative secrets manager. A thin client over the libsecretspec C ABI. Resolution happens in the Rust core, so the SDK inherits every provider with no Go-side logic. By default the resolver is loaded at runtime via purego (dlopen, no cgo), keeping go get toolchain-free. Use -tags static to stage and embed the archive, or -tags pkgconfig (0.19+) to link an installed library (see below).

The embedded ABI is named libsecretspec in SecretSpec 0.20+. It was named secretspec-ffi through 0.19; the 0.20+ loader accepts both shared-library filename families.

package main

import (
	"fmt"
	"log"

	secretspec "github.com/cachix/secretspec/secretspec-go"
)

func main() {
	resolved, err := secretspec.New().
		WithProvider("keyring://").
		WithProfile("production").
		WithReason("boot web app").
		Load()
	if err != nil {
		log.Fatal(err)
	}

	fmt.Println(resolved.Provider, resolved.Profile)
	db := resolved.Secrets["DATABASE_URL"]
	fmt.Println(db.Get()) // the value, or the file path for as_path secrets
	resolved.SetAsEnv()   // export everything into the process environment
}

A missing required secret returns *MissingRequiredError; any other failure returns *Error (with a stable .Kind).

Inline specifications (0.20+)

Use WithInlineSpec(spec, baseDir) to resolve a strict JSON declaration held in application code. baseDir resolves relative provider paths, and an older native library fails with a capability error rather than searching for a filesystem manifest. The inline v1 document uses project, profiles, and a secrets object in each profile. project.extends resolves parent manifests relative to the supplied logical base directory.

spec := map[string]any{
	"project": map[string]any{"name": "my-app"},
	"profiles": map[string]any{"default": map[string]any{
		"secrets": map[string]any{
			"API_TOKEN": map[string]any{"description": "API token"},
		},
	}},
}
resolved, err := secretspec.New().WithInlineSpec(spec, "/logical/project").Load()

Scopes (0.17+)

Use WithScope("api") to resolve only a named [scopes.api] subset. Both Resolved.Scope and Report.Scope return the selected scope:

resolved, err := secretspec.New().WithScope("api").Load()

Cleanup

as_path secrets are materialized to temp files that outlive the call. Call resolved.Close() (e.g. defer resolved.Close()) when done so the secret files do not accumulate in the temp dir.

Value-free report

Report() returns the inventory/preflight view: per-secret status and provenance, never a value. Unlike Load(), it does not fail when a required secret is missing — it appears as a SecretReport with Status "missing_required".

report, _ := secretspec.New().WithProfile("production").Report()
for _, s := range report.Secrets {
	fmt.Println(s.Name, s.Status, s.Required)
}

Binding the native resolver

Default: purego (dlopen, no cgo)

The libsecretspec cdylib is resolved at runtime, in order:

  1. The SECRETSPEC_FFI_LIB environment variable (an explicit path).
  2. A library embedded at build time with -tags embed_lib.
  3. A Cargo target directory found by searching up from the working directory (the development path).

This keeps go get toolchain-free; the cdylib is loaded at runtime rather than linked. Provide it via SECRETSPEC_FFI_LIB / a Cargo checkout, or stage the per-platform library into lib/ and build -tags embed_lib (embedded via go:embed, extracted to a per-user, owner-only cache directory at first use). Neither the cdylib nor the archive is shipped through the Go module proxy (which does not carry binary assets); they are attached to GitHub releases.

-tags static: cgo, statically linked

For a self-contained binary with no runtime library to locate, link the resolver statically. This uses cgo (a C toolchain is required) and links libsecretspec.a directly into the Go binary:

# Stage the archive + header + generated cgo LDFLAGS, then build with cgo.
bash scripts/stage-staticlib.sh
CGO_ENABLED=1 go build -tags static ./...

On Linux this can be made fully static (no dynamic libraries at all) by building the archive for a musl target and passing the static link flags:

SECRETSPEC_FFI_TARGET=x86_64-unknown-linux-musl \
  SECRETSPEC_FFI_PROFILE=release bash scripts/stage-staticlib.sh
CGO_ENABLED=1 go build -tags static \
  -ldflags '-linkmode external -extldflags "-static"' ./...

macOS links the archive in but stays self-contained-except-system-frameworks (no static libSystem). Windows stays on the default purego path. The prebuilt archives are attached to GitHub releases (go-static.yml).

Linking with pkg-config (0.19+)

Install one library type with cargo-c:

# Use "static" (the default) or "shared"; use separate prefixes for both.
bash libsecretspec/scripts/cinstall.sh "$PREFIX" static

Then use the same build command for either type:

PKG_CONFIG_PATH="$PREFIX/lib/pkgconfig" CGO_ENABLED=1 go build -tags pkgconfig ./...

Unlike staging, this also works for a go get dependency. A shared install in a non-system prefix also requires PREFIX/lib in the platform's runtime library search path.

Documentation

Overview

Package secretspec is a Go SDK for SecretSpec, a declarative secrets manager.

It is a thin client over the libsecretspec C ABI. Resolution (providers, chains, profiles, generation, as_path) happens entirely in the Rust core; this package marshals a JSON request to secretspec_resolve, parses the response envelope, and exposes it with the same vocabulary as the Rust derive crate.

Three build modes select the native resolver:

  • default (no build tag): purego (dlopen, no cgo). The library is located via the SECRETSPEC_FFI_LIB environment variable, an embedded copy, or a Cargo target directory. This keeps `go get` toolchain-free.
  • `-tags static`: cgo statically links libsecretspec.a, so the resolver is embedded in the Go binary (fully static on Linux/musl).
  • `-tags pkgconfig` (0.19+): cgo links the installed static or shared library described by libsecretspec.pc. See README.

All bindings implement the same hooks (ensureLoaded, nativeResolve, nativeABIVersion); the code below is binding-agnostic.

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func ABIVersion

func ABIVersion() (string, error)

ABIVersion returns the version reported by the native resolver.

Types

type Builder

type Builder struct {
	// contains filtered or unexported fields
}

Builder configures a resolution, mirroring the derive crate's SecretSpec::builder().

func New

func New() *Builder

New starts a resolution builder.

func (*Builder) Load

func (b *Builder) Load() (*Resolved, error)

Load resolves the secrets. It returns *MissingRequiredError if a required secret is missing, and *Error for any other failure.

func (*Builder) Report

func (b *Builder) Report() (*Report, error)

Report resolves the value-free report (the inventory/preflight view, the same one the CLI exposes as `check --json`). It never returns *MissingRequiredError: a missing required secret appears as a SecretReport with Status "missing_required". It returns *Error for a genuine failure.

func (*Builder) WithCaller added in v0.20.0

func (b *Builder) WithCaller(caller CallerContext) *Builder

func (*Builder) WithInlineSpec added in v0.20.0

func (b *Builder) WithInlineSpec(spec any, baseDir string) *Builder

WithInlineSpec resolves a strict, versioned inline declaration instead of a filesystem manifest. `spec` is serialized as the native inline-spec v1 document (project/profiles/secrets); `baseDir` resolves relative provider paths just like the directory of a manifest. Available since SecretSpec 0.20.

The native library must export `secretspec_call`. If it is older, Load and Report return a capability error rather than falling back to a manifest search.

func (*Builder) WithNoValues

func (b *Builder) WithNoValues(v bool) *Builder

func (*Builder) WithPath

func (b *Builder) WithPath(path string) *Builder

func (*Builder) WithProfile

func (b *Builder) WithProfile(p string) *Builder

func (*Builder) WithProvider

func (b *Builder) WithProvider(p string) *Builder

func (*Builder) WithReason

func (b *Builder) WithReason(reason string) *Builder

func (*Builder) WithScope added in v0.17.0

func (b *Builder) WithScope(scope string) *Builder

WithScope limits resolution to a named manifest scope (SecretSpec 0.17+).

type CallerContext added in v0.20.0

type CallerContext struct {
	Name      string `json:"name"`
	Version   string `json:"version,omitempty"`
	Operation string `json:"operation,omitempty"`
	Resource  string `json:"resource,omitempty"`
}

CallerContext identifies the software integration invoking SecretSpec. It is caller-asserted audit metadata and never supplies an access reason. Available since SecretSpec 0.20.

type Error

type Error struct {
	Kind    string
	Message string
}

Error is a resolution failure (bad manifest, provider error, reason policy).

func (*Error) Error

func (e *Error) Error() string

type MissingRequiredError

type MissingRequiredError struct {
	Missing []string
}

MissingRequiredError reports required secrets that were not found anywhere.

func (*MissingRequiredError) Error

func (e *MissingRequiredError) Error() string

type Report

type Report struct {
	Provider string
	Profile  string
	// Scope is the selected manifest scope, or nil for a full-profile report (0.17+).
	Scope   *string
	Secrets []SecretReport
}

Report is a value-free resolution snapshot: every declared secret and how it would resolve, never a value. Unlike Load, a missing required secret is reported as a SecretReport with Status "missing_required" rather than an error, so it describes a profile even when its secrets are not all available.

type Resolved

type Resolved struct {
	Provider string
	Profile  string
	// Scope is the selected manifest scope, or nil for a full-profile resolve (0.17+).
	Scope           *string
	Secrets         map[string]ResolvedSecret
	MissingOptional []string
}

Resolved is a successful resolution, mirroring the Rust Resolved wrapper.

func (*Resolved) Close

func (r *Resolved) Close() error

Close removes the temp files backing any as_path secrets in this result. The resolver persists those files (mode 0400) so their paths stay valid after resolve returns; the caller owns their lifetime. Call it (e.g. `defer resolved.Close()`) when done so secret files do not accumulate in the temp dir. Non-as_path secrets and a no_values result hold no path and are skipped, and a file already gone is not an error.

func (*Resolved) Fields

func (r *Resolved) Fields() map[string]*string

Fields returns a flat map of SECRET_NAME -> value (the file path for as_path). A secret with no usable value (e.g. under no_values) maps to a nil pointer, which marshals to JSON null, matching the null the Python, Ruby, and Node SDKs emit; the value is a non-nil pointer otherwise.

func (*Resolved) FieldsJSON

func (r *Resolved) FieldsJSON() ([]byte, error)

FieldsJSON marshals Fields() to JSON (a `{SECRET_NAME: value-or-null}` object), the input for a quicktype-generated deserializer (e.g. UnmarshalSecretSpec). See `secretspec schema`.

func (*Resolved) SetAsEnv

func (r *Resolved) SetAsEnv() error

SetAsEnv exports each resolved secret into the process environment by name. Secrets with no usable value (e.g. under no_values) are skipped rather than exported as an empty string.

type ResolvedSecret

type ResolvedSecret struct {
	Value          *string
	Path           *string
	AsPath         bool
	Source         string
	SourceProvider *string
}

ResolvedSecret is one resolved secret. Exactly one of Value / Path is set.

func (ResolvedSecret) Get

func (s ResolvedSecret) Get() string

Get returns the usable string: the file path for as_path secrets, else the value. It is the empty string when no usable value is present; use Usable to distinguish an absent value from a genuinely empty one.

func (ResolvedSecret) Usable

func (s ResolvedSecret) Usable() (string, bool)

Usable returns the secret's usable string and whether one is present: the file path for as_path secrets, otherwise the value. Both are absent when a value-less response (e.g. no_values) strips them, in which case ok is false. This is the null-aware accessor; the other SDKs express the same thing as a get() that returns null/None/nil.

type SecretReport

type SecretReport struct {
	Name           string
	Status         string // "resolved" | "missing_required" | "missing_optional"
	Required       bool
	SourceProvider *string
	DefaultApplied bool
	Generated      bool
	AsPath         bool
}

SecretReport is the value-free resolution outcome for one declared secret: how it would resolve and from where, never the value itself.

Directories

Path Synopsis
examples
quick_start command
scopes command
typed_access command

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL