Determine a request came from the proxy
Goal
Reject traffic that hits your origin without going through swarmvia. When Origin verification is enabled, swarmvia signs every forwarded request; your origin checks that signature before doing work.
Reference for headers and limits: Access → Origin verification.
Enable signing
- Open Sources → Access → Origin verification.
- Enable it and copy the
osk_…secret once. - Store it as
NTHPOOL_ORIGIN_SIGNING_SECRET(or equivalent) on the origin. - Deploy verification that reads the raw body bytes before JSON parsers or decompressors rewrite them.
The dashboard source page also has language-specific verify snippets under the examples panel.
What to verify
On each inbound request at the origin:
- Require
X-Swarmvia-Timestamp,X-Swarmvia-Nonce, andX-Swarmvia-Signature. - Reject if the timestamp is outside ±300 seconds.
- Let
nbe the raw body length. IfContent-Lengthis present, reject when it does not equaln. - Build the outbound canonical string (fields joined with
\n):
METHOD
PATH
QUERY
SHA256(BODY_PREFIX)
CONTENT_LENGTH
TIMESTAMP
NONCEBODY_PREFIX is body[0 .. min(n, 1 MiB)). CONTENT_LENGTH is the decimal
string of n (0 for empty / GET / HEAD).
- HMAC-SHA256 with the
osk_…secret; hex-encode; compare in constant time.
PATH and QUERY must match what the origin received (query without ?).
Node example
import { createHash, createHmac, timingSafeEqual } from 'node:crypto';
const MAX_SKEW_SECONDS = 300;
const PREFIX_BYTES = 1024 * 1024; // 1 MiB
export function cameThroughSwarmvia(req, rawBody) {
const timestamp = req.headers['x-swarmvia-timestamp'];
const nonce = req.headers['x-swarmvia-nonce'];
const signature = req.headers['x-swarmvia-signature'];
if (!timestamp || !nonce || !signature) return false;
if (Math.abs(Date.now() / 1000 - Number(timestamp)) > MAX_SKEW_SECONDS) {
return false;
}
const body = Buffer.isBuffer(rawBody) ? rawBody : Buffer.from(rawBody ?? '');
const declared = req.headers['content-length'];
if (declared !== undefined && Number(declared) !== body.length) return false;
const originalUrl = req.originalUrl ?? req.url;
const [path, query = ''] = originalUrl.split('?', 2);
const prefix = body.subarray(0, Math.min(body.length, PREFIX_BYTES));
const bodyHash = createHash('sha256').update(prefix).digest('hex');
const canonical = [
req.method.toUpperCase(),
path,
query,
bodyHash,
String(body.length),
timestamp,
nonce,
].join('\n');
const expected = createHmac(
'sha256',
process.env.NTHPOOL_ORIGIN_SIGNING_SECRET,
)
.update(canonical)
.digest('hex');
return (
signature.length === expected.length &&
timingSafeEqual(Buffer.from(signature), Buffer.from(expected))
);
}
// Express: capture rawBody before JSON parsing, then reject false with 403.Limits and gotchas
- Bodies larger than 1 MB still get a valid signature: only the prefix is
hashed; send a correct
Content-Lengthso swarmvia can stream without buffering the whole payload. - Without
Content-Length, swarmvia buffers up to 10 MB. Larger chunked bodies return413 payload_too_largeto the caller. - Inbound
X-Swarmvia-*headers are stripped before forwarding, so only swarmvia can set the outbound signature headers. - Retries are re-signed (new timestamp and nonce each attempt).
- Optional: track nonces at the origin for replay defense; swarmvia already uses a fresh UUID per attempt.
- When rotating, accept both secrets until you Activate the new one in the dashboard.
See also
- Access (origin verification settings)
- Sources
- Admission errors