Skip to content

Commit

Permalink
Add acmpa ✨
Browse files Browse the repository at this point in the history
This adds support to delete AWS ACM Private CAs.

Fixes #200
  • Loading branch information
weitzjdevk committed Jul 14, 2021
1 parent 922a488 commit d9566af
Show file tree
Hide file tree
Showing 15 changed files with 364 additions and 0 deletions.
5 changes: 5 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ The currently supported functionality includes:

## AWS

- Deleting all ACM Private CA in an AWS account
- Deleting all Auto scaling groups in an AWS account
- Deleting all Elastic Load Balancers (Classic and V2) in an AWS account
- Deleting all Transit Gateways in an AWS account
Expand Down Expand Up @@ -152,6 +153,9 @@ The following resources support the Config file:
- NAT Gateways
- Resource type: `nat-gateway`
- Config key: `NATGateway`
- ACM Private CAs
- Resource type: `acmpca`
- Config key: `ACMPCA`


#### Example
Expand Down Expand Up @@ -243,6 +247,7 @@ To find out what we options are supported in the config file today, consult this
| iam | none || none | none |
| secretsmanager | none || none | none |
| nat-gateway | none || none | none |
| acmpca | none || none | none |
| ec2 instance | none | none | none | none |
| iam role | none | none | none | none |
| ... (more to come) | none | none | none | none |
Expand Down
73 changes: 73 additions & 0 deletions aws/acmpca.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
package aws

import (
"time"

"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/aws/session"
"github.com/aws/aws-sdk-go/service/acmpca"
"github.com/gruntwork-io/cloud-nuke/logging"
"github.com/gruntwork-io/go-commons/errors"
)

// getAllACMPCA returns a list of all arns of ACMPCA, which can be deleted.
func getAllACMPCA(session *session.Session, region string, excludeAfter time.Time) ([]*string, error) {
svc := acmpca.New(session)

result, err := svc.ListCertificateAuthorities(&acmpca.ListCertificateAuthoritiesInput{})
if err != nil {
return nil, errors.WithStackTrace(err)
}

var arns []*string
for _, ca := range result.CertificateAuthorities {
// one can only delete CAs if they are 'ACTIVE' or 'DISABLED'
isCandidateForDeletion := *ca.Status == acmpca.CertificateAuthorityStatusActive || *ca.Status == acmpca.CertificateAuthorityStatusDisabled
if !isCandidateForDeletion {
continue
}
if excludeAfter.After(*ca.CreatedAt) {
arns = append(arns, ca.Arn)
}
}
return arns, nil
}

// nukeAllACMPCA will delete all ACMPCA, which are given by a list of arns.
func nukeAllACMPCA(session *session.Session, arns []*string) error {
if len(arns) == 0 {
logging.Logger.Infof("No ACMPCA to nuke in region %s", *session.Config.Region)
return nil
}
svc := acmpca.New(session)

logging.Logger.Infof("Deleting all ACMPCA in region %s", *session.Config.Region)
var deletedARNs []*string

for _, arn := range arns {
logging.Logger.Infof("Setting status to 'DISABLED' for ACMPCA %s in region %s", *arn, *session.Config.Region)
if _, updateStatusErr := svc.UpdateCertificateAuthority(&acmpca.UpdateCertificateAuthorityInput{
CertificateAuthorityArn: arn,
Status: aws.String(acmpca.CertificateAuthorityStatusDisabled),
}); updateStatusErr != nil {
logging.Logger.Errorf("[Failed] %s", updateStatusErr)
continue
}
logging.Logger.Infof("Did set status to 'DISABLED' for ACMPCA: %s in region %s", *arn, *session.Config.Region)

if _, deleteErr := svc.DeleteCertificateAuthority(&acmpca.DeleteCertificateAuthorityInput{
CertificateAuthorityArn: arn,
// the range is 7 to 30 days.
// since cloud-nuke should not be used in production,
// we assume that the minimum (7 days) is fine.
PermanentDeletionTimeInDays: aws.Int64(7),
}); deleteErr != nil {
logging.Logger.Errorf("[Failed] %s", deleteErr)
continue
}
deletedARNs = append(deletedARNs, arn)
logging.Logger.Infof("Deleted ACMPCA: %s", *arn)
}
logging.Logger.Infof("[OK] %d ACMPCA(s) deleted in %s", len(arns), *session.Config.Region)
return nil
}
103 changes: 103 additions & 0 deletions aws/acmpca_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
package aws

import (
"testing"
"time"

awsgo "github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/aws/session"
"github.com/aws/aws-sdk-go/service/acmpca"
"github.com/gruntwork-io/cloud-nuke/util"
"github.com/gruntwork-io/go-commons/errors"
"github.com/stretchr/testify/assert"
)

// createTestACMPCA will create am ACMPCA and return its ARN.
func createTestACMPCA(t *testing.T, session *session.Session, name string) *string {
svc := acmpca.New(session)
ca, err := svc.CreateCertificateAuthority(&acmpca.CreateCertificateAuthorityInput{
CertificateAuthorityConfiguration: &acmpca.CertificateAuthorityConfiguration{
KeyAlgorithm: awsgo.String(acmpca.KeyAlgorithmRsa2048),
SigningAlgorithm: awsgo.String(acmpca.SigningAlgorithmSha256withrsa),
Subject: &acmpca.ASN1Subject{
CommonName: awsgo.String(name),
},
},
CertificateAuthorityType: awsgo.String("ROOT"),
Tags: []*acmpca.Tag{
{
Key: awsgo.String("Name"),
Value: awsgo.String(name),
},
},
})
if err != nil {
assert.Failf(t, "Could not create ACMPCA", errors.WithStackTrace(err).Error())
}
return ca.CertificateAuthorityArn
}

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

region, err := getRandomRegion()
if err != nil {
assert.Fail(t, errors.WithStackTrace(err).Error())
}
session, err := session.NewSession(&awsgo.Config{
Region: awsgo.String(region)},
)

if err != nil {
assert.Fail(t, errors.WithStackTrace(err).Error())
}

uniqueTestID := "cloud-nuke-test-" + util.UniqueID()
arn := createTestACMPCA(t, session, uniqueTestID)
// clean up after this test
defer nukeAllACMPCA(session, []*string{arn})

newARNs, err := getAllACMPCA(session, region, time.Now().Add(1*time.Hour*-1))
if err != nil {
assert.Fail(t, "Unable to fetch list of ACMPCA arns")
}
assert.NotContains(t, awsgo.StringValueSlice(newARNs), awsgo.StringValue(arn))

allARNs, err := getAllACMPCA(session, region, time.Now().Add(1*time.Hour))
if err != nil {
assert.Fail(t, "Unable to fetch list of ACMPCA arns")
}

assert.Contains(t, awsgo.StringValueSlice(allARNs), awsgo.StringValue(arn))
}

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

region, err := getRandomRegion()
if err != nil {
assert.Fail(t, errors.WithStackTrace(err).Error())
}

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

if err != nil {
assert.Fail(t, errors.WithStackTrace(err).Error())
}

uniqueTestID := "cloud-nuke-test-" + util.UniqueID()
arn := createTestACMPCA(t, session, uniqueTestID)

if err := nukeAllACMPCA(session, []*string{arn}); err != nil {
assert.Fail(t, errors.WithStackTrace(err).Error())
}

arns, err := getAllACMPCA(session, region, time.Now().Add(1*time.Hour))
if err != nil {
assert.Fail(t, "Unable to fetch list of ACMPCA arns")
}

assert.NotContains(t, awsgo.StringValueSlice(arns), awsgo.StringValue(arn))
}
36 changes: 36 additions & 0 deletions aws/acmpca_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"
)

// ACMPA - represents all ACMPA
type ACMPCA struct {
ARNs []string
}

// ResourceName - the simple name of the aws resource
func (ca ACMPCA) ResourceName() string {
return "acmpca"
}

// ResourceIdentifiers - The volume ids of the ebs volumes
func (ca ACMPCA) ResourceIdentifiers() []string {
return ca.ARNs
}

func (ca ACMPCA) MaxBatchSize() int {
// Tentative batch size to ensure AWS doesn't throttle
return 10
}

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

return nil
}
15 changes: 15 additions & 0 deletions aws/aws.go
Original file line number Diff line number Diff line change
Expand Up @@ -220,6 +220,20 @@ func GetAllResources(targetRegions []string, excludeAfter time.Time, resourceTyp
// The order in which resources are nuked is important
// because of dependencies between resources

// ACMPCA arns
acmpca := ACMPCA{}
if IsNukeable(acmpca.ResourceName(), resourceTypes) {
arns, err := getAllACMPCA(session, region, excludeAfter)
if err != nil {
return nil, errors.WithStackTrace(err)
}
if len(arns) > 0 {
acmpca.ARNs = awsgo.StringValueSlice(arns)
resourcesInRegion.Resources = append(resourcesInRegion.Resources, acmpca)
}
}
// End ACMPCA arns

// ASG Names
asGroups := ASGroups{}
if IsNukeable(asGroups.ResourceName(), resourceTypes) {
Expand Down Expand Up @@ -620,6 +634,7 @@ func GetAllResources(targetRegions []string, excludeAfter time.Time, resourceTyp
// ListResourceTypes - Returns list of resources which can be passed to --resource-type
func ListResourceTypes() []string {
resourceTypes := []string{
ACMPCA{}.ResourceName(),
ASGroups{}.ResourceName(),
LaunchConfigs{}.ResourceName(),
LoadBalancers{}.ResourceName(),
Expand Down
1 change: 1 addition & 0 deletions config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ type Config struct {
IAMUsers ResourceType `yaml:"IAMUsers"`
SecretsManagerSecrets ResourceType `yaml:"SecretsManager"`
NatGateway ResourceType `yaml:"NatGateway"`
ACMPCA ResourceType `yaml:"ACMPCA"`
}

type ResourceType struct {
Expand Down
94 changes: 94 additions & 0 deletions config/config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ func emptyConfig() *Config {
ResourceType{FilterRule{}, FilterRule{}},
ResourceType{FilterRule{}, FilterRule{}},
ResourceType{FilterRule{}, FilterRule{}},
ResourceType{FilterRule{}, FilterRule{}},
}
}

Expand Down Expand Up @@ -52,6 +53,99 @@ func TestConfig_Empty(t *testing.T) {
return
}

// ACMPCA Tests

func TestConfigACMPCA_Empty(t *testing.T) {
configFilePath := "./mocks/acmpa_empty.yaml"
configObj, err := GetConfig(configFilePath)

require.NoError(t, err)

if !reflect.DeepEqual(configObj, emptyConfig()) {
assert.Fail(t, "Config should be empty, %+v\n", configObj.ACMPCA)
}

return
}

func TestConfigACMPCA_EmptyFilters(t *testing.T) {
configFilePath := "./mocks/acmpca_empty_filters.yaml"
configObj, err := GetConfig(configFilePath)

require.NoError(t, err)

if !reflect.DeepEqual(configObj, emptyConfig()) {
assert.Fail(t, "Config should be empty, %+v\n", configObj)
}

return
}

func TestConfigACMPCA_EmptyRules(t *testing.T) {
configFilePath := "./mocks/acmpca_empty_rules.yaml"
configObj, err := GetConfig(configFilePath)

require.NoError(t, err)

if !reflect.DeepEqual(configObj, emptyConfig()) {
assert.Fail(t, "Config should be empty, %+v\n", configObj)
}

return
}

func TestConfigACMPCA_IncludeNames(t *testing.T) {
configFilePath := "./mocks/acmpca_include_names.yaml"
configObj, err := GetConfig(configFilePath)

require.NoError(t, err)

if reflect.DeepEqual(configObj, emptyConfig()) {
assert.Fail(t, "Config should not be empty, %+v\n", configObj)
}

if len(configObj.ACMPCA.IncludeRule.NamesRegExp) == 0 {
assert.Fail(t, "ConfigObj should contain ACMPCA arn regexes, %+v\n", configObj)
}

return
}

func TestConfigACMPCA_ExcludeNames(t *testing.T) {
configFilePath := "./mocks/acmpca_exclude_names.yaml"
configObj, err := GetConfig(configFilePath)

require.NoError(t, err)

if reflect.DeepEqual(configObj, emptyConfig()) {
assert.Fail(t, "Config should not be empty, %+v\n", configObj)
}

if len(configObj.ACMPCA.ExcludeRule.NamesRegExp) == 0 {
assert.Fail(t, "ConfigObj should contain ACMPCA arn regexes, %+v\n", configObj)
}

return
}

func TestConfigACMPCA_FilterNames(t *testing.T) {
configFilePath := "./mocks/acmpca_filter_names.yaml"
configObj, err := GetConfig(configFilePath)

require.NoError(t, err)

if reflect.DeepEqual(configObj, emptyConfig()) {
assert.Fail(t, "Config should not be empty, %+v\n", configObj)
}

if len(configObj.ACMPCA.IncludeRule.NamesRegExp) == 0 ||
len(configObj.ACMPCA.ExcludeRule.NamesRegExp) == 0 {
assert.Fail(t, "ConfigObj should contain ACMPCA arn regexes, %+v\n", configObj)
}

return
}

// S3 Tests

func TestConfigS3_Empty(t *testing.T) {
Expand Down
4 changes: 4 additions & 0 deletions config/mocks/acmpca_all.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
acmpca:
include:
names_regex:
- .*
5 changes: 5 additions & 0 deletions config/mocks/acmpca_cleanup.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
acmpca:
include:
names_regex:
- ^cloud-nuke-test-
- -cloud-nuke-test-
1 change: 1 addition & 0 deletions config/mocks/acmpca_empty.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
acmpca:
Loading

0 comments on commit d9566af

Please sign in to comment.