app: id: nostr-vpn name: Nostr VPN (paid exit) version: 1.0.0 # Pinned commit, not a tag -- upstream has no release tags yet. Re-pin # deliberately in docker/nostr-vpn/Dockerfile's NVPN_COMMIT build arg; see # docs/nostr-vpn-integration-plan.md for the Phase 0 feasibility log this # pin was verified against. upstream: kind: github repo: mmalmi/nostr-vpn description: | Sells spare bandwidth as a Nostr-discovered, Cashu-metered paid exit (github.com/mmalmi/nostr-vpn). Runs rootless in its own network namespace (pasta) -- NET_ADMIN/NET_RAW are scoped to that netns, never the host. Seller mode defaults OFF (upstream's own `paid_exit.enabled` default); turning it on is a separate step (Phase 3 UI, not yet built). This replaces the old root-mode integration (image-recipe's nostr-vpn.service running `nvpn daemon` as root, auto-enabled on first login via rpc/auth.rs) that broke the rootless/no-OS-reliance invariant. That old path and its RPC TOML-rewriting code (rpc/vpn.rs::handle_vpn_add_participant) are a separate, higher-risk removal -- not done here, since it's wired into every node's login flow today, not just this app. category: money container: build: context: /opt/archipelago/docker/nostr-vpn dockerfile: Dockerfile tag: localhost/nostr-vpn:local network: pasta # Image has no image-level ENTRYPOINT/CMD (see Dockerfile) -- both this # app and nostr-vpn-web point the shared seed-config entrypoint at # different binaries/args. entrypoint: ["/usr/local/bin/archy-nvpn-entrypoint.sh"] custom_args: - /usr/local/bin/nvpn - daemon - --config - /data/config/nvpn/config.toml dependencies: - storage: 1Gi resources: memory_limit: 256Mi security: # NET_ADMIN/NET_RAW: TUN device + the exit forwarding/NAT nvpn installs # itself inside its own netns (nvpn-exit-forward-in/out, nvpn-exit-masq, # the MSS clamp) -- confirmed working rootless in Phase 0 testing, with # no capabilities beyond these two plus the sysctl below. Host iptables # and routes were confirmed untouched. capabilities: [NET_ADMIN, NET_RAW] # false: not verified read-only-root-compatible in Phase 0 testing (the # working run flags there didn't include --read-only). nvpn's own state # (config/identity/wallet) lives on the /data volume either way. readonly_root: false no_new_privileges: true network_policy: isolated # Rootless /proc/sys is read-only, so forwarding can only be set at # container-create time via this primitive (added for exactly this app -- # see commit e42bd26). nvpn only *reads* ip_forward and writes it when 0, # so setting it here once at create is enough; nvpn's own cleanup path # leaves it alone. sysctls: net.ipv4.ip_forward: "1" devices: - /dev/net/tun ports: # Paid-exit buyers dial this directly from the open internet to pay for # bandwidth -- it's the whole point of the app, not an admin surface, # and it speaks nvpn's own FIPS UDP wire protocol, not HTTP, so the app # gate cannot front it. 51822, not upstream's default 51820: that # collides with archipelago-wg (kernel WireGuard) on fleet nodes -- # found running both side by side in Phase 0 testing. - host: 51822 container: 51822 protocol: udp auth: none auth_rationale: >- FIPS UDP transport for paid-exit buyers. Anonymous by design (not HTTP), and the seller is off by default (paid_exit.enabled=false) until an operator explicitly turns on selling, so exposure here alone grants no access to anything. volumes: # Adopts whatever a node already has under the old root-mode path # (nostr-vpn.service wrote here too) -- an identity, wallet balance, or # pending Cashu credit must survive this migration, not reset. - type: bind source: /var/lib/archipelago/nostr-vpn target: /data options: [rw] environment: - NVPN_LISTEN_PORT=51822 health_check: type: exec endpoint: nvpn status interval: 30s timeout: 10s retries: 3 metadata: category: money tier: optional author: mmalmi repo: https://github.com/mmalmi/nostr-vpn features: - Sell spare bandwidth as a Cashu-metered Nostr paid exit - Rootless: own network namespace, no host network access - Seller mode off by default