-
-
Save a-h/71c9cd7f35165b0ff07545c49ef6a65e to your computer and use it in GitHub Desktop.
Go CDK CI User and Permissions Boundary
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
package main | |
import ( | |
"fmt" | |
"os" | |
"github.com/aws/aws-cdk-go/awscdk" | |
"github.com/aws/aws-cdk-go/awscdk/awsiam" | |
"github.com/aws/constructs-go/constructs/v3" | |
"github.com/aws/jsii-runtime-go" | |
) | |
type CIStackProps struct { | |
awscdk.StackProps | |
ApplicationName string | |
} | |
func NewCIStack(scope constructs.Construct, id string, props *CIStackProps) awscdk.Stack { | |
var sprops awscdk.StackProps | |
if props != nil { | |
sprops = props.StackProps | |
} | |
stack := awscdk.NewStack(scope, &id, &sprops) | |
pb := addPermissionsBoundary(stack, props) | |
user := addCIUser(stack, props) | |
pb.AttachToUser(user) | |
return stack | |
} | |
func addCIUser(stack constructs.Construct, props *CIStackProps) (user awsiam.User) { | |
// Create a role creator role. | |
roleCreatorPolicy := awsiam.NewManagedPolicy(stack, jsii.String("CIRoleCreator"), &awsiam.ManagedPolicyProps{ | |
ManagedPolicyName: jsii.String("CIRoleCreator"), | |
Description: jsii.String("Allows CI users to create roles for Lambda functions, should be used with the permission boundary."), | |
}) | |
roleCreatorPolicy.AddStatements(awsiam.NewPolicyStatement(&awsiam.PolicyStatementProps{ | |
Sid: jsii.String("passRole"), | |
Effect: awsiam.Effect_ALLOW, | |
Actions: jsii.Strings( | |
"iam:PassRole*", | |
"iam:GetRole", | |
"iam:CreateRole", | |
"iam:PutRolePolicy", | |
"iam:DeleteRolePolicy", | |
"iam:DeleteRole", | |
"iam:List*", | |
"iam:SimulatePrincipalPolicy", | |
"iam:AttachRolePolicy", | |
"iam:DetachRolePolicy", | |
"iam:PutRolePermissionsBoundary", | |
), | |
Resources: jsii.Strings("*"), | |
})) | |
// Create a group for CI users. | |
group := awsiam.NewGroup(stack, jsii.String("ci-users"), &awsiam.GroupProps{ | |
GroupName: jsii.String("ci-users"), | |
ManagedPolicies: &[]awsiam.IManagedPolicy{ | |
awsiam.ManagedPolicy_FromAwsManagedPolicyName(jsii.String("PowerUserAccess")), | |
roleCreatorPolicy, | |
}, | |
}) | |
// Create ci-user. | |
user = awsiam.NewUser(stack, jsii.String("ci-user"), &awsiam.UserProps{ | |
UserName: jsii.String("ci-user"), | |
Groups: &[]awsiam.IGroup{ | |
group, | |
}, | |
}) | |
// Give them an access key, and print it out. | |
accessKey := awsiam.NewCfnAccessKey(user, jsii.String("ci-user-accesskey"), &awsiam.CfnAccessKeyProps{ | |
UserName: user.UserName(), | |
}) | |
awscdk.NewCfnOutput(stack, jsii.String("accessKeyId"), &awscdk.CfnOutputProps{Value: accessKey.Ref()}) | |
awscdk.NewCfnOutput(stack, jsii.String("secretAccessKey"), &awscdk.CfnOutputProps{Value: accessKey.AttrSecretAccessKey()}) | |
return | |
} | |
func addPermissionsBoundary(stack constructs.Construct, props *CIStackProps) (pb awsiam.ManagedPolicy) { | |
resourceApplicationRoleWildcard := fmt.Sprintf("arn:aws:iam::%v:role/%s*", *props.Env.Account, props.ApplicationName) | |
// Create a permission boundary. | |
pb = awsiam.NewManagedPolicy(stack, jsii.String("PermissionsBoundary"), &awsiam.ManagedPolicyProps{ | |
ManagedPolicyName: jsii.String("ci-permissions-boundary"), | |
Description: jsii.String("Permission boundary to limit permissions of roles created by CI/CD user."), | |
}) | |
// Allow reading IAM information, and simulating policies. | |
pb.AddStatements(awsiam.NewPolicyStatement(&awsiam.PolicyStatementProps{ | |
Sid: jsii.String("AllowIAMReadOnly"), | |
Effect: awsiam.Effect_ALLOW, | |
Actions: jsii.Strings( | |
"iam:Get*", | |
"iam:List*", | |
"iam:SimulatePrincipalPolicy", | |
), | |
Resources: jsii.Strings("*"), | |
})) | |
// Allow services that need a wildcard resource ID because the resource path is unknown in advance e.g. API Gateway. | |
pb.AddStatements(awsiam.NewPolicyStatement(&awsiam.PolicyStatementProps{ | |
Sid: jsii.String("AllowServerlessServices"), | |
Effect: awsiam.Effect_ALLOW, | |
Actions: jsii.Strings( | |
"cognito-idp:*", | |
"dynamodb:*", | |
"ec2:CreateNetworkInterface", | |
"ec2:DeleteNetworkInterface", | |
"ec2:Describe*", | |
"events:*", | |
"kms:*", | |
"lambda:*", | |
"logs:*", | |
"s3:*", | |
"schemas:*", | |
"secretsmanager:GetSecretValue", // Only allow retrival of secrets. | |
"ses:*", // Unknown email at the moment | |
"sns:*", | |
"sqs:*", | |
"ecr:*", | |
"apprunner:*", | |
"ssm:*", // Alow SSM parameter store. | |
"states:*", | |
"synthetics:*", | |
"xray:*", | |
), | |
Resources: jsii.Strings("*"), | |
Conditions: conditionRestrictToRegions, | |
})) | |
// Allow CloudFormation deployment. | |
pb.AddStatements(awsiam.NewPolicyStatement(&awsiam.PolicyStatementProps{ | |
Sid: jsii.String("AllowCloudFormationDeployment"), | |
Effect: awsiam.Effect_ALLOW, | |
Actions: jsii.Strings( | |
"cloudformation:CreateStack", | |
"cloudformation:DescribeStackEvents", | |
"cloudformation:DescribeStackResources", | |
"cloudformation:DescribeStackResource", | |
"cloudformation:DescribeStacks", | |
"cloudformation:GetTemplate", | |
"cloudformation:ListStackResources", | |
"cloudformation:UpdateStack", | |
"cloudformation:ValidateTemplate", | |
"cloudformation:DeleteStack", | |
), | |
Resources: jsii.Strings("*"), | |
Conditions: conditionRestrictToRegions, | |
})) | |
// Allow validation of any stack. | |
pb.AddStatements(awsiam.NewPolicyStatement(&awsiam.PolicyStatementProps{ | |
Sid: jsii.String("AllowValidationOfAnyStack"), | |
Effect: awsiam.Effect_ALLOW, | |
Actions: jsii.Strings( | |
"cloudformation:ValidateTemplate", | |
), | |
Resources: jsii.Strings("*"), | |
Conditions: conditionRestrictToRegions, | |
})) | |
// Allow passing any roles that start with the application name to Lambda. | |
pb.AddStatements(awsiam.NewPolicyStatement(&awsiam.PolicyStatementProps{ | |
Sid: jsii.String("AllowPassRoleToLambda"), | |
Effect: awsiam.Effect_ALLOW, | |
Actions: jsii.Strings( | |
"iam:PassRole", | |
), | |
Resources: jsii.Strings(resourceApplicationRoleWildcard), | |
Conditions: &map[string]interface{}{ | |
"StringEquals": &map[string]interface{}{ | |
"iam:PassedToService": jsii.String("lambda.amazonaws.com"), | |
}, | |
}, | |
})) | |
// Deny permissions boundary alteration. | |
arn := awscdk.Fn_Sub(jsii.String("arn:aws:iam::${AWS::AccountId}:policy/ci-permissions-boundary"), nil) | |
pb.AddStatements(awsiam.NewPolicyStatement(&awsiam.PolicyStatementProps{ | |
Sid: jsii.String("DenyPermissionsBoundaryAlteration"), | |
Effect: awsiam.Effect_DENY, | |
Actions: jsii.Strings( | |
"iam:CreatePolicyVersion", | |
"iam:DeletePolicy", | |
"iam:DeletePolicyVersion", | |
"iam:SetDefaultPolicyVersion", | |
), | |
Resources: &[]*string{arn}, | |
})) | |
// Deny removal of permissions boundary from any role. | |
pb.AddStatements(awsiam.NewPolicyStatement(&awsiam.PolicyStatementProps{ | |
Sid: jsii.String("DenyPermissionsBoundaryRemoval"), | |
Effect: awsiam.Effect_DENY, | |
Actions: jsii.Strings( | |
"iam:DeleteRolePermissionsBoundary", | |
), | |
Resources: jsii.Strings("arn:aws:iam:::role/*"), | |
})) | |
// Allow permissions boundaries to be applied. | |
pb.AddStatements(awsiam.NewPolicyStatement(&awsiam.PolicyStatementProps{ | |
Sid: jsii.String("AllowUpsertRoleIfPermBoundaryIsBeingApplied"), | |
Effect: awsiam.Effect_ALLOW, | |
Actions: jsii.Strings( | |
"iam:CreateRole", | |
"iam:PutRolePolicy", | |
"iam:PutRolePermissionsBoundary", | |
), | |
Resources: jsii.Strings("arn:aws:iam:::role/*", "arn:aws:iam:::policy/*"), | |
Conditions: &map[string]interface{}{ | |
"StringEquals": &map[string]interface{}{ | |
"iam:PermissionsBoundary": arn, | |
}, | |
}, | |
})) | |
// Allow roles to be deleted. | |
pb.AddStatements(awsiam.NewPolicyStatement(&awsiam.PolicyStatementProps{ | |
Sid: jsii.String("AllowDeleteRole"), | |
Effect: awsiam.Effect_ALLOW, | |
Actions: jsii.Strings( | |
"iam:DetachRolePolicy", | |
"iam:DeleteRolePolicy", | |
"iam:DeleteRole", | |
), | |
Resources: jsii.Strings("arn:aws:iam:::role/*"), | |
})) | |
return pb | |
} | |
var conditionRestrictToRegions = &map[string]interface{}{ | |
"StringEquals": &map[string]interface{}{ | |
"aws:RequestedRegion": jsii.Strings( | |
"us-east-1", // Allow North Virginia for CloudFront. | |
"eu-west-1", // Europe. | |
), | |
}, | |
} | |
func main() { | |
app := awscdk.NewApp(nil) | |
NewCIStack(app, "CIStack", &CIStackProps{ | |
awscdk.StackProps{ | |
Env: &awscdk.Environment{ | |
Account: awscdk.Aws_ACCOUNT_ID(), | |
Region: awscdk.Aws_REGION(), | |
}, | |
}, | |
"ci", | |
}) | |
app.Synth(nil) | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment