diff --git a/.env.example b/.env.example index acd0192..c5ae619 100644 --- a/.env.example +++ b/.env.example @@ -22,11 +22,13 @@ ENCRYPTION_KEY= # AES-256-GCM for OAuth token encryption (dashboard) # ─── Database Passwords ────────────────────────────────────────────────────── # Used by Docker Compose for the DB containers. # -# !! The `yavio_dev` values below are LOCAL DEVELOPMENT PLACEHOLDERS and are -# !! published in a public repository. Never run a deployment that can be -# !! reached by anything you do not control on these values — yavio_service is -# !! a Postgres superuser that bypasses RLS, and the ClickHouse `default` user -# !! is unrestricted. +# !! EVERY password in this section ships BLANK, and compose refuses to start +# !! rather than substitute one. They used to default to `yavio_dev`, which is +# !! published in this public repository — including for the two most +# !! privileged accounts in the system: yavio_service is the Postgres +# !! superuser, and the ClickHouse `default` user is unrestricted and can +# !! manage access. A missing password is a startup error you fix in a minute; +# !! a published one is a credential anyone can read on GitHub. # # ./scripts/setup-env.sh generates strong random values for every password in # this section, and rewrites DATABASE_URL / CLICKHOUSE_URL to match. If you are @@ -34,13 +36,17 @@ ENCRYPTION_KEY= # AES-256-GCM for OAuth token encryption (dashboard) # that CLICKHOUSE_INGEST_PASSWORD and CLICKHOUSE_DASHBOARD_PASSWORD are applied # to their ClickHouse users by `pnpm migrate:clickhouse`, not by compose. -POSTGRES_SERVICE_PASSWORD=yavio_dev # yavio_service role: table owner, bypasses RLS +POSTGRES_SERVICE_PASSWORD= # yavio_service role: the Postgres SUPERUSER — owns the tables, + # bypasses RLS, and can run COPY ... FROM PROGRAM. No default. POSTGRES_API_PASSWORD= # yavio_api role: the app connects as this (NOT a superuser). # Deliberately has NO default: compose refuses to start rather # than fall back to a published value. setup-env.sh generates it. POSTGRES_APP_PASSWORD= # yavio_app role: RLS enforced. Applied by `pnpm migrate` — blank so an - # upgrade cannot reset the role to a published value. -CLICKHOUSE_PASSWORD=yavio_dev # default user: runs migrations + # upgrade cannot reset the role to a published value. Set this: left + # unset, the role keeps the published 'yavio_dev' from migration 0001 + # and holds DML on every table. +CLICKHOUSE_PASSWORD= # default user: unrestricted, runs migrations and manages access. + # No default — see the warning above. CLICKHOUSE_INGEST_PASSWORD= # yavio_ingest user: INSERT only. Applied by `pnpm migrate:clickhouse` # — blank so an upgrade cannot reset the user to a published value. CLICKHOUSE_DASHBOARD_PASSWORD= # yavio_dashboard user: SELECT only, row policies. Applied by @@ -55,8 +61,8 @@ CLICKHOUSE_ERASER_PASSWORD= # yavio_eraser user: ALTER DELETE on de # ─── Database Connection URLs ──────────────────────────────────────────────── # localhost for local dev; Docker Compose overrides these with container hostnames. -DATABASE_URL=postgres://yavio_service:yavio_dev@localhost:5432/yavio -CLICKHOUSE_URL=http://default:yavio_dev@localhost:8123 +DATABASE_URL=postgres://yavio_service:REPLACE_WITH_POSTGRES_SERVICE_PASSWORD@localhost:5432/yavio +CLICKHOUSE_URL=http://default:REPLACE_WITH_CLICKHOUSE_PASSWORD@localhost:8123 # ─── Dashboard ─────────────────────────────────────────────────────────────── diff --git a/docker-compose.test.yml b/docker-compose.test.yml index 132e0a5..444435b 100644 --- a/docker-compose.test.yml +++ b/docker-compose.test.yml @@ -18,7 +18,10 @@ services: POSTGRES_USER: yavio_service POSTGRES_PASSWORD: test ports: - - "${TEST_POSTGRES_PORT:-5433}:5432" + # Loopback only. The dev compose file has bound its datastores this way + # since #42; this one was publishing a password-`test` Postgres to the + # whole LAN from every developer's machine. + - "127.0.0.1:${TEST_POSTGRES_PORT:-5433}:5432" tmpfs: - /var/lib/postgresql/data healthcheck: @@ -33,7 +36,7 @@ services: CLICKHOUSE_PASSWORD: test CLICKHOUSE_DEFAULT_ACCESS_MANAGEMENT: 1 ports: - - "${TEST_CLICKHOUSE_PORT:-8124}:8123" + - "127.0.0.1:${TEST_CLICKHOUSE_PORT:-8124}:8123" tmpfs: - /var/lib/clickhouse healthcheck: diff --git a/docker-compose.yml b/docker-compose.yml index 24bbf04..6317511 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -14,7 +14,7 @@ services: environment: POSTGRES_DB: yavio POSTGRES_USER: yavio_service - POSTGRES_PASSWORD: ${POSTGRES_SERVICE_PASSWORD:-yavio_dev} + POSTGRES_PASSWORD: ${POSTGRES_SERVICE_PASSWORD:?POSTGRES_SERVICE_PASSWORD is not set. Run ./scripts/setup-env.sh, or add it to .env — it must never fall back to a published default. This role is the Postgres superuser.} volumes: - postgres_data:/var/lib/postgresql/data ports: @@ -31,7 +31,7 @@ services: image: clickhouse/clickhouse-server:24.3 restart: unless-stopped environment: - CLICKHOUSE_PASSWORD: ${CLICKHOUSE_PASSWORD:-yavio_dev} + CLICKHOUSE_PASSWORD: ${CLICKHOUSE_PASSWORD:?CLICKHOUSE_PASSWORD is not set. Run ./scripts/setup-env.sh, or add it to .env — it must never fall back to a published default. This user is unrestricted and can manage access.} CLICKHOUSE_DEFAULT_ACCESS_MANAGEMENT: 1 volumes: - clickhouse_data:/var/lib/clickhouse @@ -62,13 +62,19 @@ services: context: . dockerfile: packages/db/Dockerfile environment: - DATABASE_URL: postgres://yavio_service:${POSTGRES_SERVICE_PASSWORD:-yavio_dev}@postgres:5432/yavio - CLICKHOUSE_URL: http://default:${CLICKHOUSE_PASSWORD:-yavio_dev}@clickhouse:8123 + DATABASE_URL: postgres://yavio_service:${POSTGRES_SERVICE_PASSWORD:?POSTGRES_SERVICE_PASSWORD is not set. Run ./scripts/setup-env.sh, or add it to .env — it must never fall back to a published default. This role is the Postgres superuser.}@postgres:5432/yavio + CLICKHOUSE_URL: http://default:${CLICKHOUSE_PASSWORD:?CLICKHOUSE_PASSWORD is not set. Run ./scripts/setup-env.sh, or add it to .env — it must never fall back to a published default. This user is unrestricted and can manage access.}@clickhouse:8123 # Applied to their ClickHouse users by migrate-clickhouse.ts after # migrations run — migration 0007 creates them with a published literal. CLICKHOUSE_INGEST_PASSWORD: ${CLICKHOUSE_INGEST_PASSWORD:-} CLICKHOUSE_DASHBOARD_PASSWORD: ${CLICKHOUSE_DASHBOARD_PASSWORD:-} CLICKHOUSE_ERASER_PASSWORD: ${CLICKHOUSE_ERASER_PASSWORD:-} + # Applied to the yavio_app role by migrate.ts after migrations run. + # Compose-injected environment is NOT inherited from .env, so omitting this + # left yavio_app on the published 'yavio_dev' from 0001 even for operators + # who ran setup-env.sh and followed every documented step — the host-side + # `pnpm migrate` reads .env directly and repaired it, this path did not. + POSTGRES_APP_PASSWORD: ${POSTGRES_APP_PASSWORD:-} # Applied to the yavio_api role by migrate.ts after migrations run. POSTGRES_API_PASSWORD: ${POSTGRES_API_PASSWORD:?POSTGRES_API_PASSWORD is not set. Run ./scripts/setup-env.sh, or add it to .env — it must never fall back to a published default.} networks: @@ -86,7 +92,7 @@ services: restart: unless-stopped environment: DATABASE_URL: postgres://yavio_api:${POSTGRES_API_PASSWORD:?POSTGRES_API_PASSWORD is not set. Run ./scripts/setup-env.sh, or add it to .env — it must never fall back to a published default.}@postgres:5432/yavio - CLICKHOUSE_URL: http://default:${CLICKHOUSE_PASSWORD:-yavio_dev}@clickhouse:8123 + CLICKHOUSE_URL: http://default:${CLICKHOUSE_PASSWORD:?CLICKHOUSE_PASSWORD is not set. Run ./scripts/setup-env.sh, or add it to .env — it must never fall back to a published default. This user is unrestricted and can manage access.}@clickhouse:8123 # This service narrows the ClickHouse username; when set it uses that # user's own password instead of the default user's. CLICKHOUSE_INGEST_PASSWORD: ${CLICKHOUSE_INGEST_PASSWORD:-} @@ -114,7 +120,7 @@ services: restart: unless-stopped environment: DATABASE_URL: postgres://yavio_api:${POSTGRES_API_PASSWORD:?POSTGRES_API_PASSWORD is not set. Run ./scripts/setup-env.sh, or add it to .env — it must never fall back to a published default.}@postgres:5432/yavio - CLICKHOUSE_URL: http://default:${CLICKHOUSE_PASSWORD:-yavio_dev}@clickhouse:8123 + CLICKHOUSE_URL: http://default:${CLICKHOUSE_PASSWORD:?CLICKHOUSE_PASSWORD is not set. Run ./scripts/setup-env.sh, or add it to .env — it must never fall back to a published default. This user is unrestricted and can manage access.}@clickhouse:8123 # This service narrows the ClickHouse username; when set it uses that # user's own password instead of the default user's. CLICKHOUSE_DASHBOARD_PASSWORD: ${CLICKHOUSE_DASHBOARD_PASSWORD:-} diff --git a/packages/docs/content/docs/04-self-hosting/02-configuration.mdx b/packages/docs/content/docs/04-self-hosting/02-configuration.mdx index ea88e1a..34813a3 100644 --- a/packages/docs/content/docs/04-self-hosting/02-configuration.mdx +++ b/packages/docs/content/docs/04-self-hosting/02-configuration.mdx @@ -38,24 +38,32 @@ These must be set before starting the platform. Each should be a unique random s ## Database Passwords -| Variable | Default | Description | +**None of these have a default.** They used to fall back to `yavio_dev`, which is published in the public repository. `./scripts/setup-env.sh` generates all seven; if you fill them in by hand, use `openssl rand -hex 24`. + +| Variable | Applied by | Role | | --- | --- | --- | -| `POSTGRES_SERVICE_PASSWORD` | `yavio_dev` | PostgreSQL `yavio_service` role (table owner, bypasses RLS) | -| `POSTGRES_APP_PASSWORD` | `yavio_dev` | PostgreSQL `yavio_app` role (RLS enforced) | -| `CLICKHOUSE_PASSWORD` | `yavio_dev` | ClickHouse `default` user (runs migrations) | -| `CLICKHOUSE_INGEST_PASSWORD` | `yavio_dev` | ClickHouse `yavio_ingest` user (INSERT only) | -| `CLICKHOUSE_DASHBOARD_PASSWORD` | `yavio_dev` | ClickHouse `yavio_dashboard` user (SELECT only, row policies) | +| `POSTGRES_SERVICE_PASSWORD` | Compose | PostgreSQL `yavio_service` — the **superuser**: owns the tables, bypasses RLS | +| `POSTGRES_API_PASSWORD` | `pnpm migrate` | PostgreSQL `yavio_api` — what the application actually connects as (not a superuser) | +| `POSTGRES_APP_PASSWORD` | `pnpm migrate` | PostgreSQL `yavio_app` — RLS enforced | +| `CLICKHOUSE_PASSWORD` | Compose | ClickHouse `default` — unrestricted; runs migrations and manages access | +| `CLICKHOUSE_INGEST_PASSWORD` | `pnpm migrate:clickhouse` | ClickHouse `yavio_ingest` — INSERT only | +| `CLICKHOUSE_DASHBOARD_PASSWORD` | `pnpm migrate:clickhouse` | ClickHouse `yavio_dashboard` — SELECT only, row policies | +| `CLICKHOUSE_ERASER_PASSWORD` | `pnpm migrate:clickhouse` | ClickHouse `yavio_eraser` — `ALTER DELETE` on the events table only; cannot read what it erases | - Change all default passwords before exposing the platform to a network. The defaults are only safe for local development. + Compose refuses to start when `POSTGRES_SERVICE_PASSWORD`, `POSTGRES_API_PASSWORD` or `CLICKHOUSE_PASSWORD` is missing, rather than substituting a value. That is deliberate: a startup error takes a minute to fix, a published default is a credential anyone can read on GitHub. + + The four applied by a migration are skipped when unset, so a deployment that shares one password across users keeps working — but each one left unset means that account keeps whatever it was created with. ## Database Connection URLs | Variable | Default | Description | | --- | --- | --- | -| `DATABASE_URL` | `postgres://yavio_service:yavio_dev@localhost:5432/yavio` | PostgreSQL connection string | -| `CLICKHOUSE_URL` | `http://default:yavio_dev@localhost:8123` | ClickHouse HTTP endpoint | +| `DATABASE_URL` | `postgres://yavio_service:@localhost:5432/yavio` | PostgreSQL connection string | +| `CLICKHOUSE_URL` | `http://default:@localhost:8123` | ClickHouse HTTP endpoint | + +These embed a password, so `setup-env.sh` rewrites them whenever it generates one. Editing a password by hand means editing these too. In Docker Compose, these are overridden with container hostnames (`postgres`, `clickhouse`). diff --git a/scripts/setup-env.sh b/scripts/setup-env.sh index c755b4c..c0433d0 100755 --- a/scripts/setup-env.sh +++ b/scripts/setup-env.sh @@ -62,13 +62,32 @@ CLICKHOUSE_ERASER_PASSWORD=$(generate_db_password) # Replace values in .env. The trailing-comment form in .env.example # (`KEY=value # note`) is intentionally dropped for the secrets: a comment # after a value is fragile to parse and has already caused one outage. +# +# The value travels through the ENVIRONMENT, never through argv. This used to be +# `sed -i "s|^${key}=.*|${key}=${value}|"`, which puts every secret on a command +# line — and a command line is world-readable through `ps` and +# /proc//cmdline, while /proc//environ is readable only by the owner. +# Passing them on argv handed any local user the entire datastore credential set +# and made the chmod 600 above pointless (CWE-214). +# +# Using awk instead of sed also removes the BSD/GNU `sed -i` split and the +# escaping question entirely: awk prints the value literally, so a `|`, `&` or +# backslash from some future generator cannot break the substitution or inject a +# second line. Today's values are base64/hex and cannot contain those — this is +# about not depending on that. set_var() { - local key="$1" value="$2" - if [[ "$OSTYPE" == "darwin"* ]]; then - sed -i '' "s|^${key}=.*|${key}=${value}|" "$ENV_FILE" - else - sed -i "s|^${key}=.*|${key}=${value}|" "$ENV_FILE" - fi + local key="$1" + # umask, because `mv` keeps the temp file's mode: created under the default + # umask it would be world-readable and would silently widen .env. + ( + umask 077 + SET_VAR_KEY="$key" SET_VAR_VALUE="$2" awk ' + BEGIN { key = ENVIRON["SET_VAR_KEY"]; value = ENVIRON["SET_VAR_VALUE"] } + index($0, key "=") == 1 { print key "=" value; next } + { print } + ' "$ENV_FILE" > "$ENV_FILE.tmp" + ) + mv "$ENV_FILE.tmp" "$ENV_FILE" } set_var NEXTAUTH_SECRET "$NEXTAUTH_SECRET"