|
|
|
@@ -162,6 +162,14 @@ public cloud metadata addresses: currently only `168.63.129.16`, Azure's
|
|
|
|
|
WireServer, which serves an Azure VM its credentials. Because it is a
|
|
|
|
|
public address, listing it in `ALLOWED_EGRESS_CIDRS` reopens it.
|
|
|
|
|
|
|
|
|
|
That is all the default blocklist covers: private and reserved space,
|
|
|
|
|
plus public addresses that serve cloud credentials. A cloud provider's
|
|
|
|
|
other services on public addresses are not refused — IBM Cloud's
|
|
|
|
|
`161.26.0.0/16` and `166.8.0.0/14`, for example, which carry its DNS
|
|
|
|
|
resolvers, time servers and package mirrors. They serve no credentials,
|
|
|
|
|
reaching them can be a legitimate delivery, and every cloud has some, so
|
|
|
|
|
a partial list would promise coverage it does not give.
|
|
|
|
|
|
|
|
|
|
That default is also inconvenient for the thing webhooker is mostly
|
|
|
|
|
for: taking a public webhook and forwarding it to something on your own
|
|
|
|
|
network. A container on the same Docker network, a box on `10.x`, a
|
|
|
|
@@ -552,9 +560,8 @@ printf '%s' "$NEW_PASSWORD" | \
|
|
|
|
|
DATA_DIR=/var/lib/webhooker webhooker resetpw admin
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
In a container it is the same binary. The image's `CMD` is
|
|
|
|
|
`/app/webhooker`, and a command given to `docker run` replaces all of
|
|
|
|
|
it, so the whole command has to be given:
|
|
|
|
|
In a container it is the same binary, which the image sets as `CMD`
|
|
|
|
|
rather than `ENTRYPOINT`, so the whole command has to be given:
|
|
|
|
|
|
|
|
|
|
```bash
|
|
|
|
|
docker run --rm -v webhooker-data:/var/lib/webhooker \
|
|
|
|
@@ -691,22 +698,38 @@ those three values rather than trusting the figure. Measured at 65s on
|
|
|
|
|
Docker 29.7.2.) A container `unhealthy` with `connection refused` in
|
|
|
|
|
its health log, or a published port that resets connections, is this.
|
|
|
|
|
|
|
|
|
|
The app runs as a non-root user (`webhooker`, UID 1000), exposes port
|
|
|
|
|
8080, and includes a health check against `/.well-known/healthcheck`.
|
|
|
|
|
The `/var/lib/webhooker` volume holds all SQLite databases: the main
|
|
|
|
|
application database (`webhooker.db`), the per-webhook event databases
|
|
|
|
|
(`events-{uuid}.db`), and any archive databases written by `database`
|
|
|
|
|
targets (`archive-{uuid}.db`). Mount this as a persistent volume to
|
|
|
|
|
preserve data across container restarts.
|
|
|
|
|
The container runs as a non-root user (`webhooker`, UID 1000), exposes
|
|
|
|
|
port 8080, and includes a health check against
|
|
|
|
|
`/.well-known/healthcheck`. The `/var/lib/webhooker` volume holds all
|
|
|
|
|
SQLite databases: the main application database (`webhooker.db`), the
|
|
|
|
|
per-webhook event databases (`events-{uuid}.db`), and any archive
|
|
|
|
|
databases written by `database` targets (`archive-{uuid}.db`). Mount
|
|
|
|
|
this as a persistent volume to preserve data across container
|
|
|
|
|
restarts.
|
|
|
|
|
|
|
|
|
|
**The container sets its data directory's owner and mode itself
|
|
|
|
|
before the app starts**, so a host directory can be mounted as it is,
|
|
|
|
|
whoever owns it. The image's `ENTRYPOINT`,
|
|
|
|
|
`deploy/docker-entrypoint.sh`, starts as root, creates `DATA_DIR` if
|
|
|
|
|
it is missing, gives the directory and anything in it that belongs to
|
|
|
|
|
another user to `webhooker`, sets the directory to `0750`, and only
|
|
|
|
|
then runs the app as `webhooker`. Started with `--user`, it changes
|
|
|
|
|
nothing and runs the app as that user.
|
|
|
|
|
**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 —
|
|
|
|
@@ -714,10 +737,13 @@ 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. The directory's `0750` 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.
|
|
|
|
|
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.
|
|
|
|
|
|
|
|
|
|
### Running under upaas
|
|
|
|
|
|
|
|
|
@@ -733,6 +759,17 @@ repository's `Dockerfile` and runs it. The app needs:
|
|
|
|
|
app name, port `8080`. Leave `PORT` unset: the image's health check
|
|
|
|
|
probes `8080`.
|
|
|
|
|
- **Volume:** one host directory mounted at `/var/lib/webhooker`.
|
|
|
|
|
upaas bind-mounts the host path it is given and does not create it,
|
|
|
|
|
and the container does not start unless UID 1000 owns it (see
|
|
|
|
|
[Running with Docker](#running-with-docker)). Create it before the
|
|
|
|
|
first deploy:
|
|
|
|
|
|
|
|
|
|
```bash
|
|
|
|
|
mkdir -p /path/to/data
|
|
|
|
|
chown 1000:1000 /path/to/data
|
|
|
|
|
chmod 750 /path/to/data
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
- **Environment variables:**
|
|
|
|
|
- `WEBHOOKER_ENVIRONMENT=prod`
|
|
|
|
|
- `TRUSTED_PROXIES`: your reverse proxy's address on that Docker
|
|
|
|
@@ -991,12 +1028,12 @@ done
|
|
|
|
|
`.backup` reads through the WAL and writes a single consistent file with
|
|
|
|
|
no sidecars of its own, so the destination is complete as it stands.
|
|
|
|
|
Two caveats. First, the runtime image is `alpine:3.21` with only
|
|
|
|
|
`ca-certificates` and `su-exec` added — the `sqlite3` CLI is **not** in
|
|
|
|
|
it, so run this on the host against the volume path, or from a
|
|
|
|
|
throwaway container that mounts the volume. Second, each file is
|
|
|
|
|
captured at its own instant, so a webhook created or an event delivered
|
|
|
|
|
between two files being copied lands in one and not the other. If you
|
|
|
|
|
need the whole set coherent as of a single moment, stop the service.
|
|
|
|
|
`ca-certificates` added — the `sqlite3` CLI is **not** in it, so run
|
|
|
|
|
this on the host against the volume path, or from a throwaway container
|
|
|
|
|
that mounts the volume. Second, each file is captured at its own
|
|
|
|
|
instant, so a webhook created or an event delivered between two files
|
|
|
|
|
being copied lands in one and not the other. If you need the whole set
|
|
|
|
|
coherent as of a single moment, stop the service.
|
|
|
|
|
|
|
|
|
|
Note that `sqlite3 <db> .dump` is **not** one of these procedures: it is
|
|
|
|
|
an export, it holds a read transaction open for as long as it runs, and
|
|
|
|
@@ -1050,11 +1087,21 @@ with any `-wal`/`-shm` beside it, or wait until there are none.
|
|
|
|
|
archive not opened since a crash. A copy salvaged from a crashed
|
|
|
|
|
instance has them for everything, and needs all of them.
|
|
|
|
|
|
|
|
|
|
4. Start the service. The container gives the directory and the
|
|
|
|
|
restored files to the `webhooker` user before the app starts,
|
|
|
|
|
whoever restored them (see
|
|
|
|
|
[Running with Docker](#running-with-docker)). `AutoMigrate` runs
|
|
|
|
|
against each restored database as it is opened.
|
|
|
|
|
4. **Fix ownership.** The container runs as the non-root `webhooker`
|
|
|
|
|
user, UID 1000 / GID 1000. Restored files must be owned by (or
|
|
|
|
|
writable by) that UID, and so must the directory itself — SQLite
|
|
|
|
|
creates the `-wal` and `-shm` sidecars beside the database, so a
|
|
|
|
|
writable file inside a directory it cannot write is not enough:
|
|
|
|
|
|
|
|
|
|
```bash
|
|
|
|
|
chown -R 1000:1000 /path/to/data
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
Restoring as `root` on the host and forgetting this step is the
|
|
|
|
|
usual way a restore fails.
|
|
|
|
|
|
|
|
|
|
5. Start the service. `AutoMigrate` runs against each restored database
|
|
|
|
|
as it is opened.
|
|
|
|
|
|
|
|
|
|
### Upgrades
|
|
|
|
|
|
|
|
|
@@ -2682,7 +2729,7 @@ abuse limit later; they are tracked as future work.
|
|
|
|
|
| ------ | --------------------------- | ----------- |
|
|
|
|
|
| `GET` | `/` | Root redirect, 303 (authenticated → `/sources`, unauthenticated → `/pages/login`) |
|
|
|
|
|
| `GET` | `/.well-known/healthcheck` | Health check (JSON: `status`, `now`, `uptimeSeconds`, `uptimeHuman`, `version`, `appname`, `maintenanceMode`) |
|
|
|
|
|
| `GET`, `HEAD` | `/s/*` | Static file serving (embedded CSS, JS). `GET` and `HEAD` only — `POST`, `PUT`, `PATCH`, `DELETE`, `OPTIONS`, `TRACE` and `CONNECT` are answered `405 Method Not Allowed` with `Allow: GET, HEAD`. Any other method (such as `PROPFIND`) is refused by chi before it reaches this route, and gets `405` without an `Allow` header. Pinned by `TestStaticServesOnlyGetAndHead` |
|
|
|
|
|
| any | `/s/*` | Static file serving (embedded CSS, JS). Mounted for every method, not just `GET`/`HEAD`: chi's `Mount` registers all methods and `http.FileServer` special-cases only `HEAD` (by omitting the body), so a `POST` or `DELETE` to an asset is answered `200` with the file. Pinned by `TestStaticServesEveryMethod` |
|
|
|
|
|
| `POST` | `/webhook/{uuid}` | Webhook receiver endpoint. `POST` only — every other method is answered `405 Method Not Allowed` with `Allow: POST`. Rate limited (see [Rate Limiting](#rate-limiting)) |
|
|
|
|
|
|
|
|
|
|
#### Authentication Endpoints
|
|
|
|
@@ -3032,9 +3079,7 @@ check, see [The login endpoint](#the-login-endpoint).
|
|
|
|
|
- Prometheus metrics behind basic auth
|
|
|
|
|
- Static assets embedded in binary (no filesystem access needed at
|
|
|
|
|
runtime)
|
|
|
|
|
- The app runs as a non-root user (UID 1000) in the container; only
|
|
|
|
|
the `ENTRYPOINT` script that sets the data directory's owner runs as
|
|
|
|
|
root, before the app starts
|
|
|
|
|
- Container runs as non-root user (UID 1000)
|
|
|
|
|
- GORM soft deletes on every entity that carries `BaseModel`, which is
|
|
|
|
|
all of them but `Setting` (data preserved for audit)
|
|
|
|
|
|
|
|
|
@@ -3161,13 +3206,10 @@ version is fixed independently of the compiler's:
|
|
|
|
|
`GO_LDFLAGS`, so neither can drop the `-X` that stamps the version.
|
|
|
|
|
The version arrives as the `VERSION` build arg, since the context
|
|
|
|
|
has no `.git` (see [Version stamping](#version-stamping)).
|
|
|
|
|
3. **Runtime stage** (`alpine:3.21`) — copies the static binary and
|
|
|
|
|
`deploy/docker-entrypoint.sh`, creates the `/var/lib/webhooker`
|
|
|
|
|
directory for all SQLite databases, exposes port 8080, and includes
|
|
|
|
|
a health check against `/.well-known/healthcheck`. It sets no
|
|
|
|
|
`USER`: the `ENTRYPOINT` script starts as root, sets the data
|
|
|
|
|
directory's owner and mode, and runs the app as the non-root
|
|
|
|
|
`webhooker` user (UID 1000) through `su-exec`.
|
|
|
|
|
3. **Runtime stage** (`alpine:3.21`) — copies the static binary,
|
|
|
|
|
creates the `/var/lib/webhooker` directory for all SQLite databases,
|
|
|
|
|
runs as the non-root `webhooker` user (UID 1000), exposes port 8080,
|
|
|
|
|
and includes a health check against `/.well-known/healthcheck`.
|
|
|
|
|
|
|
|
|
|
The lint stage invokes `golangci-lint` directly rather than `make lint`:
|
|
|
|
|
it is already the pinned linter image, and `make lint` builds
|
|
|
|
|