Platform
ScaiWave ScaiGrid ScaiCore ScaiBot ScaiDrive ScaiKey Models Tools & Services
Solutions
Organisations Developers Internet Service Providers Managed Service Providers AI-in-a-Box
Resources
Support Documentation Blog Downloads
Company
About Research Careers Investment Opportunities Contact
Log in

Terraform

Provision ScaiVault resources — secrets, policies, rotation policies, PKI roles, dynamic engines — declaratively. The Terraform provider wraps the same REST API the SDKs use, so what you can do via curl you can do via tf apply.

Install the provider#

hcl
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
terraform {
  required_providers {
    scaivault = {
      source  = "scailabs/scaivault"
      version = "~> 1.0"
    }
  }
}

provider "scaivault" {
  base_url      = "https://scaivault.scailabs.ai"
  token         = var.scaivault_token  # or env SCAIVAULT_TOKEN
  partner_id    = var.partner_id       # optional
  tenant_id     = var.tenant_id        # optional
}

For Terraform Cloud / Atlantis / CI runs, set SCAIVAULT_TOKEN as an environment variable. Don't put it in .tfvars.

Anti-pattern: managing secret values in Terraform#

hcl
1
2
3
4
5
6
7
# DON'T DO THIS
resource "scaivault_secret" "stripe" {
  path = "environments/production/billing/stripe"
  data = {
    secret_key = "sk_live_xxx"  # secret in state file
  }
}

The secret value ends up in Terraform state, defeating the point of using ScaiVault. Two better patterns:

Pattern A: Provision the metadata (path, type, rotation policy) but not the value. Use lifecycle.ignore_changes on data:

hcl
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
resource "scaivault_secret" "stripe" {
  path        = "environments/production/billing/stripe"
  secret_type = "api_key"
  metadata = {
    tags  = ["billing", "stripe", "production"]
    owner = "team:billing"
  }
  options = {
    rotation_policy_id = scaivault_rotation_policy.api_keys_quarterly.id
    max_versions       = 10
  }
  lifecycle {
    ignore_changes = [data]
  }
  data = {
    secret_key = "PLACEHOLDER"  # written once by Terraform, owned by humans/automation afterwards
  }
}

The placeholder gets written once; subsequent rotations update the value without Terraform fighting them.

Pattern B: Generate via secret_policy. Terraform doesn't see the value at all:

hcl
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
resource "scaivault_secret_policy" "strong_password" {
  name        = "strong-password"
  policy_type = "password"
  fields = [{
    name      = "password"
    generator = "random"
    config = {
      length         = 32
      charset        = "alphanumeric+symbols"
      require_upper  = true
      require_digit  = true
      require_symbol = true
    }
  }]
}

resource "scaivault_secret" "db_password" {
  path             = "environments/production/billing/db-password"
  secret_type      = "kv"
  generate_from    = scaivault_secret_policy.strong_password.id
  rotation_policy  = scaivault_rotation_policy.weekly.id
}

generate_from writes the secret by running the generator, returning only metadata to Terraform. The value never enters state.

What to manage in Terraform#

The boundary that works in practice:

Resource Manage in Terraform? Why
Access policies Yes Versioned, reviewed, code-review-friendly
Policy bindings Yes Same
Rotation policies Yes Schedule and grace period are config, not data
Secret policies (value generation) Yes Templates are config
PKI roles Yes Config
PKI CAs Yes (creation), No (root key) The CA cert is config; the key stays in ScaiVault
Dynamic engines Yes Connection config
Dynamic roles Yes SQL templates are config
Federation backends Yes Connection config
Webhooks Yes URL and event list
Subscriptions Sometimes If long-lived, yes; if per-pod, manage from app
Secret values No Lives in ScaiVault; managed by services or rotation
Service accounts In ScaiKey, not ScaiVault Identity is upstream

Example: production billing service setup#

hcl
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
# A rotation policy
resource "scaivault_rotation_policy" "api_keys_quarterly" {
  name           = "api-keys-quarterly"
  interval       = "90d"
  grace_period   = "48h"
  warn_before    = "7d,1d"
  auto_generate  = false
  webhook_ids    = [scaivault_webhook.rotation_alerts.id]
}

# A weekly rotation that auto-generates strong passwords
resource "scaivault_rotation_policy" "weekly_db_passwords" {
  name           = "weekly-db-passwords"
  interval       = "7d"
  grace_period   = "24h"
  auto_generate  = true
  secret_policy_id = scaivault_secret_policy.strong_password.id
}

# Webhook receiver for rotation events
resource "scaivault_webhook" "rotation_alerts" {
  name   = "rotation-alerts"
  url    = "https://ops.acme.example/scaivault/webhook"
  secret = var.webhook_signing_secret  # stored in Terraform Cloud as sensitive
  events = ["rotation.due", "rotation.overdue", "rotation.failed"]
  filters = {
    path_prefix = "environments/production/"
  }
}

# Access policy: billing service reads its own secrets
resource "scaivault_policy" "billing_prod_reader" {
  name        = "billing-prod-reader"
  description = "Production billing service reads its credentials"
  rules = [{
    path_pattern = "environments/production/billing/**"
    permissions  = ["read"]
  }]
}

resource "scaivault_policy_binding" "billing_sa" {
  policy_id      = scaivault_policy.billing_prod_reader.id
  identity_type  = "service_account"
  identity_id    = "sa:billing-prod"
}

# Secrets — metadata only, values managed elsewhere
resource "scaivault_secret" "stripe" {
  path        = "environments/production/billing/stripe"
  secret_type = "api_key"
  metadata    = { tags = ["stripe", "production"], owner = "team:billing" }
  options     = { rotation_policy_id = scaivault_rotation_policy.api_keys_quarterly.id }
  lifecycle { ignore_changes = [data] }
  data        = { secret_key = "PLACEHOLDER" }
}

resource "scaivault_secret" "database" {
  path           = "environments/production/billing/database-password"
  secret_type    = "kv"
  generate_from  = scaivault_secret_policy.strong_password.id
  rotation_policy = scaivault_rotation_policy.weekly_db_passwords.id
  metadata       = { tags = ["database", "production"] }
}

Reading secrets in Terraform#

There's a data source for read access, useful for fetching values that already exist to feed into other resources (e.g., into an AWS provider that needs an API key):

hcl
1
2
3
4
5
6
7
8
data "scaivault_secret" "stripe_webhook_secret" {
  path = "environments/production/billing/stripe-webhook"
}

resource "aws_api_gateway_authorizer" "stripe" {
  # ...
  identity_validation_expression = data.scaivault_secret.stripe_webhook_secret.data.signing_secret
}

The value lands in Terraform state. If your state file is backed by S3+KMS with restricted access, this is acceptable for build-time configuration. For values you don't want in state, don't read them via data source — have the application read them at runtime instead.

State backend security#

Whichever way you go, treat the Terraform state as sensitive material:

  • Encrypted-at-rest backend (S3+KMS, Terraform Cloud with workspace-level encryption, GCS+CMEK).
  • IAM scoped tightly — only Terraform runners and a small admin set.
  • State lock backend (DynamoDB, gcs locks) — concurrent modification of policies is a bad time.
  • No state files in git, even encrypted.

CI integration#

Typical pipeline:

  1. terraform plan runs on every PR. Output reviewed.
  2. On merge, terraform apply runs against the production workspace.
  3. Token comes from a short-lived ScaiKey-issued JWT, scoped to whatever the runner needs (often quite broad — provisioning policies and rotation policies needs admin for that subset of operations).
  4. After apply, Terraform Cloud runs a sanity-check job that reads back a few critical policies via data.scaivault_policy and asserts shape.

Common questions#

"Can I import existing ScaiVault resources into Terraform?" Yes:

bash
1
2
terraform import scaivault_policy.billing_prod_reader pol_abc123
terraform import scaivault_rotation_policy.weekly_db_passwords rot_weekly

The provider supports import for all resource types. Run terraform plan after import to see drift between state and config; close the gap by adjusting either side.

"What about workspaces?" Use Terraform workspaces (or Terraform Cloud workspaces) to separate environments. The provider config can be parameterized: tenant_id = var.tenant_id, set per-workspace.

"Drift?" Inevitable. A human flips a policy via the admin UI; Terraform reports drift on next plan. Either revert the change or codify it. Bias toward codifying — humans aren't authoritative about what's intended.

"How do I handle dependencies between resources?" Standard Terraform dependency tracking. scaivault_policy_binding depends on scaivault_policy via interpolation; Terraform sequences correctly. The provider's API doesn't have batch operations, so a large initial apply can take a few minutes for ~100 resources.

What's next#

Updated 2026-05-17 13:26:50 View source (.md) rev 1