mirror of
https://github.com/Sea-Haven-Industries/expense-approval-bot.git
synced 2026-05-18 20:20:14 +00:00
Move Slack signing secret to Secrets Manager
This commit is contained in:
parent
bede364f16
commit
f07ce858a4
4 changed files with 31 additions and 12 deletions
12
README.md
12
README.md
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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]
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue