Skip to content

Commit

Permalink
CORE-381/Add deletion of IAM Service Linked Roles (#415)
Browse files Browse the repository at this point in the history
* This allows IAM Service Linked roles to be deleted.

In the Gruntwork Reference Architecture, we need to clean up AWSServiceRoleForAutoScaling.

* Add documentation to README

This includes docs about IAM Roles, which was previously omitted.

* Verify the deletion via the deletion task id

* Remove extra limitations on IAM service linked role and add comment to IAM role

* Remove duplicate logging

* Make sure the role doesn't exist any more
  • Loading branch information
Pete Emerson authored Feb 28, 2023
1 parent 73be37d commit f9c04a9
Show file tree
Hide file tree
Showing 7 changed files with 397 additions and 3 deletions.
8 changes: 8 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ The currently supported functionality includes:
- Deleting VPCs in an AWS Account (along with any dependency resources such as ENIs, Egress Only Gateways, and Security Groups. except for default VPCs which is handled by the dedicated `defaults-aws` subcommand)
- Inspecting and deleting all IAM users in an AWS account
- Inspecting and deleting all IAM roles (and any associated EC2 instance profiles) in an AWS account
- Inspecting and deleting all IAM service-linked roles in an AWS account
- Inspecting and deleting all IAM groups in an AWS account
- Inspecting and deleting all IAM policies in an AWS account
- Inspecting and deleting all customer managed IAM policies in an AWS account
Expand Down Expand Up @@ -315,6 +316,12 @@ The following resources support the Config file:
- IAM Users
- Resource type: `iam`
- Config key: `IAMUsers`
- IAM Roles
- Resource type: `iam-role`
- Config key: `IAMRoles`
- IAM Service-Linked Roles
- Resource type: `iam-service-linked-role`
- Config key: `IAMServiceLinkedRoles`
- Secrets Manager Secrets
- Resource type: `secretsmanager`
- Config key: `SecretsManager`
Expand Down Expand Up @@ -530,6 +537,7 @@ To find out what we options are supported in the config file today, consult this
| efs | none | ✅ | none | none |
| acmpca | none | none | none | none |
| iam role | none | ✅ | none | none |
| iam service-linked role | none | ✅ | none | none |
| iam policy | none | ✅ | none | none |
| sagemaker-notebook-instances | none | ✅ | none | none |
| ecr | none | ✅ | none | none |
Expand Down
20 changes: 20 additions & 0 deletions aws/aws.go
Original file line number Diff line number Diff line change
Expand Up @@ -1263,6 +1263,25 @@ func GetAllResources(targetRegions []string, excludeAfter time.Time, resourceTyp
}
// End IAM Roles

// IAM Service Linked Roles
iamServiceLinkedRoles := IAMServiceLinkedRoles{}
if IsNukeable(iamServiceLinkedRoles.ResourceName(), resourceTypes) {
roleNames, err := getAllIamServiceLinkedRoles(session, excludeAfter, configObj)
if err != nil {
ge := report.GeneralError{
Error: err,
Description: "Unable to retrieve IAM roles",
ResourceType: iamServiceLinkedRoles.ResourceName(),
}
report.RecordError(ge)
}
if len(roleNames) > 0 {
iamServiceLinkedRoles.RoleNames = awsgo.StringValueSlice(roleNames)
globalResources.Resources = append(globalResources.Resources, iamServiceLinkedRoles)
}
}
// End IAM Service Linked Roles

if len(globalResources.Resources) > 0 {
account.Resources[GlobalRegion] = globalResources
}
Expand Down Expand Up @@ -1299,6 +1318,7 @@ func ListResourceTypes() []string {
IAMRoles{}.ResourceName(),
IAMGroups{}.ResourceName(),
IAMPolicies{}.ResourceName(),
IAMServiceLinkedRoles{}.ResourceName(),
SecretsManagerSecrets{}.ResourceName(),
NatGateways{}.ResourceName(),
OpenSearchDomains{}.ResourceName(),
Expand Down
8 changes: 5 additions & 3 deletions aws/iam_role.go
Original file line number Diff line number Diff line change
Expand Up @@ -182,14 +182,16 @@ func shouldIncludeIAMRole(iamRole *iam.Role, excludeAfter time.Time, configObj c
return false
}

// The OrganizationAccountAccessRole is a special role that is created by AWS Organizations, and is used to allow
// users to access the AWS account. We should not delete this role, so we can filter it out of the Roles found and
// managed by cloud-nuke.
if strings.Contains(aws.StringValue(iamRole.RoleName), "OrganizationAccountAccessRole") {
return false
}

// The arns of AWS-managed IAM roles, which can only be modified or deleted by AWS, contain "aws-service-role", so we can filter them out
// The ARNs of AWS-reserved IAM roles, which can only be modified or deleted by AWS, contain "aws-reserved", so we can filter them out
// of the Roles found and managed by cloud-nuke
// The same general rule applies with roles whose arn contains "aws-reserved"
if strings.Contains(aws.StringValue(iamRole.Arn), "aws-service-role") || strings.Contains(aws.StringValue(iamRole.Arn), "aws-reserved") {
if strings.Contains(aws.StringValue(iamRole.Arn), "aws-reserved") {
return false
}

Expand Down
190 changes: 190 additions & 0 deletions aws/iam_service_linked_role.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,190 @@
package aws

import (
"errors"
"fmt"
"strings"
"sync"
"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/cloud-nuke/report"
gruntworkerrors "github.com/gruntwork-io/go-commons/errors"
"github.com/hashicorp/go-multierror"
)

// List all IAM Roles in the AWS account
func getAllIamServiceLinkedRoles(session *session.Session, excludeAfter time.Time, configObj config.Config) ([]*string, error) {
svc := iam.New(session)

allIAMServiceLinkedRoles := []*string{}
err := svc.ListRolesPages(
&iam.ListRolesInput{},
func(page *iam.ListRolesOutput, lastPage bool) bool {
for _, iamServiceLinkedRole := range page.Roles {
if shouldIncludeIAMServiceLinkedRole(iamServiceLinkedRole, excludeAfter, configObj) {
allIAMServiceLinkedRoles = append(allIAMServiceLinkedRoles, iamServiceLinkedRole.RoleName)
}
}
return !lastPage
},
)
if err != nil {
return nil, gruntworkerrors.WithStackTrace(err)
}
return allIAMServiceLinkedRoles, nil
}

func deleteIamServiceLinkedRole(svc *iam.IAM, roleName *string) error {
// Deletion ID looks like this: "
//{
// DeletionTaskId: "task/aws-service-role/autoscaling.amazonaws.com/AWSServiceRoleForAutoScaling_2/d3c4c9fc-7fd3-4a36-974a-afb0eb78f102"
//}
deletionData, err := svc.DeleteServiceLinkedRole(&iam.DeleteServiceLinkedRoleInput{
RoleName: roleName,
})
if err != nil {
return gruntworkerrors.WithStackTrace(err)
}

// Wait for the deletion to complete
time.Sleep(3 * time.Second)

var deletionStatus *iam.GetServiceLinkedRoleDeletionStatusOutput

done := false
for !done {
done = true
// Check if the deletion is complete
deletionStatus, err = svc.GetServiceLinkedRoleDeletionStatus(&iam.GetServiceLinkedRoleDeletionStatusInput{
DeletionTaskId: deletionData.DeletionTaskId,
})
if err != nil {
return gruntworkerrors.WithStackTrace(err)
}
if aws.StringValue(deletionStatus.Status) == "IN_PROGRESS" {
logging.Logger.Debugf("Deletion of IAM ServiceLinked Role %s is still in progress", aws.StringValue(roleName))
done = false
time.Sleep(3 * time.Second)
}

}

if aws.StringValue(deletionStatus.Status) != "SUCCEEDED" {
err := fmt.Sprintf("Deletion of IAM ServiceLinked Role %s failed with status %s", aws.StringValue(roleName), aws.StringValue(deletionStatus.Status))
return gruntworkerrors.WithStackTrace(errors.New(err))
}

return nil
}

// Delete all IAM Roles
func nukeAllIamServiceLinkedRoles(session *session.Session, roleNames []*string) error {
region := aws.StringValue(session.Config.Region)
svc := iam.New(session)

if len(roleNames) == 0 {
logging.Logger.Debug("No IAM Service Linked Roles to nuke")
return nil
}

// NOTE: we don't need to do pagination here, because the pagination is handled by the caller to this function,
// based on IAMRoles.MaxBatchSize, however we add a guard here to warn users when the batching fails and has a
// chance of throttling AWS. Since we concurrently make one call for each identifier, we pick 100 for the limit here
// because many APIs in AWS have a limit of 100 requests per second.
if len(roleNames) > 100 {
logging.Logger.Debugf("Nuking too many IAM Service Linked Roles at once (100): halting to avoid hitting AWS API rate limiting")
return TooManyIamRoleErr{}
}

// There is no bulk delete IAM Roles API, so we delete the batch of IAM roles concurrently using go routines
logging.Logger.Debugf("Deleting all IAM Service Linked Roles in region %s", region)
wg := new(sync.WaitGroup)
wg.Add(len(roleNames))
errChans := make([]chan error, len(roleNames))
for i, roleName := range roleNames {
errChans[i] = make(chan error, 1)
go deleteIamServiceLinkedRoleAsync(wg, errChans[i], svc, roleName)
}
wg.Wait()

// Collect all the errors from the async delete calls into a single error struct.
var allErrs *multierror.Error
for _, errChan := range errChans {
if err := <-errChan; err != nil {
allErrs = multierror.Append(allErrs, err)
logging.Logger.Debugf("[Failed] %s", err)
}
}
finalErr := allErrs.ErrorOrNil()
if finalErr != nil {
return gruntworkerrors.WithStackTrace(finalErr)
}

for _, roleName := range roleNames {
logging.Logger.Debugf("[OK] IAM Service Linked Role %s was deleted in %s", aws.StringValue(roleName), region)
}
return nil
}

func shouldIncludeIAMServiceLinkedRole(iamServiceLinkedRole *iam.Role, excludeAfter time.Time, configObj config.Config) bool {
if iamServiceLinkedRole == nil {
return false
}

if !strings.Contains(aws.StringValue(iamServiceLinkedRole.Arn), "aws-service-role") {
return false
}

if excludeAfter.Before(*iamServiceLinkedRole.CreateDate) {
return false
}

return config.ShouldInclude(
aws.StringValue(iamServiceLinkedRole.RoleName),
configObj.IAMServiceLinkedRoles.IncludeRule.NamesRegExp,
configObj.IAMServiceLinkedRoles.ExcludeRule.NamesRegExp,
)
}

func deleteIamServiceLinkedRoleAsync(wg *sync.WaitGroup, errChan chan error, svc *iam.IAM, roleName *string) {
defer wg.Done()

var result *multierror.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{
deleteIamServiceLinkedRole,
}

for _, fn := range functions {
if err := fn(svc, roleName); err != nil {
result = multierror.Append(result, err)
}
}

// Record status of this resource
e := report.Entry{
Identifier: aws.StringValue(roleName),
ResourceType: "IAM Service Linked Role",
Error: result.ErrorOrNil(),
}
report.Record(e)

errChan <- result.ErrorOrNil()
}

// Custom errors

type TooManyIamServiceLinkedRoleErr struct{}

func (err TooManyIamServiceLinkedRoleErr) Error() string {
return "Too many IAM Service Linked Roles requested at once"
}
Loading

0 comments on commit f9c04a9

Please sign in to comment.