Day 16: AWS IAM User Management with Terraform – CSV-Driven Onboarding, RBAC, Password Security, PGP Encryption, and SSO Best Practices

Introduction
Managing AWS IAM users manually becomes difficult as organizations grow. Creating users one by one, assigning permissions manually, and maintaining access controls can quickly become error-prone.
In this project, we'll automate AWS IAM user onboarding using Terraform.
We'll:
Create IAM users from a CSV file
Automatically generate usernames
Create IAM login profiles
Organize users into IAM groups
Implement Role-Based Access Control (RBAC)
Attach custom IAM policies to groups
Understand AWS password handling
Learn why Terraform cannot retrieve AWS-generated passwords
Explore PGP encryption
Discuss MFA onboarding
Compare IAM Users vs AWS IAM Identity Center (SSO)
By the end of this project, you'll understand both Terraform fundamentals and real-world IAM security practices.
Project Architecture
users.csv
↓
csvdecode()
↓
Terraform
↓
IAM Users
↓
User Tags
↓
Group Membership Logic
↓
IAM Groups
↓
IAM Policies
↓
RBAC
Project Structure
day16/
├── backend.tf
├── providers.tf
├── main.tf
├── groups.tf
├── policies.tf
└── users.csv
Configure Remote State
backend.tf
terraform {
backend "s3" {
bucket = "terraform-state-1766811759"
key = "day14ls/terraform.tfstate"
region = "us-east-1"
use_lockfile = true
encrypt = true
}
}
Explanation
This stores Terraform state remotely in Amazon S3.
Benefits:
Team Collaboration
State Persistence
Versioning
State Locking
Encryption
Understanding Each Parameter
bucket
bucket = "terraform-state-1766811759"
S3 bucket where state is stored.
key
key = "day14ls/terraform.tfstate"
Path of the state file inside S3.
use_lockfile
use_lockfile = true
Prevents multiple people from running Terraform simultaneously.
encrypt
encrypt = true
Encrypts Terraform state in S3.
Configure Provider
providers.tf
terraform {
required_version = ">= 1.6.0"
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 5.0"
}
}
}
provider "aws" {
region = "us-east-1"
}
Explanation
required_version
Ensures Terraform version compatibility.
required_providers
Downloads AWS provider plugin.
provider "aws"
Configures AWS region.
Reading Data From CSV
users.csv
first_name,last_name,email,phone,department,job_title
Michael,Scott,michael.scott@dundermifflin.com,1111111111,Education,Regional Manager
Jim,Halpert,jim.halpert@dundermifflin.com,3333333333,Engineering,Software Engineer
Angela,Martin,angela.martin@dundermifflin.com,5558190243,Engineering,Software Engineer
This simulates an HR export file.
AWS Account Information
data "aws_caller_identity" "current" {}
What is a Data Source?
Data sources read existing AWS information.
Example:
data "aws_caller_identity" "current" {}
retrieves:
AWS Account ID
User ARN
User ID
Output:
output "account_id" {
value = data.aws_caller_identity.current.account_id
}
Reading CSV Data
locals {
users = csvdecode(file("users.csv"))
}
This is one of the most important parts of the project.
file()
file("users.csv")
Reads file contents.
Example:
first_name,last_name
Jim,Halpert
returns:
Raw Text
csvdecode()
csvdecode(file("users.csv"))
Converts CSV into Terraform objects.
Example:
first_name,last_name
Jim,Halpert
becomes:
[
{
first_name = "Jim"
last_name = "Halpert"
}
]
Creating IAM Users
resource "aws_iam_user" "users" {
for_each = { for user in local.users : user.first_name => user }
name = lower("\({substr(each.value.first_name, 0, 1)}\){each.value.last_name}")
path = "/users/"
tags = {
DisplayName = "\({each.value.first_name} \){each.value.last_name}"
Department = each.value.department
JobTitle = each.value.job_title
Email = each.value.email
Phone = each.value.phone
}
}
Understanding for_each
Instead of:
resource "aws_iam_user" "jim" {}
resource "aws_iam_user" "pam" {}
resource "aws_iam_user" "dwight" {}
Terraform loops through every CSV record.
Understanding for Expression
for user in local.users : user.first_name => user
Transforms:
Michael Scott
Jim Halpert
Pam Beesly
into:
{
Michael = {...}
Jim = {...}
Pam = {...}
}
Understanding substr()
substr(each.value.first_name, 0, 1)
Example:
Michael
returns:
M
Understanding lower()
lower("MScott")
returns:
mscott
Result:
Michael Scott → mscott
Jim Halpert → jhalpert
Angela Martin → amartin
User Tags
Every user receives:
tags = {
DisplayName = ...
Department = ...
JobTitle = ...
Email = ...
Phone = ...
}
These tags drive our RBAC logic.
Creating Login Profiles
resource "aws_iam_user_login_profile" "users" {
for_each = aws_iam_user.users
user = each.value.name
password_reset_required = true
lifecycle {
ignore_changes = [
password_length,
password_reset_required,
]
}
}
Important Password Discussion
Many people expect:
Terraform
↓
Password
↓
Output
But AWS does NOT work this way.
Terraform requests:
Create Login Profile
AWS internally:
Generates Password
Stores Password
Creates Login Profile
Terraform never receives plaintext password.
Why AWS Doesn't Reveal Passwords
If AWS exposed passwords:
Terraform State
CI/CD Logs
Console Output
S3 Backend
could all contain user credentials.
This would be a massive security risk.
How To Verify Login Profiles
aws iam get-login-profile --user-name mscott
Output:
{
"LoginProfile": {
"UserName": "mscott",
"PasswordResetRequired": true
}
}
Notice:
Password is NOT returned.
IAM Groups
groups.tf
resource "aws_iam_group" "education" {
name = "Education"
}
resource "aws_iam_group" "managers" {
name = "Managers"
}
resource "aws_iam_group" "engineers" {
name = "Engineers"
}
Dynamic Group Membership
Education Group:
users = [
for user in aws_iam_user.users :
user.name
if user.tags.Department == "Education"
]
Managers Group:
users = [
for user in aws_iam_user.users :
user.name
if contains(keys(user.tags), "JobTitle")
&& can(regex("Manager|CEO", user.tags.JobTitle))
]
Understanding keys()
keys(user.tags)
Returns:
[
"DisplayName",
"Department",
"JobTitle",
"Email",
"Phone"
]
Understanding contains()
contains(keys(user.tags), "JobTitle")
Checks if JobTitle exists.
Returns:
true
or
false
Understanding regex()
regex("Manager|CEO", user.tags.JobTitle)
Matches:
Regional Manager
CEO
CEO of Sabre
Does not match:
Software Engineer
Accountant
Engineers Group
user.tags.Department == "Engineering"
This automatically places:
Jim Halpert
Angela Martin
Oscar Martinez
into Engineers.
Custom IAM Policies
Education Policy:
s3:Get*
s3:List*
Read-only S3 access.
Managers Policy:
ec2:Describe*
Read-only EC2 visibility.
Engineers Policy:
s3:*
Full S3 access.
Policy Attachments
resource "aws_iam_group_policy_attachment"
Connects:
Group
↓
Policy
Example:
Engineers
↓
EngineerS3FullAccess
In this project, we successfully automated IAM user onboarding using Terraform.
We built:
IAM Users from a CSV file
IAM Groups
Dynamic Group Membership
Role-Based Access Control (RBAC)
IAM Policies
Policy Attachments
At first, I wanted to go one step further and automatically generate passwords, store them in AWS Secrets Manager, and distribute them to users. However, while exploring Terraform and AWS IAM more deeply, I discovered an important security design decision made by AWS.
In this section, let's understand how AWS handles passwords, why Terraform cannot retrieve them, how PGP encryption solves the problem, and why most modern organizations prefer AWS IAM Identity Center (SSO).
How IAM User Password Creation Works
When we create an IAM user login profile:
resource "aws_iam_user_login_profile" "users" {
for_each = aws_iam_user.users
user = each.value.name
password_reset_required = true
}
Notice something interesting.
There is no:
password = "MyPassword123!"
argument.
Terraform only tells AWS:
Create console access for this user.
AWS then performs the following steps:
Terraform
↓
Create Login Profile
↓
AWS Generates Random Password
↓
AWS Stores Password Internally
↓
Console Access Enabled
The actual password is generated inside AWS.
Terraform never sees the plaintext password.
Where Is The Password Stored?
The password becomes part of the IAM Login Profile.
You can verify the login profile exists:
aws iam get-login-profile --user-name mscott
Example output:
{
"LoginProfile": {
"UserName": "mscott",
"CreateDate": "2026-06-15T10:00:00Z",
"PasswordResetRequired": true
}
}
Notice that AWS returns:
Username
Creation date
Password reset requirement
But AWS never returns:
{
"password": "TempPassword123!"
}
The password cannot be retrieved.
Why Doesn't AWS Simply Return The Password?
Imagine if AWS returned:
output "password" {
value = aws_iam_user_login_profile.users.password
}
Now the password would appear in:
Terraform outputs
CI/CD logs
Jenkins logs
GitLab logs
GitHub Actions logs
Terraform state files
Remote S3 backends
For example:
{
"username": "mscott",
"password": "TempPassword123!"
}
This would be a massive security risk.
Anyone with access to Terraform state could steal every user's password.
AWS intentionally prevents this.
Terraform State File Security Problem
Suppose AWS allowed Terraform to store passwords.
Your state file might contain:
{
"resources": [
{
"username": "mscott",
"password": "TempPassword123!"
}
]
}
Now anyone with access to:
terraform.tfstate
could see every user's password.
In our project we are storing state remotely in S3.
That means passwords would also be stored in:
S3 Backend
which creates another security risk.
This is one of the main reasons AWS does not expose plaintext passwords.
How PGP Encryption Solves The Problem
Instead of returning the password directly, AWS supports PGP encryption.
PGP stands for:
Pretty Good Privacy
The idea is simple.
You create:
Public Key
Private Key
Think of them as:
Public Key = Lock
Private Key = Key
Step 1 – Generate PGP Keys
You create a key pair:
Public Key
Private Key
The public key can be shared safely.
The private key must remain secret.
Step 2 – Give AWS Your Public Key
Terraform configuration:
resource "aws_iam_user_login_profile" "users" {
for_each = aws_iam_user.users
user = each.value.name
password_reset_required = true
pgp_key = file("public-key.asc")
}
Now AWS has your public key.
Step 3 – AWS Generates Password
AWS creates:
TempPassword123!
Step 4 – AWS Encrypts Password
Using your public key:
TempPassword123!
↓
PGP Encryption
↓
wcBMA4DJskdfjsdf...
Step 5 – Terraform Receives Encrypted Password
Terraform receives:
encrypted_password
not:
password
Example output:
wcBMA4DJskdfjsdf...
This encrypted value is safe to store.
Even if someone steals:
terraform.tfstate
they still cannot see the password.
Step 6 – Decrypt Using Private Key
Only the owner of the private key can decrypt:
gpg --decrypt password.gpg
Result:
TempPassword123!
Advantages Of PGP Encryption
Protects Terraform State
Without PGP:
State File
↓
Plaintext Passwords
With PGP:
State File
↓
Encrypted Passwords
Protects CI/CD Logs
Without PGP:
GitLab Logs
↓
Password Exposure
With PGP:
GitLab Logs
↓
Encrypted Data Only
Protects Remote Backends
Even if someone gains access to:
S3 State Bucket
they cannot read the passwords.
Why Not Store Passwords In Secrets Manager?
Initially I planned to:
Generate Password
↓
Store In Secrets Manager
However, I discovered a limitation.
AWS generates the IAM login password internally.
Terraform never receives the plaintext password.
Therefore:
AWS Login Password
cannot automatically be placed into:
AWS Secrets Manager
unless you use the PGP workflow.
Without PGP, Terraform simply does not know the actual password.
MFA In Production Environments
A common enterprise workflow looks like:
Create IAM User
↓
Provide Temporary Password
↓
User Logs In
↓
User Changes Password
↓
User Configures MFA
↓
MFA Enforcement Policy Applied
This avoids locking users out before they have a chance to enroll in MFA.
How SSO Changes Everything
Modern organizations rarely create IAM users manually.
Instead they use:
Microsoft Entra ID
Okta
Google Workspace
with AWS IAM Identity Center.
Architecture:
Employee
↓
Corporate Identity Provider
↓
AWS IAM Identity Center
↓
AWS Account Access
Traditional IAM User Flow
Create User
↓
Generate Password
↓
Distribute Password
↓
Change Password
↓
Enable MFA
Lots of operational overhead.
AWS SSO Flow
HR Creates Employee
↓
Employee Added To Entra ID
↓
Employee Logs In Using Corporate Password
↓
AWS Trusts Entra ID
↓
Access Granted
No password generation.
No password distribution.
No PGP.
No login profile management.
No Secrets Manager for user passwords.
Why SSO Is Considered Superior
Benefits:
Single Sign-On
Centralized Authentication
Centralized MFA
No Password Distribution
Simplified User Lifecycle
Better Security
Better Auditability
This is why most modern enterprises prefer AWS IAM Identity Center over managing hundreds of IAM users.
Final Thoughts
While building this project, I learned that creating IAM users is only part of the identity management story.
AWS intentionally prevents Terraform from accessing plaintext passwords because storing them in state files, logs, or remote backends would create significant security risks.
PGP encryption provides a secure way to retrieve generated passwords exactly once while keeping Terraform state protected.
However, modern organizations increasingly avoid this entire challenge by adopting AWS IAM Identity Center (SSO), allowing users to authenticate using their corporate identity provider and centralized MFA.
Understanding both approaches—traditional IAM users and modern SSO—is essential for anyone working with AWS and Terraform.



