+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU Affero General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License
+ along with this program. If not, see .
+
+Also add information on how to contact you by electronic and paper mail.
+
+ If your software can interact with users remotely through a computer
+network, you should also make sure that it provides a way for users to
+get its source. For example, if your program is a web application, its
+interface could display a "Source" link that leads users to an archive
+of the code. There are many ways you could offer source, and different
+solutions will be better for different programs; see section 13 for the
+specific requirements.
+
+ You should also get your employer (if you work as a programmer) or school,
+if any, to sign a "copyright disclaimer" for the program, if necessary.
+For more information on this, and how to apply and follow the GNU AGPL, see
+.
diff --git a/MAINTAINERS.md b/MAINTAINERS.md
new file mode 100644
index 0000000..564dd2b
--- /dev/null
+++ b/MAINTAINERS.md
@@ -0,0 +1,26 @@
+# Maintainers
+
+## Active maintainers
+
+| Maintainer | Areas | Security role | Status |
+|---|---|---|---|
+| [@s00ly](https://github.com/s00ly) | Repository, architecture, documentation, CI, releases | Private advisory triage and emergency release authority | Active |
+
+NomadPosting currently has one maintainer. `CODEOWNERS` routes review to that
+maintainer but does not create independent review. Live security gates remain
+blocked until a qualified reviewer other than the change author records the
+required evidence.
+
+## Contact boundaries
+
+- Report vulnerabilities through
+ [GitHub Private Vulnerability Reporting](https://github.com/s00ly/nomadposting/security/advisories/new).
+- Report Code of Conduct matters privately to
+ [yloos@protonmail.com](mailto:yloos@protonmail.com).
+- Do not use either reporting channel for the other channel's purpose.
+- Do not send credentials, VPN configurations, signer secrets, private account
+ identifiers, or unredacted captures unless a maintainer explicitly requests
+ the minimum necessary material through an appropriate private channel.
+
+Maintainer nomination, inactivity, removal, conflicts, and succession are
+defined in [GOVERNANCE.md](GOVERNANCE.md).
diff --git a/Makefile b/Makefile
index 2d0753b..dcebb1d 100644
--- a/Makefile
+++ b/Makefile
@@ -1,4 +1,4 @@
-.PHONY: build test vet format-check vuln sbom verify
+.PHONY: build test vet format-check dco-test license vuln sbom verify
build:
go build -trimpath -o bin/ivpn-controller ./cmd/ivpn
@@ -13,6 +13,12 @@ vet:
format-check:
@test -z "$$(gofmt -l ./cmd ./internal)"
+dco-test:
+ bash ./scripts/check-dco-tests.sh
+
+license:
+ bash ./scripts/check-licenses.sh
+
vuln:
go run golang.org/x/vuln/cmd/govulncheck@v1.6.0 ./...
@@ -20,4 +26,4 @@ sbom:
mkdir -p dist
go list -m -json all > dist/go-modules.sbom.json
-verify: format-check test vet vuln sbom
+verify: format-check dco-test test vet license vuln sbom
diff --git a/README.md b/README.md
index f4072f4..4dcaa5b 100644
--- a/README.md
+++ b/README.md
@@ -1,83 +1,207 @@
-# iVPN private cross-poster
+
-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.
+[](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) · [Contributing](CONTRIBUTING.md) · [Governance](GOVERNANCE.md)
-## Architecture
+
+
+> [!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
+
+What you can audit today
-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.
+
+
+
+## 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 . 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
+- [Dependency license review](docs/DEPENDENCY_LICENSES.md): transitive license evidence and scanner decision record
+- [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
+- [Governance](GOVERNANCE.md): decision, maintainer, security-authority, and succession rules
+- [Maintainers](MAINTAINERS.md): current ownership and bus-factor status
+- [Code of Conduct](CODE_OF_CONDUCT.md): community standards and private enforcement contact
+- [Name and provenance](BRAND_POLICY.md): accurate fork and official-build representation
+
+## License and contributions
-## Production blockers
+NomadPosting is licensed under the [GNU Affero General Public License,
+version 3 or later](LICENSE), identified by SPDX as `AGPL-3.0-or-later`.
+Modified versions used to provide network interaction must offer their
+corresponding source to those users as required by the license.
-- 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.
+Contributions use the same license and the [Developer Certificate of Origin
+1.1](DCO). Every commit requires an author-matching `Signed-off-by:` trailer.
+The project requires neither a CLA nor copyright assignment. See
+[CONTRIBUTING.md](CONTRIBUTING.md) before submitting changes.
-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.
diff --git a/docs/DEPENDENCY_LICENSES.md b/docs/DEPENDENCY_LICENSES.md
new file mode 100644
index 0000000..eaa0c39
--- /dev/null
+++ b/docs/DEPENDENCY_LICENSES.md
@@ -0,0 +1,69 @@
+# Dependency License Review
+
+Date: 2026-07-15
+
+NomadPosting is licensed under `AGPL-3.0-or-later`. This review covers every Go
+module used by `./...` at the versions pinned in `go.mod` and `go.sum`.
+Permitted dependency licenses are `Apache-2.0`, `BSD-2-Clause`,
+`BSD-3-Clause`, and `MIT`; each is compatible with GPLv3-family distribution.
+The project license itself is also allowed as `AGPL-3.0` because license-text
+classifiers cannot encode the separate "or later" grant stated in the README.
+Compatibility was cross-checked against the GNU project's
+[license list](https://www.gnu.org/licenses/license-list.html).
+
+## Automated evidence
+
+Run:
+
+```sh
+bash ./scripts/check-licenses.sh
+```
+
+The script pins `google/go-licenses` at `v2.0.1` and rejects every unapproved or
+unknown detected license. There are no ignored modules or license exceptions.
+
+## Reviewed modules
+
+| License | Modules |
+|---|---|
+| Apache-2.0 | `github.com/google/go-tpm v0.9.8` |
+| BSD-2-Clause | `github.com/go-webauthn/x v0.2.6` includes the `revoke` package under this license |
+| BSD-3-Clause | `github.com/go-webauthn/webauthn v0.17.4`, the remaining used packages from `github.com/go-webauthn/x v0.2.6`, `github.com/google/uuid v1.6.0`, `github.com/remyoudompheng/bigfft` at `24d4a6f8daec`, `golang.org/x/crypto v0.52.0`, `golang.org/x/sys v0.45.0`, `modernc.org/libc v1.73.4`, `modernc.org/mathutil v1.7.1`, `modernc.org/memory v1.11.0`, `modernc.org/sqlite v1.53.0` |
+| MIT | `github.com/dustin/go-humanize v1.0.1`, `github.com/fxamacker/cbor/v2 v2.9.2`, `github.com/go-viper/mapstructure/v2 v2.5.0`, `github.com/golang-jwt/jwt/v5 v5.3.1`, `github.com/mattn/go-isatty v0.0.20`, `github.com/ncruces/go-strftime v1.0.0`, `github.com/philhofer/fwd v1.2.0`, `github.com/tinylib/msgp v1.6.4`, `github.com/x448/float16 v0.8.4` |
+
+`modernc.org/libc` also carries notices for incorporated Go, musl, go-netdb,
+and NixOS/nixpkgs material under BSD-style, MIT, or public-domain terms. The
+SQLite material included by `modernc.org/sqlite` is public domain. No reviewed
+dependency imposes a copyleft term that conflicts with AGPL distribution.
+
+Any dependency or scanner update requires a fresh report and manual review of
+new, changed, unknown, or multi-license results.
+
+## Scanner decision record
+
+The scanner is a CI-only build tool. It is not linked into NomadPosting or its
+release artifacts.
+
+| Option | Coverage | Reproducibility | Supply-chain exposure | Score |
+|---|---:|---:|---:|---:|
+| [`google/go-licenses v2.0.1`](https://github.com/google/go-licenses) | 4/5 | 4/5 | 3/5 | **11/15** |
+| GitHub enterprise license policy | 4/5 | 2/5 | 4/5 | 10/15 |
+| Manual review only | 2/5 | 2/5 | 5/5 | 9/15 |
+| New custom classifier | 2/5 | 3/5 | 4/5 | 9/15 |
+
+Decision: use `google/go-licenses v2.0.1` with no exceptions. It provides
+locally reproducible transitive analysis without requiring an enterprise
+GitHub feature. Version 1.6.0 was rejected after its binary vulnerability scan
+found 17 reachable vulnerabilities, including legacy `go-git` path-traversal
+and remote-code-execution advisories. The selected v2.0.1 binary scan found zero
+reachable vulnerabilities; nine module-level advisories were present only in
+code paths the binary does not call. A custom classifier would create more
+security and maintenance risk than it removes.
+
+Blast radius: the scanner executes third-party Go code during CI and can read
+the checked-out repository and make network requests while Go downloads its
+pinned modules. Controls are an exact released version, public Go checksum
+verification, a read-only workflow token, disabled checkout credentials, no CI
+secrets, a strict license allowlist, and a five-minute step within the existing
+twenty-minute job. Replace or remove the scanner if it becomes unmaintained or
+its binary vulnerability scan gains a reachable finding.
diff --git a/docs/SECURITY.md b/docs/SECURITY.md
index 05a3ce5..fb74a8e 100644
--- a/docs/SECURITY.md
+++ b/docs/SECURITY.md
@@ -1,13 +1,68 @@
# Security Requirements and Operating Policy
-This document defines the minimum security posture for iVPN. It is normative for implementation and release. These are requirements, not evidence that a deployment satisfies them. The fuller threat analysis is in [THREAT_MODEL.md](THREAT_MODEL.md).
+This document defines the minimum security posture for NomadPosting. It is normative for implementation and release. These are requirements, not evidence that a deployment satisfies them. The fuller threat analysis is in [THREAT_MODEL.md](THREAT_MODEL.md).
## Supported use
-iVPN supports one operator publishing approved content to one X account through a dedicated France exit and one Nostr identity through rotating, allowlisted VPN countries. It is a source-IP privacy tool, not an anonymity system or an enforcement-evasion tool.
+NomadPosting supports one operator publishing approved content to one X account through a dedicated France exit and one Nostr identity through rotating, allowlisted VPN countries. It is a source-IP privacy tool, not an anonymity system or an enforcement-evasion tool.
The implementation must not support account fleets, third-party accounts, platform limit circumvention, browser automation of X, session-cookie reuse, misleading location claims, or silent network fallback.
+## Supported versions and official scope
+
+| Version | Security support | Live status |
+|---|---|---|
+| `main` | Accepted for private vulnerability reports | Dry-run research preview only |
+| Tagged releases | None exist | Unsupported |
+| Forks and third-party deployments | Controlled by their operators | Not official or certified by this project |
+
+This policy governs the `s00ly/nomadposting` repository and artifacts explicitly
+published from it. It does not add a restriction to the AGPL or certify a fork.
+The [name and provenance policy](../BRAND_POLICY.md) describes accurate use of
+project identity.
+
+No live deployment is supported. A report showing that a missing live feature
+is missing is not a vulnerability unless dry-run behavior creates a separate
+security impact. Reports that challenge a release assumption are welcome when
+they follow the research boundaries below.
+
+## Vulnerability reporting and research boundaries
+
+Use [GitHub Private Vulnerability Reporting](https://github.com/s00ly/nomadposting/security/advisories/new)
+for exploitable details. Public issues are appropriate only for
+non-exploitable design questions, threat-model hypotheses, and hardening
+proposals.
+
+This project can authorize research only against its own code and
+maintainer-controlled disposable environments. It does not authorize testing
+against X, Nostr relays, VPN providers, cloud providers, accounts, networks, or
+other third-party systems. Obtain separate permission from each owner.
+
+When testing this project:
+
+- use synthetic data, disposable keys, local fixtures, and test accounts you
+ own;
+- do not access another person's data, credentials, account, or traffic;
+- do not use denial of service, social engineering, persistence, credential
+ harvesting, or automated scanning of public infrastructure;
+- stop when a test could affect availability, disclose data, or leave the
+ disposable environment;
+- retain only the minimum sanitized evidence needed to reproduce the issue and
+ delete sensitive research artifacts after coordination.
+
+There is no bug bounty, guaranteed safe harbor, or response-time SLA. The
+maintainer targets acknowledgement within seven calendar days and an initial
+severity assessment within fourteen. These are best-effort targets. Reporter
+credit is offered only with the reporter's consent.
+
+For security-sensitive fixes, use a GitHub draft advisory and its private fork.
+Do not open a public issue or pull request containing exploit details before
+coordinated disclosure. The maintainer decides whether an advisory or CVE is
+appropriate based on affected users and release state. Under the AGPL, source
+for a modified network-served fix must be available to remote users no later
+than deployment of that fix. Corresponding source never includes production
+credentials or private operational configuration.
+
## Mandatory architecture
- Linux is the security baseline for the first release.
@@ -160,7 +215,24 @@ 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.
+
+## Security-maintainer authority
+
+The active security maintainer is listed in
+[MAINTAINERS.md](../MAINTAINERS.md). The project currently has one security
+maintainer, which is an explicit bus-factor limitation.
+
+The security maintainer may access private advisories, invite a minimum set of
+qualified reviewers, merge embargoed fixes, stop publication, revoke
+compromised artifacts, coordinate disclosure, and require credential rotation.
+Any medium-or-higher finding resets the affected verification work and all
+three production adversarial rounds. No administrator may waive that reset by
+merging, changing a label, or editing the verification record.
+
+Code of Conduct reports use the separate private channel defined in
+[CODE_OF_CONDUCT.md](../CODE_OF_CONDUCT.md). Vulnerability reporting must not be used for
+interpersonal or community-moderation complaints.
## Release gates
diff --git a/docs/VERIFICATION.md b/docs/VERIFICATION.md
index 5056c9a..4f516af 100644
--- a/docs/VERIFICATION.md
+++ b/docs/VERIFICATION.md
@@ -1,6 +1,6 @@
# Verification Record
-Date: 2026-07-14
+Date: 2026-07-16
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
@@ -11,13 +11,17 @@ VPN cross-poster, and live mode is deliberately rejected at configuration load.
| Check | Result | Evidence |
|---|---|---|
| Go version | PASS | Pinned Go 1.26.5 toolchain used for every command below. |
+| Project license | PASS | `LICENSE` matches the canonical GNU AGPL v3 text; the README applies `AGPL-3.0-or-later`. `DCO` matches the canonical DCO 1.1 text. Both comparisons normalized line endings only. |
+| DCO enforcement | PASS | `scripts/check-dco-tests.sh` accepted signed author and co-author fixtures; rejected unsigned authors, unsigned co-authors, unsigned post-adoption commits, and an empty range; and proved that commits predating DCO adoption are not retroactively rejected. The real branch range also passed from the DCO adoption boundary. Both pinned workflows run the self-tests. |
+| Community governance | PASS LOCALLY | Relative Markdown links resolve. Governance, maintainer, conduct, brand, contribution, ownership, issue-form, pull-request, and security-reporting policies are present and agree on the single-maintainer and dry-run boundaries. Remote GitHub detection and workflow evidence are required after push. |
+| Dependency licenses | PASS | `bash ./scripts/check-licenses.sh` accepted only AGPL-3.0, Apache-2.0, BSD-2-Clause, BSD-3-Clause, and MIT with no ignored modules or exceptions. See [DEPENDENCY_LICENSES.md](DEPENDENCY_LICENSES.md). |
| Formatting | PASS | `gofmt -d cmd internal` returned no diff. |
| Module integrity | PASS | `go mod verify` returned `all modules verified`. |
| Complete test suite | PASS | `go test -count=1 ./...` and a final `go test -count=3 ./...` passed all 10 packages. The repository contains 59 named tests. |
| 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
diff --git a/scripts/check-dco-tests.sh b/scripts/check-dco-tests.sh
new file mode 100755
index 0000000..e10eed6
--- /dev/null
+++ b/scripts/check-dco-tests.sh
@@ -0,0 +1,81 @@
+#!/usr/bin/env bash
+set -euo pipefail
+
+script_dir=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)
+checker="${script_dir}/check-dco.sh"
+fixture=$(mktemp -d)
+trap 'rm -rf "$fixture"' EXIT
+
+git -C "$fixture" init --quiet
+git -C "$fixture" config user.name "Test Author"
+git -C "$fixture" config user.email "author@example.invalid"
+git -C "$fixture" config core.autocrlf false
+
+run_check() {
+ (cd "$fixture" && bash "$checker" "$@")
+}
+
+printf 'root\n' >"${fixture}/fixture.txt"
+git -C "$fixture" add fixture.txt
+git -C "$fixture" commit --quiet -m "Root fixture"
+base=$(git -C "$fixture" rev-parse HEAD)
+
+printf 'before adoption\n' >>"${fixture}/fixture.txt"
+git -C "$fixture" add fixture.txt
+git -C "$fixture" commit --quiet -m "Unsigned pre-adoption fixture"
+pre_adoption=$(git -C "$fixture" rev-parse HEAD)
+
+if run_check "$base" "$pre_adoption" >/dev/null 2>&1; then
+ echo "DCO test failure: a range without DCO unexpectedly passed" >&2
+ exit 1
+fi
+
+cp "${script_dir}/../DCO" "${fixture}/DCO"
+git -C "$fixture" add DCO
+git -C "$fixture" commit --quiet --signoff -m "Adopt DCO"
+adoption=$(git -C "$fixture" rev-parse HEAD)
+
+run_check "$base" "$adoption" >/dev/null
+
+printf 'signed\n' >>"${fixture}/fixture.txt"
+git -C "$fixture" add fixture.txt
+git -C "$fixture" commit --quiet --signoff -m "Signed fixture"
+signed=$(git -C "$fixture" rev-parse HEAD)
+
+run_check "$adoption" "$signed" >/dev/null
+
+printf 'coauthored\n' >>"${fixture}/fixture.txt"
+git -C "$fixture" add fixture.txt
+git -C "$fixture" commit --quiet --signoff \
+ -m "Unsigned co-author fixture" \
+ -m "Co-authored-by: Co Author "
+unsigned_coauthor=$(git -C "$fixture" rev-parse HEAD)
+
+if run_check "$signed" "$unsigned_coauthor" >/dev/null 2>&1; then
+ echo "DCO test failure: an unsigned co-author unexpectedly passed" >&2
+ exit 1
+fi
+
+git -C "$fixture" commit --quiet --amend \
+ -m "Signed co-author fixture" \
+ -m $'Signed-off-by: Test Author \nCo-authored-by: Co Author \nSigned-off-by: Co Author '
+signed_coauthor=$(git -C "$fixture" rev-parse HEAD)
+
+run_check "$signed" "$signed_coauthor" >/dev/null
+
+printf 'unsigned after adoption\n' >>"${fixture}/fixture.txt"
+git -C "$fixture" add fixture.txt
+git -C "$fixture" commit --quiet -m "Unsigned post-adoption fixture"
+unsigned=$(git -C "$fixture" rev-parse HEAD)
+
+if run_check "$signed_coauthor" "$unsigned" >/dev/null 2>&1; then
+ echo "DCO test failure: an unsigned post-adoption commit unexpectedly passed" >&2
+ exit 1
+fi
+
+if run_check "$signed" "$signed" >/dev/null 2>&1; then
+ echo "DCO test failure: an empty range unexpectedly passed" >&2
+ exit 1
+fi
+
+echo "DCO self-tests passed."
diff --git a/scripts/check-dco.sh b/scripts/check-dco.sh
new file mode 100755
index 0000000..452ea17
--- /dev/null
+++ b/scripts/check-dco.sh
@@ -0,0 +1,101 @@
+#!/usr/bin/env bash
+set -euo pipefail
+
+if [[ $# -ne 2 ]]; then
+ echo "usage: $0 " >&2
+ exit 2
+fi
+
+base=$1
+head=$2
+
+git rev-parse --verify "${base}^{commit}" >/dev/null
+git rev-parse --verify "${head}^{commit}" >/dev/null
+
+if ! git merge-base --is-ancestor "$base" "$head"; then
+ echo "DCO failure: base ${base} is not an ancestor of head ${head}" >&2
+ exit 1
+fi
+
+if ! git cat-file -e "${head}:DCO" 2>/dev/null; then
+ echo "DCO failure: head ${head} does not contain the DCO policy" >&2
+ exit 1
+fi
+
+range="${base}..${head}"
+
+# A pull request that adopts the DCO cannot retroactively certify earlier
+# commits. Start enforcement at the first commit in the range that introduces
+# the DCO file. Once the base branch contains DCO, every new commit is checked.
+if ! git cat-file -e "${base}:DCO" 2>/dev/null; then
+ adoption=$(git rev-list --reverse "$range" -- DCO)
+ adoption=${adoption%%$'\n'*}
+ if [[ -z "$adoption" ]]; then
+ echo "DCO failure: no DCO adoption commit found in ${range}" >&2
+ exit 1
+ fi
+
+ adoption_parent=$(git rev-parse "${adoption}^" 2>/dev/null || true)
+ if [[ -n "$adoption_parent" ]]; then
+ range="${adoption_parent}..${head}"
+ else
+ range="$head"
+ fi
+
+ echo "DCO adoption detected at ${adoption}; earlier commits are outside the policy."
+fi
+
+failed=0
+checked=0
+
+while IFS= read -r commit; do
+ [[ -n "$commit" ]] || continue
+ checked=$((checked + 1))
+
+ author_name=$(git show -s --format=%an "$commit")
+ author_email=$(git show -s --format=%ae "$commit")
+ expected="${author_name} <${author_email}>"
+ required=("$expected")
+ signoffs=()
+
+ while IFS= read -r trailer; do
+ key=${trailer%%:*}
+ value=${trailer#*:}
+ value=${value#"${value%%[![:space:]]*}"}
+
+ case "${key,,}" in
+ co-authored-by)
+ required+=("$value")
+ ;;
+ signed-off-by)
+ signoffs+=("$value")
+ ;;
+ esac
+ done < <(git show -s --format=%B "$commit" | git interpret-trailers --parse)
+
+ for identity in "${required[@]}"; do
+ matched=0
+ for signoff in "${signoffs[@]}"; do
+ if [[ "${signoff,,}" == "${identity,,}" ]]; then
+ matched=1
+ break
+ fi
+ done
+
+ if [[ $matched -ne 1 ]]; then
+ echo "DCO failure: ${commit} needs 'Signed-off-by: ${identity}'" >&2
+ failed=1
+ fi
+ done
+done < <(git rev-list --reverse "$range")
+
+if [[ $checked -eq 0 ]]; then
+ echo "DCO failure: no commits found in ${range}" >&2
+ exit 1
+fi
+
+if [[ $failed -ne 0 ]]; then
+ exit 1
+fi
+
+echo "DCO check passed for ${checked} commit(s)."
diff --git a/scripts/check-licenses.sh b/scripts/check-licenses.sh
new file mode 100644
index 0000000..1f7e262
--- /dev/null
+++ b/scripts/check-licenses.sh
@@ -0,0 +1,10 @@
+#!/usr/bin/env bash
+set -euo pipefail
+
+readonly scanner='github.com/google/go-licenses/v2@v2.0.1'
+readonly allowed='AGPL-3.0,Apache-2.0,BSD-2-Clause,BSD-3-Clause,MIT'
+
+go run "$scanner" check ./... \
+ --allowed_licenses="$allowed"
+
+echo "Dependency-license check passed with no exceptions."