|
| 1 | +--- |
| 2 | +title: Split-domain WebFinger |
| 3 | +description: Run Hollo at one domain while addressing accounts under another. |
| 4 | +--- |
| 5 | + |
| 6 | +import { Aside, Code, Tabs, TabItem } from '@astrojs/starlight/components'; |
| 7 | + |
| 8 | +Hollo supports a *split-domain WebFinger* setup: your fediverse handles |
| 9 | +live under one domain (e.g. `@alice@example.com`) while the actual |
| 10 | +ActivityPub server runs under another (e.g. `https://ap.example.com`). |
| 11 | +This is the same pattern Mastodon documents under "[hosting WebFinger |
| 12 | +at the root domain][mastodon-webfinger]" and that GoToSocial calls |
| 13 | +*host-meta*-based host swapping. |
| 14 | + |
| 15 | +This is implemented on top of [Fedify's `origin` configuration option]. |
| 16 | + |
| 17 | +[mastodon-webfinger]: https://docs.joinmastodon.org/admin/config/#web_domain |
| 18 | +[Fedify's `origin` configuration option]: https://fedify.dev/manual/federation#separating-webfinger-host-from-the-server-origin |
| 19 | + |
| 20 | + |
| 21 | +Why split the domain? |
| 22 | +--------------------- |
| 23 | + |
| 24 | +The typical reason is that you already own a nice short domain |
| 25 | +(`example.com`) and want your handles to look like |
| 26 | +`@alice@example.com`, but you don't want to host Hollo at the apex of |
| 27 | +that domain — the apex is reserved for a homepage, a different web app, |
| 28 | +or an existing service. |
| 29 | + |
| 30 | +In a split-domain setup: |
| 31 | + |
| 32 | + - Users address you as `@alice@example.com` everywhere in the |
| 33 | + fediverse. |
| 34 | + - Hollo itself is served from `https://ap.example.com`. The web |
| 35 | + UI, the Mastodon-compatible API, OAuth, and the actor URIs all |
| 36 | + live there. |
| 37 | + - The apex domain `example.com` only needs to handle one thing: |
| 38 | + redirecting `/.well-known/webfinger` requests over to |
| 39 | + `ap.example.com`. |
| 40 | + |
| 41 | +<Aside type="caution"> |
| 42 | + Pick the handle domain carefully — you can't change it after |
| 43 | + federation has begun without breaking every remote follow. See the |
| 44 | + warning at the bottom of this page. |
| 45 | +</Aside> |
| 46 | + |
| 47 | + |
| 48 | +Configuration |
| 49 | +------------- |
| 50 | + |
| 51 | +Set both of these environment variables on your Hollo instance: |
| 52 | + |
| 53 | + - [`HANDLE_HOST`](/install/env/#handle_host) — the bare hostname |
| 54 | + used in handles (no scheme, no path), e.g. `example.com`. |
| 55 | + - [`WEB_ORIGIN`](/install/env/#web_origin) — the scheme + host |
| 56 | + where Hollo runs, e.g. `https://ap.example.com`. |
| 57 | + |
| 58 | +Both must be set together; setting only one causes Hollo to refuse to |
| 59 | +start. |
| 60 | + |
| 61 | +~~~~ env |
| 62 | +HANDLE_HOST=example.com |
| 63 | +WEB_ORIGIN=https://ap.example.com |
| 64 | +~~~~ |
| 65 | + |
| 66 | +With this in place, Fedify takes care of the rest: |
| 67 | + |
| 68 | + - WebFinger responses use `acct:alice@example.com` as the subject. |
| 69 | + - Actor URIs are built under `https://ap.example.com/@alice`. |
| 70 | + - The Mastodon-compatible `/api/v1/instance` and `/api/v2/instance` |
| 71 | + endpoints report `example.com` as the instance domain, so clients |
| 72 | + display the correct handle. |
| 73 | + |
| 74 | + |
| 75 | +Reverse proxy redirect |
| 76 | +---------------------- |
| 77 | + |
| 78 | +Hollo itself only listens on the `WEB_ORIGIN` host. Remote servers |
| 79 | +that look up `@alice@example.com` will send their WebFinger query to |
| 80 | +`https://example.com/.well-known/webfinger`, so you must configure |
| 81 | +that domain's reverse proxy to redirect those requests to Hollo. |
| 82 | + |
| 83 | +A 301 redirect that preserves the query string is enough. Send |
| 84 | +`/.well-known/nodeinfo` and `/.well-known/host-meta` along as well, |
| 85 | +since some implementations probe those during discovery. |
| 86 | + |
| 87 | +<Tabs> |
| 88 | + <TabItem label="nginx"> |
| 89 | +~~~~ nginx |
| 90 | +server { |
| 91 | + listen 443 ssl; |
| 92 | + server_name example.com; |
| 93 | + # … your normal site config … |
| 94 | +
|
| 95 | + location /.well-known/webfinger { |
| 96 | + return 301 https://ap.example.com$request_uri; |
| 97 | + } |
| 98 | + location /.well-known/nodeinfo { |
| 99 | + return 301 https://ap.example.com$request_uri; |
| 100 | + } |
| 101 | + location /.well-known/host-meta { |
| 102 | + return 301 https://ap.example.com$request_uri; |
| 103 | + } |
| 104 | +} |
| 105 | +~~~~ |
| 106 | + </TabItem> |
| 107 | + <TabItem label="Caddy"> |
| 108 | +~~~~ caddy |
| 109 | +example.com { |
| 110 | + # … your normal site config … |
| 111 | +
|
| 112 | + redir /.well-known/webfinger* https://ap.example.com{uri} permanent |
| 113 | + redir /.well-known/nodeinfo* https://ap.example.com{uri} permanent |
| 114 | + redir /.well-known/host-meta* https://ap.example.com{uri} permanent |
| 115 | +} |
| 116 | +~~~~ |
| 117 | + </TabItem> |
| 118 | +</Tabs> |
| 119 | + |
| 120 | +You do **not** need to redirect the `/@username` path or any actor |
| 121 | +URLs — those URLs live on `ap.example.com` and remote servers will go |
| 122 | +straight there after resolving the WebFinger response. |
| 123 | + |
| 124 | + |
| 125 | +Verifying the setup |
| 126 | +------------------- |
| 127 | + |
| 128 | +After deploying, three quick checks confirm everything is wired up: |
| 129 | + |
| 130 | + 1. WebFinger from the handle domain redirects: |
| 131 | + |
| 132 | + ~~~~ sh frame="none" |
| 133 | + curl -i "https://example.com/.well-known/webfinger?resource=acct:alice@example.com" |
| 134 | + ~~~~ |
| 135 | + |
| 136 | + should return a 301 to `https://ap.example.com/.well-known/webfinger?...`. |
| 137 | + |
| 138 | + 2. WebFinger from the server domain responds: |
| 139 | + |
| 140 | + ~~~~ sh frame="none" |
| 141 | + curl "https://ap.example.com/.well-known/webfinger?resource=acct:alice@example.com" |
| 142 | + ~~~~ |
| 143 | + |
| 144 | + should return a JRD whose `subject` is `acct:alice@example.com` |
| 145 | + and whose `self` link points at `https://ap.example.com/@alice`. |
| 146 | + |
| 147 | + 3. The instance endpoint reports the handle domain: |
| 148 | + |
| 149 | + ~~~~ sh frame="none" |
| 150 | + curl https://ap.example.com/api/v2/instance | jq .domain |
| 151 | + ~~~~ |
| 152 | + |
| 153 | + should print `"example.com"`. |
| 154 | + |
| 155 | +For a more thorough audit, point Julian Fietkau's [WebFinger Canary] |
| 156 | +at your handle domain. Mastodon and Fedify-based servers handle |
| 157 | +split-domain setups correctly today; Misskey and Pixelfed currently |
| 158 | +do not. |
| 159 | + |
| 160 | +[WebFinger Canary]: https://correct.webfinger-canary.fietkau.software/ |
| 161 | + |
| 162 | + |
| 163 | +<Aside type="danger"> |
| 164 | + **Configure `HANDLE_HOST` and `WEB_ORIGIN` before creating your |
| 165 | + first account.** |
| 166 | + |
| 167 | + Hollo stores each local account's full `@user@host` handle in the |
| 168 | + database when the account is created. Other ActivityPub |
| 169 | + implementations also cache your handle on first contact. Changing |
| 170 | + the handle domain later means your existing followers can no longer |
| 171 | + resolve you, and there is no migration path that keeps existing |
| 172 | + follow relationships intact. |
| 173 | + |
| 174 | + On startup, Hollo logs a warning if `HANDLE_HOST` disagrees with the |
| 175 | + stored handle of the existing account. Treat that warning as a |
| 176 | + problem to investigate before continuing to run the instance. |
| 177 | +</Aside> |
0 commit comments