134 lines
5.3 KiB
Markdown
134 lines
5.3 KiB
Markdown
|
|
# signal_api
|
||
|
|
|
||
|
|
Runs [signal-cli-rest-api](https://github.com/bbernhard/signal-cli-rest-api) on
|
||
|
|
the `observability` host. Gatus uses it to deliver alerts over Signal.
|
||
|
|
|
||
|
|
Gatus does not speak Signal — it POSTs JSON to this service, which holds the
|
||
|
|
Signal identity and does the protocol work.
|
||
|
|
|
||
|
|
## It is never published, and that is not optional
|
||
|
|
|
||
|
|
**This API has no authentication of any kind.** No key, no token, no basic auth.
|
||
|
|
Anything that can reach the port can send messages as your identity and read
|
||
|
|
your Signal. So the compose file publishes **no ports at all** and there is no
|
||
|
|
Caddy vhost.
|
||
|
|
|
||
|
|
Gatus reaches it over a shared docker network (`monitoring`) by service name:
|
||
|
|
`http://signal-api:8080`. That is also *why* a shared network is needed rather
|
||
|
|
than a published port — Gatus runs in a container, so `127.0.0.1` for Gatus is
|
||
|
|
the Gatus container, not the host.
|
||
|
|
|
||
|
|
The network is created by an explicit Ansible task in both this role and
|
||
|
|
`gatus`, so neither stack has to be deployed before the other.
|
||
|
|
|
||
|
|
## MODE, and why `native`
|
||
|
|
|
||
|
|
Upstream offers `normal`, `native`, `json-rpc` and `json-rpc-native`. The
|
||
|
|
json-rpc modes keep a resident JVM daemon and upstream describes them as
|
||
|
|
"increased memory".
|
||
|
|
|
||
|
|
**This VPS has 464 MB of RAM**, already running Gatus and Caddy. A resident JVM
|
||
|
|
is not affordable. `native` runs a precompiled GraalVM binary per request — no
|
||
|
|
daemon, no resident cost — and alerts are rare enough that paying startup cost
|
||
|
|
per alert is the right trade.
|
||
|
|
|
||
|
|
## Linking the device — a one-time manual step
|
||
|
|
|
||
|
|
Ansible cannot scan a QR code, so this is manual. **Do not use
|
||
|
|
`/v1/qrcodelink`** — it is broken in `native` mode.
|
||
|
|
|
||
|
|
### The trap
|
||
|
|
|
||
|
|
`GET /v1/qrcodelink?device_name=...` returns:
|
||
|
|
|
||
|
|
```json
|
||
|
|
{"error":"Couldn't create QR code: no data to encode"}
|
||
|
|
```
|
||
|
|
|
||
|
|
The linking itself is fine: running the binary directly inside the container
|
||
|
|
emits a perfectly good provisioning URI.
|
||
|
|
|
||
|
|
```
|
||
|
|
$ docker exec signal-api signal-cli-native link -n gatus
|
||
|
|
sgnl://linkdevice?uuid=...&pub_key=...
|
||
|
|
```
|
||
|
|
|
||
|
|
It is the REST wrapper that fails to capture that output in `native` mode.
|
||
|
|
|
||
|
|
**Do not "fix" this by switching MODE to `normal` or `json-rpc`.** That puts a
|
||
|
|
JVM in the path of *every alert* on a 464 MB host, permanently degrading the
|
||
|
|
running system to work around a step performed once. Generate the QR yourself
|
||
|
|
instead.
|
||
|
|
|
||
|
|
### The procedure
|
||
|
|
|
||
|
|
**`docker exec` runs as root, but the service runs as uid 1000.** Without
|
||
|
|
`--config`, signal-cli writes the linked account to `/root/.local/share/signal-cli`
|
||
|
|
— the container's ephemeral layer, NOT the mounted volume. It looks like it
|
||
|
|
worked (`Associated with: +34…`), `/v1/accounts` keeps returning `[]`, and the
|
||
|
|
account is destroyed on the next `docker compose up`. Always pass `--config`.
|
||
|
|
|
||
|
|
1. Start the link and capture the URI. It must keep running while you scan:
|
||
|
|
|
||
|
|
docker exec signal-api sh -c "rm -f /tmp/link.uri; \
|
||
|
|
nohup signal-cli-native --config /home/.local/share/signal-cli \
|
||
|
|
link -n gatus > /tmp/link.uri 2>/tmp/link.log & echo started"
|
||
|
|
sleep 10
|
||
|
|
docker exec signal-api cat /tmp/link.uri
|
||
|
|
|
||
|
|
Do **not** add `setsid`, and do **not** background `docker exec` itself from
|
||
|
|
the host — the first stops the URI appearing, the second is killed when the
|
||
|
|
Ansible task returns. The output is block-buffered because stdout is a file,
|
||
|
|
so the URI appears only after several seconds; `stdbuf` does not help, as the
|
||
|
|
buffering is GraalVM's, not libc's.
|
||
|
|
|
||
|
|
2. Render the QR on your own machine and scan it:
|
||
|
|
|
||
|
|
qrencode -o /tmp/qr.png -s 12 -m 4 "sgnl://linkdevice?uuid=...&pub_key=..."
|
||
|
|
|
||
|
|
3. Phone: Signal → Settings → Linked devices → **+** → scan. Provisioning links
|
||
|
|
expire in a couple of minutes, so generate and scan in one sitting.
|
||
|
|
|
||
|
|
4. Confirm — this must list the number, not `[]`:
|
||
|
|
|
||
|
|
docker exec signal-api curl -s http://localhost:8080/v1/accounts
|
||
|
|
|
||
|
|
5. Send a test message:
|
||
|
|
|
||
|
|
docker exec signal-api curl -s -X POST -H "Content-Type: application/json" \
|
||
|
|
-d '{"message":"test","number":"+34…","recipients":["+34…"]}' \
|
||
|
|
http://localhost:8080/v2/send
|
||
|
|
|
||
|
|
Alerts are sent **from your own number**, so sending to yourself lands in Note
|
||
|
|
to Self. If the device is ever unlinked from the phone, alerts stop silently —
|
||
|
|
which is why this service is itself monitored.
|
||
|
|
|
||
|
|
### If the phone says "network error"
|
||
|
|
|
||
|
|
The phone is not the problem. `chat.signal.org` resolves to AWS Global
|
||
|
|
Accelerator **dualstack** addresses with the AAAA records first, this container
|
||
|
|
has no IPv6 address at all, and this host's IPv6 path is broken — the same edge
|
||
|
|
that returned a bogus 404 for the Go tarball. signal-cli reaches for an
|
||
|
|
unreachable IPv6 address and dies with `Link request error: Connection closed!`,
|
||
|
|
while the phone can only report a failed handshake.
|
||
|
|
|
||
|
|
That is what `gai.conf` (mounted at `/etc/gai.conf`) fixes. If linking starts
|
||
|
|
failing again, check it is still mounted and that `getent ahosts chat.signal.org`
|
||
|
|
returns an IPv4 address first.
|
||
|
|
|
||
|
|
## Backups
|
||
|
|
|
||
|
|
Deliberately **not** backed up. The data directory holds Signal private keys,
|
||
|
|
and the recovery path is to link again from the phone — which takes a minute and
|
||
|
|
does not depend on any stored artefact. Backing it up would copy a credential
|
||
|
|
off the host to buy nothing.
|
||
|
|
|
||
|
|
## Verifying
|
||
|
|
|
||
|
|
```bash
|
||
|
|
docker ps --filter name=signal-api
|
||
|
|
docker exec signal-api curl -fsS http://localhost:8080/v1/health
|
||
|
|
docker exec signal-api curl -fsS http://localhost:8080/v1/accounts
|
||
|
|
docker logs signal-api --tail 50
|
||
|
|
```
|