Skip to content

Security & permissions

Security guidance for Notifycat lives in a few focused pages — Slack scopes in Slack app setup, signature verification in GitHub webhook setup and Bitbucket webhook setup, and vulnerability reporting in the repository's security policy. This page pulls the operational answer to "is my install safe?" into one checklist and explains the least-privilege model Notifycat is built around.

Least-privilege checklist

Run through this before you point a production webhook at Notifycat:

  • [ ] .env is owned by you and not world-readable (0600). The setup wizard writes it this way; see File permissions to confirm or fix an existing install.
  • [ ] GITHUB_WEBHOOK_SECRET (GitHub) or BITBUCKET_WEBHOOK_SECRET (Bitbucket) was generated with openssl rand -base64 32 and is set to the same value in .env and in the webhook settings. Only the secret for your active git_provider is required.
  • [ ] Unless you use per-path routing, the running server has no read token configured — it needs only the webhook secret to verify deliveries.
  • [ ] If you set the optional GITHUB_TOKEN or BITBUCKET_TOKEN, it is read-only (fine-grained scopes for webhook config reads and PR file lists only), not a write-scoped token.
  • [ ] The Slack bot has only the documented scopes — nothing broader.
  • [ ] Public traffic terminates TLS in front of Notifycat (the Compose install does this with Caddy); the server itself speaks plain HTTP only on the internal network.

What Notifycat asks for — and what it doesn't

Notifycat is not a GitHub App or a Bitbucket App. It receives ordinary repository webhooks authenticated by a shared secret. There is no OAuth flow, no installation, and no app-level permission grant to review.

The runtime server needs exactly two secrets:

Secret Why Scope
GITHUB_WEBHOOK_SECRET or BITBUCKET_WEBHOOK_SECRET Verify the HMAC signature on every incoming webhook. Only the one that matches your git_provider is required. A shared string — not a token, grants no API access.
SLACK_BOT_TOKEN Post, update, and react on the PR's Slack message. Only the bot scopes Notifycat documents.

It deliberately never requires org-admin access, write-scoped tokens, a GitHub App installation, or broad Slack scopes such as chat:write on every channel. If a setup guide ever asks you to over-provision beyond the table above, that is a bug.

The optional read-only tokens

By default neither GITHUB_TOKEN nor BITBUCKET_TOKEN is used by the server. They are read by notifycat-config validate (and the doctor) to query webhook configuration and confirm the expected PR events are subscribed. Without them those checks are skipped and everything else works.

The one runtime use for either token is per-path routing: when a repository's mapping has a paths: block, the server reads each PR's changed files to pick the path channel. Without a token, path rules are inert and PRs route to the repository tier.

For GitHub: a fine-grained token with Webhooks: Read is enough for validation; add Pull requests: Read for per-path routing. See GitHub webhook setup for details. For Bitbucket: an access token with repository + pullrequest + webhook scopes covers both uses; a scoped Atlassian API token with the equivalent read:*:bitbucket scopes works too. See Bitbucket webhook setup for the full options including the Free-plan Basic-auth path.

Generating the webhook secret

Generate the secret — GITHUB_WEBHOOK_SECRET or BITBUCKET_WEBHOOK_SECRET, depending on your provider — with:

openssl rand -base64 32

Set the output as the value in .env and in the webhook's secret field, byte-identical — paste, don't retype.

Why exactly this way: the signature check accepts any byte string (HMAC-SHA256 has no format requirement), but a hand-made secret containing $, #, quotes, or spaces routinely gets mangled by .env parsing or shell interpolation — and the resulting one-byte difference fails every delivery with 401. Base64 output is strong (256 bits of randomness) and survives .env files, shells, and Compose untouched.

Two rules follow:

  • One secret per deployment. Every webhook pointing at this Notifycat instance must carry the same secret. A webhook created with a different one 401s while the rest work — and validate cannot catch it, because the git host's API never returns webhook secrets.
  • Store it unquoted in .env. The value is read verbatim, so quotes that reach it become part of the secret and break the HMAC.

Rotating the webhook secret

  1. Generate a new secret with the same command.
  2. Update GITHUB_WEBHOOK_SECRET / BITBUCKET_WEBHOOK_SECRET in .env.
  3. Update the webhook's secret field to the same value.
  4. Restart Notifycat if your runtime does not reload environment variables.

Deliveries sent between steps 2 and 3 fail with 401 — keep the window short, then redeliver the failed events from the webhook's delivery history.

Signature validation — why the secret matters

Notifycat verifies an HMAC-SHA256 signature on every incoming request and rejects anything without a valid signature before the payload is parsed. The two providers differ only in the header name:

Provider Header Value format
GitHub X-Hub-Signature-256 sha256=<hex-digest>
Bitbucket X-Hub-Signature sha256=<hex-digest>

Both sign the raw request body with the configured webhook secret. The shared secret is the only thing standing between your Slack channel and a forged PR event, so treat it like a password: long, random, stored in your secret manager, and rotated if exposed. Unsigned requests (no header) are rejected with 401 on both providers — Bitbucket sends no header at all when the webhook secret field is left blank, so the secret is mandatory. Signature-troubleshooting checklists are in GitHub webhook setup → Signature verification and Bitbucket webhook setup → Signature verification.

For Bitbucket deployments, optional IP allowlisting via ip-ranges.atlassian.com provides an additional layer of defense — see Bitbucket webhook setup → Optional IP allowlisting.

File permissions

.env holds your webhook secret and Slack token, so it must not be world-readable. The notifycat setup wizard writes it as 0600 (owner read/write only) and prints .env written (0600) when it does.

To confirm or fix the permissions on an existing install:

ls -l .env            # expect: -rw------- (0600)
chmod 600 .env        # fix if it is anything more permissive

.env is gitignored — never commit it, and never paste its contents into an issue or PR. The same applies to config.yaml and anything under data/.

Reporting a vulnerability

Found a security issue? Do not open a public issue. Follow the private process in the security policy.