feat(setup): add a setup token for first-run setup (#3437)

This commit is contained in:
Hampus authored and GitHub committed 2026-10-13 00:17:24 +02:00
1 parent 8e39380ee6
commit 78272521bb
135 files changed
+3045 -771

No files matched your search

@@ -18,7 +18,7 @@ The pass unlocks nothing in the software, and self-hosting stays free. If you ha
We are grateful to everyone supporting the project through [Fluxer Plutonium](https://fluxer.app/plutonium) or [donations](https://fluxer.app/donate). All of our code is free and open source on [GitHub](https://github.com/fluxerapp/fluxer).
:::
Fluxer reads its settings from environment variables. They live in a file named `.env`, in the same directory as `docker-compose.yml`.
Fluxer reads its settings from environment variables. They are in a file named `.env`, in the same directory as `docker-compose.yml`.
The installer in [Get started](/operator/get-started/) writes `.env` and fills in every secret. Start with [Core identity and public address](#core-identity-and-public-address) and [Secrets you must generate](#secrets-you-must-generate).
@@ -1146,7 +1146,7 @@ Set by Docker. Node identity in metrics and logs.
#### `FLUXER_ENV`
Default `development`. The runtime mode. `development`, `production`, or `test`. Compose pins `production`, which gates Postgres validation and the admin cookie flags.
Default `development`. The runtime mode. `development`, `production`, or `test`. Compose pins `production`, which gates Postgres validation and the admin cookie flags. On a self-hosted instance, `production` also enforces the [setup token](#setup-token).
#### `FLUXER_SELF_HOSTED`
@@ -1278,7 +1278,13 @@ Default empty. The incident history page of the status page.
#### `FLUXER_INSTANCE_SETUP_CONFIGURED`
Default `false`. Whether setup is marked complete. The stored `app_public_config` row wins whenever its `setup.configured` is a boolean. This variable supplies the default only while no such row exists, and only on a self-hosted instance. Off a self-hosted instance the state is always true whatever this says.
Default `false`. Whether setup is marked complete. The stored `app_public_config` row wins whenever its `setup.configured` is a boolean. This variable supplies the default only while no such row exists, and only on a self-hosted instance. Off a self-hosted instance the state is always true whatever this says. Setting it to `true` on a new instance skips the setup wizard and opens registration, and nobody becomes admin by registering first. The admin account is then created by a [registration request](/http-api/authentication/#register-an-account) that includes `setup_token`.
<a id="setup-token"></a>
#### `FLUXER_SETUP_TOKEN`
Default empty. The setup token of a new self-hosted instance, 32 to 256 characters, and a value outside that range fails startup. Until the first admin account exists, only the registration that presents this token becomes the admin. While the instance is also unconfigured, every registration must present it. Unset, the API generates a token on the first start, stores only its hash and writes the token to its log once. A value set here always replaces the generated one. It is read by the `api` service only and is ignored once the first admin exists. Off a self-hosted instance it is ignored. The installer writes one into `.env`, and [Create the first admin](/operator/get-started/#first-run) has where the wizard asks for it.
#### `FLUXER_ACCOUNT_IDENTITY`
@@ -1300,7 +1306,7 @@ Default empty. The guild that grants the visionary role. Snowflake.
Default empty. The role granted there. Snowflake.
Completing setup saves that state independently of the environment default. Later branding or legal updates preserve it. Setting `FLUXER_INSTANCE_SETUP_CONFIGURED` back to `false` does not reopen the wizard or restore the unauthenticated setup access and first-registration admin grant described in [Get started](/operator/get-started/).
Completing setup saves that state independently of the environment default. Later branding or legal updates preserve it. Setting `FLUXER_INSTANCE_SETUP_CONFIGURED` back to `false` does not reopen the wizard or restore the admin grant described in [Create the first admin](/operator/get-started/#first-run).
## API and worker settings
@@ -72,8 +72,8 @@ Choose the hostname the instance answers on. Step 4 writes it into `.env` as `FL
Create an `A` record for that hostname pointing at the machine's public address, and an `AAAA` record when the machine has a public IPv6 address. A hosting provider shows that address in its console. On a home connection the address belongs to the router, and the router's status page shows it.
:::caution[Finish setup before anyone else reaches the hostname]
An unconfigured instance takes registrations from anyone, and lets any account that registers read and change the instance configuration. Registration defaults to `open`. The window opens the moment DNS and the ports are live, so finish the wizard in [Step 6](#first-run) in the same sitting that brings the stack up.
:::caution[Keep the setup token private]
An unconfigured instance creates no account without its setup token, and the registration that presents it becomes the admin. After that the token stops working and registration follows the registration mode, which defaults to `open`.
:::
## Step 3: Open the ports
@@ -146,7 +146,7 @@ powershell -ExecutionPolicy Bypass -File .\install.ps1 -Domain chat.example.com
The run prints one line per phase and ends with the URL to open.
`~/fluxer` then holds `docker-compose.yml`, `docker-compose.proxy.yml`, `tunnel.compose.yml`, `external-object-store.compose.yml`, `Caddyfile` and `.env.example`, plus a `.env` readable only by you. Every value that ships as `CHANGE_ME` in `.env.example` is a fresh random value. Every command from here on runs in that directory.
`~/fluxer` then holds `docker-compose.yml`, `docker-compose.proxy.yml`, `tunnel.compose.yml`, `external-object-store.compose.yml`, `Caddyfile` and `.env.example`, plus a `.env` readable only by you. Every value that ships as `CHANGE_ME` in `.env.example` is a fresh random value. The installer also writes `FLUXER_SETUP_TOKEN`, the setup token that [Step 6](#first-run) asks for. Every command from here on runs in that directory.
### Serving on another port
@@ -226,11 +226,24 @@ Every path returns 200. `/.well-known/fluxer` lists the endpoints clients use, w
Open the hostname in a browser. A fresh instance is unconfigured, so it serves the setup wizard.
The wizard first asks how members sign in. Usernames is the default and needs no email setup at all. Email suits an instance that sends mail through SMTP. The choice is fixed once the owner account exists. [Sign-in methods](/operator/sign-in/) explains both.
After the welcome screen the wizard asks for the setup token. The token proves that you run the instance, and nobody can create an account on an unconfigured instance without it.
Then create the owner account, and choose branding, registration mode, community policy, media expiry, integrations and premium settings. Completing the wizard closes setup access.
- The installer prints the token as its last step, and keeps it as `FLUXER_SETUP_TOKEN` in `.env`.
- An instance brought up by hand without that variable has a token the API generated. The API writes it to its log once, on the first start.
The first registration receives the wildcard admin ACL, which is full access to the admin dashboard, unless registration is set to hold new accounts for approval. On a username instance that is the first registration with a password. On an email instance it is the first registration that includes an email address. Finishing the wizard grants that same ACL to whichever account completes it, when that account holds none.
```bash
docker compose logs api | grep -i 'setup token'
```
When the token is lost, set [`FLUXER_SETUP_TOKEN`](/operator/configuration/#setup-token) in `.env` and run `docker compose up -d`.
The mobile app has no field for the token, so do the first-run setup in a browser. When the browser is already signed in to an account that is not an admin, the wizard says so and offers to sign out.
The wizard then asks how members sign in. Usernames is the default and needs no email setup at all. Email suits an instance that sends mail through SMTP. The choice is fixed once the owner account exists. [Sign-in methods](/operator/sign-in/) explains both.
Then create the owner account, and choose branding, registration mode, community policy, media expiry, integrations and premium settings.
The registration that presents the setup token receives the wildcard admin ACL, which is full access to the admin dashboard, in every registration mode. On a username instance that registration needs a password. On an email instance it needs an email address. The token stops working once the admin account exists.
On a username instance the wizard shows the owner's [recovery kit](/operator/sign-in/#recovery-kits) next. Store it somewhere safe. Without the kit, a forgotten owner password needs another admin to issue a [reset link](/operator/sign-in/#admin-reset-links). Register a passkey or a second admin account before you open registration.
@@ -63,9 +63,11 @@ helm install fluxer ./fluxer/deploy/helm/fluxer \
The first install pulls every image, creates the volumes and the buckets, and takes a few minutes. `helm test fluxer -n fluxer` then checks that the edge answers and serves the discovery document for the hostname. Keep `fluxer-values.yaml`, because every later upgrade passes it again.
:::caution[Finish setup before anyone else reaches the hostname]
An unconfigured instance lets any account that registers read and change the instance configuration. Register the first account and finish the wizard in the same sitting that brings the release up, as [Create the first admin](/operator/get-started/#first-run) describes.
:::
The setup wizard asks for the setup token, as [Create the first admin](/operator/get-started/#first-run) describes. The chart keeps it in the Secret:
```bash
kubectl -n fluxer get secret fluxer-env -o jsonpath='{.data.FLUXER_SETUP_TOKEN}' | base64 -d
```
## Exposure
@@ -159,6 +161,7 @@ The chart keeps every secret in one Secret named `fluxer-env`. Each key below ta
| `FLUXER_MEDIA_PROXY_UPLOAD_RELAY_SECRET_BASE64` | Generated, 32 random bytes in base64 |
| `FLUXER_ADMIN_SECRET_KEY_BASE` | Generated, 64 letters and digits |
| `FLUXER_ADMIN_OAUTH_CLIENT_SECRET` | Generated, 64 letters and digits |
| `FLUXER_SETUP_TOKEN` | Generated, 64 letters and digits |
| `FLUXER_LIVEKIT_API_KEY` | The fixed value `fluxer` |
| `FLUXER_LIVEKIT_API_SECRET` | Generated, 64 letters and digits |
| `FLUXER_VAPID_PUBLIC_KEY` | Generated P-256 pair |
@@ -176,7 +179,7 @@ kubectl -n fluxer exec postgres-0 -- psql -U fluxer -c "ALTER ROLE fluxer PASSWO
### A Secret you manage
Set `secrets.existingSecret` to the name of a Secret you create, holding every key in the table above. The chart then renders no Secret and never changes yours. The install stops when the Secret does not exist, lacks a key, or holds a setting that disagrees with one the chart sets itself, and it names the Secret and the keys. After editing it, restart the release so the pods read the new values:
Set `secrets.existingSecret` to the name of a Secret you create, holding every key in the table above. `FLUXER_SETUP_TOKEN` is the one optional key. Without it the API makes a token on its first start and writes it to its log once. The chart then renders no Secret and never changes yours. The install stops when the Secret does not exist, lacks a key, or holds a setting that disagrees with one the chart sets itself, and it names the Secret and the keys. After editing it, restart the release so the pods read the new values:
```bash
kubectl -n fluxer rollout restart deployment,statefulset -l app.kubernetes.io/instance=fluxer
@@ -315,7 +318,7 @@ kubectl -n fluxer exec postgres-0 -- sh -c 'pg_dump -U "$POSTGRES_USER" -d "$POS
Keep `fluxer-env.yaml` as safe as the database. Without it a restored database has no matching Postgres password, and every signed session and token stops working.
Uploads live on the volume `data-seaweedfs-0`. Snapshot it with your storage provider's VolumeSnapshot class. Without one, stop the store and copy the volume out through a helper pod:
Uploads are on the volume `data-seaweedfs-0`. Snapshot it with your storage provider's VolumeSnapshot class. Without one, stop the store and copy the volume out through a helper pod:
```bash
kubectl -n fluxer scale statefulset seaweedfs --replicas=0
@@ -45,7 +45,7 @@ Members register and sign in with an email address. Email verification and passw
## Choosing the method
The setup wizard asks how members sign in, straight after the theme step and before the owner account is created. Usernames is the recommended choice. Picking Email there switches the instance before the owner registers.
The setup wizard asks how members sign in, after the [setup token](/operator/get-started/#first-run) and theme steps and before the owner account is created. Usernames is the recommended choice. Picking Email there switches the instance before the owner registers.
When Email is picked, the same step asks how usernames are tagged. No tags is the recommended choice.
@@ -79,6 +79,8 @@ printf '\nFLUXER_PROFILE_PSEUDONYM_SECRET=%s\n' "$(openssl rand -hex 32)" >> .en
Do not paste `FLUXER_ERLANG_COOKIE=$(openssl rand -hex 32)` into `.env`. Compose does not execute shell commands in that file.
An upgrade does not write `FLUXER_SETUP_TOKEN`. An instance with no admin account yet asks for the setup token after the upgrade. [Create the first admin](/operator/get-started/#first-run) has where to find it. An instance whose admin access was granted outside setup also logs a generated setup token after the upgrade.
## Keep a local compose change
An upgrade replaces the stack files, including direct edits to `docker-compose.yml`, `docker-compose.proxy.yml`, `tunnel.compose.yml`, `external-object-store.compose.yml` or the `Caddyfile`. Use `--dry-run` to see which files will change.