Develop
Terraform provider
The wardengate/wardengate Terraform provider treats every administrative concept in Wardengate — organizations, roles, identity providers, targets, policies, approval routes, MFA policies — as declarative resources. If you already review your infrastructure as HCL, treat your access plane the same way and cut the number of manual changes to zero.
Install
Declare the provider in a required_providers block and run terraform init. Terraform will pull the latest release matching your version constraint from the public registry.
terraform {
required_version = ">= 1.6"
required_providers {
wardengate = {
source = "wardengate/wardengate"
version = "~> 1.6"
}
}
}Provider configuration
The provider needs a host and an API token. Source the token from an environment variable — WG_API_TOKEN by default — so it never lands in state or in version control. Mint a dedicated service-principal token for Terraform; do not share it with humans or with the CLI.
provider "wardengate" {
host = "https://wardengate.example.com"
# api_token is read from WG_API_TOKEN by default.
# Set it explicitly only if you are shimming through a secret manager.
# api_token = var.wg_api_token
}wardengate_organization
The top-level tenancy unit. Most installs have one organization per business unit or environment.
resource "wardengate_organization" "acme" {
name = "acme"
display_name = "Acme Corporation"
default_session_minutes = 60
}wardengate_role
Custom roles compose administrative permissions. Bind them to groups in your IdP so membership changes flow through automatically.
resource "wardengate_role" "policy_author" {
name = "policy-author"
permissions = [
"policy:read",
"policy:write",
"target:read",
]
}wardengate_identity_provider
Wire up SAML, OIDC, and SCIM connections. Keep the metadata URL rather than pasting a certificate — rotation is automatic that way.
resource "wardengate_identity_provider" "okta" {
name = "corp-okta"
protocol = "saml"
metadata_url = "https://corp.okta.example.com/app/wardengate/sso/metadata"
attribute_mapping = {
email = "email"
display_name = "name"
groups = "wardengate_groups"
}
scim {
enabled = true
token = var.okta_scim_token
}
}wardengate_target
A brokered endpoint: an SSH host, an RDP server, a database, a web application. For large fleets, generate target resources from your inventory (a JSON file checked into a repo, a Consul lookup, a dynamic data source) with for_each.
resource "wardengate_target" "web_01" {
name = "web-01.prod"
protocol = "ssh"
address = "10.10.4.20:22"
tags = {
env = "prod"
role = "web"
}
account {
name = "deploy"
credential_ref = "vault/ssh/deploy-prod"
}
}wardengate_policy
Policies bind principals to targets under constraints. Because targets and principals are both resolved through tags and groups, a single policy resource usually covers a whole environment.
resource "wardengate_policy" "prod_ssh_read" {
name = "prod-ssh-read"
principals {
groups = ["sre"]
}
targets {
tags = {
env = "prod"
}
}
protocols = ["ssh"]
accounts = ["readonly"]
constraints {
mfa = "required"
session_minutes = 60
}
recording {
mode = "full"
}
}wardengate_approval_route
Approval routes control where approval-gated requests land (Slack, Teams, PagerDuty, email) and how they escalate if no one acts in time.
resource "wardengate_approval_route" "sre_leads_slack" {
name = "sre-leads-slack"
channel = "slack"
target = "#sre-approvals"
webhook_secret = var.slack_webhook_secret
fallback {
channel = "email"
target = "sre-leads@example.com"
after_minutes = 10
}
}wardengate_mfa_policy
Step-up MFA policies override the default session requirements for matching targets. A common pattern is a permissive baseline for staging and a phishing-resistant requirement for prod.
resource "wardengate_mfa_policy" "step_up_prod" {
name = "step-up-prod"
match {
target_tags = { env = "prod" }
}
require = ["webauthn", "totp"]
max_age_minutes = 15
}Data sources
Data sources are read-only lookups you can reference from other resources. The three most common are wardengate_user, wardengate_group, and wardengate_target.
data "wardengate_user" "avery" {
email = "avery@example.com"
}
data "wardengate_group" "sre" {
name = "sre"
}
data "wardengate_target" "web_01" {
name = "web-01.prod"
}
output "avery_id" {
value = data.wardengate_user.avery.id
}Plan, review, apply
The recommended workflow mirrors what you already do for cloud resources: a pull request runs terraform plan and posts the diff as a PR comment; merges to main trigger terraform apply in a protected environment with human approval for destructive changes.
# .github/workflows/wardengate.yml
name: wardengate
on:
pull_request:
paths: ["infra/wardengate/**"]
push:
branches: [main]
paths: ["infra/wardengate/**"]
jobs:
plan:
runs-on: ubuntu-latest
defaults:
run:
working-directory: infra/wardengate
env:
WG_API_TOKEN: ${{ secrets.WG_API_TOKEN }}
steps:
- uses: actions/checkout@v4
- uses: hashicorp/setup-terraform@v3
- run: terraform init -input=false
- run: terraform fmt -check
- run: terraform validate
- run: terraform plan -out=tf.plan
apply:
needs: plan
if: github.ref == 'refs/heads/main'
runs-on: ubuntu-latest
environment: wardengate-prod
# ...Importing existing resources
If the platform has been operated by hand and you want to adopt Terraform without rebuilding, import the live state into your configuration. Do this resource type by resource type — start with policies, which are the highest-risk to desynchronise.
# Discover the existing resource's id from the console or wgctl.
wgctl target describe web-01.prod --output json | jq -r .id
# tgt_01HE6W7R8Q9JXN
# Generate a config stub and import the id into state.
terraform import wardengate_target.web_01 tgt_01HE6W7R8Q9JXN
# Then run a plan and reconcile the drift between your HCL and reality.
terraform planDrift detection
Run terraform plan on a cron — hourly is fine — and alert on any non-empty diff. Drift on a policy is a security-relevant event: somebody has edited the running configuration out of band, and you want to know about it immediately. Most teams wire the plan job to the same webhook the rest of their platform alerts run through.
State backend
State holds the mapping between HCL names and the real IDs inside Wardengate. Use a remote backend with locking — S3 + DynamoDB, GCS, Terraform Cloud, or Spacelift all work — and protect it like a secret: it contains the identifiers (never the tokens) of every managed resource.
terraform {
backend "s3" {
bucket = "acme-tfstate"
key = "wardengate/prod.tfstate"
region = "us-east-1"
dynamodb_table = "acme-tfstate-locks"
encrypt = true
}
}Upgrading the provider
Provider upgrades follow semver. A minor bump is additive — new resources, new attributes — and is safe to adopt with terraform init -upgrade. A major bump may rename attributes or change default values; the release notes always include a migration section with a diff that you can paste on top of an existing config.
# Bump the version constraint in required_providers, then:
terraform init -upgrade
terraform plan
# Pin to a known-good release when you are about to apply:
# version = "1.6.4"Related
- API reference — the HTTP surface the provider calls.
- CLI reference — useful for one-off imports and for simulating policy decisions.
- Session policies — concept reference for the fields in
wardengate_policy.