Fix Docker local controls and setup guidance

Allow the bundled Docker frontend proxy to reach local-operator endpoints through the private compose bridge without trusting LAN clients. This restores Time Machine, MeshChat key creation, AI pins/layers, and related local controls in Docker installs. Refresh first-run guidance so Docker users know to configure OpenSky and AIS keys through .env.
This commit is contained in:
BigBodyCobain
2026-05-02 20:18:46 -06:00
parent 8d3c7a51b7
commit eb0288ee4e
4 changed files with 51 additions and 8 deletions
+30 -3
View File
@@ -13,6 +13,7 @@ import hmac
import asyncio
import hmac as _hmac_mod
import hashlib as _hashlib_mod
import ipaddress
import json as json_mod
import logging
import time
@@ -235,10 +236,36 @@ def _is_local_or_docker(host: str) -> bool:
return host in {"127.0.0.1", "::1", "localhost"}
def _docker_bridge_local_operator_enabled() -> bool:
return str(os.environ.get("SHADOWBROKER_TRUST_DOCKER_BRIDGE_LOCAL_OPERATOR", "")).strip().lower() in {
"1",
"true",
"yes",
"on",
}
def _is_docker_bridge_host(host: str) -> bool:
try:
ip = ipaddress.ip_address(host)
except ValueError:
return False
# Docker Desktop and the default compose bridge normally sit inside
# 172.16.0.0/12. Keep this narrower than "any private IP" so a user who
# intentionally binds the backend to LAN does not silently trust LAN clients.
return ip in ipaddress.ip_network("172.16.0.0/12")
def _is_trusted_local_runtime_host(host: str) -> bool:
if _is_local_or_docker(host):
return True
return _docker_bridge_local_operator_enabled() and _is_docker_bridge_host(host)
def require_local_operator(request: Request):
"""Allow local tooling on loopback / Docker internal network, or a valid admin key."""
host = (request.client.host or "").lower() if request.client else ""
if _is_local_or_docker(host) or (_debug_mode_enabled() and host == "test"):
if _is_trusted_local_runtime_host(host) or (_debug_mode_enabled() and host == "test"):
return
admin_key = _current_admin_key()
presented = str(request.headers.get("X-Admin-Key", "") or "").strip()
@@ -362,8 +389,8 @@ async def require_openclaw_or_local(request: Request):
"""
host = (request.client.host or "").lower() if request.client else ""
# 1. Local loopback — always allowed
if _is_local_or_docker(host) or (_debug_mode_enabled() and host == "test"):
# 1. Local runtime path — loopback, plus bundled Docker bridge when compose opts in
if _is_trusted_local_runtime_host(host) or (_debug_mode_enabled() and host == "test"):
return
# 2. Admin key — full trust
+12
View File
@@ -86,6 +86,18 @@ class TestRequireLocalOperator:
def test_rfc1918_172_blocked_without_key(self):
assert self._call_with_host("172.16.0.5") == 403
def test_docker_bridge_blocked_without_compose_opt_in(self):
with patch.dict("os.environ", {"SHADOWBROKER_TRUST_DOCKER_BRIDGE_LOCAL_OPERATOR": ""}):
assert self._call_with_host("172.18.0.3") == 403
def test_docker_bridge_passes_with_compose_opt_in(self):
with patch.dict("os.environ", {"SHADOWBROKER_TRUST_DOCKER_BRIDGE_LOCAL_OPERATOR": "1"}):
assert self._call_with_host("172.18.0.3") == 200
def test_lan_ip_still_blocked_with_compose_opt_in(self):
with patch.dict("os.environ", {"SHADOWBROKER_TRUST_DOCKER_BRIDGE_LOCAL_OPERATOR": "1"}):
assert self._call_with_host("192.168.1.100") == 403
def test_rfc1918_192168_blocked_without_key(self):
assert self._call_with_host("192.168.1.100") == 403
+3
View File
@@ -27,6 +27,9 @@ services:
- MESH_RELAY_PEERS=${MESH_RELAY_PEERS:-}
# Shared transport auth for operator peer push. Must be set to a unique secret per deployment.
- MESH_PEER_PUSH_SECRET=${MESH_PEER_PUSH_SECRET:-}
# The bundled Docker UI talks to the backend across Docker's private bridge.
# Treat that bridge as local operator access while ports remain bound to 127.0.0.1 by default.
- SHADOWBROKER_TRUST_DOCKER_BRIDGE_LOCAL_OPERATOR=${SHADOWBROKER_TRUST_DOCKER_BRIDGE_LOCAL_OPERATOR:-1}
volumes:
- backend_data:/app/data
restart: unless-stopped
+6 -5
View File
@@ -4,7 +4,7 @@ import React, { useState, useEffect } from 'react';
import { motion, AnimatePresence } from 'framer-motion';
import { X, ExternalLink, Key, Shield, Radar, Globe, Satellite, Ship, Radio } from 'lucide-react';
const CURRENT_ONBOARDING_VERSION = '0.9.7';
const CURRENT_ONBOARDING_VERSION = '0.9.7-docker-keys-1';
const STORAGE_KEY = `shadowbroker_onboarding_complete_v${CURRENT_ONBOARDING_VERSION}`;
const LEGACY_STORAGE_KEY = 'shadowbroker_onboarding_complete';
@@ -19,7 +19,7 @@ const API_GUIDES = [
'Create a free account at opensky-network.org',
'Go to Dashboard → OAuth → Create Client',
'Copy your Client ID and Client Secret',
'Paste both into Settings → Aviation',
'Set OPENSKY_CLIENT_ID and OPENSKY_CLIENT_SECRET in your .env file, then restart ShadowBroker',
],
url: 'https://opensky-network.org/index.php?option=com_users&view=registration',
color: 'cyan',
@@ -33,7 +33,7 @@ const API_GUIDES = [
'Register at aisstream.io',
'Navigate to your API Keys page',
'Generate a new API key',
'Paste it into Settings → Maritime',
'Set AIS_API_KEY in your .env file, then restart ShadowBroker',
],
url: 'https://aisstream.io/authenticate',
color: 'blue',
@@ -218,8 +218,9 @@ const OnboardingModal = React.memo(function OnboardingModal({
</p>
<p className="text-sm text-[var(--text-secondary)] font-mono leading-relaxed">
OpenSky Network and AIS Stream are the free keys that make ShadowBroker
useful immediately: live aircraft and vessel tracking. Add these first;
trust modes and the no-key sources can come next.
useful immediately: live aircraft and vessel tracking. For Docker installs,
create or edit the .env file next to docker-compose.yml, then run docker
compose up -d. For local source installs, edit backend/.env and restart.
</p>
</div>
</div>