Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 27 additions & 0 deletions infra/deployments/forms/rds/post-apply.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
#!/usr/bin/env bash

set -euo pipefail

echo "Checking for user-created RDS secrets to clean up..."

mapfile -t user_secrets < <(
aws secretsmanager list-secrets --no-include-planned-deletion --no-paginate \
--filter Key=name,Values=rds-db-credentials/ |
jq -r '.SecretList[].Name | select(test("^rds-db-credentials/cluster-\\w+/[\\w-]+/\\d+$"))'
)

if [[ ${#user_secrets[@]} -eq 0 ]]; then
echo "No user-created secrets found."
exit 0
fi

echo "Found ${#user_secrets[@]} user-created secret(s) to delete:"

for secret_name in "${user_secrets[@]}"; do
echo " Deleting: ${secret_name}"
aws secretsmanager delete-secret \
--secret-id "${secret_name}" \
--recovery-window-in-days 30
done

echo "Done."
14 changes: 13 additions & 1 deletion infra/modules/deployer-access/policy.tf
Original file line number Diff line number Diff line change
Expand Up @@ -929,7 +929,7 @@ data "aws_iam_policy_document" "cloud_control_api" {

data "aws_iam_policy_document" "secrets_manager" {
statement {
sid = "ManageSecretsManagerSecrets"
sid = "ManageRdsDbCredentialSecrets"
actions = [
"secretsmanager:CreateSecret",
"secretsmanager:DeleteSecret",
Expand All @@ -939,10 +939,22 @@ data "aws_iam_policy_document" "secrets_manager" {
"secretsmanager:PutSecretValue",
"secretsmanager:UpdateSecretVersionStage",
"secretsmanager:GetSecretValue",
"secretsmanager:DescribeSecret",
"secretsmanager:ListSecretVersionIds",
]
resources = [
"arn:aws:secretsmanager:eu-west-2:${var.account_id}:secret:data-api/${var.environment_name}/*",
"arn:aws:secretsmanager:eu-west-2:${var.account_id}:secret:rds-db-credentials/cluster-*",
]
effect = "Allow"
}

statement {
sid = "ListRdsDbCredentialSecrets"
actions = [
"secretsmanager:ListSecrets",
]
resources = ["*"]
effect = "Allow"
}
}
2 changes: 1 addition & 1 deletion infra/modules/engineer-access/policies.tf
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,7 @@ resource "aws_iam_policy" "query_rds_with_data_api" {
]
Effect = "Allow"
Resource = [
"arn:aws:secretsmanager:eu-west-2:${local.account_id}:secret:data-api/${var.env_name}/*"
"arn:aws:secretsmanager:eu-west-2:${local.account_id}:secret:rds-db-credentials/cluster-*/forms-*" // allow access to secrets for any cluster with the name prefix `forms-`. ie. don't allow access to `root` credentials, while allowing access to app creds.
]
},
]
Expand Down
46 changes: 45 additions & 1 deletion infra/modules/rds/parameters.tf
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ resource "aws_ssm_parameter" "database_password" {
}
}

# TODO: delete `data_api_credentials` secrets after migrating to autolocate secrets.
resource "aws_secretsmanager_secret" "data_api_credentials" {
#checkov:skip=CKV_AWS_149:The secret is already using the default key, which is sufficient
#checkov:skip=CKV2_AWS_57:We're setting this manually from an authoritative source, so rotation would be actively harmful
Expand All @@ -50,7 +51,6 @@ resource "aws_secretsmanager_secret_version" "data_api_credentials" {
})
}


resource "aws_ssm_parameter" "database_url" {
#checkov:skip=CKV_AWS_337:The parameter is already using the default key
#checkov:skip=CKV2_FORMS_AWS_7:Database URLs should update when passwords or endpoints change
Expand All @@ -66,3 +66,47 @@ resource "aws_ssm_parameter" "database_url" {
each.key
)
}

locals {
autolocate_secret_timestamp = "1778583245"
database_creds = merge(
{
for k, v in var.apps_list : k => merge(v, {
password = aws_ssm_parameter.database_password[k].value
})
},
{
root = {
username = "root"
password = aws_ssm_parameter.database_password_for_root_user.value
}
}
)
}

resource "aws_secretsmanager_secret" "data_api_credentials_autolocate" {
#checkov:skip=CKV_AWS_149:The secret is already using the default key, which is sufficient
#checkov:skip=CKV2_AWS_57:We're setting this manually from an authoritative source, so rotation would be actively harmful

for_each = local.database_creds

name = "rds-db-credentials/${aws_rds_cluster.cluster_aurora_v2.cluster_resource_id}/${each.key}"
description = "RDS database ${each.value.username} credentials for ${aws_rds_cluster.cluster_aurora_v2.id}"
}

resource "aws_secretsmanager_secret_version" "data_api_credentials_autolocate" {
for_each = local.database_creds

secret_id = aws_secretsmanager_secret.data_api_credentials_autolocate[each.key].id
secret_string = jsonencode({
// Preamble: Data API adds these fields when creating secrets, so let's include them as well.
dbInstanceIdentifier = aws_rds_cluster.cluster_aurora_v2.id
engine = aws_rds_cluster.cluster_aurora_v2.engine
host = aws_rds_cluster.cluster_aurora_v2.endpoint
port = aws_rds_cluster.cluster_aurora_v2.port
resourceId = aws_rds_cluster.cluster_aurora_v2.cluster_resource_id

username = each.value.username
password = each.value.password
})
}
1 change: 1 addition & 0 deletions support/forms-cli/.rspec
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
--require spec_helper
6 changes: 5 additions & 1 deletion support/forms-cli/lib/commands/data_api.rb
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ def run
parse_options
return unless aws_authenticated? && valid_options?

@connection = DataApiConnection.new(fetch_environment, @options[:database], @options[:cluster])
@connection = DataApiConnection.new(fetch_environment, @options[:database], @options[:cluster], @options[:user])

begin
print execute_statement
Expand Down Expand Up @@ -74,6 +74,10 @@ def parse_options
opts.on("-sSTATEMENT", "--statement=STATEMENT", "[Mandatory] The statement to execute") do |statement|
@options[:statement] = statement
end

opts.on("-uUSER", "--user=USER", "The database user to connect as") do |user|
@options[:user] = user
end
}.parse!
end

Expand Down
14 changes: 12 additions & 2 deletions support/forms-cli/lib/utilities/data_api_connection.rb
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,11 @@

# Executes statements on AWS RDS using the Data API.
class DataApiConnection
def initialize(env, database_name, cluster_name)
def initialize(env, database_name, cluster_name, database_user = nil)
@env = env
@database_name = database_name
@cluster_name = cluster_name || default_cluster_name
@database_user = database_user || database_name

@data_service = Aws::RDSDataService::Client.new
@rds = Aws::RDS::Client.new
Expand Down Expand Up @@ -43,7 +44,7 @@ def default_cluster_name
end

def query_credential_arn
secret_name = "data-api/#{@env}/#{@database_name}/rds-credentials"
secret_name = "rds-db-credentials/#{query_database_resource_id}/#{@database_user}"

begin
secret = @secrets_manager.describe_secret({ secret_id: secret_name })
Expand All @@ -61,4 +62,13 @@ def query_database_cluster_arn

arn
end

def query_database_resource_id
params = { db_cluster_identifier: @cluster_name }
resource_id = @rds.describe_db_clusters(params)&.db_clusters&.[](0)&.db_cluster_resource_id

raise "Database cluster was not found" if resource_id.nil?

resource_id
end
end
2 changes: 1 addition & 1 deletion support/forms-cli/spec/commands/data_api_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@

expect(DataApiConnection)
.to have_received(:new)
.with("dev", "forms-admin", "cluster-name")
.with("dev", "forms-admin", "cluster-name", nil)
.at_least(:once)
end

Expand Down
2 changes: 1 addition & 1 deletion support/forms-cli/spec/fixtures/rds.rb
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,6 @@ module RDSFixtures

def self.describe_db_clusters
@rds_stub.stub_data(:describe_db_clusters,
{ db_clusters: [{ db_cluster_arn: "cluster-arn" }] })
{ db_clusters: [{ db_cluster_arn: "cluster-arn", db_cluster_resource_id: "cluster-resource-id" }] })
end
end
4 changes: 2 additions & 2 deletions support/forms-cli/spec/fixtures/secretsmanager.rb
Original file line number Diff line number Diff line change
Expand Up @@ -32,8 +32,8 @@ def self.empty_list_secrets
def self.describe_secret
@secrets_manager_stub.stub_data(:describe_secret,
{
arn: "arn:aws:secretsmanager:eu-west-2:123456789012:secret:data-api/dev/forms-admin/rds-credentials-AbCdEf",
name: "data-api/dev/forms-admin/rds-credentials",
arn: "arn:aws:secretsmanager:eu-west-2:123456789012:secret:rds-db-credentials/cluster-resource-id/forms-admin-AbCdEf",
name: "rds-db-credentials/cluster-resource-id/forms-admin",
description: "Data API credentials for forms-admin in dev environment",
})
end
Expand Down
5 changes: 5 additions & 0 deletions support/forms-cli/spec/spec_helper.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
# frozen_string_literal: true

RSpec.configure do |config|
config.before { allow($stdout).to receive(:write) }
end
10 changes: 5 additions & 5 deletions support/forms-cli/spec/utilities/data_api_connection_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
secrets_manager_mock = instance_double(Aws::SecretsManager::Client)
allow(secrets_manager_mock)
.to receive(:describe_secret)
.with(hash_including(secret_id: "data-api/dev/forms-admin/rds-credentials"))
.with(hash_including(secret_id: "rds-db-credentials/cluster-resource-id/forms-admin"))
.and_return(SecretsManagerFixtures.describe_secret)

secrets_manager_mock
Expand Down Expand Up @@ -56,7 +56,7 @@
.to have_received(:execute_statement)
.with(hash_including(
resource_arn: "cluster-arn",
secret_arn: "arn:aws:secretsmanager:eu-west-2:123456789012:secret:data-api/dev/forms-admin/rds-credentials-AbCdEf",
secret_arn: "arn:aws:secretsmanager:eu-west-2:123456789012:secret:rds-db-credentials/cluster-resource-id/forms-admin-AbCdEf",
))
.at_least(:once)
end
Expand All @@ -77,7 +77,7 @@

expect(secrets_manager_mock)
.to have_received(:describe_secret)
.with(hash_including(secret_id: "data-api/dev/forms-admin/rds-credentials"))
.with(hash_including(secret_id: "rds-db-credentials/cluster-resource-id/forms-admin"))
.at_least(:once)
end

Expand Down Expand Up @@ -111,7 +111,7 @@
secrets_manager_mock_no_secret = instance_double(Aws::SecretsManager::Client)
allow(secrets_manager_mock_no_secret)
.to receive(:describe_secret)
.with(hash_including(secret_id: "data-api/dev/forms-admin/rds-credentials"))
.with(hash_including(secret_id: "rds-db-credentials/cluster-resource-id/forms-admin"))
.and_raise(Aws::SecretsManager::Errors::ResourceNotFoundException.new("context", "Secret not found"))

secrets_manager_mock_no_secret
Expand All @@ -126,7 +126,7 @@
it "raises an error about missing secret" do
expect {
described_class.new("dev", "forms-admin", "cluster-name").execute_statement("select * from testing;")
}.to raise_error(/Data API credential secret 'data-api\/dev\/forms-admin\/rds-credentials' was not found/)
}.to raise_error(/Data API credential secret 'rds-db-credentials\/cluster-resource-id\/forms-admin' was not found/)
end
end
end
10 changes: 4 additions & 6 deletions support/forms-cli/spec/utilities/helpers_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,10 @@

describe ".aws_authenticated?" do
context "when not authenticated" do
it "returns false" do
expect(dummy_class.new.aws_authenticated?).to be false
end

it "prints warning to stdout" do
expect { dummy_class.new.aws_authenticated? }.to output(/You must be authenticated/).to_stdout
it "returns false and prints warning to stdout" do
result = nil
expect { result = dummy_class.new.aws_authenticated? }.to output(/You must be authenticated/).to_stdout
expect(result).to be false
end
end

Expand Down