Implement least privilege IAM policies with scoped permissions, permission boundaries, and IRSA for Kubernetes
The principle of least privilege ensures that IAM entities (users, roles, services) have only the permissions necessary to perform their tasksβnothing more. This reduces the blast radius of security incidents and meets compliance requirements.
flowchart TB
subgraph "Permission Layers"
PB[Permission Boundary]
RP[Role Policy]
RP2[Resource Policy]
end
subgraph "Effective Permissions"
EP[Intersection of All Layers]
end
PB --> EP
RP --> EP
RP2 --> EP
style EP fill:#90EE90
A permission boundary sets the maximum permissions a role can have, regardless of its attached policies. Think of it as a "ceiling" on permissions.
AWS-managed roles with predefined permissions for specific services. Use these when available instead of creating custom roles.
For Kubernetes workloads on EKS, IRSA allows pods to assume IAM roles without embedding credentials. This is the recommended approach for EKS.
Resource: "*" when possibleβ Using AdministratorAccess for applications
β Hardcoding credentials in code
β Overly permissive Resource: "*" statements
β Sharing IAM roles across unrelated services
β Long-lived access keys instead of temporary credentials
This example creates a Lambda function with:
π Location: terraform/examples/iam-least-privilege/
# Permission boundary - the ceiling for this role
resource "aws_iam_policy" "permission_boundary" {
name = "${local.name_prefix}-boundary"
policy = jsonencode({
Version = "2012-10-17"
Statement = [
{
Effect = "Allow"
Action = ["s3:*", "dynamodb:*", "logs:*"]
Resource = "*"
Condition = {
StringEquals = {
"aws:RequestedRegion" = var.aws_region
}
}
},
{
Effect = "Deny"
Action = ["iam:*", "organizations:*", "sts:AssumeRole"]
Resource = "*"
}
]
})
}
# Role with scoped policy
resource "aws_iam_role_policy" "lambda_policy" {
role = aws_iam_role.lambda.id
policy = jsonencode({
Version = "2012-10-17"
Statement = [
{
Effect = "Allow"
Action = ["s3:GetObject", "s3:PutObject"]
Resource = "${aws_s3_bucket.data.arn}/*" # Scoped to specific bucket
}
]
})
}
This example creates:
π Location: cdk/examples/iam-least-privilege/
// Create IRSA role with scoped permissions
const serviceAccountRole = new iam.Role(this, 'PodRole', {
assumedBy: new iam.FederatedPrincipal(
cluster.openIdConnectProvider.openIdConnectProviderArn,
{
StringEquals: {
[`${cluster.clusterOpenIdConnectIssuer}:sub`]:
`system:serviceaccount:${namespace}:${serviceAccountName}`,
[`${cluster.clusterOpenIdConnectIssuer}:aud`]: 'sts.amazonaws.com'
}
},
'sts:AssumeRoleWithWebIdentity'
),
permissionsBoundary: permissionBoundary,
});
// Add scoped S3 policy
serviceAccountRole.addToPolicy(new iam.PolicyStatement({
effect: iam.Effect.ALLOW,
actions: ['s3:GetObject', 's3:PutObject'],
resources: [`${dataBucket.bucketArn}/*`],
conditions: {
StringEquals: {
's3:x-amz-acl': 'bucket-owner-full-control'
}
}
}));
Resource: "*" without conditions* actions