OperatePrivate dashboard
Operate

Private dashboard

Keep the checkout and the merchant API public, and make the dashboard reachable only for your team.

Markdown

Customers need the checkout. Your shop needs the merchant API. Only your team needs the dashboard, and the dashboard is where money moves. Block the dashboard on every public host name. Then reach it through a private entrance.

Two-factor codes still apply. These steps reduce who can try a password at all.

Public and private paths

Path Who needs it
/i/, /pay/ Customers (checkout)
/x/ Customers and the exchange (pay with any coin)
/v1/ Your shop's server (merchant API)
/assets/, /branding/, /favicon.svg Customers (checkout styles, images and project logos)
/healthz Uptime monitors
/dashboard and everything under it Only your team

/ redirects to /dashboard.

With a path in the site URL, add the prefix. For example, /turmite/dashboard.

A checkout domain serves only the paths above, without /dashboard. It answers 404 for every other path. The steps below are for the host of the site URL.

Option 1: Tailscale

Tailscale makes a private network between your devices and the server. The dashboard gets an address that only your devices can open.

  1. Install Tailscale on the server and sign in:

    curl -fsSL https://tailscale.com/install.sh | sh
    sudo tailscale up
  2. Install Tailscale on your laptop and phone. Sign in with the same account.

  3. In the Tailscale admin console, turn on MagicDNS and HTTPS certificates.

  4. Run tailscale status on the server. Note its name, for example turmite.example-tailnet.ts.net.

  5. Add a private entrance to the Caddyfile. Replace both host names with your own:

    http://:8081 {
    	reverse_proxy api:8080 {
    		header_up Host pay.example.com
    		header_up Origin ^https://turmite\.example-tailnet\.ts\.net$ https://pay.example.com
    	}
    }
  6. Publish port 8081 on the loopback interface only, in docker-compose.override.yml:

    services:
      caddy:
        ports:
          - "127.0.0.1:8081:8081"
  7. Start Caddy, then give the port to Tailscale:

    cd /opt/turmite
    sudo docker compose up -d caddy
    sudo tailscale serve --bg http://127.0.0.1:8081
  8. Open https://turmite.example-tailnet.ts.net/dashboard/login from a device on your tailnet.

Docker Compose reads docker-compose.override.yml automatically. install.sh --upgrade keeps it and your Caddyfile.

Turmite accepts a dashboard POST only when its Origin matches the origin of the site URL. The entrance sends the site URL's host to Turmite. The Origin line changes only requests that come from your tailnet name. Do not replace it with a fixed value. A fixed value turns off the cross-site check for this entrance.

If the .ts.net name does not resolve on your laptop, turn on Tailscale DNS there: sudo tailscale set --accept-dns=true.

Alternative: a site URL on the tailnet

You can also set TURMITE_SITE_URL to the tailnet name and give every project a checkout domain. Payment and invoice links then use the checkout domain of the project. A checkout domain serves the checkout and the merchant API, and never the dashboard. Webhooks leave the server directly, so they still work.

This setup needs more changes:

  • Caddy cannot get a public certificate for a .ts.net name. Serve the site URL through tailscale serve, and give Caddy a site block for each checkout domain.
  • Turmite gives the exchange a webhook URL on the site URL, which the exchange cannot reach. The worker also polls each swap every 10 seconds, so swaps still complete.

Option 2: Cloudflare Access

Cloudflare Access (Zero Trust) asks for a team sign-in before a request reaches the server.

  1. In Cloudflare Zero Trust, open Access → Applications.
  2. Add a self-hosted application.
  3. Set the domain to pay.example.com and the path to dashboard.
  4. Add a policy that allows only your team's email addresses.
  5. Save the application.

The checkout and the merchant API stay open, because the application covers only /dashboard.

Option 3: Cloudflare WAF rule

A WAF custom rule blocks the dashboard on the public host names.

  1. In Cloudflare, open the domain, then Security → WAF → Custom rules.

  2. Select Create rule, then Edit expression.

  3. Paste this expression:

    (http.host in {"pay.example.com"} and starts_with(http.request.uri.path, "/dashboard"))
  4. Set the action to Block.

  5. Deploy the rule.

To keep the dashboard open from your office, add your addresses before the last bracket:

(http.host in {"pay.example.com"} and starts_with(http.request.uri.path, "/dashboard") and not ip.src in {203.0.113.10})

Add each public host name to the {...} list, separated by spaces. A host in another Cloudflare zone needs the same rule in that zone.

Option 4: block on the server

A Cloudflare rule protects only traffic that goes through Cloudflare. To make the server refuse the dashboard on public names, add two lines to each public site block in the Caddyfile:

{$TURMITE_CADDY_ADDRESS} {
	import turmite
	@dashboard path /dashboard /dashboard/*
	respond @dashboard 404
}

Then run sudo docker compose restart caddy.

Do not add these lines to the private entrance on port 8081. If you do, you lock yourself out.

Use Tailscale to sign in. Use a WAF rule or the Caddy block on every public host name. Use the Caddy block when you do not use Cloudflare.

Check the result

From a browser that is not on your tailnet:

  1. https://pay.example.com/dashboard/login is blocked or returns 404.
  2. https://pay.example.com/healthz returns ok.
  3. A payment link from your shop opens the checkout.

From a device on your tailnet:

  1. The dashboard opens on the private entrance.
  2. You can sign in and save a setting.