Serve an instance on a public URL
Serve a Henceforth instance at a public https:// URL behind a reverse
proxy, claim it as its first administrator with hf, and log in again later
with the same URL.
Prerequisites
Section titled “Prerequisites”- A host name that resolves to your proxy, and a TLS certificate for it. The
steps use
hf.example.com. - A reverse proxy that terminates TLS and forwards to
henceforth serve(nginx, Caddy, a cloud load balancer, and so on). - A PostgreSQL database for the instance, owned by the role in
DATABASE_URL, thehenceforthbinary, andHENCEFORTH_MASTER_KEYset in the server’s environment. At every start the server revokesCONNECTon that database fromPUBLIC(which only the owner can do), so any other role that connects (a backup or monitoring role) needsGRANT CONNECT ON DATABASE "<db>" TO "<role>";. - An ed25519 or ECDSA P-256 SSH key pair on the machine you will run
hffrom, for example~/.ssh/id_ed25519. RSA keys are not accepted. - The
hfbinary on yourPATH.hf --versionconfirms it is installed.
1. Put a TLS-terminating reverse proxy in front of the server
Section titled “1. Put a TLS-terminating reverse proxy in front of the server”Forward https://hf.example.com to the address henceforth serve listens on
(--port, default 8080). The server speaks plain HTTP to the proxy.
Decide where the proxy runs, because step 2 asks the server for the interface
to listen on (--bind):
- Proxy on the same host as the server: bind
127.0.0.1. Only processes on that host can reach the port, so nothing gets past the proxy’s TLS. - Proxy on another machine: bind the private address of the server’s
network interface that the proxy reaches, and firewall the port so only the
proxy connects.
0.0.0.0also works, with the same firewall. - Server or proxy in a container: bind
0.0.0.0inside the container and publish the port to the interface you want (for example-p 127.0.0.1:8080:8080for a proxy on the host). A proxy container on a bridge network cannot reach the host’s127.0.0.1; put both containers on one network and point the proxy at the server container’s name.
The server never learns its own URL from the request: it ignores Host,
X-Forwarded-Host and every other forwarded header. Step 2 is what tells it
which URL is its own.
2. Start the server with its public origin and the claim key
Section titled “2. Start the server with its public origin and the claim key”henceforth serve \ --database-url "$DATABASE_URL" \ --metadata-path ./metadata \ --public-origin https://hf.example.com \ --bind 127.0.0.1 \ --claim-key-file ~/.ssh/id_ed25519.pub--public-origin takes an origin: scheme, host and an optional port, with no
path and no trailing slash. It is repeatable, and HENCEFORTH_PUBLIC_ORIGINS
takes the same values comma-separated. https:// is required except for
loopback hosts. The --claim-key-file (or HENCEFORTH_CLAIM_KEY_FILE) names
the public key that may claim a new instance.
The example binds 127.0.0.1 for a proxy on the same host; use the value from
step 1 for your layout. --bind takes the IP address of the interface to
listen on, not a host name and not host:port (the port is --port).
HENCEFORTH_BIND is the same setting. 0.0.0.0 is every IPv4 interface; ::
is every IPv6 interface and, on Linux with the default
net.ipv6.bindv6only=0, IPv4 too. Production has no default: henceforth serve without --dev refuses to start, with an error naming the flag, if
--bind is missing. The server logs the address it bound at startup, in the
bound_addr field of the henceforth listening on line.
--public-origin is required: henceforth serve without --dev refuses to
start, with an error naming the flag, if no public origin is configured. The
server accepts signatures only for the origins you list. It never treats
localhost as its own unless you say so, because a reverse proxy on the same
host makes every request look local. To serve from this machine only, declare
the loopback origin yourself, for example --public-origin http://localhost:8080
together with --bind 127.0.0.1; a loopback origin is accepted only from a
loopback connection.
hf dev and henceforth serve --dev need no --public-origin and no
--bind: with --dev the server listens on 127.0.0.1, and with no public
origin it accepts loopback origins on the port it listens on. Once you configure
any origin, that default is off. --dev --bind 0.0.0.0 opens a development
server to your network on purpose.
3. Claim the instance
Section titled “3. Claim the instance”From the machine that holds the matching private key:
hf login prod https://hf.example.com --sshhf signs for exactly the URL you typed, so type the public URL, not the
address the proxy forwards to. It shows what it is about to claim before it
signs anything:
Claim https://hf.example.com as administrator 'alice', signing with key SHA256:... ? This cannot be undone. [y/N]Check the origin and the key fingerprint, then answer y. The fingerprint is
the SHA256: value that ssh-keygen -lf <key>.pub prints for the same key.
Pass --name <name> to choose the administrator name and --yes to skip the
prompt in a script.
4. Log in with the same URL later
Section titled “4. Log in with the same URL later”hf login prod --sshWith no URL, hf uses the one stored for the target prod. Logging in with a
different spelling of the address (another host name, http://, the proxy’s
internal address) signs for a different origin, which the server refuses with
ORIGIN_MISMATCH unless it is also configured as a public origin.
Verify
Section titled “Verify”hf whoamiprints your principal’s id (a UUID) followed by @ prod.
Troubleshooting
Section titled “Troubleshooting”ORIGIN_MISMATCH: the URL you typed is not one of the server’s public origins. Start the server with--public-originset to exactly that URL, or log in with the URL it is configured for. The error message shows the originhfsigned for.GET /statuslists the origins the server accepts underpublic_origins. A loopback origin you configured is refused from a non-loopback connection by design.- A redirect error (
the server answered 301 ... hf does not follow redirects when authenticating): the URL redirects somewhere else, for example fromhttp://tohttps://. Log in with the final URL. - The proxy answers
502, or the port refuses connections from another host: the server listens only on the interface--bindnames. Point the proxy at that address and port, or change--bindas step 1 describes. The address is in the server’s startup log, in thebound_addrfield. - The server exits with
requires an explicit bind address: you started it without--devand without--bind. Pass--bind(or setHENCEFORTH_BIND) as step 1 describes. - The server exits with
requires at least one public origin: you started it without--devand without--public-origin. Pass the public URL, or--public-origin http://localhost:<port>for a box-local instance. INSTANCE_ALREADY_CLAIMED: someone claimed the instance first.hf loginhandles this itself by logging in with a fresh signature. If the key you signed with is not registered on the instance, that fallback login fails withUNKNOWN_KEY; log in with a registered key, or ask an administrator to enroll yours.SSH_KEY_ALREADY_ENROLLED: the key you claimed with is already stored on a principal in the instance’s database. A key belongs to one principal, once, whatever its comment, andhfdoes not fall back to a login for this error. The claim created nothing, so the instance is still unclaimed. Restart the server with--claim-keyor--claim-key-filenaming a key no principal holds, then claim again with the matching private key.
See also
Section titled “See also”- Authentication architecture — why the signature binds the origin, and what an attacker could do without it.
- Auth threat model — what the instance defends against, the known gaps, and what an operator must do beyond these steps.
- Run
henceforth serve --helpandhf login --helpfor every flag.