fix: Security hardening from CT116 deep-dive review

- API keys moved from hardcoded dict to env var (API_KEYS JSON) with fallback
- Rate limiting added: token bucket per API key (Redis-backed), 429 responses with Retry-After
- Rate limit tiers: enterprise 120/min, professional 60/min, starter 20/min
- X-RateLimit-* headers on all responses
- Dashboard polling reduced from 3s to 5s backend, 10s JS fallback
- SSE detection disables redundant polling when stream is connected
- Deleted ts_patch.py (dead one-shot migration, already applied)
- Added ssl/README.md documenting upstream SSL termination

Ref: Relay #444 (Mumuni CT116 harness deep-dive)
Reviewed-by: Abiba <abiba@sysloggh.com>
This commit is contained in:
Abiba
2026-06-02 10:37:10 +00:00
parent 060a47fce9
commit 9a633583ab
4 changed files with 66 additions and 84 deletions
+9 -3
View File
@@ -16,7 +16,7 @@ def fetch_state():
def broadcast_loop():
while True:
time.sleep(3)
time.sleep(5)
data = fetch_state(); payload = json.dumps(data)
with sse_lock:
dead = [q for q in sse_subscribers if not q.put(payload)]
@@ -256,7 +256,13 @@ var aHTML=agents.map(function(a){return'<div class="mb-2" style="font-size:11px"
$('perf-agents').innerHTML=aHTML;
}else{$('perf-agents').innerHTML='<div class="text-secondary small text-center py-3">-</div>';}
}
function poll(){fetch('/api/state').then(function(r){return r.json()}).then(function(data){render(data);$('connection-status').textContent='live';}).catch(function(){$('connection-status').textContent='reconnecting';});}
var sseConnected = false;
function poll(){if(sseConnected)return;fetch('/api/state').then(function(r){return r.json()}).then(function(data){render(data);$('connection-status').textContent='live';}).catch(function(){$('connection-status').textContent='reconnecting';});}
// SSE connection tracking
var es = new EventSource('/api/stream');
es.onopen = function(){sseConnected = true; $('connection-status').textContent = 'live (SSE)';};
es.onmessage = function(e){try{render(JSON.parse(e.data));}catch(ex){}};
es.onerror = function(){sseConnected = false; $('connection-status').textContent = 'polling';};
function loadScatter(){
var m=$('scatter-model').value;
fetch('/api/scatter?window=24&model='+m).then(function(r){return r.json()}).then(renderScatter).catch(function(){});
@@ -293,7 +299,7 @@ el.innerHTML='<svg viewBox="-35 0 140 115" style="width:100%;height:200px">'+gri
var models=[];pts.forEach(function(p){if(models.indexOf(p.model)===-1)models.push(p.model);});
lg.innerHTML=models.map(function(m){return'<span class="d-flex align-items-center gap-1 small"><svg width="10" height="10"><circle cx="5" cy="5" r="3.5" fill="'+(mcol[m]||'#38bdf8')+'"/></svg>'+mlab[m]+'</span>';}).join('');
}
poll();setInterval(poll,3000);loadTS();loadPerf();setInterval(loadPerf,15000);loadScatter();setInterval(loadScatter,30000);
poll();setInterval(poll,10000);loadTS();loadPerf();setInterval(loadPerf,15000);loadScatter();setInterval(loadScatter,30000);
</script>
</body>
</html>"""