The FinOps system requires specific IAM permissions to function properly. This document outlines the minimal required permissions following the principle of least privilege.
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "EC2ResourceScanning",
"Effect": "Allow",
"Action": [
"ec2:DescribeVolumes",
"ec2:DescribeInstances",
"ec2:DescribeSnapshots",
"ec2:DescribeNetworkInterfaces",
"ec2:DescribeSecurityGroups",
"ec2:DescribeRegions",
"ec2:DescribeAvailabilityZones"
],
"Resource": "*"
},
{
"Sid": "RDSResourceScanning",
"Effect": "Allow",
"Action": [
"rds:DescribeDBInstances",
"rds:DescribeDBClusters",
"rds:DescribeDBSnapshots",
"rds:ListTagsForResource"
],
"Resource": "*"
},
{
"Sid": "CloudWatchMetricsAccess",
"Effect": "Allow",
"Action": [
"cloudwatch:GetMetricStatistics",
"cloudwatch:GetMetricData",
"cloudwatch:ListMetrics"
],
"Resource": "*"
},
{
"Sid": "DynamoDBAccess",
"Effect": "Allow",
"Action": [
"dynamodb:PutItem",
"dynamodb:UpdateItem",
"dynamodb:BatchWriteItem"
],
"Resource": [
"arn:aws:dynamodb:*:*:table/finops-cleanup-*",
"arn:aws:dynamodb:*:*:table/finops-cleanup-*/index/*"
]
},
{
"Sid": "SNSNotifications",
"Effect": "Allow",
"Action": ["sns:Publish"],
"Resource": ["arn:aws:sns:*:*:finops-*"]
}
]
}{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "DynamoDBStreamAccess",
"Effect": "Allow",
"Action": [
"dynamodb:DescribeStream",
"dynamodb:GetRecords",
"dynamodb:GetShardIterator",
"dynamodb:ListStreams"
],
"Resource": ["arn:aws:dynamodb:*:*:table/finops-cleanup-*/stream/*"]
},
{
"Sid": "DynamoDBTableUpdates",
"Effect": "Allow",
"Action": ["dynamodb:UpdateItem", "dynamodb:PutItem", "dynamodb:GetItem"],
"Resource": ["arn:aws:dynamodb:*:*:table/finops-cleanup-*"]
},
{
"Sid": "SNSAlertPublishing",
"Effect": "Allow",
"Action": ["sns:Publish"],
"Resource": ["arn:aws:sns:*:*:finops-*"]
}
]
}{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "DynamoDBReadAccess",
"Effect": "Allow",
"Action": [
"dynamodb:Scan",
"dynamodb:Query",
"dynamodb:GetItem",
"dynamodb:BatchGetItem"
],
"Resource": [
"arn:aws:dynamodb:*:*:table/finops-cleanup-*",
"arn:aws:dynamodb:*:*:table/finops-cleanup-*/index/*"
]
},
{
"Sid": "DynamoDBWriteAccess",
"Effect": "Allow",
"Action": ["dynamodb:UpdateItem", "dynamodb:DeleteItem"],
"Resource": ["arn:aws:dynamodb:*:*:table/finops-cleanup-*"]
},
{
"Sid": "ResourceCleanupPermissions",
"Effect": "Allow",
"Action": [
"ec2:DeleteVolume",
"ec2:TerminateInstances",
"ec2:DeleteSnapshot",
"ec2:DeleteNetworkInterface",
"ec2:DeleteSecurityGroup",
"ec2:CreateSnapshot",
"ec2:CreateTags",
"rds:DeleteDBInstance",
"rds:DeleteDBCluster",
"rds:CreateDBSnapshot",
"rds:AddTagsToResource"
],
"Resource": "*",
"Condition": {
"StringEquals": {
"aws:RequestedRegion": ["us-east-1", "us-west-2"]
}
}
},
{
"Sid": "ResourceTaggingPermissions",
"Effect": "Allow",
"Action": [
"ec2:CreateTags",
"ec2:DeleteTags",
"rds:AddTagsToResource",
"rds:RemoveTagsFromResource"
],
"Resource": "*"
}
]
}{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "PreventProtectedResourceDeletion",
"Effect": "Deny",
"Action": [
"ec2:DeleteVolume",
"ec2:TerminateInstances",
"rds:DeleteDBInstance"
],
"Resource": "*",
"Condition": {
"StringEquals": {
"aws:ResourceTag/finops:protect": "true"
}
}
},
{
"Sid": "RequireReasonForDeletion",
"Effect": "Deny",
"Action": ["ec2:DeleteVolume", "ec2:TerminateInstances"],
"Resource": "*",
"Condition": {
"Null": {
"aws:RequestTag/finops:deletion-reason": "true"
}
}
}
]
}{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "BusinessHoursCleanupOnly",
"Effect": "Deny",
"Action": [
"ec2:DeleteVolume",
"ec2:TerminateInstances",
"rds:DeleteDBInstance"
],
"Resource": "*",
"Condition": {
"DateGreaterThan": {
"aws:CurrentTime": "18:00Z"
},
"DateLessThan": {
"aws:CurrentTime": "08:00Z"
}
}
}
]
}{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "CrossAccountAssumeRole",
"Effect": "Allow",
"Action": ["sts:AssumeRole"],
"Resource": [
"arn:aws:iam::ACCOUNT-1:role/FinOpsScanner",
"arn:aws:iam::ACCOUNT-2:role/FinOpsScanner",
"arn:aws:iam::ACCOUNT-3:role/FinOpsScanner"
]
}
]
}{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": {
"AWS": "arn:aws:iam::CENTRAL-ACCOUNT:role/FinOpsCentralScanner"
},
"Action": "sts:AssumeRole",
"Condition": {
"StringEquals": {
"sts:ExternalId": "finops-scanner-12345"
}
}
}
]
}{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "AggressiveDevCleanup",
"Effect": "Allow",
"Action": [
"ec2:DeleteVolume",
"ec2:TerminateInstances",
"rds:DeleteDBInstance"
],
"Resource": "*",
"Condition": {
"StringEquals": {
"aws:ResourceTag/Environment": [
"dev",
"development",
"test",
"staging"
]
}
}
}
]
}{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "ProductionSafeguards",
"Effect": "Deny",
"Action": [
"ec2:DeleteVolume",
"ec2:TerminateInstances",
"rds:DeleteDBInstance"
],
"Resource": "*",
"Condition": {
"StringEquals": {
"aws:ResourceTag/Environment": ["prod", "production", "live"]
},
"Bool": {
"aws:MultiFactorAuthPresent": "false"
}
}
}
]
}{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "KMSKeyAccess",
"Effect": "Allow",
"Action": [
"kms:Describe*",
"kms:List*",
"kms:Decrypt",
"kms:CreateGrant"
],
"Resource": ["arn:aws:kms:*:*:key/*"],
"Condition": {
"StringEquals": {
"kms:ViaService": [
"ec2.*.amazonaws.com",
"rds.*.amazonaws.com",
"dynamodb.*.amazonaws.com"
]
}
}
}
]
}{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "VPCEndpointAccess",
"Effect": "Allow",
"Principal": {
"AWS": "arn:aws:iam::ACCOUNT:role/FinOpsLambdaRole"
},
"Action": ["dynamodb:*", "sns:*"],
"Resource": "*",
"Condition": {
"StringEquals": {
"aws:SourceVpce": ["vpce-dynamodb-12345", "vpce-sns-67890"]
}
}
}
]
}# Lambda VPC Security Group
FinOpsLambdaSecurityGroup:
Type: AWS::EC2::SecurityGroup
Properties:
GroupDescription: Security group for FinOps Lambda functions
VpcId: !Ref VpcId
SecurityGroupEgress:
- IpProtocol: tcp
FromPort: 443
ToPort: 443
CidrIp: 0.0.0.0/0
Description: HTTPS outbound for AWS API calls
- IpProtocol: tcp
FromPort: 80
ToPort: 80
CidrIp: 0.0.0.0/0
Description: HTTP outbound for package downloads
Tags:
- Key: Name
Value: finops-lambda-sg{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "CloudTrailLogging",
"Effect": "Allow",
"Action": [
"logs:CreateLogGroup",
"logs:CreateLogStream",
"logs:PutLogEvents",
"logs:DescribeLogStreams"
],
"Resource": ["arn:aws:logs:*:*:log-group:/aws/lambda/finops-*"]
}
]
}{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "BillingAndCostAccess",
"Effect": "Allow",
"Action": [
"ce:GetCostAndUsage",
"ce:GetReservationCoverage",
"ce:GetReservationPurchaseRecommendation",
"ce:ListCostCategoryDefinitions",
"ce:GetReservationUtilization"
],
"Resource": "*"
}
]
}The system automatically creates service-linked roles for:
- Lambda execution: Basic Lambda execution permissions
- DynamoDB streams: Stream record processing
- EventBridge: Scheduled scan triggering
FinOpsServiceRole:
Type: AWS::IAM::Role
Properties:
RoleName: FinOpsServiceRole
AssumeRolePolicyDocument:
Version: "2012-10-17"
Statement:
- Effect: Allow
Principal:
Service:
- lambda.amazonaws.com
Action: sts:AssumeRole
ManagedPolicyArns:
- arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole
Policies:
- PolicyName: FinOpsCustomPolicy
PolicyDocument: !Ref FinOpsCustomPolicyDocument# Audit unused permissions quarterly
aws iam get-account-authorization-details \
--filter Role \
--max-items 50 | \
jq '.Roles[] | select(.RoleName | contains("FinOps"))'# Required tags for all FinOps resources
RequiredTags:
- finops:managed: "true"
- finops:environment: "prod|dev|staging"
- finops:owner: "team@company.com"{
"Condition": {
"Bool": {
"aws:MultiFactorAuthPresent": "true"
},
"NumericLessThan": {
"aws:MultiFactorAuthAge": "3600"
}
}
}# Lambda function logging template
import json
import logging
logger = logging.getLogger()
logger.setLevel(logging.INFO)
def lambda_handler(event, context):
logger.info({
'action': 'resource_cleanup',
'resources': event.get('resources', []),
'user': context.aws_request_id,
'timestamp': datetime.utcnow().isoformat()
})Next Reading: