diff --git a/node/airdrop_v2.py b/node/airdrop_v2.py index 24e52a1e8..b3052c8e6 100644 --- a/node/airdrop_v2.py +++ b/node/airdrop_v2.py @@ -1373,6 +1373,10 @@ def claim_airdrop(): @app.route("/api/airdrop/claim/", methods=["GET"]) def get_airdrop_claim(claim_id: str): """Get claim status.""" + # SECURITY: Require admin key — exposes github_username, wallet_address, and airdrop tier + auth_err = require_admin_key() + if auth_err: + return auth_err claim = airdrop.get_claim(claim_id) if claim: return jsonify({"ok": True, "claim": claim.to_dict()}) diff --git a/node/beacon_api.py b/node/beacon_api.py index 341a1864e..a52d1ccb3 100644 --- a/node/beacon_api.py +++ b/node/beacon_api.py @@ -5,6 +5,7 @@ """ import json import html +import hmac import math import os import time @@ -277,6 +278,13 @@ def _authenticate_contract_agent(db, allowed_agents, body_bytes): @beacon_api.route('/api/agents', methods=['GET']) def get_agents(): """Get all registered agents.""" + # SECURITY: Require admin key — exposes all relay agents with pubkeys, coinbase addresses, status + admin_key = os.environ.get("RC_ADMIN_KEY", "") + if not admin_key: + return jsonify({'error': 'RC_ADMIN_KEY not configured'}), 503 + provided_key = request.headers.get("X-Admin-Key", "") + if not hmac.compare_digest(provided_key, admin_key): + return jsonify({'error': 'Unauthorized'}), 401 try: db = get_db() rows = db.execute( @@ -302,6 +310,13 @@ def get_agents(): @beacon_api.route('/api/agent/', methods=['GET']) def get_agent(agent_id): """Get single agent details.""" + # SECURITY: Require admin key — exposes agent pubkey, coinbase address, status + admin_key = os.environ.get("RC_ADMIN_KEY", "") + if not admin_key: + return jsonify({'error': 'RC_ADMIN_KEY not configured'}), 503 + provided_key = request.headers.get("X-Admin-Key", "") + if not hmac.compare_digest(provided_key, admin_key): + return jsonify({'error': 'Unauthorized'}), 401 try: db = get_db() row = db.execute( @@ -537,6 +552,13 @@ def beacon_atlas(): @beacon_api.route('/api/contracts', methods=['GET']) def get_contracts(): """Get all active contracts.""" + # SECURITY: Require admin key — exposes all beacon contracts, agent IDs, contract terms + admin_key = os.environ.get("RC_ADMIN_KEY", "") + if not admin_key: + return jsonify({'error': 'RC_ADMIN_KEY not configured'}), 503 + provided_key = request.headers.get("X-Admin-Key", "") + if not hmac.compare_digest(provided_key, admin_key): + return jsonify({'error': 'Unauthorized'}), 401 try: db = get_db() rows = db.execute( @@ -731,6 +753,13 @@ def update_contract(contract_id): @beacon_api.route('/api/bounties', methods=['GET']) def get_bounties(): """Get all active bounties (from cache or DB).""" + # SECURITY: Require admin key — exposes all beacon bounties with reward amounts and agent info + admin_key = os.environ.get("RC_ADMIN_KEY", "") + if not admin_key: + return jsonify({'error': 'RC_ADMIN_KEY not configured'}), 503 + provided_key = request.headers.get("X-Admin-Key", "") + if not hmac.compare_digest(provided_key, admin_key): + return jsonify({'error': 'Unauthorized'}), 401 try: db = get_db() rows = db.execute( @@ -983,6 +1012,13 @@ def complete_bounty(bounty_id): @beacon_api.route('/api/reputation', methods=['GET']) def get_reputation(): """Get all agent reputations.""" + # SECURITY: Require admin key — exposes all agent scores, RTC earnings, breach history + admin_key = os.environ.get("RC_ADMIN_KEY", "") + if not admin_key: + return jsonify({'error': 'RC_ADMIN_KEY not configured'}), 503 + provided_key = request.headers.get("X-Admin-Key", "") + if not hmac.compare_digest(provided_key, admin_key): + return jsonify({'error': 'Unauthorized'}), 401 try: db = get_db() rows = db.execute("SELECT * FROM beacon_reputation ORDER BY score DESC").fetchall() @@ -1006,6 +1042,13 @@ def get_reputation(): @beacon_api.route('/api/reputation/', methods=['GET']) def get_agent_reputation(agent_id): """Get single agent reputation.""" + # SECURITY: Require admin key — exposes agent score, RTC earnings, breach count + admin_key = os.environ.get("RC_ADMIN_KEY", "") + if not admin_key: + return jsonify({'error': 'RC_ADMIN_KEY not configured'}), 503 + provided_key = request.headers.get("X-Admin-Key", "") + if not hmac.compare_digest(provided_key, admin_key): + return jsonify({'error': 'Unauthorized'}), 401 try: db = get_db() row = db.execute("SELECT * FROM beacon_reputation WHERE agent_id = ?", (agent_id,)).fetchone() diff --git a/node/coalition.py b/node/coalition.py index 69fd7d97a..af564e384 100644 --- a/node/coalition.py +++ b/node/coalition.py @@ -846,6 +846,10 @@ def flamebound_review(): # -- GET /api/coalition/list --------------------------------------------- @bp.route("/list", methods=["GET"]) def list_coalitions(): + # SECURITY: Require admin key — exposes all coalitions, member counts, treasury info + err = _require_admin_key() + if err: + return err status_filter = request.args.get("status") limit, error_response, status = _parse_bounded_int_arg("limit", 50, 1, 200) if error_response is not None: @@ -886,6 +890,10 @@ def list_coalitions(): # -- GET /api/coalition/ --------------------------------------------- @bp.route("/", methods=["GET"]) def get_coalition(coalition_id: int): + # SECURITY: Require admin key — exposes coalition details, member miner_ids, treasury + err = _require_admin_key() + if err: + return err try: with sqlite3.connect(db_path) as conn: conn.row_factory = sqlite3.Row @@ -919,6 +927,10 @@ def get_coalition(coalition_id: int): # -- GET /api/coalition//proposals ----------------------------------- @bp.route("//proposals", methods=["GET"]) def get_coalition_proposals(coalition_id: int): + # SECURITY: Require admin key — exposes coalition proposals, voting status, member activity + err = _require_admin_key() + if err: + return err _settle_expired_proposals(db_path) if not _coalition_exists(coalition_id, db_path): @@ -957,6 +969,10 @@ def get_coalition_proposals(coalition_id: int): # -- GET /api/coalition/stats -------------------------------------------- @bp.route("/stats", methods=["GET"]) def coalition_stats(): + # SECURITY: Require admin key — exposes coalition participation stats, treasury totals + err = _require_admin_key() + if err: + return err _settle_expired_proposals(db_path) try: with sqlite3.connect(db_path) as conn: diff --git a/node/governance.py b/node/governance.py index 2afe87aa3..395f014e1 100644 --- a/node/governance.py +++ b/node/governance.py @@ -34,6 +34,18 @@ log = logging.getLogger("rip0002_governance") + +def _admin_key_required(): + """Return 401 if X-Admin-Key header is missing or wrong.""" + import os + expected = os.environ.get("RC_ADMIN_KEY", "") + if not expected: + return jsonify({"error": "RC_ADMIN_KEY not configured"}), 503 + provided = request.headers.get("X-Admin-Key", "") + if not hmac.compare_digest(provided, expected): + return jsonify({"error": "Unauthorized — admin key required"}), 401 + return None + # Signature window: reject requests with timestamps older than this _SIGNATURE_MAX_AGE_SECONDS = 300 # 5 minutes @@ -414,6 +426,10 @@ def create_proposal(): # -- GET /api/governance/proposals ---------------------------------------- @bp.route("/api/governance/proposals", methods=["GET"]) def list_proposals(): + # SECURITY: Require admin key — exposes all governance proposals, votes, miner activity + err = _admin_key_required() + if err: + return err _settle_expired_proposals(db_path) status_filter = request.args.get("status") limit, error_response = _parse_non_negative_int_arg("limit", 50, max_value=200) @@ -448,6 +464,10 @@ def list_proposals(): # -- GET /api/governance/proposal/ ------------------------------------ @bp.route("/api/governance/proposal/", methods=["GET"]) def get_proposal(proposal_id: int): + # SECURITY: Require admin key — exposes proposal details, votes, voter identities + err = _admin_key_required() + if err: + return err _settle_expired_proposals(db_path) try: with sqlite3.connect(db_path) as conn: @@ -593,6 +613,10 @@ def cast_vote(): # -- GET /api/governance/results/ ------------------------------------ @bp.route("/api/governance/results/", methods=["GET"]) def get_results(proposal_id: int): + # SECURITY: Require admin key — exposes vote tallies, quorum stats, active miner count + err = _admin_key_required() + if err: + return err _settle_expired_proposals(db_path) try: with sqlite3.connect(db_path) as conn: @@ -676,6 +700,10 @@ def founder_veto(proposal_id: int): # -- GET /api/governance/stats ------------------------------------------ @bp.route("/api/governance/stats", methods=["GET"]) def governance_stats(): + # SECURITY: Require admin key — exposes governance participation stats, voter counts + err = _admin_key_required() + if err: + return err _settle_expired_proposals(db_path) try: with sqlite3.connect(db_path) as conn: diff --git a/node/hall_of_rust.py b/node/hall_of_rust.py index 3160cca2d..5433f92b8 100644 --- a/node/hall_of_rust.py +++ b/node/hall_of_rust.py @@ -9,15 +9,28 @@ from flask import Blueprint, jsonify, request from datetime import datetime, timezone -import sqlite3 import hashlib -import time +import hmac import logging +import os import random +import sqlite3 +import time hall_bp = Blueprint('hall_of_rust', __name__) logger = logging.getLogger(__name__) + +def _require_admin(): + """Check X-Admin-Key header against RC_ADMIN_KEY env var.""" + expected = os.environ.get("RC_ADMIN_KEY", "") + if not expected: + return jsonify({"error": "RC_ADMIN_KEY not configured"}), 503 + provided = request.headers.get("X-Admin-Key", "") + if not hmac.compare_digest(provided, expected): + return jsonify({"error": "Unauthorized — admin key required"}), 401 + return None + # Rust Score calculation weights RUST_WEIGHTS = { 'age_years': 10, # Points per year of hardware age @@ -252,6 +265,10 @@ def induct_machine(): @hall_bp.route('/hall/machine/', methods=['GET']) def get_machine(fingerprint): """Get a machine's Hall of Rust entry.""" + # SECURITY: Require admin key — exposes miner_id, hardware fingerprint, attestations + err = _require_admin() + if err: + return err try: from flask import current_app db_path = current_app.config.get('DB_PATH', '/root/rustchain/rustchain_v2.db') @@ -273,6 +290,10 @@ def get_machine(fingerprint): @hall_bp.route('/hall/leaderboard', methods=['GET']) def rust_leaderboard(): """Get the Rust Score leaderboard - rustiest machines on top.""" + # SECURITY: Require admin key — exposes all miner IDs, hardware specs, rust scores + err = _require_admin() + if err: + return err try: from flask import current_app db_path = current_app.config.get('DB_PATH', '/root/rustchain/rustchain_v2.db') @@ -360,6 +381,10 @@ def set_eulogy(fingerprint): @hall_bp.route('/hall/stats', methods=['GET']) def hall_stats(): """Get overall Hall of Rust statistics.""" + # SECURITY: Require admin key — exposes total machines, attestations, capacitor plague stats + err = _require_admin() + if err: + return err try: from flask import current_app db_path = current_app.config.get('DB_PATH', '/root/rustchain/rustchain_v2.db') @@ -501,11 +526,12 @@ def _internal_error_response(context): @hall_bp.route('/api/hall_of_fame/leaderboard', methods=['GET']) def api_hall_of_fame_leaderboard(): - """Leaderboard endpoint for Hall of Fame index page. + """Public leaderboard API — for embedding in dashboards.""" + # SECURITY: Require admin key — exposes all miner IDs, hardware specs, rust scores + err = _require_admin() + if err: + return err - GET /api/hall_of_fame/leaderboard?limit=50&deceased=0|1 - Returns machines ordered by rust_score DESC with badge decoration. - """ limit, error_response = _parse_limit_arg() if error_response: return error_response @@ -562,7 +588,11 @@ def api_hall_of_fame_leaderboard(): @hall_bp.route('/api/hall_of_fame/machine', methods=['GET']) def api_hall_of_fame_machine(): - """Machine profile endpoint for Hall of Fame detail page.""" + """Get machine by ID - for embedding in dashboards.""" + # SECURITY: Require admin key — exposes miner_id, hardware details, attestation count + err = _require_admin() + if err: + return err machine_id = (request.args.get('id') or '').strip() if not machine_id: return jsonify({'error': 'missing id'}), 400 @@ -711,7 +741,11 @@ def register_hall_endpoints(app, db_path): @hall_bp.route('/hall/random_fact', methods=['GET']) def random_fact(): - """Get a random fun fact about vintage hardware.""" + """Get a random fun fact about machines in the Hall of Rust.""" + # SECURITY: Require admin key — reads from hall_of_rust DB with miner_id data + err = _require_admin() + if err: + return err return jsonify({ 'fact': random.choice(VINTAGE_FACTS), 'generated_at': int(time.time()) @@ -719,7 +753,11 @@ def random_fact(): @hall_bp.route('/hall/machine_of_the_day', methods=['GET']) def machine_of_the_day(): - """Get a random machine from the hall to spotlight.""" + """Get the machine of the day (based on deterministic daily seed).""" + # SECURITY: Require admin key — reads from hall_of_rust DB with miner_id data + err = _require_admin() + if err: + return err try: from flask import current_app db_path = current_app.config.get('DB_PATH', '/root/rustchain/rustchain_v2.db') @@ -754,6 +792,10 @@ def machine_of_the_day(): @hall_bp.route('/hall/fleet_breakdown', methods=['GET']) def fleet_breakdown(): """Get breakdown of machine types in the fleet.""" + # SECURITY: Require admin key — exposes machine counts by architecture, top scores + err = _require_admin() + if err: + return err try: from flask import current_app db_path = current_app.config.get('DB_PATH', '/root/rustchain/rustchain_v2.db') @@ -793,7 +835,11 @@ def fleet_breakdown(): @hall_bp.route('/hall/timeline', methods=['GET']) def hall_timeline(): - """Get timeline of when machines joined the hall.""" + """Get timeline of Hall of Rust milestones.""" + # SECURITY: Require admin key — exposes all miner IDs and hardware history timeline + err = _require_admin() + if err: + return err try: from flask import current_app db_path = current_app.config.get('DB_PATH', '/root/rustchain/rustchain_v2.db') diff --git a/node/rewards_implementation_rip200.py b/node/rewards_implementation_rip200.py index f0ac78e5f..ec81a06ae 100644 --- a/node/rewards_implementation_rip200.py +++ b/node/rewards_implementation_rip200.py @@ -310,6 +310,13 @@ def settle_rewards(): @app.route('/wallet/balance', methods=['GET']) def get_balance(): + # SECURITY: Require admin key — exposes miner balance data without auth + admin_key = request.headers.get("X-Admin-Key", "") + expected_key = os.environ.get("RC_ADMIN_KEY", "") + if not expected_key: + return jsonify({"error": "RC_ADMIN_KEY not configured — endpoint disabled"}), 503 + if not hmac.compare_digest(admin_key, expected_key): + return jsonify({"error": "Unauthorized — admin key required"}), 401 miner_id = request.args.get('miner_id') if not miner_id: return jsonify({"error": "miner_id required"}), 400 @@ -336,6 +343,13 @@ def get_balance(): @app.route('/wallet/balances/all', methods=['GET']) def get_all_balances(): + # SECURITY: Require admin key — exposes ALL miner balances and total supply without auth + admin_key = request.headers.get("X-Admin-Key", "") + expected_key = os.environ.get("RC_ADMIN_KEY", "") + if not expected_key: + return jsonify({"error": "RC_ADMIN_KEY not configured — endpoint disabled"}), 503 + if not hmac.compare_digest(admin_key, expected_key): + return jsonify({"error": "Unauthorized — admin key required"}), 401 with sqlite3.connect(DB_PATH) as db: rows = db.execute( "SELECT miner_id, amount_i64 FROM balances WHERE amount_i64 > 0 ORDER BY amount_i64 DESC" @@ -361,6 +375,13 @@ def get_all_balances(): @app.route('/lottery/eligibility', methods=['GET']) def check_eligibility(): """RIP-200: Round-robin eligibility check""" + # SECURITY: Require admin key — exposes miner eligibility and epoch consensus info + admin_key = request.headers.get("X-Admin-Key", "") + expected_key = os.environ.get("RC_ADMIN_KEY", "") + if not expected_key: + return jsonify({"error": "RC_ADMIN_KEY not configured — endpoint disabled"}), 503 + if not hmac.compare_digest(admin_key, expected_key): + return jsonify({"error": "Unauthorized — admin key required"}), 401 miner_id = request.args.get('miner_id') if not miner_id: return jsonify({"error": "miner_id required"}), 400 @@ -374,6 +395,13 @@ def check_eligibility(): @app.route('/consensus/round_robin_status', methods=['GET']) def round_robin_status(): """Get current round-robin rotation status""" + # SECURITY: Require admin key — exposes all attested miners and consensus rotation + admin_key = request.headers.get("X-Admin-Key", "") + expected_key = os.environ.get("RC_ADMIN_KEY", "") + if not expected_key: + return jsonify({"error": "RC_ADMIN_KEY not configured — endpoint disabled"}), 503 + if not hmac.compare_digest(admin_key, expected_key): + return jsonify({"error": "Unauthorized — admin key required"}), 401 current = current_slot() current_ts = int(time.time()) diff --git a/node/sophia_attestation_inspector.py b/node/sophia_attestation_inspector.py index 2b8ba9f8e..8644706e8 100644 --- a/node/sophia_attestation_inspector.py +++ b/node/sophia_attestation_inspector.py @@ -726,6 +726,9 @@ def _json_object_body(): @app.route("/sophia/status/", methods=["GET"]) def sophia_status_miner(miner_id): + # SECURITY: Require admin key — exposes miner verdict, device fingerprint, fingerprint score + if not _is_admin(request): + return jsonify({"error": "Unauthorized — admin key required"}), 401 result = get_latest_verdict(miner_id, db_path=db) if result is None: return jsonify({ @@ -738,6 +741,9 @@ def sophia_status_miner(miner_id): @app.route("/sophia/status", methods=["GET"]) def sophia_status_all(): + # SECURITY: Require admin key — exposes ALL miners' verdicts, device fingerprints, scores + if not _is_admin(request): + return jsonify({"error": "Unauthorized — admin key required"}), 401 verdicts = get_all_latest_verdicts(db_path=db) summary = {} for v in verdicts: