diff --git a/.gitignore b/.gitignore index 8bfec31..b95b324 100644 --- a/.gitignore +++ b/.gitignore @@ -6,10 +6,10 @@ *vulnerabilities*report*.json *report*json.zip .version - +.idea/ *.code-workspace !project.code-workspace # Please, add your custom content below! -.DS_Store \ No newline at end of file +.DS_Store diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000..6389b61 --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,67 @@ +fail_fast: false +exclude: '^.venv/.*' +default_install_hook_types: [pre-commit, pre-push, commit-msg, prepare-commit-msg] +default_stages: [pre-commit] +repos: + - repo: https://github.com/pre-commit/pre-commit-hooks + rev: v5.0.0 + hooks: + - id: check-ast + - id: check-toml + - id: check-yaml + - id: check-json + - id: end-of-file-fixer + - id: fix-byte-order-marker + - id: trailing-whitespace + - id: check-executables-have-shebangs + - id: check-symlinks + - id: destroyed-symlinks + - id: mixed-line-ending + - id: detect-aws-credentials + - id: detect-private-key + - id: fix-byte-order-marker + - id: requirements-txt-fixer + + - repo: local + hooks: + - id: trivy + name: trivy + entry: make tf-trivy + language: system + files: \.tf(vars)?$ + pass_filenames: false + - id: tf-format + name: tf-format + entry: make tf-format + language: system + files: (\.tf|\.tfvars)$ + exclude: \.terraform/.*$ + pass_filenames: false + - id: tf-lint + name: tf-lint + entry: make tf-lint + language: system + files: (\.tf|\.tfvars)$ + exclude: \.terraform/.*$ + pass_filenames: false + - id: shellcheck + name: shellcheck + entry: make shellcheck + language: system + files: (\.sh)$ + pass_filenames: false + - id: secrets + name: git secrets + entry: scripts/check-secrets.sh + language: script + pass_filenames: false + - id: secrets-commit-msg + name: git secrets check commit message + entry: scripts/check-secrets.sh commit-msg + language: system + stages: [commit-msg] + - id: secrets-prep-commit-msg + name: git secrets pre check commit message + entry: scripts/check-secrets.sh commit-msg + language: system + stages: [prepare-commit-msg] diff --git a/.tflint.hcl b/.tflint.hcl new file mode 100644 index 0000000..551aa9f --- /dev/null +++ b/.tflint.hcl @@ -0,0 +1,14 @@ + +plugin "aws" { + enabled = true + version = "0.41.0" + source = "github.com/terraform-linters/tflint-ruleset-aws" +} + +config { + plugin_dir = "~/.tflint.d/plugins" + call_module_type = "local" + ignore_module = { + "does-not-work" = true + } +} diff --git a/.tool-versions b/.tool-versions index 32db55a..a64a6a0 100644 --- a/.tool-versions +++ b/.tool-versions @@ -1,7 +1,9 @@ # This file is for you! Please, updated to the versions agreed by your team. -terraform 1.7.0 pre-commit 3.6.0 +terraform 1.7.0 +trivy 0.64.1 +tflint 0.58.1 # ============================================================================== # The section below is reserved for Docker image versions. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..a6a8dc9 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,42 @@ +# Contributing + +## dependencies +tools used: +- make +- git +- [asdf version manager](https://asdf-vm.com/guide/getting-started.html) + +## first run ... + +### install project tools +use asdf to ensure required tools are installed ... configured tools are in [.tool-versions](.tool-versions) +```bash +cd ~/work/terraform-aws-backup +for plugin in $(grep -E '^\w+' .tool-versions | cut -d' ' -f1); do asdf plugin add $plugin; done +asdf install +``` + +### setup git-secrets +git secrets scanning uses the awslabs https://github.com/awslabs/git-secrets there are options on how to install but +```bash +# if the command `git secrets` does not work in your repo +# the git-secrets script needs to be added to somewhere in your PATH +# for example if $HOME/.local/bin is in your PATH environment variable +# then: +wget https://raw.githubusercontent.com/awslabs/git-secrets/refs/heads/master/git-secrets -O ~/.local/bin/git-secrets +chmod +x ~/.local/bin/git-secrets +``` + +### install pre-commit hooks +```shell +pre-commit install +``` + + +### secrets +the git-secrets script will try and avoid accidental committing of secrets +patterns are excluded using [.gitdisallowed](.gitdisallowed) and allow listed using [.gitallowed](.gitallowed) +You can check for secrets / test patterns at any time though with +```shell +make check-secrets-all +``` diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..eafce88 --- /dev/null +++ b/Makefile @@ -0,0 +1,52 @@ +SHELL:=/bin/bash -o pipefail -O globstar +.SHELLFLAGS = -ec +.PHONY: build dist +.DEFAULT_GOAL := list +make := make --no-print-directory + +list: + @grep '^[^#[:space:]].*:' Makefile + + +guard-%: + @if [[ "${${*}}" == "" ]]; then \ + echo "env var: $* not set"; \ + exit 1; \ + fi + +######################################################################################################################## +## +## Makefile for this project things +## +######################################################################################################################## +pwd := ${PWD} +dirname := $(notdir $(patsubst %/,%,$(CURDIR))) + +tf-lint: + tflint --chdir=modules/aws-backup-source --config "$(pwd)/.tflint.hcl" + tflint --chdir=modules/aws-backup-destination --config "$(pwd)/.tflint.hcl" + tflint --chdir=examples/source --config "$(pwd)/.tflint.hcl" + tflint --chdir=examples/destination --config "$(pwd)/.tflint.hcl" + +tf-format-check: + terraform fmt -check -recursive + +tf-format: + terraform fmt --recursive + +tf-trivy: + trivy conf --exit-code 1 ./ --skip-dirs "**/.terraform" + +shellcheck: + @docker run --rm -i -v ${PWD}:/mnt:ro koalaman/shellcheck -f gcc -e SC1090,SC1091 `find . \( -path "*/.venv/*" -prune -o -path "*/build/*" -prune -o -path "*/dist/*" -prune -o -path "*/.tox/*" -prune \) -o -type f -name '*.sh' -print` + +lint: tf-lint tf-trivy shellcheck + +check-secrets: + scripts/check-secrets.sh + +check-secrets-all: + scripts/check-secrets.sh unstaged + +.env: + echo "LOCALSTACK_PORT=$$(python -c 'import socket; s=socket.socket(); s.bind(("", 0)); print(s.getsockname()[1])')" > .env diff --git a/examples/destination/aws-backups.tf b/examples/destination/aws-backups.tf index 4485b76..4bcc073 100644 --- a/examples/destination/aws-backups.tf +++ b/examples/destination/aws-backups.tf @@ -1,4 +1,5 @@ -provider "aws" { + +provider "aws" { alias = "source" region = "eu-west-2" } @@ -16,10 +17,8 @@ data "aws_caller_identity" "current" {} locals { # Adjust these as required - project_name = "my-shiny-project" - environment_name = "dev" - - source_account_id = data.aws_arn.source_terraform_role.account + project_name = "my-shiny-project" + source_account_id = data.aws_arn.source_terraform_role.account destination_account_id = data.aws_caller_identity.current.account_id } @@ -39,13 +38,18 @@ resource "aws_kms_key" "destination_backup_key" { Principal = { AWS = "arn:aws:iam::${local.destination_account_id}:root" } - Action = "kms:*" + Action = "kms:*" Resource = "*" } ] }) } +resource "aws_kms_alias" "destination_backup" { + target_key_id = aws_kms_key.destination_backup_key.id + name = "alias/${local.project_name}-backup-destination" +} + module "destination" { source = "../../modules/aws-backup-destination" diff --git a/examples/destination/versions.tf b/examples/destination/versions.tf new file mode 100644 index 0000000..49201ad --- /dev/null +++ b/examples/destination/versions.tf @@ -0,0 +1,12 @@ +terraform { + required_providers { + + aws = { + source = "hashicorp/aws" + version = "> 5" + } + + } + + required_version = ">= 1.9.5" +} diff --git a/examples/source-bootstrap/permissions.tf b/examples/source-bootstrap/permissions.tf index a4677af..ec14a15 100644 --- a/examples/source-bootstrap/permissions.tf +++ b/examples/source-bootstrap/permissions.tf @@ -85,5 +85,5 @@ resource "aws_iam_policy" "source_account_backup_permissions" { resource "aws_iam_role_policy_attachment" "source_account_backup_permissions" { policy_arn = aws_iam_policy.source_account_backup_permissions.arn - role = var.terraform_apply_role_name + role = var.terraform_apply_role_name } diff --git a/examples/source/aws-backups.tf b/examples/source/aws-backups.tf index b57747b..fd1bed6 100644 --- a/examples/source/aws-backups.tf +++ b/examples/source/aws-backups.tf @@ -1,4 +1,4 @@ -provider "aws" { +provider "aws" { alias = "source" region = "eu-west-2" } @@ -14,18 +14,31 @@ data "aws_arn" "destination_vault_arn" { locals { # Adjust these as required - project_name = "my-shiny-project" - environment_name = "dev" - - source_account_id = data.aws_caller_identity.current.account_id + project_name = "my-shiny-project" + environment_name = "dev" + account_name = "${local.project_name}-${local.environment_name}" + source_account_id = data.aws_caller_identity.current.account_id destination_account_id = data.aws_arn.destination_vault_arn.account } # First, we create an S3 bucket for compliance reports. You may already have a module for creating # S3 buckets with more refined access rules, which you may prefer to use. +# todo: review and remove these ignores +# trivy:ignore:AVD-AWS-0088 +# trivy:ignore:AVD-AWS-0089 +# trivy:ignore:AVD-AWS-0090 +# trivy:ignore:AVD-AWS-0132 resource "aws_s3_bucket" "backup_reports" { - bucket_prefix = "${local.project_name}-backup-reports" + bucket_prefix = "${local.project_name}-backup-reports" +} + +resource "aws_s3_bucket_public_access_block" "backup_reports" { + bucket = aws_s3_bucket.backup_reports.id + ignore_public_acls = true + block_public_acls = true + restrict_public_buckets = true + block_public_policy = true } # Now we have to configure access to the report bucket. @@ -54,7 +67,7 @@ resource "aws_s3_bucket_policy" "backup_reports_policy" { Principal = { AWS = "arn:aws:iam::${local.source_account_id}:role/aws-service-role/reports.backup.amazonaws.com/AWSServiceRoleForBackupReports" }, - Action = "s3:PutObject", + Action = "s3:PutObject", Resource = "${aws_s3_bucket.backup_reports.arn}/*", Condition = { StringEquals = { @@ -73,114 +86,76 @@ resource "aws_s3_bucket_policy" "backup_reports_policy" { # First we need some contextual data data "aws_caller_identity" "current" {} -data "aws_region" "current" {} - -# Now we can define the key itself -resource "aws_kms_key" "backup_notifications" { - description = "KMS key for AWS Backup notifications" - deletion_window_in_days = 7 - enable_key_rotation = true - policy = jsonencode({ - Version = "2012-10-17" - Statement = [ - { - Effect = "Allow" - Sid = "Enable IAM User Permissions" - Principal = { - AWS = "arn:aws:iam::${local.source_account_id}:root" - } - Action = "kms:*" - Resource = "*" - }, - { - Effect = "Allow" - Principal = { - Service = "sns.amazonaws.com" - } - Action = ["kms:GenerateDataKey*", "kms:Decrypt"] - Resource = "*" - }, - { - Effect = "Allow" - Principal = { - Service = "backup.amazonaws.com" - } - Action = ["kms:GenerateDataKey*", "kms:Decrypt"] - Resource = "*" - }, - ] - }) -} # Now we can deploy the source and destination modules, referencing the resources we've created above. module "source" { source = "../../modules/aws-backup-source" - backup_copy_vault_account_id = local.destination_account_id - backup_copy_vault_arn = data.aws_arn.destination_vault_arn.arn - environment_name = local.environment_name - bootstrap_kms_key_arn = aws_kms_key.backup_notifications.arn - project_name = local.project_name - reports_bucket = aws_s3_bucket.backup_reports.bucket - terraform_role_arns = [data.aws_caller_identity.current.arn] - - backup_plan_config = { - "compliance_resource_types": [ - "S3" - ], - "rules": [ - { - "copy_action": { - "delete_after": 4 - }, - "lifecycle": { - "delete_after": 2 - }, - "name": "daily_kept_for_2_days", - "schedule": "cron(0 0 * * ? *)" - } - ], - "selection_tag": "NHSE-Enable-Backup" - # The selection_tags are optional and can be used to - # provide fine grained resource selection with existing tagging - "selection_tags": [ - { - "key": "Environment" - "value": "myenvironment" - } - ] - } + name_prefix = local.account_name + backup_copy_vault_account_id = local.destination_account_id + backup_copy_vault_arn = data.aws_arn.destination_vault_arn.arn + environment_name = local.environment_name + project_name = local.project_name + reports_bucket = aws_s3_bucket.backup_reports.bucket + terraform_role_arns = [data.aws_caller_identity.current.arn] + + backup_plan_config = { + "compliance_resource_types" : [ + "S3" + ], + "rules" : [ + { + "copy_action" : { + "delete_after" : 4 + }, + "lifecycle" : { + "delete_after" : 2 + }, + "name" : "daily_kept_for_2_days", + "schedule" : "cron(0 0 * * ? *)" + } + ], + "selection_tag" : "NHSE-Enable-Backup" + # The selection_tags are optional and can be used to + # provide fine grained resource selection with existing tagging + "selection_tags" : [ + { + "key" : "Environment" + "value" : "myenvironment" + } + ] + } # Note here that we need to explicitly disable DynamoDB and Aurora backups in the source account. # The default config in the module enables backups for all resource types. - backup_plan_config_dynamodb = { - "compliance_resource_types": [ - "DynamoDB" - ], - "rules": [ - ], - "enable": false, - "selection_tag": "NHSE-Enable-Backup" - } - - backup_plan_config_ebsvol = { - "compliance_resource_types": [ - "EBS" - ], - "rules": [ - ], - "enable": false, - "selection_tag": "NHSE-Enable-Backup" - } - - backup_plan_config_aurora = { - "compliance_resource_types": [ - "Aurora" - ], - "rules": [ - ], - "enable": false, - "selection_tag": "NHSE-Enable-Backup" - } + backup_plan_config_dynamodb = { + "compliance_resource_types" : [ + "DynamoDB" + ], + "rules" : [ + ], + "enable" : false, + "selection_tag" : "NHSE-Enable-Backup" + } + + backup_plan_config_ebsvol = { + "compliance_resource_types" : [ + "EBS" + ], + "rules" : [ + ], + "enable" : false, + "selection_tag" : "NHSE-Enable-Backup" + } + + backup_plan_config_aurora = { + "compliance_resource_types" : [ + "Aurora" + ], + "rules" : [ + ], + "enable" : false, + "selection_tag" : "NHSE-Enable-Backup" + } } diff --git a/examples/source/versions.tf b/examples/source/versions.tf new file mode 100644 index 0000000..49201ad --- /dev/null +++ b/examples/source/versions.tf @@ -0,0 +1,12 @@ +terraform { + required_providers { + + aws = { + source = "hashicorp/aws" + version = "> 5" + } + + } + + required_version = ">= 1.9.5" +} diff --git a/modules/aws-backup-destination/terraform.tf b/modules/aws-backup-destination/terraform.tf new file mode 100644 index 0000000..c2ed869 --- /dev/null +++ b/modules/aws-backup-destination/terraform.tf @@ -0,0 +1,7 @@ +terraform { + required_providers { + template = { + version = "> 5" + } + } +} diff --git a/modules/aws-backup-destination/variables.tf b/modules/aws-backup-destination/variables.tf index 6684b7b..87dd9a5 100644 --- a/modules/aws-backup-destination/variables.tf +++ b/modules/aws-backup-destination/variables.tf @@ -20,12 +20,6 @@ variable "account_id" { type = string } -variable "region" { - description = "The region we should be operating in" - type = string - default = "eu-west-2" -} - variable "kms_key" { description = "The KMS key used to secure the vault" type = string diff --git a/modules/aws-backup-destination/versions.tf b/modules/aws-backup-destination/versions.tf new file mode 100644 index 0000000..49201ad --- /dev/null +++ b/modules/aws-backup-destination/versions.tf @@ -0,0 +1,12 @@ +terraform { + required_providers { + + aws = { + source = "hashicorp/aws" + version = "> 5" + } + + } + + required_version = ">= 1.9.5" +} diff --git a/modules/aws-backup-source/kms.tf b/modules/aws-backup-source/kms.tf index e8a07a2..fa9ad5f 100644 --- a/modules/aws-backup-source/kms.tf +++ b/modules/aws-backup-source/kms.tf @@ -54,3 +54,48 @@ data "aws_iam_policy_document" "backup_key_policy" { resources = ["*"] } } + + +# Now we can define the key itself +resource "aws_kms_key" "backup_notifications" { + count = var.backup_notifications_kms_key_arn == null ? 1 : 0 + description = "KMS key for AWS Backup notifications" + deletion_window_in_days = 7 + enable_key_rotation = true + policy = jsonencode({ + Version = "2012-10-17" + Statement = [ + { + Effect = "Allow" + Sid = "Enable IAM User Permissions" + Principal = { + AWS = "arn:aws:iam::${data.aws_caller_identity.current.account_id}:root" + } + Action = "kms:*" + Resource = "*" + }, + { + Effect = "Allow" + Principal = { + Service = "sns.amazonaws.com" + } + Action = ["kms:GenerateDataKey*", "kms:Decrypt"] + Resource = "*" + }, + { + Effect = "Allow" + Principal = { + Service = "backup.amazonaws.com" + } + Action = ["kms:GenerateDataKey*", "kms:Decrypt"] + Resource = "*" + }, + ] + }) +} + +resource "aws_kms_alias" "backup_notifications" { + count = var.backup_notifications_kms_key_arn == null ? 1 : 0 + target_key_id = aws_kms_key.backup_notifications.id + name = var.name_prefix != null ? "alias/${var.name_prefix}/backup-notifications" : "alias/${var.environment_name}/backup-notifications" +} diff --git a/modules/aws-backup-source/locals.tf b/modules/aws-backup-source/locals.tf index 1941381..5361309 100644 --- a/modules/aws-backup-source/locals.tf +++ b/modules/aws-backup-source/locals.tf @@ -13,5 +13,5 @@ locals { var.backup_plan_config_aurora.enable ? [aws_backup_framework.aurora[0].arn] : [] )) aurora_overrides = var.backup_plan_config_aurora.restore_testing_overrides == null ? null : jsondecode(var.backup_plan_config_aurora.restore_testing_overrides) - terraform_role_arns = var.terraform_role_arns != [] ? var.terraform_role_arns : [var.terraform_role_arn] + terraform_role_arns = length(var.terraform_role_arns) > 0 ? var.terraform_role_arns : [var.terraform_role_arn] } diff --git a/modules/aws-backup-source/sns.tf b/modules/aws-backup-source/sns.tf index f1e4286..21041f7 100644 --- a/modules/aws-backup-source/sns.tf +++ b/modules/aws-backup-source/sns.tf @@ -1,7 +1,7 @@ resource "aws_sns_topic" "backup" { count = var.notifications_target_email_address != "" ? 1 : 0 name = "${var.name_prefix}-notifications" - kms_master_key_id = var.bootstrap_kms_key_arn + kms_master_key_id = var.backup_notifications_kms_key_arn == null ? aws_kms_key.backup_notifications[0].arn : var.backup_notifications_kms_key_arn policy = data.aws_iam_policy_document.allow_backup_to_sns.json } diff --git a/modules/aws-backup-source/variables.tf b/modules/aws-backup-source/variables.tf index b2a45b1..53cec83 100644 --- a/modules/aws-backup-source/variables.tf +++ b/modules/aws-backup-source/variables.tf @@ -14,9 +14,10 @@ variable "notifications_target_email_address" { default = "" } -variable "bootstrap_kms_key_arn" { +variable "backup_notifications_kms_key_arn" { description = "The ARN of the bootstrap KMS key used for encryption at rest of the SNS topic." type = string + default = null } variable "reports_bucket" { diff --git a/scripts/check-secrets.sh b/scripts/check-secrets.sh new file mode 100755 index 0000000..551bc48 --- /dev/null +++ b/scripts/check-secrets.sh @@ -0,0 +1,39 @@ +#!/usr/bin/env bash +set -euo pipefail + +scan_type=${1-pre-commit} + +if ! git secrets -- 1> /dev/null; then + echo "git secrets is not installed" + echo "the git-secrets file needs to be in your PATH" + echo "to install:" + echo " wget https://raw.githubusercontent.com/awslabs/git-secrets/refs/heads/master/git-secrets -O ~/.local/bin/git-secrets && chmod +x ~/.local/bin/git-secrets" + exit 1 +fi + +echo "scan type: ${scan_type}" + +git secrets --register-aws +if [[ -e ./.gitdisallowed ]]; then + git secrets --add-provider -- grep -Ev '^(#.*|\s*$)' .gitdisallowed || true + git secrets --add --allowed '^\.gitdisallowed:[0-9]+:.*' || true +fi + +if { [ "${scan_type}" == "unstaged" ]; } ; then + echo "scanning staged and unstaged files for secrets" + git secrets --scan --recursive + git secrets --scan --untracked +elif { [ "${scan_type}" == "staged" ]; } ; then + echo "scanning staged files for secrets" + git secrets --scan --recursive +elif { [ "${scan_type}" == "commit-msg" ]; } ; then + echo "checking commit msg for secrets" + git secrets --commit_msg_hook -- "${2}" +elif { [ "${scan_type}" == "prep-commit-msg" ]; } ; then + echo "checking commit msg for secrets" + git secrets --prepare_commit_msg_hook -- "${2}" +else + echo "scanning for secrets" + # if staged files exist, this will scan staged files only, otherwise normal scan + git secrets --pre_commit_hook +fi