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"