diff --git a/.github/workflows/_deploy.yml b/.github/workflows/_deploy.yml index d3b913541..579513d97 100644 --- a/.github/workflows/_deploy.yml +++ b/.github/workflows/_deploy.yml @@ -47,7 +47,7 @@ jobs: branch_name=${branch_name#*refs/tags/} echo "branch_name=${branch_name}" >> $GITHUB_OUTPUT - # BACKUPS_LOGIC (Source account needs layers building) + # Source account for immutable backups needs layers building build: runs-on: [self-hosted, ci] needs: get-branch-from-workflow-file @@ -62,14 +62,14 @@ jobs: save-to-cache: "true" restore-from-cache: "false" cache-suffix: ${{ env.CACHE_NAME }} - - if: ${{ env.SCOPE != 'per_workspace' && inputs.account == 'dev'}} + - if: ${{ env.SCOPE != 'per_workspace' && (inputs.account == 'prod' || inputs.account == 'dev') }} uses: ./.github/actions/make/ with: command: build save-to-cache: "true" restore-from-cache: "false" cache-suffix: ${{ env.CACHE_NAME }} - - if: ${{ env.SCOPE != 'per_workspace' && inputs.account != 'dev'}} + - if: ${{ env.SCOPE != 'per_workspace' && (inputs.account != 'prod' && inputs.account != 'dev' )}} uses: ./.github/actions/make/ with: command: poetry--update diff --git a/CHANGELOG.md b/CHANGELOG.md index afb42c850..71508b1de 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ # Changelog +## 2025-04-02 +- [PI-872] Enable backups in PROD +- [PI-873] Update support policy to have 2 policies + ## 2025-04-01 - [PI-848] Add info box to swaager explaining prodID usage in non-prod envs - [PI-870] Sonarcloud fixes diff --git a/VERSION b/VERSION index 38ad6010f..a37f64a19 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -2025.04.01 +2025.04.02 diff --git a/changelog/2025-04-02.md b/changelog/2025-04-02.md new file mode 100644 index 000000000..9c8c0129a --- /dev/null +++ b/changelog/2025-04-02.md @@ -0,0 +1,2 @@ +- [PI-872] Enable backups in PROD +- [PI-873] Update support policy to have 2 policies diff --git a/infrastructure/terraform/per_account/backups/aws-backups.tf b/infrastructure/terraform/per_account/backups/aws-backups.tf index c61586b1a..efd8ccc21 100644 --- a/infrastructure/terraform/per_account/backups/aws-backups.tf +++ b/infrastructure/terraform/per_account/backups/aws-backups.tf @@ -1,9 +1,17 @@ -data "aws_secretsmanager_secret" "source_account_id" { - name = "backups-source-account-id" +data "aws_secretsmanager_secret" "source_account_id_prod" { + name = "backups-source-account-id-prod" } -data "aws_secretsmanager_secret_version" "source_account_id" { - secret_id = data.aws_secretsmanager_secret.source_account_id.id +data "aws_secretsmanager_secret_version" "source_account_id_prod" { + secret_id = data.aws_secretsmanager_secret.source_account_id_prod.id +} + +data "aws_secretsmanager_secret" "source_account_id_dev" { + name = "backups-source-account-id-dev" +} + +data "aws_secretsmanager_secret_version" "source_account_id_dev" { + secret_id = data.aws_secretsmanager_secret.source_account_id_dev.id } @@ -15,12 +23,33 @@ resource "aws_kms_key" "destination_backup_key" { enable_key_rotation = true } -module "destination" { +module "destination_prod" { + source = "../modules/aws-backup-destination" + + source_account_name = "prod" # please note that the assigned value would be the prefix in aws_backup_vault.vault.name - change to dev/prod + account_id = var.assume_account + source_account_id = data.aws_secretsmanager_secret_version.source_account_id_prod.secret_string + kms_key = aws_kms_key.destination_backup_key.arn + enable_vault_protection = false +} + +### +# Destination vault ARN output +### + +output "destination_vault_arn_prod" { + # The ARN of the backup vault in the destination account is needed by + # the source account to copy backups into it. + value = module.destination_prod.vault_arn +} + + +module "destination_dev" { source = "../modules/aws-backup-destination" - source_account_name = "dev" # please note that the assigned value would be the prefix in aws_backup_vault.vault.name - change to dev/prod BACKUPS_LOGIC + source_account_name = "dev" # please note that the assigned value would be the prefix in aws_backup_vault.vault.name - change to dev/prod account_id = var.assume_account - source_account_id = data.aws_secretsmanager_secret_version.source_account_id.secret_string + source_account_id = data.aws_secretsmanager_secret_version.source_account_id_dev.secret_string kms_key = aws_kms_key.destination_backup_key.arn enable_vault_protection = false } @@ -29,8 +58,8 @@ module "destination" { # Destination vault ARN output ### -output "destination_vault_arn" { +output "destination_vault_arn_dev" { # The ARN of the backup vault in the destination account is needed by # the source account to copy backups into it. - value = module.destination.vault_arn + value = module.destination_dev.vault_arn } diff --git a/infrastructure/terraform/per_account/backups/parameters/main.tf b/infrastructure/terraform/per_account/backups/parameters/main.tf index 476be9dae..70b88646d 100644 --- a/infrastructure/terraform/per_account/backups/parameters/main.tf +++ b/infrastructure/terraform/per_account/backups/parameters/main.tf @@ -28,8 +28,13 @@ JSON } -resource "aws_secretsmanager_secret" "source-account-id-for-backup" { - name = "${terraform.workspace}-source-account-id" +resource "aws_secretsmanager_secret" "source-account-id-for-backup-prod" { + name = "${terraform.workspace}-source-account-id-prod" + description = "ID of the account we want to backup" +} + +resource "aws_secretsmanager_secret" "source-account-id-for-backup-dev" { + name = "${terraform.workspace}-source-account-id-dev" description = "ID of the account we want to backup" } diff --git a/infrastructure/terraform/per_account/dev/aws-backups.tf b/infrastructure/terraform/per_account/dev/aws-backups.tf index fa82a4270..5cacf0901 100644 --- a/infrastructure/terraform/per_account/dev/aws-backups.tf +++ b/infrastructure/terraform/per_account/dev/aws-backups.tf @@ -1,4 +1,3 @@ -# BACKUPS_LOGIC data "aws_secretsmanager_secret" "destination_vault_arn" { name = "destination_vault_arn" } diff --git a/infrastructure/terraform/per_account/dev/main.tf b/infrastructure/terraform/per_account/dev/main.tf index d61098164..1c11dd945 100644 --- a/infrastructure/terraform/per_account/dev/main.tf +++ b/infrastructure/terraform/per_account/dev/main.tf @@ -96,7 +96,6 @@ resource "aws_route53_zone" "dev-ns" { name = "api.cpm.dev.national.nhs.uk" } -# BACKUPS_LOGIC module "layers" { for_each = toset(var.layers) source = "../../modules/api_worker/api_layer" @@ -106,7 +105,6 @@ module "layers" { source_path = "${path.module}/../../../../src/layers/${each.key}/dist/${each.key}.zip" } -# BACKUPS_LOGIC module "third_party_layers" { for_each = toset(var.third_party_layers) source = "../../modules/api_worker/api_layer" diff --git a/infrastructure/terraform/per_account/dev/parameters/main.tf b/infrastructure/terraform/per_account/dev/parameters/main.tf index 04553c6ed..447d28616 100644 --- a/infrastructure/terraform/per_account/dev/parameters/main.tf +++ b/infrastructure/terraform/per_account/dev/parameters/main.tf @@ -55,8 +55,8 @@ resource "aws_secretsmanager_secret" "ldap-changelog-password" { name = "${terraform.workspace}-ldap-changelog-password" } -resource "aws_secretsmanager_secret" "etl_notify_slack_webhook_url" { - name = "${terraform.workspace}--etl-notify-slack-webhook-url" +resource "aws_secretsmanager_secret" "notify_slack_webhook_url" { + name = "${terraform.workspace}-notify-slack-webhook-url" } resource "aws_secretsmanager_secret" "apigee-app-client-info" { @@ -67,12 +67,10 @@ resource "aws_secretsmanager_secret" "external-id" { name = "${terraform.workspace}-external-id" } -# BACKUPS_LOGIC resource "aws_secretsmanager_secret" "destination_vault_arn" { name = "destination_vault_arn" } -# BACKUPS_LOGIC resource "aws_secretsmanager_secret" "destination_account_id" { name = "destination_account_id" } diff --git a/infrastructure/terraform/per_account/dev/parameters/vars.tf b/infrastructure/terraform/per_account/dev/parameters/vars.tf index 8750b8aa2..8a6a64855 100644 --- a/infrastructure/terraform/per_account/dev/parameters/vars.tf +++ b/infrastructure/terraform/per_account/dev/parameters/vars.tf @@ -30,12 +30,10 @@ variable "workspace_type" { default = "PERSISTENT" } -# BACKUPS_LOGIC variable "layers" { type = list(string) } -# BACKUPS_LOGIC variable "third_party_layers" { type = list(string) } diff --git a/infrastructure/terraform/per_account/dev/vars.tf b/infrastructure/terraform/per_account/dev/vars.tf index b4e4bc352..ec37790ec 100644 --- a/infrastructure/terraform/per_account/dev/vars.tf +++ b/infrastructure/terraform/per_account/dev/vars.tf @@ -29,17 +29,14 @@ variable "budget_limit" { type = string } -# BACKUPS_LOGIC variable "python_version" { default = "python3.12" } -# BACKUPS_LOGIC variable "layers" { type = list(string) } -# BACKUPS_LOGIC variable "third_party_layers" { type = list(string) } diff --git a/infrastructure/terraform/per_account/int/parameters/main.tf b/infrastructure/terraform/per_account/int/parameters/main.tf index 4784a7f25..b29608361 100644 --- a/infrastructure/terraform/per_account/int/parameters/main.tf +++ b/infrastructure/terraform/per_account/int/parameters/main.tf @@ -39,10 +39,6 @@ resource "aws_secretsmanager_secret" "apigee-app-key" { name = "${terraform.workspace}-apigee-app-key" } -resource "aws_secretsmanager_secret" "etl_notify_slack_webhook_url" { - name = "${terraform.workspace}--etl-notify-slack-webhook-url" -} - resource "aws_secretsmanager_secret" "sds-hscn-endpoint" { name = "${terraform.workspace}-sds-hscn-endpoint" } diff --git a/infrastructure/terraform/per_account/modules/notify/main.tf b/infrastructure/terraform/per_account/modules/notify/main.tf index d89d6b458..f8d428db7 100644 --- a/infrastructure/terraform/per_account/modules/notify/main.tf +++ b/infrastructure/terraform/per_account/modules/notify/main.tf @@ -1,5 +1,5 @@ data "aws_secretsmanager_secret" "slack_webhook_url" { - name = "${var.environment}--etl-notify-slack-webhook-url" + name = "${var.environment}-notify-slack-webhook-url" } data "aws_secretsmanager_secret_version" "slack_webhook_url" { diff --git a/infrastructure/terraform/per_account/prod/aws-backups.tf b/infrastructure/terraform/per_account/prod/aws-backups.tf new file mode 100644 index 000000000..5cacf0901 --- /dev/null +++ b/infrastructure/terraform/per_account/prod/aws-backups.tf @@ -0,0 +1,155 @@ +data "aws_secretsmanager_secret" "destination_vault_arn" { + name = "destination_vault_arn" +} + +data "aws_secretsmanager_secret_version" "destination_vault_arn" { + secret_id = data.aws_secretsmanager_secret.destination_vault_arn.id +} + +data "aws_secretsmanager_secret" "destination_account_id" { + name = "destination_account_id" +} + +data "aws_secretsmanager_secret_version" "destination_account_id" { + secret_id = data.aws_secretsmanager_secret.destination_account_id.id +} + +# 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. + +resource "aws_s3_bucket" "backup_reports" { + bucket_prefix = "${local.project}-backup-reports" +} + +# Now we have to configure access to the report bucket. + +resource "aws_s3_bucket_ownership_controls" "backup_reports" { + bucket = aws_s3_bucket.backup_reports.id + rule { + object_ownership = "BucketOwnerPreferred" + } +} + +resource "aws_s3_bucket_acl" "backup_reports" { + depends_on = [aws_s3_bucket_ownership_controls.backup_reports] + + bucket = aws_s3_bucket.backup_reports.id + acl = "private" +} + +resource "aws_s3_bucket_policy" "backup_reports_policy" { + bucket = aws_s3_bucket.backup_reports.id + policy = jsonencode({ + Version = "2012-10-17", + Statement = [ + { + Effect = "Allow", + Principal = { + AWS = "arn:aws:iam::${var.assume_account}:role/aws-service-role/reports.backup.amazonaws.com/AWSServiceRoleForBackupReports" + }, + Action = "s3:PutObject", + Resource = "${aws_s3_bucket.backup_reports.arn}/*", + Condition = { + StringEquals = { + "s3:x-amz-acl" = "bucket-owner-full-control" + } + } + } + ] + }) +} + +# We need a key for the SNS topic that will be used for notifications from AWS Backup. This key +# will be used to encrypt the messages sent to the topic before they are sent to the subscribers, +# but isn't needed by the recipients of the messages. + + +# 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::${var.assume_account}: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" { + name = "alias/key-for-backup-notify" + target_key_id = aws_kms_key.backup_notifications.key_id +} + +# 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 = data.aws_secretsmanager_secret_version.destination_account_id.secret_string + backup_copy_vault_arn = data.aws_secretsmanager_secret_version.destination_vault_arn.secret_string + environment_name = var.environment + bootstrap_kms_key_arn = aws_kms_key.backup_notifications.arn + project_name = local.project + reports_bucket = aws_s3_bucket.backup_reports.bucket + terraform_role_arn = "arn:aws:iam::${var.assume_account}:role/${var.assume_role}" + python_version = var.python_version + notify_lambda_arn = module.notify.arn + + backup_plan_config_dynamodb = { + "compliance_resource_types" : [ + "DynamoDB" + ], + "rules" : [ + { + "copy_action" : { + "delete_after" : 31 + }, + "lifecycle" : { + "delete_after" : 4 + }, + "name" : "daily_kept_for_2_days", + "schedule" : "cron(0 20 * * ? *)" + } + ], + "enable" : true, + "selection_tag" : "NHSE-Enable-Backup" + } +} + + +module "notify" { + source = "../modules/notify/" + + assume_account = var.assume_account + project_name = local.project + python_version = var.python_version + environment = var.environment + event_layer_arn = element([for instance in module.layers : instance if instance.name == "event"], 0).layer_arn + third_party_layer_arn = element([for instance in module.third_party_layers : instance if instance.name == "third_party_core"], 0).layer_arn +} diff --git a/infrastructure/terraform/per_account/prod/main.tf b/infrastructure/terraform/per_account/prod/main.tf index ea9d23fcd..9b3101eb7 100644 --- a/infrastructure/terraform/per_account/prod/main.tf +++ b/infrastructure/terraform/per_account/prod/main.tf @@ -96,69 +96,20 @@ resource "aws_route53_zone" "prod-ns" { name = "api.cpm.national.nhs.uk" } -module "snapshot_bucket" { - source = "terraform-aws-modules/s3-bucket/aws" - version = "3.15.2" - bucket = "${local.project}--${replace(terraform.workspace, "_", "-")}--snapshot" - versioning = { - enabled = true - } - tags = { - Name = "${local.project}--${replace(terraform.workspace, "_", "-")}--snapshot" - } +module "layers" { + for_each = toset(var.layers) + source = "../../modules/api_worker/api_layer" + name = each.key + python_version = var.python_version + layer_name = "${local.project}--${replace(terraform.workspace, "_", "-")}--${replace(each.key, "_", "-")}" + source_path = "${path.module}/../../../../src/layers/${each.key}/dist/${each.key}.zip" } -resource "aws_s3_bucket_policy" "snapshot_bucket_policy" { - bucket = module.snapshot_bucket.s3_bucket_id - policy = jsonencode({ - Version = "2012-10-17", - Statement = [ - { - Sid = "AWSAccessLogDeliveryWrite", - Effect = "Allow", - Principal = { - Service = "logging.s3.amazonaws.com" - }, - Action = "s3:PutObject", - Resource = "${module.snapshot_bucket.s3_bucket_arn}/*" - }, - { - Sid = "AWSAccessLogDeliveryAclCheck", - Effect = "Allow", - Principal = { - Service = "logging.s3.amazonaws.com" - }, - Action = "s3:GetBucketAcl", - Resource = "${module.snapshot_bucket.s3_bucket_arn}" - }, - { - Sid = "denyInsecureTransport", - Effect = "Deny", - Principal = "*", - Action = "s3:*", - Resource = [ - "${module.snapshot_bucket.s3_bucket_arn}", - "${module.snapshot_bucket.s3_bucket_arn}/*" - ], - Condition = { - Bool = { - "aws:SecureTransport" = "false" - } - } - }, - { - Sid = "AllowDynamoDBExport", - Effect = "Allow", - Principal = { - Service = "dynamodb.amazonaws.com" - }, - Action = [ - "s3:PutObject", - "s3:AbortMultipartUpload", - "s3:ListMultipartUploadParts" - ], - Resource = "${module.snapshot_bucket.s3_bucket_arn}/*" - } - ] - }) +module "third_party_layers" { + for_each = toset(var.third_party_layers) + source = "../../modules/api_worker/api_layer" + name = each.key + python_version = var.python_version + layer_name = "${local.project}--${replace(terraform.workspace, "_", "-")}--${replace(each.key, "_", "-")}" + source_path = "${path.module}/../../../../src/layers/third_party/dist/${each.key}.zip" } diff --git a/infrastructure/terraform/per_account/prod/parameters/main.tf b/infrastructure/terraform/per_account/prod/parameters/main.tf index 4784a7f25..70f0d6e68 100644 --- a/infrastructure/terraform/per_account/prod/parameters/main.tf +++ b/infrastructure/terraform/per_account/prod/parameters/main.tf @@ -39,10 +39,6 @@ resource "aws_secretsmanager_secret" "apigee-app-key" { name = "${terraform.workspace}-apigee-app-key" } -resource "aws_secretsmanager_secret" "etl_notify_slack_webhook_url" { - name = "${terraform.workspace}--etl-notify-slack-webhook-url" -} - resource "aws_secretsmanager_secret" "sds-hscn-endpoint" { name = "${terraform.workspace}-sds-hscn-endpoint" } @@ -59,6 +55,18 @@ resource "aws_secretsmanager_secret" "ldap-changelog-password" { name = "${terraform.workspace}-ldap-changelog-password" } +resource "aws_secretsmanager_secret" "notify_slack_webhook_url" { + name = "${terraform.workspace}-notify-slack-webhook-url" +} + resource "aws_secretsmanager_secret" "external-id" { name = "${terraform.workspace}-external-id" } + +resource "aws_secretsmanager_secret" "destination_vault_arn" { + name = "destination_vault_arn" +} + +resource "aws_secretsmanager_secret" "destination_account_id" { + name = "destination_account_id" +} diff --git a/infrastructure/terraform/per_account/prod/parameters/vars.tf b/infrastructure/terraform/per_account/prod/parameters/vars.tf index 8d34250b8..8a6a64855 100644 --- a/infrastructure/terraform/per_account/prod/parameters/vars.tf +++ b/infrastructure/terraform/per_account/prod/parameters/vars.tf @@ -29,3 +29,11 @@ variable "workspace_type" { type = string default = "PERSISTENT" } + +variable "layers" { + type = list(string) +} + +variable "third_party_layers" { + type = list(string) +} diff --git a/infrastructure/terraform/per_account/prod/vars.tf b/infrastructure/terraform/per_account/prod/vars.tf index 3c3029cd3..04668c99e 100644 --- a/infrastructure/terraform/per_account/prod/vars.tf +++ b/infrastructure/terraform/per_account/prod/vars.tf @@ -28,3 +28,15 @@ variable "budget_limit" { default = "1300" type = string } + +variable "python_version" { + default = "python3.12" +} + +variable "layers" { + type = list(string) +} + +variable "third_party_layers" { + type = list(string) +} diff --git a/infrastructure/terraform/per_account/qa/parameters/main.tf b/infrastructure/terraform/per_account/qa/parameters/main.tf index 4784a7f25..b29608361 100644 --- a/infrastructure/terraform/per_account/qa/parameters/main.tf +++ b/infrastructure/terraform/per_account/qa/parameters/main.tf @@ -39,10 +39,6 @@ resource "aws_secretsmanager_secret" "apigee-app-key" { name = "${terraform.workspace}-apigee-app-key" } -resource "aws_secretsmanager_secret" "etl_notify_slack_webhook_url" { - name = "${terraform.workspace}--etl-notify-slack-webhook-url" -} - resource "aws_secretsmanager_secret" "sds-hscn-endpoint" { name = "${terraform.workspace}-sds-hscn-endpoint" } diff --git a/infrastructure/terraform/per_account/ref/parameters/main.tf b/infrastructure/terraform/per_account/ref/parameters/main.tf index 2ddca03b6..78e3eac6c 100644 --- a/infrastructure/terraform/per_account/ref/parameters/main.tf +++ b/infrastructure/terraform/per_account/ref/parameters/main.tf @@ -39,10 +39,6 @@ resource "aws_secretsmanager_secret" "apigee-app-key" { name = "${terraform.workspace}-apigee-app-key" } -resource "aws_secretsmanager_secret" "etl_notify_slack_webhook_url" { - name = "${terraform.workspace}--etl-notify-slack-webhook-url" -} - resource "aws_secretsmanager_secret" "sds-hscn-endpoint" { name = "${terraform.workspace}-sds-hscn-endpoint" } diff --git a/infrastructure/terraform/per_workspace/modules/api_storage/dynamodb.tf b/infrastructure/terraform/per_workspace/modules/api_storage/dynamodb.tf index c2900c0ea..1db0f912b 100644 --- a/infrastructure/terraform/per_workspace/modules/api_storage/dynamodb.tf +++ b/infrastructure/terraform/per_workspace/modules/api_storage/dynamodb.tf @@ -19,7 +19,7 @@ module "dynamodb_table" { { Name = var.name }, - var.environment == "dev" ? { + var.environment == "prod" ? { "NHSE-Enable-Backup" = "True" } : {} ) diff --git a/pyproject.toml b/pyproject.toml index 370da1267..a03cbe126 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "connecting-party-manager" -version = "2025.04.01" +version = "2025.04.02" description = "Repository for the Connecting Party Manager API and related services" authors = ["NHS England"] license = "LICENSE.md" diff --git a/scripts/infrastructure/policies/deployment2-policy.json b/scripts/infrastructure/policies/deployment2-policy.json index 51f30f802..053db4c79 100644 --- a/scripts/infrastructure/policies/deployment2-policy.json +++ b/scripts/infrastructure/policies/deployment2-policy.json @@ -159,7 +159,7 @@ "arn:aws:secretsmanager:*:${ACCOUNT_ID}:secret:*-apigee-credentials-*", "arn:aws:secretsmanager:*:${ACCOUNT_ID}:secret:*-apigee-cpm-apikey-*", "arn:aws:secretsmanager:*:${ACCOUNT_ID}:secret:*-apigee-app-key-*", - "arn:aws:secretsmanager:*:${ACCOUNT_ID}:secret:*-etl-notify-slack-webhook-url-*" + "arn:aws:secretsmanager:*:${ACCOUNT_ID}:secret:*-notify-slack-webhook-url-*" ] }, { diff --git a/scripts/infrastructure/policies/development2-policy.json b/scripts/infrastructure/policies/development2-policy.json index 51f30f802..053db4c79 100644 --- a/scripts/infrastructure/policies/development2-policy.json +++ b/scripts/infrastructure/policies/development2-policy.json @@ -159,7 +159,7 @@ "arn:aws:secretsmanager:*:${ACCOUNT_ID}:secret:*-apigee-credentials-*", "arn:aws:secretsmanager:*:${ACCOUNT_ID}:secret:*-apigee-cpm-apikey-*", "arn:aws:secretsmanager:*:${ACCOUNT_ID}:secret:*-apigee-app-key-*", - "arn:aws:secretsmanager:*:${ACCOUNT_ID}:secret:*-etl-notify-slack-webhook-url-*" + "arn:aws:secretsmanager:*:${ACCOUNT_ID}:secret:*-notify-slack-webhook-url-*" ] }, { diff --git a/scripts/infrastructure/policies/support-policy.json b/scripts/infrastructure/policies/support-policy.json index 94135b6fb..21972429f 100644 --- a/scripts/infrastructure/policies/support-policy.json +++ b/scripts/infrastructure/policies/support-policy.json @@ -252,30 +252,6 @@ "apigateway:POST" ], "Resource": ["arn:aws:apigateway:*::/*"] - }, - { - "Sid": "CertificateSupportPermissions", - "Effect": "Allow", - "Action": ["acm:ListTagsForCertificate", "acm:DescribeCertificate"], - "Resource": ["arn:aws:acm:*:{$ACCOUNT_ID}:certificate*"] - }, - { - "Sid": "BillingSupportPermissions", - "Effect": "Allow", - "Action": [ - "ce:GetCostAndUsage", - "ce:GetCostForcast", - "ce:GetAnomalies", - "ce:GetTags", - "budgets:ViewBudget" - ], - "Resource": [ - "arn:aws:ce:*:{$ACCOUNT_ID}:GetCostAndUsage", - "arn:aws:ce:*:{$ACCOUNT_ID}:GetCostForecast", - "arn:aws:ce:*:{$ACCOUNT_ID}:GetTags", - "arn:aws:ce:*:{$ACCOUNT_ID}:anomalymonitor/*", - "arn:aws:budgets::{$ACCOUNT_ID}:budget/*" - ] } ] } diff --git a/scripts/infrastructure/policies/support1-policy.json b/scripts/infrastructure/policies/support1-policy.json new file mode 100644 index 000000000..21972429f --- /dev/null +++ b/scripts/infrastructure/policies/support1-policy.json @@ -0,0 +1,257 @@ +{ + "Version": "2012-10-17", + "Statement": [ + { + "Sid": "IamSupportPermissions", + "Effect": "Allow", + "Action": [ + "iam:GetPolicyVersion", + "iam:ListRoleTags", + "iam:ListPolicyTags", + "iam:ListRolePolicies", + "iam:GetRole", + "iam:GetPolicy", + "iam:ListGroupPolicies", + "iam:ListPolicyVersions", + "iam:GetGroupPolicy", + "iam:GetRolePolicy", + "iam:ListPolicies", + "iam:ListRoles", + "iam:ListGroups", + "iam:ListUsers", + "iam:ListAttachedRolePolicies" + ], + "Resource": [ + "arn:aws:iam::${ACCOUNT_ID}:role/*", + "arn:aws:iam::${ACCOUNT_ID}:group/*", + "arn:aws:iam::${ACCOUNT_ID}:policy/*" + ] + }, + { + "Sid": "IamSupportPermissionsAll", + "Effect": "Allow", + "Action": ["iam:ListAccountAliases", "iam:GetAccountSummary"], + "Resource": ["*"] + }, + { + "Sid": "DynamoDbSupportPermissions", + "Effect": "Allow", + "Action": [ + "dynamodb:BatchGetItem", + "dynamodb:UpdateTimeToLive", + "dynamodb:ConditionCheckItem", + "dynamodb:PutItem", + "dynamodb:DeleteItem", + "dynamodb:DescribeContributorInsights", + "dynamodb:Scan", + "dynamodb:ListTagsOfResource", + "dynamodb:Query", + "dynamodb:UpdateItem", + "dynamodb:CreateBackup", + "dynamodb:DescribeTimeToLive", + "dynamodb:PartiQLSelect", + "dynamodb:DescribeTable", + "dynamodb:GetItem", + "dynamodb:DescribeContinuousBackups", + "dynamodb:DescribeKinesisStreamingDestination", + "dynamodb:DescribeTableReplicaAutoScaling", + "dynamodb:ListContributorInsights", + "dynamodb:DescribeReservedCapacityOfferings", + "dynamodb:ListGlobalTables", + "dynamodb:ListTables", + "dynamodb:DescribeReservedCapacity", + "dynamodb:ListBackups", + "dynamodb:ListImports", + "dynamodb:DescribeLimits", + "dynamodb:DescribeEndpoints", + "dynamodb:ListExports", + "dynamodb:ListStreams" + ], + "Resource": [ + "arn:aws:dynamodb:*:${ACCOUNT_ID}:table/*", + "arn:aws:dynamodb::${ACCOUNT_ID}:global-table/*" + ] + }, + { + "Sid": "CloudFormationSupportPermissions", + "Effect": "Allow", + "Action": [ + "cloudformation:DescribeStacks", + "cloudformation:ListStackResources", + "cloudformation:ListStacks" + ], + "Resource": ["*"] + }, + { + "Sid": "TagSupportPermissions", + "Effect": "Allow", + "Action": [ + "tag:GetResources", + "tag:GetTagValues", + "tag:GetTagKeys", + "tag:GetComplianceSummary", + "tag:DescribeReportCreation" + ], + "Resource": "*" + }, + { + "Sid": "LambdaSupportPermissions", + "Effect": "Allow", + "Action": [ + "lambda:ListAliases", + "lambda:ListFunctionUrlConfigs", + "lambda:ListTags", + "lambda:ListVersionsByFunction", + "lambda:ListLayerVersions", + "lambda:GetAlias", + "lambda:GetFunction", + "lambda:GetLayerVersion", + "lambda:GetFunctionEventInvokeConfig", + "lambda:ListProvisionedConcurrencyConfigs", + "lambda:GetFunctionUrlConfig" + ], + "Resource": [ + "arn:aws:lambda:*:${ACCOUNT_ID}:function:*", + "arn:aws:lambda:*:${ACCOUNT_ID}:layer:*:*" + ] + }, + { + "Sid": "LambdaSupportAllPermissions", + "Effect": "Allow", + "Action": [ + "lambda:ListFunctions", + "lambda:ListLayers", + "lambda:GetAccountSettings" + ], + "Resource": ["*"] + }, + { + "Sid": "ResourceGroupSupportPermissions", + "Effect": "Allow", + "Action": [ + "resource-groups:GetAccountSettings", + "resource-groups:GetGroupQuery", + "resource-groups:GetTags", + "resource-groups:GetGroup", + "resource-groups:GetGroupConfiguration", + "resource-groups:ListGroupResources", + "resource-groups:SearchResources", + "resource-groups:ListGroups" + ], + "Resource": ["*"] + }, + { + "Sid": "CloudwatchSupportPermissions", + "Effect": "Allow", + "Action": [ + "cloudwatch:ListTagsForResource", + "cloudwatch:ListMetrics", + "cloudwatch:ListMetricStreams", + "cloudwatch:DescribeAlarmHistory", + "cloudwatch:DescribeAlarms", + "cloudwatch:DescribeAlarmsForMetric", + "cloudwatch:DescribeAnomalyDetectors", + "cloudwatch:DescribeInsightRules", + "cloudwatch:GetDashboard", + "cloudwatch:GetInsightRuleReport", + "cloudwatch:GetMetricData", + "cloudwatch:GetMetricWidgetImage", + "cloudwatch:GetMetricStream", + "cloudwatch:GetMetricStatistics", + "cloudwatch:GenerateQuery", + "cloudwatch:ListManagedInsightRules", + "cloudwatch:ListDashboards" + ], + "Resource": [ + "arn:aws:cloudwatch:*:${ACCOUNT_ID}:alarm:*", + "arn:aws:cloudwatch::${ACCOUNT_ID}:dashboard/*", + "arn:aws:cloudwatch:*:${ACCOUNT_ID}:insight-rule/*", + "arn:aws:cloudwatch:*:${ACCOUNT_ID}:metric-stream/*" + ] + }, + { + "Sid": "LogSupportPermissions", + "Effect": "Allow", + "Action": [ + "logs:ListAnomalies", + "logs:GetDelivery", + "logs:GetDeliverySource", + "logs:GetLogEvents", + "logs:GetDeliveryDestinationPolicy", + "logs:GetDeliveryDestination", + "logs:GetLogAnomalyDetector", + "logs:ListTagsForResource", + "logs:GetLogDelivery", + "logs:ListLogDeliveries", + "logs:StartLiveTail", + "logs:StopLiveTail", + "logs:DescribeQueryDefinitions", + "logs:DescribeResourcePolicies", + "logs:DescribeDestinations", + "logs:DescribeQueries", + "logs:DescribeLogGroups", + "logs:DescribeAccountPolicies", + "logs:DescribeDeliverySources", + "logs:StopQuery", + "logs:TestMetricFilter", + "logs:DescribeDeliveryDestinations", + "logs:DescribeExportTasks", + "logs:DescribeDeliveries", + "logs:GetDataProtectionPolicy", + "logs:GetLogRecord", + "logs:DescribeSubscriptionFilters", + "logs:StartQuery", + "logs:DescribeMetricFilters", + "logs:FilterLogEvents", + "logs:Unmask", + "logs:ListTagsForResource", + "logs:GetQueryResults", + "logs:ListTagsLogGroup", + "logs:DescribeLogStreams", + "logs:ListLogAnomalyDetectors", + "logs:GetLogGroupFields" + ], + "Resource": [ + "arn:aws:logs:*:${ACCOUNT_ID}:anomaly-detector:*", + "arn:aws:logs:*:${ACCOUNT_ID}:delivery:*", + "arn:aws:logs:*:${ACCOUNT_ID}:delivery-destination:*", + "arn:aws:logs:*:${ACCOUNT_ID}:delivery-source:*", + "arn:aws:logs:*:${ACCOUNT_ID}:destination:*", + "arn:aws:logs:*:${ACCOUNT_ID}:log-group:*", + "arn:aws:logs:*:${ACCOUNT_ID}:log-group:*:log-stream:*" + ] + }, + { + "Sid": "KMSSupportPermissions", + "Effect": "Allow", + "Action": [ + "kms:DescribeKey", + "kms:Decrypt", + "kms:DisableKey", + "kms:DisableKeyRotation", + "kms:EnableKey", + "kms:EnableKeyRotation", + "kms:GetKeyPolicy", + "kms:GetKeyRotationStatus", + "kms:ListAliases", + "kms:ListResourceTags", + "kms:ScheduleKeyDeletion", + "kms:UpdateAlias", + "kms:UpdateKeyDescription" + ], + "Resource": ["*"] + }, + { + "Sid": "APIGatewaySupportPermissions", + "Effect": "Allow", + "Action": [ + "apigateway:DELETE", + "apigateway:GET", + "apigateway:PATCH", + "apigateway:PUT", + "apigateway:POST" + ], + "Resource": ["arn:aws:apigateway:*::/*"] + } + ] +} diff --git a/scripts/infrastructure/policies/support2-policy.json b/scripts/infrastructure/policies/support2-policy.json new file mode 100644 index 000000000..ede9e9e5d --- /dev/null +++ b/scripts/infrastructure/policies/support2-policy.json @@ -0,0 +1,28 @@ +{ + "Version": "2012-10-17", + "Statement": [ + { + "Sid": "CertificateSupportPermissions", + "Effect": "Allow", + "Action": ["acm:ListTagsForCertificate", "acm:DescribeCertificate"], + "Resource": ["*"] + }, + { + "Sid": "BudgetSupportPermissions", + "Effect": "Allow", + "Action": ["budgets:ViewBudget"], + "Resource": ["*"] + }, + { + "Sid": "BillingSupportPermissions", + "Effect": "Allow", + "Action": [ + "ce:GetCostAndUsage", + "ce:GetCostForecast", + "ce:GetAnomalies", + "ce:GetTags" + ], + "Resource": ["*"] + } + ] +} diff --git a/scripts/infrastructure/roles.mk b/scripts/infrastructure/roles.mk index 25e453f9b..99dc84834 100644 --- a/scripts/infrastructure/roles.mk +++ b/scripts/infrastructure/roles.mk @@ -12,6 +12,12 @@ manage--non-mgmt-development-policies: aws--login ## Create or update IAM Polici manage--mgmt-development-policies: aws--login ## Create or update IAM Policies @AWS_ACCESS_KEY_ID=$(AWS_ACCESS_KEY_ID) AWS_SECRET_ACCESS_KEY=$(AWS_SECRET_ACCESS_KEY) AWS_SESSION_TOKEN=$(AWS_SESSION_TOKEN) bash $(PATH_TO_INFRASTRUCTURE)/roles/manage-mgmt-aws-development-policies.sh +manage--non-mgmt-support-policies: aws--login ## Create or update IAM Policies + @AWS_ACCESS_KEY_ID=$(AWS_ACCESS_KEY_ID) AWS_SECRET_ACCESS_KEY=$(AWS_SECRET_ACCESS_KEY) AWS_SESSION_TOKEN=$(AWS_SESSION_TOKEN) bash $(PATH_TO_INFRASTRUCTURE)/roles/manage-non-mgmt-aws-support-policies.sh + +manage--mgmt-support-policies: aws--login ## Create or update IAM Policies + @AWS_ACCESS_KEY_ID=$(AWS_ACCESS_KEY_ID) AWS_SECRET_ACCESS_KEY=$(AWS_SECRET_ACCESS_KEY) AWS_SESSION_TOKEN=$(AWS_SESSION_TOKEN) bash $(PATH_TO_INFRASTRUCTURE)/roles/manage-mgmt-aws-support-policies.sh + manage--non-mgmt-test-policies: aws--login ## Create or update IAM Policies @AWS_ACCESS_KEY_ID=$(AWS_ACCESS_KEY_ID) AWS_SECRET_ACCESS_KEY=$(AWS_SECRET_ACCESS_KEY) AWS_SESSION_TOKEN=$(AWS_SESSION_TOKEN) bash $(PATH_TO_INFRASTRUCTURE)/roles/manage-non-mgmt-aws-support-integration-policies.sh diff --git a/scripts/infrastructure/roles/manage-mgmt-aws-support-policies.sh b/scripts/infrastructure/roles/manage-mgmt-aws-support-policies.sh new file mode 100755 index 000000000..79debb705 --- /dev/null +++ b/scripts/infrastructure/roles/manage-mgmt-aws-support-policies.sh @@ -0,0 +1,59 @@ +#!/bin/bash + +function _substitute_environment_variables() { + eval "cat << EOF +$(cat $1) +EOF" +} + +AWS_REGION_NAME="eu-west-2" + +ACCOUNT_ID=$(aws sts get-caller-identity | jq -r .Account) + +# +# Check we're running this against MGMT +# +. "./scripts/aws/helpers.sh" +if ! _validate_current_account "MGMT"; then + echo "Please login to mgmt profile before running this script" + exit 1 +fi + +# +# Create the NHSDevelopmentPolicy that will be used for Developer access and +# NHSDevelopmentRole. This policy is split into 2 as the file size was too large. +# + +policy_name="NHSSupportPolicy" + +for policy_number in "1" "2"; do + tf_policy=$(_substitute_environment_variables ./scripts/infrastructure/policies/support${policy_number}-policy.json) + aws iam get-policy --policy-arn "arn:aws:iam::${ACCOUNT_ID}:policy/${policy_name}${policy_number}" &>/dev/null + if [ $? != 0 ]; then + aws iam create-policy \ + --policy-name "${policy_name}${policy_number}" \ + --policy-document "${tf_policy}" \ + --region "${AWS_REGION_NAME}" || + exit 1 + fi + # We update the version because this updates all roles and we don't have to detach and delete. + versions=$(aws iam list-policy-versions --policy-arn "arn:aws:iam::${ACCOUNT_ID}:policy/${policy_name}${policy_number}" --region "${AWS_REGION_NAME}") + num_versions=$(echo "$versions" | jq -r '.Versions | length') + # There has got to be at least 2 versions. + if [ "$num_versions" -ge 2 ]; then + # Extract the oldest version using jq + oldest_version=$(echo "$versions" | jq -r '.Versions | sort_by(.CreateDate) | .[0].VersionId') + + aws iam delete-policy-version \ + --policy-arn "arn:aws:iam::${ACCOUNT_ID}:policy/${policy_name}${policy_number}" \ + --version-id "${oldest_version}" || + exit 1 + fi + + aws iam create-policy-version \ + --policy-arn "arn:aws:iam::${ACCOUNT_ID}:policy/${policy_name}${policy_number}" \ + --policy-document "${tf_policy}" \ + --set-as-default \ + --region "${AWS_REGION_NAME}" || + exit 1 +done diff --git a/scripts/infrastructure/roles/manage-non-mgmt-aws-support-policies.sh b/scripts/infrastructure/roles/manage-non-mgmt-aws-support-policies.sh new file mode 100755 index 000000000..7fc1d6d93 --- /dev/null +++ b/scripts/infrastructure/roles/manage-non-mgmt-aws-support-policies.sh @@ -0,0 +1,59 @@ +#!/bin/bash + +function _substitute_environment_variables() { + eval "cat << EOF +$(cat $1) +EOF" +} + +AWS_REGION_NAME="eu-west-2" + +ACCOUNT_ID=$(aws sts get-caller-identity | jq -r .Account) + +# +# Check we're not running this against MGMT +# +. "./scripts/aws/helpers.sh" +if _validate_current_account "MGMT"; then + echo "Please login to non-mgmt profile before running this script" + exit 1 +fi + +# +# Create the NHSDevelopmentPolicy that will be used for Developer access and +# NHSDevelopmentRole. This policy is split into 2 as the file size was too large. +# + +policy_name="NHSSupportPolicy" + +for policy_number in "1" "2"; do + tf_policy=$(_substitute_environment_variables ./scripts/infrastructure/policies/support${policy_number}-policy.json) + aws iam get-policy --policy-arn "arn:aws:iam::${ACCOUNT_ID}:policy/${policy_name}${policy_number}" &>/dev/null + if [ $? != 0 ]; then + aws iam create-policy \ + --policy-name "${policy_name}${policy_number}" \ + --policy-document "${tf_policy}" \ + --region "${AWS_REGION_NAME}" || + exit 1 + fi + # We update the version because this updates all roles and we don't have to detach and delete. + versions=$(aws iam list-policy-versions --policy-arn "arn:aws:iam::${ACCOUNT_ID}:policy/${policy_name}${policy_number}" --region "${AWS_REGION_NAME}") + num_versions=$(echo "$versions" | jq -r '.Versions | length') + # There has got to be at least 2 versions. + if [ "$num_versions" -ge 2 ]; then + # Extract the oldest version using jq + oldest_version=$(echo "$versions" | jq -r '.Versions | sort_by(.CreateDate) | .[0].VersionId') + + aws iam delete-policy-version \ + --policy-arn "arn:aws:iam::${ACCOUNT_ID}:policy/${policy_name}${policy_number}" \ + --version-id "${oldest_version}" || + exit 1 + fi + + aws iam create-policy-version \ + --policy-arn "arn:aws:iam::${ACCOUNT_ID}:policy/${policy_name}${policy_number}" \ + --policy-document "${tf_policy}" \ + --set-as-default \ + --region "${AWS_REGION_NAME}" || + exit 1 +done diff --git a/scripts/infrastructure/terraform/terraform-commands.sh b/scripts/infrastructure/terraform/terraform-commands.sh index 7bc5d5d92..0991d1159 100644 --- a/scripts/infrastructure/terraform/terraform-commands.sh +++ b/scripts/infrastructure/terraform/terraform-commands.sh @@ -125,7 +125,7 @@ function _terraform_plan() { -var "layers=${layers}" \ -var "third_party_layers=${third_party_layers}" || return 1 else - if [[ "${account}" = "dev" ]]; then # BACKUPS_LOGIC + if [[ "${account}" = "prod" || "${account}" = "dev" ]]; then # Immutable backups requires layers, but is only enabled in prod terraform plan $args \ -out="$plan_file" \ -var-file="$var_file" \