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.

Documentation menu

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:

Build and install (macOS on Apple silicon shown)
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.0

Use 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

main.tf
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 }
Provider arguments
ArgumentEnvironmentDefaultMeaning
api_urlMUTUALTRUST_API_URLhttps://api.mutualtrust.ioAPI origin.
api_credentialMUTUALTRUST_API_CREDENTIALrequiredAPI 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

Roles each resource needs
ResourceRole in the credential’s organization
mutualtrust_spaceowner or trust_admin
mutualtrust_serviceowner, trust_admin or service_owner to create; trust_admin or service_owner to change
mutualtrust_invitationservice_owner or trust_admin
mutualtrust_grant_stateservice_owner or trust_admin in the organization that owns the service
mutualtrust_workload_issuerpartner_admin
mutualtrust_support_grantowner
Data sourcesany role that can read the object

Resources

Resources
ResourceManagesChanges in placeDestroy
mutualtrust_spaceA trust space and its private trust anchor.none (all arguments replace)Removed from state; the space stays (warning).
mutualtrust_serviceAn API with its route templates.contact, grant_approvalRemoved from state; the service stays (warning).
mutualtrust_invitationA scoped partner invitation; token and accept_url are sensitive.none (triggers issues a new one)Cancels it while pending.
mutualtrust_grant_stateThe state of an existing grant: active, suspended or revoked.stateLeaves the grant as it is.
mutualtrust_workload_issuerAn approved workload OIDC issuer.noneRemoved from state; the approval stays (warning).
mutualtrust_support_grantExpiring, reason-coded staff support access.noneRevokes the access.
A space, a service and an invitation
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, suspend
# 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 sources
Data sourceReturns
mutualtrust_overviewOrganizations, spaces and services visible to the credential.
mutualtrust_serviceA service with route templates, latest policy revision, readiness and gateways.
mutualtrust_grantOne grant from your side, including the workload principal gateways forward.
mutualtrust_grantsGrants of a service (service_id) or held by a partner organization (org_id), optionally by state.
mutualtrust_trust_bundleA 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.

aws-alb-trust-store/main.tf
# 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.

kubernetes-trust-bundle/main.tf
# 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-Key derived 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-Match with 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.
  • 429 and 503 responses are retried with backoff, honoring Retry-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

Import IDs
ResourceID
mutualtrust_spacespc_...
mutualtrust_servicesvc_...
mutualtrust_invitationsvc_.../inv_... (the one-time token is not recoverable)
mutualtrust_grant_stategr_...
mutualtrust_workload_issuerorg_.../wli_...
mutualtrust_support_grantorg_.../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.

Run
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