From 495c502e6965eeaaa70574c6fa34d458fc1ce0d7 Mon Sep 17 00:00:00 2001 From: Nirdosh Date: Fri, 20 Aug 2021 21:11:57 +0545 Subject: [PATCH] Assume role and target ac validation fix. --- README.md | 42 +++++++++++++++++------------------------ utils/cloudformation.go | 33 ++++++++++++++++++++------------ utils/s3.go | 37 ++++++++++++++++++++++-------------- 3 files changed, 61 insertions(+), 51 deletions(-) diff --git a/README.md b/README.md index 3bf8ad6..0bdb8fb 100644 --- a/README.md +++ b/README.md @@ -6,27 +6,25 @@ CFN Teardown is a tool to delete matching CloudFormation stacks respecting stack - Stack name pattern matching for deletion. -- Generates stack dependencies in a file from which shows how loosely or tighly coupled the stacks are. +- Generates stack dependencies in a file from which shows how loosely or tighly coupled the stacks are. - Builds dependency tree for intelligent/faster teardown. - Multiple safety checks to prevent accidental deletion. - Supports slack notification for deletion status updates via webhook. - --- ### Install - +Run downloader script: ```bash - -go get github.com/nirdosh17/cfn-teardown - +curl -f https://raw.githubusercontent.com/nirdosh17/cfn-teardown/master/download.sh | sh ``` +OR -**OR** download binary from [HERE](https://github.com/nirdosh17/cfn-teardown/releases) +Download binary manually from [HERE](https://github.com/nirdosh17/cfn-teardown/releases). ### Usage @@ -86,19 +84,14 @@ Configuration for this command can be set in three different ways in the precede ``` -See Available configurations via: +See available configurations via: `cfn-teardown --help` -```bash -cfn-teardown --help -cfn-teardown listDependencies --help -cfn-teardown deleteStacks --help -``` -### Algorithm +### Stack Teardown Strategy -1. Finds matching stacks based on the regex provided +1. Find matching stacks based on the regex provided -2. Prepares stack dependencies +2. Prepare stack dependencies
It looks something like this: @@ -139,31 +132,30 @@ cfn-teardown deleteStacks --help ```
-3. Alerts slack channel(if provided) and waits before initiating deletion. Starts deletion immediately if no wait time is provided. +3. Alert slack channel(if provided) and waits before initiating deletion. Starts deletion immediately if no wait time is provided. -4. Selects stacks which are eligible for deletion. A stack is eligible for deletion if it's exports are imported by no other stacks. In simple terms, it should have no dependencies. +4. Select stacks which are eligible for deletion. A stack is eligible for deletion if it's exports are imported by no other stacks. In simple terms, it should have no dependencies. -5. Initiates delete requests concurrently for all selected stacks. +5. Send delete requests for all selected stacks. + +6. Wait for 30 seconds(configurable) before scanning eligible stacks again. Checks If the stack has been already deleted and if deleted updates stack status in the dependency tree. -6. Waits for 30 seconds(configurable) before scanning eligible stacks again. Checks If the stack has been already deleted and if deleted updates stack status in the dependency tree. - 7. This process (sending delete requests, waiting, checking stack status) is repeated until all stacks have status `DELETE_COMPLETE`. - + 8. If a stack is not deleted even after exhausting all retries(default 5), teardown is halted and manual intervention is requested. - + ### Assume Role By default it tries to use the IAM role of environment it is currently running in. But we can also supply role arn if we want the script to assume a different role. - ### Safety Checks for Accidental Deletion - `DRY_RUN` flag must be explicitely set to `false` to activate delete functionality - `ABORT_WAIT_TIME_MINUTES` flag lets us to decide how much to wait before initiating delete as you might want to confirm the stacks that are about to get deleted -- `TARGET_ACCOUNT_ID` flag will check the supplied account id with aws session account id during runtime to confirm that we are deleting stacks in the desired aws account +- `TARGET_ACCOUNT_ID`: If provided, this flag confirms that the given aws account id matches with account id in the aws session during runtime to make sure that we are deleting stacks in the desired aws account ### Limitation diff --git a/utils/cloudformation.go b/utils/cloudformation.go index 97e685b..cb65d2d 100644 --- a/utils/cloudformation.go +++ b/utils/cloudformation.go @@ -245,13 +245,25 @@ func (dm CFNManager) Session() (*cloudformation.CloudFormation, error) { Profile: dm.AWSProfile, })) - desiredAccount, err := dm.IsDesiredAWSAccount(sess) - if err != nil { - return nil, err + // validation for target account id + if dm.TargetAccountId != "" { + aID, err := dm.AWSSessionAccountID(sess) + if err != nil { + fmt.Printf("Error requesting AWS caller identity: %v", err.Error()) + return nil, err + } + + if aID != dm.TargetAccountId { + return nil, fmt.Errorf( + "[CFN] Target account id (%v) did not match with account id (%v) in the current AWS session", + dm.TargetAccountId, + aID, + ) + } } - // to make things easy while running this script locally - if desiredAccount { + if dm.NukeRoleARN == "" { + // this means, we are using given aws profile return cloudformation.New(sess), nil } else { // Create the credentials from AssumeRoleProvider if nuke role arn is provided @@ -261,16 +273,13 @@ func (dm CFNManager) Session() (*cloudformation.CloudFormation, error) { } } -func (dm CFNManager) IsDesiredAWSAccount(sess *session.Session) (bool, error) { +func (dm CFNManager) AWSSessionAccountID(sess *session.Session) (acID string, err error) { svc := sts.New(sess) result, err := svc.GetCallerIdentity(&sts.GetCallerIdentityInput{}) if err != nil { fmt.Printf("Error requesting AWS caller identity: %v", err.Error()) - return false, err - } - - if *result.Account == dm.TargetAccountId { - return true, err + return } - return false, err + acID = *result.Account + return } diff --git a/utils/s3.go b/utils/s3.go index fba89d7..ac469cf 100644 --- a/utils/s3.go +++ b/utils/s3.go @@ -74,32 +74,41 @@ func (sm S3Manager) Session() (*s3.S3, error) { Profile: sm.AWSProfile, })) - desiredAccount, err := sm.IsDesiredAWSAccount(sess) - if err != nil { - return nil, err + // validation for target account id + if sm.TargetAccountId != "" { + aID, err := sm.AWSSessionAccountID(sess) + if err != nil { + fmt.Printf("Error requesting AWS caller identity: %v", err.Error()) + return nil, err + } + + if aID != sm.TargetAccountId { + return nil, fmt.Errorf( + "[S3] Target account id (%v) did not match with account id (%v) in the current AWS session", + sm.TargetAccountId, + aID, + ) + } } - // to make things easy while running this script locally - if desiredAccount { - return s3.New(sess), err + if sm.NukeRoleARN == "" { + // this means, we are using given aws profile + return s3.New(sess), nil } else { // Create the credentials from AssumeRoleProvider if nuke role arn is provided creds := stscreds.NewCredentials(sess, sm.NukeRoleARN) // Create service client value configured for credentials from assumed role - return s3.New(sess, &aws.Config{Credentials: creds, MaxRetries: &AWS_SDK_MAX_RETRY}), err + return s3.New(sess, &aws.Config{Credentials: creds, MaxRetries: &AWS_SDK_MAX_RETRY}), nil } } -func (sm S3Manager) IsDesiredAWSAccount(sess *session.Session) (bool, error) { +func (sm S3Manager) AWSSessionAccountID(sess *session.Session) (acID string, err error) { svc := sts.New(sess) result, err := svc.GetCallerIdentity(&sts.GetCallerIdentityInput{}) if err != nil { fmt.Printf("Error requesting AWS caller identity: %v", err.Error()) - return false, err - } - - if *result.Account == sm.TargetAccountId { - return true, err + return } - return false, err + acID = *result.Account + return }