All posts

EngineeringMar 202611 min read

The Difference Between NHI and Human IAM Roles in CloudTrail

CloudTrail records every S3 access, but it doesn't tell you whether a human or a Lambda function made the call. Here's how to distinguish non-human identities from human actors — and why it changes your detection strategy entirely.

This article describes the product at the time of publication. Historical examples may use earlier workflows. See the current Shield capabilities and plans for the latest product information.

You've enabled CloudTrail S3 data events. You're streaming them into your SIEM. You have behavioral baselines per IAM role. And then you open an investigation and realize: this "IAM role" is actually used by a Lambda function during weekday nights and a data science team during business hours. The baseline is meaningless — it's averaging two completely different behavioral profiles into one noisy model.

This is the NHI problem in behavioral detection. Non-human identities (NHIs) — Lambda functions, service accounts, CI/CD pipelines, AI agents — operate S3 access patterns that look nothing like human access. If they share an IAM role with humans, or even just share a behavioral baseline, your detection accuracy suffers dramatically.

How to Distinguish NHI from Human in CloudTrail

CloudTrail gives you the signals to classify identity type — you just need to know where to look. The key fields are:

1. userAgent

The User-Agent field in CloudTrail events is the most reliable identity classifier. AWS SDK versions, CLI versions, and console access each produce distinct user-agent strings:

# Human via AWS Console:
userAgent: "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7)...aws-console"

# Human via AWS CLI:
userAgent: "aws-cli/2.15.0 Python/3.11.5 Darwin/23.0.0 botocore/2.0.0"

# Lambda function via Python SDK:
userAgent: "python-requests/2.31.0 Botocore/1.32.0"

# Terraform / infrastructure automation:
userAgent: "APN/1.0 HashiCorp/1.0 Terraform/1.7.0..."

# Kubernetes pod via Go SDK:
userAgent: "aws-sdk-go/1.50.0 (go1.21; linux; amd64)"

# GitHub Actions CI/CD:
userAgent: "aws-cli/2.15.0...github-actions"

A simple user-agent parser can classify the vast majority of your CloudTrail principals in seconds. Lambda functions running Python will almost always show boto3/botocore. GitHub Actions will show aws-cli from known CI/CD IP ranges. Console access will show the Mozilla/5.0 console string. The exceptions (custom SDK configurations, unusual environments) can be classified by IP or session context.

2. sourceIPAddress

Source IP is a strong secondary classifier:

  • AWS service-internal IPs (e.g., Lambda, ECS, EC2 within VPC) — these indicate service-to-service calls
  • Known CI/CD IP ranges (GitHub Actions, GitLab, CircleCI all publish their egress IP ranges) — indicates automation
  • AWS infrastructure addresses (*.amazonaws.com) — indicates cross-service calls
  • Corporate network ranges — indicates human access from managed devices
  • Consumer ISP ranges — indicates human access from personal/remote devices
  • Unknown/new IPs — highest risk signal, especially for established roles
IDENTITY CLASSIFICATION FROM CLOUDTRAIL SIGNALSTYPEUSER-AGENTSOURCE IPMFAACCESS PATTERNHUMANaws-console / aws-cliCorporate / VPNyesIrregular, burstySERVICEaws-sdk-java / boto3VPC internalnoSteady, predictableLAMBDAboto3 / botocoreAWS internalnoBurst, short-livedCI/CDaws-cli + GH ActionsKnown CI rangesnoDeploy windowsAI_AGENTboto3 / custom SDKVPC / LambdanoQuery-driven, doc rangeseach identity type gets its own behavioral baseline — a Lambda and a human on the same role are modeled separately
Identity classification from CloudTrail signals — each type gets its own behavioral baseline

3. sessionContext

The sessionContext field reveals how the credentials were obtained — which tells you a lot about the identity type:

# Human with MFA (high trust):
"sessionContext": {
  "sessionIssuer": { "type": "Role", "principalId": "AROA...", "arn": "arn:aws:iam::...:role/HumanRole" },
  "attributes": { "mfaAuthenticated": "true", "creationDate": "2026-03-09T09:14:00Z" }
}

# Lambda assumed role (service identity):
"sessionContext": {
  "sessionIssuer": { "type": "Role", "arn": "arn:aws:iam::...:role/my-lambda-role" },
  "ec2RoleDelivery": null,
  "attributes": { "mfaAuthenticated": "false" }
}

# EC2 instance profile (service-to-service):
"sessionContext": {
  "sessionIssuer": { "type": "Role", "arn": "arn:aws:iam::...:role/my-ec2-role" },
  "ec2RoleDelivery": "1.0"
}

Key signals: mfaAuthenticated: true strongly suggests human access. The principalId (who assumed the role) tells you the source identity — a Lambda execution role will have a Lambda ARN as the issuer, while a human will have a user ARN.

Why This Changes Your Detection Strategy

Once you can classify identity types, your behavioral detection changes fundamentally:

  • Separate baselines per identity type — Lambda and human access to the same role are modeled independently, eliminating the averaging problem
  • Different anomaly thresholds — service identities have predictable, machine-regular patterns; small deviations are high-signal. Human access has natural variance; larger deviations are needed to fire
  • Identity-specific detectors — "new IP for this role" is much more interesting for a Lambda (which should always call from AWS-internal IPs) than for a human (who might travel, use VPN, work from home)
  • Cross-assumer detection — alert when a role that was previously only assumed by Lambda functions suddenly gets a human MFA session (possible credential compromise)
  • NHI inventory — track which service identities exist, what they access, and flag "orphaned NHIs" whose source service has been decommissioned but the credentials still work
NHI inventory showing identity type distribution
NHI inventory: service identity classification lets you see the distribution of human vs non-human access per IAM role

Common Anti-Patterns That Break Identity Visibility

Even with good tooling, these IAM anti-patterns make NHI detection harder:

  • Shared roles — one IAM role used by both a Lambda and a human. Fix: one role per identity, named to reflect its consumer
  • Service accounts masquerading as users — a "service" that assumes a human's role by having access to long-lived user credentials. Fix: service accounts should assume dedicated service roles, never user roles
  • Over-permissioned instance profiles — an EC2 instance with S3 access that's actually used by 12 different applications. Fix: task-level IAM roles via ECS task definitions or pod-level IRSA in EKS
  • Long-lived access keys for Lambda — Lambda should use execution roles via STS, not static access keys. Static keys in Lambda code are a critical NHI risk
  • Unnamed automation roles — roles named "automation-role" or "service-account" with no indication of the consuming system. Fix: name roles to reflect their source: lambda-rag-pipeline-prod, github-actions-deploy, etc.

Getting Started with NHI Visibility

If you're starting from zero, here's a practical sequence:

  • Inventory your IAM roles — use IAM Access Analyzer or AWS Config to list all roles with S3 permissions and when they were last used
  • Classify manually first — go through the top 20 most active S3 roles and classify each as HUMAN or SERVICE/LAMBDA/CI_CD by examining the userAgent in recent CloudTrail events
  • Build a naming convention — rename roles to reflect their consumer type and owner team. This makes automated classification far more accurate
  • Enable Entroptics NHI classification — connect your CloudTrail stream and let the identity classifier run. Review the automated classifications on your first 50 roles to calibrate confidence
  • Set separate baselines per identity type — once classification is running, separate baselines are built automatically per type
  • Alert on identity type changes — when a role previously only accessed by Lambda suddenly gets a human console session, that's your highest-priority alert
  • NHI
  • IAM
  • CloudTrail
  • Identity
  • DevSecOps