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.
This commit is contained in:
Executable
+71
@@ -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" <<EOF
|
||||||
|
server {
|
||||||
|
listen 167.17.77.8:80;
|
||||||
|
server_name $DOMAIN;
|
||||||
|
location /.well-known/acme-challenge/ { root /var/www/acme-challenge; }
|
||||||
|
location / { return 301 https://\$host\$request_uri; }
|
||||||
|
}
|
||||||
|
EOF
|
||||||
|
ln -sf ../sites-available/$DOMAIN.conf /etc/nginx/sites-enabled/$DOMAIN.conf
|
||||||
|
nginx -t && nginx -s reload
|
||||||
|
certbot certonly --webroot -w /var/www/acme-challenge -d $DOMAIN \
|
||||||
|
--key-type ecdsa --non-interactive --agree-tos -m hostmaster@floonet.dev
|
||||||
|
fi
|
||||||
|
# Full vhost (:80 redirect + :443 proxy).
|
||||||
|
install -m0644 "$UNIT_SRC_DIR/us-east/$DOMAIN.conf" "$VHOST"
|
||||||
|
ln -sf ../sites-available/$DOMAIN.conf /etc/nginx/sites-enabled/$DOMAIN.conf
|
||||||
|
nginx -t && nginx -s reload
|
||||||
|
|
||||||
|
echo "==> 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"
|
||||||
@@ -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 <BASE_URL><path>.
|
||||||
|
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
|
||||||
@@ -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
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user