From 133384660e0d342013c08ea32195defe36e7e539 Mon Sep 17 00:00:00 2001 From: Tobserver Agent Date: Tue, 28 Jul 2026 17:12:28 +0700 Subject: [PATCH] BIT-31 Expose Bitcoin Pulse API through nginx --- docs/bitcoin-pulse-ingress.md | 63 +++++++++++++++++++++++++++++++++++ modules/bitcoin-pulse.nix | 32 ++++++++++++++++++ 2 files changed, 95 insertions(+) create mode 100644 docs/bitcoin-pulse-ingress.md diff --git a/docs/bitcoin-pulse-ingress.md b/docs/bitcoin-pulse-ingress.md new file mode 100644 index 0000000..cc438ef --- /dev/null +++ b/docs/bitcoin-pulse-ingress.md @@ -0,0 +1,63 @@ +# Bitcoin Pulse public ingress + +Bitcoin Pulse uses the dedicated API hostname +`bitcoin-pulse.tobiasostner.de`. The native iOS client calls this hostname +without a path prefix. A same-origin path proxy is unnecessary because there is +no browser frontend, and no CORS headers are configured because native iOS +requests are not subject to browser CORS enforcement. + +## DNS and TLS + +The `bitcoin-pulse` A record under `tobiasostner.de` points to Tobserver's public +IPv4 address, `162.55.103.218`. There is no AAAA record until Tobserver has +working public IPv6 connectivity. + +The NixOS nginx virtual host obtains and renews its certificate through the +existing ACME configuration and redirects plaintext HTTP to HTTPS. Only ports +80 and 443 are public ingress for the API. The application listens on +`127.0.0.1:3000`; port 3000 is not opened by the host firewall, so nginx is the +only public path to the backend. + +## Proxy policy + +nginx preserves the original request path and forwards it to +`http://127.0.0.1:3000`. It supplies `Host`, `X-Real-IP`, `X-Forwarded-For`, and +`X-Forwarded-Proto`, and uses short local-upstream timeouts: two seconds to +connect and ten seconds to read or send. + +Requests are limited per source IP to an average of 300 requests per minute, +with a burst of 50 requests, and rejected with HTTP 429 when the burst is +exhausted. This allows normal polling and multiple devices behind a shared NAT +while bounding abusive traffic. Request bodies are limited to 16 KiB; the +current public API consists only of small GET requests. + +## Deployment verification + +After a human reviews and merges the change, deploy the resulting NixOS +generation and verify locally on Tobserver: + +```console +systemctl is-active nginx.service bitcoin-pulse.service +nginx -t +ss -ltn '( sport = :3000 )' +curl --fail --silent http://127.0.0.1:3000/health | jq -e '.status == "OK"' +``` + +The socket listing must show only `127.0.0.1:3000`, not `0.0.0.0:3000` or the +public address. From a machine outside Tobserver, verify DNS, HTTPS, the health +endpoint, and each functional endpoint: + +```console +dig +short A bitcoin-pulse.tobiasostner.de +curl --fail --silent https://bitcoin-pulse.tobiasostner.de/health \ + | jq -e '.status == "OK"' +curl --fail --silent https://bitcoin-pulse.tobiasostner.de/block-height | jq +curl --fail --silent https://bitcoin-pulse.tobiasostner.de/difficulty-adjustment | jq +curl --fail --silent https://bitcoin-pulse.tobiasostner.de/price | jq +``` + +The A lookup must return `162.55.103.218`. The HTTPS certificate must be valid +for `bitcoin-pulse.tobiasostner.de`; `/health` must return HTTP 200 and +`{"status":"OK"}`. The functional endpoints must return HTTP 200 and JSON after +the collectors have populated their data. A connection from outside Tobserver +to TCP port 3000 must fail. diff --git a/modules/bitcoin-pulse.nix b/modules/bitcoin-pulse.nix index 0936541..b3fd969 100644 --- a/modules/bitcoin-pulse.nix +++ b/modules/bitcoin-pulse.nix @@ -265,5 +265,37 @@ in }; }; + services.nginx = { + appendHttpConfig = '' + limit_req_zone $binary_remote_addr zone=bitcoin_pulse_api:10m rate=300r/m; + ''; + + virtualHosts."bitcoin-pulse.tobiasostner.de" = { + enableACME = true; + forceSSL = true; + + locations."/" = { + proxyPass = "http://127.0.0.1:3000"; + extraConfig = '' + limit_req zone=bitcoin_pulse_api burst=50 nodelay; + limit_req_status 429; + client_max_body_size 16k; + + proxy_http_version 1.1; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_set_header Connection ""; + + proxy_connect_timeout 2s; + proxy_read_timeout 10s; + proxy_send_timeout 10s; + proxy_next_upstream off; + ''; + }; + }; + }; + environment.systemPackages = [ pkgs.postgresql_18 ]; }