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.
- **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).
## Setup
1. Store the Slack bot token:
1. Store the two Slack credentials in Secrets Manager:
```bash
aws secretsmanager create-secret \
--name expense-bot-slack-token \
--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:
```bash
cp samconfig.toml.example samconfig.toml
```
- `SlackBotTokenSecretArn` — ARN from step 1
- `SlackSigningSecret` — Slack app → Basic Information → Signing Secret
- `SlackBotTokenSecretArn` — ARN of the bot-token secret
- `SlackSigningSecretArn` — ARN of the signing-secret secret
3. Build and deploy:
```bash

View file

@ -9,7 +9,7 @@ capabilities = "CAPABILITY_IAM"
confirm_changeset = true
parameter_overrides = [
"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]

View file

@ -1,3 +1,4 @@
import base64
import hashlib
import hmac
import json
@ -7,15 +8,17 @@ import time
import boto3
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"]
_cached_signing_secret: str | None = None
def handler(event, context):
body = event.get("body") or ""
if event.get("isBase64Encoded"):
import base64
body = base64.b64decode(body).decode("utf-8")
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": ""}
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:
if not timestamp or not signature:
return False
@ -53,6 +65,7 @@ def verify_signature(body: str, timestamp: str, signature: str) -> bool:
return False
if abs(time.time() - ts) > 300:
return False
secret = get_signing_secret()
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)

View file

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