Migrate secrets from SSM Parameter Store to Secrets Manager

- Slack bot token and signing secret now read from Secrets Manager
- 3CX credentials (domain, client-id, client-secret) moved to
  Secrets Manager under afterhours-shift-manager/3cx-* prefix
- Channel ID is now a non-secret CloudFormation parameter (ShiftChannel)
- Add shared secrets.py helper for Secrets Manager reads
- Remove SSM and KMS IAM policies, add secretsmanager:GetSecretValue
This commit is contained in:
Adam Moussa 2026-05-12 15:41:44 -04:00
parent 0fed60248a
commit 7979e2a4e7
5 changed files with 43 additions and 73 deletions

View file

@ -10,31 +10,18 @@ import os
from datetime import datetime
from zoneinfo import ZoneInfo
import boto3
from shared.three_cx_client import ThreeCXClient
from shared.schedule import ShiftSchedule
from shared.secrets import get_secret
logger = logging.getLogger()
logger.setLevel(logging.INFO)
EASTERN = ZoneInfo("America/New_York")
# System extensions to exclude from roster sync
EXCLUDE_NAMES = {"Voicemail", "IVR", "Fax"}
def get_3cx_credentials() -> dict:
ssm = boto3.client("ssm")
prefix = os.environ.get("TCX_SSM_PREFIX", "/3cx-scheduler")
params = ssm.get_parameters_by_path(Path=prefix, WithDecryption=True)
creds = {}
for p in params["Parameters"]:
key = p["Name"].split("/")[-1]
creds[key] = p["Value"]
return creds
def handler(event, context):
now = datetime.now(EASTERN)
@ -52,12 +39,12 @@ def handler(event, context):
"Starting roster sync from 3CX group '%s' at %s", group_name, now.isoformat()
)
creds = get_3cx_credentials()
secret_prefix = os.environ["TCX_SECRET_PREFIX"]
client = ThreeCXClient(
domain=creds["domain"],
domain=get_secret(f"{secret_prefix}domain"),
auth_mode="oauth",
client_id=creds["client_id"],
client_secret=creds["client_secret"],
client_id=get_secret(f"{secret_prefix}client-id"),
client_secret=get_secret(f"{secret_prefix}client-secret"),
)
members = client.get_group_members(group_name)

View file

@ -0,0 +1,16 @@
"""Fetch secrets from AWS Secrets Manager."""
import boto3
_client = None
def _get_client():
global _client
if _client is None:
_client = boto3.client("secretsmanager")
return _client
def get_secret(secret_id: str) -> str:
return _get_client().get_secret_value(SecretId=secret_id)["SecretString"]

View file

@ -3,10 +3,10 @@
import logging
import os
import boto3
from slack_bolt.adapter.aws_lambda import SlackRequestHandler
from app import create_app
from shared.secrets import get_secret
logger = logging.getLogger()
logger.setLevel(logging.INFO)
@ -16,23 +16,15 @@ logging.basicConfig(
format="%(asctime)s %(levelname)s %(name)s: %(message)s", level=logging.INFO
)
# Lazy-initialized app singleton
_slack_handler = None
def _get_handler() -> SlackRequestHandler:
global _slack_handler
if _slack_handler is None:
ssm = boto3.client("ssm")
bot_token = ssm.get_parameter(
Name=os.environ["SLACK_BOT_TOKEN_PARAM"], WithDecryption=True
)["Parameter"]["Value"]
signing_secret = ssm.get_parameter(
Name=os.environ["SLACK_SIGNING_SECRET_PARAM"], WithDecryption=True
)["Parameter"]["Value"]
schedule_channel = ssm.get_parameter(
Name=os.environ["SHIFT_CHANNEL_PARAM"], WithDecryption=True
)["Parameter"]["Value"]
bot_token = get_secret(os.environ["SLACK_BOT_TOKEN_SECRET"])
signing_secret = get_secret(os.environ["SLACK_SIGNING_SECRET"])
schedule_channel = os.environ["SHIFT_CHANNEL"]
app = create_app(bot_token, signing_secret, schedule_channel=schedule_channel)
_slack_handler = SlackRequestHandler(app=app)

View file

@ -12,6 +12,7 @@ from slack_sdk import WebClient
from shared.blocks import build_pay_summary_blocks, build_week_schedule
from shared.schedule import FALLBACK_EXTENSION, ShiftSchedule
from shared.secrets import get_secret
logger = logging.getLogger()
logger.setLevel(logging.INFO)
@ -147,13 +148,8 @@ def handler(event, context):
)
return {"skipped": True}
ssm = boto3.client("ssm")
bot_token = ssm.get_parameter(
Name=os.environ["SLACK_BOT_TOKEN_PARAM"], WithDecryption=True
)["Parameter"]["Value"]
channel_id = ssm.get_parameter(
Name=os.environ["SHIFT_CHANNEL_PARAM"], WithDecryption=True
)["Parameter"]["Value"]
bot_token = get_secret(os.environ["SLACK_BOT_TOKEN_SECRET"])
channel_id = os.environ["SHIFT_CHANNEL"]
schedule = ShiftSchedule()
slack = WebClient(token=bot_token)

View file

@ -6,6 +6,9 @@ Parameters:
Timezone:
Type: String
Default: "America/New_York"
ShiftChannel:
Type: String
Description: Slack channel ID for schedule posts and shift notifications
SchedulerFunctionName:
Type: String
Default: "3cx-ring-group-scheduler"
@ -63,9 +66,9 @@ Resources:
Environment:
Variables:
SHIFT_TABLE: !Ref ShiftTable
SLACK_BOT_TOKEN_PARAM: /afterhours-shift-manager/slack-bot-token
SLACK_SIGNING_SECRET_PARAM: /afterhours-shift-manager/slack-signing-secret
SHIFT_CHANNEL_PARAM: /afterhours-shift-manager/channel-id
SLACK_BOT_TOKEN_SECRET: afterhours-shift-manager/slack-bot-token
SLACK_SIGNING_SECRET: afterhours-shift-manager/slack-signing-secret
SHIFT_CHANNEL: !Ref ShiftChannel
SCHEDULER_FUNCTION_NAME: !Ref SchedulerFunctionName
TZ: !Ref Timezone
Policies:
@ -74,16 +77,9 @@ Resources:
- Statement:
- Effect: Allow
Action:
- ssm:GetParameter
- secretsmanager:GetSecretValue
Resource:
- !Sub "arn:aws:ssm:${AWS::Region}:${AWS::AccountId}:parameter/afterhours-shift-manager/*"
- Effect: Allow
Action:
- kms:Decrypt
Resource: "*"
Condition:
StringEquals:
"kms:ViaService": !Sub "ssm.${AWS::Region}.amazonaws.com"
- !Sub "arn:aws:secretsmanager:${AWS::Region}:${AWS::AccountId}:secret:afterhours-shift-manager/*"
- Effect: Allow
Action:
- lambda:InvokeFunction
@ -108,8 +104,8 @@ Resources:
Environment:
Variables:
SHIFT_TABLE: !Ref ShiftTable
SLACK_BOT_TOKEN_PARAM: /afterhours-shift-manager/slack-bot-token
SHIFT_CHANNEL_PARAM: /afterhours-shift-manager/channel-id
SLACK_BOT_TOKEN_SECRET: afterhours-shift-manager/slack-bot-token
SHIFT_CHANNEL: !Ref ShiftChannel
SES_SENDER: noreply@seahaven.com
PAYROLL_RECIPIENTS: payroll@seahaven.com
PAY_REPORT_USER: U0A3SC48T47
@ -120,16 +116,9 @@ Resources:
- Statement:
- Effect: Allow
Action:
- ssm:GetParameter
- secretsmanager:GetSecretValue
Resource:
- !Sub "arn:aws:ssm:${AWS::Region}:${AWS::AccountId}:parameter/afterhours-shift-manager/*"
- Effect: Allow
Action:
- kms:Decrypt
Resource: "*"
Condition:
StringEquals:
"kms:ViaService": !Sub "ssm.${AWS::Region}.amazonaws.com"
- !Sub "arn:aws:secretsmanager:${AWS::Region}:${AWS::AccountId}:secret:afterhours-shift-manager/*"
- Effect: Allow
Action:
- ses:SendEmail
@ -164,7 +153,7 @@ Resources:
Environment:
Variables:
SHIFT_TABLE: !Ref ShiftTable
TCX_SSM_PREFIX: /3cx-scheduler
TCX_SECRET_PREFIX: afterhours-shift-manager/3cx-
SYNC_GROUP: DEFAULT
TZ: !Ref Timezone
Policies:
@ -173,19 +162,9 @@ Resources:
- Statement:
- Effect: Allow
Action:
- ssm:GetParametersByPath
- ssm:GetParameter
- ssm:GetParameters
- secretsmanager:GetSecretValue
Resource:
- !Sub "arn:aws:ssm:${AWS::Region}:${AWS::AccountId}:parameter/3cx-scheduler"
- !Sub "arn:aws:ssm:${AWS::Region}:${AWS::AccountId}:parameter/3cx-scheduler/*"
- Effect: Allow
Action:
- kms:Decrypt
Resource: "*"
Condition:
StringEquals:
"kms:ViaService": !Sub "ssm.${AWS::Region}.amazonaws.com"
- !Sub "arn:aws:secretsmanager:${AWS::Region}:${AWS::AccountId}:secret:afterhours-shift-manager/*"
Events:
# Daily at 6am ET (before the 7am schedule post and 8am 3CX scheduler)
# EST: 6am ET = 11:00 UTC (Nov-Mar)