From 90accf2f397d5f661b8d8ecfd2139f4836b1aa1f Mon Sep 17 00:00:00 2001 From: Adam Moussa <166072409+amoussa1229@users.noreply.github.com> Date: Tue, 2 Jun 2026 20:29:18 -0400 Subject: [PATCH] Migrate secrets from SSM to Secrets Manager API tokens and credentials must live in Secrets Manager per secrets-and-config.md, but the four original payment Lambdas still read 10 SecureString SSM params. Move them to three grouped secrets (slack-bot-token plaintext, boa-check-mgmt and boa-reporting as JSON), matching the pattern the expense Lambdas already use. IAM is scoped to secretsmanager:GetSecretValue per secret; the VPC Lambdas reach the public endpoint over the existing NAT path. Test/reissue scripts and the client-ssm dependency are updated/removed accordingly. Refs: #3 --- package-lock.json | 22 -------------- package.json | 1 - scripts/reissue-checks.cjs | 20 +++++-------- scripts/test-boa-prod.js | 44 +++++++++++---------------- scripts/test-boa-sandbox.js | 38 ++++++++++------------- src/fetchBoaTransactions.js | 23 +++++++------- src/processPaymentCsv.js | 31 +++++++------------ src/processPayrollEmail.js | 13 ++++---- src/slackAppHome.js | 13 ++++---- template.yaml | 60 +++++++++++++++---------------------- 10 files changed, 96 insertions(+), 169 deletions(-) diff --git a/package-lock.json b/package-lock.json index 4e8285a..6840bb9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -13,7 +13,6 @@ "@aws-sdk/client-s3": "^3.1059.0", "@aws-sdk/client-secrets-manager": "^3.1059.0", "@aws-sdk/client-sqs": "^3.1059.0", - "@aws-sdk/client-ssm": "^3.1059.0", "@aws-sdk/lib-dynamodb": "^3.1059.0", "csv-parse": "^6.2.1", "mailparser": "^3.9.9" @@ -337,27 +336,6 @@ "node": ">=20.0.0" } }, - "node_modules/@aws-sdk/client-ssm": { - "version": "3.1059.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/client-ssm/-/client-ssm-3.1059.0.tgz", - "integrity": "sha512-c6VaL8BKDeDSHWz59ELsYZcQit7r4aofVm07acWnxw1DQy5amQagOymPDhYwIpB6H3wZNMDu4FK3FxecNDzGww==", - "license": "Apache-2.0", - "dependencies": { - "@aws-crypto/sha256-browser": "5.2.0", - "@aws-crypto/sha256-js": "5.2.0", - "@aws-sdk/core": "^3.974.16", - "@aws-sdk/credential-provider-node": "^3.972.49", - "@aws-sdk/types": "^3.973.10", - "@smithy/core": "^3.24.6", - "@smithy/fetch-http-handler": "^5.4.6", - "@smithy/node-http-handler": "^4.7.6", - "@smithy/types": "^4.14.3", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=20.0.0" - } - }, "node_modules/@aws-sdk/core": { "version": "3.974.16", "resolved": "https://registry.npmjs.org/@aws-sdk/core/-/core-3.974.16.tgz", diff --git a/package.json b/package.json index ac7998c..42c60f5 100644 --- a/package.json +++ b/package.json @@ -12,7 +12,6 @@ "@aws-sdk/client-s3": "^3.1059.0", "@aws-sdk/client-secrets-manager": "^3.1059.0", "@aws-sdk/client-sqs": "^3.1059.0", - "@aws-sdk/client-ssm": "^3.1059.0", "@aws-sdk/lib-dynamodb": "^3.1059.0", "csv-parse": "^6.2.1", "mailparser": "^3.9.9" diff --git a/scripts/reissue-checks.cjs b/scripts/reissue-checks.cjs index d4596e5..52f56a3 100644 --- a/scripts/reissue-checks.cjs +++ b/scripts/reissue-checks.cjs @@ -1,16 +1,16 @@ const { DynamoDBClient } = require("@aws-sdk/client-dynamodb"); const { DynamoDBDocumentClient, ScanCommand } = require("@aws-sdk/lib-dynamodb"); -const { SSMClient, GetParameterCommand } = require("@aws-sdk/client-ssm"); +const { SecretsManagerClient, GetSecretValueCommand } = require("@aws-sdk/client-secrets-manager"); const ddb = DynamoDBDocumentClient.from(new DynamoDBClient({ region: "us-east-1" })); -const ssm = new SSMClient({ region: "us-east-1" }); +const secrets = new SecretsManagerClient({ region: "us-east-1" }); const BOA_BASE_URL = "https://api.bofa.com"; -async function getSSMParam(name) { - const { Parameter } = await ssm.send( - new GetParameterCommand({ Name: name, WithDecryption: true }) +async function getSecretJson(secretId) { + const { SecretString } = await secrets.send( + new GetSecretValueCommand({ SecretId: secretId }) ); - return Parameter.Value; + return JSON.parse(SecretString); } async function getAccessToken(applicationID, clientId, clientSecret) { @@ -115,13 +115,7 @@ async function getAccessToken(applicationID, clientId, clientSecret) { } // Get credentials - const [appId, clientId, clientSecret, accountNumber, companyId] = await Promise.all([ - getSSMParam("/payments-dashboard/boa-check-mgmt-app-id"), - getSSMParam("/payments-dashboard/boa-check-mgmt-client-id"), - getSSMParam("/payments-dashboard/boa-check-mgmt-token"), - getSSMParam("/payments-dashboard/boa-account-number"), - getSSMParam("/payments-dashboard/boa-company-id"), - ]); + const { appId, clientId, token: clientSecret, accountNumber, companyId } = await getSecretJson("payments-dashboard/boa-check-mgmt"); // Fill in account number for (const item of issueList) { diff --git a/scripts/test-boa-prod.js b/scripts/test-boa-prod.js index ad9606f..d941cbd 100644 --- a/scripts/test-boa-prod.js +++ b/scripts/test-boa-prod.js @@ -8,16 +8,16 @@ * node scripts/test-boa-prod.js */ -import { SSMClient, GetParameterCommand } from "@aws-sdk/client-ssm"; +import { SecretsManagerClient, GetSecretValueCommand } from "@aws-sdk/client-secrets-manager"; -const ssm = new SSMClient(); +const secrets = new SecretsManagerClient(); const BASE_URL = "https://api.bofa.com"; -async function getSSMParam(name) { - const { Parameter } = await ssm.send( - new GetParameterCommand({ Name: name, WithDecryption: true }) +async function getSecretJson(secretId) { + const { SecretString } = await secrets.send( + new GetSecretValueCommand({ SecretId: secretId }) ); - return Parameter.Value; + return JSON.parse(SecretString); } async function getAccessToken(applicationID, clientId, clientSecret) { @@ -44,27 +44,19 @@ async function getAccessToken(applicationID, clientId, clientSecret) { } async function main() { - console.log("Loading credentials from SSM...\n"); + console.log("Loading credentials from Secrets Manager...\n"); - const [ - checkMgmtAppId, - checkMgmtClientId, - checkMgmtSecret, - reportingAppId, - reportingClientId, - reportingSecret, - accountNumber, - bankId, - ] = await Promise.all([ - getSSMParam("/payments-dashboard/boa-check-mgmt-app-id"), - getSSMParam("/payments-dashboard/boa-check-mgmt-client-id"), - getSSMParam("/payments-dashboard/boa-check-mgmt-token"), - getSSMParam("/payments-dashboard/boa-reporting-app-id"), - getSSMParam("/payments-dashboard/boa-account-info-client-id"), - getSSMParam("/payments-dashboard/boa-account-info-token"), - getSSMParam("/payments-dashboard/boa-account-number"), - getSSMParam("/payments-dashboard/boa-bank-id"), - ]); + const checkMgmt = await getSecretJson("payments-dashboard/boa-check-mgmt"); + const reporting = await getSecretJson("payments-dashboard/boa-reporting"); + + const checkMgmtAppId = checkMgmt.appId; + const checkMgmtClientId = checkMgmt.clientId; + const checkMgmtSecret = checkMgmt.token; + const reportingAppId = reporting.appId; + const reportingClientId = reporting.clientId; + const reportingSecret = reporting.token; + const accountNumber = reporting.accountNumber; + const bankId = reporting.bankId; console.log("Credentials loaded.\n"); diff --git a/scripts/test-boa-sandbox.js b/scripts/test-boa-sandbox.js index 3e93e30..f59851f 100644 --- a/scripts/test-boa-sandbox.js +++ b/scripts/test-boa-sandbox.js @@ -11,17 +11,17 @@ * node scripts/test-boa-sandbox.js */ -import { SSMClient, GetParameterCommand } from "@aws-sdk/client-ssm"; +import { SecretsManagerClient, GetSecretValueCommand } from "@aws-sdk/client-secrets-manager"; -const ssm = new SSMClient(); +const secrets = new SecretsManagerClient(); const SANDBOX_BASE = "https://api-sb.bofa.com"; const AUTH_URL = `${SANDBOX_BASE}/authn/v1/client-authentication`; -async function getSSMParam(name) { - const { Parameter } = await ssm.send( - new GetParameterCommand({ Name: name, WithDecryption: true }) +async function getSecretJson(secretId) { + const { SecretString } = await secrets.send( + new GetSecretValueCommand({ SecretId: secretId }) ); - return Parameter.Value; + return JSON.parse(SecretString); } async function getAccessToken(applicationID, clientId, clientSecret) { @@ -51,23 +51,17 @@ async function getAccessToken(applicationID, clientId, clientSecret) { } async function main() { - console.log("Loading credentials from SSM...\n"); + console.log("Loading credentials from Secrets Manager...\n"); - const [ - checkMgmtClientId, - checkMgmtSecret, - accountInfoClientId, - accountInfoSecret, - accountNumber, - companyId, - ] = await Promise.all([ - getSSMParam("/payments-dashboard/boa-check-mgmt-client-id"), - getSSMParam("/payments-dashboard/boa-check-mgmt-token"), - getSSMParam("/payments-dashboard/boa-account-info-client-id"), - getSSMParam("/payments-dashboard/boa-account-info-token"), - getSSMParam("/payments-dashboard/boa-account-number"), - getSSMParam("/payments-dashboard/boa-company-id"), - ]); + const checkMgmt = await getSecretJson("payments-dashboard/boa-check-mgmt"); + const reporting = await getSecretJson("payments-dashboard/boa-reporting"); + + const checkMgmtClientId = checkMgmt.clientId; + const checkMgmtSecret = checkMgmt.token; + const accountInfoClientId = reporting.clientId; + const accountInfoSecret = reporting.token; + const accountNumber = checkMgmt.accountNumber; + const companyId = checkMgmt.companyId; console.log("Credentials loaded.\n"); diff --git a/src/fetchBoaTransactions.js b/src/fetchBoaTransactions.js index a891fee..660e8ab 100644 --- a/src/fetchBoaTransactions.js +++ b/src/fetchBoaTransactions.js @@ -1,17 +1,20 @@ import { DynamoDBClient } from "@aws-sdk/client-dynamodb"; import { DynamoDBDocumentClient, ScanCommand, UpdateCommand } from "@aws-sdk/lib-dynamodb"; -import { SSMClient, GetParameterCommand } from "@aws-sdk/client-ssm"; +import { SecretsManagerClient, GetSecretValueCommand } from "@aws-sdk/client-secrets-manager"; const ddb = DynamoDBDocumentClient.from(new DynamoDBClient()); -const ssm = new SSMClient(); +const secrets = new SecretsManagerClient(); const TABLE_NAME = process.env.TABLE_NAME; const BOA_BASE_URL = process.env.BOA_BASE_URL; -async function getSSMParam(name) { - const { Parameter } = await ssm.send( - new GetParameterCommand({ Name: name, WithDecryption: true }) +let cachedCreds; +async function getReportingCreds() { + if (cachedCreds) return cachedCreds; + const { SecretString } = await secrets.send( + new GetSecretValueCommand({ SecretId: process.env.BOA_REPORTING_SECRET_NAME }) ); - return Parameter.Value; + cachedCreds = JSON.parse(SecretString); + return cachedCreds; } async function getAccessToken(applicationID, clientId, clientSecret) { @@ -34,13 +37,7 @@ async function getAccessToken(applicationID, clientId, clientSecret) { } export const handler = async () => { - const [appId, clientId, clientSecret, accountNumber, bankId] = await Promise.all([ - getSSMParam(process.env.BOA_REPORTING_APP_ID_PARAM), - getSSMParam(process.env.BOA_REPORTING_CLIENT_ID_PARAM), - getSSMParam(process.env.BOA_REPORTING_SECRET_PARAM), - getSSMParam(process.env.BOA_ACCOUNT_NUMBER_PARAM), - getSSMParam(process.env.BOA_BANK_ID_PARAM), - ]); + const { appId, clientId, token: clientSecret, accountNumber, bankId } = await getReportingCreds(); const bearerToken = await getAccessToken(appId, clientId, clientSecret); diff --git a/src/processPaymentCsv.js b/src/processPaymentCsv.js index bc282f4..4fe2660 100644 --- a/src/processPaymentCsv.js +++ b/src/processPaymentCsv.js @@ -1,20 +1,23 @@ import { S3Client, GetObjectCommand } from "@aws-sdk/client-s3"; import { DynamoDBClient } from "@aws-sdk/client-dynamodb"; import { DynamoDBDocumentClient, GetCommand, PutCommand, UpdateCommand, ScanCommand } from "@aws-sdk/lib-dynamodb"; -import { SSMClient, GetParameterCommand } from "@aws-sdk/client-ssm"; +import { SecretsManagerClient, GetSecretValueCommand } from "@aws-sdk/client-secrets-manager"; import { parse } from "csv-parse/sync"; const s3 = new S3Client(); const ddb = DynamoDBDocumentClient.from(new DynamoDBClient()); -const ssm = new SSMClient(); +const secrets = new SecretsManagerClient(); const TABLE_NAME = process.env.TABLE_NAME; const BOA_BASE_URL = process.env.BOA_BASE_URL; -async function getSSMParam(name) { - const { Parameter } = await ssm.send( - new GetParameterCommand({ Name: name, WithDecryption: true }) +let cachedCreds; +async function getCheckMgmtCreds() { + if (cachedCreds) return cachedCreds; + const { SecretString } = await secrets.send( + new GetSecretValueCommand({ SecretId: process.env.BOA_CHECK_MGMT_SECRET_NAME }) ); - return Parameter.Value; + cachedCreds = JSON.parse(SecretString); + return cachedCreds; } async function getAccessToken(applicationID, clientId, clientSecret) { @@ -97,13 +100,7 @@ async function backfillBoA() { console.log(`Backfill: ${toSubmit.length} checks to submit`); - const [appId, clientId, clientSecret, accountNumber, companyId] = await Promise.all([ - getSSMParam(process.env.BOA_CHECK_MGMT_APP_ID_PARAM), - getSSMParam(process.env.BOA_CHECK_MGMT_CLIENT_ID_PARAM), - getSSMParam(process.env.BOA_CHECK_MGMT_SECRET_PARAM), - getSSMParam(process.env.BOA_ACCOUNT_NUMBER_PARAM), - getSSMParam(process.env.BOA_COMPANY_ID_PARAM), - ]); + const { appId, clientId, token: clientSecret, accountNumber, companyId } = await getCheckMgmtCreds(); const bearerToken = await getAccessToken(appId, clientId, clientSecret); const BOA_BATCH_SIZE = 100; @@ -379,13 +376,7 @@ export const handler = async (event) => { // Submit to CashPro if there are any new issues or cancels if (newChecks.length || cancelChecks.length) { - const [appId, clientId, clientSecret, accountNumber, companyId] = await Promise.all([ - getSSMParam(process.env.BOA_CHECK_MGMT_APP_ID_PARAM), - getSSMParam(process.env.BOA_CHECK_MGMT_CLIENT_ID_PARAM), - getSSMParam(process.env.BOA_CHECK_MGMT_SECRET_PARAM), - getSSMParam(process.env.BOA_ACCOUNT_NUMBER_PARAM), - getSSMParam(process.env.BOA_COMPANY_ID_PARAM), - ]); + const { appId, clientId, token: clientSecret, accountNumber, companyId } = await getCheckMgmtCreds(); const bearerToken = await getAccessToken(appId, clientId, clientSecret); diff --git a/src/processPayrollEmail.js b/src/processPayrollEmail.js index d51178a..161ffed 100644 --- a/src/processPayrollEmail.js +++ b/src/processPayrollEmail.js @@ -8,13 +8,13 @@ import { BatchWriteCommand, } from "@aws-sdk/lib-dynamodb"; import { SQSClient, SendMessageCommand } from "@aws-sdk/client-sqs"; -import { SSMClient, GetParameterCommand } from "@aws-sdk/client-ssm"; +import { SecretsManagerClient, GetSecretValueCommand } from "@aws-sdk/client-secrets-manager"; import { simpleParser } from "mailparser"; const s3 = new S3Client(); const ddb = DynamoDBDocumentClient.from(new DynamoDBClient()); const sqs = new SQSClient(); -const ssm = new SSMClient(); +const secrets = new SecretsManagerClient(); const TABLE_NAME = process.env.TABLE_NAME; const CHANNEL_ID = process.env.PAYROLL_CHANNEL_ID; @@ -23,13 +23,10 @@ const QUEUE_URL = process.env.PAYROLL_BATCH_QUEUE_URL; let cachedToken; async function getSlackToken() { if (cachedToken) return cachedToken; - const { Parameter } = await ssm.send( - new GetParameterCommand({ - Name: process.env.SLACK_BOT_TOKEN_PARAM, - WithDecryption: true, - }) + const { SecretString } = await secrets.send( + new GetSecretValueCommand({ SecretId: process.env.SLACK_BOT_TOKEN_SECRET_NAME }) ); - cachedToken = Parameter.Value; + cachedToken = SecretString; return cachedToken; } diff --git a/src/slackAppHome.js b/src/slackAppHome.js index e6f47e9..23b47d0 100644 --- a/src/slackAppHome.js +++ b/src/slackAppHome.js @@ -1,21 +1,18 @@ import { DynamoDBClient } from "@aws-sdk/client-dynamodb"; import { DynamoDBDocumentClient, GetCommand, ScanCommand } from "@aws-sdk/lib-dynamodb"; -import { SSMClient, GetParameterCommand } from "@aws-sdk/client-ssm"; +import { SecretsManagerClient, GetSecretValueCommand } from "@aws-sdk/client-secrets-manager"; const ddb = DynamoDBDocumentClient.from(new DynamoDBClient()); -const ssm = new SSMClient(); +const secrets = new SecretsManagerClient(); const TABLE_NAME = process.env.TABLE_NAME; let cachedToken; async function getSlackToken() { if (cachedToken) return cachedToken; - const { Parameter } = await ssm.send( - new GetParameterCommand({ - Name: process.env.SLACK_BOT_TOKEN_PARAM, - WithDecryption: true, - }) + const { SecretString } = await secrets.send( + new GetSecretValueCommand({ SecretId: process.env.SLACK_BOT_TOKEN_SECRET_NAME }) ); - cachedToken = Parameter.Value; + cachedToken = SecretString; return cachedToken; } diff --git a/template.yaml b/template.yaml index a6acb1a..5026c60 100644 --- a/template.yaml +++ b/template.yaml @@ -241,7 +241,7 @@ Resources: Timeout: 60 Environment: Variables: - SLACK_BOT_TOKEN_PARAM: /payments-dashboard/slack-bot-token + SLACK_BOT_TOKEN_SECRET_NAME: payments-dashboard/slack-bot-token PAYROLL_CHANNEL_ID: C0AV5RBMYKU PAYROLL_BATCH_QUEUE_URL: !Ref PayrollBatchQueue Events: @@ -265,8 +265,11 @@ Resources: BucketName: !Sub seahaven-payroll-emails-${AWS::AccountId} - DynamoDBCrudPolicy: TableName: !Ref DashboardTable - - SSMParameterReadPolicy: - ParameterName: payments-dashboard/slack-bot-token + - Version: "2012-10-17" + Statement: + - Effect: Allow + Action: secretsmanager:GetSecretValue + Resource: !Sub arn:aws:secretsmanager:${AWS::Region}:${AWS::AccountId}:secret:payments-dashboard/slack-bot-token-* - SQSSendMessagePolicy: QueueName: !GetAtt PayrollBatchQueue.QueueName - SQSPollerPolicy: @@ -281,11 +284,7 @@ Resources: Environment: Variables: BOA_BASE_URL: https://api.bofa.com - BOA_CHECK_MGMT_APP_ID_PARAM: /payments-dashboard/boa-check-mgmt-app-id - BOA_CHECK_MGMT_CLIENT_ID_PARAM: /payments-dashboard/boa-check-mgmt-client-id - BOA_CHECK_MGMT_SECRET_PARAM: /payments-dashboard/boa-check-mgmt-token - BOA_ACCOUNT_NUMBER_PARAM: /payments-dashboard/boa-account-number - BOA_COMPANY_ID_PARAM: /payments-dashboard/boa-company-id + BOA_CHECK_MGMT_SECRET_NAME: payments-dashboard/boa-check-mgmt VpcConfig: SubnetIds: - !Ref PrivateSubnet @@ -307,16 +306,11 @@ Resources: BucketName: !Sub seahaven-payments-csv-${AWS::AccountId} - DynamoDBCrudPolicy: TableName: !Ref DashboardTable - - SSMParameterReadPolicy: - ParameterName: payments-dashboard/boa-check-mgmt-app-id - - SSMParameterReadPolicy: - ParameterName: payments-dashboard/boa-check-mgmt-client-id - - SSMParameterReadPolicy: - ParameterName: payments-dashboard/boa-check-mgmt-token - - SSMParameterReadPolicy: - ParameterName: payments-dashboard/boa-account-number - - SSMParameterReadPolicy: - ParameterName: payments-dashboard/boa-company-id + - Version: "2012-10-17" + Statement: + - Effect: Allow + Action: secretsmanager:GetSecretValue + Resource: !Sub arn:aws:secretsmanager:${AWS::Region}:${AWS::AccountId}:secret:payments-dashboard/boa-check-mgmt-* - Version: "2012-10-17" Statement: - Effect: Allow @@ -338,7 +332,7 @@ Resources: - !Ref LambdaSecurityGroup Environment: Variables: - SLACK_BOT_TOKEN_PARAM: /payments-dashboard/slack-bot-token + SLACK_BOT_TOKEN_SECRET_NAME: payments-dashboard/slack-bot-token Events: SlackEvent: Type: HttpApi @@ -348,8 +342,11 @@ Resources: Policies: - DynamoDBReadPolicy: TableName: !Ref DashboardTable - - SSMParameterReadPolicy: - ParameterName: payments-dashboard/slack-bot-token + - Version: "2012-10-17" + Statement: + - Effect: Allow + Action: secretsmanager:GetSecretValue + Resource: !Sub arn:aws:secretsmanager:${AWS::Region}:${AWS::AccountId}:secret:payments-dashboard/slack-bot-token-* - Version: "2012-10-17" Statement: - Effect: Allow @@ -373,11 +370,7 @@ Resources: Environment: Variables: BOA_BASE_URL: https://api.bofa.com - BOA_REPORTING_APP_ID_PARAM: /payments-dashboard/boa-reporting-app-id - BOA_REPORTING_CLIENT_ID_PARAM: /payments-dashboard/boa-account-info-client-id - BOA_REPORTING_SECRET_PARAM: /payments-dashboard/boa-account-info-token - BOA_ACCOUNT_NUMBER_PARAM: /payments-dashboard/boa-account-number - BOA_BANK_ID_PARAM: /payments-dashboard/boa-bank-id + BOA_REPORTING_SECRET_NAME: payments-dashboard/boa-reporting Events: DailySchedule: Type: Schedule @@ -388,16 +381,11 @@ Resources: Policies: - DynamoDBCrudPolicy: TableName: !Ref DashboardTable - - SSMParameterReadPolicy: - ParameterName: payments-dashboard/boa-reporting-app-id - - SSMParameterReadPolicy: - ParameterName: payments-dashboard/boa-account-info-client-id - - SSMParameterReadPolicy: - ParameterName: payments-dashboard/boa-account-info-token - - SSMParameterReadPolicy: - ParameterName: payments-dashboard/boa-account-number - - SSMParameterReadPolicy: - ParameterName: payments-dashboard/boa-bank-id + - Version: "2012-10-17" + Statement: + - Effect: Allow + Action: secretsmanager:GetSecretValue + Resource: !Sub arn:aws:secretsmanager:${AWS::Region}:${AWS::AccountId}:secret:payments-dashboard/boa-reporting-* - Version: "2012-10-17" Statement: - Effect: Allow