zudo-slack-notify

Type to search...

to open search from anywhere

Secrets and rotation

The secret inventory, pnpm ops:push-secrets, rotating the relay key and Slack token, and when to require secrets.

Secret inventory

SecretLives inUsed by
NOTIFY_API_KEYWorker secret; operator store worker.env; sender env ZUDO_SLACK_NOTIFY_API_KEYWorker, and every sender
SLACK_BOT_TOKENWorker secret; operator store worker.envWorker only
SLACK_TARGETSWorker secret; operator store worker.envWorker only
ZUDO_SLACK_NOTIFY_URLOperator store sender.envSenders (not sensitive)
CLOUDFLARE_API_TOKENGitHub repository secretDeploy workflows only
CLOUDFLARE_ACCOUNT_IDGitHub repository secretDeploy workflows only

Three homes, no others:

  • Cloudflare Worker secrets: the runtime values.

  • GitHub repository secrets: Cloudflare deploy credentials only. Never the Slack token or relay key.

  • The operator store at $DROPBOX_ROOT/env/zudo-slack-notify/: the source you push from, credentials/worker.env and credentials/sender.env.

Senders never hold SLACK_BOT_TOKEN or a Cloudflare token.

pnpm ops:push-secrets

pnpm ops:push-secrets runs scripts/push-worker-secrets.mjs.

  • Reads $DROPBOX_ROOT/env/zudo-slack-notify/credentials/worker.env. Use --env-file <path> to read another file; if DROPBOX_ROOT is unset and no --env-file is given, it exits with an error. The file is KEY=value lines; comments, blank lines, an export prefix, and one pair of surrounding quotes are accepted.

  • Uploads only the non-empty values of NOTIFY_API_KEY, SLACK_BOT_TOKEN, and SLACK_TARGETS, in a single wrangler secret bulk run against app/. The temporary file it hands to wrangler is private and is removed afterwards.

  • Never prints values. Output is one line per name: NAME: set, NAME: skipped (empty), or NAME: would set (dry run). Validation errors name the key and the rule, never the value.

  • Validates shape first, with the same checks the Worker applies at runtime (it imports them from app/src/notification.ts): the key is 32 to 256 printable ASCII characters without whitespace, the token is xoxb- followed by 5 to 495 letters, digits, or dashes, and the targets are a JSON object of at most 16 KiB with 1 to 100 aliases, each lowercase kebab-case (^[a-z][a-z0-9]*(?:-[a-z0-9]+)*$, at most 64 characters) and mapped to a channel ID matching ^[CGD][A-Z0-9]{8,63}$.

  • If any non-empty value is invalid, it uploads nothing at all. It also fails if every value is empty.

  • --dry-run validates and reports which names it would upload, without uploading.

Exit codes: 0 success (or a valid dry run), 1 a value failed validation, nothing was left to upload, or wrangler failed, 2 a usage problem (unknown argument, missing --env-file path, unreadable env file, no DROPBOX_ROOT).

pnpm ops:push-secrets --dry-run
pnpm ops:push-secrets
pnpm ops:push-secrets --env-file ./worker.local.env

Empty means skip, not delete

A blank value in worker.env leaves the deployed secret untouched, and the script prints the wrangler secret delete command for each skipped name. To revoke one, delete it explicitly from app/:

cd app
pnpm exec wrangler secret delete SLACK_BOT_TOKEN

Deleting a secret makes the Worker answer 503 server_misconfigured again.

Rotate the relay key

  1. Generate a new key: openssl rand -hex 32.

  2. Update NOTIFY_API_KEY in worker.env and ZUDO_SLACK_NOTIFY_API_KEY in sender.env.

  3. Run pnpm ops:push-secrets.

The old key stops working as soon as the new secret is live, so update every sender file around the same time. Until they match, senders get 401 unauthorized (CLI exit 1).

Rotate the Slack token

  1. In the Slack app dashboard, reinstall the app or regenerate the token and copy the new xoxb- value.

  2. Update SLACK_BOT_TOKEN in worker.env.

  3. Run pnpm ops:push-secrets.

No sender change is needed. A revoked or stale token shows up as slack_rejected with invalid_auth, token_revoked, or token_expired in the message.

Change targets

Edit SLACK_TARGETS in worker.env and push. Invite the bot to any new channel first, or sends fail with not_in_channel. An alias removed from the map returns 403 target_not_allowed.

When to add [secrets] required

app/wrangler.toml deliberately has no [secrets] required block yet. The first deploy happens before the Slack bot token exists, so the Worker must deploy without it and fail closed. Once Slack is wired and all three secrets exist on the deployed Worker, add:

[secrets]
required = ["NOTIFY_API_KEY", "SLACK_BOT_TOKEN", "SLACK_TARGETS"]

so a later deploy cannot silently ship without them. Keep the top-level scalars above every [table] in the file.

The SLACK_WIRED smoke variable

The post-deploy smoke test posts to /v1/notify without credentials and inspects the status. The GitHub repository variable SLACK_WIRED selects what is acceptable:

SLACK_WIREDUnauthenticated POST answer
not true (default)401 passes, and 503 with code server_misconfigured is also tolerated
trueOnly 401 passes; any 503 fails

Any success response fails in both modes, since an unauthenticated POST must never succeed. deploy-app.yml passes the repository variable to the script as SLACK_WIRED.

Set it to true after the first delivered message, as described in Send the first message. After that, a 503 means a secret went missing and the smoke run fails.

Revision History

CreatedUpdated