Terraform provider
The Mutual Trust Terraform provider manages trust spaces, services, partner invitations, grant states, workload issuers and support access through the public Mutual Trust API, and reads trust bundles for load balancers and ingress controllers.
Install
The provider address is sectigo/mutualtrust. It is built with the HashiCorp Terraform Plugin Framework (plugin protocol 6, Terraform 1.0 or later); the acceptance tests run on Terraform 1.17. To build it from the repository and install it into Terraform's local plugin directory:
cd services/terraform-provider-mutualtrust
go build -o ~/.terraform.d/plugins/registry.terraform.io/sectigo/mutualtrust/0.1.0/darwin_arm64/terraform-provider-mutualtrust_v0.1.0Use the linux_amd64, darwin_amd64 or windows_amd64 directory for other platforms. Release archives for every platform are built with goreleaser from .goreleaser.yml, signed SHA-256 checksums included.
Configure
terraform {
required_providers {
mutualtrust = {
source = "sectigo/mutualtrust"
version = "~> 0.1"
}
}
}
# The credential is read from MUTUALTRUST_API_CREDENTIAL when api_credential is not set.
provider "mutualtrust" {
# api_url = "https://api.mutualtrust.io"
# api_credential = var.mutualtrust_api_credential
}| Argument | Environment | Default | Meaning |
|---|---|---|---|
api_url | MUTUALTRUST_API_URL | https://api.mutualtrust.io | API origin. |
api_credential | MUTUALTRUST_API_CREDENTIAL | required | API credential (mtk_...), sensitive. It acts for one organization with the roles chosen when it was created. |
Create the credential in the portal under Organization, API credentials, and keep it in your secret manager. Credentials are created from a signed-in session only: a credential cannot mint another credential, which is why there is no mutualtrust_api_credential resource.
Roles
| Resource | Role in the credential’s organization |
|---|---|
mutualtrust_space | owner or trust_admin |
mutualtrust_service | owner, trust_admin or service_owner to create; trust_admin or service_owner to change |
mutualtrust_invitation | service_owner or trust_admin |
mutualtrust_grant_state | service_owner or trust_admin in the organization that owns the service |
mutualtrust_workload_issuer | partner_admin |
mutualtrust_support_grant | owner |
| Data sources | any role that can read the object |
Resources
| Resource | Manages | Changes in place | Destroy |
|---|---|---|---|
mutualtrust_space | A trust space and its private trust anchor. | none (all arguments replace) | Removed from state; the space stays (warning). |
mutualtrust_service | An API with its route templates. | contact, grant_approval | Removed from state; the service stays (warning). |
mutualtrust_invitation | A scoped partner invitation; token and accept_url are sensitive. | none (triggers issues a new one) | Cancels it while pending. |
mutualtrust_grant_state | The state of an existing grant: active, suspended or revoked. | state | Leaves the grant as it is. |
mutualtrust_workload_issuer | An approved workload OIDC issuer. | none | Removed from state; the approval stays (warning). |
mutualtrust_support_grant | Expiring, reason-coded staff support access. | none | Revokes the access. |
resource "mutualtrust_space" "payments" {
org_id = "org_01jq3v8m2k9x4h7c5n6p0r1s2t"
name = "Payments"
environment = "sandbox"
}
resource "mutualtrust_service" "payments" {
space_id = mutualtrust_space.payments.id
name = "Payments API"
contact = "api-platform@example.com"
grant_approval = "sponsor_review"
route_templates = [
{ name = "Read payments", methods = ["GET", "HEAD"], path = "/payments/*" },
{ name = "Create payments", methods = ["POST"], path = "/payments" },
{ name = "Refunds", methods = ["POST"], path = "/refunds/*", restricted = true },
]
}
# Invite a partner by the handle they shared from their organization settings.
resource "mutualtrust_invitation" "acme" {
service_id = mutualtrust_service.payments.id
consumer = { org_handle = "acme-logistics-7k2p" }
scope = [
{ methods = ["GET"], path = "/payments/*" },
]
expires_in_hours = 72
}
# Hand the one-time link to the partner through your secret store, never in plain output.
output "acme_accept_url" {
value = mutualtrust_invitation.acme.accept_url
sensitive = true
}Grant state
Grants come into being when a partner accepts an invitation. mutualtrust_grant_state sets the state you want: active approves a pending grant (services with grant_approval = "sponsor_review") or resumes a suspended one, suspended keeps the slot reserved, and revoked is terminal (a pending grant is rejected). A plan that asks a revoked or expired grant to change fails with a clear error.
# Approve grants waiting for review on a sponsor_review service. Approved grants stay in
# the set (they are then active), so the plan settles after one apply. The service ID is an
# input so the for_each keys are known when Terraform plans.
variable "payments_service_id" { type = string }
data "mutualtrust_grants" "payments" {
service_id = var.payments_service_id
}
resource "mutualtrust_grant_state" "approved" {
for_each = { for g in data.mutualtrust_grants.payments.grants : g.id => g if contains(["pending", "active"], g.state) }
grant_id = each.key
state = "active"
}
# Suspend one partner during an incident; set state = "active" again to resume.
resource "mutualtrust_grant_state" "acme" {
grant_id = "gr_01jq3v8m2k9x4h7c5n6p0r1s2t"
state = "suspended"
reason_code = "security_incident"
note = "INC-4821: credential exposure under investigation"
}Data sources
| Data source | Returns |
|---|---|
mutualtrust_overview | Organizations, spaces and services visible to the credential. |
mutualtrust_service | A service with route templates, latest policy revision, readiness and gateways. |
mutualtrust_grant | One grant from your side, including the workload principal gateways forward. |
mutualtrust_grants | Grants of a service (service_id) or held by a partner organization (org_id), optionally by state. |
mutualtrust_trust_bundle | A space’s CA certificates as PEM, per-issuer metadata and a sha256 that changes with the bundle. |
Trust bundles for load balancers and ingress
certificates_pem holds the issuers in state active or retiring, the same CAs gateways trust through the signed manifest. When the space adds or rotates an issuing CA, sha256 changes and the next apply updates everything that consumes the bundle.
AWS ALB trust store
The ALB verifies partner client certificates against the trust store and forwards the leaf to mt-enforcer on each target; listener, target group and enforcer settings are in the AWS ALB guide.
# Keep an AWS ALB mutual TLS trust store in step with a Mutual Trust space.
#
# The ALB verifies partner client certificates against the space's CA certificates and
# forwards the leaf to mt-enforcer on each target (see
# https://mutualtrust.io/docs/gateways/aws-alb for the listener, target group and
# enforcer settings). When the space adds or rotates an issuing CA, the bundle's
# sha256 changes and the next apply uploads a new object version and points the
# trust store at it.
terraform {
required_version = ">= 1.6"
required_providers {
mutualtrust = { source = "sectigo/mutualtrust", version = "~> 0.1" }
aws = { source = "hashicorp/aws", version = "~> 5.80" }
}
}
variable "space_id" { type = string }
variable "bundle_bucket" { type = string } # versioning enabled
variable "load_balancer_arn" { type = string }
variable "server_certificate_arn" { type = string }
variable "enforcer_target_group_arn" { type = string }
data "mutualtrust_trust_bundle" "partners" {
space_id = var.space_id
}
resource "aws_s3_object" "client_ca" {
bucket = var.bundle_bucket
key = "mutualtrust/${var.space_id}/client-ca.pem"
content = data.mutualtrust_trust_bundle.partners.certificates_pem
content_type = "application/x-pem-file"
}
resource "aws_lb_trust_store" "partners" {
name = "mt-${substr(var.space_id, 4, 16)}"
ca_certificates_bundle_s3_bucket = aws_s3_object.client_ca.bucket
ca_certificates_bundle_s3_key = aws_s3_object.client_ca.key
ca_certificates_bundle_s3_object_version = aws_s3_object.client_ca.version_id
}
resource "aws_lb_listener" "mtls" {
load_balancer_arn = var.load_balancer_arn
port = 443
protocol = "HTTPS"
ssl_policy = "ELBSecurityPolicy-TLS13-1-2-2021-06"
certificate_arn = var.server_certificate_arn
mutual_authentication {
mode = "verify"
trust_store_arn = aws_lb_trust_store.partners.arn
advertise_trust_store_ca_names = "on"
}
default_action {
type = "forward"
target_group_arn = var.enforcer_target_group_arn
}
}
output "trust_bundle_sha256" {
value = data.mutualtrust_trust_bundle.partners.sha256
}Kubernetes Secret for ingress-nginx
ingress-nginx reads the client CA from the Secret named in nginx.ingress.kubernetes.io/auth-tls-secret; the enforcer chart and annotations are in the ingress-nginx guide.
# Publish a Mutual Trust space's CA certificates as the Secret ingress-nginx uses to
# verify partner client certificates (nginx.ingress.kubernetes.io/auth-tls-secret).
# See https://mutualtrust.io/docs/gateways/kubernetes-ingress-nginx for the enforcer
# chart and the auth-url annotations.
terraform {
required_version = ">= 1.6"
required_providers {
mutualtrust = { source = "sectigo/mutualtrust", version = "~> 0.1" }
kubernetes = { source = "hashicorp/kubernetes", version = "~> 2.35" }
}
}
variable "space_id" { type = string }
variable "namespace" {
type = string
default = "orders"
}
data "mutualtrust_trust_bundle" "partners" {
space_id = var.space_id
}
resource "kubernetes_secret" "client_ca" {
metadata {
name = "mt-client-ca"
namespace = var.namespace
annotations = {
"mutualtrust.io/space" = var.space_id
"mutualtrust.io/bundle-sha256" = data.mutualtrust_trust_bundle.partners.sha256
}
}
data = {
"ca.crt" = data.mutualtrust_trust_bundle.partners.certificates_pem
}
}
# Ingress annotations that consume the Secret:
# nginx.ingress.kubernetes.io/auth-tls-secret: orders/mt-client-ca
# nginx.ingress.kubernetes.io/auth-tls-verify-client: "on"
# nginx.ingress.kubernetes.io/auth-tls-verify-depth: "2"Safe retries and concurrency
- Creates send an
Idempotency-Keyderived from the resource’s configuration, and grant transitions one derived from the grant, its revision and the action. A retried or re-run apply replays the original result instead of creating a duplicate or applying a transition twice; the API keeps keys for 24 hours. - Grant transitions send
If-Matchwith the revision the provider read. If the grant changed in between, the provider reads it again once and decides afresh; it never overwrites a change it has not seen. 429and503responses are retried with backoff, honoringRetry-After.- After every apply, a plan of the same configuration is empty: the provider reads back exactly what it wrote. The acceptance tests check this for every resource.
Objects the API keeps
Spaces, services and workload issuer approvals are permanent in Mutual Trust: certificates, grants, receipts and the audit trail refer to them. For these resources terraform destroy removes the object from state, leaves it in place and reports a warning. Run terraform import to manage it again. To stop all partner access to a service, revoke its grants with mutualtrust_grant_state and disable its gateways.
Import
| Resource | ID |
|---|---|
mutualtrust_space | spc_... |
mutualtrust_service | svc_... |
mutualtrust_invitation | svc_.../inv_... (the one-time token is not recoverable) |
mutualtrust_grant_state | gr_... |
mutualtrust_workload_issuer | org_.../wli_... |
mutualtrust_support_grant | org_.../sg_... |
Tests
Unit tests cover the HTTP client (problem mapping, retries, idempotency and If-Match headers) and the schema and state mapping. Acceptance tests run real plans and applies against a local control plane (npm run dev): they sign in as a sandbox guest, create the sample Orders API, mint an API credential and exercise every resource and data source, including import and an empty plan after every apply.
cd services/terraform-provider-mutualtrust
go vet ./... && go test ./...
TF_ACC=1 go test ./internal/provider/ -run TestAcc -v # against http://127.0.0.1:8787