Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
209 changes: 160 additions & 49 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,83 +1,194 @@
# iVPN private cross-poster
<div align="center">

iVPN is a single-operator Go control plane for approving one exact text payload for X, Nostr, or both. It is designed around fail-closed egress, private routing metadata, and honest location semantics.
# NomadPosting

**Current safety state: dry-run only.** The application refuses `IVPN_DRY_RUN=false`. X OAuth, the official X create-post connector, NIP-46 boundaries, Nostr quorum logic, country selection, the broker contract, and infrastructure topology exist, but real Linux namespace execution is deliberately disabled until packet-capture evidence proves there is no direct fallback.
**Building a cross-poster that fails closed.**

VPN egress changes network origin. It does not hide X credentials, a Nostr pubkey, content, timing, or account identity, and the app never claims the operator was physically in an exit country.
[![verify](https://github.com/s00ly/nomadposting/actions/workflows/ci.yml/badge.svg)](https://github.com/s00ly/nomadposting/actions/workflows/ci.yml)

## What is implemented
Proof-oriented cross-posting research for Nostr and X, written in Go.

- Go 1.26.5 control plane with server-rendered HTML and SQLite WAL.
- Two-passkey production readiness policy, WebAuthn ceremonies, strict sessions, and CSRF checks.
- AES-256-GCM envelope encryption with a fresh data key per draft, auth record, receipt, audit event, and X OAuth token record.
- One-time offline recovery-code rotation and failure rate limiting; the code is shown only when generated.
- Exact normalized-payload preview, SHA-256 approval binding, optional scheduling, cancellation, emergency stop, and manual reconciliation without blind resend.
- Official `POST https://api.x.com/2/tweets` connector, fixed endpoint, no `geo`, bounded responses, one refresh after a definitive 401, 429 reset handling, and `UNKNOWN` on ambiguous transport results.
- OAuth 2.0 Authorization Code with S256 PKCE using only `tweet.read`, `tweet.write`, `users.read`, and `offline.access`; encrypted token persistence and official revocation.
- NIP-01 canonical event IDs, NIP-46 kind restrictions, NIP-42 relay/challenge binding, one signature reused across three reviewed relays, and a two-relay quorum.
- CSPRNG Nostr country selection, no adjacent country repeat, three-country minimum, and a fixed France policy for X.
- Registry-only broker requests. Arbitrary commands, routes, paths, addresses, and shell fragments are not representable.
- Orange, purple, and graphite-gray cypherpunk UI with system fonts, visible focus, reduced motion, label-plus-color states, and no pre-dispatch country disclosure.
- Seven-day deletion of resolved job metadata and receipts while unresolved `UNKNOWN` and `PARTIAL` jobs remain available for reconciliation.
[Threat model](docs/THREAT_MODEL.md) · [Security policy](docs/SECURITY.md) · [Verification record](docs/VERIFICATION.md) · [Deployment boundary](deploy/DEPLOYMENT.md)

## Architecture
</div>

> [!CAUTION]
> **Research preview: dry-run only.** NomadPosting cannot publish posts or protect live credentials today. The controller rejects `IVPN_DRY_RUN=false`, and the privileged network executor is deliberately disabled until Linux packet captures prove zero direct fallback. Do not use real credentials.

NomadPosting is a security-first control plane for approving one exact text payload for Nostr, X, or both. The design aims to route each platform through policy-bound VPN egress without turning failure, ambiguity, or exit-country metadata into false privacy claims.

We are hardening the design in public so privacy engineers, Linux networking specialists, Nostr implementers, Go reviewers, accessibility experts, and adversarial testers can turn explicit release gates into reproducible proof.

## Why this exists

Cross-posting is easy. Proving that a publisher did not escape a failed VPN, duplicate an ambiguous request, leak credentials into logs, or imply a false physical location is harder.

NomadPosting turns those failure modes into explicit state transitions, testable security properties, and release gates. Freedom technology should make fewer promises and provide better evidence.

## Privacy boundary

A VPN changes network origin, not identity. X credentials, a Nostr public key, content, timing, VPN-provider metadata, and public activity remain correlatable. An exit country is routing metadata, never proof of the operator's physical location.

| The design aims to | It does not promise |
|---|---|
| Bind human approval to the exact payload and destinations | Anonymity, unlinkability, or untraceability |
| Fail closed when an isolated tunnel or route is unhealthy | Protection from a compromised host, signer, gateway, or VPN provider |
| Keep future Nostr country choices private | A physical-location claim or false geotag |
| Preserve `UNKNOWN` after ambiguous delivery instead of blindly resending | Atomic publication or rollback across X and Nostr |
| Keep X on one stable, dedicated exit | Platform-enforcement, rate-limit, or geographic-control evasion |

There is no browser posting, cookie reuse, CAPTCHA handling, residential proxy support, account fleet, or error-triggered IP switching.

## Current status

| Area | State |
|---|---|
| Approval, scheduling, encrypted content, audit, cancellation, and emergency stop | Implemented and tested |
| X and Nostr protocol/state modules | Implemented for dry-run verification; not wired to live transports |
| Nostr country-selection policy and typed broker boundary | Implemented and tested |
| Linux network namespaces, WireGuard, nftables, and validating DNS | Not implemented |
| Broker-backed dispatcher and live publication | Intentionally disabled |
| Production readiness | **No** |

The exact evidence, limitations, and unresolved gates are recorded in the [verification record](docs/VERIFICATION.md).

## Help harden NomadPosting

This project needs reviewers more than cheerleaders.

| Priority | Work that needs proof |
|---|---|
| P0 | Per-attempt Linux network namespaces, WireGuard, nftables, validating DNS, teardown, and physical-interface packet captures under tunnel loss |
| P0 | Broker-backed dispatcher with platform-pinned HTTP and WebSocket transports |
| P0 | Concrete NIP-46 transport, signer binding, permission enforcement, and BIP-340/Schnorr verification |
| P1 | Crash recovery and same-exit X reconciliation without blind duplicates |
| P1 | TPM or operating-system key sealing and encryption of currently plaintext operational indexes |
| P1 | NIP-11/NIP-65 relay review, X weighted-length conformance, and provisioned gateway health checks |
| P2 | Complete keyboard, screen-reader, 200% zoom, reduced-motion, and visual-regression testing |

Good starting points:

- challenge a trust boundary or untested assumption in the [threat model](docs/THREAT_MODEL.md);
- reproduce one of the [code-level adversarial rounds](docs/VERIFICATION.md#code-level-adversarial-rounds);
- turn a release blocker into a failing-before, passing-after test;
- review the egress selector, state machine, secret handling, or accessible UI without using live credentials.

Every privacy claim needs a named threat, a trust boundary, and a reproducible test. Medium-or-higher findings reopen the affected adversarial rounds. No `xfail`, `--no-verify`, swallowed errors, hardcoded bypasses, or unpinned dependencies.

## Target architecture

Purple nodes exist in the dry-run codebase. Orange dashed nodes are blocked release work.

```mermaid
flowchart LR
B["Management-VPN browser"] --> C["Unprivileged Go controller"]
C --> D["Encrypted SQLite WAL"]
C --> S["NIP-46 remote signer"]
C --> K["Root-owned broker socket"]
K --> X["Pinned France namespace for X"]
K --> N["Random eligible namespace for Nostr"]
X --> XA["Official X API"]
N --> R["Three reviewed wss relays"]
C --> D["SQLite WAL + envelope-encrypted sensitive records"]
C --> P["Testable X and Nostr protocol modules"]
C --> K["Typed broker boundary"]
P -. "transport missing" .-> S["NIP-46 signer"]
K -. "executor missing" .-> N["Fresh network namespace + WireGuard"]
N -.-> X["Dedicated France exit → official X API"]
N -.-> R["Random eligible Nostr exit → configured relays"]

classDef built fill:#211F28,stroke:#9B6DFF,color:#ECEAF0,stroke-width:2px;
classDef gate fill:#17161C,stroke:#FF731A,color:#ECEAF0,stroke-width:2px,stroke-dasharray:5 4;
class B,C,D,P,K built;
class S,N,X,R gate;
```

The target diagram is not a claim that the disabled namespace executor is complete. See [deployment gates](deploy/DEPLOYMENT.md) and the [threat model](docs/THREAT_MODEL.md).
The diagram is a target, not a deployment claim. X is intentionally pinned to one stable France exit. Only Nostr is designed for country rotation, and production rotation still depends on the missing isolation proof.

## Local dry-run
<details>
<summary><strong>What you can audit today</strong></summary>

Install Go 1.26.5, then in PowerShell:
- Go 1.26.5 server-rendered control plane with SQLite WAL.
- Two-passkey WebAuthn policy, CSRF-bound sessions, offline recovery-code rotation, and rate limiting.
- AES-256-GCM envelope encryption for content, credentials, OAuth tokens, receipts, auth records, and audit details. Job state, timestamps, destination flags, and payload hashes remain plaintext indexes.
- Exact normalized-payload preview and SHA-256 approval binding across content, destinations, and schedule.
- Explicit `DRAFT → APPROVED → ROUTING → PUBLISHING → COMPLETE | PARTIAL | FAILED | UNKNOWN` transitions.
- Testable official X API and OAuth 2.0 PKCE modules with fixed endpoints, bounded responses, no `geo`, one refresh after a definitive 401, and no automatic resend after an ambiguous transmission.
- NIP-01 event construction, NIP-46 kind restrictions, NIP-42 challenge binding, exact signed-event reuse, three configured relay identities, and modeled two-ack quorum. Production relay review and onboarding are not implemented.
- CSPRNG Nostr country selection, no adjacent-country repeat, three-healthy-country minimum, independent endpoint selection, and stable X egress policy.
- Registry-only broker requests: arbitrary commands, paths, routes, addresses, and shell fragments are not representable.
- Orange, purple, and graphite-gray cypherpunk UI with visible focus, reduced motion, label-plus-color states, and no pre-dispatch country disclosure.

</details>

## Five-minute dry run

Requirements: [Go 1.26.5](https://go.dev/doc/install) and a local browser. This mode is for UI and state-flow review only.

### Linux or macOS

```sh
git clone https://github.com/s00ly/nomadposting.git
cd nomadposting

export IVPN_MASTER_KEY="$(go run ./cmd/ivpn --generate-master-key)"
export IVPN_DEV_MODE=true
export IVPN_ORIGIN=http://localhost:8443
export IVPN_RPID=localhost
export IVPN_DRY_RUN=true

go run ./cmd/ivpn
```

### PowerShell

```powershell
go build -o bin/ivpn.exe ./cmd/ivpn
$env:IVPN_MASTER_KEY = (& .\bin\ivpn.exe --generate-master-key)
git clone https://github.com/s00ly/nomadposting.git
Set-Location nomadposting

$env:IVPN_MASTER_KEY = (& go run ./cmd/ivpn --generate-master-key)
$env:IVPN_DEV_MODE = 'true'
$env:IVPN_ORIGIN = 'http://localhost:8443'
$env:IVPN_RPID = 'localhost'
$env:IVPN_DRY_RUN = 'true'
.\bin\ivpn.exe
```

Open `http://127.0.0.1:8443`. Development mode bypasses passkey enforcement so local UI and state flows can be inspected. It must never be used for live credentials or publishing.
go run ./cmd/ivpn
```

Production configuration also requires TLS certificate/key paths, a random bootstrap token of at least 32 characters, an HTTPS origin, and host/TPM-backed secret delivery. Prefer `IVPN_MASTER_KEY_FILE` and `IVPN_BOOTSTRAP_TOKEN_FILE`; the app rejects relative, oversized, symlinked, or group/world-readable files on Linux. `.env` files are excluded from Git and are not an approved production secret store.
Open <http://127.0.0.1:8443>. Development mode bypasses passkey enforcement and must never be used with live credentials. Runtime data is written under the ignored `data/` directory.

`IVPN_X_ESTIMATED_CHARGE` is operator-supplied from the account's current X billing portal. If absent, the approval preview says the estimate is unavailable. The code does not invent a price.
The `IVPN_*` prefix is a legacy internal name in the prototype configuration and is expected to change before a release.

## Verification
## Verify the code

```sh
go mod verify
go test -race -count=1 ./...
go test -count=1 ./...
go vet ./...
go run golang.org/x/vuln/cmd/govulncheck@v1.6.0 ./...
```

The CI workflow pins its GitHub Actions to exact upstream tag commits and pins govulncheck to v1.6.0. See [verification evidence and unresolved gates](docs/VERIFICATION.md).
GitHub Actions additionally runs the race detector on Ubuntu and generates a module inventory for SBOM input. Actions and scanner versions are pinned in [`.github/workflows/ci.yml`](.github/workflows/ci.yml).

## Report a vulnerability

Do not put exploitable details, credentials, VPN configuration, account identifiers, or canary values in a public issue. Use [GitHub Private Vulnerability Reporting](https://github.com/s00ly/nomadposting/security/advisories/new). Public issues are appropriate for non-exploitable design questions, threat-model challenges, and hardening proposals.

No production release exists. Reports against `main` are accepted. Read the [security policy](docs/SECURITY.md) before testing.

## Project principles

- **Fail closed.** Missing proof disables live behavior.
- **Approval is exact.** Any payload or destination change invalidates consent.
- **Ambiguity is a state.** A timeout after transmission is not permission to resend.
- **Location is not identity.** Exit-country metadata never becomes a physical-presence claim.
- **Official protocols only.** No browser automation, session-cookie reuse, or platform-control workarounds.
- **Evidence over adjectives.** Builds are useful; packet captures, fault injection, and reproducible negative tests are release evidence.

## Documentation map

- [Threat model](docs/THREAT_MODEL.md): assets, actors, trust boundaries, abuse cases, and adversarial rounds
- [Security policy](docs/SECURITY.md): normative controls, secret handling, logging, retention, and release gates
- [Verification record](docs/VERIFICATION.md): passed checks, known findings, and explicit NO-GO decisions
- [Deployment boundary](deploy/DEPLOYMENT.md): disabled broker and Linux release gate
- [Architecture decisions](docs/adr): product boundary, location ethics, and retention
- [Infrastructure topology](infra/README.md): credential-free OpenTofu manifest, not provisioned infrastructure

## Production blockers
## License status

- No reviewed Linux namespace/WireGuard/nftables executor yet.
- No packet-capture proof for tunnel removal during DNS, TLS, OAuth refresh, signing, transmit, or response handling.
- No broker-backed dispatcher or pinned per-platform network transports. Approved jobs remain queued, and X OAuth/publishing stays unavailable.
- No provisioned cloud gateways, validating resolvers, static IP verification, or provider budget alerts.
- No concrete NIP-46 transport or BIP-340/Schnorr verification adapter.
- No TPM/host-secret-store sealing adapter; strict secret-file loading alone is not sealing.
- Exact job state, timestamps, destination flags, and payload hashes remain plaintext SQLite indexes; other sensitive records are envelope-encrypted.
- No live X reconciliation worker, live relay review, complete accessibility audit, or seven-day canary.
- No tested-account evidence or written X clarification for per-post country rotation. X remains pinned to France by policy.
No license has been selected yet. The source is public for inspection, but this repository is **not legally open source** until an OSI-approved license grants permission to use, modify, and redistribute it. Please open design reviews and test proposals, but avoid code contributions that create authorship ambiguity until the licensing decision is recorded.

These are fail-closed blockers, not follow-up polish. Live mode stays disabled until all release gates in [SECURITY.md](docs/SECURITY.md) pass three clean adversarial rounds.
If this threat model matters to you, star the repository so more privacy and security reviewers can find the work.
2 changes: 1 addition & 1 deletion docs/SECURITY.md
Original file line number Diff line number Diff line change
Expand Up @@ -160,7 +160,7 @@ On suspected token, signer, or VPN-key compromise:
6. Correct the root cause, not only the exposed credential.
7. Rerun all three adversarial rounds before restoring unattended operation.

Do not place real secrets in a public issue. Until a private reporting channel is published, use the repository host's private security-advisory mechanism if available and share only redacted reproduction details.
Do not place real secrets or exploitable details in a public issue. Use [GitHub Private Vulnerability Reporting](https://github.com/s00ly/nomadposting/security/advisories/new) and share only the minimum redacted reproduction material needed to investigate. No production release exists; reports against `main` are accepted. Public issues are appropriate for non-exploitable design questions and hardening proposals.

## Release gates

Expand Down
4 changes: 2 additions & 2 deletions docs/VERIFICATION.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Verification Record

Date: 2026-07-14
Date: 2026-07-15

This record separates code-level evidence from production network evidence. The
repository is a dry-run control plane and safety scaffold. It is not a working
Expand All @@ -17,7 +17,7 @@ VPN cross-poster, and live mode is deliberately rejected at configuration load.
| Static analysis | PASS | `go vet ./...` returned no findings. |
| Reachable vulnerability scan | PASS WITH NOTE | `govulncheck v1.6.0` found zero symbol- or package-level vulnerabilities. See dependency note below. |
| Linux build | PASS | Both `cmd/ivpn` and `cmd/netbroker` cross-built for `linux/amd64` with `CGO_ENABLED=0` and `-trimpath`. |
| Race detector | NOT RUN LOCALLY | This Windows environment has no CGO compiler. `go test -race` correctly failed with `-race requires cgo`. The pinned Ubuntu CI workflow is the required race gate, but no remote CI result exists yet. |
| Race detector | PASS IN CI; NOT RUN LOCALLY | This Windows environment has no CGO compiler. `go test -race` correctly failed locally with `-race requires cgo`. The pinned Ubuntu workflow passed the race suite in [run 29382281777](https://github.com/s00ly/nomadposting/actions/runs/29382281777). |
| OpenTofu validation | NOT RUN | OpenTofu is not installed in this environment. The infrastructure directory is a topology manifest, not resource definitions. |

## Code-level adversarial rounds
Expand Down