diff --git a/quill/networking-and-dns.mdx b/quill/networking-and-dns.mdx index 1b7b35d668..f3710913e4 100644 --- a/quill/networking-and-dns.mdx +++ b/quill/networking-and-dns.mdx @@ -59,7 +59,7 @@ By default, all five resolve to the IP address provided during sign-up: | -------------------- | ---------------------------------------------------------------------------------------- | | `dashboard.` | The operator dashboard, dashboard API requests, and health checks. | | `api.` | Programmatic access to the Quill API and health checks, without the dashboard UI. | -| `public.` | Embedded chat pages and their chat requests. | +| `public.` | Embedded chat pages, widget assets, chat requests, and public channel webhooks. | | `db.` | Direct RavenDB access for applications authenticated with a client certificate. | | `a.` | RavenDB's advertised node URL, which RavenDB.Client may receive through topology discovery. | @@ -68,11 +68,10 @@ The first four hostnames expose Quill's customer-facing service surfaces. Quill does not publish corresponding `-tcp` records. Direct RavenDB access through `db.` or a topology-discovered `a.` address uses HTTPS on port `443`; -the native RavenDB TCP listener is not exposed by default. -See [Direct database access](security-and-architecture/network-architecture.mdx#direct-database-access). +the native RavenDB TCP listener is not exposed by default. +Learn more in [Direct database access](security-and-architecture/network-architecture.mdx#direct-database-access). -For details about how connections to these hostnames are routed and protected, -see [The Quill hostnames](security-and-architecture/network-architecture.mdx#the-quill-hostnames). +For details about how connections to these hostnames are routed and protected, see [The Quill hostnames](security-and-architecture/network-architecture.mdx#the-quill-hostnames). The behavior of the RavenDB node hostname is described in [The `a` hostname](#the-a-hostname). @@ -120,8 +119,8 @@ Because the records belong to a RavenDB-managed DNS zone: * You do not receive DNS-provider credentials or a DNS console for the instance. * Moving or restarting your Quill does not update the records automatically. -When a record must change, the update is requested through the RavenDB DNS service at `api.ravendb.net`. -The supported update command reads the license file from the activated setup package and uses it to authorize the request for the licensed domain. +When a record must change, run the supported `update-dns` command from the Docker host. +The command reads the licensed domain from the activated server certificate and uses the setup package's `license.json` file to authorize the request to the RavenDB DNS service at `api.ravendb.net`. The exact command and the records it updates are described in [Update the records](#update-the-records). @@ -201,7 +200,7 @@ The Quill domain and the URLs used by operators, chat users, and applications re | Wildcard server certificate | The certificate is issued for `*.`, not for a particular IP address. | | Activated setup package and license | Your Quill continues using the existing activated setup package and its license file. | | Dashboard API key | The key is independent of the host's IP address; supply the same `QUILL_API_KEY` value to a replacement container. | -| Quill configuration and data | Apps, agents, channels, conversations, RavenDB databases, and certificate registrations remain in the `quill-data` volume. | +| Quill configuration and data | Apps, agents, channels, conversations, RavenDB databases, and certificate registrations remain in the persistent Docker volume mounted at `/var/lib/quill`. | Repointing the DNS records therefore does not require a new domain, new URLs, a new wildcard certificate, or a different Dashboard API key. @@ -213,12 +212,12 @@ For information about preserving or replacing the Dashboard API key, see [Operat #### Moving to another host -A DNS update does not copy your Quill's data or container configuration. +* A DNS update does not copy your Quill's data or container configuration. -When moving your Quill to another host, transfer the `quill-data` volume and preserve the required deployment environment values, -including `QUILL_API_KEY` and `QUILL_LICENSE_KEY`, before directing traffic to the new host. +* When moving your Quill to another host, transfer the persistent Docker volume mounted at `/var/lib/quill` and preserve the required deployment environment values, + including `QUILL_API_KEY` and `QUILL_LICENSE_KEY`, before directing traffic to the new host. -If only the existing host's externally reachable IP address changes, no data-volume migration is required. +* If only the existing host's externally reachable IP address changes, no data-volume migration is required. @@ -226,55 +225,32 @@ If only the existing host's externally reachable IP address changes, no data-vol -{/* TODO before publication - See RavenDB-27372: -Do not publish either `docker exec quill update-dns --ip ...` -or the direct `/app/ravendb/rvn dns update ...` command -until the release image is finalized and the supported command is verified. - -The default release image currently has neither the wrapper nor an `rvn` version that supports `dns update`. -Dockerfile.source has them but is planned for removal. -The dashboard currently displays the wrapper command. -*/} - -Run the DNS update from the Docker host after your Quill is available at its new address. +Run the DNS update from the Docker host after the Quill container is running on the host with its new externally reachable address. The container must: * be running and activated; - * contain the activated setup package and its `license.json` file; + * contain the activated setup package, including its server certificate and `license.json` file; * have outbound HTTPS access to `api.ravendb.net`. -The DNS update uses the license file for authorization. -It does not use the Dashboard API key or an authenticated browser session. - ---- - -### Find the base domain - -If you do not know the instance's base domain, read `PublicServerUrl` from the activated setup package: - -```bash -docker exec quill jq -r '.PublicServerUrl' \ - /var/lib/quill/setup/A/settings.json -``` -
- -The value has the form `https://a.`. -Use only the `` portion in the DNS update command, without `https://`, `a.`, or a port number. +The command derives the base domain from the server certificate and uses the license file for authorization. +You do not need to supply the domain, the Dashboard API key, or an authenticated browser session. --- ### Update the five DNS records -Run the following command, replacing `` with the base domain and `` with the host's new address: +In the commands below, `` is the name specified by `docker run --name`. +By default, it matches your Quill name. + +Run the following command, replacing `` with the host's new address: ```bash -docker exec quill /app/ravendb/rvn dns update \ - -l /var/lib/quill/setup/license.json \ - -d \ - -n =a,dashboard,db,public,api +docker exec update-dns --ip ```
+If the container uses the default name, the dashboard's **Settings > IP configuration** page generates this command with the container name filled in after you enter a new IPv4 address. + The command updates all five DNS records in one request: * `dashboard.` @@ -292,17 +268,13 @@ Callers may still receive the previous address from DNS caches, as explained in #### Assigning more than one address -To assign several addresses to each of the five DNS hostnames, list them before the `=` and separate them with commas: +* To assign several addresses to each of the five DNS hostnames, separate them with commas in the value passed to `--ip`: -```bash -docker exec quill /app/ravendb/rvn dns update \ - -l /var/lib/quill/setup/license.json \ - -d \ - -n ,=a,dashboard,db,public,api -``` -
+ ```bash + docker exec update-dns --ip , + ``` -The command accepts both IPv4 and IPv6 addresses. +* The command accepts both IPv4 and IPv6 addresses. @@ -310,11 +282,11 @@ The command accepts both IPv4 and IPv6 addresses. #### If the command times out -The command waits up to 15 minutes for the DNS service to complete the request. +* The command waits up to 15 minutes for the DNS service to complete the request. -A timeout means that the command stopped waiting. -It does not necessarily mean that the DNS update was rejected or rolled back. -Verify the published records before running the command again. +* A timeout means that the command stopped waiting. + It does not necessarily mean that the DNS update was rejected or rolled back. + Verify the published records before running the command again. @@ -362,10 +334,6 @@ Restarting an affected browser or application may clear its local cache, but it After allowing for propagation, confirm the current answers as described in [Verify the change](#verify-the-change). -{/* TTL verified against the live Quill DNS records on 2026-08-17. -Reverify if the DNS service configuration changes before publication. -*/} -
@@ -444,8 +412,8 @@ Use the point at which verification fails to identify the affected layer: | Symptom | Likely cause | Next check | | ------- | ------------ | ---------- | -| The DNS update command exits with an error | The new IP address is invalid, the activated setup package or license file is unavailable, outbound HTTPS failed, or the DNS service rejected the request | Read the returned error, verify the new IP address and setup files, confirm outbound access to `api.ravendb.net`, and verify all five DNS records before retrying | -| A hostname does not resolve | The hostname or base domain is incorrect, or the record was not provisioned | Compare the hostname with the `PublicServerUrl` in the activated setup package | +| The DNS update command exits with an error | The new IP address is invalid, the activated server certificate or license file is unavailable, outbound HTTPS failed, or the DNS service rejected the request | Read the returned error, verify the new IP address and the activated setup package, confirm outbound access to `api.ravendb.net`, and verify all five DNS records before retrying | +| A hostname does not resolve | The hostname or base domain is incorrect, the record was not provisioned, or a resolver cached an earlier "not found" response | Compare the hostname with the `PublicServerUrl` in the activated setup package. If the hostname was recently provisioned, wait up to 15 minutes for the cached response to expire, then resolve it again | | A hostname still resolves to the old address | The answer is cached or the update has not completed | Allow for the TTL, then resolve all five hostnames again | | The hostname resolves correctly, but the connection times out | External TCP port `443` is blocked or does not reach container port `443` | Check the firewall, cloud security rules, NAT or load-balancer forwarding, and Docker port mapping | | The hostname resolves correctly, but the connection is refused | The container is stopped or nothing is accepting the forwarded connection | Check the container state and Docker port mapping | @@ -458,22 +426,24 @@ Use the point at which verification fails to identify the affected layer: ### Check the container and published port +In the commands below, `` is the name specified by `docker run --name`. By default, it matches your Quill name. + Confirm that the container is running: ```bash -docker inspect quill --format "Status={{.State.Status}}" +docker inspect --format "Status={{.State.Status}}" ```
Confirm that the container's HTTPS port is published: ```bash -docker port quill 443/tcp +docker port 443/tcp ```
If the container is stopped, start it before continuing. -If port `443` is not published, review the Docker Compose configuration. +If port `443` is not published, review how the container was started and confirm that container port `443` is published to the host. --- @@ -482,7 +452,7 @@ If port `443` is not published, review the Docker Compose configuration. Bypass external DNS, the host firewall, and nginx by calling the Quill web application directly: ```bash -docker exec quill curl --fail --silent --show-error \ +docker exec curl --fail --silent --show-error \ http://127.0.0.1:5000/healthz ```
@@ -490,7 +460,7 @@ docker exec quill curl --fail --silent --show-error \ Then inspect the activation state: ```bash -docker exec quill curl --silent --show-error \ +docker exec curl --silent --show-error \ http://127.0.0.1:5000/api/bootstrap/status ```
@@ -506,23 +476,43 @@ If the internal health check also fails, investigate the Quill web application a Read the most recent Quill web application messages: ```bash -docker exec quill tail -n 100 \ +docker exec tail -n 100 \ /var/lib/quill/logs/web.log ```
-For nginx routing or TLS-termination failures, read: +For nginx request-routing and response-status details, read the access log: + +```bash +docker exec tail -n 100 \ + /var/lib/quill/proxy/access.log +``` +
+ +The access log currently has no timestamps or built-in rotation and can continue growing in the persistent data volume. Monitor its size. + +The access log covers HTTP requests to `dashboard.`, `api.`, and `public.`; +connections passed through to RavenDB for `db.` and `a.` do not appear there. + +Quill does not record `/healthz` requests routed to the dashboard or API listeners in the access log. +On `public.`, `/healthz` is not an allowed endpoint; the resulting `404` request is logged. + +For nginx startup, configuration, certificate-loading, or upstream connection failures, read the error log: ```bash -docker exec quill tail -n 100 \ +docker exec tail -n 100 \ /var/lib/quill/logs/proxy.log ```
+The current `warn` log level does not record common client-side TLS handshake failures or SNI-preread diagnostics that nginx emits below `warn`. + +Quill masks embed-link and Slack webhook tokens in both proxy logs, retaining only the first six characters for correlation. + For RavenDB startup or direct database failures, read: ```bash -docker exec quill tail -n 100 \ +docker exec tail -n 100 \ /var/lib/quill/logs/ravendb.log ```
@@ -530,7 +520,7 @@ docker exec quill tail -n 100 \ For wildcard-certificate renewal failures, read: ```bash -docker exec quill tail -n 100 \ +docker exec tail -n 100 \ /var/lib/quill/logs/certwatch.log ```
@@ -553,13 +543,13 @@ For client-certificate requirements, see [Direct database access](security-and-a Routing, TLS termination, and authorization are handled by your Quill after the connection reaches the host on port `443`. -* If the host's externally reachable IP address changes, update all five DNS records. - The update uses the activated license file to authorize the request. +* If the host's externally reachable IP address changes, run `docker exec update-dns --ip `. + The command derives the domain from the activated server certificate and uses the activated license to authorize the update of all five DNS records. * Updating DNS does not move your Quill to another host. - Preserve the `quill-data` volume and the required deployment environment values when moving it. + Preserve the persistent Docker volume mounted at `/var/lib/quill` and the required deployment environment values when moving it. * The records have a TTL of 30 minutes. After an update, verify both DNS resolution and HTTPS connectivity. -
\ No newline at end of file + diff --git a/quill/security-and-architecture/network-architecture.mdx b/quill/security-and-architecture/network-architecture.mdx index cc51b3a113..ddd3ac9273 100644 --- a/quill/security-and-architecture/network-architecture.mdx +++ b/quill/security-and-architecture/network-architecture.mdx @@ -39,14 +39,34 @@ Its three main components are RavenDB, the Quill web application, and nginx. Stores the documents mirrored from your SQL source and the data belonging to each Quill app, including its agents, channels, and conversations. * **The Quill web application** - Serves the dashboard, the Quill API, and the public chat pages. - It listens on port `5000` inside the container. + Serves the dashboard, the Quill API, public chat pages, and the Slack event webhook. + It listens only on the container's loopback interface, at `127.0.0.1:5000`. * **nginx** Acts as Quill's reverse proxy and external TLS entry point. It listens on port `443` and uses the requested hostname to route each connection to either the Quill web application or RavenDB. -In the default Docker Compose configuration, only port `443` is published to the host. +The process that starts and monitors the container's services runs as `root`. +The one-time volume-preparation step, privileged setup in individual service scripts, and the long-running certificate-watcher and console-monitor services also run as `root`. + +RavenDB, the Quill web application, nginx, and the four file-logging services run under the dedicated, non-login `quill` operating-system account. + +The certificate watcher drops to `quill` when regenerating nginx certificate files, and nginx is granted the capability it needs to bind to port `443` without running as `root`. + + + +#### Start the container as root + +* Do not override the image's default user with a non-root user by using Docker's `--user` option. + The container must start as `root` so it can prepare the data volume and start its services. + It then runs the network-facing services under the `quill` account. + +* The `quill` account has UID and GID `999`. Named Docker volumes are prepared automatically. + If you bind-mount a host directory at `/var/lib/quill`, its filesystem must allow the container to assign the managed paths to UID/GID `999` and allow that identity to write them. + + + +The default Quill deployment publishes only port `443` to the host. The internal ports used by the Quill web application and RavenDB are not published, so external connections can only enter through nginx. @@ -60,7 +80,7 @@ At the beginning of a TLS connection, the caller includes the requested hostname nginx reads this hostname before terminating TLS and uses it to select one of two destination services: * **The web hostnames** (`dashboard.*`, `api.*`, and `public.*`) route to the Quill web application. - nginx terminates TLS using Quill's wildcard server certificate and forwards the request as plain HTTP over the container's internal network. + nginx terminates TLS using Quill's wildcard server certificate and forwards the request as plain HTTP over the container's loopback interface. * **The database hostnames** (`db.*` and `a.*`) route to RavenDB. nginx passes the encrypted connection through unchanged. @@ -108,14 +128,15 @@ In the examples below, `` represents this base domain. | -------------------------------- | --------------------- | ----------------------------------------------------- | ----------------------------------- | | `dashboard.acme.myquill.ai` | Quill web application | The operator dashboard, the Quill API, and `/healthz` | Operators using a browser | | `api.acme.myquill.ai` | Quill web application | The Quill API and `/healthz`; no dashboard UI | Scripts and monitoring tools | -| `public.acme.myquill.ai` | Quill web application | Embedded chat pages and their chat streams | End users | +| `public.acme.myquill.ai` | Quill web application | Embedded chat pages, widget assets, and Slack webhooks | End users and Slack | | `db.acme.myquill.ai` | RavenDB | Direct RavenDB access over mutual TLS | Applications using `RavenDB.Client` | nginx restricts which web routes each hostname can expose: * `api.` forwards `/api/*` and `/healthz`. Other paths return `404`. -* `public.` forwards only `/apps/{slug}/embed/*`. Other paths return `404`. -* `dashboard.` serves the dashboard, `/api/*`, and `/healthz`, but returns `404` for public embed routes. +* `public.` forwards `/apps/{slug}/embed/*`, `/widget/assets/*`, and `/webhooks/slack/{token}`. + Other paths return `404`. +* `dashboard.` serves the dashboard, `/api/*`, and `/healthz`, but returns `404` for public embed and webhook routes. @@ -133,23 +154,28 @@ Use `db.` when configuring your own applications. -`public.` is Quill's end-user surface. +`public.` is Quill's end-user and Slack callback surface. It does not use the Dashboard API key, a browser session, or a client certificate. -Instead, access is granted by the embed-link token included in the URL. +Embedded chat access is granted by the embed-link token included in the URL. +Slack event deliveries use a per-channel token in the callback URL and a Slack request signature. -nginx forwards these three endpoint patterns on `public.`: +nginx forwards these four endpoint patterns on `public.`: -| Endpoint | Purpose | -| -------------------------------------- | ----------------------------------------------------- | -| `GET /apps/{slug}/embed/{token}` | Loads the embedded chat page | -| `POST /apps/{slug}/embed/{token}/chat` | Sends a prompt and streams the agent's reply | +| Endpoint | Purpose | +| -------------------------------------- | ------------------------------------------------------ | +| `GET /apps/{slug}/embed/{token}` | Loads the embedded chat page | +| `POST /apps/{slug}/embed/{token}/chat` | Sends a prompt and streams the agent's reply | | `GET /widget/assets/{file}` | Loads the JavaScript and CSS required by the chat page | +| `POST /webhooks/slack/{token}` | Receives Slack event deliveries | The embedded chat page loads its widget assets from the same `public.` origin. Any proxy or web application firewall placed in front of Quill must allow this path. All other paths on `public.` return `404`. +Quill masks URL-based embed-link and Slack webhook tokens in nginx's access and error logs, +retaining only the first six characters for correlation. + Anyone with an active embed link can use its chat page, subject to the link and channel controls: * The link belongs to one channel in one Quill app. @@ -185,7 +211,8 @@ All operational API endpoints for apps, agents, channels, connection strings, co -The dashboard and database surfaces use separate credentials. +Access to Quill's dashboard and operational APIs requires the Dashboard API key or an authenticated browser session. +Direct RavenDB access requires a client certificate that RavenDB recognizes. @@ -244,24 +271,32 @@ You can generate client certificates from the dashboard's Certificates page and - - -#### Publish only port 443 + -* The default `docker-compose.yml` publishes only the nginx entry point on port `443`. +### Internal service ports -* RavenDB does not start until the setup package is available. - It then starts in secured mode and listens only on the container's loopback interface, at `127.0.0.1:8443`. - This allows nginx and the Quill web application to reach RavenDB from inside the container while preventing other containers from connecting directly. +RavenDB starts in secured mode after the setup package becomes available and listens on the container's loopback interface at `127.0.0.1:8443`. +The Quill web application listens on the loopback interface at `127.0.0.1:5000`. -* The Quill web application listens on port `5000` inside the container, but the default configuration does not publish that port. - Do not publish it: requests sent directly to port `5000` bypass the hostname restrictions enforced by nginx. +These internal addresses allow the container's services to communicate without accepting connections from the host or peer containers. -* For diagnostics, run commands inside the container instead of publishing an internal port: +For diagnostics, run commands inside the container. In this command, `` is the name specified by `docker run --name` and, by default, matches your Quill name. - ```bash - docker exec quill curl -s http://127.0.0.1:5000/api/bootstrap/status - ``` +```bash +docker exec curl -s http://127.0.0.1:5000/api/bootstrap/status +``` +
+ +
+ + + +#### Keep internal ports unpublished + +* Publish only nginx's external entry point on port `443`. + +* Do not publish RavenDB's or the Quill web application's internal ports; + nginx must remain the only external entry point. @@ -269,6 +304,10 @@ You can generate client certificates from the dashboard's Certificates page and + + +### Certificate use + The Quill setup package contains a wildcard server certificate for `*.`. This certificate covers the four customer-facing hostnames and RavenDB's `a.` routing alias. @@ -278,8 +317,27 @@ You do not install the server certificate manually. During activation, Quill extracts the setup package and configures both nginx and RavenDB to use the certificate: * nginx converts the certificate and private key into the required PEM format and presents it for `dashboard.`, `api.`, and `public.`. * For `db.` and `a.`, nginx passes TLS through unchanged, so RavenDB presents the certificate itself. + + + + + +### Certificate storage + +Quill stores the setup package and generated TLS material beneath `/var/lib/quill`. +On a newly prepared volume, the volume-preparation step creates the `setup`, `certs`, and `proxy` directories with owner-only access. +Setup-package extraction then creates `setup/A`, and before RavenDB starts, Quill sets `setup`, `setup/A`, and `certs` to owner-only access. -RavenDB checks the server certificate once an hour and renews it automatically. +The copied server PFX and nginx private key are readable and writable only by their owner. +Inside the container, the `quill` account owns this material; `root` can also access it. + + + + + +### Renewal and monitoring + +RavenDB checks the server certificate once an hour and renews it automatically. By default, renewal begins when 30% of the certificate's validity period remains (about 27 days for a 90-day Let's Encrypt certificate). Renewal requires outbound HTTPS access to `api.ravendb.net` and the default Let's Encrypt ACME service at `acme-v02.api.letsencrypt.org`. @@ -293,12 +351,17 @@ Successful nginx certificate reloads are recorded in `/var/lib/quill/logs/certwa Inspect these logs from the host: +In the commands below, `` is the name specified by `docker run --name`. +By default, it matches your Quill name. + ```bash -docker exec quill tail -n 100 /var/lib/quill/logs/ravendb.log -docker exec quill tail -n 100 /var/lib/quill/logs/certwatch.log +docker exec tail -n 100 /var/lib/quill/logs/ravendb.log +docker exec tail -n 100 /var/lib/quill/logs/certwatch.log ```
+
+ #### Why port 443 is unavailable before activation @@ -309,7 +372,7 @@ docker exec quill tail -n 100 /var/lib/quill/logs/certwatch.log * This is expected. Check the activation status from inside the container: ```bash - docker exec quill curl -s http://127.0.0.1:5000/api/bootstrap/status + docker exec curl -s http://127.0.0.1:5000/api/bootstrap/status ``` * Once activation reports `Ready`, connect through port `443` normally. @@ -337,7 +400,7 @@ nginx forwards the encrypted TLS connection unchanged. RavenDB then: * The generated RavenDB settings bind the native TCP listener to `tcp://127.0.0.1:38888`, so it is reachable only from inside the container. - RavenDB advertises this listener as `tcp://a.:38888`, but the default Docker Compose configuration publishes only port `443`, + RavenDB advertises this listener as `tcp://a.:38888`, but the default Quill deployment publishes only port `443`, and nginx does not route port `38888`. * Direct access through `db.` therefore uses RavenDB's HTTPS API on port `443`. @@ -375,16 +438,30 @@ Applications that require direct access must use their own client certificate wi -* The default Docker Compose configuration publishes one port, `443`, with nginx as the external entry point. +* The default Quill deployment publishes one port, `443`, with nginx as the external entry point. + +* The container must start as `root` so it can prepare the data volume and start its services. + RavenDB, the Quill web application, nginx, and the four file-logging services then run under the unprivileged `quill` account. + The web application and RavenDB listen only on the container's loopback interface. + * Four customer-facing hostnames expose the dashboard, API, public chat, and direct RavenDB surfaces. The additional `a.` hostname is RavenDB's advertised node alias. -* The public hostname exposes only embed-link chat routes, which require an active embed-link token. + +* The public hostname exposes embed-link chat and widget routes, plus the Slack event webhook. + The chat and Slack routes use URL-based tokens, and Slack also validates the request signature. + * Quill's operational APIs require the Dashboard API key or an authenticated browser session. + * Direct RavenDB access uses the HTTPS API on port `443`, requires a recognized client certificate, and preserves end-to-end mutual TLS through nginx. + * The default Quill deployment does not expose RavenDB's native TCP listener, so features such as data subscriptions are unavailable externally. + * One wildcard server certificate covers the four customer-facing hostnames and the `a.` alias. + +* Quill restricts access to the setup package and TLS private-key material inside the container and masks URL-based capability tokens in nginx logs. + * Quill's admin client certificate is used only inside the Quill container and is not exposed to callers. diff --git a/quill/security-and-architecture/operator-authentication.mdx b/quill/security-and-architecture/operator-authentication.mdx index b1210ddd2a..4943d30fee 100644 --- a/quill/security-and-architecture/operator-authentication.mdx +++ b/quill/security-and-architecture/operator-authentication.mdx @@ -32,19 +32,18 @@ import ContentFrame from '@site/src/components/ContentFrame'; Quill reads its Dashboard API key from the `QUILL_API_KEY` environment variable when the container starts. -In the default Docker Compose configuration, you supply it in the `.env` file next to `docker-compose.yml`. +You supply it through the `-e QUILL_API_KEY=...` option in the `docker run` command. -The key is required: +The key is required for protected operational access: -* The default Docker Compose configuration prevents the container from starting when `QUILL_API_KEY` is missing. -* If Quill is started another way without the key, it rejects all requests to protected operational API endpoints. +* If Quill starts without the key, it rejects all requests to protected operational API endpoints. It does not allow anonymous access or use a default key. * When the key is configured, it can be used to create a browser session or authenticate an API request directly. For validation, Quill stores a salted SHA-256 hash in its configuration database and compares hashes using a constant-time operation. Quill does not expose the original key through its dashboard or API. -The original value remains in your deployment configuration, such as the `.env` file and the container environment. +The original value remains in your deployment configuration or secret store and in the container environment. Protect access to that configuration as you would any other secret. @@ -64,7 +63,7 @@ Using the License key instead results in `401 Unauthorized`. If you replace the signup-issued Dashboard API key with a value of your own, use a high-entropy value. -Quill logs a startup warning when `QUILL_API_KEY` is shorter than 16 characters. +Quill logs a warning the first time it validates a Dashboard API key if `QUILL_API_KEY` is shorter than 16 characters. @@ -85,15 +84,14 @@ The browser sends this cookie with later requests, so you do not need to enter t Quill validates the session cookie independently and does not re-check it against the Dashboard API key. Signing out does not invalidate a copy of the cookie held elsewhere. -A session created before the Dashboard API key is changed therefore remains valid until it expires -or the container is recreated. See [Replacing an exposed key](#replacing-an-exposed-key). +A session created before the Dashboard API key is changed therefore remains valid until it expires after eight hours of inactivity. +Recreating the container with the same persistent volume does not invalidate it. See [Replacing an exposed key](#replacing-an-exposed-key). #### The sign-in rate limit is per client IP address -* The login endpoint accepts 10 attempts per minute from each client IP address and returns `429 Too Many Requests` - when that address's limit is exceeded. +* The login endpoint accepts 10 attempts per minute from each client IP address and returns `429 Too Many Requests` when that address's limit is exceeded. * nginx passes the original caller address to Quill, as described in [Network Architecture](network-architecture.mdx#how-a-connection-is-routed). Login attempts from different client IP addresses therefore use separate rate-limit buckets. @@ -145,12 +143,13 @@ The same key is issued for every Quill instance registered under the same email By default, you configure each of those Quill instances with the signup-issued key: -* Every Quill instance that is still configured with that key accepts it. -* If you replace `QUILL_API_KEY` on a Quill instance, that instance accepts the replacement value instead. - The change does not rotate the signup-issued key or affect other Quill instances. -* If the signup-issued key is exposed, treat every Quill instance that still accepts it as affected. - Replace the key and recreate the container on each affected instance. - See [Replacing an exposed key](#replacing-an-exposed-key). + * Every Quill instance that is still configured with that key accepts it. + + * If you replace `QUILL_API_KEY` on a Quill instance, that instance accepts the replacement value instead. + The change does not rotate the signup-issued key or affect other Quill instances. + + * If the signup-issued key is exposed, treat every Quill instance that still accepts it as affected. + Replace the key and recreate the container on each affected instance. See [Replacing an exposed key](#replacing-an-exposed-key).
@@ -159,8 +158,7 @@ By default, you configure each of those Quill instances with the signup-issued k Quill cannot derive the original Dashboard API key from the salted hash in its configuration database, and it does not expose the key through its dashboard or API. -First, check the deployment configuration from which `QUILL_API_KEY` was supplied, -such as the `.env` file or your secret store. +First, check the deployment configuration or secret store from which `QUILL_API_KEY` was supplied. If the Quill instance still uses the signup-issued key, you can also recover it in either of these ways: @@ -185,34 +183,23 @@ and recreate the container. -### Docker Compose - -Edit `QUILL_API_KEY` in the `.env` file next to `docker-compose.yml`, then run: - -```bash -docker compose up -d -``` -
- -Compose detects the changed environment and recreates the container with the new key. - -
+### Recreate the container - - -### docker run +In these commands, `` is the name specified after `--name` in your original `docker run` command. +By default, it matches your Quill name. +`` is the volume name specified before `:/var/lib/quill` in that command. Remove the existing container, then rerun it with the new key and the same data volume: ```bash -docker rm -f quill +docker rm -f -docker run -d --name quill \ +docker run -d --name \ --restart unless-stopped \ -p 443:443 \ -e QUILL_LICENSE_KEY="YOUR_EXISTING_LICENSE_KEY" \ -e QUILL_API_KEY="YOUR_NEW_DASHBOARD_API_KEY" \ - -v quill-data:/var/lib/quill \ + -v :/var/lib/quill \ ravendb/quill:latest ```
@@ -220,37 +207,54 @@ docker run -d --name quill \ If your original command used different ports, image tags, environment options, or volume names, preserve those values when recreating the container. -
- -#### Recreate the container; do not restart it +#### Restarting the container is not enough -Quill reads `QUILL_API_KEY` from the container environment when it starts. -Restarting the existing container reuses the same environment, so `docker restart quill` -keeps the exposed key in force. +* Quill reads `QUILL_API_KEY` from the container environment when it starts. + Restarting the existing container reuses the same environment, so `docker restart ` + keeps the exposed key in force. -A browser session is not re-checked against the Dashboard API key after login. -Restarting the existing container therefore also leaves previously issued session cookies valid -until they expire after eight hours of inactivity. +* A browser session is not re-checked against the Dashboard API key after login. + Changing the key therefore does not revoke previously issued session cookies. -With the default container layout, the cookie-protection keys are stored in the container’s writable layer, -outside the quill-data volume. Recreating the container discards those keys and invalidates previously issued sessions. +* With the default container layout, the web application stores its cookie-protection keys beneath + `/var/lib/quill/.aspnet/DataProtection-Keys`, inside the persistent volume. + Recreating the container with that volume preserves the keys, so existing sessions remain valid until they expire after eight hours of inactivity. + Continued activity renews the session. + +* Quill does not currently provide a built-in way to invalidate all active browser sessions. -Recreating the container preserves the Quill data stored in the `quill-data` volume, -including its databases, activated setup package, and configured apps. + + +#### Reuse the existing data volume + +* Recreating the container preserves the Quill data stored in the persistent volume mounted at + `/var/lib/quill`, including its databases, activated setup package, and configured apps. -Do not delete that volume or run `docker compose down -v`; those operations remove the persisted Quill data. +* Do not delete that volume; doing so removes the persisted Quill data. -Which Quill instances must be updated depends on which key was exposed: + + + -* If a locally chosen replacement key was exposed, replace it on that instance. -* If the signup-issued key was exposed, replace it on every Quill instance that still accepts it. - Changing `QUILL_API_KEY` locally does not rotate the account-level signup key or update other Quill instances. -* Store each replacement key in your deployment secret store. - The signup email continues to contain the original signup-issued key. + + +### Which Quill instances to update + +The instances you must update depend on which key was exposed: + + * If a locally chosen replacement key was exposed, replace it on that instance. + + * If the signup-issued key was exposed, replace it on every Quill instance that still accepts it. + Changing `QUILL_API_KEY` locally does not rotate the account-level signup key or update other Quill instances. + + * Store each replacement key in your deployment secret store. + The signup email continues to contain the original signup-issued key. + + @@ -287,7 +291,8 @@ Anyone holding the Dashboard API key has full operator access to the dashboard a Quill treats all key holders as the same operator and cannot distinguish which person performed an action. If someone who knows the key should no longer have access, replace it on every affected Quill instance -and recreate those containers. +and recreate those containers to load the new value. This prevents new authentication with the exposed key, +but does not invalidate browser sessions that were already issued.
@@ -296,8 +301,7 @@ and recreate those containers. * The Dashboard API key is required for Quill's operational API. - The default Docker Compose configuration prevents startup without it. - If Quill is started another way without the key, all requests to protected operational API endpoints are rejected. + If Quill starts without the key, all requests to protected operational API endpoints are rejected. * Quill stores a salted hash in its configuration database and does not expose the original key through its dashboard or API. Keep the original value in your deployment secret store. @@ -314,7 +318,8 @@ and recreate those containers. Recover a locally chosen key from your deployment configuration, or configure another key. * To replace an exposed key, update `QUILL_API_KEY` and recreate every affected container. - Restarting an existing container does not load a changed environment or invalidate existing browser sessions. + Restarting an existing container does not load a changed environment. + Neither restarting nor recreating the container with the same persistent volume invalidates existing browser sessions. * The Dashboard API key does not authenticate embed links, direct RavenDB connections, `/healthz`, or `/api/bootstrap/status`. Those surfaces use embed tokens, mTLS client certificates, or anonymous access.