Skip to content
Draft
7 changes: 7 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,13 @@ OPENROUTERAPIKEY='<add_your_openrouter_api_key_here>'
# If you want to use IO.net, add your key here
IONETAPIKEY='<add_your_ionet_api_key_here>'

########################################
# Admin Access Control
# Set this to protect admin endpoints (explorer admin, validator management, etc.)
# When set, requests must include this key via X-Admin-Key header or admin_key param
# To rotate: change the value and restart the service
ADMIN_API_KEY=''

########################################
# API Rate Limiting Configuration
RATE_LIMIT_ENABLED='false' # Enable/disable API key rate limiting
Expand Down
32 changes: 32 additions & 0 deletions backend/protocol_rpc/dependencies.py
Original file line number Diff line number Diff line change
Expand Up @@ -198,3 +198,35 @@ def get_llm_provider_registry(

def get_rate_limiter(request: Request):
return _peek_state_attr(_get_app_state(request), "rate_limiter")


def require_admin_key(request: Request) -> None:
"""FastAPI dependency that enforces ADMIN_API_KEY for explorer admin routes.

Same logic as the JSON-RPC ``require_admin_access`` decorator:
- ADMIN_API_KEY set -> requires matching ``X-Admin-Key`` header
- VITE_IS_HOSTED=true without ADMIN_API_KEY -> blocked entirely
- Neither set -> open access (local dev)
"""
import os

admin_api_key = os.getenv("ADMIN_API_KEY")
is_hosted = os.getenv("VITE_IS_HOSTED") == "true"

if admin_api_key:
request_key = request.headers.get("X-Admin-Key")
if request_key == admin_api_key:
return
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Invalid or missing admin key",
)

if is_hosted:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Operation not available in hosted mode",
)

# Local dev = open access
return
17 changes: 17 additions & 0 deletions backend/protocol_rpc/explorer/admin_router.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
"""FastAPI router for explorer admin endpoints (protected by ADMIN_API_KEY)."""

from fastapi import APIRouter, Depends

from backend.protocol_rpc.dependencies import require_admin_key

explorer_admin_router = APIRouter(
prefix="/api/explorer/admin",
tags=["explorer-admin"],
dependencies=[Depends(require_admin_key)],
)


@explorer_admin_router.get("/verify")
def verify_admin():
"""Health-check endpoint to verify admin access."""
return {"status": "ok", "admin": True}
Loading
Loading