Founder of ToDesktop
We recently added Debian APT repository support to ToDesktop. We needed to dynamically sign Debian repo manifests with PGP without exposing PGP private keys to unnecessary risk or turning our system into a tangled mess. The solution? Cloudflare Workers service bindings. They let us isolate sensitive operations, keep our codebase clean, and get isolated visibility into what's happening. Here's how we made it work.
Customers have been asking us for Debian APT repo support for a long time. We finally decided to do it. With most other solutions, the Debian manifest files are signed at build time. This didn't really jive with how we store version release info in our database and also it meant giving build containers access to the private keys — which we wanted to avoid.
We looked at letting our main CDN worker handle signing directly? But, that’s just as bad. Keys in the wrong place would expose unnecessary risk. We needed a way to sign on the fly without making things overly complexity.
With Cloudflare Workers service bindings, we split the load. The main CDN worker stays lean, serving files like a champ. When a request hits an uncached Debian Release
endpoint needing a signed manifest, it calls a separate PGP worker through a binding. The CDN worker doesn’t need to know how to sign—it just says, “Hey, handle this,” and gets a signed response back.
Service bindings let one worker call another’s functions like they’re local. Cloudflare’s magic makes it seamless. Here’s the gist in the CDN worker:
const signedContent = await env.SIGNING_WORKER.signMessage(content);
That env.SIGNING_WORKER
is the binding, pointing to our PGP worker. The CDN worker stays clean and clueless about signing operations and private keys.
It's not immediately obvious but this isn’t just a neat trick. It actually has a bunch of benefits for security, maintenance, and visibility. Here’s the breakdown:
Typescript helps make sure that the interface is correct, everyone knows what’s expected. The PGP worker’s interface looks like this:
import { WorkerEntrypoint } from 'cloudflare:workers';
export default class PgpWorker extends WorkerEntrypoint {
// we use openPGP.js to sign messages
public async signMessage(body: string, detached: boolean): Promise<Response>;
}
And the CDN worker's Env
:
import type PgpWorker from '@todesktop/pgp';
import { Service } from '@cloudflare/workers-types';
export interface Env {
ENVIRONMENT: 'dev' | 'prod';
R2_BUCKET: R2Bucket;
SIGNING_WORKER: Service<PgpWorker>;
}
Now, env.SIGNING_WORKER.signMessage
is type-safe with autocompletion.
By isolating our signing operations in a dedicated worker, we achieved better security through key isolation, improved visibility through focused monitoring, and simplified maintenance through clear separation of concerns.
The result is a system that's more secure, easier to maintain, and provides the on-the-fly signing capabilities our customers need.