Move Slack signing secret to Secrets Manager

This commit is contained in:
Adam Moussa 2026-04-20 18:01:48 -04:00
parent bede364f16
commit f07ce858a4
4 changed files with 31 additions and 12 deletions

View file

@ -16,25 +16,29 @@ Channel chain: Submitted → Processed → Authorized → Matched.
- **Receiver Lambda** (`src/receiver/`) — fronted by API Gateway HTTP API. Verifies the Slack request signature, handles the URL-verification handshake, and async-invokes the processor. Returns 200 within Slack's 3-second window to prevent retries. - **Receiver Lambda** (`src/receiver/`) — fronted by API Gateway HTTP API. Verifies the Slack request signature, handles the URL-verification handshake, and async-invokes the processor. Returns 200 within Slack's 3-second window to prevent retries.
- **Processor Lambda** (`src/processor/`) — does the 5-step sequence of Slack API calls. No inbound HTTP; invoked only by the receiver. - **Processor Lambda** (`src/processor/`) — does the 5-step sequence of Slack API calls. No inbound HTTP; invoked only by the receiver.
- **Secrets** — Slack bot token in AWS Secrets Manager. Signing secret passed as a SAM template parameter (`NoEcho`). - **Secrets** — Slack bot token and signing secret each stored as separate AWS Secrets Manager secrets. Each Lambda only has IAM access to the secret it needs.
Runtime: Python 3.12 on arm64, no third-party dependencies (stdlib `urllib` for HTTP, `boto3` from the Lambda runtime). Runtime: Python 3.12 on arm64, no third-party dependencies (stdlib `urllib` for HTTP, `boto3` from the Lambda runtime).
## Setup ## Setup
1. Store the Slack bot token: 1. Store the two Slack credentials in Secrets Manager:
```bash ```bash
aws secretsmanager create-secret \ aws secretsmanager create-secret \
--name expense-bot-slack-token \ --name expense-bot-slack-token \
--secret-string "xoxb-..." --secret-string "xoxb-..."
aws secretsmanager create-secret \
--name expense-bot-slack-signing-secret \
--secret-string "..."
``` ```
2. Copy the sample SAM config and fill in real values: 2. Copy the sample SAM config and fill in real values:
```bash ```bash
cp samconfig.toml.example samconfig.toml cp samconfig.toml.example samconfig.toml
``` ```
- `SlackBotTokenSecretArn` — ARN from step 1 - `SlackBotTokenSecretArn` — ARN of the bot-token secret
- `SlackSigningSecret` — Slack app → Basic Information → Signing Secret - `SlackSigningSecretArn` — ARN of the signing-secret secret
3. Build and deploy: 3. Build and deploy:
```bash ```bash

View file

@ -9,7 +9,7 @@ capabilities = "CAPABILITY_IAM"
confirm_changeset = true confirm_changeset = true
parameter_overrides = [ parameter_overrides = [
"SlackBotTokenSecretArn=arn:aws:secretsmanager:us-east-1:ACCOUNT_ID:secret:expense-bot-slack-token-XXXXXX", "SlackBotTokenSecretArn=arn:aws:secretsmanager:us-east-1:ACCOUNT_ID:secret:expense-bot-slack-token-XXXXXX",
"SlackSigningSecret=REPLACE_WITH_SLACK_APP_SIGNING_SECRET", "SlackSigningSecretArn=arn:aws:secretsmanager:us-east-1:ACCOUNT_ID:secret:expense-bot-slack-signing-secret-XXXXXX",
] ]
[default.build.parameters] [default.build.parameters]

View file

@ -1,3 +1,4 @@
import base64
import hashlib import hashlib
import hmac import hmac
import json import json
@ -7,15 +8,17 @@ import time
import boto3 import boto3
lambda_client = boto3.client("lambda") lambda_client = boto3.client("lambda")
secrets_client = boto3.client("secretsmanager")
SIGNING_SECRET = os.environ["SLACK_SIGNING_SECRET"] SLACK_SIGNING_SECRET_ARN = os.environ["SLACK_SIGNING_SECRET_ARN"]
PROCESSOR_FUNCTION_NAME = os.environ["PROCESSOR_FUNCTION_NAME"] PROCESSOR_FUNCTION_NAME = os.environ["PROCESSOR_FUNCTION_NAME"]
_cached_signing_secret: str | None = None
def handler(event, context): def handler(event, context):
body = event.get("body") or "" body = event.get("body") or ""
if event.get("isBase64Encoded"): if event.get("isBase64Encoded"):
import base64
body = base64.b64decode(body).decode("utf-8") body = base64.b64decode(body).decode("utf-8")
headers = {k.lower(): v for k, v in (event.get("headers") or {}).items()} headers = {k.lower(): v for k, v in (event.get("headers") or {}).items()}
@ -44,6 +47,15 @@ def handler(event, context):
return {"statusCode": 200, "body": ""} return {"statusCode": 200, "body": ""}
def get_signing_secret() -> str:
global _cached_signing_secret
if _cached_signing_secret:
return _cached_signing_secret
result = secrets_client.get_secret_value(SecretId=SLACK_SIGNING_SECRET_ARN)
_cached_signing_secret = result["SecretString"]
return _cached_signing_secret
def verify_signature(body: str, timestamp: str, signature: str) -> bool: def verify_signature(body: str, timestamp: str, signature: str) -> bool:
if not timestamp or not signature: if not timestamp or not signature:
return False return False
@ -53,6 +65,7 @@ def verify_signature(body: str, timestamp: str, signature: str) -> bool:
return False return False
if abs(time.time() - ts) > 300: if abs(time.time() - ts) > 300:
return False return False
secret = get_signing_secret()
base = f"v0:{timestamp}:{body}".encode("utf-8") base = f"v0:{timestamp}:{body}".encode("utf-8")
expected = "v0=" + hmac.new(SIGNING_SECRET.encode("utf-8"), base, hashlib.sha256).hexdigest() expected = "v0=" + hmac.new(secret.encode("utf-8"), base, hashlib.sha256).hexdigest()
return hmac.compare_digest(expected, signature) return hmac.compare_digest(expected, signature)

View file

@ -6,10 +6,9 @@ Parameters:
SlackBotTokenSecretArn: SlackBotTokenSecretArn:
Type: String Type: String
Description: ARN of the Secrets Manager secret containing the Slack bot token (xoxb-...) Description: ARN of the Secrets Manager secret containing the Slack bot token (xoxb-...)
SlackSigningSecret: SlackSigningSecretArn:
Type: String Type: String
Description: Slack app signing secret (for verifying Event API requests) Description: ARN of the Secrets Manager secret containing the Slack app signing secret
NoEcho: true
Globals: Globals:
Function: Function:
@ -46,13 +45,16 @@ Resources:
Environment: Environment:
Variables: Variables:
PROCESSOR_FUNCTION_NAME: !Ref ProcessorFunction PROCESSOR_FUNCTION_NAME: !Ref ProcessorFunction
SLACK_SIGNING_SECRET: !Ref SlackSigningSecret SLACK_SIGNING_SECRET_ARN: !Ref SlackSigningSecretArn
Policies: Policies:
- Version: '2012-10-17' - Version: '2012-10-17'
Statement: Statement:
- Effect: Allow - Effect: Allow
Action: lambda:InvokeFunction Action: lambda:InvokeFunction
Resource: !GetAtt ProcessorFunction.Arn Resource: !GetAtt ProcessorFunction.Arn
- Effect: Allow
Action: secretsmanager:GetSecretValue
Resource: !Ref SlackSigningSecretArn
Events: Events:
SlackEvents: SlackEvents:
Type: HttpApi Type: HttpApi