├── backup-policies ├── AWSBackupVault-ServiceControlPolicy.json ├── bcp-tier2-BackupPolicy.json ├── bcp-tier3-BackupPolicy.json ├── bcp-tier4-BackupPolicy.json └── bcp-tier1-BackupPolicy.json ├── terraform ├── main.tf ├── Makefile └── org_policies.tf ├── scripts ├── delete_vault.sh └── delete_all_vaults.sh ├── .gitignore ├── README.md └── cloudformation └── AWSBackup-Vaults-StackSetTemplate.yaml /backup-policies/AWSBackupVault-ServiceControlPolicy.json: -------------------------------------------------------------------------------- 1 | { 2 | "Version": "2012-10-17", 3 | "Statement": [ 4 | { 5 | "Sid": "ProtectVaults", 6 | "Effect": "Deny", 7 | "Action": [ 8 | "backup:CancelLegalHold", 9 | "backup:DeleteBackupVault", 10 | "backup:DeleteBackupVaultAccessPolicy", 11 | "backup:DeleteBackupVaultLockConfiguration", 12 | "backup:DeleteRecoveryPoint", 13 | "backup:DisassociateRecoveryPoint", 14 | "backup:UpdateRecoveryPointLifecycle" 15 | ], 16 | "Resource": [ 17 | "*" 18 | ] 19 | } 20 | ] 21 | } -------------------------------------------------------------------------------- /terraform/main.tf: -------------------------------------------------------------------------------- 1 | # Copyright 2023 Chris Farris 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | terraform { 16 | required_providers { 17 | aws = { 18 | source = "hashicorp/aws" 19 | } 20 | } 21 | 22 | required_version = ">= 0.14.9" 23 | 24 | backend "s3" { 25 | key = "pht-awsbackup-management.tfstate" 26 | region = "us-east-1" 27 | } 28 | } 29 | 30 | provider "aws" { 31 | region = "us-east-1" 32 | } 33 | 34 | # resource "random_pet" "prefix" {} 35 | 36 | data "aws_organizations_organization" "my_organization" {} 37 | 38 | -------------------------------------------------------------------------------- /scripts/delete_vault.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # Copyright 2023 Chris Farris 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | 16 | VAULT=$1 17 | 18 | if [ -z "$VAULT" ] ; then 19 | echo "USAGE: $0 " 20 | exit 1 21 | fi 22 | 23 | RECOVERY_POINTS=`aws backup list-recovery-points-by-backup-vault --backup-vault-name $VAULT --query RecoveryPoints[].RecoveryPointArn --output text ` 24 | for a in $RECOVERY_POINTS ; do 25 | echo "Deleting $a" 26 | aws backup delete-recovery-point --backup-vault-name $VAULT --recovery-point-arn $a 27 | done 28 | echo "Deleting Vault $VAULT" 29 | aws backup delete-backup-vault --backup-vault-name $VAULT 30 | -------------------------------------------------------------------------------- /scripts/delete_all_vaults.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # Copyright 2023 Chris Farris 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | 16 | 17 | REGIONS=`aws ec2 describe-regions --query Regions[].RegionName --output text` 18 | 19 | for r in $REGIONS ; do 20 | VAULTS=`aws backup list-backup-vaults --query BackupVaultList[].BackupVaultName --output text --region $r` 21 | 22 | for v in $VAULTS ; do 23 | RECOVERY_POINTS=`aws backup list-recovery-points-by-backup-vault --backup-vault-name $v --query RecoveryPoints[].RecoveryPointArn --output text --region $r --max-results 1000` 24 | for a in $RECOVERY_POINTS ; do 25 | echo "Deleting RecoveryPoint $a from $v" 26 | aws backup delete-recovery-point --backup-vault-name $v --recovery-point-arn $a --region $r 27 | done 28 | echo "Deleting Vault $v in $r" 29 | aws backup delete-backup-vault --backup-vault-name $v --region $r 30 | done 31 | done -------------------------------------------------------------------------------- /backup-policies/bcp-tier2-BackupPolicy.json: -------------------------------------------------------------------------------- 1 | { 2 | "plans": { 3 | "bcp-tier2-default": { 4 | "regions": { 5 | "@@assign": [ 6 | "eu-north-1", 7 | "ap-south-1", 8 | "eu-west-3", 9 | "eu-west-2", 10 | "eu-west-1", 11 | "ap-northeast-3", 12 | "ap-northeast-2", 13 | "ap-northeast-1", 14 | "sa-east-1", 15 | "ca-central-1", 16 | "ap-southeast-1", 17 | "ap-southeast-2", 18 | "eu-central-1", 19 | "us-east-1", 20 | "us-east-2", 21 | "us-west-1", 22 | "us-west-2" 23 | ] 24 | }, 25 | "rules": { 26 | "BCP_tier2_Rule": { 27 | "schedule_expression": {"@@assign": "cron(0 * ? * * *)"}, 28 | "start_backup_window_minutes": {"@@assign": "60"}, 29 | "complete_backup_window_minutes": {"@@assign": "480"}, 30 | "lifecycle": {"delete_after_days": {"@@assign": "180"} }, 31 | "target_backup_vault_name": {"@@assign": "BCPVault-tier2"} } 32 | }, 33 | "backup_plan_tags": { 34 | "CreatedBy": { 35 | "tag_key": {"@@assign": "CreatedBy"}, 36 | "tag_value": {"@@assign": "AWS Backup Policy bcp-tier2-default"} 37 | } 38 | }, 39 | "selections": { 40 | "tags": { 41 | "aws_backup_bcp_tier-tier2": { 42 | "iam_role_arn": {"@@assign": "arn:aws:iam::$account:role/CentralAWSBackupRole"}, 43 | "tag_key": {"@@assign": "aws_backup_bcp_tier"}, 44 | "tag_value": {"@@assign": ["tier2"] } } 45 | } 46 | }, 47 | "advanced_backup_settings": { 48 | "ec2": {"windows_vss": {"@@assign": "enabled"} } 49 | } 50 | } 51 | } 52 | } -------------------------------------------------------------------------------- /backup-policies/bcp-tier3-BackupPolicy.json: -------------------------------------------------------------------------------- 1 | { 2 | "plans": { 3 | "bcp-tier3-default": { 4 | "regions": { 5 | "@@assign": [ 6 | "eu-north-1", 7 | "ap-south-1", 8 | "eu-west-3", 9 | "eu-west-2", 10 | "eu-west-1", 11 | "ap-northeast-3", 12 | "ap-northeast-2", 13 | "ap-northeast-1", 14 | "sa-east-1", 15 | "ca-central-1", 16 | "ap-southeast-1", 17 | "ap-southeast-2", 18 | "eu-central-1", 19 | "us-east-1", 20 | "us-east-2", 21 | "us-west-1", 22 | "us-west-2" 23 | ] 24 | }, 25 | "rules": { 26 | "BCP_tier3_Rule": { 27 | "schedule_expression": {"@@assign": "cron(0 0 ? * * *)"}, 28 | "start_backup_window_minutes": {"@@assign": "60"}, 29 | "complete_backup_window_minutes": {"@@assign": "480"}, 30 | "lifecycle": {"delete_after_days": {"@@assign": "90"} }, 31 | "target_backup_vault_name": {"@@assign": "BCPVault-tier3"} } 32 | }, 33 | "backup_plan_tags": { 34 | "CreatedBy": { 35 | "tag_key": {"@@assign": "CreatedBy"}, 36 | "tag_value": {"@@assign": "AWS Backup Policy bcp-tier3-default"} 37 | } 38 | }, 39 | "selections": { 40 | "tags": { 41 | "aws_backup_bcp_tier-tier3": { 42 | "iam_role_arn": {"@@assign": "arn:aws:iam::$account:role/CentralAWSBackupRole"}, 43 | "tag_key": {"@@assign": "aws_backup_bcp_tier"}, 44 | "tag_value": {"@@assign": ["tier3"] } } 45 | } 46 | }, 47 | "advanced_backup_settings": { 48 | "ec2": {"windows_vss": {"@@assign": "enabled"} } 49 | } 50 | } 51 | } 52 | } -------------------------------------------------------------------------------- /backup-policies/bcp-tier4-BackupPolicy.json: -------------------------------------------------------------------------------- 1 | { 2 | "plans": { 3 | "bcp-tier4-default": { 4 | "regions": { 5 | "@@assign": [ 6 | "eu-north-1", 7 | "ap-south-1", 8 | "eu-west-3", 9 | "eu-west-2", 10 | "eu-west-1", 11 | "ap-northeast-3", 12 | "ap-northeast-2", 13 | "ap-northeast-1", 14 | "sa-east-1", 15 | "ca-central-1", 16 | "ap-southeast-1", 17 | "ap-southeast-2", 18 | "eu-central-1", 19 | "us-east-1", 20 | "us-east-2", 21 | "us-west-1", 22 | "us-west-2" 23 | ] 24 | }, 25 | "rules": { 26 | "BCP_tier4_Rule": { 27 | "schedule_expression": {"@@assign": "cron(0 0 ? * * *)"}, 28 | "start_backup_window_minutes": {"@@assign": "60"}, 29 | "complete_backup_window_minutes": {"@@assign": "480"}, 30 | "lifecycle": {"delete_after_days": {"@@assign": "45"} }, 31 | "target_backup_vault_name": {"@@assign": "BCPVault-tier4"} 32 | } 33 | }, 34 | "backup_plan_tags": { 35 | "CreatedBy": { 36 | "tag_key": {"@@assign": "CreatedBy"}, 37 | "tag_value": {"@@assign": "AWS Backup Policy bcp-tier4-default"} 38 | } 39 | }, 40 | "selections": { 41 | "tags": { 42 | "aws_backup_bcp_tier4": { 43 | "iam_role_arn": {"@@assign": "arn:aws:iam::$account:role/CentralAWSBackupRole"}, 44 | "tag_key": {"@@assign": "aws_backup_bcp_tier"}, 45 | "tag_value": {"@@assign": ["tier4"] } 46 | } 47 | } 48 | }, 49 | "advanced_backup_settings": { 50 | "ec2": {"windows_vss": {"@@assign": "enabled"} } 51 | } 52 | } 53 | } 54 | } -------------------------------------------------------------------------------- /terraform/Makefile: -------------------------------------------------------------------------------- 1 | # Copyright 2023 Chris Farris 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | ifndef env 16 | $(error env is not set) 17 | endif 18 | 19 | include ../$(env).tfbackend 20 | 21 | tf-init: 22 | terraform init -backend-config=../$(env).tfbackend -reconfigure 23 | 24 | tf-plan: 25 | terraform plan -out=$(env)-terraform.tfplan -no-color 26 | 27 | tf-apply: 28 | terraform apply $(env)-terraform.tfplan 29 | @rm $(env)-terraform.tfplan 30 | terraform output -json > ../output-$(env).json 31 | @aws s3 cp ../output-$(env).json s3://$(bucket)/output-$(env).json 32 | 33 | tf-destroy: 34 | terraform destroy --auto-approve 35 | 36 | tf-refresh: 37 | terraform apply -refresh-only 38 | 39 | tf-force: 40 | terraform apply -auto-approve 41 | terraform output -json > ../output-$(env).json 42 | @aws s3 cp ../output-$(env).json s3://$(bucket)/output-$(env).json 43 | 44 | tf-output: 45 | terraform output -json > ../output-$(env).json 46 | 47 | tf-show: 48 | @terraform show $(env)-terraform.tfplan -no-color 49 | 50 | tf-plan-save: 51 | @aws s3 cp $(env)-terraform.tfplan s3://$(bucket)/$(env)-terraform.tfplan 52 | 53 | tf-plan-fetch: 54 | @aws s3 cp s3://$(bucket)/$(env)-terraform.tfplan $(env)-terraform.tfplan 55 | @aws s3 rm s3://$(bucket)/$(env)-terraform.tfplan -------------------------------------------------------------------------------- /terraform/org_policies.tf: -------------------------------------------------------------------------------- 1 | # Copyright 2023 Chris Farris 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | 16 | resource "aws_organizations_policy" "vault_protection_scp" { 17 | name = "vault_protection_scp" 18 | description = "Prevent Deletion of Backup Vaults and Recovery Points" 19 | type = "SERVICE_CONTROL_POLICY" 20 | content = file("${path.module}/../backup-policies/AWSBackupVault-ServiceControlPolicy.json") 21 | } 22 | resource "aws_organizations_policy_attachment" "vault_protection_scp" { 23 | policy_id = aws_organizations_policy.vault_protection_scp.id 24 | target_id = data.aws_organizations_organization.my_organization.roots[0].id 25 | } 26 | 27 | 28 | resource "aws_organizations_policy" "bcp_tier1_backup_policy" { 29 | name = "bcp_tier1_backup_policy" 30 | description = "BCP Tier 1 Backup Policy" 31 | type = "BACKUP_POLICY" 32 | content = file("${path.module}/../backup-policies/bcp-tier1-BackupPolicy.json") 33 | } 34 | resource "aws_organizations_policy_attachment" "bcp_tier1_backup_policy" { 35 | policy_id = aws_organizations_policy.bcp_tier1_backup_policy.id 36 | target_id = data.aws_organizations_organization.my_organization.roots[0].id 37 | } 38 | 39 | 40 | resource "aws_organizations_policy" "bcp_tier2_backup_policy" { 41 | name = "bcp_tier2_backup_policy" 42 | description = "BCP Tier 2 Backup Policy" 43 | type = "BACKUP_POLICY" 44 | content = file("${path.module}/../backup-policies/bcp-tier2-BackupPolicy.json") 45 | } 46 | resource "aws_organizations_policy_attachment" "bcp_tier2_backup_policy" { 47 | policy_id = aws_organizations_policy.bcp_tier2_backup_policy.id 48 | target_id = data.aws_organizations_organization.my_organization.roots[0].id 49 | } 50 | 51 | 52 | resource "aws_organizations_policy" "bcp_tier3_backup_policy" { 53 | name = "bcp_tier3_backup_policy" 54 | description = "BCP Tier 3 Backup Policy" 55 | type = "BACKUP_POLICY" 56 | content = file("${path.module}/../backup-policies/bcp-tier3-BackupPolicy.json") 57 | } 58 | resource "aws_organizations_policy_attachment" "bcp_tier3_backup_policy" { 59 | policy_id = aws_organizations_policy.bcp_tier3_backup_policy.id 60 | target_id = data.aws_organizations_organization.my_organization.roots[0].id 61 | } 62 | 63 | 64 | resource "aws_organizations_policy" "bcp_tier4_backup_policy" { 65 | name = "bcp_tier4_backup_policy" 66 | description = "BCP Tier 4 Backup Policy" 67 | type = "BACKUP_POLICY" 68 | content = file("${path.module}/../backup-policies/bcp-tier4-BackupPolicy.json") 69 | } 70 | resource "aws_organizations_policy_attachment" "bcp_tier4_backup_policy" { 71 | policy_id = aws_organizations_policy.bcp_tier4_backup_policy.id 72 | target_id = data.aws_organizations_organization.my_organization.roots[0].id 73 | } 74 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Copyright 2023 Chris Farris 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | # Generic places I've shoved things from a scratch/perspective 16 | Notes.md 17 | 18 | # Build-time crud 19 | *.zip 20 | *-test-event.json 21 | *-config.json 22 | *dist-info 23 | 24 | # Terraform Ignores 25 | .terraform 26 | *.tfplan 27 | .terraform.lock.hcl 28 | terraform.tfstate.d 29 | 30 | # Deploy specific env vars passed to makefile. Not to be shared. 31 | config.* 32 | 33 | # Manifests are inputs to Cloudformation. They contain deploy specific things (like payer accounts) 34 | *Manifest.yaml 35 | cloudformation/*Transformed*.yaml 36 | *.tfbackend 37 | output-*.json 38 | 39 | # Created by https://www.gitignore.io/api/osx,python 40 | 41 | ### OSX ### 42 | *.DS_Store 43 | .AppleDouble 44 | .LSOverride 45 | 46 | # Icon must end with two \r 47 | Icon 48 | 49 | # Thumbnails 50 | ._* 51 | 52 | # Files that might appear in the root of a volume 53 | .DocumentRevisions-V100 54 | .fseventsd 55 | .Spotlight-V100 56 | .TemporaryItems 57 | .Trashes 58 | .VolumeIcon.icns 59 | .com.apple.timemachine.donotpresent 60 | 61 | # Directories potentially created on remote AFP share 62 | .AppleDB 63 | .AppleDesktop 64 | Network Trash Folder 65 | Temporary Items 66 | .apdisk 67 | 68 | ### Python ### 69 | # Byte-compiled / optimized / DLL files 70 | __pycache__/ 71 | *.py[cod] 72 | *$py.class 73 | 74 | # C extensions 75 | *.so 76 | 77 | # Distribution / packaging 78 | .Python 79 | env/ 80 | build/ 81 | develop-eggs/ 82 | dist/ 83 | downloads/ 84 | eggs/ 85 | .eggs/ 86 | # lib/ 87 | lib64/ 88 | parts/ 89 | sdist/ 90 | var/ 91 | wheels/ 92 | *.egg-info/ 93 | .installed.cfg 94 | *.egg 95 | 96 | # PyInstaller 97 | # Usually these files are written by a python script from a template 98 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 99 | *.manifest 100 | *.spec 101 | 102 | # Installer logs 103 | pip-log.txt 104 | pip-delete-this-directory.txt 105 | 106 | # Unit test / coverage reports 107 | htmlcov/ 108 | .tox/ 109 | .coverage 110 | .coverage.* 111 | .cache 112 | nosetests.xml 113 | coverage.xml 114 | *,cover 115 | .hypothesis/ 116 | 117 | # Translations 118 | *.mo 119 | *.pot 120 | 121 | # Django stuff: 122 | *.log 123 | local_settings.py 124 | 125 | # Flask stuff: 126 | instance/ 127 | .webassets-cache 128 | 129 | # Scrapy stuff: 130 | .scrapy 131 | 132 | # Sphinx documentation 133 | docs/_build/ 134 | 135 | # PyBuilder 136 | target/ 137 | 138 | # Jupyter Notebook 139 | .ipynb_checkpoints 140 | 141 | # pyenv 142 | .python-version 143 | 144 | # celery beat schedule file 145 | celerybeat-schedule 146 | 147 | # SageMath parsed files 148 | *.sage.py 149 | 150 | # dotenv 151 | .env 152 | 153 | # virtualenv 154 | .venv 155 | venv/ 156 | ENV/ 157 | 158 | # Spyder project settings 159 | .spyderproject 160 | .spyproject 161 | 162 | # Rope project settings 163 | .ropeproject 164 | 165 | # mkdocs documentation 166 | /site 167 | 168 | 169 | # End of https://www.gitignore.io/api/osx,python 170 | 171 | 172 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # pht-awsbackup-management 2 | Scripts and IaC to create a ransomware resilient AWS Backup System 3 | 4 | You can read more about this s[olution on our website](https://www.primeharbor.com/blog/awsbackup/). 5 | 6 | ## How to leverage this solution 7 | 8 | The automated backup and protection of critical data is based on simple tagging. If the tag `aws_backup_bcp_tier` is applied to a database or EBS volume, it will be automatically backed up based on the tag's value. 9 | `tier1` - backed up hourly, copied to a second region, retained for 180 days 10 | `tier2` - backed up hourly, stored only in the original region, retained for 180 days 11 | `tier3` - backed up daily, stored in the original region, retained for 90 days 12 | `tier4` - backed up daily, stored in the original region, retained for 45 days 13 | `none` - Not backed up, but an option to ensure all resources have been reviewed. 14 | 15 | 16 | ## Installation 17 | 18 | ### Prerequisites. 19 | 20 | The following should be enabled via the AWS Organizations Management Account: 21 | 1. The CloudFormation StackSet to deploy the vaults requires an account with [CloudFormation Delegated admin](https://us-east-1.console.aws.amazon.com/cloudformation/home?region=us-east-1#/stacksets) access. 22 | 2. You must enable Backup Policies in the [Organizations Console](https://us-east-1.console.aws.amazon.com/organizations/v2/home/policies/backup-policy) 23 | 3. You must enable Service Control Policies in the [Organizations Console](https://us-east-1.console.aws.amazon.com/organizations/v2/home/policies/service-control-policy) 24 | 4. Enable [AWS Backup Delegated Administrator](https://us-east-1.console.aws.amazon.com/backup/home?region=us-east-1#/settings), by selecting a secure account in your organization which will be able to monitor all backup jobs. First enable "Cross account monitoring", then add the account by "[Register Delegated Administrator](https://us-east-1.console.aws.amazon.com/backup/home?region=us-east-1#settings/delegatedadministrator)" 25 | 5. [Enable](https://us-east-1.console.aws.amazon.com/backup/home?region=us-east-1#/settings) the Resource Types, in the Regions you care about **from your Organizations Management Account**. 26 | 27 | ## Deploy the Vaults 28 | 29 | The Backups Vaults are deployed via AWS Organizations CloudFormation StackSets. This ensures every account in every (enabled) region has the target vaults. The StackSet template needs to be deployed only once, preferably from us-east-1, from an AWS account that is a [registered delegated administrator for CloudFormation](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/stacksets-orgs-delegated-admin.html?icmpid=docs_cfn_console). 30 | 31 | The only parameter that needs to be changes is the OU Target for your organization. You can get the Root OU identifier with this command: 32 | ```bash 33 | aws organizations list-roots --query 'Roots[0].Id' --output text 34 | ``` 35 | 36 | The [AWSBackup-Vaults-StackSetTemplate.yaml](cloudformation/AWSBackup-Vaults-StackSetTemplate.yaml) template will create a StackSet and deploy the necessary vaults in all regions. Additionally, it will create the IAM Role `CentralAWSBackupRole` as part of the stack instance deployed to `us-east-1`. 37 | 38 | Once the vaults are fully deployed, you can create the backup policies and SCP. 39 | 40 | ## Deploy the backup policies. 41 | 42 | Due to [limitations](https://www.chrisfarris.com/post/organization-cloudformation/) with AWS CloudFormation's support for AWS Organizations, this part is implemented in terraform. 43 | 44 | 1. Create an S3 Bucket to store the state: 45 | ```bash 46 | aws s3 mb s3://aws-backup-state-ACCOUNTID 47 | ``` 48 | 2. Create a file called MYORG.tfbackend with contents as such: 49 | ``` 50 | bucket="aws-backup-state-ACCOUNTID" 51 | ``` 52 | 3. Run the Terraform 53 | ```bash 54 | cd terraform 55 | make env=MYORG tf-init 56 | make env=MYORG tf-plan 57 | make env=MYORG tf-apply 58 | ``` 59 | 60 | -------------------------------------------------------------------------------- /backup-policies/bcp-tier1-BackupPolicy.json: -------------------------------------------------------------------------------- 1 | { 2 | "plans": { 3 | "bcp-tier1-default": { 4 | "regions": { 5 | "@@assign": [ 6 | "eu-north-1", 7 | "ap-south-1", 8 | "eu-west-3", 9 | "eu-west-2", 10 | "eu-west-1", 11 | "ap-northeast-3", 12 | "ap-northeast-2", 13 | "ap-northeast-1", 14 | "sa-east-1", 15 | "ca-central-1", 16 | "ap-southeast-1", 17 | "ap-southeast-2", 18 | "eu-central-1", 19 | "us-east-2", 20 | "us-west-1", 21 | "us-west-2" 22 | ] 23 | }, 24 | "rules": { 25 | "BCP_Tier1_Rule": { 26 | "schedule_expression": {"@@assign": "cron(0 * ? * * *)"}, 27 | "start_backup_window_minutes": {"@@assign": "60"}, 28 | "complete_backup_window_minutes": {"@@assign": "480"}, 29 | "lifecycle": {"delete_after_days": {"@@assign": "180"} }, 30 | "target_backup_vault_name": {"@@assign": "BCPVault-tier1"}, 31 | "recovery_point_tags": { 32 | "cost-center": { 33 | "tag_key": {"@@assign": "cost-center"}, 34 | "tag_value": {"@@assign": "bcp-tier1-default"} 35 | } 36 | }, 37 | "copy_actions": { 38 | "arn:aws:backup:us-east-1:$account:backup-vault:BCPVault-CrossRegional-us-east-1": { 39 | "target_backup_vault_arn": { 40 | "@@assign": "arn:aws:backup:us-east-1:$account:backup-vault:BCPVault-CrossRegional-us-east-1" 41 | }, 42 | "lifecycle": { 43 | "delete_after_days": {"@@assign": "180"} 44 | } 45 | } 46 | } 47 | } 48 | }, 49 | "backup_plan_tags": { 50 | "CreatedBy": { 51 | "tag_key": {"@@assign": "CreatedBy"}, 52 | "tag_value": {"@@assign": "AWS Backup Policy bcp-tier1-default"} 53 | } 54 | }, 55 | "selections": { 56 | "tags": { 57 | "aws_backup_bcp_tier-tier1": { 58 | "iam_role_arn": {"@@assign": "arn:aws:iam::$account:role/CentralAWSBackupRole"}, 59 | "tag_key": {"@@assign": "aws_backup_bcp_tier"}, 60 | "tag_value": {"@@assign": ["tier1"] } 61 | } 62 | } 63 | }, 64 | "advanced_backup_settings": { 65 | "ec2": {"windows_vss": {"@@assign": "enabled"} } 66 | } 67 | }, 68 | "bcp-tier1-us-east-1": { 69 | "regions": { 70 | "@@assign": ["us-east-1"] 71 | }, 72 | "rules": { 73 | "BCP_Tier1_Rule": { 74 | "schedule_expression": {"@@assign": "cron(0 * ? * * *)"}, 75 | "start_backup_window_minutes": {"@@assign": "60"}, 76 | "complete_backup_window_minutes": {"@@assign": "480"}, 77 | "lifecycle": {"delete_after_days": {"@@assign": "180"} 78 | }, 79 | "target_backup_vault_name": {"@@assign": "BCPVault-tier1"}, 80 | "recovery_point_tags": { 81 | "cost-center": { 82 | "tag_key": {"@@assign": "cost-center"}, 83 | "tag_value": {"@@assign": "bcp-tier1-us-east-1"} 84 | } 85 | }, 86 | "copy_actions": { 87 | "arn:aws:backup:us-west-2:$account:backup-vault:BCPVault-CrossRegional-us-west-2": { 88 | "target_backup_vault_arn": { 89 | "@@assign": "arn:aws:backup:us-west-2:$account:backup-vault:BCPVault-CrossRegional-us-west-2" 90 | }, 91 | "lifecycle": { 92 | "delete_after_days": {"@@assign": "180"} 93 | } 94 | } 95 | } 96 | } 97 | }, 98 | "backup_plan_tags": { 99 | "CreatedBy": { 100 | "tag_key": {"@@assign": "CreatedBy"}, 101 | "tag_value": {"@@assign": "AWS Backup Policy bcp-tier1-us-east-1"} 102 | } 103 | }, 104 | "selections": { 105 | "tags": { 106 | "aws_backup_bcp_tier-tier1": { 107 | "iam_role_arn": {"@@assign": "arn:aws:iam::$account:role/CentralAWSBackupRole"}, 108 | "tag_key": {"@@assign": "aws_backup_bcp_tier"}, 109 | "tag_value": {"@@assign": ["tier1"] } } 110 | } 111 | }, 112 | "advanced_backup_settings": { 113 | "ec2": {"windows_vss": {"@@assign": "enabled"} } 114 | } 115 | } 116 | } 117 | } -------------------------------------------------------------------------------- /cloudformation/AWSBackup-Vaults-StackSetTemplate.yaml: -------------------------------------------------------------------------------- 1 | # Copyright 2023 Chris Farris 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | AWSTemplateFormatVersion: '2010-09-09' 16 | Description: Deploy Backup Vaults in every account & core regions 17 | 18 | Parameters: 19 | 20 | pRoleName: 21 | Description: Name of the IAM Role to deploy in us-east-1 for the AWS Backup Service 22 | Type: String 23 | Default: CentralAWSBackupRole 24 | 25 | pVaultNamePrefix: 26 | Description: Prefix for the Backup Vault Name to be deployed in all Accounts 27 | Type: String 28 | Default: BCPVault 29 | 30 | pAWSOrgUnit: 31 | Description: AWS Organizations OU to deploy this stackset to. Probably should be the root OU 32 | Type: String 33 | AllowedPattern: '^(ou-[a-z0-9]{4,32}-[a-z0-9]{8,32}|r-[a-z0-9]{4,32})$' 34 | 35 | Resources: 36 | 37 | VaultStackSet: 38 | Type: AWS::CloudFormation::StackSet 39 | Properties: 40 | AutoDeployment: 41 | Enabled: True 42 | RetainStacksOnAccountRemoval: True 43 | CallAs: DELEGATED_ADMIN 44 | Capabilities: 45 | - CAPABILITY_IAM 46 | - CAPABILITY_NAMED_IAM 47 | Description: Organizational StackSet to Deploy AWS Backup Vaults 48 | OperationPreferences: 49 | # Per the docs: MaxConcurrentCount is at most one more than the FailureToleranceCount. 50 | FailureTolerancePercentage: 100 51 | MaxConcurrentPercentage: 100 52 | Parameters: 53 | - ParameterKey: pRoleName 54 | ParameterValue: !Ref pRoleName 55 | - ParameterKey: pVaultNamePrefix 56 | ParameterValue: !Ref pVaultNamePrefix 57 | PermissionModel: SERVICE_MANAGED 58 | StackInstancesGroup: 59 | - DeploymentTargets: 60 | OrganizationalUnitIds: 61 | - !Ref pAWSOrgUnit 62 | Regions: 63 | - us-east-1 64 | - eu-north-1 65 | - ap-south-1 66 | - eu-west-3 67 | - eu-west-2 68 | - eu-west-1 69 | - ap-northeast-3 70 | - ap-northeast-2 71 | - ap-northeast-1 72 | - sa-east-1 73 | - ca-central-1 74 | - ap-southeast-1 75 | - ap-southeast-2 76 | - eu-central-1 77 | - us-east-2 78 | - us-west-1 79 | - us-west-2 80 | StackSetName: !Sub "${AWS::StackName}-StackSet" 81 | TemplateBody: | 82 | AWSTemplateFormatVersion: '2010-09-09' 83 | Description: Deploys AWS Backup Vaults for use with an Organizational Backup Policy. 84 | 85 | Parameters: 86 | 87 | pVaultNamePrefix: 88 | Description: Name of the Backup Vault to create 89 | Type: String 90 | 91 | pRoleName: 92 | Description: Name of the ServiceLinked Role to Create. **DO NOT CHANGE** 93 | Type: String 94 | 95 | Conditions: 96 | cCreateServiceLinkedRole: !Equals [ !Ref "AWS::Region", "us-east-1"] 97 | cCreateCrossRegionalVault: !Or [ !Equals [ !Ref "AWS::Region", "us-east-1"], !Equals [ !Ref "AWS::Region", "us-west-2"] ] 98 | 99 | Resources: 100 | 101 | RequiredServiceRole: 102 | Condition: cCreateServiceLinkedRole 103 | Type: "AWS::IAM::Role" 104 | Properties: 105 | RoleName: !Ref pRoleName 106 | AssumeRolePolicyDocument: 107 | Version: "2012-10-17" 108 | Statement: 109 | - Effect: "Allow" 110 | Principal: 111 | Service: 112 | - "backup.amazonaws.com" 113 | Action: 114 | - "sts:AssumeRole" 115 | ManagedPolicyArns: 116 | - "arn:aws:iam::aws:policy/service-role/AWSBackupServiceRolePolicyForBackup" 117 | 118 | Tier1Vault: 119 | Type: AWS::Backup::BackupVault 120 | Properties: 121 | # AccessPolicy: Json 122 | BackupVaultName: !Sub ${pVaultNamePrefix}-tier1 123 | BackupVaultTags: 124 | deployed_by : Centrally Managed AWS Backup CloudFormation Stack. 125 | LockConfiguration: 126 | # ChangeableForDays: Integer 127 | MaxRetentionDays: 180 128 | MinRetentionDays: 180 129 | 130 | Tier2Vault: 131 | Type: AWS::Backup::BackupVault 132 | Properties: 133 | # AccessPolicy: Json 134 | BackupVaultName: !Sub ${pVaultNamePrefix}-tier2 135 | BackupVaultTags: 136 | deployed_by : Centrally Managed AWS Backup CloudFormation Stack. 137 | LockConfiguration: 138 | # ChangeableForDays: Integer 139 | MaxRetentionDays: 180 140 | MinRetentionDays: 180 141 | 142 | Tier3Vault: 143 | Type: AWS::Backup::BackupVault 144 | Properties: 145 | # AccessPolicy: Json 146 | BackupVaultName: !Sub ${pVaultNamePrefix}-tier3 147 | BackupVaultTags: 148 | deployed_by : Centrally Managed AWS Backup CloudFormation Stack. 149 | LockConfiguration: 150 | # ChangeableForDays: Integer 151 | MaxRetentionDays: 90 152 | MinRetentionDays: 90 153 | 154 | Tier4Vault: 155 | Type: AWS::Backup::BackupVault 156 | Properties: 157 | # AccessPolicy: Json 158 | BackupVaultName: !Sub ${pVaultNamePrefix}-tier4 159 | BackupVaultTags: 160 | deployed_by : Centrally Managed AWS Backup CloudFormation Stack. 161 | LockConfiguration: 162 | # ChangeableForDays: Integer 163 | MaxRetentionDays: 45 164 | MinRetentionDays: 45 165 | 166 | CrossRegionalVault: 167 | Type: AWS::Backup::BackupVault 168 | Condition: cCreateCrossRegionalVault 169 | Properties: 170 | # AccessPolicy: Json 171 | BackupVaultName: !Sub ${pVaultNamePrefix}-CrossRegional-${AWS::Region} 172 | BackupVaultTags: 173 | deployed_by : Centrally Managed AWS Backup CloudFormation Stack. 174 | LockConfiguration: 175 | # ChangeableForDays: Integer 176 | MaxRetentionDays: 180 177 | MinRetentionDays: 180 178 | 179 | Outputs: 180 | TemplateVersion: 181 | Value: "1.0.1" 182 | 183 | ######## End of StackSet Instance Template 184 | 185 | Outputs: 186 | 187 | TemplateVersion: 188 | Value: "1.0.0" 189 | --------------------------------------------------------------------------------