diff --git a/.vitepress/config.ts b/.vitepress/config.ts index a7ad1ef..2bf35e5 100644 --- a/.vitepress/config.ts +++ b/.vitepress/config.ts @@ -42,7 +42,10 @@ export default defineConfig({ items: [ {text: 'Intro to Gotify Plugins', link: '/docs/plugin'}, {text: 'Writing Plugins', link: '/docs/plugin-write'}, - {text: 'Building and Deploying Plugins', link: '/docs/plugin-deploy'}, + { + text: 'Building and Deploying Plugins', + link: '/docs/plugin-deploy', + }, ], }, { @@ -54,8 +57,11 @@ export default defineConfig({ ], }, { - text: 'Miscellaneous', + text: 'Guides', items: [ + {text: 'OpenID Connect (OIDC)', link: '/docs/oidc'}, + {text: 'Session Elevation', link: '/docs/session-elevation'}, + {text: 'Migrate to 3.x', link: '/docs/migrate-to-3'}, {text: 'Apache reverse proxy', link: '/docs/apache'}, {text: 'Caddy 2 reverse proxy', link: '/docs/caddy'}, {text: 'Haproxy reverse proxy', link: '/docs/haproxy'}, diff --git a/config.data.ts b/config.data.ts new file mode 100644 index 0000000..1020076 --- /dev/null +++ b/config.data.ts @@ -0,0 +1,34 @@ +import {createMarkdownRenderer} from 'vitepress'; + +const SOURCE = 'https://raw.githubusercontent.com/gotify/server/master/gotify-server.env.example'; + +const uncomment = (line: string): string => line.replace(/^#( ?)/, ''); +const codeBlock = (text: string) => '```\n' + text.trimEnd() + '\n```'; + +const renderSetting = (block: string[]): string => { + const assignment = block.find((l) => /^[A-Z][A-Z0-9_]*=/.test(uncomment(l))); + if (!assignment) throw Error('could not find assignment ' + block); + + const name = uncomment(assignment).split('=')[0]; + return ['### ' + name, codeBlock(block.join('\n'))].join('\n\n'); +}; + +export default { + async load() { + const res = await fetch(SOURCE); + if (!res.ok) throw Error('could not fetch gotify-server.env.example'); + + const [header, ...blocks] = (await res.text()) + .trimEnd() + .split(/\n *\n/) + .map((b) => b.split('\n')) + .filter((b) => b.some((l) => l.trim())); + + const markdown = [ + codeBlock(header.map(uncomment).join('\n')), + ...blocks.map(renderSetting), + ].join('\n\n'); + + return (await createMarkdownRenderer('.')).render(markdown); + }, +}; diff --git a/docs/config.md b/docs/config.md index a4d46a8..c5f595e 100644 --- a/docs/config.md +++ b/docs/config.md @@ -1,5 +1,20 @@ # Configuration + + +::: details 3.x Config + +Gotify 3.x is configured through environment variables, which can be loaded from +an env file. [`gotify-server.env.example`](https://raw.githubusercontent.com/gotify/server/refs/heads/master/gotify-server.env.example) + +
+ +::: + +::: details 2.x Config + gotify/server can be configured per config file and environment variables. When using docker it is recommended to use environment variables. @@ -40,33 +55,46 @@ server: enabled: false # if the certificate should be requested from letsencrypt accepttos: false # if you accept the tos from letsencrypt cache: data/certs # the directory of the cache from letsencrypt + directoryurl: # override the directory url of the ACME server + # Let's Encrypt highly recommend testing against their staging environment before using their production environment. + # Staging server has high rate limits for testing and debugging, issued certificates are not valid + # example: https://acme-staging-v02.api.letsencrypt.org/directory hosts: # the hosts for which letsencrypt should request certificates - # - mydomain.tld - # - myotherdomain.tld + # - mydomain.tld + # - myotherdomain.tld responseheaders: # response headers are added to every response (default: none) - # X-Custom-Header: "custom value" + # X-Custom-Header: "custom value" + trustedproxies: # IPs or IP ranges of trusted proxies. Used to obtain the remote ip via the X-Forwarded-For header. (configure 127.0.0.1 to trust sockets) - # - 127.0.0.1 - # - 192.168.178.0/24 + # - 127.0.0.1/32 # - ::1 + securecookie: false # If the secure flag should be set on cookies. See https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Set-Cookie#secure cors: # Sets cors headers only when needed and provides support for multiple allowed origins. Overrides Access-Control-* Headers in response headers. alloworigins: - # - ".+.example.com" - # - "otherdomain.com" + # - ".+.example.com" + # - "otherdomain.com" allowmethods: - # - "GET" - # - "POST" + # - "GET" + # - "POST" allowheaders: - # - "Authorization" - # - "content-type" - + # - "Authorization" + # - "content-type" stream: pingperiodseconds: 45 # the interval in which websocket pings will be sent. Only change this value if you know what you are doing. - allowedorigins: # allowed origins for websocket connections (same origin is always allowed, default only same origin) + allowedorigins: # allowed origins for websocket connections (same origin is always allowed) # - ".+.example.com" # - "otherdomain.com" -database: # see below +oidc: + enabled: false # Enable OpenID Connect login, allowing users to authenticate via an external identity provider (e.g. Keycloak, Authelia, Google). + issuer: # The OIDC issuer URL. This is the base URL of your identity provider, used to discover endpoints. Example: "https://auth.example.com/realms/myrealm" + clientid: # The client ID registered with your identity provider for this application. + clientsecret: # The client secret for the registered client. + redirecturl: http://gotify.example.org/auth/oidc/callback # The callback URL that the identity provider redirects to after authentication. Must match exactly what is configured in your identity provider. + autoregister: true # If true, automatically create a new user on first OIDC login. If false, only existing users can log in via OIDC. + usernameclaim: preferred_username # The OIDC claim used to determine the username. Common values: "preferred_username" or "email". + +database: # for database see (configure database section) dialect: sqlite3 connection: data/gotify.db defaultuser: # on database creation, gotify creates an admin user (these values will only be used for the first start, if you want to edit the user after the first start use the WebUI) @@ -118,7 +146,8 @@ GOTIFY_SERVER_SSL_CERTFILE= GOTIFY_SERVER_SSL_CERTKEY= GOTIFY_SERVER_SSL_LETSENCRYPT_ENABLED=false GOTIFY_SERVER_SSL_LETSENCRYPT_ACCEPTTOS=false -GOTIFY_SERVER_SSL_LETSENCRYPT_CACHE=certs +GOTIFY_SERVER_SSL_LETSENCRYPT_CACHE=data/certs +GOTIFY_SERVER_SSL_LETSENCRYPT_DIRECTORYURL= # GOTIFY_SERVER_SSL_LETSENCRYPT_HOSTS=[mydomain.tld, myotherdomain.tld] # GOTIFY_SERVER_RESPONSEHEADERS={X-Custom-Header: "custom value", x-other: value} # GOTIFY_SERVER_TRUSTEDPROXIES=[127.0.0.1,192.168.178.2/24] @@ -127,6 +156,7 @@ GOTIFY_SERVER_SSL_LETSENCRYPT_CACHE=certs # GOTIFY_SERVER_CORS_ALLOWHEADERS=[X-Gotify-Key, Authorization] # GOTIFY_SERVER_STREAM_ALLOWEDORIGINS=[.+.example\.com, otherdomain\.com] GOTIFY_SERVER_STREAM_PINGPERIODSECONDS=45 +GOTIFY_SERVER_SECURECOOKIE=false GOTIFY_DATABASE_DIALECT=sqlite3 GOTIFY_DATABASE_CONNECTION=data/gotify.db GOTIFY_DEFAULTUSER_NAME=admin @@ -135,4 +165,13 @@ GOTIFY_PASSSTRENGTH=10 GOTIFY_UPLOADEDIMAGESDIR=data/images GOTIFY_PLUGINSDIR=data/plugins GOTIFY_REGISTRATION=false +GOTIFY_OIDC_ENABLED=false +GOTIFY_OIDC_ISSUER= +GOTIFY_OIDC_CLIENTID= +GOTIFY_OIDC_CLIENTSECRET= +GOTIFY_OIDC_REDIRECTURL=http://gotify.example.org/auth/oidc/callback +GOTIFY_OIDC_AUTOREGISTER=true +GOTIFY_OIDC_USERNAMECLAIM=preferred_username ``` + +::: diff --git a/docs/migrate-to-3.md b/docs/migrate-to-3.md new file mode 100644 index 0000000..c23cd75 --- /dev/null +++ b/docs/migrate-to-3.md @@ -0,0 +1,116 @@ +# Migrate to 3.x + +- The `config.yml` file is no longer supported, convert it to the new env format + with [`migrate-config`](#migrating-your-config). +- If you set list or map environment variables, their syntax changed, see + [List and map syntax](#list-and-map-syntax). +- If you have scripts hitting client-token endpoints, they may now need + [elevation](#adapting-your-scripts). + +## Config Changes + +### YAML config file removed + +The YAML config file (`config.yml`) is no longer supported. Gotify can now be +only configured by environment variables, which can be loaded from an env file. +The first existing file from this search order is loaded: + +1. `gotify-server.env` (in the working directory) +2. `$XDG_CONFIG_HOME/gotify/gotify-server.env` (`$XDG_CONFIG_HOME` falls back to `$HOME/.config` when unset) +3. `/etc/gotify/server.env` + +See the [Configuration](/docs/config) page for the full list of variables. + +### Migrating your config + +The `migrate-config` command converts an existing `config.yml` to the new env +format. It prints the result to stdout. + +```bash +$ gotify-server migrate-config config.yml > gotify-server.env +``` + +With Docker: + +```bash +$ docker run --rm -v "$(pwd)/config.yml:/app/config.yml" gotify/server \ + migrate-config config.yml > gotify-server.env +``` + +### Environment List and map syntax + +Defining settings via environment variables was already possible, but the syntax +for list and map values has changed. If you set any of the variables below, update +their format. + +**Lists** are now comma-separated instead of a YAML array: + +- `GOTIFY_SERVER_TRUSTEDPROXIES` +- `GOTIFY_SERVER_CORS_ALLOWORIGINS` +- `GOTIFY_SERVER_CORS_ALLOWMETHODS` +- `GOTIFY_SERVER_CORS_ALLOWHEADERS` +- `GOTIFY_SERVER_STREAM_ALLOWEDORIGINS` +- `GOTIFY_SERVER_SSL_LETSENCRYPT_HOSTS` +- `GOTIFY_OIDC_SCOPES` + +```bash +# before +GOTIFY_SERVER_TRUSTEDPROXIES=[127.0.0.1/32, ::1] +# after +GOTIFY_SERVER_TRUSTEDPROXIES=127.0.0.1/32,::1 +``` + +**Maps** are now a JSON object instead of a YAML map: + +- `GOTIFY_SERVER_RESPONSEHEADERS` + +```bash +# before +GOTIFY_SERVER_RESPONSEHEADERS={X-Custom-Header: "custom value"} +# after +GOTIFY_SERVER_RESPONSEHEADERS={"X-Custom-Header":"custom value"} +``` + +## API Changes + +Introduces step-up authentication via time-limited [session +elevation](./session-elevation.md). A session/client token must re-authenticate +before sensitive, hard-to-undo actions. + +HTTP Basic auth and application tokens are unaffected. + +### Endpoints that now require elevation + +With a non-elevated client token these return `403`: + +| Endpoint | Action | +| :-------------------------------------------- | :----------------------------- | +| `POST /current/user/password` | Change current user's password | +| `DELETE /client/{id}` | Delete a client | +| `DELETE /application/{id}` | Delete an application | +| `POST /client/{id}/elevate` | Elevate a client token | +| `GET /user`, `GET`/`POST`/`DELETE /user/{id}` | Manage users (admin) | + +The `Client` and `CurrentUser` models have gotten elevation-related fields. See the +[API documentation](/api-docs) for details. + +### Adapting your scripts + +Scripts that hit the endpoints above with a client token now need that token to +be elevated. Either: + +- Use HTTP Basic auth as they are elevated by default. +- Elevate the client in the WebUI or the api with basic auth. + +## CLI Changes + +The binary now uses subcommands. You should migrate to using the `serve` +subcommand. For backwards compatibility running gotify without a command will +continue to serve the server. + +```bash +$ ./gotify-linux-amd64 serve +``` + +The Docker image already defaults to `serve`, so `docker run` and Docker Compose +setups keep working unchanged. diff --git a/docs/oidc.md b/docs/oidc.md new file mode 100644 index 0000000..58e61d0 --- /dev/null +++ b/docs/oidc.md @@ -0,0 +1,108 @@ +# OpenID Connect (OIDC) + +[[toc]] + +Gotify supports OpenID Connect for Single Sign-On (SSO), allowing users to authenticate via an external identity provider such as Authelia or Dex. + +::: warning +The identity provider **must** support [PKCE](https://oauth.net/2/pkce/) (Proof Key for Code Exchange). IdPs without PKCE support are currently unsupported. +::: + +## Configuration + +| Variable | Description | +| :----------------------------- | :--------------------------------------------------------------------------------------------------------- | +| `GOTIFY_OIDC_ENABLED` | Enable OIDC login. | +| `GOTIFY_OIDC_ISSUER` | The OIDC issuer URL. Used to discover endpoints via `/.well-known/openid-configuration`. | +| `GOTIFY_OIDC_CLIENTID` | The client ID registered with your identity provider. | +| `GOTIFY_OIDC_CLIENTSECRET` | The client secret. | +| `GOTIFY_OIDC_REDIRECTURL` | The callback URL the identity provider redirects to after authentication. Must match your provider config. | +| `GOTIFY_OIDC_AUTOREGISTER` | Automatically create a new Gotify user on first OIDC login. | +| `GOTIFY_OIDC_USERNAMECLAIM` | The OIDC claim used to determine the username. Common values: `preferred_username` or `email`. | +| `GOTIFY_OIDC_LINK_BY_USERNAME` | Link an OIDC identity to an existing local user with the same username. Disabled by default. | +| `GOTIFY_OIDC_SCOPES` | Comma-separated scopes to request. Defaults to `openid,profile,email`. | + +```bash +GOTIFY_OIDC_ENABLED=true +GOTIFY_OIDC_ISSUER=https://auth.example.org +GOTIFY_OIDC_CLIENTID=gotify +GOTIFY_OIDC_CLIENTSECRET=YOUR_CLIENT_SECRET +GOTIFY_OIDC_REDIRECTURL=https://gotify.example.org/auth/oidc/callback +GOTIFY_OIDC_AUTOREGISTER=true +GOTIFY_OIDC_USERNAMECLAIM=preferred_username +GOTIFY_OIDC_LINK_BY_USERNAME=false +GOTIFY_OIDC_SCOPES=openid,profile,email +``` + +See the [Configuration](/docs/config) page for the full config reference. + +### Redirect URL + +- The redirect URL must always end with `/auth/oidc/callback`. +- If Gotify is served at the root, the redirect URL is `https://gotify.example.org/auth/oidc/callback`. +- If Gotify is served on a sub-path (e.g. behind a reverse proxy at `/gotify/`), the sub-path must be included: `https://example.org/gotify/auth/oidc/callback`. +- For the **Android app** to support OIDC login, you must add `gotify://oidc/callback` as an additional redirect URL in your identity provider's client configuration. + +This URL must match **exactly** between the Gotify config and your identity provider's client configuration. + +## Linking by username + +Gotify identifies users by username. When the OIDC username claim clashes with an existing local user that is not yet bound to an OIDC identity, this login is rejected by default. Set `GOTIFY_OIDC_LINK_BY_USERNAME=true` to bind OIDC identities to existing local users. + +Only enable it if you trust that usernames in your identity provider map to the same people as your Gotify usernames. + +## Sample IdP Config + +### Authelia + +[Authelia](https://www.authelia.com/) is a self-hosted authentication and authorization server. + +::: details Authelia configuration (configuration.yml) + +```yml +identity_providers: + oidc: + clients: + - client_id: 'gotify' + client_name: 'gotify' + client_secret: '$pbkdf2-sha512$310000$...' # generate with: authelia crypto hash generate pbkdf2 + public: false + authorization_policy: 'two_factor' + require_pkce: true + pkce_challenge_method: 'S256' + consent_mode: implicit + redirect_uris: + - 'https://gotify.example.org/auth/oidc/callback' # See redirect url docs + - 'gotify://oidc/callback' # Required for Android app OIDC login + scopes: + - 'openid' + - 'profile' + - 'email' + response_types: + - 'code' + grant_types: + - 'authorization_code' + access_token_signed_response_alg: 'none' + userinfo_signed_response_alg: 'none' + token_endpoint_auth_method: 'client_secret_basic' +``` + +::: + +### Dex + +[Dex](https://dexidp.io/) is a federated OpenID Connect provider. + +::: details Dex configuration + +```yml +staticClients: + - id: gotify + redirectURIs: + - 'https://gotify.example.org/auth/oidc/callback' # See redirect url docs + - 'gotify://oidc/callback' # Required for Android app OIDC login + name: 'Gotify' + secret: secret +``` + +::: diff --git a/docs/session-elevation.md b/docs/session-elevation.md new file mode 100644 index 0000000..92a9509 --- /dev/null +++ b/docs/session-elevation.md @@ -0,0 +1,38 @@ +# Session Elevation + +[[toc]] + +Session elevation requires you to re-authenticate before performing sensitive, +hard-to-undo actions, even while already logged in. + +## What requires elevation + +These actions need an **elevated** session; with a plain client token they +return `403 Forbidden`: + +| Action | Endpoint | +| :----------------------------- | :-------------------------------------------- | +| Change current user's password | `POST /current/user/password` | +| Delete a client | `DELETE /client/{id}` | +| Delete an application | `DELETE /application/{id}` | +| Elevate a session | `POST /client/{id}/elevate` | +| Manage users (admin) | `GET /user`, `GET`/`POST`/`DELETE /user/{id}` | + +## How to elevate + +### In the WebUI + +When you trigger a protected action, the UI prompts you to re-authenticate and +then performs the action. + +### Via the API + +If you use a client token in a script, you have two options. + +1. basic-auth requests are always elevated (see below), so you can call the protected endpoint directly with username and password: + + ```bash + curl -u "user:password" -X DELETE "https://gotify.example.com/client/7" + ``` + +2. If you must authenticate with the token itself, elevate its session first. This can be done via the API or the WebUI.