Skip to content

Feature request: DNS provider integration (Cloudflare) for automatic records, wildcard certs, and tunnels #37

Description

@useifjr

Summary

Openship manages custom domains and issues Let's Encrypt certificates, but has no DNS provider integration. I'd like to add one, starting with Cloudflare, and I'm happy to implement it — I have a design worked out and wanted to check direction with you before opening a PR.

Motivation

Adding a custom domain today means copying the A record and the _openship-challenge TXT record out of the dashboard, creating them by hand at the registrar, then coming back to click Verify. That works, but it's the manual step that most separates the self-hosted experience from Vercel/Netlify.

It also blocks two things:

  1. Wildcard certificates. The README advertises wildcards, but Let's Encrypt only issues those via the DNS-01 challenge, which needs write access to a DNS zone. Without a DNS integration there's no way to fully automate it.
  2. cloudflare.provider.ts in the tunneling module is currently a stub that throws ProviderNotImplementedError. Its header comment already sketches the intended design (quick tunnel via cloudflared --url, named tunnel via API token), and the named-tunnel flow needs exactly the same credential storage and DNS record writes.

Cloudflare is the most common DNS provider among self-hosters and the API is free, so one integration covers all three.

Proposal

A new apps/api/src/modules/dns/ module structured to mirror modules/tunneling/ — a DnsProvider interface, a registry.ts with the same resolveProvider / listProviders / describeProviders shape, and providers/cloudflare.provider.ts as the first implementation. Adding Route53 or DigitalOcean later would be one file plus one registry line.

The interface stays small: preflight, findZone, listRecords, upsertRecord, deleteRecord. findZone walks the hostname's labels from most to least specific and returns null when nothing matches, since "this domain isn't on Cloudflare" is an expected outcome rather than an error.

Credentials would reuse the pattern backup_destination already established: an org-scoped row with the token encrypted through encryptSecretField() and the existing enc1: AES-256-GCM envelope. No new crypto, and scoped API tokens only (Zone:Read + DNS:Edit), with the legacy Global API Key rejected at connect time.

Three features on top of that layer:

  1. Automatic records. addDomain() hooks in right where it currently calls buildRecords() — if a connected provider owns the zone, create the routing record and the challenge TXT, then fire verification in the background the same way verifyDomain() already fires SSL provisioning. removeDomain() cleans up, best-effort.
  2. DNS-01 for wildcards. An optional { challenge } hint on SslProvider.provisionCert (defaulted, so existing implementations keep compiling). Certbot runs with manual auth/cleanup hooks that call back into the API over loopback with a short-lived single-use token, so credentials stay in the API process and never touch a remote deploy target's disk — which matters given resolveSslProvider() may run certbot over SSH.
  3. Cloudflare Tunnel. Implement the existing stub to the design its own comment describes, reusing the credential store for the named-tunnel flow.

The important property: with no provider connected, every path falls back to exactly today's behavior. Provider failures — token expired, rate limited, Cloudflare down — degrade to the manual flow and never block adding a domain, removing one, or deploying.

A detail worth calling out

Records would be created with proxied: false. The orange-cloud proxy terminates TLS at the edge and intercepts /.well-known/acme-challenge/, which breaks the HTTP-01 challenge the certbot path depends on. DNS-only is the correct default; once DNS-01 lands, proxying becomes safe to opt into.

Relationship to #12

#12 asks for the opposite direction and I think the two are complementary rather than overlapping: it wants Openship to stand back when ingress and TLS are managed externally (skip certbot, don't require the domain to resolve to the SSH address). This proposal is for when the user wants Openship to drive DNS on their behalf. Both are useful, and they'd be selected by different users with different setups — neither forecloses the other.

Questions before I start

  1. Is a DNS provider abstraction something you want in the codebase, or would you rather keep DNS entirely out of scope?
  2. Would you prefer this as one PR or split — provider layer first, then DNS-01, then the tunnel?
  3. Any objection to a new dns_credential table, or would you rather this live somewhere existing?

Happy to adjust the design to whatever fits your plans. If you'd rather not take this at all, no hard feelings — better to ask first than show up with an unwanted PR.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions