Skip to main content
The official Terraform provider manages KnoxCall resources through the /v1 Management API. It is built with the Terraform Plugin Framework and speaks plugin protocol 6. This page is the guide: how to install it, how it authenticates, and what it does with your secrets. Per-attribute reference documentation is generated from the provider schema and ships with the provider.
Not published yet. The provider is not on the Terraform Registry, not on the OpenTofu registry, and has no released version. A terraform init against source = "knoxcall/knoxcall" fails today, because there is nothing there to resolve. The only supported way to run it is a local build behind a dev_overrides block — see Install below.

Requirements

  • Terraform >= 1.11 or OpenTofu >= 1.11 for any resource carrying a write-only attribute — which is every resource that accepts secret material. The two ecosystems shipped write-only attributes in different years; 1.11 is the floor in both.
  • Terraform >= 1.10 for the ephemeral resource.
  • A KnoxCall tenant, and an API key or OAuth client for it.

Install

Because nothing is published, Terraform cannot fetch the provider. Build it and point the CLI at the binary:
Then add a dev_overrides block to the CLI configuration file — ~/.terraformrc on Linux and macOS, %APPDATA%\terraform.rc on Windows:
Do not run terraform init in a configuration that uses an overridden provider. Init tries to resolve knoxcall/knoxcall from the registry, where it does not exist, and fails. Run terraform plan and terraform apply directly; Terraform prints a warning that overrides are in effect and skips the dependency lock file. If a stale .terraform.lock.hcl names this provider, delete it.dev_overrides loads whatever binary is on disk at plan time, with no version check, so rebuild after every change.

Authenticate

The 90% path is an OAuth client, and it is the default:
Alternatively configure a single api_key (or $KNOXCALL_API_KEY) — a tenant API key (tk_…) or a pre-minted OAuth token (kc_…). It conflicts with client_id/client_secret. Even with api_key, the default auth_mode = "auto" still authenticates with OAuth: the key is used as client credentials (its tk_… prefix is the client id, the whole key is the secret) to mint a short-lived access token, and only falls back once — with a warning — to sending the raw key as a bearer token if the token endpoint answers invalid_client.
auth_mode = "bearer" is the legacy escape hatch. On a tenant with bootstrap_only_keys — the default for recently created tenants — every bearer request fails with 401 use_oauth. If you are starting today, use OAuth.
tenant_slug deliberately has no environment-variable fallback. An assertion whose expected value comes from the same environment as the credential asserts nothing.

Sandbox

Live and Test are separate data spaces with separate credentials. One provider block manages one (tenant, data space) pair, so managing both means two provider blocks:
sandbox = true selects the Test base URL. An explicit base_url wins over the flag. The credential must belong to the data space it is pointed at — a mismatch is a server-side 403 wrong_key_type, not a silent cross-space write.

Secrets and Terraform state

What the provider will not write to state

There is no plain value attribute on any secret resource — not a sensitive one, not an optional one, not a deprecated one. Secret material you supply is supplied through write-only attributes: Terraform reads them from your configuration, the provider sends them to the API, and the framework nullifies them before they can reach a plan file, a state file, or the raw terraform.tfstate bytes. The /v1 API never returns a stored secret value, so there is nothing to read back on refresh either. Each write-only attribute is paired with a persisted _wo_version integer. Terraform can never see a write-only value, so that integer is the entire mechanism by which a rotation is expressible:
The proof, not just the claim. An acceptance gate in the provider’s own test suite (internal/provider/state_safety_gate_test.go) iterates every registered write-only attribute and, for each, applies a real configuration carrying a unique sentinel value, then requires that sentinel to appear nowhere in the parsed state, nowhere in the plan at either pre-apply or post-refresh, and nowhere in the raw bytes on disk — while separately proving the value did reach the API, because a provider that silently dropped every write-only value would otherwise score perfectly. A host-side scanner then checks the server’s own idempotency and audit stores for the same sentinel. The gate is exercised on Terraform 1.11.4 — the floor, which also proves the write-only cases skip rather than error on an older CLI — and on the current release.

What is in state, and why

Four attributes hold credentials the platform mints and returns exactly once. They are Computed and Sensitive — redacted in CLI output, written to state in cleartext: This is a limit of the plugin framework, not a choice: Computed and WriteOnly are mutually exclusive, so no schema both captures a value the server generates and refuses to persist it. Rather than leave it implicit, each of the four is registered in internal/provider/wo_guarded_attributes.go with a declared classification and the plan item that would remove it, and a guard fails the build if an attribute carrying secret material is neither write-only nor listed there — or if a listed exception outlives the attribute it excused. The recommended webhook configuration is therefore the one with no signing secret in it at all:

Two blind spots you need to know about

Both are properties of write-only attributes in general rather than of this provider, and both will bite somebody who has not read them.
1. An out-of-band rotation is invisible to terraform plan unless you pin value_version. Terraform never holds the value, so it cannot diff it. Left unset, knoxcall_secret.value_version reports a server-side rotation only in refresh output — never as a planned action, and not at all under terraform plan -refresh=false, which is standard in CI.Pin it and the same rotation becomes a real diff with a non-zero terraform plan -detailed-exitcode:
A pinned version is an assertion, not an instruction — no API sets the counter. If it stops matching, the apply fails and names the two ways out: re-pin to accept the rotation, or bump value_wo_version to rotate through Terraform. It is deliberately suppressed for secret_type = "custodial", where KnoxCall owns the value and may rotate it hourly.
2. Editing a write-only value without bumping its _wo_version companion is inert. The new value is never sent, no diff appears, and the apply reports success. Drive the value and its version from one variable where you can.
Two consequences worth stating once: the value your configuration declares is intent, value_version is reality, and the tenant audit log is history — every rotation Terraform performs is recorded server-side as a rotate event, which is where the change history lives given a plan cannot show it. And a secret’s value is held per environment server-side, but the provider models only the base environment’s value today; there is no resource for a per-environment value override.

Resources

Every resource supports create, read, delete and terraform import. All but two also update in place: KnoxCall has no PATCH for route actions or API keys, so every argument of knoxcall_route_action and knoxcall_api_key forces a replacement rather than an in-place edit. Import IDs are frozen ahead of first publish, because an import ID is a public interface. The write-only: prefix seeds the _wo_version companion to 1, so a configuration that already declares the pair at version 1 gives an empty first plan rather than proposing to rewrite the credential:
No import form recovers a secret’s value — KnoxCall never returns a stored value, which is the property the resource exists to preserve. Put the value back in configuration as value_wo; it is not re-sent until value_wo_version changes. For the same reason an imported knoxcall_webhook has a null secret_key, and an imported knoxcall_client_credential a null private_key_pem. Data sourcesknoxcall_account, knoxcall_client, knoxcall_environment, knoxcall_environments, knoxcall_route, knoxcall_secret, knoxcall_secrets, knoxcall_webhook_event_types. None of them can return secret material, and a test drives every one against a stub returning credential-shaped fields and then searches the state artifact to prove it. Ephemeral resourcesknoxcall_oauth2_access_token hands the live upstream access token behind a knoxcall_secret_oauth2 to the consuming resource for the duration of one operation, and never writes it to state. KnoxCall owns the refresh, so the upstream refresh token never leaves the platform.
Terraform opens ephemeral resources during plan when every argument is known, so a plan mints a real token; referencing an unknown attribute — as above, the id of a resource created in the same run — defers the fetch to apply. Do not route the token through an output: outputs are stored in state, and Terraform refuses an ephemeral value there unless the output is itself declared ephemeral = true.

Behavior

The provider’s API client follows the SDK parity standard where applicable:
  • Retries: 408/429/500/502/503/504 retried up to 3 attempts with half-jitter exponential backoff (100ms base, 5s cap). 409 is never retried. On 429 a server Retry-After is honored, capped at 30s.
  • Idempotency: every mutating request carries a ULID X-Idempotency-Key generated once per logical request — stable across retries, so transport failures are also safe to retry.
  • 401: access tokens are minted via the client_credentials grant and refreshed proactively before expiry; an unexpected 401 on a resource request purges the cached token and re-mints once before failing with a clear diagnostic. Under auth_mode = "bearer" there is nothing to re-mint, so a 401 fails fast.
  • Secret hygiene: api_key and client_secret are Sensitive in the schema and never appear in errors, logs, or diagnostics (client_id is a public identifier and stays legible so a rejected client is identifiable).
  • Cancellation: all requests and retry backoffs respect Terraform’s operation context — interrupts abort promptly.
Every error diagnostic ends with the request id from the /v1 response envelope:
Quote it in a support request or a bug report; it is what correlates your failure with the server’s own logs.

Where to go next