From be15c78121f458c0ffba14aff645435c4b48aaa7 Mon Sep 17 00:00:00 2001 From: 2ro <17595647+2ro@users.noreply.github.com> Date: Fri, 3 Jul 2026 03:15:38 -0400 Subject: [PATCH] floonet-strfry: us-east production deploy bundle for the nm.floonet.dev name authority Bundle that stands the bundled name authority up behind nginx at https://nm.floonet.dev in paid-name mode wired to the on-box GoblinPay: * nm.floonet.dev.conf nginx vhost mirroring the relay.floonet.dev pattern (same listen IP + certbot webroot), TLS -> 127.0.0.1:8193, sets X-Real-IP (which the per-IP rate limiter keys off). * floonet-authority.service.d/10-us-east.conf drop-in over the generic hardened unit: swaps DynamicUser for the stable unprivileged goblin account and relocates the DB into the /opt/goblin tree (one backup root), inheriting every other sandbox directive. * floonet-authority.env.example FLOONET_PAY_MODE=name, GOBLINPAY_URL at the loopback GoblinPay; the real GP token is filled from goblinpay.env at deploy time, never here. * deploy.sh idempotent runbook: build on-box, install, two-phase certbot (acme :80 -> cert -> :443), start. Never touches goblin-nip05d or firewalld. --- deploy/us-east/deploy.sh | 71 +++++++++++++++++++ deploy/us-east/floonet-authority.env.example | 40 +++++++++++ .../10-us-east.conf | 27 +++++++ deploy/us-east/nm.floonet.dev.conf | 38 ++++++++++ 4 files changed, 176 insertions(+) create mode 100755 deploy/us-east/deploy.sh create mode 100644 deploy/us-east/floonet-authority.env.example create mode 100644 deploy/us-east/floonet-authority.service.d/10-us-east.conf create mode 100644 deploy/us-east/nm.floonet.dev.conf diff --git a/deploy/us-east/deploy.sh b/deploy/us-east/deploy.sh new file mode 100755 index 0000000..8056e08 --- /dev/null +++ b/deploy/us-east/deploy.sh @@ -0,0 +1,71 @@ +#!/usr/bin/env bash +# Deploy / re-deploy the Floonet name authority on the us-east box behind nginx +# at https://nm.floonet.dev, in paid-name mode wired to the local GoblinPay. +# +# Idempotent: safe to re-run. Run as root on the box. It does NOT touch the +# goblin.st production name service (goblin-nip05d) and never toggles firewalld. +# +# Prereqs already satisfied on us-east (documented for reproducibility): +# * DNS: nm.floonet.dev A -> 167.17.77.8 (pdnsutil add-record floonet.dev nm A 14400 167.17.77.8) +# * Rust 1.95 toolchain; GoblinPay live on 127.0.0.1:8192 (GP_API_TOKEN in goblinpay.env) +set -euo pipefail + +REPO_DIR="${REPO_DIR:-/opt/goblin/gpbuild/floonet-name-authority}" # crate checkout +DATA_DIR="/opt/goblin/floonet-authority" +BIN=/usr/local/bin/floonet-name-authority +ENV_FILE=/etc/floonet-authority.env +UNIT_SRC_DIR="$(cd "$(dirname "$0")/.." && pwd)" # deploy/ +GP_ENV=/opt/goblin/goblinpay/goblinpay.env +DOMAIN=nm.floonet.dev +PORT=8193 + +echo "==> build (on-box, glibc-matched)" +( cd "$REPO_DIR" && . "$HOME/.cargo/env" 2>/dev/null || true; cargo build --release --locked ) +install -m0755 "$REPO_DIR/target/release/floonet-name-authority" "$BIN" + +echo "==> data dir" +install -d -m0750 -o goblin -g goblin "$DATA_DIR" + +echo "==> env file (token pulled from GoblinPay's env, never echoed)" +if [ ! -f "$ENV_FILE" ]; then + GP_TOKEN="$(sed -n 's/^GP_API_TOKEN=//p' "$GP_ENV" | tr -d '\r\n')" + sed "s#__REPLACE_WITH_GP_API_TOKEN__#${GP_TOKEN}#" \ + "$UNIT_SRC_DIR/us-east/floonet-authority.env.example" > "$ENV_FILE" + chown root:goblin "$ENV_FILE"; chmod 0640 "$ENV_FILE" +fi + +echo "==> systemd unit + us-east drop-in" +install -m0644 "$UNIT_SRC_DIR/systemd/floonet-authority.service" /etc/systemd/system/ +install -d -m0755 /etc/systemd/system/floonet-authority.service.d +install -m0644 "$UNIT_SRC_DIR/us-east/floonet-authority.service.d/10-us-east.conf" \ + /etc/systemd/system/floonet-authority.service.d/ +systemctl daemon-reload + +echo "==> nginx: acme (:80) first, then certbot, then TLS (:443)" +VHOST=/etc/nginx/sites-available/$DOMAIN.conf +if [ ! -f /etc/letsencrypt/live/$DOMAIN/fullchain.pem ]; then + # Stand up a temporary :80-only vhost so the HTTP-01 webroot resolves. + cat > "$VHOST" < start the authority (paid-name mode)" +systemctl enable --now floonet-authority +sleep 1 +systemctl --no-pager --full status floonet-authority | head -5 +curl -fsS "http://127.0.0.1:$PORT/api/v1/health" && echo " <- local health ok" +echo "==> done: https://$DOMAIN" diff --git a/deploy/us-east/floonet-authority.env.example b/deploy/us-east/floonet-authority.env.example new file mode 100644 index 0000000..080cd30 --- /dev/null +++ b/deploy/us-east/floonet-authority.env.example @@ -0,0 +1,40 @@ +# /etc/floonet-authority.env — us-east production template (nm.floonet.dev). +# +# Copy to /etc/floonet-authority.env, fill GOBLINPAY_TOKEN from +# /opt/goblin/goblinpay/goblinpay.env (GP_API_TOKEN), then: +# install -m0640 -o root -g goblin floonet-authority.env /etc/floonet-authority.env +# Keep the real file (with the token) OUT of git — this is only the template. + +# --- Identity --- +# Names are name@nm.floonet.dev; the well-known is served at this host, so the +# @domain and BASE_URL host must both equal nm.floonet.dev (the authority +# refuses to start otherwise). BASE_URL is load-bearing: NIP-98 `u`-tags are +# verified against . +FLOONET_DOMAIN=nm.floonet.dev +FLOONET_BASE_URL=https://nm.floonet.dev +FLOONET_RELAYS=wss://relay.floonet.dev +FLOONET_NAMES_BIND=127.0.0.1:8193 +# FLOONET_NAMES_DB is set by the systemd drop-in: +# /opt/goblin/floonet-authority/names.db + +# --- Paid names via GoblinPay (Grin), admin-priced --- +# off | name | write. `name` = claiming name@domain costs FLOONET_NAME_PRICE_GRIN. +FLOONET_PAY_MODE=name +FLOONET_NAME_PRICE_GRIN=1 +# GoblinPay runs on the same box (goblin-pay.service, 127.0.0.1:8192). The +# authority calls {GOBLINPAY_URL}/invoice and GET /invoice/{id} server-side +# with a Bearer token; payers still land on GoblinPay's public hosted pay_url +# (GP_PUBLIC_URL). Loopback avoids a needless public round-trip. +GOBLINPAY_URL=http://127.0.0.1:8192 +GOBLINPAY_TOKEN=__REPLACE_WITH_GP_API_TOKEN__ +# Optional: instant settlement instead of polling. If set, point a GoblinPay +# webhook at https://nm.floonet.dev/api/v1/goblinpay/webhook. +#GOBLINPAY_WEBHOOK_SECRET= + +# --- Rate-limit ceilings (per X-Real-IP; nginx sets it from $remote_addr) --- +FLOONET_READ_RATE_MAX=120 +FLOONET_READ_RATE_WINDOW_SECS=60 +FLOONET_WRITE_RATE_MAX=10 +FLOONET_WRITE_RATE_WINDOW_SECS=3600 + +RUST_LOG=info diff --git a/deploy/us-east/floonet-authority.service.d/10-us-east.conf b/deploy/us-east/floonet-authority.service.d/10-us-east.conf new file mode 100644 index 0000000..da6bd2a --- /dev/null +++ b/deploy/us-east/floonet-authority.service.d/10-us-east.conf @@ -0,0 +1,27 @@ +# us-east production overrides for the generic hardened unit +# (deploy/systemd/floonet-authority.service). +# +# The box keeps every Goblin service's data under /opt/goblin (a single backup +# root) and runs them as the unprivileged `goblin` account, so we swap the +# generic unit's DynamicUser for a stable owner and relocate the writable path. +# The base unit's own comment sanctions exactly this ("If you need a stable +# owner for the data dir ... set User="). Every other hardening directive from +# the base unit (ProtectSystem=strict, NoNewPrivileges, ProtectHome, the +# @system-service syscall filter, RestrictAddressFamilies, ...) is inherited +# unchanged — goblin's home is /opt/goblin/nip05d, not under /home, so +# ProtectHome=yes stays safe. +[Service] +DynamicUser=no +User=goblin +Group=goblin + +# The base unit ships StateDirectory=floonet-authority plus a matching +# /var/lib writable path. Clear both (empty assignment resets the list) and +# point everything at the /opt/goblin tree instead. +StateDirectory= +ReadWritePaths= +ReadWritePaths=/opt/goblin/floonet-authority +WorkingDirectory=/opt/goblin/floonet-authority + +# Applied after the base unit, so this wins over the base Environment= line. +Environment=FLOONET_NAMES_DB=/opt/goblin/floonet-authority/names.db diff --git a/deploy/us-east/nm.floonet.dev.conf b/deploy/us-east/nm.floonet.dev.conf new file mode 100644 index 0000000..91002d5 --- /dev/null +++ b/deploy/us-east/nm.floonet.dev.conf @@ -0,0 +1,38 @@ +# Floonet name authority — nm.floonet.dev +# +# TLS terminates here; the authority listens on 127.0.0.1:8193 and keys ALL +# per-IP rate limiting off X-Real-IP, so setting it from $remote_addr is +# SECURITY-CRITICAL: a missing value collapses every client into one bucket +# and defeats the limiter. Mirrors the relay.floonet.dev vhost (same listen +# IP, same certbot webroot, same header set) minus the WebSocket upgrade, +# since the authority is a plain JSON/REST service. +server { + listen 167.17.77.8:80; + server_name nm.floonet.dev; + location /.well-known/acme-challenge/ { root /var/www/acme-challenge; } + location / { return 301 https://$host$request_uri; } +} +server { + listen 167.17.77.8:443 ssl http2; + server_name nm.floonet.dev; + ssl_certificate /etc/letsencrypt/live/nm.floonet.dev/fullchain.pem; + ssl_certificate_key /etc/letsencrypt/live/nm.floonet.dev/privkey.pem; + ssl_protocols TLSv1.2 TLSv1.3; + add_header X-Content-Type-Options "nosniff" always; + access_log /var/log/nginx/nm.floonet.dev.access.log; + error_log /var/log/nginx/nm.floonet.dev.error.log; + + # Registration / quote bodies are tiny JSON; cap to keep abuse cheap. + client_max_body_size 16k; + + location / { + proxy_pass http://127.0.0.1:8193; + 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 https; + proxy_read_timeout 30s; + proxy_send_timeout 30s; + } +}