Terraform and OpenTofu
The vulnetix/resolve provider for Terraform and OpenTofu. Install, authenticate with a service account's ApiKey or an Authentik token, one resource per kind, import by name, write-only secrets, how state is read, and the registries. Published on the Terraform registry as vulnetix/resolve; OpenTofu listing is pending.
The vulnetix/resolve provider manages Vulnetix configuration as code. Each resource is one vulnetix.com/v1 config document of one kind, applied through the same server-side engine as vulnetix config apply and the console’s Download as code. Resources, their examples and their docs are generated from the kind schemas in config-schemas, so a resource has the fields its kind has.
It is one binary for Terraform and OpenTofu, 1.11 or later: secrets use write-only arguments, which arrived in 1.11.
Install
The provider is published on the Terraform registry as vulnetix/resolve. Its source is Vulnetix/terraform-provider-resolve. The current release is v1.3.0. A configuration needs the source and a version constraint:
terraform {
required_providers {
vulnetix = {
source = "vulnetix/resolve"
version = "~> 1.3"
}
}
}
# The credential is read from VULNETIX_ORG_ID and VULNETIX_API_KEY when not set
# here. Keep it out of the configuration: pass it from your CI secret store.
provider "vulnetix" {
# endpoint = "https://www.vulnetix.com/api/site/v1"
# org_id = var.vulnetix_org_id
# api_key = var.vulnetix_api_key # the ApiKey of a developer or owner service account
# manager = "terraform"
}
resource "vulnetix_quality_gate" "default" {
severity = "high"
block_malware = true
}
Then terraform init && terraform apply. terraform init downloads the provider from the registry and checks it against the release’s signed checksums. The local name stays vulnetix, so resources are vulnetix_* even though the source address is vulnetix/resolve. ~> 1.1 accepts any 1.x release from 1.1 on and never a 2.0; pin version = "1.1.0" for an exact release. Commit .terraform.lock.hcl so every machine installs the same build.
To authenticate, see Authentication. The provider needs Terraform or OpenTofu 1.11 or later.
OpenTofu
The provider is the same binary for both tools. Once the OpenTofu registry lists it, the configuration above works unchanged with tofu init && tofu apply.
Verifying a release
Each release publishes zips per platform, a manifest, terraform-provider-resolve_<version>_SHA256SUMS and a detached signature ..._SHA256SUMS.sig. Releases are signed with the RSA key 9BC243B2CE39D50B4ACEFA84BE9293952EA5128B. The registry returns the public key with the download details of a version, under signing_keys.gpg_public_keys. To check a release by hand, import the key, download the two files from the GitHub release and run:
curl -s https://registry.terraform.io/v1/providers/vulnetix/resolve/1.1.0/download/linux/amd64 \
| jq -r '.signing_keys.gpg_public_keys[0].ascii_armor' | gpg --import
gpg --verify terraform-provider-resolve_1.1.0_SHA256SUMS.sig terraform-provider-resolve_1.1.0_SHA256SUMS
Confirm that the fingerprint gpg reports is the one above, then check the zip you use against SHA256SUMS with sha256sum -c.
Building from source
To run a build of your own, for OpenTofu until it is listed or to try a change, build the provider and point the tool at the binary with a development override:
git clone https://github.com/Vulnetix/terraform-provider-resolve
cd terraform-provider-resolve
go build -o "$HOME/go/bin/terraform-provider-resolve" .
# ~/.terraformrc, or ~/.tofurc for OpenTofu
provider_installation {
dev_overrides {
"vulnetix/resolve" = "/home/you/go/bin"
}
direct {}
}
With an override, skip init for this provider and run plan or apply directly.
Authentication
The provider uses the credentials Vulnetix already issues. Set exactly one of the two forms; both together is an error, and neither gives a warning.
| Argument | Environment variable | Sent as | Notes |
|---|---|---|---|
api_key with org_id |
VULNETIX_API_KEY with VULNETIX_ORG_ID |
Authorization: ApiKey <org_id>:<api_key> |
The ApiKey of a service account. Sensitive. Keep it out of the configuration. |
token |
VULNETIX_API_TOKEN |
Authorization: Bearer <token> |
An Authentik access token or a session token. It names its own organisation, so there is no org_id. Sensitive. |
endpoint |
VULNETIX_ENDPOINT |
Default https://www.vulnetix.com/api/site/v1. The console API; the engine’s routes are under /config/.... Set it to point at another deployment. |
|
manager |
none | Default terraform. Recorded as the owner of every object this provider applies. |
An owner creates the service account with the developer role (or owner) and gives its ApiKey to the automation. A developer can apply every kind except four, Organization, Member, ServiceAccount and VaultEntry, which need an owner credential. The five SIEM kinds are written in Guardian with the same credential. A member service account is refused with HTTP 403 on apply. A person’s ApiKey works too, if that person is an owner. See credentials for automation.
Resources
One resource per kind that the engine stores: every resource, with its kind and group. A kind’s page has the resource’s Minimal and Advanced HCL beside the YAML and the CLI form, and its import command. Each resource’s arguments are the kind’s spec in snake case (blockMalware is block_malware), beside name, labels and annotations from metadata.
The kinds that are validate only, LicensePolicy and LicenseExceptions, have no resource: the server stores nothing for them, so there is nothing for Terraform to manage. A kind with a released schema that the engine does not serve is generated but not offered.
Data sources read what the engine and the platform hold: the kinds the server applies, the AI model catalogue, VEX statements, the repositories the GitHub App reaches, and detection rule feeds.
Import
Import any object by its name (metadata.name). A kind with one object per tenant is named default:
terraform import vulnetix_quality_gate.default default
terraform import vulnetix_jail_policy.payments_api payments-api
The first read after an import fills in every field the server holds. Each kind’s page shows the import command for its resource, and the repository keeps an import.sh beside every example. vulnetix config export --format hcl writes resources with import {} blocks for what a tenant already has, so the first plan shows no changes; see Export.
How state is read, and what a change does
Most kinds merge a document into the stored object, and the tenant’s row can hold values the configuration never set. So a refresh reads only the arguments you manage: an argument removed from the configuration stops being managed and keeps its last server value. Kinds that are wholly declared, such as ApiSpecSource, MalwareIoc, RemediationPolicy, RiskStrategy, Suppression and VexSource, are replaced instead: a removed argument goes back to its default. A resource’s own page in the registry says which.
Some kinds replace the object itself when a field changes, because the platform’s own console only creates and deletes them. WizConnection is replaced through those writers, and a messaging connection cannot change type. A kind written in a second phase (the five SIEM kinds and VaultEntry) answers a single PUT with 502 external_failed when its store refuses the write, and records nothing.
Renamed arguments
An argument is the kind’s spec property in snake case. A property whose name the resource reserves (name, labels, annotations, status, id and Terraform’s meta-arguments) is renamed, and the document key is unchanged:
| Kind | Document key | Argument |
|---|---|---|
Campaign |
name |
display_name |
TeaProduct |
name |
product_name |
TeaComponent |
name |
component_name |
VaultEntry |
name |
entry_name |
VaultEntry |
provider |
provider_slug |
FactoryBudget |
provider |
model_providers |
RepositorySettings |
labels (the repository as an asset) |
asset_labels |
metadata.name stays name, and metadata.labels stays labels. Each kind’s page shows the resource’s arguments in its HCL samples.
Status, phases and values shown once
Every resource has a computed status block:
| Attribute | Meaning |
|---|---|
status.phase |
Applied for most objects. Pending for a Member whose invitation is not accepted. AwaitingAuthorization for a messaging connection, such as Slack, that a person must authorize in a browser. Unmanaged when no manager owns the object. Terraform does not wait for a person: Pending and AwaitingAuthorization are successful applies, and a refresh shows the phase when it changes. |
status.authorization_url |
Where to authorize, while the phase is AwaitingAuthorization. |
status.extra |
Other fields the kind reports, such as invitationExpires. |
status.once |
Sensitive. Credentials the server shows only when it creates the object: a ServiceAccount’s apiKey and secret, a messaging connection’s signingSecret, a LogSource’s ingest token. |
Destroying a resource
Destroying a resource sends DELETE /config/objects/{kind}/{name}. It deletes the object, or lets go of it where the kind says so: deleting a GitHubInstallation never uninstalls the App, and deleting an Organization never deletes the tenant.
A kind with one object per tenant (a singleton, named default) always has something behind it, so destroying it puts the tenant back where it was before anyone configured it:
| Kind | What destroy does |
|---|---|
QualityGate |
Deletes the tenant’s row. The CLI’s own flags decide every check again. |
SsvcPolicy |
Deletes the row. The tenant decides with the default policy. |
NotificationDefaults |
Removes the setting. The catalogue defaults apply. |
FactorySettings |
Resets its keys to their defaults. The settings document stays. |
ScannerProfile |
Resets its four keys (scanners, custom, slsaMin, evidence). |
PackageFirewallPolicy |
Restores the column defaults and clears the ecosystem overrides, which is the open posture of a tenant with no policy. Mirrors are left alone. |
AiFirewallPolicy |
Removes only the rows of spec.capabilities. Guardrails, model and provider entries stay. |
AwsConnection |
Removes the connection as the console’s forced disconnect does. The CloudFormation stacks stay in each account. |
Organization |
Lets go. The tenant is never deleted. |
Destroying a resource that another manager owns fails. See the next section.
Ownership and drift
Every apply is recorded with the provider’s manager, terraform by default. A GitOps vulnetix config apply --prune deletes only objects its own manager owns, so it leaves Terraform’s alone, and Terraform cannot delete an object another manager owns. Change manager only if you run more than one Terraform configuration against one tenant and want them kept apart.
Secrets
A field named in a schema’s x-vulnetix.secrets is generated as two arguments:
<name>_wo: write-only. It is sent in the document and never stored in the plan or the state. The server never returns it either.<name>_wo_version: change this number to send the value again. Terraform cannot see a change to a write-only value.
Feed <name>_wo from an ephemeral variable or an ephemeral resource:
variable "vllm_api_key" {
type = string
sensitive = true
ephemeral = true
}
resource "vulnetix_ai_provider" "vllm" {
name = "acme-vllm"
auth = "bearer"
base_url = "https://llm.acme.example"
protocol = "openai-chat"
api_key_wo = var.vllm_api_key
api_key_wo_version = 1
}
OpenTofu
The provider is the same binary for both tools. Everything above applies to OpenTofu 1.11 or later: write-only arguments, ephemeral variables and import. The provider’s acceptance suite runs under both. Installing it through the OpenTofu registry is pending its listing; see OpenTofu under Install.
Registries
| Registry | Address | Status |
|---|---|---|
| Terraform registry | registry.terraform.io/vulnetix/resolve |
Published. v1.0.0 and v1.1.0 are listed. |
| OpenTofu registry | registry.opentofu.org/vulnetix/resolve |
Pending listing. The registry accepts the signing key, but the provider is not listed yet and tofu init fails with does not have a provider named. |
The address is vulnetix/resolve: namespace vulnetix, type resolve. The local name stays vulnetix, so the resources keep the vulnetix_ prefix. Both registries require the repository to be named terraform-provider-{type} and the release archives to be named terraform-provider-{type}_{version}_{os}_{arch}.zip, which is why the repository is Vulnetix/terraform-provider-resolve.
A release is a vX.Y.Z tag on the repository. Its workflow publishes a GitHub release in the registry layout: zips per platform, a manifest, SHA256SUMS and a detached GPG signature that both registries read.
| Release | Change |
|---|---|
| v1.1.0 | Adds drift_protection to vulnetix_organization, the Drift protection switch. Needs a server-side migration to turn on. |
| v1.0.0 | The first release |
The two AWS modules are separate repositories, released as aws-onboarding v1.0.1 and aws-siem-shipping v1.0.0. They are installed by Git source and are not on the Terraform registry yet.