AWS IAM is the authorization fabric of every AWS resource. Every API callβwhether from a human, a service, or a third-party applicationβmust be authenticated as a principal and authorized by policy evaluation before AWS executes it. Getting IAM wrong is the root cause of most cloud breaches (exposed access keys, over-permissive roles, misconfigured bucket policies). This knowledge base covers the full security stack from IAM primitives through threat detection, giving you crisp answers to every interview question on the topic.
A principal is an entity that can make API calls to AWS:
| Type | Attached To | Stored In | Use Case |
|---|---|---|---|
| AWS Managed Policy | Users, Groups, Roles | AWS account (shared) | Common reusable policies (e.g., AmazonS3ReadOnlyAccess) |
| Customer Managed Policy | Users, Groups, Roles | Your account | Custom reusable policies you maintain |
| Inline Policy | Exactly one user/group/role | Embedded in the entity | Strict 1:1 relationship; deleted with entity |
| Resource-Based Policy | Attached to a resource (S3, KMS, Lambda) | The resource itself | Cross-account access without assuming a role |
| Permission Boundary | IAM User or Role | Account | Maximum permissions any identity-based policy can grant (not additive) |
| Service Control Policy (SCP) | AWS Organization OU or account | AWS Organizations | Maximum permissions for all principals in the OU/account |
| Session Policy | Passed inline during AssumeRole | Temporary session | Narrow a role's permissions for a specific session |
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "AllowS3Read",
"Effect": "Allow",
"Principal": { "AWS": "arn:aws:iam::123456789012:role/MyRole" },
"Action": ["s3:GetObject", "s3:ListBucket"],
"Resource": [
"arn:aws:s3:::my-bucket",
"arn:aws:s3:::my-bucket/*"
],
"Condition": {
"StringEquals": { "s3:prefix": ["docs/"] },
"Bool": { "aws:SecureTransport": "true" }
}
}
]
}
Key fields: Effect (Allow/Deny), Action (service:operation), Resource (ARN), Condition (optional constraints). Principal is only used in resource-based policies and trust policies.
1. Explicit DENY in any policy β DENY (stops immediately)
2. SCP restricts? β DENY
3. Resource-based policy allows? β ALLOW (for cross-account: both sides must allow)
4. Identity-based policy allows? β ALLOW
5. Permission boundary allows? β (identity policy must also allow)
6. Session policy allows? β (identity policy must also allow)
7. Default β IMPLICIT DENY
For same-account access, the effective permission is:
effective = identity_policy β© permission_boundary β© session_policy β© SCP
Any explicit Deny overrides everything. An Allow only works if no layer blocks it.
For principal in Account A to access resource in Account B:
A permission boundary is an IAM managed policy attached to a user or role that sets the maximum permissions. If a user has AdministratorAccess but a boundary only allows S3, the user can only access S3. Useful when delegating IAM management to developers without letting them escalate privileges.
Effective Permission = identity_policy AND permission_boundary
A permission boundary does not grant permissions; it limits them.
| Condition Key | Usage |
|---|---|
aws:RequestedRegion | Restrict calls to specific regions |
aws:SourceIp / aws:VpcSourceIp | IP-based restrictions |
aws:PrincipalArn | Match the calling principal's ARN |
aws:MultiFactorAuthPresent | Require MFA for sensitive actions |
aws:SecureTransport | Require HTTPS |
s3:prefix | Restrict S3 ListBucket to specific prefixes |
sts:ExternalId | Confused deputy protection for cross-account |
aws:PrincipalOrgID | Restrict to principals within an AWS Organization |
Resource ARNs as specifically as possible (avoid * where possible)."Bool": { "aws:MultiFactorAuthPresent": "true" }.access-keys-rotated.aws iam get-credential-report to audit all users' key ages.A role has two policies:
Principal element.// Trust Policy β allows EC2 service to assume this role
{
"Version": "2012-10-17",
"Statement": [{
"Effect": "Allow",
"Principal": { "Service": "ec2.amazonaws.com" },
"Action": "sts:AssumeRole"
}]
}
169.254.169.254/latest/meta-data/iam/security-credentials/<role-name>. IMDSv2 is required (token-based, not just IP-hop exploitable).sts:AssumeRole on Account B's role ARN.sts:ExternalId condition to prevent confused deputy attacks when Account B trusts a third party.// Trust policy with ExternalId (confused deputy protection)
{
"Condition": {
"StringEquals": { "sts:ExternalId": "unique-customer-id-123" }
}
}
Assuming Role A, then using those credentials to assume Role B. Session duration resets at each hop (max 1 hour for chained sessions, regardless of role's MaxSessionDuration). Chaining is tracked in CloudTrail.
MaxSessionDuration).| API | Used By | Notes |
|---|---|---|
AssumeRole | IAM users, roles, services | Returns temp credentials (AccessKeyId, SecretAccessKey, SessionToken) |
AssumeRoleWithWebIdentity | OIDC federated identities (Cognito, GitHub Actions, K8s IRSA) | JWT validated against OIDC provider |
AssumeRoleWithSAML | Enterprise SSO (ADFS, Okta via SAML 2.0) | SAML assertion validated |
GetSessionToken | IAM users requiring MFA | Elevates session to include MFA context |
GetFederationToken | Custom federation brokers | Returns temp credentials for a federated user |
Principal calls STS API
β STS validates caller's identity
β STS checks caller's permission to assume the role (trust policy)
β STS generates temp credentials with TTL
β Caller uses AccessKeyId + SecretAccessKey + SessionToken for API calls
β Credentials expire; caller must re-assume
Used by: Cognito Identity Pools, GitHub Actions, Kubernetes IRSA (IAM Roles for Service Accounts). See CI/CD & DevOps for pipeline-level OIDC trust setup and keyless credential patterns.
GitHub Actions workflow β OIDC token (JWT) from GitHub
β AssumeRoleWithWebIdentity with JWT + RoleArn
β STS validates JWT signature against GitHub's JWKS endpoint
β Returns temp AWS credentials valid for the workflow
Trust policy for GitHub Actions:
{
"Condition": {
"StringEquals": {
"token.actions.githubusercontent.com:aud": "sts.amazonaws.com",
"token.actions.githubusercontent.com:sub": "repo:myorg/myrepo:ref:refs/heads/main"
}
}
}
Attached directly to the S3 bucket. Can allow cross-account access without role assumption.
{
"Statement": [{
"Effect": "Allow",
"Principal": { "AWS": "arn:aws:iam::999999999999:root" },
"Action": ["s3:GetObject"],
"Resource": "arn:aws:s3:::my-bucket/*",
"Condition": {
"StringEquals": { "aws:PrincipalOrgID": "o-xxxxxxxxxx" }
}
}]
}
Block Public Access settings override bucket policies for public access. S3 Object Ownership controls ACLs (disable ACLs = bucket owner enforced).
Every KMS key must have a key policy (unlike other resource-based policies, there is no fallback). The key policy must explicitly allow the account root to delegate via IAM policies, otherwise only the policy listed in the key policy has access.
{
"Statement": [
{
"Sid": "Enable IAM User Permissions",
"Effect": "Allow",
"Principal": { "AWS": "arn:aws:iam::123456789012:root" },
"Action": "kms:*",
"Resource": "*"
},
{
"Sid": "Allow Lambda to use the key",
"Effect": "Allow",
"Principal": { "AWS": "arn:aws:iam::123456789012:role/LambdaRole" },
"Action": ["kms:Decrypt", "kms:GenerateDataKey"],
"Resource": "*"
}
]
}
Allow other AWS services or accounts to invoke a Lambda function.
aws lambda add-permission \
--function-name myFunction \
--statement-id AllowS3Invoke \
--action lambda:InvokeFunction \
--principal s3.amazonaws.com \
--source-arn arn:aws:s3:::my-bucket \
--source-account 123456789012
source-account prevents confused deputy when a service (S3) from any account could trigger the function.
Root
βββ Management Account
βββ OU: Security
β βββ Account: Security Tooling (GuardDuty delegated admin)
βββ OU: Production
β βββ Account: Prod App
β βββ Account: Prod Data
βββ OU: Sandbox
βββ Account: Developer Sandbox
Deny (blacklist) or Allow (whitelist with explicit allow on specific actions).// Deny all actions outside approved regions
{
"Effect": "Deny",
"NotAction": [
"iam:*", "sts:*", "cloudfront:*", "route53:*", "support:*", "budgets:*"
],
"Resource": "*",
"Condition": {
"StringNotEquals": {
"aws:RequestedRegion": ["us-east-1", "us-west-2", "eu-west-1"]
}
}
}
// Prevent disabling CloudTrail
{
"Effect": "Deny",
"Action": ["cloudtrail:StopLogging", "cloudtrail:DeleteTrail"],
"Resource": "*"
}
// Require MFA for sensitive actions
{
"Effect": "Deny",
"Action": ["iam:*", "organizations:*"],
"Resource": "*",
"Condition": {
"BoolIfExists": { "aws:MultiFactorAuthPresent": "false" }
}
}
GuardDuty, Security Hub, IAM Access Analyzer, and Macie can all designate a security tooling account as the delegated administrator, receiving findings from all member accounts.
| User Pool | Identity Pool | |
|---|---|---|
| Purpose | Authentication (who you are) | Authorization (AWS credentials) |
| Returns | JWT tokens (ID, Access, Refresh) | Temporary AWS credentials (STS) |
| Use Case | Sign up, sign in, MFA, hosted UI | Exchange JWT/SAML for AWS access |
| Federated IdPs | Google, Facebook, SAML, OIDC | User Pool, Google, Facebook, SAML, OIDC, unauthenticated guests |
User signs in β User Pool authenticates β Returns:
ID Token (JWT, claims about the user β for your app)
Access Token (JWT, OAuth 2.0 scopes β for API calls)
Refresh Token (long-lived, get new ID/Access tokens)
email, sub, custom attributes). Verify signature with Cognito's JWKS endpoint.User authenticates with User Pool (or Google, etc.)
β Receives JWT
β Calls Cognito Identity Pool with JWT
β Identity Pool calls STS AssumeRoleWithWebIdentity
β Returns temporary AWS credentials
β User directly calls AWS services ([S3](/aws/storage-s3), [DynamoDB](/aws/dynamodb-data-services)) with those credentials
Identity Pools define authenticated role and unauthenticated role (guest access). Enhanced flow adds fine-grained role mapping per user group.
Cognito-managed OAuth 2.0 / OIDC endpoint. Supports Authorization Code with PKCE, Implicit (legacy), and Client Credentials flows. Custom domain + SSL required for production. Returns authorization code β exchange for tokens via /oauth2/token endpoint.
AWSManagedRulesCommonRuleSet β OWASP Top 10 (SQLi, XSS, bad inputs).AWSManagedRulesKnownBadInputsRuleSet β known exploit patterns.AWSManagedRulesAmazonIpReputationList β AWS threat intelligence.AWSManagedRulesBotControlRuleSet β bot detection (common bots, targeted bots with browser fingerprinting).AWSManagedRulesAntiDDoSRuleSet β DDoS attack signatures (requires Shield Advanced).Aggregate requests by IP or custom key over a 5-minute window. Trigger on threshold β apply action (Block, CAPTCHA). Can scope to specific URIs or header values. First line of defense against credential stuffing and scraping.
Send full request logs to S3, CloudWatch Logs, or Kinesis Data Firehose. Use Athena + S3 for ad-hoc analysis. COUNT action during testing before switching to BLOCK.
| Feature | Shield Standard | Shield Advanced |
|---|---|---|
| Cost | Free, automatic | $3,000/month per org + data transfer fees |
| Coverage | L3/L4 volumetric DDoS (SYN floods, UDP reflection) | L3/L4 + L7 (HTTP floods), application-layer attacks |
| SRT Access | No | Yes β AWS Shield Response Team 24/7 |
| Cost Protection | No | Yes β credits for scaling costs during DDoS |
| DDoS Dashboard | No | Yes β real-time attack visibility |
| Proactive Engagement | No | Yes β SRT contacts you during events |
| Resources Protected | All AWS | EC2, ELB, CloudFront, Global Accelerator, Route 53 |
Shield Advanced integrates with WAF at no additional WAF cost for protected resources. Use Health-Based Detection (Route 53 health checks) to detect application-level impact.
| Type | Who Controls Material | Rotation | Cross-Account |
|---|---|---|---|
| AWS Managed Key | AWS | Automatic (every year) | No |
| Customer Managed Key (CMK) | You (key policy) | Optional automatic or manual | Yes (key policy) |
| Customer-Provided Key (SSE-C) | You (sent per-request) | Your responsibility | N/A |
| External Key Store (XKS) | Your HSM (on-premises) | Your responsibility | N/A |
KMS never encrypts large data directly. Instead:
1. GenerateDataKey β returns (plaintext data key, encrypted data key)
2. Encrypt data locally with plaintext data key (AES-256-GCM)
3. Store encrypted data key alongside ciphertext
4. Discard plaintext data key from memory
To decrypt:
1. Call KMS Decrypt with encrypted data key β get plaintext data key
2. Decrypt data locally
3. Discard plaintext data key
This keeps KMS API calls minimal and supports large data. Used by S3, EBS, RDS, Lambda environment variables, etc.
"Principal": {"AWS": "arn:aws:iam::ACCOUNT:root"} then IAM policies in that account can also grant KMS access.Temporary, delegated permissions to use a key. Created by kms:CreateGrant. Used by services (EBS, S3) to grant temporary access to specific operations on your behalf. No need to modify key policy. Retired when no longer needed via kms:RetireGrant.
Primary key in one region; replica keys in others. Same key material, different ARNs. Allows decrypt in a different region than where data was encrypted. Useful for disaster recovery and global applications.
| Secrets Manager | Parameter Store | |
|---|---|---|
| Primary Use | Database credentials, API keys, OAuth tokens | Configuration data, feature flags, secrets (via SecureString) |
| Automatic Rotation | Yes β built-in Lambda rotation for RDS, Redshift, DocumentDB; custom rotation Lambda | No native rotation (can implement with EventBridge + Lambda) |
| Cost | $0.40/secret/month + $0.05 per 10,000 API calls | Standard: free. Advanced: $0.05/parameter/month. Higher throughput: $0.05 per 10,000 API calls |
| Max Secret Size | 65,536 bytes | 4,096 bytes (standard), 8,192 bytes (advanced) |
| Versioning | Yes (AWSPENDING, AWSCURRENT, AWSPREVIOUS) | Yes (version labels) |
| Cross-Account | Yes (resource-based policy) | No |
| Encryption | KMS (required) | KMS (optional for SecureString) |
| Hierarchies | No | Yes (/app/prod/db-url) |
Rotation Lambda lifecycle:
createSecret β generate new credentials, store as AWSPENDING
setSecret β apply new credentials to the service (e.g., RDS password change)
testSecret β verify new credentials work
finishSecret β swap AWSPENDING β AWSCURRENT; old AWSCURRENT β AWSPREVIOUS
RDS Proxy integration: caches database connections, transparently uses latest secret during rotation with zero downtime.
| Security Group | NACL | |
|---|---|---|
| Level | Instance/ENI (network interface) | Subnet |
| State | Stateful β return traffic auto-allowed | Stateless β must explicitly allow inbound AND outbound |
| Rules | Allow only (no explicit deny) | Allow and Deny |
| Evaluation | All rules evaluated, most permissive wins | Rules evaluated in order (lowest number first); first match wins |
| Default | New SG: deny all inbound, allow all outbound | Default NACL: allow all. Custom NACL: deny all |
| Scope | Can reference other SGs as source/dest | Only CIDR ranges |
sg-alb-id as source for sg-app).0.0.0.0/0 inbound on SSH/RDP; use SSM Session Manager instead.Log metadata (not payload) of IP traffic in/out of ENIs, subnets, or VPCs. Fields: version, account-id, interface-id, srcaddr, dstaddr, srcport, dstport, protocol, packets, bytes, start, end, action (ACCEPT/REJECT), log-status.
Destinations: CloudWatch Logs, S3. Query with CloudWatch Insights or Athena. Use for:
Records every AWS API call (management plane actions): who called what, from where, at what time, with what result. Essential for security forensics, compliance, and change auditing. For metrics, dashboards, and log-based alerting built on top of CloudTrail, see Observability.
{
"eventVersion": "1.08",
"userIdentity": {
"type": "AssumedRole",
"principalId": "AROAEXAMPLE:session-name",
"arn": "arn:aws:sts::123:assumed-role/AdminRole/session-name",
"accountId": "123456789012"
},
"eventTime": "2026-03-28T12:00:00Z",
"eventSource": "s3.amazonaws.com",
"eventName": "DeleteBucket",
"awsRegion": "us-east-1",
"sourceIPAddress": "1.2.3.4",
"requestParameters": { "bucketName": "critical-data-bucket" },
"responseElements": null,
"errorCode": "AccessDenied",
"errorMessage": "Access Denied"
}
Partition the CloudTrail S3 prefix by region/year/month/day. Create an Athena table with the CloudTrail schema. Example queries:
-- Find all API calls by a specific principal in the last 24 hours
SELECT eventtime, eventsource, eventname, errorcode
FROM cloudtrail_logs
WHERE useridentity.arn LIKE '%suspicious-role%'
AND eventtime > '2026-03-27'
ORDER BY eventtime DESC;
-- Find all DeleteBucket calls
SELECT eventtime, sourceipaddress, requestparameters
FROM cloudtrail_logs
WHERE eventname = 'DeleteBucket';
Detects unusual API activity (write management events). Compares baseline to current activity. Generates Insights events for anomalies like unusual IAM API call rates.
Continuous threat detection ML service. For a full comparison of GuardDuty, Macie, Inspector, and Security Hub, see Security Services. Ingests: CloudTrail management + S3 data events, VPC Flow Logs, DNS logs, EKS audit logs, RDS login activity, Lambda network activity, S3 access patterns, Malware Protection (EBS volumes).
| Category | Example Findings |
|---|---|
| Backdoor | Backdoor:EC2/C&CActivity.B β EC2 communicating with known C2 |
| Behavior | Behavior:EC2/NetworkPortUnusual β instance using unusual port |
| CryptoMining | CryptoCurrency:EC2/BitcoinTool.B β crypto mining detected |
| Recon | Recon:IAMUser/MaliciousIPCaller β API calls from malicious IP |
| Stealth | Stealth:IAMUser/CloudTrailLoggingDisabled β trail stopped |
| Trojan | Trojan:EC2/BlackholeTraffic β traffic to sinkhole domain |
| UnauthorizedAccess | UnauthorizedAccess:IAMUser/ConsoleLoginSuccess.B β console login from Tor |
| Execution | Execution:ECS/MaliciousFile β malware in container |
| PrivilegeEscalation | PrivilegeEscalation:IAMUser/AnomalousBehavior β unusual IAM action |
Filter out low-value findings (known benign activity) from current and future findings. Does not delete underlying data. Suppression rules match on finding type, severity, resource, or attribute.
In AWS Organizations, designate a security account as GuardDuty delegated admin. All member accounts auto-enrolled. Aggregated findings in the security account. Individual accounts cannot disable GuardDuty.
GuardDuty Finding β EventBridge Rule β Lambda
Lambda:
- Isolate EC2 instance (change SG to deny-all)
- Revoke IAM credentials (add inline deny policy)
- Create snapshot of EBS volume for forensics
- Send notification to Slack/PagerDuty
See Lambda for function configuration and EC2/ECS/EKS for instance isolation details.
Single pane of glass for AWS security findings. Aggregates findings from: GuardDuty, Inspector, Macie, IAM Access Analyzer, Firewall Manager, Systems Manager Patch Manager, and third-party partners (CrowdStrike, Palo Alto, etc.). Pairs with CI/CD pipelines for automated remediation workflows.
Security Hub runs continuous automated checks against your AWS resources. Findings are mapped to ASFF (Amazon Security Finding Format) and scored by severity.
| Standard | What It Checks |
|---|---|
| AWS Foundational Security Best Practices (FSBP) | AWS-specific controls (MFA on root, no public S3, etc.) |
| CIS AWS Foundations Benchmark v1.4/v3.0 | 50+ controls for AWS account hardening |
| PCI DSS v3.2.1 | Controls for cardholder data environments |
| NIST SP 800-53 | Federal security controls |
| SOC 2 | Trust service criteria |
NEW β NOTIFIED β SUPPRESSED / RESOLVED
Integrate with Jira/ServiceNow via EventBridge + Lambda to auto-create tickets for high-severity findings.
Q: What is the difference between an IAM role and an IAM user? A: An IAM user has long-term, static credentials (password + access key) representing a specific human or application. An IAM role has no long-term credentials; instead, it is assumed by trusted principals (users, services, external identities) via STS, which issues short-lived temporary credentials. Use roles for all service-to-service communication, cross-account access, and federated identities. Prefer roles over users for non-human access because temporary credentials limit the blast radius of a compromise.
Q: How does policy evaluation work when there are conflicting allow and deny statements?
A: An explicit Deny always wins over any Allow, regardless of where it lives (identity policy, resource policy, SCP, permission boundary). The evaluation order is: explicit Deny first β SCP restrictions β resource-based policy β identity-based policy β permission boundary β session policy β implicit Deny (default). This means even if a user has AdministratorAccess, a single Deny statement in an SCP or permission boundary will block that action.
Q: An EC2 instance in your VPC cannot reach an S3 bucket. How do you troubleshoot?
A: Check in order: (1) Does the EC2 instance role have s3:GetObject/s3:PutObject on the bucket ARN? (2) Does the S3 bucket policy have an explicit Deny overriding it (e.g., restricting to a VPC endpoint)? (3) Is S3 Block Public Access blocking it? (4) Is there a VPC Gateway Endpoint for S3 in the route table for the subnet? If not, traffic goes through the internet β does the instance have a route to the internet (NAT for private subnet, IGW for public)? (5) Does the security group allow outbound HTTPS (443) to S3? (6) Does the NACL allow outbound HTTPS and inbound ephemeral ports (1024-65535)?
Q: Explain envelope encryption and why KMS doesn't encrypt data directly.
A: KMS has a 4KB payload limit and adds latency per API call. Envelope encryption solves this: you call GenerateDataKey to get a plaintext data key (256-bit AES) and its KMS-encrypted copy. You encrypt your data locally with the plaintext key (fast, no KMS call), then discard the plaintext key and store the encrypted key alongside the ciphertext. To decrypt, you call KMS Decrypt on the encrypted data key, then use the returned plaintext key to decrypt locally. This keeps all large data operations local while KMS protects the key material.
Q: What is a confused deputy attack and how do you prevent it in AWS?
A: The confused deputy problem occurs when a trusted service uses its authority to perform actions on behalf of an attacker. In AWS, a third-party SaaS (Account B) has a role in your account (Account A) to read your S3. Without protection, Account B's software could substitute your role ARN with another customer's role ARN, reading their data using its AWS service permissions. Prevention: add a sts:ExternalId condition to the role's trust policy. The SaaS must supply the unique external ID (which only you and the SaaS know) when assuming the role, proving the call is legitimately on your behalf.
Q: What is the difference between a Security Group and a NACL? Which would you use to block a specific IP? A: Security Groups are stateful and operate at the ENI level β return traffic is automatically allowed without an explicit rule. They only support Allow rules. NACLs are stateless and operate at the subnet level β you must explicitly allow both inbound and outbound traffic including return packets. NACLs support Deny rules and evaluate in ascending rule number order (first match wins). To block a specific IP address, use a NACL (or WAF for HTTP traffic) because Security Groups cannot deny β they only allow. Place the Deny rule at a lower number than any Allow rules.
Q: How do you securely allow a Lambda function in Account A to read from an S3 bucket in Account B?
A: Two-sided permission grant is required. In Account B, add a bucket policy statement granting s3:GetObject to arn:aws:iam::ACCOUNT_A:role/LambdaExecutionRole. In Account A, the Lambda's execution role must have an IAM policy allowing s3:GetObject on Account B's bucket ARN. Both sides must allow; either alone is insufficient for cross-account access. Use aws:PrincipalOrgID condition in the bucket policy for an added guardrail, ensuring only principals from your organization can be granted access.
Q: What does GuardDuty detect and how does it differ from CloudTrail? A: CloudTrail is a raw audit log β it records every API call but does no analysis. GuardDuty is a threat detection service that consumes CloudTrail, VPC Flow Logs, and DNS logs as inputs and applies ML models and threat intelligence feeds to detect malicious patterns: EC2 instances calling C2 servers, crypto mining, credential exfiltration, unusual API call sequences (privilege escalation, data exfiltration). CloudTrail = what happened; GuardDuty = what looks malicious. GuardDuty finding triggers EventBridge for automated response.
Q: How do SCPs differ from IAM policies? Can an SCP grant permissions?
A: SCPs are guardrails, not grants. An SCP restricts the maximum permissions available to all principals in an AWS Organization OU or account β including the account's root user. An SCP cannot grant any permissions; it only limits what IAM policies can allow. If an SCP allows s3:* and denies nothing else, a user still needs an IAM policy that explicitly allows S3 access. SCPs also never affect the management account.
Q: When would you use Cognito User Pools vs Identity Pools? Can you use both together? A: User Pools handle application-level authentication: sign-up, sign-in, MFA, password reset. They return JWTs your API can validate. Identity Pools handle AWS authorization: they exchange a JWT (from a User Pool, Google, SAML, etc.) for temporary AWS credentials via STS. Use them together when your application needs to both authenticate users AND have those users call AWS services directly (S3, DynamoDB, IoT). The User Pool authenticates, and the Identity Pool federates those identities into AWS IAM roles.
Q: How would you detect if an IAM access key has been compromised?
A: (1) GuardDuty: UnauthorizedAccess:IAMUser/ConsoleLoginSuccess.B or recon finding types detect unusual API calls from malicious IPs/Tor. (2) CloudTrail: search for API calls from unusual IPs, unusual regions, at unusual times. (3) IAM Access Analyzer: check for unexpected resource access. (4) AWS Health events and credential report for old/unrotated keys. (5) AWS Config rule access-keys-rotated. Response: immediately deactivate the key, attach an explicit Deny inline policy to the user, investigate CloudTrail for the blast radius, rotate all secrets the compromised identity could access.
Q: What is IAM Access Analyzer and how does it help with least privilege? A: Access Analyzer has three functions: (1) External access analysis β continuously scans IAM roles, S3 buckets, KMS keys, Lambda functions, SQS queues, Secrets Manager secrets to find any that allow access from outside the zone of trust (your account or org). (2) Policy validation β checks policies for correctness, security warnings, and adherence to best practices before deployment. (3) Policy generation β uses CloudTrail activity logs (up to 90 days) to generate a least-privilege policy that includes only the actions a principal actually used, eliminating permission bloat.
Q: Explain the KMS key policy and why it is different from other resource-based policies.
A: Every KMS CMK must have a key policy β there is no implicit default grant via IAM alone. If you create a CMK and do not include the root account delegation statement ("Principal": {"AWS": "arn:aws:iam::ACCOUNT:root"}, "Action": "kms:*"), then only the entities explicitly named in the key policy can use the key, even if they have kms:* in their IAM policy. This is unlike S3, where a missing bucket policy falls through to IAM. The key policy is the authoritative source; it must explicitly enable IAM delegation or list every principal. This prevents inadvertent key access due to overly broad IAM policies.
Root account:
- MFA enabled, no access keys
- Budget alert set
- CloudTrail organization trail enabled β S3 with MFA delete
- GuardDuty delegated admin β security account
- Security Hub delegated admin β security account
- IAM Access Analyzer organization-level analyzer
- Config rules: required-tags, restricted-ssh, mfa-enabled-for-iam-console
- SCP: deny-root-access, require-mfa, restrict-regions, prevent-cloudtrail-disable
Client
β (HTTPS)
CloudFront + WAF (Managed rules + rate limiting)
β
API Gateway (Cognito User Pool Authorizer or Lambda Authorizer)
β (validates JWT, injects principal context)
Lambda (execution role: least-privilege, no wildcard resources)
β
DynamoDB / S3 / RDS Proxy
β
Secrets Manager (rotation enabled, RDS Proxy caches)
Each layer has IAM implications: API Gateway authorizers validate identity, Lambda execution roles scope resource access, S3 bucket policies enforce per-resource grants, and DynamoDB supports fine-grained attribute-level access via IAM condition keys. Deploy and manage the full stack via CDK / IaC.
Application start:
Lambda/ECS calls Secrets Manager GetSecretValue
β returns JSON { "username": "...", "password": "..." }
β cached in-process for TTL
Rotation event:
EventBridge scheduled β rotation Lambda
β createSecret β setSecret β testSecret β finishSecret
β RDS Proxy detects new AWSCURRENT β seamless connection refresh
β Zero downtime
CI/CD Account:
CodePipeline role β assumes DeployRole in Target Account
Trust policy: allows ci-cd-account:CodePipelineRole with ExternalId
Target Account:
DeployRole: permissions to CloudFormation, ECS update, Lambda deploy
SCPs: prevent deploy role from modifying CloudTrail, GuardDuty, or security tooling
Security Account (delegated admin):
Receives all GuardDuty + Security Hub findings
Remediation Lambda auto-isolates resources on CRITICAL findings
See CI/CD & CodePipeline for CodePipeline setup and CDK deployment patterns.
Root
βββ Management Account (billing only, no workloads)
βββ Security OU
β βββ Security Tooling Account (GuardDuty admin, Security Hub)
β βββ Log Archive Account (CloudTrail, Config history)
βββ Infrastructure OU
β βββ Network Account (Transit Gateway, VPC hub)
β βββ Shared Services Account (AD, DNS, AMI library)
βββ Workloads OU
β βββ Production OU
β β βββ Prod Account A
β β βββ Prod Account B
β βββ Staging OU
β βββ Development OU
βββ Sandbox OU (dev experiments, no production data)
SCPs define the maximum available permissions for all IAM entities in an account. They apply in addition to (not instead of) IAM policies β the effective permission is the intersection.
// Deny all actions outside allowed regions (data residency)
{
"Version": "2012-10-17",
"Statement": [{
"Sid": "DenyOutsideAllowedRegions",
"Effect": "Deny",
"NotAction": [
"iam:*", "sts:*", "organizations:*", "support:*",
"cloudfront:*", "route53:*", "waf:*" // global services exempt
],
"Resource": "*",
"Condition": {
"StringNotEquals": {"aws:RequestedRegion": ["us-east-1", "eu-west-1"]}
}
}]
}
// Deny root account usage (attach to all workload OUs)
{
"Statement": [{
"Sid": "DenyRootAccess",
"Effect": "Deny",
"Action": "*",
"Resource": "*",
"Condition": {"StringLike": {"aws:PrincipalArn": "arn:aws:iam::*:root"}}
}]
}
// Require MFA for sensitive actions
{
"Statement": [{
"Sid": "DenyWithoutMFA",
"Effect": "Deny",
"Action": ["iam:DeleteVirtualMFADevice", "iam:DeactivateMFADevice"],
"Resource": "*",
"Condition": {"BoolIfExists": {"aws:MultiFactorAuthPresent": "false"}}
}]
}
// Prevent leaving the organization or disabling security services
{
"Statement": [{
"Sid": "ProtectSecurityServices",
"Effect": "Deny",
"Action": [
"organizations:LeaveOrganization",
"guardduty:DeleteDetector",
"cloudtrail:DeleteTrail",
"config:DeleteConfigurationRecorder",
"securityhub:DeleteHub"
],
"Resource": "*"
}]
}
Control Tower automates account vending and applies a baseline of guardrails to new accounts:
Corporate AD / Okta / Azure AD
β SAML 2.0 / SCIM provisioning
βΌ
IAM Identity Center
β Permission Sets (IAM policies)
βΌ
Multiple AWS Accounts
(users get temporary credentials via AWS access portal)
// CDK: IAM Identity Center permission set
import * as sso from 'aws-cdk-lib/aws-sso';
const developerPermissionSet = new sso.CfnPermissionSet(this, 'DeveloperAccess', {
instanceArn: 'arn:aws:sso:::instance/ssoins-...',
name: 'DeveloperAccess',
description: 'Read/write access for developers',
sessionDuration: 'PT8H', // 8-hour sessions
managedPolicies: ['arn:aws:iam::aws:policy/PowerUserAccess'],
inlinePolicy: JSON.stringify({
Statement: [{
Effect: 'Deny',
Action: ['iam:*', 'organizations:*'],
Resource: '*',
}]
}),
});
// Assign: Group "Developers" β DeveloperAccess β Staging accounts
new sso.CfnAssignment(this, 'DevAssignment', {
instanceArn: 'arn:aws:sso:::instance/ssoins-...',
permissionSetArn: developerPermissionSet.attrPermissionSetArn,
principalType: 'GROUP',
principalId: 'ad-group-id-for-developers',
targetType: 'AWS_ACCOUNT',
targetId: '111111111111', // staging account
});
# Configure SSO profile
aws configure sso
# β SSO start URL: https://my-company.awsapps.com/start
# β SSO region: us-east-1
# β Account: staging (select from list)
# β Permission set: DeveloperAccess
# Use profile (auto-refreshes temporary credentials)
aws s3 ls --profile my-staging-developer
# Authenticate (opens browser for MFA)
aws sso login --profile my-staging-developer
ABAC grants permissions based on matching tags on the IAM principal AND the resource.
// IAM policy using principal tags
{
"Version": "2012-10-17",
"Statement": [{
"Effect": "Allow",
"Action": ["ec2:StartInstances", "ec2:StopInstances", "ec2:TerminateInstances"],
"Resource": "arn:aws:ec2:us-east-1:*:instance/*",
"Condition": {
"StringEquals": {
"ec2:ResourceTag/Owner": "${aws:PrincipalTag/email}",
"ec2:ResourceTag/Environment": "${aws:PrincipalTag/environment}"
}
}
}]
}
${aws:PrincipalTag/email} substitutes the tag value from the IAM role/user at request timeemail=dev@company.com, environment=dev; tag EC2 instances with same Owner and Environment tags// ECS task can only access DynamoDB items belonging to its tenant
{
"Statement": [{
"Effect": "Allow",
"Action": ["dynamodb:GetItem", "dynamodb:PutItem", "dynamodb:Query"],
"Resource": "arn:aws:dynamodb:*:*:table/Orders",
"Condition": {
"StringEquals": {
"dynamodb:LeadingKeys": "${aws:PrincipalTag/tenantId}"
}
}
}]
}
Permission boundaries define the maximum permissions an IAM entity can have, regardless of identity-based policies.
Problem: You want developers to create Lambda execution roles without letting them escalate to admin.
// Permission boundary: caps what the created role can do
{
"Version": "2012-10-17",
"Statement": [
// 1. Allow useful actions
{"Effect": "Allow", "Action": ["s3:*", "dynamodb:*", "logs:*", "xray:*"], "Resource": "*"},
// 2. Deny IAM (prevents privilege escalation)
{"Effect": "Deny", "Action": "iam:*", "Resource": "*"},
// 3. Deny billing
{"Effect": "Deny", "Action": ["aws-portal:*", "budgets:*"], "Resource": "*"}
]
}
// Developer policy: allow creating roles ONLY if they attach the boundary
{
"Statement": [{
"Effect": "Allow",
"Action": ["iam:CreateRole", "iam:AttachRolePolicy", "iam:PutRolePolicy"],
"Resource": "arn:aws:iam::*:role/app-*",
"Condition": {
"StringEquals": {
"iam:PermissionsBoundary": "arn:aws:iam::123456789012:policy/DeveloperBoundary"
}
}
}]
}
Now developers can create roles with prefix app-* but every created role is capped by the boundary β they can never create an admin role.
VPC Endpoint Policies restrict which S3 buckets or DynamoDB tables can be accessed via the endpoint, even if IAM allows more. See VPC & Networking for endpoint types and setup.
// S3 Gateway Endpoint policy: only allow access to company buckets
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": "*",
"Action": ["s3:GetObject", "s3:PutObject"],
"Resource": [
"arn:aws:s3:::company-data-*",
"arn:aws:s3:::company-data-*/*"
]
},
// Block access to S3 buckets outside the organization (prevents data exfiltration)
{
"Effect": "Deny",
"Principal": "*",
"Action": "s3:*",
"Resource": "*",
"Condition": {
"StringNotEquals": {"aws:ResourceOrgID": "o-exampleorgid111"}
}
}
]
}
This prevents: compromised EC2 from uploading exfiltrated data to attacker's S3 bucket (outside org).
IAM is the authorization layer for every AWS service. Explore how it applies in practice: