diff --git a/README.md b/README.md index 1fd28bb..5a8dcea 100644 --- a/README.md +++ b/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 diff --git a/samconfig.toml.example b/samconfig.toml.example index 7066907..58979dc 100644 --- a/samconfig.toml.example +++ b/samconfig.toml.example @@ -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] diff --git a/src/receiver/app.py b/src/receiver/app.py index 7d2f7f2..22ec4cf 100644 --- a/src/receiver/app.py +++ b/src/receiver/app.py @@ -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) diff --git a/template.yaml b/template.yaml index 3a6aa60..1291310 100644 --- a/template.yaml +++ b/template.yaml @@ -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