Files
esh-pfi-infrastructure/stacks/hrafn/.env.example
T
vh b6924de728 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.
2026-08-22 15:05:11 -07:00

57 lines
3.2 KiB
Bash

# hrafn — fleet browser-fetch service. Copy to `.env` and set HRAFN_TOKEN.
# This file is the canonical config surface (contract § Configuration).
# ── auth (required) ──────────────────────────────────────────────────
# Shared bearer token required on POST /v1/fetch. Provision via the
# vault (secrets-broker); rotation = rotate the vault entry + recycle.
HRAFN_TOKEN=CHANGE_ME
# ── network ──────────────────────────────────────────────────────────
HRAFN_BIND=0.0.0.0
HRAFN_PORT=8080
# ── fetch limits ─────────────────────────────────────────────────────
# Max post-content-decoding body bytes of the main navigation response;
# oversize is rejected (413), never truncated. Both raw and dom modes.
HRAFN_MAX_BODY_BYTES=10485760
# Whole-request budget default + hard cap (seconds). timeout_s INCLUDES
# politeness-queue wait. A request may not exceed the cap.
HRAFN_DEFAULT_TIMEOUT_S=30
HRAFN_MAX_TIMEOUT_S=120
# Default dom-mode wait state: load | domcontentloaded | networkidle.
HRAFN_DEFAULT_WAIT_UNTIL=domcontentloaded
# ── concurrency + browser lifecycle ──────────────────────────────────
# Global in-flight cap (acquired on leaving the politeness queue).
HRAFN_CONCURRENCY=4
# Recycle the browser after N requests or M seconds of age.
HRAFN_BROWSER_MAX_REQUESTS=200
HRAFN_BROWSER_MAX_AGE_S=1800
# ── politeness gate (per registrable domain) ─────────────────────────
# Minimum seconds between fetch STARTS per domain (0 = no throttle).
HRAFN_DOMAIN_MIN_INTERVAL_S=5
# Per-domain interval overrides: comma-separated domain=seconds pairs.
# HRAFN_DOMAIN_OVERRIDES=reddit.com=10, example.com=2
HRAFN_DOMAIN_OVERRIDES=
# Waiting-request queue depth per domain (excludes the in-flight fetch;
# 0 = no queue). Overflow → 429 queue_full.
HRAFN_QUEUE_DEPTH=4
# Upstream Retry-After (on 429/503) embargoes the domain; clamped to
# this many seconds so a hostile Retry-After can't lock a domain for a day.
HRAFN_MAX_EMBARGO_S=300
# ── SSRF ─────────────────────────────────────────────────────────────
# Private/reserved targets are DENIED by default. To permit specific
# internal targets, list IPs/CIDRs (comma-separated). Empty = deny all
# private ranges (the safe default). Write entries in IPv4 form for IPv4
# targets: resolved IPv4-mapped IPv6 addresses are canonicalised to IPv4
# before matching, so an IPv6-mapped CIDR (::ffff:10.0.0.0/120) will not
# match them.
# HRAFN_PRIVATE_ALLOWLIST=10.0.0.0/8, 192.168.1.5
HRAFN_PRIVATE_ALLOWLIST=
# ── live smoke (dev only) ────────────────────────────────────────────
# Set to 1 to enable the opt-in Reddit RSS smoke test. Off in CI.
# HRAFN_LIVE_SMOKE=1