mirror of
https://github.com/fluxerapp/fluxer
synced 2026-10-17 13:05:32 +09:00
feat(setup): add a setup token for first-run setup (#3437)
This commit is contained in:
1 parent
8e39380ee6
commit
78272521bb
135 files changed
+3045
-771
No files matched your search
@@ -28,8 +28,6 @@ No Admin operation declares an OAuth2 scope, an MFA requirement, or a sudo requi
|
||||
|
||||
An Admin API key authenticates as the account that created it, and Fluxer reads it only on a path below `/v1/admin`. A request presenting a key on any other path is treated as a request with no credential. Fluxer authorises a key-authenticated request twice. The key's own ACL set must satisfy the operation, and unless the owning account holds `*` that account must itself hold one of the ACLs the operation names. When an ACL is removed from a key, or from the account that owns the key, the key no longer passes a check for that ACL on its next request. The key does not need to be rotated.
|
||||
|
||||
Initial setup is the single exception. While `app_public.setup.configured` is false, a session credential reaches the [instance configuration](/admin-api/instance/) operations without holding any ACL. The session that switches setup to configured is granted `*` immediately. After `app_public.setup.configured` becomes true, those operations require an accepted credential and the ACLs they name.
|
||||
|
||||
Admin authorisation runs before request validation on every operation except [Queue bulk job](/admin-api/bulk-jobs/#queue-bulk-job), whose body is validated first because its ACL is selected from that body. An account lacking the required ACL is therefore refused with 403 even when its query string or body is also malformed.
|
||||
|
||||
## ACL evaluation
|
||||
|
||||
@@ -12,12 +12,6 @@ Every write is a merge over the stored configuration, and an omitted key leaves
|
||||
|
||||
Reading configuration requires the [Admin ACL](/admin-api/#acl-registry) `instance:config:view`, and writing it requires `instance:config:update`. The limit configuration has its own pair, `instance:limit_config:view` and `instance:limit_config:update`.
|
||||
|
||||
:::note[Initial setup accepts a plain session credential]
|
||||
Until setup is marked complete, [Get instance configuration](#get-instance-configuration), [Update instance configuration](#update-instance-configuration), [Create branding asset](#create-branding-asset), and [Create SMTP test](#create-smtp-test) also accept a session credential holding no Admin ACL.
|
||||
:::
|
||||
|
||||
That relaxation applies only to a session credential, so an Admin API key and a bearer token are evaluated normally even before setup is complete. Completing setup grants the acting session the wildcard ACL. Registration URLs, pending registrations, and limit configuration always need their own ACL.
|
||||
|
||||
:::caution[No read returns a stored secret]
|
||||
A secret is reported by a companion boolean such as `client_secret_set`. Except for the single sign-on client secret, that boolean is true whether the value is stored here or supplied by deployment configuration.
|
||||
:::
|
||||
@@ -627,7 +621,7 @@ Display metadata for one [limit key](/http-api/instance/#limit-keys), which an e
|
||||
|
||||
<RouteHeader method="GET" path="/v1/admin/instance/config" auditReason />
|
||||
|
||||
Returns the [instance configuration](#instance-configuration-object) object. Requires `instance:config:view`, or a session credential until setup is marked complete.
|
||||
Returns the [instance configuration](#instance-configuration-object) object. Requires `instance:config:view`.
|
||||
|
||||
### Response
|
||||
|
||||
@@ -637,7 +631,7 @@ Returns the [instance configuration](#instance-configuration-object) object. Req
|
||||
|
||||
### Side effects
|
||||
|
||||
The operation records one [Admin audit entry](/admin-api/#admin-audit-entry-object) with action `get_instance_config`, target type `instance_config`, target ID `0`, and metadata keys `registration_url_count` and `pending_registration_count`. Before setup is complete, a session credential holding no Admin ACL records the entry with its own account as the acting Admin.
|
||||
The operation records one [Admin audit entry](/admin-api/#admin-audit-entry-object) with action `get_instance_config`, target type `instance_config`, target ID `0`, and metadata keys `registration_url_count` and `pending_registration_count`.
|
||||
|
||||
### Rate limit
|
||||
|
||||
@@ -647,7 +641,7 @@ The operation records one [Admin audit entry](/admin-api/#admin-audit-entry-obje
|
||||
|
||||
<RouteHeader method="PATCH" path="/v1/admin/instance/config" auditReason />
|
||||
|
||||
Applies a merge patch to the stored configuration and returns the resulting [instance configuration](#instance-configuration-object) object. Requires `instance:config:update`, or a session credential until setup is marked complete.
|
||||
Applies a merge patch to the stored configuration and returns the resulting [instance configuration](#instance-configuration-object) object. Requires `instance:config:update`.
|
||||
|
||||
:::note[Every section is optional and merged independently]
|
||||
The body has one optional object for each section. Fluxer leaves an absent section alone. Within a section, an absent key keeps its stored value, and a key sent as null clears the stored value where the schema permits null.
|
||||
@@ -728,9 +722,9 @@ The order is `gateway_rollout`, `push_relay`, `domain_migration`, `voice_p2p`, `
|
||||
|
||||
Fluxer publishes a `gateway_rollout` change to the Gateway cluster. A `push_relay` change reaches the push service without a restart. A `billing` change applies without a restart. Premium mode changes affect the limits in force without replacing the saved limit configuration. On self-hosted deployments, `everyone` hides premium-filtered rules. Switching back to `mirror` restores them unless an Admin has replaced the limit configuration in the meantime. Enabling single community mode creates the community when none is designated, with the acting Admin as owner.
|
||||
|
||||
Initial setup completes on the first update that sets `app_public.setup.configured` to true from a session credential whose account holds neither `admin:authenticate` nor the wildcard. That update grants the account the wildcard Admin ACL and marks the deployment as bootstrapped.
|
||||
An update that sets `app_public.setup.configured` to true also marks the deployment as bootstrapped, so no later registration receives the wildcard Admin ACL.
|
||||
|
||||
The operation records one [Admin audit entry](/admin-api/#admin-audit-entry-object) with action `update_instance_config`, target type `instance_config`, and target ID `0`. The metadata key `sections` lists the names of the non-null top-level sections in the body, comma-separated in alphabetical order, and is absent when the body supplies none. The update that completes initial setup and grants the wildcard ACL also records `granted_acls` as `*`. No value from the body is recorded. A request that fails part way records no entry.
|
||||
The operation records one [Admin audit entry](/admin-api/#admin-audit-entry-object) with action `update_instance_config`, target type `instance_config`, and target ID `0`. The metadata key `sections` lists the names of the non-null top-level sections in the body, comma-separated in alphabetical order, and is absent when the body supplies none. No value from the body is recorded. A request that fails part way records no entry.
|
||||
|
||||
### Rate limit
|
||||
|
||||
@@ -740,7 +734,7 @@ The operation records one [Admin audit entry](/admin-api/#admin-audit-entry-obje
|
||||
|
||||
<RouteHeader method="POST" path="/v1/admin/instance/config/branding-assets" auditReason />
|
||||
|
||||
Uploads an image, stores its [Media Proxy](/media-proxy/overview/) URL in the selected branding slot, and returns the resulting [instance configuration](#instance-configuration-object) object. Requires `instance:config:update`, or a session credential until setup is marked complete.
|
||||
Uploads an image, stores its [Media Proxy](/media-proxy/overview/) URL in the selected branding slot, and returns the resulting [instance configuration](#instance-configuration-object) object. Requires `instance:config:update`.
|
||||
|
||||
### JSON body
|
||||
|
||||
@@ -776,7 +770,7 @@ The operation records one [Admin audit entry](/admin-api/#admin-audit-entry-obje
|
||||
|
||||
<RouteHeader method="POST" path="/v1/admin/instance/config/smtp-tests" auditReason />
|
||||
|
||||
Opens a connection to an SMTP server, authenticates against it, and reports the outcome as an [SMTP test result](#smtp-test-result-object) object without storing anything. Requires `instance:config:update`, or a session credential until setup is marked complete.
|
||||
Opens a connection to an SMTP server, authenticates against it, and reports the outcome as an [SMTP test result](#smtp-test-result-object) object without storing anything. Requires `instance:config:update`.
|
||||
|
||||
:::note[A rejected credential is still a successful test]
|
||||
A test that runs to completion answers 200 whether or not the server accepted the connection. The `ok` field is the verdict and `error` is the failure text.
|
||||
|
||||
@@ -8,7 +8,7 @@ import RouteHeader from '@/components/RouteHeader.astro';
|
||||
|
||||
Admin message routes read, search, and permanently delete the messages of one channel. Another reports the progress of a shred job.
|
||||
|
||||
Shredding and bulk deletion for a whole account live under [Admin users](/admin-api/users/).
|
||||
Shredding and bulk deletion for a whole account are under [Admin users](/admin-api/users/).
|
||||
|
||||
## Admin message object
|
||||
|
||||
|
||||
@@ -572,6 +572,7 @@ Registration verifies a [CAPTCHA](/topics/captcha/). It permits 3 attempts per h
|
||||
| invite_code? | ?string | The invite accepted immediately after registration (0-256 characters) |
|
||||
| registration_url_code?<sup>6</sup> | ?string | The administrator-issued registration URL code (1-256 characters) |
|
||||
| theme? | string | The initial theme preference, one of `dark`, `dark_legacy`, `coal`, `light`, or `system` |
|
||||
| setup_token?<sup>7</sup> | ?string | The setup token of a new self-hosted instance (1-256 characters) |
|
||||
|
||||
<sup>1</sup> Omitting the email address creates an unclaimed account, which has no recovery path until it is claimed. An address whose domain has no usable DNS records, or whose top-level domain the instance blocks, returns the field code `INVALID_EMAIL_ADDRESS`, and an address already in use returns `EMAIL_ALREADY_IN_USE`. A `username` instance accepts any string of 1-320 characters here, because older apps send one, and discards it. It stores no address and sends no email. It does not check the address against DNS or the blocklists, or whether another account uses it
|
||||
|
||||
@@ -585,10 +586,14 @@ Registration verifies a [CAPTCHA](/topics/captcha/). It permits 3 attempts per h
|
||||
|
||||
<sup>6</sup> A code supplied on an instance with administrator registration URLs disabled, and a code that does not resolve, both return 400 `REGISTRATION_URL_INVALID`. A valid code overrides the closed registration mode and replaces the instance approval mode with its own approval setting
|
||||
|
||||
<sup>7</sup> Required while a self-hosted instance is unconfigured and has no admin account, where an absent or blank value returns the field code `SETUP_TOKEN_REQUIRED`. Accepted until the first admin account exists, and a wrong value returns the field code `SETUP_TOKEN_INVALID`. Ignored afterwards, on a hosted instance, and on a self-hosted instance that does not [enforce the token](/operator/configuration/#setup-token). Surrounding whitespace is trimmed
|
||||
|
||||
Fluxer resolves the region from the client IP address, and an age below the minimum for that region returns the field code `MUST_BE_MINIMUM_AGE`. That minimum is 13 years unless the region sets a different minimum, and the applied minimum appears only in the localised message.
|
||||
|
||||
Registration returns 403 `REGISTRATION_CLOSED` when the instance is closed and no valid registration URL was supplied. A username whose discriminator space is exhausted returns the field code `TOO_MANY_USERS_WITH_THIS_USERNAME`. A username or display name containing a blocked substring returns 403 `CONTENT_BLOCKED`.
|
||||
|
||||
After the route rate limit, the CAPTCHA and schema validation, the setup token is checked before every other registration rule. A registration that presents the correct token is admitted whatever the registration mode is, is exempt from the registration allowances above, and receives the wildcard [Admin ACL](/admin-api/#acl-registry) when it creates a claimed account, which needs `email` on an `email` instance and `password` on a `username` instance. A client IP address is permitted 10 wrong tokens per 30 minutes, and further requests that present a token from that address return 429 until the window passes, the correct token included. A request with no token does not count.
|
||||
|
||||
:::caution[Registration reports an address already in use]
|
||||
[Log in with a password](#log-in-with-a-password) and [request password recovery](#request-password-recovery) are enumeration-safe and registration is not. An address that already holds an account returns the field code `EMAIL_ALREADY_IN_USE`, so an unauthenticated caller learns from the response whether that address is registered.
|
||||
:::
|
||||
@@ -602,10 +607,10 @@ There is no retry key. A repeated request with the same values creates a second
|
||||
| Status | Body | Condition |
|
||||
| --- | --- | --- |
|
||||
| 200 | [authentication token response](#authentication-token-response) \| [registration pending approval response](#registration-pending-approval-response-object) | Registration completed or entered the approval state |
|
||||
| 400 | [error response](/http-api/#error-response) | The body, CAPTCHA, account fields, or registration URL code is invalid |
|
||||
| 400 | [error response](/http-api/#error-response) | The body, CAPTCHA, account fields, registration URL code, or setup token is invalid |
|
||||
| 403 | [error response](/http-api/#error-response) | SSO enforcement, registration policy, or content moderation rejects the registration |
|
||||
| 423 | [error response](/http-api/#error-response) | `GENERAL_ERROR`, because a self-hosted instance with no account yet is still settling its [sign-in method](/http-api/instance/#set-the-setup-sign-in-method) after 15 seconds |
|
||||
| 429 | [rate limit response](/topics/rate-limits/#rate-limit-response-object) | A route, global, email, IP, or subnet bucket denies the request |
|
||||
| 429 | [rate limit response](/topics/rate-limits/#rate-limit-response-object) | A route, global, email, IP, or subnet bucket denies the request, or the address has used its wrong setup token allowance |
|
||||
| 500 | [error response](/http-api/#error-response) | Unexpected internal failure occurs |
|
||||
| 503 | [error response](/http-api/#error-response) | The instance is at its in-flight request ceiling |
|
||||
|
||||
|
||||
@@ -2003,6 +2003,14 @@ Referenced attachment "{filename}" wasn't found in message attachments
|
||||
|
||||
Session timed out
|
||||
|
||||
### `SETUP_TOKEN_INVALID`
|
||||
|
||||
That setup token is not correct. Check the token and try again.
|
||||
|
||||
### `SETUP_TOKEN_REQUIRED`
|
||||
|
||||
This instance is not set up yet. Finish setup in a web browser with the setup token before creating accounts.
|
||||
|
||||
### `SNOWFLAKE_OUT_OF_RANGE`
|
||||
|
||||
Invalid snowflake
|
||||
|
||||
@@ -6,7 +6,7 @@ description: Guild member objects, profiles, voice moderation, ownership, remova
|
||||
|
||||
import RouteHeader from '@/components/RouteHeader.astro';
|
||||
|
||||
A guild member is an account that has joined a guild. The membership has a nickname, avatar, role set, and moderation state that apply in that guild alone. Indexed member queries live on [Guild member search](/http-api/guild-member-search/), and bans on [Guild moderation](/http-api/guild-moderation/).
|
||||
A guild member is an account that has joined a guild. The membership has a nickname, avatar, role set, and moderation state that apply in that guild alone. Indexed member queries are on [Guild member search](/http-api/guild-member-search/), and bans on [Guild moderation](/http-api/guild-moderation/).
|
||||
|
||||
An unknown guild returns 404 `UNKNOWN_GUILD`, a non-member receives 403 `MISSING_PERMISSIONS`, and a guild marked unavailable returns 403 `MISSING_ACCESS`. [Transfer guild ownership](#transfer-guild-ownership) also returns 403 `ACCESS_DENIED` when the guild exists in storage but the Gateway reports it as not found.
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@ description: Guild objects, creation templates, configuration registries, and li
|
||||
|
||||
import RouteHeader from '@/components/RouteHeader.astro';
|
||||
|
||||
A guild is a community with its own channels, roles, members, and configuration. Membership and ownership transfer live on [Guild members](/http-api/guild-members/), and channels on [Guild channels](/http-api/guild-channels/).
|
||||
A guild is a community with its own channels, roles, members, and configuration. Membership and ownership transfer are on [Guild members](/http-api/guild-members/), and channels on [Guild channels](/http-api/guild-channels/).
|
||||
|
||||
[List current user guilds](#list-current-user-guilds) and [Get guild](#get-guild) declare the `guilds` [OAuth2 scope](/http-api/oauth2/#oauth2-scopes), and a bearer credential without that scope receives 403 `MISSING_OAUTH_SCOPE`. Every other route rejects a bearer credential with 403 `ACCESS_DENIED`.
|
||||
|
||||
|
||||
@@ -444,11 +444,14 @@ Whether the deployment has finished its initial configuration, and where an oper
|
||||
| configured | boolean | Whether initial deployment configuration is complete |
|
||||
| admin_url<sup>1</sup> | ?string | The admin application URL used to continue setup, or null |
|
||||
| account_identity_locked? | boolean | Whether the sign-in method can no longer change, sent only while a self-hosted deployment is not configured |
|
||||
| setup_token_required? | boolean | Whether registration needs the setup token, sent only while a self-hosted deployment is not configured |
|
||||
|
||||
<sup>1</sup> The value is the deployment's configured admin endpoint, and it is null when no admin endpoint is configured
|
||||
|
||||
`account_identity_locked` is true once the first account exists, and on a deployment stored as already in use. [Set the setup sign-in method](#set-the-setup-sign-in-method) returns 409 there.
|
||||
|
||||
`setup_token_required` is true while [Register an account](/http-api/authentication/#register-an-account) refuses a body without `setup_token`. It is false once the first admin account exists, and on a deployment that does not enforce the token.
|
||||
|
||||
## Public legal configuration object
|
||||
|
||||
The public document URLs a client links from its registration form and its report flows.
|
||||
@@ -615,8 +618,8 @@ Chooses how members sign in on a new self-hosted deployment, and on an `email` d
|
||||
|
||||
The setup wizard calls this before the owner account exists. The choice stays open on a self-hosted deployment until setup is complete or the first account exists. After that, on a deployment that was already in use before username sign-in existed, and on every hosted deployment, the route returns 409 `ACCOUNT_IDENTITY_LOCKED`. The choice and the first registration take turns, so a choice never lands after the first account exists. A repeated call while the choice is open replaces the earlier one.
|
||||
|
||||
:::caution[Anyone can call this before the first account exists]
|
||||
The route needs no credential, the same as the rule that makes the first registration the instance admin. An operator finishes setup in the same sitting that brings the deployment up.
|
||||
:::caution[The setup token stands in for a credential]
|
||||
The route needs no credential. While `setup_token_required` is true in the [public setup state](#public-setup-state-object), a call without the correct `setup_token` is refused.
|
||||
:::
|
||||
|
||||
### JSON body
|
||||
@@ -625,18 +628,21 @@ The route needs no credential, the same as the rule that makes the first registr
|
||||
| --- | --- | --- |
|
||||
| mode | string | The [account identity mode](#account-identity-modes) to use |
|
||||
| tag_style?<sup>1</sup> | string | How usernames are tagged, one of the [tag styles](#tag-styles). Defaults to `none` |
|
||||
| setup_token?<sup>2</sup> | ?string | The setup token of the deployment (1-256 characters) |
|
||||
|
||||
<sup>1</sup> A `username` instance always uses `none`. Sending `random` with `username` returns the field code `TAG_STYLE_REQUIRES_EMAIL_SIGN_IN` on `tag_style`
|
||||
|
||||
<sup>2</sup> Checked the way [Register an account](/http-api/authentication/#register-an-account) checks it, with the same field codes and the same allowance for wrong tokens
|
||||
|
||||
### Response
|
||||
|
||||
| Status | Body | Condition |
|
||||
| --- | --- | --- |
|
||||
| 200 | [account identity](#account-identity-object) object | The mode was stored |
|
||||
| 400 | [error response](/http-api/#error-response) | The body is invalid, or `random` tags were requested with `username` |
|
||||
| 400 | [error response](/http-api/#error-response) | The body is invalid, `random` tags were requested with `username`, or the setup token is missing or wrong |
|
||||
| 409 | [error response](/http-api/#error-response) | `ACCOUNT_IDENTITY_LOCKED`, because the sign-in method can no longer change |
|
||||
| 423 | [error response](/http-api/#error-response) | `GENERAL_ERROR`, because a registration or another choice held the turn for more than 15 seconds |
|
||||
| 429 | [rate limit response](/topics/rate-limits/#rate-limit-response-object) | Route or global bucket denies the request |
|
||||
| 429 | [rate limit response](/topics/rate-limits/#rate-limit-response-object) | Route or global bucket denies the request, or the address has used its wrong setup token allowance |
|
||||
|
||||
### Side effects
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@ description: User objects, public profiles, account tags, and the settings recor
|
||||
|
||||
import RouteHeader from '@/components/RouteHeader.astro';
|
||||
|
||||
A user is a Fluxer account. Another account sees the [partial user object](#partial-user-object), and the owner sees the [user object](#user-object). Account mutation lives on [Current user](/http-api/users/current-user/).
|
||||
A user is a Fluxer account. Another account sees the [partial user object](#partial-user-object), and the owner sees the [user object](#user-object). Account mutation is on [Current user](/http-api/users/current-user/).
|
||||
|
||||
Every route on this page takes a user session token or a bot token. An OAuth2 bearer credential is rejected with 403 `ACCESS_DENIED`.
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@ description: Recent mentions, saved messages, and deletion of the caller's own m
|
||||
|
||||
import RouteHeader from '@/components/RouteHeader.astro';
|
||||
|
||||
Recent mentions and saved messages are private lists that only the account owning them can read. Further routes delete the messages the caller authored, one filtered and immediate, the other unfiltered and delayed by a day. Data harvests live on [Data harvests](/http-api/users/data-harvest/).
|
||||
Recent mentions and saved messages are private lists that only the account owning them can read. Further routes delete the messages the caller authored, one filtered and immediate, the other unfiltered and delayed by a day. Data harvests are on [Data harvests](/http-api/users/data-harvest/).
|
||||
|
||||
These routes are user-only. Fluxer rejects a bot or OAuth2 bearer credential with 403 `ACCESS_DENIED`.
|
||||
|
||||
|
||||
@@ -86,7 +86,7 @@ An over-length string draws two entries for the one path.
|
||||
| double_tap_action | int32 | [Double tap action](#double-tap-actions) on a message |
|
||||
| swipe_right_to_left_action | int32 | [Swipe right to left action](#swipe-right-to-left-actions) in a channel |
|
||||
|
||||
<sup>1</sup> The entries live inside the snapshot, and the account [memes](/http-api/memes/) collection holds none of them
|
||||
<sup>1</sup> The entries are inside the snapshot, and the account [memes](/http-api/memes/) collection holds none of them
|
||||
|
||||
<sup>2</sup> The only bare scalar on the root message, so it has no presence and reads as false until a client stores true. It applies to the content a client is about to send, and the first-party client leaves the URLs inside inline code and code blocks unchanged
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -4,7 +4,7 @@ title: Announcement channels
|
||||
description: Publishing messages from an announcement channel into the channels that follow it.
|
||||
---
|
||||
|
||||
An announcement channel is a guild text channel, [type](/http-api/channels/#channel-types) `5`, whose messages can be published. Other channels follow it, and each published message is copied into every following channel. The copy is posted by a channel follower [webhook](/http-api/webhooks/#webhook-types) that lives in the following channel.
|
||||
An announcement channel is a guild text channel, [type](/http-api/channels/#channel-types) `5`, whose messages can be published. Other channels follow it, and each published message is copied into every following channel. The copy is posted by a channel follower [webhook](/http-api/webhooks/#webhook-types) that is in the following channel.
|
||||
|
||||
Every guild can create announcement channels. [Create guild channel](/http-api/guild-channels/#create-guild-channel) accepts `type` `5`, and an announcement channel has the fields, permissions, and slowmode of a text channel.
|
||||
|
||||
|
||||
@@ -196,6 +196,7 @@ $FluxerSecretKeys = @(
|
||||
@{Name = 'FLUXER_ADMIN_SECRET_KEY_BASE'; Kind = 'hex'}
|
||||
@{Name = 'FLUXER_ADMIN_OAUTH_CLIENT_SECRET'; Kind = 'hex'}
|
||||
@{Name = 'LIVEKIT_API_SECRET'; Kind = 'hex'}
|
||||
@{Name = 'FLUXER_SETUP_TOKEN'; Kind = 'hex'}
|
||||
@{Name = 'FLUXER_VAPID_PUBLIC_KEY'; Kind = 'vapid_public'}
|
||||
@{Name = 'FLUXER_VAPID_PRIVATE_KEY'; Kind = 'vapid_private'}
|
||||
)
|
||||
@@ -214,6 +215,13 @@ function Write-FluxerLine([string]$Message) {
|
||||
Write-Host $Message
|
||||
}
|
||||
|
||||
function Write-FluxerFirstRun([string]$EnvPath) {
|
||||
Write-FluxerLine 'Open it and enter this setup token to create the admin account:'
|
||||
Write-FluxerLine ''
|
||||
Write-FluxerLine " $(Get-FluxerEnvValue $EnvPath 'FLUXER_SETUP_TOKEN')"
|
||||
Write-FluxerLine ''
|
||||
}
|
||||
|
||||
function Write-FluxerProblem([string]$Message) {
|
||||
[Console]::Error.WriteLine($Message)
|
||||
}
|
||||
@@ -692,7 +700,8 @@ function Move-FluxerStackFiles([string]$StagingDir, [string]$TargetDir) {
|
||||
# knows. FLUXER_VAPID_EMAIL is optional, and compose derives admin@FLUXER_DOMAIN while it is unset.
|
||||
# The five other non-secret keys in the list above ship correct in .env.example and need no edit.
|
||||
#
|
||||
# Every secret in .env.example contains the literal CHANGE_ME. A key whose name ends in _BASE64
|
||||
# Every secret in .env.example contains the literal CHANGE_ME, apart from FLUXER_SETUP_TOKEN,
|
||||
# which sits there as a commented line. A key whose name ends in _BASE64
|
||||
# takes 32 random bytes as base64, every other key takes 32 random bytes as hex, and the VAPID pair
|
||||
# comes from the generator above.
|
||||
#
|
||||
@@ -1457,7 +1466,7 @@ function Get-FluxerFreeKb([string]$Path) {
|
||||
# docker compose up -d
|
||||
function Copy-FluxerVolumes([string]$Record, [string]$Project, [string]$TargetDir) {
|
||||
if (-not (Test-FluxerStackDefinesService 'seaweedfs' $TargetDir)) {
|
||||
Write-FluxerLine 'Skipping the uploads copy. This stack defines no seaweedfs service, so its objects live outside the stack and only the operator of that store can copy them.'
|
||||
Write-FluxerLine 'Skipping the uploads copy. This stack defines no seaweedfs service, so its objects are outside the stack and only the operator of that store can copy them.'
|
||||
return
|
||||
}
|
||||
$present = @()
|
||||
@@ -2263,7 +2272,8 @@ function Invoke-FluxerInstall {
|
||||
|
||||
if ($NoStart) {
|
||||
Write-FluxerLine "Run docker compose up -d in $targetDir to start the instance."
|
||||
Write-FluxerLine "Secrets live in $envPath. Back that file up."
|
||||
Write-FluxerFirstRun $envPath
|
||||
Write-FluxerLine "Secrets are in $envPath. Back that file up."
|
||||
exit 0
|
||||
}
|
||||
|
||||
@@ -2281,8 +2291,8 @@ function Invoke-FluxerInstall {
|
||||
}
|
||||
Test-FluxerPublicHealth $readyOrigin
|
||||
Write-FluxerLine "Instance ready at $readyOrigin"
|
||||
Write-FluxerLine 'Open it and create the first admin account. Finish the setup wizard in the same sitting.'
|
||||
Write-FluxerLine "Secrets live in $envPath. Back that file up."
|
||||
Write-FluxerFirstRun $envPath
|
||||
Write-FluxerLine "Secrets are in $envPath. Back that file up."
|
||||
} finally {
|
||||
Pop-Location
|
||||
}
|
||||
|
||||
@@ -119,6 +119,7 @@ FLUXER_MEDIA_PROXY_UPLOAD_RELAY_SECRET_BASE64 base64
|
||||
FLUXER_ADMIN_SECRET_KEY_BASE hex
|
||||
FLUXER_ADMIN_OAUTH_CLIENT_SECRET hex
|
||||
LIVEKIT_API_SECRET hex
|
||||
FLUXER_SETUP_TOKEN hex
|
||||
FLUXER_VAPID_PUBLIC_KEY vapid_public
|
||||
FLUXER_VAPID_PRIVATE_KEY vapid_private
|
||||
KEYS
|
||||
@@ -241,6 +242,13 @@ fluxer_say() {
|
||||
printf '%s\n' "$1"
|
||||
}
|
||||
|
||||
fluxer_say_first_run() {
|
||||
fluxer_say 'Open it and enter this setup token to create the admin account:'
|
||||
fluxer_say ''
|
||||
fluxer_say " $(fluxer_env_scalar FLUXER_SETUP_TOKEN)"
|
||||
fluxer_say ''
|
||||
}
|
||||
|
||||
fluxer_fail() {
|
||||
printf '%s\n' "$2" >&2
|
||||
exit "$1"
|
||||
@@ -984,7 +992,8 @@ fluxer_generate_vapid() {
|
||||
# other non-secret keys in the list above ship correct in .env.example and need no
|
||||
# edit.
|
||||
#
|
||||
# Every secret in .env.example contains the literal CHANGE_ME. A key whose name
|
||||
# Every secret in .env.example contains the literal CHANGE_ME, apart from
|
||||
# FLUXER_SETUP_TOKEN, which sits there as a commented line. A key whose name
|
||||
# ends in _BASE64 takes openssl rand -base64 32, every other key takes
|
||||
# openssl rand -hex 32, and the VAPID pair comes from the generator above.
|
||||
#
|
||||
@@ -1689,7 +1698,7 @@ fluxer_free_kb() {
|
||||
# docker compose up -d
|
||||
fluxer_copy_volumes() {
|
||||
if ! fluxer_stack_defines_service seaweedfs; then
|
||||
fluxer_say 'Skipping the uploads copy. This stack defines no seaweedfs service, so its objects live outside the stack and only the operator of that store can copy them.'
|
||||
fluxer_say 'Skipping the uploads copy. This stack defines no seaweedfs service, so its objects are outside the stack and only the operator of that store can copy them.'
|
||||
return 0
|
||||
fi
|
||||
fluxer_backup_volumes > "$fluxer_scratch/backup-volumes"
|
||||
@@ -2240,8 +2249,8 @@ fluxer_say 'That .env serves https on 443, which is the only layout this script
|
||||
|
||||
if [ "$opt_no_start" -eq 1 ]; then
|
||||
fluxer_say "Start the instance with $fluxer_engine compose up -d in $opt_dir."
|
||||
fluxer_say 'Open it and create the first admin account. Finish the setup wizard in the same sitting.'
|
||||
fluxer_say "Secrets live in $opt_dir/.env. Back that file up."
|
||||
fluxer_say_first_run
|
||||
fluxer_say "Secrets are in $opt_dir/.env. Back that file up."
|
||||
exit 0
|
||||
fi
|
||||
|
||||
@@ -2265,5 +2274,5 @@ fi
|
||||
fluxer_probe "$fluxer_ready_origin"
|
||||
|
||||
fluxer_say "Instance ready at $fluxer_ready_origin"
|
||||
fluxer_say 'Open it and create the first admin account. Finish the setup wizard in the same sitting.'
|
||||
fluxer_say "Secrets live in $opt_dir/.env. Back that file up."
|
||||
fluxer_say_first_run
|
||||
fluxer_say "Secrets are in $opt_dir/.env. Back that file up."
|
||||
Reference in new issue
Block a user