One switch, HATCH_AUTH (Auth__Enabled underneath it), and three
postures.
- Off — the default, and right for one computer: nobody is asked who they are.
- On — sign in with Google: nobody reaches the board until they do.
- Required — On, behind a TLS proxy: how to run a Hatch more than one computer reaches.
A Hatch reachable from a network with the wall Off is open to everyone on that network, not only to whoever is sitting at it — Off asks nothing of anybody, wherever the request comes from. People, roles and keys names the same three states from the other side, in terms of who may do what.
Off#
Nothing asks who anybody is, and everyone is one local person. That person
is named from Settings → Your name; failing that, the account that
started Hatch; failing that, friend (Services/Auth/LocalCaller.cs).
Every request is allowed — there is no gate to pass
(Services/Auth/AuthGate.cs:268).
A program fares the same way. A runner with no key names itself with a
header, so the trail still says which runner did what, though a name is all
that header proves; it is dropped only once the wall goes up
(Common/AuthMiddleware.cs:51). No key is needed here, and hatch config
takes a blank. The container runner works only in
this posture: it fetches its credential by asking Hatch for it, and a Hatch
with its wall up refuses that ask outright.
On#
A request carrying no cookie and no key is refused, and a page load with
neither is sent to sign in at /apps/auth/. The runner header means
nothing here — On listens only to a cookie or a key. Manage → Users and
Manage → API keys appear in the nav, because there is now a role to
hold and a key to mint. migrate and api have to be given the same
Auth__* lines, which Compose already does (compose.yaml:56-61).
Putting a front door on it#
Turning the wall on, from nothing, in six steps.
1. Make a Google OAuth client. In the Google Cloud console, create an OAuth client of type Web application. Add this as an authorized redirect URI:
<your-hatch-origin>/api/auth/google/callback
Configure the consent screen too, and while it is unpublished, add as test users the Google accounts that may sign in.
2. Write a .env beside compose.yaml. Compose reads it on its own,
and git ignores it:
HATCH_AUTH=true
HATCH_GOOGLE_CLIENT_ID=<your client id>
HATCH_GOOGLE_CLIENT_SECRET=<your client secret>
On http://localhost:8080 that is all: the redirect address is worked out
from the request. Behind TLS, where the address people type is not the one
the api sees, add
HATCH_GOOGLE_REDIRECT_URI=<your-hatch-origin>/api/auth/google/callback
(Behind a reverse proxy says why).
3. Run docker compose up -d again, then check docker compose logs migrate. With no Administrator yet, it says:
No Administrator exists yet. The next person to sign in with Google becomes the Administrator.
and, if a Google variable above did not arrive, it also says:
Google sign-in is not configured: set Auth__Google__ClientId and Auth__Google__ClientSecret.
Both are written once, by the one-shot migrate step, and only when the
wall is on (Program.cs:416-419).
4. The first new Google identity to sign in while no admin exists becomes one, so sign in yourself before you share the address. People, roles and keys has the rest: how somebody after that is approved, or added ahead of time.
5. The runner now needs a key. Open Manage → API keys, mint one, and give it to the runner:
hatch config --origin <your-hatch-origin> --key <the key>
The runner has the rest of what that line does.
6. If you are locked out, see Locked out, below.
Locked out#
Set HATCH_AUTH=false, or delete the line, and run docker compose up -d.
That is the whole rollback, and nothing is lost: your projects, issues,
people and keys are all there when the wall goes back up.
Behind a reverse proxy#
TLS ends at the proxy. The API itself serves plain http on 8080, and
redirects to https only when an https port is configured — which neither
Compose nor the image does (Program.cs:468-482). Compose passes the Auth__*
lines it already names (compose.yaml:59-64 and 86-91) and none of the
ones below; to set one, add it under environment: for api in a
compose.override.yaml beside compose.yaml.
The cookie.
__Secure-hatch_grant(Auth__CookieName),Secure,HttpOnly,SameSite=Lax, path/(Services/Auth/AuthCookie.cs:116-129). It is kept 400 days (AuthCookie.cs:26) and issued again once it isAuth__GrantRenewAfterDaysold, 30 unless you say otherwise (Services/Auth/AuthOptions.cs:104). Because it isSecure, Chromium and Firefox accept it over http only fromhttp://localhost, not from any other http address (a LAN IP, say), and Safari does not accept it even on localhost. That is why the proxy has to speak https.The redirect address. The API honours only
X-Forwarded-ForandX-Forwarded-Proto, from any proxy, and neverX-Forwarded-Host(Program.cs:457-466), so the host it sees is the proxy's own, not the one people type. Behind a proxy, set both in.env:HATCH_GOOGLE_REDIRECT_URI=<your-hatch-origin>/api/auth/google/callback HATCH_PUBLIC_URL=<your-hatch-origin>The first is the address Google sends a person back to (
compose.yaml:91); the second is the address a pull request's link back to its ticket is built from (compose.yaml:101).One origin. The proxy sends
/apps/*,/api/*,/.well-known/assetlinks.jsonand/health/*to the API from one host. The cookie, and the installed app's scope (/,src/Hatch.Web/apps/hatch/public/manifest.webmanifest:6), belong to that origin.One sign-in across subdomains.
Auth__CookieDomain, for example.${DOMAIN}, makes one sign-in cover every host under it. Empty, the default, keeps the cookie host-only (AuthOptions.cs:74-79).Hosts never challenged.
Auth__ExemptHosts__0,Auth__ExemptHosts__1, and so on. The port is dropped and case ignored, and the host is checked before any path (Services/Auth/AuthGate.cs:248,309-323).What stays open with the wall up. Every path
AuthGate.csexempts, matched by whole path segment and case-insensitively (exemptPaths,AuthGate.cs:115-189;exemptExactPaths,204-214):/health/liveand/health/ready, because a probe presents no cookie, and gating them fails readiness on every instance./apps/auth,/authand/auth/(the last two exactly, not as prefixes),/api/auth/verify,/api/auth/redeemand/api/auth/google(sign-in start and callback): the way in. Gating the way in is a redirect loop./.well-known/assetlinks.json, which Android's own link verifier fetches with no cookie./api/hatch-revision, which commit this replica runs. The caller who most needs it is a stale tab about to be refused, so it is answered to everyone./api/app-version, the content-hashed asset filenames of each app, which the kiosk shell fetches with no cookie to tell a dashboard from the proxy's error page./api/ui-logs,/api/vm-console-logs,/api/kiosk/provisioning-infoand/api/sun-events, which callers with no cookie reach.- The media library's configured
RequestPath,/mediaunless changed (Services/Media/MediaLibraryOptions.cs:30).
Every refusal logs the Warning
Auth refused {Reason} for {Host}{Path} from {ClientIp}(AuthGate.cs:303-305). Read it withdocker compose logs api.
Forward auth#
For a proxy that asks before it forwards, the question goes to Hatch.
Traefik's forwardAuth is the example; no proxy configuration ships with
Hatch.
- The endpoint.
GET /api/auth/verify(Controllers/AuthController.cs:59-124). It readsX-Forwarded-Method,X-Forwarded-Host,X-Forwarded-UriandX-Forwarded-Prototo learn what the proxy was asked, and the cookie or anAuthorization: Bearerkey to learn who is asking. - What it answers, always with
Cache-Control: no-storeon a refusal:204when the caller is allowed — withX-Hatch-GrantandX-Hatch-Labelwhen a cookie let them in, for the proxy to copy onto the request it forwards.302to the sign-in page for a browser loading a page (Services/Auth/AuthChallenge.cs:31-50). The address is absolute only for a host insideAuth__CookieDomain; anywhere else, or with that domain empty, it is relative (AuthChallenge.cs:82-110).401for anything else, and always for a caller that sent a key.204for everybody, with the wall off.
- The proxy as the only enforcer.
Auth__EnforceInProcess=falseleaves refusing to the proxy. Anything that reaches the API directly is then as open as it was before the wall existed (AuthOptions.cs:41-44). Compose never sets it.
The installed app#
- The manifest is behind the wall, which is why
src/Hatch.Web/apps/hatch/index.html:6links it withcrossorigin="use-credentials": a fetch with no cookie gets a 401, and an app whose manifest 401s never offers to install (docs/hatch.md:160-172). - Android's asset links.
HATCH_ANDROID_PACKAGEandHATCH_ANDROID_CERT_FINGERPRINT(Apps__AssetLinks__0__PackageNameandApps__AssetLinks__0__Sha256CertFingerprints__0,compose.yaml:111-112) are served at/.well-known/assetlinks.json(Modules/AssetLinksController.cs:25-44). It is open with the wall up. It answers404while both are unset. A malformed value stops the API at start (Modules/AssetLinksValidation.cs). A second phone isApps__AssetLinks__1__PackageNameandApps__AssetLinks__1__Sha256CertFingerprints__0(compose.yaml:105-110). Hatch on your phone shows how to read the two values off the phone.