feat(hrafn): register the browser-fetch service and tighten its env perms
nevermore-claude handed hrafn to infra-ops for uptime ownership. Intake: verified health independently, brought it into the inventory, and fixed one security defect found during the check. - canonicalize stacks/hrafn/ (compose.yaml + .env.example + README) - list hrafn in the ana-docker running-stacks table - README records the load-bearing bits: shm_size 1gb is required for Chromium, playwright and the base image are version-coupled, SSRF denies private targets by default, and restart:unless-stopped does NOT act on healthcheck failure Security fix applied on the host, not in this tree: /opt/docker/compose/ hrafn/.env was 0644 with a live 57-char bearer token in it, readable by every local account (verified by reading it as `nobody`). Tightened to 0600; owner unchanged so the deploy path still works, confirmed via `docker compose config` as lkraven. Container untouched and still healthy. Known gaps recorded rather than silently accepted: no CI deploy, and the image builds from an rsync'd working tree living in the compose directory, so the running image has no commit provenance.
This commit is contained in:
@@ -0,0 +1,81 @@
|
||||
# hrafn
|
||||
|
||||
Browser-fetch service — a genuine Chromium kept behind a REST API so consumers
|
||||
never have to embed a browser themselves. It exists because bot-gated sites
|
||||
(Reddit first) 403 every non-browser client; hrafn is the one place in the fleet
|
||||
that keeps a real browser warm.
|
||||
|
||||
**Deploys to:**
|
||||
- **ana-docker** (10.250.50.70) — internal-only, no published host port.
|
||||
Consumers reach it at `http://hrafn:8080` on `traefik-net`.
|
||||
|
||||
Built and handed to infra-ops for uptime ownership by nevermore-claude,
|
||||
2026-08-22. Upstream source: `vh/hrafn` (private gitea repo).
|
||||
|
||||
## Reaching it
|
||||
|
||||
There is **no host port** — this is deliberate (contract § Deployment). Only
|
||||
containers on `traefik-net` can talk to it:
|
||||
|
||||
```bash
|
||||
# from another container on traefik-net
|
||||
curl -H "Authorization: Bearer $HRAFN_TOKEN" \
|
||||
-X POST http://hrafn:8080/v1/fetch -d '{"url":"https://example.com"}'
|
||||
|
||||
# from the host, for diagnostics only
|
||||
ssh infra-ops@10.250.50.70 \
|
||||
'sudo docker run --rm --network traefik-net curlimages/curl:latest \
|
||||
-s -o /dev/null -w "%{http_code}\n" http://hrafn:8080/readyz'
|
||||
```
|
||||
|
||||
`/readyz` and the other health endpoints are unauthenticated; `POST /v1/fetch`
|
||||
requires the bearer token.
|
||||
|
||||
## Credentials
|
||||
|
||||
`HRAFN_TOKEN` lives in `/opt/docker/compose/hrafn/.env` on the host and is
|
||||
vaulted at `ana-docker/hrafn/bearer-token` (secrets-broker). The `.env` is
|
||||
**0600** — it was 0644 on handoff, which exposed a live bearer token to every
|
||||
local account on the box; tightened 2026-08-22. Keep it 0600.
|
||||
|
||||
## Operational notes
|
||||
|
||||
- **`shm_size: 1gb` is load-bearing.** Chromium crashes on some pages with
|
||||
Docker's 64 MB default. Don't trim it to save memory.
|
||||
- **Playwright and the base image are version-coupled.** Base is
|
||||
`mcr.microsoft.com/playwright/python:v1.62.0-noble` with playwright pinned to
|
||||
`1.62.0`. Bump both together or the browser binary and the client library
|
||||
disagree.
|
||||
- **SSRF is denied by default** — private and reserved targets are refused
|
||||
unless listed in `HRAFN_PRIVATE_ALLOWLIST`, which is empty. Leave it empty
|
||||
unless there is a specific internal target and a reason.
|
||||
- **`restart: unless-stopped` does not act on the healthcheck.** Docker restarts
|
||||
on *exit*, not on *unhealthy*. A wedged-but-running Chromium will sit there
|
||||
marked unhealthy indefinitely. Beszel on this host reports container status,
|
||||
so that is the alerting path.
|
||||
|
||||
## Deploy
|
||||
|
||||
**No CI deploy is wired.** As of the handoff the image builds from a working
|
||||
tree rsync'd into the compose directory, so `/opt/docker/compose/hrafn/`
|
||||
contains the full source (`src/`, `tests/`, `pyproject.toml`, …) rather than
|
||||
just a compose file. That is a deviation from the house convention and means
|
||||
the running image has no commit provenance.
|
||||
|
||||
Current update path:
|
||||
|
||||
```bash
|
||||
ssh infra-ops@10.250.50.70
|
||||
cd /opt/docker/compose/hrafn
|
||||
# rsync the updated working tree in, then:
|
||||
sudo docker compose build && sudo docker compose up -d
|
||||
sudo docker compose ps # expect (healthy)
|
||||
```
|
||||
|
||||
## Known gaps
|
||||
|
||||
| gap | impact | status |
|
||||
|---|---|---|
|
||||
| No CI deploy | manual rsync + build; no provenance for what is running | offered by nevermore-claude, not yet built |
|
||||
| Source tree in the compose dir | deviates from `/opt/docker/compose/<stack>/compose.yaml` convention | needs a host-clone or registry-image deploy shape |
|
||||
| No homepage entry | internal-only service, so arguably correct | intentional for now |
|
||||
Reference in New Issue
Block a user