Access Control in Terraform: IAM Permissions & Workspaces
In large Terraform ecosystems the risk that someone might accidentally (or intentionally 🙂 ) change something grows. Access control in Terraform is always the first line of defense and it’s not just about IAM roles, but also how to structure Terraform workspaces and permissions around them.
This blog covers best practices for securing infrastructure deployments using IAM and Terraform workspace permissions.
Why Access Control Matters in Terraform
When having multiple engineers working on shared workspaces system is more prone to errors or state drifts. A common situation is when secrets meant for the development environment accidentally get exposed in a pull request or an engineer change silently makes its way into production.
Without clear access boundaries, a minor misstep can ripple across environments. Access control ensures every engineer works safely within the scope and the production remains untouchable unless it absolutely needs to be.
Split Infrastructure by Workspace
Use separate workspaces for:
- Environments: dev, qa, prod
- Components: network, kubernetes, monitoring
Example:
| Component | dev | qa | prod |
| Network | network-dev | network-qa | network-prod |
| EKS | eks-dev | eks-qa | eks-prod |
| Monitoring | monitoring-dev | monitoring-qa | monitoring-prod |
This ensures a change to the eks-dev workspace can’t accidentally affect the production cluster.
IAM Access setup
Since Terraform needs to have permissions to interact with AWS accounts we need to give it permissions to do so. Is it mandatory to have IAM technical user for Terraform? Well, not necessarily. You could either use IAM role or IAM user and role is most common approach.
IAM role is ideal for CICD pipelines or when using AWS SSO or federated access. You assume the role from another identity and this way is more secured and with short-lived credentials.
When using IAM user for sure that user could access account directly which is not the best practice, so you would rather assign a role to it. Additionally this approach could be simpler for local dev/testing by utilizing long-lived keys but is harder to manage them securely.
Example:
You create IAM user for the pipeline gitlab-terraform-user and it will assume IAM role to do actual deployments.
- Create IAM user with path prefix
resource "aws_iam_user" "terraform_user" {
name = "gitlab-terraform-user"
path = "/technical/cicd/"
}
2. Create IAM role to be assumed by that user
resource "aws_iam_role" "terraform_deploy_role" {
name = "terraform-deploy-role"
assume_role_policy = jsonencode({
Version = "2012-10-17",
Statement = [{
Effect = "Allow",
Principal = {
AWS = aws_iam_user.terraform_user.arn
},
Action = "sts:AssumeRole"
}]
})
}
3. Attach IAM policy to the role
resource "aws_iam_policy" "deploy_policy" {
name = "TerraformDeployPolicy"
policy = jsonencode({
Version = "2012-10-17",
Statement = [{
Effect = "Allow",
Action = [
"*",
],
Resource = "*"
}]
})
}
resource "aws_iam_role_policy_attachment" "attach_deploy_policy" {
role = aws_iam_role.terraform_deploy_role.name
policy_arn = aws_iam_policy.deploy_policy.arn
}
4. IAM user permissions to assume the role
resource "aws_iam_user_policy" "allow_assume_role" {
name = "AssumeTerraformRole"
user = aws_iam_user.terraform_user.name
policy = jsonencode({
Version = "2012-10-17",
Statement = [{
Effect = "Allow",
Action = "sts:AssumeRole",
Resource = aws_iam_role.terraform_deploy_role.arn
}]
})
}
5. In GitLab CICD use aws sts assume-role with the role ARN to get temporary credentials for Terraform:
aws sts assume-role \
--role-arn arn:aws:iam::123456789012:role/terraform-deploy-role \
--role-session-name terraformSession
Important note
As you might noticed the IAM policy has admin privileges which of course doesn’t align with the least privileges best practice but both approaches have pros and cons. Of course, that least privileges is much more secure but you will hit permission errors quite often which will slow down the workflow and also slow down some important hotfixes. However, least privilege access is more controlled and prevents issues. By utilizing admin access your deployment is fast and without blockers. Regardless you can always have admin access for dev and/or qa but setting up different access for prod account.
Account-Specific Config with locals and lookup()
When managing infrastructure across multiple AWS accounts (e.g., dev, qa, prod), it’s common to need different IAM roles or ARNs per account. In Terraform, you can use locals and lookup().
By defining a local map of account-specific values, Terraform can dynamically select the correct role or resource ARN based on the current account. This makes your code:
- Reusable across environments
- Easier to manage and maintain
- Safer, avoiding hardcoded account-specific logic
Let’s say your pipeline deploys to different AWS accounts. With this pattern, Terraform automatically picks the correct IAM role for the current account:
locals {
account_to_roles = {
"dev-account-id" = {
admin = "arn:aws:iam::dev-account-id:role/AdminRole"
}
"prod-account-id" = {
admin = "arn:aws:iam::prod-account-id:role/RestrictedAdminRole"
}
}
current_account_id = data.aws_caller_identity.current.account_id
admin_role_arn = lookup(local.account_to_roles[local.current_account_id], "admin", null)
}
Now you can use local.admin_role_arn in IAM policies, iam:PassRole permissions, or module inputs — fully automated and environment-aware.
resource "aws_iam_policy" "pass_role_policy" {
name = "pass-role"
policy = jsonencode({
Version = "2012-10-17",
Statement = [{
Effect = "Allow",
Action = "iam:PassRole",
Resource = local.admin_role_arn
}]
})
}Conclusion
In complex Terraform environments, access control is very important. Combining IAM with structured Terraform workspaces helps prevent accidental changes and enforces clear boundaries between teams and environments.
While full admin access may speed up early development, implementing least privilege and account-aware configurations (via locals and lookup) leads to a safer, more scalable setup in the long run.
Whether using IAM users or roles, always aim to codify access as part of your infrastructure — reviewed, versioned, and secure.
If this blog saved you time, support me with a coffee!
Thanks to everyone who’s supported!







