Ngate basic

From CompleteNoobs
Jump to navigation Jump to search
Please Select a Licence from the LICENCE_HEADERS page
And place at top of your page
If no Licence is Selected/Appended, Default will be CC0

Default Licence IF there is no Licence placed below this notice! When you edit this page, you agree to release your contribution under the CC0 Licence

LICENCE: More information about the cc0 licence can be found here:
https://creativecommons.org/share-your-work/public-domain/cc0

The person who associated a work with this deed has dedicated the work to the public domain by waiving all of his or her rights to the work worldwide under copyright law, including all related and neighboring rights, to the extent allowed by law.

You can copy, modify, distribute and perform the work, even for commercial purposes, all without asking permission.

Licence:

Statement of Purpose

The laws of most jurisdictions throughout the world automatically confer exclusive Copyright and Related Rights (defined below) upon the creator and subsequent owner(s) (each and all, an "owner") of an original work of authorship and/or a database (each, a "Work").

Certain owners wish to permanently relinquish those rights to a Work for the purpose of contributing to a commons of creative, cultural and scientific works ("Commons") that the public can reliably and without fear of later claims of infringement build upon, modify, incorporate in other works, reuse and redistribute as freely as possible in any form whatsoever and for any purposes, including without limitation commercial purposes. These owners may contribute to the Commons to promote the ideal of a free culture and the further production of creative, cultural and scientific works, or to gain reputation or greater distribution for their Work in part through the use and efforts of others.

For these and/or other purposes and motivations, and without any expectation of additional consideration or compensation, the person associating CC0 with a Work (the "Affirmer"), to the extent that he or she is an owner of Copyright and Related Rights in the Work, voluntarily elects to apply CC0 to the Work and publicly distribute the Work under its terms, with knowledge of his or her Copyright and Related Rights in the Work and the meaning and intended legal effect of CC0 on those rights.

1. Copyright and Related Rights. A Work made available under CC0 may be protected by copyright and related or neighboring rights ("Copyright and Related Rights"). Copyright and Related Rights include, but are not limited to, the following:

   the right to reproduce, adapt, distribute, perform, display, communicate, and translate a Work;
   moral rights retained by the original author(s) and/or performer(s);
   publicity and privacy rights pertaining to a person's image or likeness depicted in a Work;
   rights protecting against unfair competition in regards to a Work, subject to the limitations in paragraph 4(a), below;
   rights protecting the extraction, dissemination, use and reuse of data in a Work;
   database rights (such as those arising under Directive 96/9/EC of the European Parliament and of the Council of 11 March 1996 on the legal protection of databases, and under any national implementation thereof, including any amended or successor version of such directive); and
   other similar, equivalent or corresponding rights throughout the world based on applicable law or treaty, and any national implementations thereof.

2. Waiver. To the greatest extent permitted by, but not in contravention of, applicable law, Affirmer hereby overtly, fully, permanently, irrevocably and unconditionally waives, abandons, and surrenders all of Affirmer's Copyright and Related Rights and associated claims and causes of action, whether now known or unknown (including existing as well as future claims and causes of action), in the Work (i) in all territories worldwide, (ii) for the maximum duration provided by applicable law or treaty (including future time extensions), (iii) in any current or future medium and for any number of copies, and (iv) for any purpose whatsoever, including without limitation commercial, advertising or promotional purposes (the "Waiver"). Affirmer makes the Waiver for the benefit of each member of the public at large and to the detriment of Affirmer's heirs and successors, fully intending that such Waiver shall not be subject to revocation, rescission, cancellation, termination, or any other legal or equitable action to disrupt the quiet enjoyment of the Work by the public as contemplated by Affirmer's express Statement of Purpose.

3. Public License Fallback. Should any part of the Waiver for any reason be judged legally invalid or ineffective under applicable law, then the Waiver shall be preserved to the maximum extent permitted taking into account Affirmer's express Statement of Purpose. In addition, to the extent the Waiver is so judged Affirmer hereby grants to each affected person a royalty-free, non transferable, non sublicensable, non exclusive, irrevocable and unconditional license to exercise Affirmer's Copyright and Related Rights in the Work (i) in all territories worldwide, (ii) for the maximum duration provided by applicable law or treaty (including future time extensions), (iii) in any current or future medium and for any number of copies, and (iv) for any purpose whatsoever, including without limitation commercial, advertising or promotional purposes (the "License"). The License shall be deemed effective as of the date CC0 was applied by Affirmer to the Work. Should any part of the License for any reason be judged legally invalid or ineffective under applicable law, such partial invalidity or ineffectiveness shall not invalidate the remainder of the License, and in such case Affirmer hereby affirms that he or she will not (i) exercise any of his or her remaining Copyright and Related Rights in the Work or (ii) assert any associated claims and causes of action with respect to the Work, in either case contrary to Affirmer's express Statement of Purpose.

4. Limitations and Disclaimers.

   No trademark or patent rights held by Affirmer are waived, abandoned, surrendered, licensed or otherwise affected by this document.
   Affirmer offers the Work as-is and makes no representations or warranties of any kind concerning the Work, express, implied, statutory or otherwise, including without limitation warranties of title, merchantability, fitness for a particular purpose, non infringement, or the absence of latent or other defects, accuracy, or the present or absence of errors, whether or not discoverable, all to the greatest extent permissible under applicable law.
   Affirmer disclaims responsibility for clearing rights of other persons that may apply to the Work or any use thereof, including without limitation any person's Copyright and Related Rights in the Work. Further, Affirmer disclaims responsibility for obtaining any necessary consents, permissions or other rights required for any use of the Work.
   Affirmer understands and acknowledges that Creative Commons is not a party to this document and has no duty or obligation with respect to this CC0 or use of the Work.

LICENCE: When you edit this page, you agree to release your contribution under the MIT Licence

LICENCE

Copyright <YEAR> <COPYRIGHT HOLDER>

Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.


nGate — Deploy to a Fresh Ubuntu 24.04 Server

From CompleteNoobs

This guide walks an operator through deploying nGate on top of an existing stage-1 nostr-rs-relay running on a Vultr Ubuntu 24.04 box. After this you'll have:

  • The relay still running as before.
  • Caddy still terminating TLS as before.
  • A new sidecar container (ngate-sync) running alongside,
 scanning Hive every 6 hours, updating the relay's pubkey_whitelist
 automatically when v4call-server operators announce themselves.
  • Cryptographic enforcement: only posts that prove ownership of both
 the Hive account AND the Nostr key (via mutual attestation) get whitelisted.

Prerequisites

You've already:

  • Followed stage 1 and have a working
 wss://nostr.YOUR-DOMAIN/ relay.
  • Followed stage 2 enough to be comfortable with the
 Nostr protocol fundamentals.
  • Have at least one v4call-server announce post on Hive (signed via
 server-sign.html, announced via server-announce.html)
 to use as test input.

If you don't have a v4call-server post yet, you can still deploy nGate and test it (the scan will find existing operators' posts), you just won't be in the whitelist until you publish your own.

Contents

Step 1: SSH to the relay box

ssh root@nostr.YOUR-DOMAIN

If you set up a non-root user during stage 1, use that and prefix everything with sudo as needed.

Step 2: Install runtime dependencies

nGate needs a few things beyond what stage 1 installed. Inside the sidecar container, all deps are auto-installed (Alpine image bakes Node + jq + curl + docker-cli + yq). Outside the container — for dry-runs and manual sync commands — install on the host:

apt update
apt install -y nodejs npm jq curl

Verify:

for c in bash curl jq sed awk node; do command -v $c >/dev/null && echo "✓ $c" || echo "✗ $c MISSING"; done

All six should print ✓.

Step 3: Clone nGate

The relay lives at /opt/nostr-relay/. Clone nGate next to it so the bind-mount paths in docker-compose.example.yml work:

cd /opt/nostr-relay
git clone https://github.com/CompleteNoobs/nGate src

That gives you:

/opt/nostr-relay/
├── config.toml             ← from stage 1
├── Caddyfile               ← from stage 1
├── docker-compose.yml      ← from stage 1
├── data/                   ← from stage 1 (relay sqlite DB)
├── caddy_data/             ← from stage 1
└── src/                    ← NEW (nGate repo)
    ├── scripts/
    ├── ngate.yaml.example
    ├── docker-compose.example.yml
    └── ...

For convenience, symlink the scripts dir + copy the YAML template up to /opt/nostr-relay/ (so bind-mount paths are simple):

cd /opt/nostr-relay
ln -s src/scripts scripts
cp src/ngate.yaml.example ngate.yaml

Now scripts/ngate-*.sh work from /opt/nostr-relay/.

💡 Tip: you can also clone nGate anywhere else and adjust the NGATE_* env-vars in ngate.yaml to point at the right paths. The clone-into-src/-and-symlink approach is just the path of least resistance.

Step 4: Install npm dependencies

The Hive ECDSA + Nostr schnorr helpers are Node scripts that need @hiveio/dhive and nostr-tools:

cd /opt/nostr-relay/src/scripts
npm install

This creates node_modules/ in the scripts folder (~35MB). Gitignored — won't bloat the repo. One-time cost; subsequent git pull + npm install is incremental.

Verify both deps are present:

ls node_modules/@hiveio/dhive/package.json
ls node_modules/nostr-tools/package.json

Both should exist.

Step 5: Bootstrap config.toml

Your stage-1 config.toml has an [authorization] section with hand-pasted hex pubkeys. nGate manages a specific block of this file between BEGIN/END marker comments. Run the bootstrap to wrap your existing section:

cd /opt/nostr-relay
./scripts/ngate-apply.sh --bootstrap

Output should look like:

ngate-apply: ✓ Bootstrapped — wrapped [authorization] in BEGIN/END markers.
ngate-apply:   ngate-apply now manages everything between those markers.
ngate-apply:   Operator-edited keys go in: /opt/nostr-relay/seed.toml

Inspect config.toml:

cat /opt/nostr-relay/config.toml

You should see your existing pubkey_whitelist entries, now wrapped:

# === BEGIN NGATE-MANAGED — DO NOT EDIT BY HAND ===
[authorization]
pubkey_whitelist = [
  "your_existing_hex_1",
  "your_existing_hex_2",
  "your_existing_hex_3",
]
# === END NGATE-MANAGED ===

⚠ From this point forward, don't hand-edit between those markers. nGate will overwrite that region on every sync. Manual additions go in seed.toml (next step).

Step 6: Create seed.toml (CRITICAL — don't skip)

Why this matters: nGate's first --apply with --allow-removals can delete entries that aren't currently backed by a valid v4call-server post on Hive — including yours, if your operator key isn't yet announced. Without a seed entry, you can lock yourself out.

Make a seed.toml at /opt/nostr-relay/seed.toml listing every hex pubkey that should ALWAYS be allowed, regardless of discovery state. Most importantly: your own operator key.

nano /opt/nostr-relay/seed.toml

Paste:

# nGate seed list — operator-managed, never auto-removed.
# One hex pubkey per line. Comments allowed.
#
# Critical: include your own operator key here so nGate can't accidentally
# lock you out if your v4call-server post temporarily fails verification.
your_operator_hex_pubkey_here   # @noblemage's nostr key

If you've been running stage 1 with 3 manual keys, you might want all 3 as seeds initially. Worst case is one extra entry in the whitelist; not a correctness problem.

Step 7: Configure ngate.yaml

Open the YAML config and edit for your relay:

nano /opt/nostr-relay/ngate.yaml

The defaults are sensible. Key fields to customise:

  • instance_name — appears in logs. e.g. nostr-hive-book.
  • scan_interval_seconds — default 21600 (6 hours). If you
 want faster updates and don't mind the Hive RC, drop to 3600 (1 hour).
  • gate.accountescrow (default; gates the
 operational account that holds caller funds) or hive_account
 (gates the announce-signing account).
  • gate.min_hp — minimum HP staked. 3 is a fine
 test threshold. Production servers might want 30+.
  • gate.min_token_* — leave blank to disable; set to a
 Hive-Engine token symbol + amount if you want token-gating.
  • max_consecutive_failures — 3 = ~18 hours of absence before
 remove. Keep default unless you have a reason.
  • restart_command — default docker restart nostr-relay
 matches the container_name in your docker-compose.yml. Don't change
 unless you renamed the relay container.
  • paths.* — only change if you put nGate somewhere other than
 /opt/nostr-relay/.

Step 8: Dry-run the chain

Before turning the sidecar loose, run the full pipeline manually in --dry-run mode (no writes, no restarts):

cd /opt/nostr-relay
./scripts/ngate-scan.sh 2>/dev/null \
  | ./scripts/ngate-verify.sh 2>&1 \
  | NGATE_MIN_HP=3 ./scripts/ngate-gate.sh 2>&1 \
  | ./scripts/ngate-apply.sh

Expected stderr output sequence:

  • ngate-scan: ok, N posts
  • ngate-verify: OK / REJECT / skip for each candidate
  • ngate-gate: OK / REJECT for each verified candidate
  • ngate-apply: seed: X loaded, current: Y in whitelist, stdin:
 Z passed, summary: add=… keep=… tolerate=… remove=… → DRY RUN, no
 files changed.

Read the output. Check that:

  • Your own operator key is in the SEED count.
  • Your v4call-server post (if you have one) appears as OK in verify and
 passes the gate (or fails for an understandable reason).
  • The summary "new whitelist size" looks plausible.
  • config.toml hasn't been modified (cat config.toml shows the
 same content as after bootstrap).

If anything looks off, fix it (re-sign + re-announce, adjust ngate.yaml, adjust seed.toml) before going live.

Step 9: Merge sidecar into docker-compose.yml

Your stage-1 docker-compose.yml already has the nostr-relay and caddy services. The nGate sidecar adds a third. Open both files:

nano /opt/nostr-relay/docker-compose.yml
# In another terminal:
cat /opt/nostr-relay/src/docker-compose.example.yml

Copy the ngate-sync service block from the example into your real docker-compose.yml. The block looks like:

services:
  # ... your existing nostr-relay + caddy services ...
  ngate-sync:
    build:
      context: .
      dockerfile: src/scripts/Dockerfile
    container_name: ngate-sync
    restart: unless-stopped
    volumes:
      - ./src/scripts:/app/scripts
      - ./ngate.yaml:/app/ngate.yaml:ro
      - ./seed.toml:/app/seed.toml:ro
      - ./config.toml:/app/config.toml
      - ./state.json:/app/state.json
      - /var/run/docker.sock:/var/run/docker.sock
    environment:
      - NGATE_YAML=/app/ngate.yaml
    depends_on:
      - nostr-relay
    networks:
      - relaynet

(Note: ./src/scripts instead of ./scripts because we kept the cloned repo at src/ and symlinked. If you did it differently, adjust the volume paths.)

Make sure to pre-create the state.json so the bind-mount works:

echo '{"last_run":"","last_apply":"","restart_log":[],"candidates":{}}' > /opt/nostr-relay/state.json

⚠ Security note: the sidecar mounts /var/run/docker.sock so it can docker restart nostr-relay. This gives the sidecar container effective root on the host. Acceptable on a single-purpose relay box. NOT acceptable on a multi-tenant machine. If that bothers you, set restart_command in ngate.yaml to a noop (e.g. true) and restart the relay manually after each cycle — configuration writes still work; only the restart is skipped.

Step 10: Build and start the sidecar

cd /opt/nostr-relay
docker compose build ngate-sync
docker compose up -d ngate-sync

First build downloads node:20-alpine + yq + docker-cli (~150MB). Subsequent builds are fast (cached layers). On startup the sidecar runs npm install inside the bind-mounted scripts dir if node_modules isn't present — should be quick if you already ran step 4.

Check the container's running:

docker compose ps ngate-sync

Should show Up.

Step 11: Watch a cycle

Tail the logs:

docker compose logs -f ngate-sync

The sidecar runs ONE cycle immediately at startup, then sleeps for scan_interval_seconds between cycles. The first cycle should show:

[2026-05-13T…] [nostr-hive-book] starting ngate-sync instance=… interval=21600s …
[2026-05-13T…] [nostr-hive-book] ─── cycle starting ───
ngate-scan: trying https://api.hive.blog …
ngate-scan:   ok, N posts
ngate-scan: scan complete
ngate-verify: OK @cnoobs/call.completenoobs.com — Hive sig + Nostr attestation valid …
ngate-verify: OK @v4call/v4call.com — Hive sig + Nostr attestation valid …
ngate-verify: OK @hive-book/hive-book.com — Hive sig + Nostr attestation valid …
ngate-gate: config: account=escrow mode=or hp_enabled=1 (>=3, delegated=false) …
ngate-gate: OK @cnoobs/… — passed via hp hp=…
ngate-gate: complete — total=3 passed=3 rejected=0 errors=0
ngate-apply: seed: 1 pubkey(s) loaded from /app/seed.toml
ngate-apply: current: 3 pubkey(s) in /app/config.toml whitelist
ngate-apply: stdin: 3 candidate(s) passed gate this run
ngate-apply: summary: add=… keep=… tolerate=0 remove=0 seed=1 → new whitelist size = …
ngate-apply: ✓ wrote new whitelist to /app/config.toml
ngate-apply: restarting relay: docker restart nostr-relay
ngate-apply: ✓ restart succeeded
[2026-05-13T…] [nostr-hive-book] ─── cycle complete (apply ok) ───
[2026-05-13T…] [nostr-hive-book] sleeping 21600s until next cycle

After this, your config.toml's nGate-managed block should reflect the discovered+gated set.

Verify the relay still works:

docker compose ps nostr-relay   # Up
# From your laptop:
nak req -k 1 -a YOUR_HEX wss://nostr.YOUR-DOMAIN

If it returns events, the restart worked cleanly.

Step 12: Routine monitoring

Status snapshot
From inside the sidecar:
docker compose exec ngate-sync /app/scripts/ngate-status.sh
Or from the host (env-vars on the command):
NGATE_YAML=/opt/nostr-relay/ngate.yaml \
NGATE_STATE_PATH=/opt/nostr-relay/state.json \
NGATE_CONFIG_PATH=/opt/nostr-relay/config.toml \
NGATE_SEED_PATH=/opt/nostr-relay/seed.toml \
/opt/nostr-relay/scripts/ngate-status.sh
Logs
  • docker compose logs -f ngate-sync — live stream
  • tail -F /opt/nostr-relay/ngate-sync.log — bind-mounted host file
Manual sync
If you want a fresh cycle right now (don't wait 6 hours):
docker compose exec ngate-sync /app/scripts/ngate-sync.sh --once
Or restart the sidecar (runs one cycle on startup):
docker compose restart ngate-sync
Inspect state.json
Shows per-key first-seen, last-seen, consecutive_misses, source
(discovery|seed|removed):
jq . /opt/nostr-relay/state.json

Updating nGate

When the repo updates:

cd /opt/nostr-relay/src
git pull
cd scripts && npm install   # in case dep versions changed
cd /opt/nostr-relay
docker compose build ngate-sync   # only needed if Dockerfile changed
docker compose restart ngate-sync

The scripts dir is bind-mounted, so script-only changes take effect on next sync without rebuild. Restart picks them up immediately.

Known gotchas

"Keychain not detected" in server-announce (browser-side, not nGate)
Some browsers don't expose window.hive_keychain even with the
extension installed (iOS Safari, Brave on iOS, etc.). server-announce.html
has a posting-key paste fallback that uses dhive to broadcast directly —
scroll past the Keychain button to the "POSTING KEY PASTE" section. Key
never leaves your browser.
nGate-verify rejects with "no nostr_attestation in well-known OR post"
Your v4call-server post is pre-attestation, OR the well-known doesn't have
the nostr_attestation field, OR the post body doesn't have a
NOSTR-ATTESTATION: base64 line. Re-sign in
server-sign.html (Option B canonical) and re-announce. The
full chain produces both the well-known field AND the post-body line in
one signed event.
nGate-verify rejects with "Hive signature DID NOT verify"
Most common cause: signature was computed under a DIFFERENT canonical
payload shape than what nGate-verify reconstructs. Re-sign with the
current server-sign.html (which produces the Option B / SHORT canonical
that nGate-verify and v4call's existing federation code both expect).
nGate-verify rejects with "attestation v4call_hive_account tag (X) ≠ post's HIVE-ACCOUNT (Y)"
You signed the attestation with one Hive account name (e.g.
hive-book) but the Hive post declares a different one (e.g.
hive-book.com). Re-sign with consistent values across the
whole flow (server-sign → server-announce → Hive post).
Sidecar can't restart relay ("permission denied" on docker.sock)
Less common, but if the relay box has docker access locked down: either
loosen perms on /var/run/docker.sock (default world-writable
on most distros) OR change restart_command in ngate.yaml to
a noop and restart the relay manually after each sync.
Sidecar in a restart loop
Probably an invalid ngate.yaml or missing
node_modules. Check:
docker compose logs --tail=50 ngate-sync
The startup log shows YAML parse errors clearly. If
node_modules is missing the sidecar will install it on first
start; subsequent runs are fast.
"Restart cap hit" in logs
nGate refuses to restart the relay more than
max_restarts_per_day times in 24h. If you see this
repeatedly, something upstream is causing the whitelist to flap.
Investigate state.json and check whether candidates are missing-then-back
repeatedly (suggests Hive RPC flakiness or a v4call-server post
disappearing temporarily).
"Sanity bound triggered" in logs
nGate refused to apply because the result would have removed every
non-seed entry. Triggered when upstream had errors AND
--allow-removals was somehow set. With the sidecar's
PIPESTATUS-aware logic, this shouldn't happen in practice; if it does,
check upstream phase exit codes.

Removing nGate (back to manual whitelist)

If you want to disable nGate and go back to hand-editing config.toml:

cd /opt/nostr-relay
docker compose down ngate-sync
docker compose rm -f ngate-sync

Then manually edit config.toml:

nano /opt/nostr-relay/config.toml

Either:

  • Delete the BEGIN/END marker comments (keep the
 [authorization] section between them as-is), OR
  • Replace the entire managed block with a hand-curated whitelist.

Restart the relay to pick up the new config:

docker compose restart nostr-relay

The relay reads config.toml on startup regardless of markers. nGate's markers are JUST for nGate's internal "what to manage" logic; the relay itself ignores them as TOML comments.

When you're done

After 24 hours of clean runs, nGate has handled at least one normal cycle (scan + verify + gate + maybe-apply). At that point you can:

  • Stake some HIVE on your test operator to confirm a transition (off-gate
 → passing the gate after stake).
  • Publish a v4call-server post for a new operator and watch it get picked
 up on the next cycle.
  • Deploy nGate to your second relay box (nostr.v4call.com) with a
 staggered scan time (set scan_interval_seconds the same but
 schedule the startup at a different time of day for faster
 federation-wide pickup).

For the next phase of work (stage 3.6 refinements + stage 4 strfry migration), see STATUS.md in the nGate repo.