Skip to content

v1.0.0-rc13

Pre-release
Pre-release

Choose a tag to compare

@github-actions github-actions released this 13 Jul 22:04
· 28 commits to main since this release
v1.0.0-rc13
b4c7482

Cumulative since v1.0.0-rc10: rc11 and rc12 were cut without CHANGELOG
entries, so the changes they carried are recorded here rather than lost.

Fixed

  • Container images now report their real version. The Dockerfile built with
    -ldflags="-s -w" and no -X injection, and the image workflow passed no
    version build-arg, so the binary fell back to the hardcoded
    version.Version = "1.0.0-rc10". Every image from rc11 onward reported
    itself as 1.0.0-rc10
    — the image tag said one thing and serverInfo /
    /healthz said another. release.yml already injected the version correctly
    into the standalone binaries; the image build was the one path that did not.

    The Dockerfile now takes ARG VERSION and injects it via -X, and
    image.yml passes ${{ steps.meta.outputs.version }}. The fallback in
    internal/version is now dev, not a release-shaped number: a
    plausible-but-wrong version is worse than an obviously-unset one, because it
    fails silently. That stale constant is exactly why this went unnoticed across
    two release candidates.

Added

  • Operator-configured data retention with an enforcing purge. The four
    Postgres tables the keyless bundle writes (write_audit, signer_quota,
    signer_blacklist, admin_audit) previously had no purge, TTL, or expiry
    of any kind
    — rows accumulated indefinitely, and the retention periods our
    own documentation cited were enforced by nothing. A new in-process purge now
    enforces per-table windows: MCP_WRITE_AUDIT_RETENTION,
    MCP_WRITE_AUDIT_GRANT_ROLE_RETENTION, MCP_SIGNER_QUOTA_RETENTION,
    MCP_SIGNER_BLACKLIST_RETENTION, MCP_ADMIN_AUDIT_RETENTION, and
    MCP_RETENTION_PURGE_INTERVAL (default 1h).

    Every window defaults to unset, which means retain indefinitely — no
    existing deployment changes behavior on upgrade. Retention is deliberately
    operator-chosen rather than hardcoded: a self-hosting operator's obligations
    are theirs to determine. The point of the feature is that a window an
    operator does set is now enforced by the artifact rather than promised by a
    document. Boot fails on a negative window, on a window with a non-positive
    purge interval (it would never fire), and on a grantRole window shorter
    than the ordinary one (it would purge the administrative trail before the
    routine traffic it exists to outlive). Deletes are batched so a large first
    sweep cannot hold long row locks. See docs/DATA_HANDLING.md § 8.3 and
    docs/RUNBOOK.md § "Data retention".

  • write_audit.method_selector (migration 0005). Records the 4-byte ABI
    method selector of each broadcast. Under keyless writes every relayed
    transaction shares one destination (checkRelayScope permits only the anchor
    precompile), so to_addr could not distinguish an administrative grantRole
    call from a routine anchor write — which made the longer grantRole
    retention window our privacy policy promises unenforceable. The selector
    is derived from the loaded ABI (anchor.Client.MethodSelector), never
    hardcoded: this precompile's grantRole takes four arguments, so its selector
    is not the well-known OpenZeppelin grantRole(bytes32,address) value. A
    selector is a public function identifier and carries no caller data. Rows
    written before this migration carry an empty selector and are treated as
    ordinary writes.

  • Trusted-proxy header hardening (C3/C5). Two defense-in-depth
    controls, both gated on the existing NVNM_TRUST_PROXY_HEADERS
    (default false, unchanged): (C3) the fail-rate and anon-read
    limiters' client-IP derivation now walks a configurable number of
    trusted hops in from the right of X-Forwarded-For ++ RemoteAddr
    instead of trusting the leftmost (client-forgeable) XFF entry, so
    a forged left-prefix can no longer mint its own rate-limit bucket;
    (C5) a new requireForwardedHTTPS middleware rejects a request
    carrying an explicit non-https X-Forwarded-Proto with 403,
    catching a plaintext-downgrade path inside the trust boundary — it
    is deliberately lenient when X-Forwarded-Proto is absent (the
    ingress remains the primary, fail-closed TLS gate; see
    docs/RUNBOOK.md § "Trusted-proxy header invariants (C3/C5)" for
    the fail-open rationale). New env var NVNM_TRUSTED_PROXY_HOPS
    (int, default 1, must be >= 1; boot fails loud otherwise) sets
    the trusted chain depth (1 = single ingress, 2 = CDN +
    ingress); documented canonically in docs/DATA_HANDLING.md § 5.
    Both controls are defense-in-depth behind the deploy-side
    invariants (proxy strips inbound XFF; ingress sets XFP and
    terminates TLS), which remain operator-owned.

  • API-key hashing now supports HMAC-SHA256 under a server-held pepper
    (KEY_HMAC_PEPPER, optional; KEY_HMAC_PEPPER_PREVIOUS for one
    rotation window). Keys carry a hash_version; legacy SHA-256 keys
    (v0) continue to authenticate unchanged via versioned candidate
    lookup. Setting KEY_HMAC_PEPPER_PREVIOUS without KEY_HMAC_PEPPER
    fails boot.

  • Postgres key-store backend (KEY_STORE_BACKEND=postgres). An
    optional Postgres-backed key store for multi-replica deployments where
    all replicas must share a single authoritative api_keys table.
    File backend (KEY_STORE_BACKEND=file) remains the default and is
    unchanged. New env vars: KEY_STORE_BACKEND, KEY_STORE_DSN
    (required when postgres). Dependencies added: pgx/v5, goose/v3.

  • Advisory-locked boot-time migrations. goose migrations for the
    Postgres backend run automatically at startup under a
    pg_advisory_lock, making concurrent replica boot safe without a
    separate migration job.

  • Persisted lazy v0→v1 rehash (Postgres backend). On first
    authenticated use, a legacy v0 key (plain SHA-256) is transparently
    rehashed to v1 (HMAC-SHA256 under KEY_HMAC_PEPPER) and the updated
    digest is committed to the api_keys table.

  • Hard pepper gate (ErrPepperRequired). When
    KEY_STORE_BACKEND=postgres and AUTH_PROVIDER=apikey, boot fails
    immediately if KEY_HMAC_PEPPER is unset. Closes the risk of a
    misconfigured replica falling back to unprotected v0 hashing on a
    shared store.

  • Key expiry enforcement (Phase 4). expires_at is now enforced at
    Lookup on both file and Postgres backends. A key expires when
    now >= expires_at; a zero/NULL expires_at never expires.

  • Three-way reject taxonomy with bounded disclosure. Auth rejections
    are classified into three messages: invalid API key (no-match —
    also returned on the timing-equalised miss path so non-holders cannot
    distinguish unknown-key from known-key), key expired (matched row,
    expiry passed; appends — renew at <KEY_RENEWAL_URL> when set), and
    key revoked (matched row, enabled=false). The specific messages
    are only reachable by a caller holding the matched key's bytes.

  • KEY_DEFAULT_TTL env var (default 8760h ≈ 1 year). Applied by
    the admin REST API and key-mgmt create when no per-key ttl
    override is given. 0 means no default expiry. Does not apply to the
    static MCP_API_KEY path (always non-expiring).

  • KEY_RENEWAL_URL env var (optional). URL appended to the key expired rejection message; empty = no hint appended.

  • Admin REST API TTL support. POST /admin/keys and
    PATCH /admin/keys/{id} accept an optional "ttl" field (duration
    string; "0"/"none"/"never" = no expiry; non-positive parsed
    duration → HTTP 400). Admin responses (KeySummary) now include
    expires_at; the zero-value 0001-01-01T00:00:00Z means no expiry.

  • key-mgmt CLI TTL and renew. key-mgmt create gains --ttl <dur|0> (default from KEY_DEFAULT_TTL). New key-mgmt renew <client-id> --ttl <dur|0> subcommand extends or clears expiry from
    now; --ttl is required (the default TTL is not applied on renew).

Fixed

  • nvnm_overview no longer tells agents a keyless deployment requires an
    API key.
    The prereqs list was a package-level constant asserting that
    broadcasting needs "an API key on this server with the writer or admin
    role" — false since the authless flip, and model-visible to every connecting
    client. It is now derived from cfg.KeylessWrites, the same flag
    RequiresAuth reads to make the actual enforcement decision, so the stated
    requirement cannot drift from the enforced one. A keyless deployment now
    reports that no credential is required and that the wallet signature is the
    only identity the server sees.
  • docs/RUNBOOK.md no longer claims a hosted authless deployment "runs no
    key store at all."
    The code does not permit that: loadAPIKeys fails boot
    with ErrHTTPAuthRequired when neither MCP_API_KEY nor
    MCP_API_KEYS_FILE is set on the HTTP transport, and that guard has no
    keyless exception. Such a deployment holds an operator credential that is
    never issued to any caller; callers remain wholly anonymous, but the
    Bearer-validation path stays mounted and an invalid token draws a 401. The
    same incorrect claim is corrected in internal/config's KeylessPGDSN
    godoc.

Verifying signatures

Each binary is shipped with a Cosign keyless signature (via Sigstore OIDC) and a SHA-256 checksum.

# Verify checksum
shasum -a 256 -c nvnm-mcp-server-v1.0.0-rc13-<os>-<arch>.sha256

# Verify Cosign signature
cosign verify-blob \
  --certificate nvnm-mcp-server-v1.0.0-rc13-<os>-<arch>.cert.pem \
  --signature nvnm-mcp-server-v1.0.0-rc13-<os>-<arch>.sig \
  --certificate-identity-regexp 'https://github.com/NVNM-Chain/nvnm-mcp-server/.*' \
  --certificate-oidc-issuer 'https://token.actions.githubusercontent.com' \
  nvnm-mcp-server-v1.0.0-rc13-<os>-<arch>