Skip to content

Tailscale Setec Provider

The Tailscale Setec provider stores versioned secrets in a lightweight service reached and authorized through your tailnet.

Providersetec
URIsetec://HOST[:PORT][?prefix=PATH][&tls=false]
AccessRead, write, delete, and discovery
Best forSelf-hosted secrets shared with tailnet workloads
AuthenticationTailscale identity and Setec grants
Build featuresetec
Default storagesecretspec/{project}/{profile}/{key}

Complete the setup, then point SecretSpec at the Setec server:

Terminal window
$ secretspec set DATABASE_URL --provider setec://secrets.example.ts.net
Enter value for DATABASE_URL: postgresql://localhost/mydb
Secret 'DATABASE_URL' saved to setec (profile: default)
$ secretspec get DATABASE_URL --provider setec://secrets.example.ts.net
$ secretspec run --provider setec://secrets.example.ts.net -- npm start
  • A running Setec server reachable from the machine’s tailnet
  • A Tailscale policy granting the caller the required Setec actions
  • A SecretSpec build containing the setec feature (included by default in 0.21+)

Setec authenticates each request using its Tailscale connection identity. No API token, password, or SecretSpec provider credential is required.

Grant only the actions needed by the workflow:

SecretSpec operationSetec actions
get, check, or runget
set or persistent generationput, activate
deleteinfo, delete
init --frominfo

Setec’s put operation appends an inactive version when a secret already exists. SecretSpec follows it with activate, making the written value the one returned by subsequent unpinned reads.

See Setec’s API and permission documentation and server setup guide.

setec://HOST[:PORT][?prefix=PATH][&tls=false]
  • HOST is required and normally uses the Setec server’s tailnet DNS name.
  • PORT selects a non-default server port.
  • prefix prepends a slash-separated namespace to convention-created names.
  • HTTPS is the default. Set tls=false explicitly for a local development server; do not use plaintext HTTP across an untrusted network.

URI paths, userinfo, fragments, and unknown query parameters are rejected.

setec://secrets.example.ts.net
setec://secrets.example.ts.net:8443
setec://secrets.example.ts.net?prefix=platform
setec://127.0.0.1:8080?prefix=local&tls=false

Check a provider alias into secretspec.toml so the server and namespace are shared by the team:

secretspec.toml
[providers]
production_setec = "setec://secrets.example.ts.net?prefix=platform"
[profiles.production]
DATABASE_URL = { description = "Production database", providers = ["production_setec"] }

Convention-managed secrets use this Setec name:

[prefix/]secretspec/{project}/{profile}/{key}

For example, DATABASE_URL in project myapp, profile production, and a platform prefix is stored as platform/secretspec/myapp/production/DATABASE_URL.

Each set appends a Setec version and activates the returned version. If the write succeeds but activation fails, SecretSpec reports the stored version so an operator can inspect or activate it. SecretSpec does not cache Setec values; each resolution reads the server’s current active version unless a reference pins one.

Set ref.item to the exact Setec secret name. Add a positive integer ref.version to read a historical version instead of the active one:

secretspec.toml
[providers]
setec_prod = "setec://secrets.example.ts.net"
[profiles.production]
API_TOKEN = { description = "Current API token", ref = { item = "prod/api-token" }, providers = ["setec_prod"] }
OLD_SIGNING_KEY = { description = "Signing key version 4", ref = { item = "prod/signing-key", version = "4" }, providers = ["setec_prod"] }

Unversioned references can be written and deleted. Version-pinned references are read-only: writing creates a new Setec version, while SecretSpec deletion represents deleting the complete secret rather than one historical version.

Setec discovery lists metadata only and never copies secret values into the manifest. It is bounded to the convention namespace for the requested project and profile:

Terminal window
$ secretspec init --from setec://secrets.example.ts.net?prefix=platform \
--project myapp --profile production

Only direct children of platform/secretspec/myapp/production/ become declarations. Other projects, profiles, and nested names are ignored. The caller needs Setec info access for names it should discover.

Join the runner or workload to the tailnet, grant its tagged Tailscale identity the minimum Setec actions, and use the same provider URI as local clients. No long-lived Setec credential needs to be injected into the job.

Terminal window
$ secretspec run --provider setec://secrets.example.ts.net -- deploy
  • Setec’s access decisions come from Tailscale identity and grants; access to the hostname alone does not grant a Setec action.
  • SecretSpec disables redirects so a Setec request body cannot be replayed to an origin selected by a server response.
  • tls=false sends secret values over plaintext HTTP and is intended only for isolated local development.
  • SecretSpec preserves arbitrary bytes in Setec values (0.21+). Text-only consumers still require valid UTF-8.
  • A write can create a new inactive version before activate fails. The error identifies this partial result; the previously active value remains active.