Cloudflare deployment

August 20, 2026 ยท View on GitHub

The adapter is optional. DSH Remote works with any identity-aware HTTPS reverse proxy that preserves the external Host and publishes only /m.

Before starting

  1. Keep the DSH web server on 127.0.0.1. The macOS wizard also checks the actual listening socket with lsof.
  2. Install cloudflared yourself and verify it is on PATH. The plugin never downloads, installs, or updates it.
  3. Choose an unused hostname exactly one label below an active Cloudflare zone, such as dsh.example.com. This is the depth covered by Universal SSL on a full DNS setup. Deeper names require Total TLS or an advanced/custom certificate and are intentionally left to manual setup.
  4. Create a scoped API Token with the five permissions shown in the local UI.
  5. Decide which single email address may receive the Access OTP.

The Token can be pasted for one request. On macOS, a plugin config may instead name an existing generic-password Keychain item. Status checks query only item metadata; -w is added only to an explicit plan/apply/rollback operation. The secret is held in memory for that request and never returned or journaled.

Guided flow

Open http://127.0.0.1:3080/remote/ on the computer.

  1. Preflight checks local /m, loopback binding, the existing cloudflared binary, Token access, account, active zone, DNS conflicts, tunnel names, Access apps, policies, and OTP providers.
  2. Plan lists every resource and whether macOS launchd can be automated.
  3. Confirm requires typing the exact hostname.
  4. Apply creates a locally managed named tunnel, proxied CNAME, OTP IdP when absent, self-hosted Access app, exact-email Allow policy requiring that OTP login method, local credentials/config, and on macOS an exact launchd job.
  5. Verify waits within a 95-second propagation deadline and requires an unauthenticated request to receive Cloudflare Access's login challenge. A public HTTP 200 is treated as a failure because it would mean Access is not enforced.
  6. Phone test opens /m/, completes the emailed OTP, then uses the fragment-only one-time pairing link issued by the local control page.

Ingress invariant

The generated local tunnel config is equivalent to:

ingress:
  - hostname: dsh.example.com
    path: ^/m(?:/.*)?$
    service: http://127.0.0.1:3080
  - service: http_status:404

There is deliberately no httpHostHeader override. Rewriting Host to 127.0.0.1 or localhost would make a public tunnel request look local to the plugin and is prohibited.

Recovery and rollback

The journal is written before each external create. A returned Cloudflare ID is persisted before advancing. If the process dies after a create request but before an ID was saved, the wizard performs an exact read-back. Zero candidates clears the intent and permits a safe retry. Any candidate keeps ownership ambiguous, so retry and rollback stop without deleting anything; the operator must inspect that resource manually.

Rollback performs all drift checks before the first delete. It checks resource IDs and expected fields, unknown Access child policies, local file hashes, and the full launchd argument array. Any drift means zero deletion. When safe, it unloads launchd, removes the Access app/policy, DNS record, tunnel, owned config files, and saved public origin. It retains the account-global OTP provider, logs, and the audit journal.

Official API references: