HCP Terraform: Teams, Policies, Health Checks and Dynamic Credentials
Scenario
Your organization is rolling out HCP Terraform to all engineering teams. You need to set up teams with role-based access, enforce policies that prevent non-compliant infrastructure, enable drift detection via health assessments, and eliminate static AWS credentials with OIDC dynamic credentials.
- Understand HCP Terraform Teams and permission levels (read, plan, write, admin).
- Understand Sentinel and OPA policy enforcement (advisory, soft-mandatory, hard-mandatory).
- Enable Workspace Health Assessments for automated drift detection.
- Understand Dynamic Credentials (OIDC) — how HCP Terraform authenticates to AWS without static keys.
Additional Context
Sentinel is HashiCorp's policy-as-code framework. Policies are written in the Sentinel language and enforced between plan and apply. OPA (Open Policy Agent) is an open-source alternative using Rego.
Policy enforcement levels:
- advisory: log warning, don't block
- soft-mandatory: block by default, but can be overridden by admins
- hard-mandatory: block always, no override
Dynamic Credentials (OIDC): HCP Terraform generates a short-lived OIDC token for each run. AWS trusts this token via an IAM OIDC provider → IAM role. No static AWS_ACCESS_KEY_ID needed. Credentials are scoped to a single run and expire automatically.
Teams & Permissions (HCP Terraform UI)
# Teams are configured in the HCP Terraform UI:
# Settings → Teams → Create Team
# Permission Levels:
# ┌────────────┬───────┬──────┬───────┬───────┐
# │ Permission │ Read │ Plan │ Write │ Admin │
# ├────────────┼───────┼──────┼───────┼───────┤
# │ View state │ ✓ │ ✓ │ ✓ │ ✓ │
# │ Queue plan │ │ ✓ │ ✓ │ ✓ │
# │ Apply runs │ │ │ ✓ │ ✓ │
# │ Lock/unlock│ │ │ ✓ │ ✓ │
# │ Settings │ │ │ │ ✓ │
# └────────────┴───────┴──────┴───────┴───────┘Sentinel Policy Example
# policy.sentinel — require all S3 buckets to have a ManagedBy tag
import "tfplan/v2" as tfplan
s3_buckets = filter tfplan.resource_changes as _, rc {
rc.type is "aws_s3_bucket" and
rc.mode is "managed" and
(rc.change.actions contains "create" or rc.change.actions contains "update")
}
main = rule {
all s3_buckets as _, bucket {
bucket.change.after.tags contains "ManagedBy"
}
}
# sentinel.hcl — policy configuration
# policy "require-managed-by-tag" {
# enforcement_level = "hard-mandatory" # no override allowed
# }Workspace Health Assessment (UI Configuration)
# Enable in HCP Terraform UI:
# Workspace → Settings → Health → Enable Health Assessments
# What it does:
# 1. Periodically runs terraform plan -refresh-only
# 2. Compares cloud reality to state
# 3. Reports drift as "unhealthy" workspace
# 4. Shows exactly what changed outside Terraform
# Requires:
# - HCP Terraform Plus or Enterprise tier
# - Workspace must have cloud credentials configuredDynamic Credentials (OIDC) Setup
# AWS side: Create an OIDC provider and IAM role
# Step 1: OIDC Provider in AWS
resource "aws_iam_openid_connect_provider" "hcp_terraform" {
url = "https://app.terraform.io"
client_id_list = ["aws.workload.identity"]
thumbprint_list = ["9e99a48a9960b14926bb7f3b02e22da2b0ab7280"]
}
# Step 2: IAM Role that HCP Terraform can assume
resource "aws_iam_role" "hcp_terraform" {
name = "hcp-terraform-role"
assume_role_policy = jsonencode({
Version = "2012-10-17"
Statement = [{
Effect = "Allow"
Principal = { Federated = aws_iam_openid_connect_provider.hcp_terraform.arn }
Action = "sts:AssumeRoleWithWebIdentity"
Condition = {
StringEquals = {
"app.terraform.io:aud" = "aws.workload.identity"
}
StringLike = {
"app.terraform.io:sub" = "organization:my-org:project:*:workspace:*:run_phase:*"
}
}
}]
})
}
# Step 3: Attach permissions to the role
resource "aws_iam_role_policy_attachment" "admin" {
role = aws_iam_role.hcp_terraform.name
policy_arn = "arn:aws:iam::aws:policy/AdministratorAccess"
}
# Step 4: In HCP Terraform workspace settings:
# Add environment variable: TFC_AWS_PROVIDER_AUTH = true
# Add environment variable: TFC_AWS_RUN_ROLE_ARN = arn:aws:iam::ACCOUNT:role/hcp-terraform-role
# No more static AWS keys needed!Sentinel Result: true This result means that all Sentinel policies passed and the protected behavior is allowed. 1 policies evaluated: ✓ require-managed-by-tag: hard-mandatory — PASSED

