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.
|
- **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
|
||||||
|
|
|
||||||
|
|
@ -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]
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue