Skip to content

feat: add SendGrid Event Webhook signature verifier#26

Merged
nickmarden merged 4 commits into
mainfrom
feat/sendgrid-verifier
Jun 11, 2026
Merged

feat: add SendGrid Event Webhook signature verifier#26
nickmarden merged 4 commits into
mainfrom
feat/sendgrid-verifier

Conversation

@nickmarden

@nickmarden nickmarden commented May 11, 2026

Copy link
Copy Markdown
Contributor

Summary

  • Adds sendgrid verifier type for SendGrid Event Webhook (and Inbound Parse) authentication
  • Verifies ECDSA P-256 signature over SHA256(timestamp + raw_body), base64(ASN.1 DER) encoded in X-Twilio-Email-Event-Webhook-Signature
  • Public key accepted as PEM or as the base64-encoded DER (SPKI) that the SendGrid UI displays; optional max_timestamp_age enables replay protection
  • Wired through config validation, the handler builder, the Helm configmap.yaml, the example config, and the configure-route skill
  • SendGrid does not publish stable webhook source IPs, so routes rely on signature verification rather than an IP allowlist

Closes #25

Test plan

  • make check — lint, 100% coverage in touched packages, both binaries build
  • helm lint charts/gatekeeperd clean
  • Verifier unit tests cover: valid signature (PEM + base64 DER keys), missing/expired/unparseable timestamp, missing/invalid signature, wrong signing key, tampered payload, max_timestamp_age=0 skip path, and key-parsing error branches (empty, garbage, non-DER, RSA, wrong curve)
  • Handler tests cover successful sendgrid build path and invalid-key error path
  • Live test against a real SendGrid Event Webhook (cannot run from CI)

Bundled: VERSION binding fix

Alongside the SendGrid verifier this PR now also threads the build-time VERSION through to both binaries so released images stop reporting 0.1.0 regardless of their tag.

Files touched by this second commit (d008531):

  • Dockerfile, Dockerfile.relay: add ARG VERSION=dev and -trimpath -ldflags="-s -w -X main.version=${VERSION}" on go build.
  • .github/workflows/release.yml: pass build-args: VERSION=${{ steps.meta.outputs.VERSION }} on all four docker/build-push-action steps.
  • .github/workflows/pr-images.yml: same, using the existing TAG output (pr-N-<sha>).
  • cmd/gatekeeperd/main.go, cmd/gatekeeper-relay/main.go: default var version to "dev" (was "0.1.0").

Verified locally:

go build -ldflags "-X main.version=test-0.2.12" -o /tmp/gatekeeperd ./cmd/gatekeeperd
/tmp/gatekeeperd  # → "gatekeeperd test-0.2.12"

make check passes at 98.9% coverage.

Adds a `sendgrid` verifier type that authenticates SendGrid Event Webhook
(and Inbound Parse) deliveries using ECDSA P-256 over SHA-256 of
`timestamp + payload`. The public key is supplied as PEM or as the
base64-encoded DER (SubjectPublicKeyInfo) shown in the SendGrid UI. An
optional `max_timestamp_age` enables replay protection.

Closes #25
@sonarqubecloud

Copy link
Copy Markdown

@codecov

codecov Bot commented May 11, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

@github-actions

github-actions Bot commented May 11, 2026

Copy link
Copy Markdown

Docker Images Built

Images are available for testing:

# gatekeeperd
docker pull ghcr.io/tight-line/gatekeeperd:pr-26-6318176

# gatekeeper-relay
docker pull ghcr.io/tight-line/gatekeeper-relay:pr-26-6318176

docker-compose.yml

GATEKEEPERD_IMAGE=ghcr.io/tight-line/gatekeeperd:pr-26-6318176 \
RELAY_IMAGE=ghcr.io/tight-line/gatekeeper-relay:pr-26-6318176 \
docker-compose --profile relay up

Helm (values override)

image:
  repository: ghcr.io/tight-line/gatekeeperd  # or gatekeeper-relay
  tag: "pr-26-6318176"

Images expire ~15 days after PR closes.

@sonarqubecloud

Copy link
Copy Markdown

Same fix sgotel just got. Previously every gatekeeperd/gatekeeper-relay
image (0.1.0 through 0.2.11) reported "0.1.0" at startup because:
- The Dockerfile and Dockerfile.relay built with `go build` and no ldflags.
- Neither release.yml nor pr-images.yml passed any build-args.
- cmd/gatekeeperd/main.go (and the relay equivalent) hard-coded
  `var version = "0.1.0"`.

So `make-tag` would tag, Chart.yaml would bump, but the resulting binary
would lie about its version forever after.

Changes:
- Dockerfile / Dockerfile.relay: ARG VERSION=dev plus
  -trimpath -ldflags="-s -w -X main.version=${VERSION}" on the go build.
- release.yml: pass build-args VERSION=${{steps.meta.outputs.VERSION}}
  on all 4 docker/build-push-action steps (gatekeeperd + relay, amd64-
  fast + multi-arch).
- pr-images.yml: same, using TAG (pr-N-<sha>) so PR builds also report
  a useful version string.
- cmd/{gatekeeperd,gatekeeper-relay}/main.go: default `var version` to
  "dev" so an unset-ldflags build is honest about being unstamped.

Verified locally:
  go build -ldflags "-X main.version=test-0.2.12" -o /tmp/gatekeeperd ./cmd/gatekeeperd
  /tmp/gatekeeperd  → "gatekeeperd test-0.2.12"

Bundled with the SendGrid verifier PR since both want to ship together;
this PR's title remains accurate.
…ings

Snyk flagged a critical vulnerability in golang.org/x/net/idna@0.48.0
(Improper Authentication, SNYK-GOLANG-GOLANGORGXNETIDNA-17116876, fixed
in 0.54.0). Pulled in transitively through golang.org/x/crypto/acme/autocert.

Bumps:
- golang.org/x/net    0.48.0 → 0.56.0
- golang.org/x/crypto 0.47.0 → 0.53.0
- golang.org/x/sys    0.40.0 → 0.46.0
- golang.org/x/text   0.33.0 → 0.38.0

`make check` still passes (98.9% coverage, 0 lint, build OK).
@sonarqubecloud

Copy link
Copy Markdown

@nickmarden nickmarden merged commit 7a2ce5c into main Jun 11, 2026
10 checks passed
@nickmarden nickmarden deleted the feat/sendgrid-verifier branch June 11, 2026 21:14
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Add SendGrid Event Webhook signature verification

1 participant