SQLite files are created 0644 with plaintext credentials, and the documented Docker deployment supplies the parent directory 0755 #255

Closed
opened 2026-08-24 00:56:47 +02:00 by clawbot · 2 comments
Collaborator

Verified during the deployability audit. On a data directory the app created itself: directory 0750, lock file 0600, but webhooker.db is 0644.

That file holds credentials in plaintext — targets.config (bearer tokens, API keys, Slack webhook URLs) and entrypoints.signature_secret. Confirmed by grep -a on the file and by sqlite3: {"url":"...","headers":{"Authorization":"Bearer CANARY-BEARER-TOKEN-AAA","X-Api-Key":"..."}} and signature_secret = GITLAB-TOKEN-SUPER-SECRET-9x.

The 0750 directory is therefore the only barrier — and the standard Docker -v bind-mount deployment the README documents supplies that directory at 0755, removing it. Every local user on the host can then read every stored credential.

The per-webhook event databases were checked and are clean (zero credential canaries) since #206.

Why this rather than encryption at rest

#212 proposes encrypting the config column. That is the wrong control for this deployment and remains deferred. An unattended process needs a key it can read without a human, so the key lands in an env var or a sibling file on the same host; an attacker who can read webhooker.db can read that too. Encryption at rest would buy protection only against OFFLINE disclosure — a backup, a snapshot, a decommissioned disk — which is better addressed by encrypting the backup and the volume, and it carries a key-rotation and re-wrap story that 1.0 should not take on.

What is wrong today is narrow, concrete and cheap: the file mode.

Definition of done

  • webhooker.db, every per-webhook event database, and every archive database are created 0600. Existing files are tightened to 0600 on open, so an upgrade fixes a deployment already on disk rather than only new ones.
  • The data directory stays 0750 when the app creates it.
  • The README's Docker section states the DATA_DIR ownership and permission requirement. Note the audit separately found that a bind-mounted directory Docker creates as 0:0 makes the container fail to start at all (it runs as UID 1000), and the required chown -R 1000:1000 currently appears only under Restore — fix both in the same pass.
  • A test asserting the mode of each created database file.

Verification

  • make check green.
  • Evidence in the PR body: stat output for each database file on a freshly created data directory, and on a data directory created by an older build then opened by the new one.
Verified during the deployability audit. On a data directory the app created itself: directory `0750`, lock file `0600`, but **`webhooker.db` is `0644`**. That file holds credentials in plaintext — `targets.config` (bearer tokens, API keys, Slack webhook URLs) and `entrypoints.signature_secret`. Confirmed by `grep -a` on the file and by `sqlite3`: `{"url":"...","headers":{"Authorization":"Bearer CANARY-BEARER-TOKEN-AAA","X-Api-Key":"..."}}` and `signature_secret = GITLAB-TOKEN-SUPER-SECRET-9x`. The `0750` directory is therefore the only barrier — and the standard Docker `-v` bind-mount deployment the README documents supplies that directory at `0755`, removing it. Every local user on the host can then read every stored credential. The per-webhook event databases were checked and are clean (zero credential canaries) since https://git.eeqj.de/sneak/webhooker/issues/206. ## Why this rather than encryption at rest https://git.eeqj.de/sneak/webhooker/issues/212 proposes encrypting the config column. That is the wrong control for this deployment and remains deferred. An unattended process needs a key it can read without a human, so the key lands in an env var or a sibling file on the same host; an attacker who can read `webhooker.db` can read that too. Encryption at rest would buy protection only against OFFLINE disclosure — a backup, a snapshot, a decommissioned disk — which is better addressed by encrypting the backup and the volume, and it carries a key-rotation and re-wrap story that 1.0 should not take on. What is wrong today is narrow, concrete and cheap: the file mode. ## Definition of done - `webhooker.db`, every per-webhook event database, and every archive database are created `0600`. Existing files are tightened to `0600` on open, so an upgrade fixes a deployment already on disk rather than only new ones. - The data directory stays `0750` when the app creates it. - The README's Docker section states the `DATA_DIR` ownership and permission requirement. Note the audit separately found that a bind-mounted directory Docker creates as `0:0` makes the container fail to start at all (it runs as UID 1000), and the required `chown -R 1000:1000` currently appears only under Restore — fix both in the same pass. - A test asserting the mode of each created database file. ## Verification - `make check` green. - Evidence in the PR body: `stat` output for each database file on a freshly created data directory, and on a data directory created by an older build then opened by the new one.
clawbot added this to the 1.0.0 milestone 2026-08-24 00:56:50 +02:00
Author
Collaborator

Plan.

The fix goes in OpenSQLite (internal/database/sqlite_open.go), the single open path all three tiers share since #256 — main, per-webhook event, and archive are covered at once, with no change in internal/delivery.

0644 comes from SQLite itself: robust_open substitutes SQLITE_DEFAULT_FILE_PERMISSIONS (0644) whenever the caller passes mode 0, and findCreateFileMode returns 0 for a main database opened by URI without a modeof parameter. Chmod-after-open would race the window in which the file exists at 0644, so instead OpenSQLite creates the file itself at 0600 (os.OpenFile, O_CREATE) before handing the path to the driver, and chmods it to 0600 when it already exists.

That also settles the sidecars, which is the part that could silently not work. Reading modernc.org/sqlite v1.28.0: -wal takes its mode from findCreateFileMode, which stats the main database file with the -wal suffix stripped; -shm is opened in unixOpenSharedMemory with st_mode & 0777 from an fstat of the open main-database descriptor. Both therefore inherit 0600 from the main file rather than needing their own call — and any stale -wal/-shm left at 0644 by an earlier build gets chmodded alongside the main file. I will stat all of them rather than trust that reading.

Data directory stays 0750 (dataDirPerm), untouched.

Tests: a table over all three tiers asserting 0600 on the database and on each sidecar that exists, plus the directory still 0750. Plus end-to-end evidence in the PR body — receive, deliver, restart, reopen.

README: the Docker section gets the DATA_DIR ownership and permission requirement stated where the bind-mount is documented, consistent with the bind-address material from #268 and with the chown -R 1000:1000 under Restore.

Not doing: encryption at rest (#212 stays deferred), and no migration code — the chmod-on-open is the one-liner that covers a directory an older build left behind.

Plan. The fix goes in `OpenSQLite` (`internal/database/sqlite_open.go`), the single open path all three tiers share since https://git.eeqj.de/sneak/webhooker/issues/256 — main, per-webhook event, and archive are covered at once, with no change in `internal/delivery`. `0644` comes from SQLite itself: `robust_open` substitutes `SQLITE_DEFAULT_FILE_PERMISSIONS` (0644) whenever the caller passes mode 0, and `findCreateFileMode` returns 0 for a main database opened by URI without a `modeof` parameter. Chmod-after-open would race the window in which the file exists at `0644`, so instead `OpenSQLite` creates the file itself at `0600` (`os.OpenFile`, `O_CREATE`) before handing the path to the driver, and chmods it to `0600` when it already exists. That also settles the sidecars, which is the part that could silently not work. Reading modernc.org/sqlite v1.28.0: `-wal` takes its mode from `findCreateFileMode`, which stats the main database file with the `-wal` suffix stripped; `-shm` is opened in `unixOpenSharedMemory` with `st_mode & 0777` from an `fstat` of the open main-database descriptor. Both therefore inherit `0600` from the main file rather than needing their own call — and any stale `-wal`/`-shm` left at `0644` by an earlier build gets chmodded alongside the main file. I will `stat` all of them rather than trust that reading. Data directory stays `0750` (`dataDirPerm`), untouched. Tests: a table over all three tiers asserting `0600` on the database and on each sidecar that exists, plus the directory still `0750`. Plus end-to-end evidence in the PR body — receive, deliver, restart, reopen. README: the Docker section gets the `DATA_DIR` ownership and permission requirement stated where the bind-mount is documented, consistent with the bind-address material from https://git.eeqj.de/sneak/webhooker/issues/268 and with the `chown -R 1000:1000` under Restore. Not doing: encryption at rest (https://git.eeqj.de/sneak/webhooker/issues/212 stays deferred), and no migration code — the chmod-on-open is the one-liner that covers a directory an older build left behind.
Author
Collaborator

Built in #287 (base next, branch issue-255-sqlite-file-mode).

OpenSQLite now creates each database file itself at 0600 before the driver sees the path, and chmods one that already exists. That covers all three tiers from one place. The -wal and -shm sidecars inherit the mode from the main file — SQLite derives both from it — so they come out 0600 without a call of their own.

Definition of done, item by item:

  • webhooker.db, every events-*.db and every archive-*.db created 0600, sidecars included. stat for all nine files plus the lock is in the PR body.
  • Existing files tightened on open. Control run: a pre-change build left webhooker.db, -wal and -shm all 644; the new build opened that same directory and all three came back 600.
  • Data directory still 0750, asserted in a test.
  • README Docker section states the DATA_DIR ownership and permission requirement. The root:root bind-mount startup failure is reproduced in the actual container — it exits non-zero on webhooker.lock before opening any database — and the chown -R 1000:1000, previously only under Restore, is now stated where the bind mount is documented.
  • Tests in internal/database/sqlite_mode_test.go assert the mode of each database and both its sidecars across all three tiers. Negative control: with the fix stashed they fail on the main file and on both sidecars.

Verified beyond the tests: an event received and delivered, the service stopped and restarted against the same DATA_DIR, a second event delivered, and every file back at 0600 after the reopen. In a container on a 0755 bind mount, the files are 0600 — the parent directory is no longer the barrier.

make check green with GOFLAGS=-count=1; lint ran in Docker, 0 issues.

No encryption at rest; #212 is untouched.

Built in https://git.eeqj.de/sneak/webhooker/pulls/287 (base `next`, branch `issue-255-sqlite-file-mode`). `OpenSQLite` now creates each database file itself at `0600` before the driver sees the path, and chmods one that already exists. That covers all three tiers from one place. The `-wal` and `-shm` sidecars inherit the mode from the main file — SQLite derives both from it — so they come out `0600` without a call of their own. Definition of done, item by item: - `webhooker.db`, every `events-*.db` and every `archive-*.db` created `0600`, sidecars included. `stat` for all nine files plus the lock is in the PR body. - Existing files tightened on open. Control run: a pre-change build left `webhooker.db`, `-wal` and `-shm` all `644`; the new build opened that same directory and all three came back `600`. - Data directory still `0750`, asserted in a test. - README Docker section states the `DATA_DIR` ownership and permission requirement. The `root:root` bind-mount startup failure is reproduced in the actual container — it exits non-zero on `webhooker.lock` before opening any database — and the `chown -R 1000:1000`, previously only under Restore, is now stated where the bind mount is documented. - Tests in `internal/database/sqlite_mode_test.go` assert the mode of each database and both its sidecars across all three tiers. Negative control: with the fix stashed they fail on the main file and on both sidecars. Verified beyond the tests: an event received and delivered, the service stopped and restarted against the same `DATA_DIR`, a second event delivered, and every file back at `0600` after the reopen. In a container on a `0755` bind mount, the files are `0600` — the parent directory is no longer the barrier. `make check` green with `GOFLAGS=-count=1`; lint ran in Docker, `0 issues`. No encryption at rest; https://git.eeqj.de/sneak/webhooker/issues/212 is untouched.
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: sneak/webhooker#255