# 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:

| Direction | Mechanism | Guarantee |
| --- | --- | --- |
| Writing a secret | `value_wo`, a [write-only argument](https://developer.hashicorp.com/terraform/language/resources/ephemeral#write-only-arguments) (Terraform 1.11+) | Never written to state or to the plan file. |
| Reading a secret | an [ephemeral resource](https://developer.hashicorp.com/terraform/language/resources/ephemeral) (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

```hcl
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:

```bash
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.

> **Warning:** 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

```hcl
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.

```hcl
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.

> **Warning:** `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

```hcl
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:

```hcl
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.

```hcl
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:

```hcl
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`](/docs/guides/run), for wiring an entire environment
into a task definition:

```hcl
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](/docs/guides/environments) 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:

```hcl
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" }
  }
}
```

| Module | Encapsulates |
| --- | --- |
| `modules/application` | An application, its environments, the groups each composes, and per-environment runtime tokens with their key grants. |
| `modules/group` | A shared group and its environments, with slugs lined up for composition. |
| `modules/service-token` | One 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:

```hcl
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:

```bash
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](/docs/guides/agent-proxy/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](https://registry.terraform.io/providers/seekritdev/seekrit/latest/docs).
For the underlying endpoints, see the [API reference](/docs/reference/api).
