diff --git a/.env.example b/.env.example index ab501198c..01aa1e97f 100644 --- a/.env.example +++ b/.env.example @@ -38,6 +38,7 @@ SETUP_BOOTSTRAP_TOKEN=change-this-to-a-long-random-secret # The file must be mounted into every service that uses it, and must not be empty. # POSTGRES_PASSWORD_FILE=/run/secrets/bookorbit_postgres_password # JWT_SECRET_FILE=/run/secrets/bookorbit_jwt_secret +# PODCAST_ENCRYPTION_KEY_FILE=/run/secrets/bookorbit_podcast_encryption_key # SETUP_BOOTSTRAP_TOKEN_FILE=/run/secrets/bookorbit_setup_bootstrap_token # External/public URL used by emails and Kobo endpoints diff --git a/README.md b/README.md index 3d068c20a..1de429fd3 100644 --- a/README.md +++ b/README.md @@ -112,35 +112,10 @@ BOOKS_HOST_PATH=./books # folder on your server where your book fil POSTGRES_PASSWORD= # database password - openssl rand -hex 24 JWT_SECRET= # signs login tokens - openssl rand -hex 32 -PODCAST_ENCRYPTION_KEY= # encrypts podcast URLs - openssl rand -hex 32 SETUP_BOOTSTRAP_TOKEN= # one-time setup wizard token - openssl rand -hex 16 ``` -Sensitive values can instead be supplied through mounted files by setting the corresponding `_FILE` variable, such as `JWT_SECRET_FILE=/run/secrets/bookorbit_jwt_secret`. Leave the direct variable blank, mount the file into every service that uses it, and restart. Keep the blank assignment rather than deleting the line: the bundled `docker-compose.yml` marks `POSTGRES_PASSWORD`, `JWT_SECRET`, and `SETUP_BOOTSTRAP_TOKEN` as required, so removing them entirely fails before the container starts. BookOrbit rejects configurations where both forms are non-empty, and rejects a file that is missing, unreadable, or empty. This convention is supported for database credentials, application encryption keys, `GITHUB_RELEASES_TOKEN`, `JWT_SECRET`, and `SETUP_BOOTSTRAP_TOKEN`. - -For example, a Compose override can mount one database password file into both containers: - -```yaml -services: - app: - secrets: [bookorbit_postgres_password] - postgres: - environment: - POSTGRES_PASSWORD_FILE: ${POSTGRES_PASSWORD_FILE?required} - secrets: [bookorbit_postgres_password] - -secrets: - bookorbit_postgres_password: - file: ./secrets/postgres_password -``` - -Set `POSTGRES_PASSWORD=` and `POSTGRES_PASSWORD_FILE=/run/secrets/bookorbit_postgres_password` in `.env`. The override passes the file path to PostgreSQL while the app receives it from the existing `.env` import. Other container platforms may mount secret files directly without a Compose override. - -On a NAS, or any host where your book folder is owned by a user other than UID 1000, also set `PUID` and `PGID` to match that owner. Run `id -u` and `id -g` as the owning user to find them. Getting these wrong is the most common cause of permission errors on first scan. - -When upgrading an installation that previously used podcasts without `PODCAST_ENCRYPTION_KEY`, set it to the current `JWT_SECRET` first. A new key would make existing encrypted podcast URLs unreadable. Fresh installations should use an independent generated key. - -Optionally set `LIBRARY_BROWSE_ROOT=/books` to start the library folder picker at `/books` instead of `/`. +On a NAS, or any host where your book folder is owned by a user other than UID 1000, also set `PUID` and `PGID` to match that owner. Getting these wrong is the most common cause of permission errors on first scan. Then start: @@ -150,11 +125,7 @@ docker compose up -d Open `http://your-server-ip:3000` and complete setup using your `SETUP_BOOTSTRAP_TOKEN`. -After configuring OIDC and linking at least one active administrator, you can set `DISABLE_LOCAL_AUTH=true` and restart BookOrbit to remove and reject password sign-in. BookOrbit refuses to start if that would leave no usable OIDC administrator. Set it back to `false` and restart to recover access during an identity-provider outage. - -An OIDC provider signed by a private certificate authority requires its PEM CA bundle to be mounted read-only in the BookOrbit container. Set `NODE_EXTRA_CA_CERTS` to that in-container path and restart BookOrbit. This adds the private authority to Node's normal certificate verification; it does not disable TLS security. Private-network issuers also require `OIDC_ALLOW_LOCAL_ISSUERS=true`. - -For the full installation guide including reverse proxy setup, file permissions on NAS, external databases, and environment variable reference, see **[bookorbit.app/installation](https://bookorbit.app/installation)**. +For the full installation guide including reverse proxy setup, file permissions on NAS, secrets from mounted files, external databases, OIDC hardening, and environment variable reference, see **[bookorbit.app/installation](https://bookorbit.app/installation)**. ## KOReader Plugin diff --git a/server/file-env.sh b/server/file-env.sh index ae04dde88..1246dae88 100644 --- a/server/file-env.sh +++ b/server/file-env.sh @@ -66,6 +66,7 @@ load_file_env() { GITHUB_RELEASES_TOKEN \ JWT_SECRET \ MIGRATION_ENCRYPTION_KEY \ + PODCAST_ENCRYPTION_KEY \ POSTGRES_PASSWORD \ SETUP_BOOTSTRAP_TOKEN do diff --git a/server/src/config/docker-env-file.test.ts b/server/src/config/docker-env-file.test.ts index 8e9720a0f..c4c2687c4 100644 --- a/server/src/config/docker-env-file.test.ts +++ b/server/src/config/docker-env-file.test.ts @@ -38,16 +38,26 @@ describe('Docker file-backed environment values', () => { expect(result.stdout).toBe(' file-secret-value |unset'); }); - it('loads application encryption keys', () => { - const secretPath = join(secretDir, 'book-request-key'); - writeFileSync(secretPath, 'book-request-encryption-key'); + it.each([ + 'BOOK_REQUEST_ENCRYPTION_KEY', + 'DATABASE_URL', + 'EMAIL_ENCRYPTION_KEY', + 'GITHUB_RELEASES_TOKEN', + 'JWT_SECRET', + 'MIGRATION_ENCRYPTION_KEY', + 'PODCAST_ENCRYPTION_KEY', + 'POSTGRES_PASSWORD', + 'SETUP_BOOTSTRAP_TOKEN', + ])('loads %s from a mounted file', (name) => { + const secretPath = join(secretDir, 'secret-value'); + writeFileSync(secretPath, `${name}-from-file`); - const result = runHelper('load_file_env; printf "%s" "$BOOK_REQUEST_ENCRYPTION_KEY"', { - BOOK_REQUEST_ENCRYPTION_KEY_FILE: secretPath, + const result = runHelper(`load_file_env; printf "%s" "$${name}"`, { + [`${name}_FILE`]: secretPath, }); expect(result.status).toBe(0); - expect(result.stdout).toBe('book-request-encryption-key'); + expect(result.stdout).toBe(`${name}-from-file`); }); it('rejects non-empty direct and file values together', () => {