Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

support nuking iam-roles #251

Closed
wants to merge 2 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
.idea
vendor
.vscode
cloud-nuke
cloud-nuke
config.yaml
.envrc
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ The currently supported functionality includes:
- Deleting all default VPCs in an AWS account
- Deleting VPCs in an AWS Account (except for default VPCs which is handled by the dedicated `defaults-aws` subcommand)
- Deleting all IAM users in an AWS account
- Deleting all IAM roles in an AWS account (except aws defined roles and OrganizationAccountAccessRole)
- Deleting all Secrets Manager Secrets in an AWS account
- Deleting all NAT Gateways in an AWS account
- Deleting all IAM Access Analyzers in an AWS account
Expand Down
16 changes: 16 additions & 0 deletions aws/aws.go
Original file line number Diff line number Diff line change
Expand Up @@ -695,6 +695,21 @@ func GetAllResources(targetRegions []string, excludeAfter time.Time, resourceTyp
}
// End IAM Users

// IAM Roles
iamRoles := IAMRoles{}
if IsNukeable(iamRoles.ResourceName(), resourceTypes) {
roleNames, err := getAllIamRoles(session, excludeAfter, configObj)

if err != nil {
return nil, errors.WithStackTrace(err)
}
if len(roleNames) > 0 {
iamRoles.RoleNames = awsgo.StringValueSlice(roleNames)
globalResources.Resources = append(globalResources.Resources, iamRoles)
}
}
// End IAM Roles

if len(globalResources.Resources) > 0 {
account.Resources[GlobalRegion] = globalResources
}
Expand Down Expand Up @@ -727,6 +742,7 @@ func ListResourceTypes() []string {
LambdaFunctions{}.ResourceName(),
S3Buckets{}.ResourceName(),
IAMUsers{}.ResourceName(),
IAMRoles{}.ResourceName(),
SecretsManagerSecrets{}.ResourceName(),
NatGateways{}.ResourceName(),
OpenSearchDomains{}.ResourceName(),
Expand Down
176 changes: 176 additions & 0 deletions aws/iam_role.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,176 @@
package aws

import (
"strings"
"time"

"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/aws/session"
"github.com/aws/aws-sdk-go/service/iam"
"github.com/gruntwork-io/cloud-nuke/config"
"github.com/gruntwork-io/cloud-nuke/logging"
"github.com/gruntwork-io/go-commons/errors"
"github.com/hashicorp/go-multierror"
)

// List all IAM users in the AWS account and returns a slice of the UserNames
func getAllIamRoles(session *session.Session, excludeAfter time.Time, configObj config.Config) ([]*string, error) {
svc := iam.New(session)

var roleNames []*string

// TODO: Probably use ListRoles together with ListRolesPages in case there are lots of roles
output, err := svc.ListRoles(&iam.ListRolesInput{})
if err != nil {
return nil, errors.WithStackTrace(err)
}

for _, role := range output.Roles {
if strings.Contains(aws.StringValue(role.RoleName), "OrganizationAccountAccessRole") {
continue
}
if strings.Contains(aws.StringValue(role.Arn), "aws-service-role") || strings.Contains(aws.StringValue(role.Arn), "aws-reserved") {
continue
}

if config.ShouldInclude(aws.StringValue(role.RoleName), configObj.IAMRoles.IncludeRule.NamesRegExp, configObj.IAMRoles.ExcludeRule.NamesRegExp) && excludeAfter.After(*role.CreateDate) {
roleNames = append(roleNames, role.RoleName)
}
}

return roleNames, nil
}

func detachRolePolicies(svc *iam.IAM, roleName *string) error {
policiesOutput, err := svc.ListAttachedRolePolicies(&iam.ListAttachedRolePoliciesInput{
RoleName: roleName,
})
if err != nil {
return errors.WithStackTrace(err)
}

for _, attachedPolicy := range policiesOutput.AttachedPolicies {
arn := attachedPolicy.PolicyArn
_, err = svc.DetachRolePolicy(&iam.DetachRolePolicyInput{
PolicyArn: arn,
RoleName: roleName,
})
if err != nil {
logging.Logger.Errorf("[Failed] %s", err)
return errors.WithStackTrace(err)
}
logging.Logger.Infof("Detached Policy %s from Role %s", aws.StringValue(arn), aws.StringValue(roleName))
}

return nil
}

func deleteInlineRolePolicies(svc *iam.IAM, roleName *string) error {
policyOutput, err := svc.ListRolePolicies(&iam.ListRolePoliciesInput{
RoleName: roleName,
})
if err != nil {
logging.Logger.Errorf("[Failed] %s", err)
return errors.WithStackTrace(err)
}

for _, policyName := range policyOutput.PolicyNames {
_, err := svc.DeleteRolePolicy(&iam.DeleteRolePolicyInput{
PolicyName: policyName,
RoleName: roleName,
})
if err != nil {
logging.Logger.Errorf("[Failed] %s", err)
return errors.WithStackTrace(err)
}
logging.Logger.Infof("Deleted Inline Policy %s from Role %s", aws.StringValue(policyName), aws.StringValue(roleName))
}

return nil
}

func removeRoleFromInstanceProfiles(svc *iam.IAM, roleName *string) error {
resp, err := svc.ListInstanceProfilesForRole(&iam.ListInstanceProfilesForRoleInput{
RoleName: roleName,
})
if err != nil {
logging.Logger.Errorf("[Failed] %s", err)
return errors.WithStackTrace(err)
}

for _, profile := range resp.InstanceProfiles {
_, err := svc.RemoveRoleFromInstanceProfile(&iam.RemoveRoleFromInstanceProfileInput{
RoleName: roleName,
InstanceProfileName: profile.InstanceProfileName,
})
if err != nil {
logging.Logger.Errorf("[Failed] %s", err)
return errors.WithStackTrace(err)
}

logging.Logger.Infof("Removed Role %s from Instance Profile %s", aws.StringValue(roleName), aws.StringValue(profile.InstanceProfileName))
}

return nil
}

func deleteIamRole(svc *iam.IAM, roleName *string) error {
_, err := svc.DeleteRole(&iam.DeleteRoleInput{
RoleName: roleName,
})
if err != nil {
return errors.WithStackTrace(err)
}

return nil
}

// Nuke a single user
func nukeRole(svc *iam.IAM, roleName *string) error {
// Functions used to really nuke an IAM Role as a role can have many attached
// items we need delete/detach them before actually deleting it.
// NOTE: The actual role deletion should always be the last one. This way we
// can guarantee that it will fail if we forgot to delete/detach an item.
functions := []func(svc *iam.IAM, roleName *string) error{
detachRolePolicies,
deleteInlineRolePolicies,
removeRoleFromInstanceProfiles,
deleteIamRole,
}

for _, fn := range functions {
if err := fn(svc, roleName); err != nil {
return err
}
}

return nil
}

// Delete all IAM Roles
func nukeAllIamRoles(session *session.Session, roleNames []*string) error {
if len(roleNames) == 0 {
logging.Logger.Info("No IAM Users to nuke")
return nil
}

logging.Logger.Info("Deleting all IAM Users")

deletedUsers := 0
svc := iam.New(session)
multiErr := new(multierror.Error)

for _, roleName := range roleNames {
err := nukeRole(svc, roleName)
if err != nil {
logging.Logger.Errorf("[Failed] %s", err)
multierror.Append(multiErr, err)
} else {
deletedUsers++
logging.Logger.Infof("Deleted IAM Role: %s", *roleName)
}
}

logging.Logger.Infof("[OK] %d IAM Roles(s) terminated", deletedUsers)
return multiErr.ErrorOrNil()
}
134 changes: 134 additions & 0 deletions aws/iam_role_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
package aws

import (
"testing"
"time"

"github.com/aws/aws-sdk-go/aws"
awsgo "github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/aws/session"
"github.com/aws/aws-sdk-go/service/iam"
"github.com/gruntwork-io/cloud-nuke/config"
"github.com/gruntwork-io/cloud-nuke/util"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)

func TestListIamRoles(t *testing.T) {
t.Parallel()

region, err := getRandomRegion()
require.NoError(t, err)

session, err := session.NewSession(&awsgo.Config{
Region: awsgo.String(region)},
)
require.NoError(t, err)

roleNames, err := getAllIamRoles(session, time.Now(), config.Config{})
require.NoError(t, err)

assert.NotEmpty(t, roleNames)
}

func createTestRole(t *testing.T, session *session.Session, name string) error {
svc := iam.New(session)

input := &iam.CreateRoleInput{
RoleName: aws.String(name),
AssumeRolePolicyDocument: aws.String(`{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": {
"Service": "ec2.amazonaws.com"
},
"Action": "sts:AssumeRole"
}
]
}`),
}

_, err := svc.CreateRole(input)
require.NoError(t, err)

return nil
}

func TestCreateIamRole(t *testing.T) {
t.Parallel()

region, err := getRandomRegion()
require.NoError(t, err)

session, err := session.NewSession(&awsgo.Config{
Region: awsgo.String(region)},
)
require.NoError(t, err)

name := "cloud-nuke-test-" + util.UniqueID()
roleNames, err := getAllIamRoles(session, time.Now(), config.Config{})
require.NoError(t, err)
assert.NotContains(t, awsgo.StringValueSlice(roleNames), name)

err = createTestRole(t, session, name)
defer nukeAllIamRoles(session, []*string{&name})
require.NoError(t, err)

roleNames, err = getAllIamRoles(session, time.Now(), config.Config{})
require.NoError(t, err)
assert.Contains(t, awsgo.StringValueSlice(roleNames), name)
}

func TestNukeIamRoles(t *testing.T) {
t.Parallel()

region, err := getRandomRegion()
require.NoError(t, err)

session, err := session.NewSession(&awsgo.Config{
Region: awsgo.String(region)},
)
require.NoError(t, err)

name := "cloud-nuke-test-" + util.UniqueID()
err = createTestRole(t, session, name)
require.NoError(t, err)

err = nukeAllIamRoles(session, []*string{&name})
require.NoError(t, err)
}

func TestTimeFilterExclusionNewlyCreatedIamRole(t *testing.T) {
t.Parallel()

region, err := getRandomRegion()
require.NoError(t, err)

session, err := session.NewSession(&awsgo.Config{
Region: awsgo.String(region)},
)
require.NoError(t, err)

// Assert role didn't exist
name := "cloud-nuke-test-" + util.UniqueID()
roleNames, err := getAllIamRoles(session, time.Now(), config.Config{})
require.NoError(t, err)
assert.NotContains(t, awsgo.StringValueSlice(roleNames), name)

// Creates a role
err = createTestRole(t, session, name)
defer nukeAllIamRoles(session, []*string{&name})

// Assert role is created
roleNames, err = getAllIamRoles(session, time.Now(), config.Config{})
require.NoError(t, err)
assert.Contains(t, awsgo.StringValueSlice(roleNames), name)

// Assert role doesn't appear when we look at roles older than 1 Hour
olderThan := time.Now().Add(-1 * time.Hour)
roleNames, err = getAllIamRoles(session, olderThan, config.Config{})
require.NoError(t, err)
assert.NotContains(t, awsgo.StringValueSlice(roleNames), name)
}
36 changes: 36 additions & 0 deletions aws/iam_role_types.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
package aws

import (
awsgo "github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/aws/session"
"github.com/gruntwork-io/go-commons/errors"
)

// IAMRoles - represents all IAMRoles on the AWS Account
type IAMRoles struct {
RoleNames []string
}

// ResourceName - the simple name of the aws resource
func (r IAMRoles) ResourceName() string {
return "iam-role"
}

// ResourceIdentifiers - The IAM UserNames
func (r IAMRoles) ResourceIdentifiers() []string {
return r.RoleNames
}

// Tentative batch size to ensure AWS doesn't throttle
func (r IAMRoles) MaxBatchSize() int {
return 200
}

// Nuke - nuke 'em all!!!
func (r IAMRoles) Nuke(session *session.Session, identifiers []string) error {
if err := nukeAllIamRoles(session, awsgo.StringSlice(identifiers)); err != nil {
return errors.WithStackTrace(err)
}

return nil
}
Loading