Create every SQLite file 0600 (closes #255)
All checks were successful
check / check (push) Successful in 3m24s

webhooker.db holds target configuration in plaintext — bearer tokens,
API keys, Slack webhook URLs — and the session encryption key, and it
was created 0644. The 0750 data directory was therefore the only
barrier, and a Docker bind mount supplies that directory at 0755,
which removes it: every local user on the host could read every stored
credential.

The mode is settled in OpenSQLite, the single open path all three
tiers share, so the main database, the per-webhook event databases and
the archive databases are covered in one place.

0644 comes from SQLite itself: robust_open substitutes
SQLITE_DEFAULT_FILE_PERMISSIONS whenever it is handed mode 0, and
findCreateFileMode yields 0 for a main database opened by URI with no
`modeof` parameter. A chmod after opening would leave a window in
which the credentials are on disk world-readable, so OpenSQLite
creates the file itself at 0600 before the driver sees the path.

That also settles the WAL sidecars, which carry the same rows and
would otherwise leave the fix worthless. SQLite derives both from the
main database file — `-wal` through findCreateFileMode, which stats
the path with the suffix stripped, and `-shm` in unixOpenSharedMemory
from an fstat of the open database descriptor — so a main file at 0600
produces sidecars at 0600. Verified by stat rather than by reading the
driver: a pre-change build leaves webhooker.db, -wal and -shm all 644;
this one leaves all three 600, in a 0755 bind mount, for all three
tiers, with delivery and restart working.

Existing files are chmodded on open, so a directory an earlier build
left 0644 — including a developer's scratch directory — is fixed
without any migration machinery.

This is not encryption at rest. An unattended process needs a key it
can read without a human, so the key lands beside the data and an
attacker who can read the database can read it too.

The data directory stays 0750: the group bit may matter to a
deployment, and with the files at 0600 the directory is no longer the
barrier.

README: the Docker section now states the DATA_DIR ownership
requirement where the bind mount is documented. A `-v` source path
Docker creates is root:root, and the container runs as UID 1000, so it
fails to start on the lock file; the chown that fixes it appeared only
under Restore.
This commit is contained in:
2026-08-24 02:10:12 +00:00
parent 48cf93ec7e
commit 1230ae990e
3 changed files with 370 additions and 0 deletions

View File

@@ -666,6 +666,44 @@ databases written by `database` targets (`archive-{uuid}.db`). Mount
this as a persistent volume to preserve data across container
restarts.
**The bind-mounted directory must be owned by UID 1000, or the
container does not start.** Docker creates a `-v` source path that
does not exist yet as `root:root`, and the process runs as UID 1000,
so it cannot take its `DATA_DIR` lock:
```
webhooker: locking data directory /var/lib/webhooker: open
/var/lib/webhooker/webhooker.lock: permission denied
```
It exits non-zero at that point, before opening any database. Create
the directory ahead of the first `docker run`:
```bash
mkdir -p /path/to/data
chown 1000:1000 /path/to/data
chmod 750 /path/to/data
```
The same `chown` is what a restore needs — see step 4 of
[Restore](#restore). A **named volume** does not have this problem:
Docker copies the image's ownership onto a volume it initializes, and
the image creates `/var/lib/webhooker` owned by `webhooker`.
**The file modes are not yours to set, and do not depend on the
directory.** `webhooker.db` holds target configuration in plaintext —
bearer tokens, API keys, Slack webhook URLs — along with the session
encryption key, so webhooker creates every SQLite file it owns `0600`:
each database and both of its `-wal` and `-shm` sidecars, across all
three tiers. Files an earlier build left `0644` are tightened when
they are opened. A `DATA_DIR` webhooker creates itself is `0750`, but
a bind mount supplies its own directory and Docker's default for one
it creates is `0755`; the `0600` files hold there regardless. The
`chmod 750` above is defence in depth — it stops other local users
listing the directory and learning your webhook UUIDs from the
`events-{uuid}.db` filenames — not the barrier protecting the
credentials.
## Deployment behind a reverse proxy
webhooker terminates no TLS of its own. It serves plaintext HTTP and
@@ -1622,6 +1660,11 @@ webhooker uses **separate SQLite database files**: a main application
database for configuration data and per-webhook databases for event
storage. All database files live in the `DATA_DIR` directory.
Every one of them is created `0600`, and so is each `-wal` and `-shm`
sidecar. See
[Running with Docker](#running-with-docker) for what that does and
does not protect.
**Main Application Database** (`{DATA_DIR}/webhooker.db`) — stores
configuration and application state: