seekrit
Docs/Terraform provider

Terraform provider

The seekrit Terraform provider manages your secrets infrastructure as code: applications, environments, groups, composition, service tokens, key grants, and secret values. It authenticates with an admin service token and drives the same API as the CLI and the dashboard.

The interesting part is what it does with secret values. seekrit is end-to-end encrypted, and Terraform's instinct is to write everything it touches into state — so a naive provider would leave a plaintext copy of every secret in terraform.tfstate, outside seekrit, which defeats the point. This provider instead uses the two places Terraform promises not to keep a value:

DirectionMechanismGuarantee
Writing a secretvalue_wo, a write-only argument (Terraform 1.11+)Never written to state or to the plan file.
Reading a secretan ephemeral resource (Terraform 1.10+)Held in memory for one operation, then discarded.

Encryption and decryption happen in the provider process, exactly as they do in your browser and your CLI. seekrit receives ciphertext.

note

There is deliberately no data "seekrit_secret". A data source stores its result in state, so it cannot exist here. Reading a value is always an ephemeral block.

Configure the provider

terraform {
  required_version = ">= 1.11.0"

  required_providers {
    seekrit = {
      source  = "seekritdev/seekrit"
      version = "~> 0.1"
    }
  }
}

provider "seekrit" {
  endpoint = "https://api.seekrit.dev" # or SEEKRIT_API_URL
  org_id   = "org_your_org_id"         # or SEEKRIT_ORG
  token    = var.seekrit_token         # or SEEKRIT_TOKEN (sensitive)
}

Every argument falls back to its environment variable, so CI can configure the provider without the credential appearing in your configuration.

token must be an admin service token:

seekrit token create --admin --name terraform

Admin is required because the provider creates applications, mints tokens, and grants keys. The token string embeds its own private key — that is what lets a client do the cryptography without seekrit ever holding a key.

caution

An admin token is key material: it can decrypt every environment it holds a grant on, and mint credentials that can. Scope it to one organization, keep it in your CI secret store, and rotate it by minting a replacement and revoking the old one.

Structure

resource "seekrit_application" "web" {
  name = "Web"
  slug = "web"
}

resource "seekrit_group" "shared" {
  name = "Shared"
  slug = "shared"
}

resource "seekrit_environment" "production" {
  application_id = seekrit_application.web.id
  name           = "Production"
  slug           = "production"
}

# The group's own environment. Composition matches on slug, so this is the one a
# `production` application environment inherits.
resource "seekrit_environment" "shared_production" {
  group_id = seekrit_group.shared.id
  name     = "Production"
  slug     = "production"
}

resource "seekrit_environment_group" "web_uses_shared" {
  environment_id = seekrit_environment.production.id
  group_id       = seekrit_group.shared.id
  position       = 0 # higher wins on name collisions
}

Creating an environment generates its data key in the provider and wraps it to your token, so the same configuration can go on to write secrets into it.

Credentials

A runtime credential is two resources, and both are required. Binding the token to an environment is what lets it call resolve without naming one; the key grant is what lets it decrypt anything. A bound token with no grant authenticates and then resolves nothing.

resource "seekrit_service_token" "ci" {
  name           = "ci-deploy"
  role           = "member"
  environment_id = seekrit_environment.production.id
  expires_at     = "2027-01-01T00:00:00Z"
}

resource "seekrit_environment_key_grant" "ci_production" {
  environment_id = seekrit_environment.production.id
  principal_type = "service_token"
  principal_id   = seekrit_service_token.ci.id
}

output "ci_token" {
  value     = seekrit_service_token.ci.token
  sensitive = true
}

The provider mints the keypair and the token string locally and sends seekrit only the hash and the public key. For the grant it fetches its own wrapped data key, unwraps it in-process, and re-wraps it to the recipient — so the plaintext key exists only inside the provider, and seekrit stores nothing it can open.

caution

seekrit_service_token.token is the one value this provider does put in state. A resource that creates a credential has nowhere else to keep it — the API returns it exactly once — which is the same trade aws_iam_access_key.secret makes. Use an encrypted state backend and restrict who can read state. The token cannot be recovered on import.

Secret values

variable "database_url" {
  type      = string
  sensitive = true
  ephemeral = true # Terraform refuses to persist it anywhere
}

resource "seekrit_secret" "database_url" {
  environment_id = seekrit_environment.production.id
  name           = "DATABASE_URL"

  value_wo         = var.database_url
  value_wo_version = 1
}

value_wo_version is load-bearing. Because Terraform never stores the value, it cannot see when the value changes — so a plan with an edited value_wo and an unchanged version is a no-op. Bump the version whenever the value moves.

Writing several at once needs one wrinkle: for_each must iterate something Terraform can evaluate at plan time, and an ephemeral value is not that. So the names come from a plain variable and the values from an ephemeral map:

variable "secret_names" {
  type    = set(string)
  default = ["DATABASE_URL", "STRIPE_SECRET_KEY", "SENTRY_DSN"]
}

variable "secret_values" {
  type      = map(string)
  sensitive = true
  ephemeral = true
}

resource "seekrit_secret" "app" {
  for_each = var.secret_names

  environment_id   = seekrit_environment.production.id
  name             = each.key
  value_wo         = var.secret_values[each.key]
  value_wo_version = 1
}

This is the right way round: the shape of the plan comes from the names, and only the values are secret.

Reading a secret into another provider

This is where the provider earns its place — handing a credential to another provider without it landing in state or in a plan file on the way.

ephemeral "seekrit_secret" "db_password" {
  environment_id = seekrit_environment.production.id
  name           = "DATABASE_PASSWORD"
}

resource "aws_db_instance" "app" {
  # …
  password_wo         = ephemeral.seekrit_secret.db_password.value
  password_wo_version = 1
}

An ephemeral value can also configure a provider, which is evaluated per operation and never stored:

ephemeral "seekrit_secret" "cloudflare_token" {
  environment_id = seekrit_environment.production.id
  name           = "CLOUDFLARE_API_TOKEN"
}

provider "cloudflare" {
  api_token = ephemeral.seekrit_secret.cloudflare_token.value
}

seekrit_secrets does the same for a whole environment at once — the Terraform equivalent of seekrit run, for wiring an entire environment into a task definition:

ephemeral "seekrit_secrets" "production" {
  environment_id = seekrit_environment.production.id
}
# .values is name → value (sensitive); .names is just the names.
note

seekrit_secrets returns an environment's own secrets. Values inherited from composed groups are merged by the resolve API at runtime, not here — read the group's environment directly if you need them.

Modules

Three modules cover the shapes that otherwise repeat. Source them from the provider's repository with a pinned tag:

module "shared" {
  source = "git::https://github.com/seekritdev/terraform-provider-seekrit.git//modules/group?ref=v0.1.0"

  name         = "Shared"
  slug         = "shared"
  environments = { production = {}, staging = {} }
}

module "web" {
  source = "git::https://github.com/seekritdev/terraform-provider-seekrit.git//modules/application?ref=v0.1.0"

  name = "Web"
  slug = "web"

  environments = {
    production = { groups = [module.shared.group_id] }
    staging    = { groups = [module.shared.group_id] }
  }

  # A bound, granted credential per environment — both halves, one input.
  runtime_tokens = {
    ci-production = { environment = "production" }
    ci-staging    = { environment = "staging" }
  }
}
ModuleEncapsulates
modules/applicationAn application, its environments, the groups each composes, and per-environment runtime tokens with their key grants.
modules/groupA shared group and its environments, with slugs lined up for composition.
modules/service-tokenOne token plus grants across several environments — the shape no single bound token can express.

Secret values stay in the configuration that owns them rather than becoming module inputs: a value has to arrive through an ephemeral variable, and passing it across a module boundary adds a hop without removing any constraint.

Adopting existing infrastructure

Data sources reference what Terraform does not own, so you can adopt gradually instead of importing everything at once:

data "seekrit_application" "web" {
  slug = "web"
}

data "seekrit_environment" "production" {
  application_id = data.seekrit_application.web.id
  slug           = "production"
}

# Grant a token minted in the dashboard access to a Terraform-managed environment.
data "seekrit_service_token" "existing_ci" {
  name = "ci-deploy"
}

seekrit_organization, seekrit_application, seekrit_group, seekrit_environment, and seekrit_service_token are available. None of them exposes a secret value.

Every resource imports too — by id, except the link-shaped ones:

terraform import seekrit_application.web app_2f8Kd1PqRzY
terraform import seekrit_environment_group.web_uses_shared env_9QpZ3vLmKd8:grp_7Hn2mQxLTb4
terraform import seekrit_environment_key_grant.alice env_9QpZ3vLmKd8:ek_3XcVb6NmQw1
terraform import seekrit_secret.database_url env_9QpZ3vLmKd8:DATABASE_URL

An imported secret brings the secret under management without its value — the next apply writes whatever value_wo says. An imported token can be renamed and revoked but never handed to a consumer again.

Things worth knowing

Slugs are identity. Composition and the resolve API match on slugs, so the provider treats them as immutable: changing a slug replaces the resource, and replacing an environment destroys the secrets in it. Rename the name freely. The same applies to a token's role, environment binding, and expiry — changing any of them mints a new token, because the old secret string cannot be re-issued.

The provider can only encrypt for environments it can decrypt. Writing a secret needs the environment's data key, which the provider gets by unwrapping its own grant. Environments it created have one; for an environment created elsewhere, grant the Terraform token access first — with seekrit grant, or a seekrit_environment_key_grant resource in the same configuration.

With recovery enabled, environments created by Terraform are not recovery-protected at creation, because the provider does not hold your organization's recovery key. Run seekrit recovery sync to backfill.

Agent access policy is not managed here. Policy bundles are signed in the browser by a key the server cannot forge, and publishing requires a user session precisely so an admin credential cannot widen an agent's own policy — see Agent access policy. A provider authenticating as a service token is the wrong principal by design.

Reference

Per-resource documentation, generated from the provider's schemas, lives on the Terraform Registry. For the underlying endpoints, see the API reference.