1 Commits
Author SHA1 Message Date
sneak c7722ef3da Rewrite README.md sentence by sentence
check / check (push) Canceled after 0s
Rewritten from scratch against the code on next. The old text was
the list of what a reader needs covered.

Model: fable-5-1
2026-10-25 09:10:28 +00:00
+36 -23
View File
@@ -87,6 +87,9 @@ docker run -d \
webhooker
```
The first-boot password goes to the container's log. Read it with `docker logs`
and the container's name.
In production, put a reverse proxy in front of webhooker: a server that accepts
the public connections and passes them on. Read
[Behind a reverse proxy](#behind-a-reverse-proxy) first.
@@ -182,7 +185,8 @@ creates every SQLite file with mode `0600`.
from this repository's `Dockerfile` and runs it. Add no port mapping, because
upaas publishes every mapped port on all interfaces of the host. Put the app on
your reverse proxy's Docker network instead, and mount one host directory at
`/var/lib/webhooker`.
`/var/lib/webhooker`. The first-boot password goes to the container's log. Read
it with `docker logs` and the name upaas gave the container.
### Behind a reverse proxy
@@ -190,7 +194,7 @@ webhooker speaks plain HTTP. It does not handle TLS, the encryption behind
HTTPS: it holds no certificate and accepts no HTTPS connections. The reverse
proxy holds the certificate and reaches webhooker over a private address. On a
bare host that is loopback, the address only this host can reach. For the image
it is the Docker bridge.
it is the Docker bridge, the private network Docker puts containers on.
Four things must be right.
@@ -202,10 +206,10 @@ Four things must be right.
from any connecting machine, so the proxy must overwrite whatever the client
sent.
3. **Cover the proxy in `TRUSTED_PROXIES`.** Otherwise every client shares one
rate-limit bucket, the count a limit keeps per client. The default list
covers the Docker bridge address, such as `172.17.0.1`, that the image sees a
proxy at. It does not cover loopback, so on a bare host set
`TRUSTED_PROXIES=127.0.0.1`.
rate-limit bucket, the count a limit keeps per client. A proxy on the host
reaches the container from the Docker bridge address, such as `172.17.0.1`.
The default list covers it. The list does not cover loopback, so on a bare
host set `TRUSTED_PROXIES=127.0.0.1`.
4. **Pass on the client's `Host` header with its port.** webhooker checks a
form's `Origin` against the `Host` header it receives. A `Host` without the
port makes every form submission fail with `403` on any port other than 443.
@@ -233,7 +237,9 @@ server {
}
```
The block matches webhooker's 1 MB body cap and its 60 second request timeout.
The block matches webhooker's 1 MB body cap. Its `proxy_read_timeout` is longer
than webhooker's 60 second request timeout, so webhooker's own `504` reaches the
client first.
### HSTS
@@ -375,12 +381,13 @@ SSRF guard refuses these ranges:
The SSRF guard runs when a target is saved and again when each delivery
connects, so a hostname that changes its address later is still checked.
Redirects are followed up to ten hops, and each hop is checked. A hop to another
host or port, or from `https` to `http`, carries none of the configured or
forwarded headers, so a redirect cannot pass a credential to another host.
host or port, or from `https` to `http`, drops every configured or forwarded
header except `Content-Type`. So a redirect cannot pass a credential to another
host.
Egress means the connections webhooker opens outward to deliver.
[`ALLOWED_EGRESS_CIDRS`](#allowing-egress-to-your-own-network) opens named
networks to it.
[`ALLOWED_EGRESS_CIDRS`](#allowing-egress-to-your-own-network) lets deliveries
reach networks you name.
### What the logs contain
@@ -394,8 +401,8 @@ The `admin` password is printed in the clear on the first boot and by
query string, headers and body.
When `SENTRY_DSN` is set, error reports go to Sentry, an error reporting
service. A report carries no query string, body or cookies, and the route
pattern (the route as registered, such as `/h/{uuid}`) in place of the path.
service. A report carries no query string, body or cookies. In place of the path
it carries the route pattern, the route as registered, such as `/h/{uuid}`.
## Design
@@ -410,8 +417,9 @@ delivery.
The main database holds configuration. Each webhook has its own event database,
so writes for one webhook never wait on another webhook's database. The program
starts in `cmd/webhooker/main.go`, and its packages are under `internal/`. The
pages' HTML, CSS and JavaScript are embedded in the binary.
starts in `cmd/webhooker/main.go`, and its logic is under `internal/`. The
`static` and `templates` packages embed the pages' HTML, CSS and JavaScript in
the binary.
## Configuration reference
@@ -432,7 +440,10 @@ is allowed. In `prod`, no CORS headers are sent.
### `PORT`
The TCP port the listener binds. Default `8080`.
The TCP port the listener binds. Default `8080`. Docker marks the image's
container healthy by a check every 30 seconds. The check requests port `8080`
whatever `PORT` says, so a container started with another `PORT` reports
unhealthy.
### `BIND_ADDRESS`
@@ -522,8 +533,10 @@ you list, and read the responses in the event log. List the smallest blocks that
cover your destinations, and never `0.0.0.0/0` or `::/0`.
These addresses stay blocked whatever you list. They are link-local space, the
unspecified addresses, and cloud metadata endpoints, the addresses a cloud gives
each virtual machine for its own credentials:
unspecified addresses and cloud metadata endpoints. The unspecified addresses
are `0.0.0.0` and `::`, which name no host. A cloud metadata endpoint is the
address a cloud gives each virtual machine for its own credentials. The blocked
addresses are:
- `169.254.0.0/16` and `fe80::/10`;
- `fd00:ec2::254/128` and `fd00:ec2::23/128`;
@@ -545,11 +558,11 @@ default but is reopened by a list that covers it.
An `http` target posts the event body to its URL with the event's `Content-Type`
and headers, then the target's configured **Headers**, which replace event
headers of the same name. Every `http` and `slack` request carries
`User-Agent: webhooker/{version}`, replacing the sender's `User-Agent` and any
configured one. A `2xx` response is a success. The first 4 KiB of the response
body is stored with the attempt. **Timeout** is at most 300 seconds, and blank
means the 30 second default. **Pass the query string on to this target** appends
the event's query string to the URL.
`User-Agent: webhooker/{version}`. The **Headers** field refuses `User-Agent`. A
`2xx` response is a success. The first 4 KiB of the response body is stored with
the attempt. **Timeout** is at most 300 seconds, and blank means the 30 second
default. **Pass the query string on to this target** appends the event's query
string to the URL.
A `slack` target posts a message that describes the event and shows its body in
a code block, cut at 3500 bytes. An archive target, whose stored type is