Skip to content

security: publisher private key compromise — DEFAULT_PUBLISHER_KEY swept by drainer bot on DEV #103

Description

@TaprootFreak

Summary

The hardcoded DEFAULT_PUBLISHER_KEY in node/src/lib.rs:44-45 is a publicly known test key (1234567890abcdef × 4). Drainer bots watching Mutinynet for incoming transactions to addresses derived from publicly-grepable keys sweep the publisher wallet within minutes of any topup — minimum-fee, single-input → single-output, rotating recipient addresses.

Forensic evidence

Publisher Taproot address derived from the burned key: tb1pvyhjfd90wmlw2dyjjdg2k0zqfp6mr9sufu4ztjwu78z2t0h5pjgshwz8up

Esplora stats: funded_txo_count: 312, spent_txo_count: 312 (all received → all spent), lifetime ~10.3M sats.

4 historical drain transactions on this address (signet/Mutinynet):

Drain TX (first 16 chars) In Out (to drainer) Fee Drainer address (rotating)
8c8fe64ddb036981… 1,000,000 999,889 111 tb1pulayahl2u6mcutpwqw3v70g34mlhamtuetm32wpplclanx95d20slv35wc
a7211d0d24aa0c6a… 98,605 98,494 111 tb1pnf9fe29mrdcxulpph9lkyk92fh8rd9jt6jllmc8d75nzux54s9lqkhdga2
e9ef0b16797c5b89… 98,884 98,773 111 tb1ppkal7eaxzepwylueraxrt25gzgdn6xjs04pphgupz7yzekz5scqqt8vtt2
6173868903d4592a… 99,163 99,052 111 tb1plql0j9umkey086c0nk7pawz4vhjj3a4a7sauj2w9crz0me3x29sqasfzxy

Signature pattern (consistent across all 4):

  • Single input from publisher
  • Single output to a unique destination (no change back to publisher)
  • Fee exactly 111 sats (network-minimum)
  • TX size exactly 162 bytes / 444 weight (no witness data — pure key-path spend)
  • Triggered within minutes of each top-up arriving in mempool

This is a fully-automated mempool sweeper bot keyed on the publicly known private key.

Root cause

node/src/lib.rs:80-87:

pub static ref PUBLISHER_KEY: String = {
    let key = std::env::var("PUBLISHER_KEY")
        .unwrap_or_else(|_| DEFAULT_PUBLISHER_KEY.to_string());
    if NETWORK_CONFIG.is_mainnet && key == DEFAULT_PUBLISHER_KEY {
        panic!("PUBLISHER_KEY env var must be set for mainnet");
    }
    key
};

The mainnet-only panic guard allowed DEV (and any other non-mainnet deploy) to silently fall back to the burned test key. Any deployment whose secret-management pipeline did not list PUBLISHER_KEY as a required env var would silently inherit the burned key on every non-mainnet network.

Impact

Scope Severity
Non-mainnet deploys (DEV, signet) Wallet drained within ~3 min of every topup. Mint inscriptions cannot broadcast (publisher empty). API E2E tests dev_skip!-masked failures until #94 made the failure visible.
Mainnet deploys Not impacted by the silent-fallback path — the mainnet-only panic guard catches an unset PUBLISHER_KEY on mainnet. Code hygiene risk remains regardless.
Code hygiene Hardcoded test key + network-conditional guard is an audit red flag, even after the fallback is removed.

Fix plan

  1. Code (PR #102 — Draft): Remove DEFAULT_PUBLISHER_KEY entirely. PUBLISHER_KEY env var is REQUIRED on every network (DEV / signet / mainnet). Panic at startup if not set, with a message pointing at openssl rand -hex 32 for local dev.
  2. Operator action: Provision a freshly-generated 32-byte hex PUBLISHER_KEY in your deployment's secret manager and wire it through to the node container env. Must happen BEFORE PR #102 merges — otherwise the next deploy panics.
  3. CI (in PR #102): Inject PUBLISHER_KEY: 0000…0001 as a 32-byte hex placeholder in the node-tests and coverage job env (clearly NOT a secret; chosen so a future grep for the burned 1234… key catches regressions).

Lessons

  • Hardcoded secrets in production code are toxic even when behind a mainnet guard — the guard only catches the configured-network case, not a key intentionally checked into source.
  • The previous dev_skip!() macro pattern in node/tests/api_remote.rs masked the 503 ("publisher empty") errors as test-passes. #94 removes that masking — without it, this issue would still be invisible behind a green CI.

Related

  • PR #94test: harden suite — remove dev_skip masking + publisher preflight (already merged)
  • PR #102security: require PUBLISHER_KEY env var on every network (no default) (Draft)
  • Issue #89Mint state-desync class (related: removed DEV_SKIP_BROADCAST_FAILURE which masked some publisher-empty failures)

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't workingoperationsOperations / production hardeningpre-mainnetIssues that must be tracked but not necessarily implemented before Mainnet go-livesecuritySecurity hardening

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions