A Cloudflare Worker that proxies OpenPGP Web Key Directory (WKD) requests to Proton Mail's API for your custom domains.
If you use Proton Mail with custom domains, this Worker enables WKD key discovery so that email clients can automatically find your OpenPGP public keys via the standard WKD protocol.
When an email client looks up an OpenPGP key for user@yourdomain.com, it queries either:
https://openpgpkey.yourdomain.com/hu/<hash>?l=user(direct method)https://yourdomain.com/.well-known/openpgpkey/hu/<hash>?l=user(advanced method)
This Worker intercepts those requests via Cloudflare route patterns and proxies them to Proton Mail's WKD endpoint, which serves the actual key data.
- Supports unlimited custom domains via a single Worker secret
- Handles both WKD direct (subdomain) and advanced (
.well-knownpath) methods - Dashboard-managed routes and a
DOMAINSWorker secret so real domains stay out of this public repo - One-time DNS setup for
openpgpkey.*subdomains - 100% test coverage with Cloudflare Workers vitest integration
- Full observability: structured logging, traces, and logpush
This is the primary guidance for operators and anyone forking the Worker.
Production DOMAINS is a Cloudflare Worker secret, not a plain Wrangler vars entry and not a GitHub Actions secret. The Worker reads env.DOMAINS at runtime as a comma-separated list of hostnames (no spaces required). Secrets bind the same way as vars, so existing code does not change.
Never commit the value. Do not put real domains in git, in wrangler.jsonc vars, in docs examples, in tests, or in GitHub Actions secrets or variables. This public repository has no GitHub DOMAINS secret.
Use either path. Do not put the value in the repo.
- Cloudflare dashboard: Worker
wkd-proxy-worker→ Settings → Secrets → add or updateDOMAINS. - Workstation CLI (account token on that machine only):
wrangler secret put DOMAINSPaste the comma-separated list when prompted, for example:
example.com,example.org,example.net
That example is documentation only. Production uses your real custom domains in the secret, never in git.
wrangler.jsonc lists DOMAINS under secrets.required. Workers Builds (pnpm deploy:cloudflare, which is wrangler deploy) fails closed if the secret is missing. A Worker that somehow ran without it would return HTTP 500 when env.DOMAINS is empty.
If DOMAINS still exists as a dashboard plain variable, convert it to a secret before the next Builds deploy. Secrets survive deploys. Leftover plains do not, because this project no longer uses keep_vars.
Tests (vitest.config.ts, test/index.spec.ts) and .dev.vars.example use only example.com, example.org, and example.net. Copy .dev.vars.example to .dev.vars for local wrangler dev. Never copy production domains into those files.
Fork this repo and clone your fork locally.
pnpm installIn the Cloudflare dashboard, open Worker wkd-proxy-worker → Settings →
Builds and connect this repository. Production branch: main. Deploy
command: pnpm deploy:cloudflare. Leave non-production branch builds off.
Set production DOMAINS as a Worker secret using Production DOMAINS secret. GitHub Actions has no Cloudflare deploy token.
For each domain, add these Worker routes in the dashboard:
| Pattern | Purpose |
|---|---|
openpgpkey.{domain}/* |
WKD direct method (subdomain) |
{domain}/.well-known/openpgpkey/* |
WKD advanced method (path) |
*.{domain}/.well-known/openpgpkey/* |
WKD advanced on any subdomain |
Create a proxied CNAME openpgpkey.{domain} → {domain}. If the root zone
has no A, AAAA, or CNAME (common for email-only domains), add proxied
placeholders 192.0.2.1 and 100:: so Cloudflare can intercept
.well-known requests. Existing website records stay untouched.
wrangler.jsonc sets workers_dev = false and secrets.required to
["DOMAINS"] and omits routes and vars. Wrangler still applies any
vars that are in the config, so DOMAINS must not appear there.
Push to main. GitHub Actions runs typecheck, lint, tests, and
cf:check. Cloudflare Workers Builds deploys the Worker.
curl https://openpgpkey.yourdomain.com/policyA 200 response confirms the Worker is serving WKD requests for that domain.
pnpm run devThis starts a local dev server. Copy .dev.vars.example to .dev.vars
so local DOMAINS uses the example list (example.com, example.org,
example.net). See Production DOMAINS secret.
pnpm run test # Run tests
pnpm run coverage # Run with 100% coverage enforcement
pnpm run typecheck # TypeScript strict mode
pnpm run lint # ESLint strict type-checkedTests inject the same example list. They never use production domains.
GitHub Actions validates the pull request. Cloudflare Workers Builds deploys
from main with pnpm deploy:cloudflare. That command is
wrangler deploy. Dashboard routes stay because routes is omitted.
Production DOMAINS is a Worker secret. Set and rotate it as described in
Production DOMAINS secret.
The Worker only intercepts the three WKD route patterns. Other hostname traffic is unchanged.
GitHub runs the MNPPI Public Token-Free Security check for each pull request. That org-required workflow uses hosted runners and no MNPPI secrets.
- Update the Worker
DOMAINSsecret (dashboard Secrets orwrangler secret put DOMAINS). Never commit the value. See ProductionDOMAINSsecret. - Add or remove the three route patterns for that domain.
- Add or delete the
openpgpkey.*CNAME. Add a root placeholder only when the zone has no A, AAAA, or CNAME.
- Custom domains added to Cloudflare (DNS managed by Cloudflare)
- Proton Mail account with those custom domains configured
- OpenPGP keys published in Proton Mail for the email addresses you want discoverable
This project is licensed under the GNU Affero General Public License v3.0.