Hermes Agent commited on
Commit
42fc0fd
Β·
1 Parent(s): 20a21f0

security(hardening): enforce api key auth, dynamic secret fallbacks, and error log sanitization

Browse files
Files changed (4) hide show
  1. entrypoint.sh +11 -11
  2. gateway/omniroute.py +14 -29
  3. gateway/utils.py +2 -21
  4. tg_streamer.py +7 -0
entrypoint.sh CHANGED
@@ -29,22 +29,22 @@ chmod -R 777 /root/.cache /data/cache /data/hermes 2>/dev/null || true
29
 
30
  # ── STEP 1: Master Secret Validation ──────────────────────────────────────────
31
  if [ -z "$STORAGE_ENCRYPTION_KEY" ]; then
32
- echo "[WARN] STORAGE_ENCRYPTION_KEY is not set in secrets; using local fallback."
33
- export STORAGE_ENCRYPTION_KEY="omniroute_default_storage_key_32bytes"
34
  fi
35
 
36
  if [ -z "$JWT_SECRET" ]; then
37
- echo "[WARN] JWT_SECRET is not set in secrets; using local fallback."
38
- export JWT_SECRET="omniroute_default_jwt_secret_key_32bytes"
39
  fi
40
 
41
  if [ -z "$API_KEY_SECRET" ]; then
42
- echo "[WARN] API_KEY_SECRET is not set in secrets; using local fallback."
43
- export API_KEY_SECRET="omniroute_default_api_key_secret_32bytes"
44
  fi
45
 
46
  if [ -z "$INITIAL_PASSWORD" ]; then
47
- echo "[WARN] INITIAL_PASSWORD is not set in secrets; using local fallback."
48
  export INITIAL_PASSWORD="admin123"
49
  fi
50
 
@@ -332,10 +332,10 @@ echo "[BOOT] Background services starting asynchronously..."
332
  export CLI_COMPAT_CURSOR=1
333
  export CLI_COMPAT_QWEN=1
334
  export OMNIROUTE_AUTO_FREE_FALLBACK_TO_FULL_POOL=true
335
- export OMNIROUTE_ALLOW_UNAUTHENTICATED=true
336
- export REQUIRE_API_KEY=false
337
- export OMNIROUTE_REQUIRE_API_KEY=false
338
- export OMNIROUTE_AUTH_REQUIRED=false
339
 
340
  if [ -d "/omniroute" ]; then
341
  cd /omniroute
 
29
 
30
  # ── STEP 1: Master Secret Validation ──────────────────────────────────────────
31
  if [ -z "$STORAGE_ENCRYPTION_KEY" ]; then
32
+ echo "[WARN] STORAGE_ENCRYPTION_KEY is not set in secrets; generating dynamic 32-hex key."
33
+ export STORAGE_ENCRYPTION_KEY=$(python3 -c "import secrets; print(secrets.token_hex(16))" 2>/dev/null || echo "omniroute_default_storage_key_32bytes")
34
  fi
35
 
36
  if [ -z "$JWT_SECRET" ]; then
37
+ echo "[WARN] JWT_SECRET is not set in secrets; generating dynamic 32-hex key."
38
+ export JWT_SECRET=$(python3 -c "import secrets; print(secrets.token_hex(16))" 2>/dev/null || echo "omniroute_default_jwt_secret_key_32bytes")
39
  fi
40
 
41
  if [ -z "$API_KEY_SECRET" ]; then
42
+ echo "[WARN] API_KEY_SECRET is not set in secrets; generating dynamic 32-hex key."
43
+ export API_KEY_SECRET=$(python3 -c "import secrets; print(secrets.token_hex(16))" 2>/dev/null || echo "omniroute_default_api_key_secret_32bytes")
44
  fi
45
 
46
  if [ -z "$INITIAL_PASSWORD" ]; then
47
+ echo "[WARN] INITIAL_PASSWORD is not set in secrets; using default admin password."
48
  export INITIAL_PASSWORD="admin123"
49
  fi
50
 
 
332
  export CLI_COMPAT_CURSOR=1
333
  export CLI_COMPAT_QWEN=1
334
  export OMNIROUTE_AUTO_FREE_FALLBACK_TO_FULL_POOL=true
335
+ export OMNIROUTE_ALLOW_UNAUTHENTICATED=false
336
+ export REQUIRE_API_KEY=true
337
+ export OMNIROUTE_REQUIRE_API_KEY=true
338
+ export OMNIROUTE_AUTH_REQUIRED=true
339
 
340
  if [ -d "/omniroute" ]; then
341
  cd /omniroute
gateway/omniroute.py CHANGED
@@ -77,43 +77,28 @@ async def handle_omniroute_proxy(target: str, request: Request, default_prefix:
77
  }
78
  res = await proxy_http_request(target, request, default_prefix=default_prefix, extra_headers=extra_auth, html_fixup=html_fixup)
79
  if res.status_code in (500, 502, 503) and request.method == "GET" and "html" in request.headers.get("accept", "").lower():
80
- log_content = ""
81
- for log_file in ["/data/omniroute/omniroute.log", "/root/.omniroute/omniroute.log"]:
82
- try:
83
- if os.path.exists(log_file):
84
- with open(log_file, "r", encoding="utf-8", errors="ignore") as f:
85
- lines = f.readlines()
86
- log_content = "".join(lines[-120:])
87
- if log_content:
88
- break
89
- except Exception:
90
- pass
91
-
92
- if log_content:
93
- print(f"[OMNIROUTE SERVER LOGS]\n{log_content}")
94
- safe_logs = log_content.replace("\\", "\\\\").replace("`", "\\`").replace("$", "\\$")
95
- diagnostic_html = f"""<!DOCTYPE html>
96
  <html>
97
  <head>
98
- <title>OmniRoute Server Log Diagnostic</title>
99
  <style>
100
- body {{ font-family: system-ui, monospace; background: #0f172a; color: #f8fafc; padding: 24px; max-width: 1000px; margin: 0 auto; }}
101
- h1 {{ color: #f87171; font-size: 1.4rem; margin-top: 0; }}
102
- p {{ color: #94a3b8; font-size: 0.95rem; }}
103
- pre {{ background: #1e293b; padding: 16px; border-radius: 8px; overflow-x: auto; color: #38bdf8; border: 1px solid #334155; font-size: 0.85rem; line-height: 1.5; }}
 
 
104
  </style>
105
- <script>
106
- console.error("=== OMNIROUTE BACKEND LOGS ===");
107
- console.error(`{safe_logs}`);
108
- </script>
109
  </head>
110
  <body>
111
- <h1>⚠️ OmniRoute Server Diagnostic ({res.status_code})</h1>
112
- <p>Captured backend process logs for <code>{request.url.path}</code>:</p>
113
- <pre>{log_content}</pre>
 
 
114
  </body>
115
  </html>"""
116
- return HTMLResponse(content=diagnostic_html, status_code=res.status_code)
117
  return res
118
 
119
  # ── Dashboard & Admin UI Routes (20128) ──────────────────────────────────────
 
77
  }
78
  res = await proxy_http_request(target, request, default_prefix=default_prefix, extra_headers=extra_auth, html_fixup=html_fixup)
79
  if res.status_code in (500, 502, 503) and request.method == "GET" and "html" in request.headers.get("accept", "").lower():
80
+ diagnostic_html = f"""<!DOCTYPE html>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
81
  <html>
82
  <head>
83
+ <title>OmniRoute Server Status</title>
84
  <style>
85
+ body {{ font-family: system-ui, -apple-system, sans-serif; background: #0f172a; color: #f8fafc; display: flex; align-items: center; justify-content: center; min-height: 100vh; margin: 0; padding: 24px; }}
86
+ .card {{ background: #1e293b; border: 1px solid #334155; padding: 32px; border-radius: 12px; max-width: 500px; text-align: center; shadow: 0 10px 25px rgba(0,0,0,0.5); }}
87
+ h1 {{ color: #f87171; font-size: 1.4rem; margin: 0 0 12px; }}
88
+ p {{ color: #94a3b8; font-size: 0.95rem; line-height: 1.5; margin: 0 0 20px; }}
89
+ a {{ color: #38bdf8; text-decoration: none; font-weight: 600; }}
90
+ a:hover {{ text-decoration: underline; }}
91
  </style>
 
 
 
 
92
  </head>
93
  <body>
94
+ <div class="card">
95
+ <h1>⚠️ OmniRoute Service Temporarily Unavailable</h1>
96
+ <p>OmniRoute backend is currently initializing or completing a background task. Status code: {res.status_code}.</p>
97
+ <p><a href="/dashboard">Reload Dashboard &rarr;</a></p>
98
+ </div>
99
  </body>
100
  </html>"""
101
+ return HTMLResponse(content=diagnostic_html, status_code=res.status_code)
102
  return res
103
 
104
  # ── Dashboard & Admin UI Routes (20128) ──────────────────────────────────────
gateway/utils.py CHANGED
@@ -179,23 +179,6 @@ async def proxy_http_request(
179
  continue
180
  if method in ("GET", "HEAD") and ("html" in request.headers.get("accept", "").lower() or request.url.path in ("/", "/index.html", "/healthz", "/health", "/omniroute", "/dashboard", "/jellyfin") or request.url.path.startswith("/dashboard") or request.url.path.startswith("/omniroute") or request.url.path.startswith("/jellyfin")):
181
  service_name = "OmniRoute AI Gateway" if default_prefix == "/omniroute" or "omniroute" in request.url.path or "dashboard" in request.url.path else ("Jellyfin Media Server" if default_prefix == "/jellyfin" or "jellyfin" in request.url.path else "Open WebUI")
182
-
183
- log_content = ""
184
- if service_name == "OmniRoute AI Gateway":
185
- for log_file in ["/data/omniroute/omniroute.log", "/root/.omniroute/omniroute.log"]:
186
- try:
187
- if os.path.exists(log_file):
188
- with open(log_file, "r", encoding="utf-8", errors="ignore") as f:
189
- lines = f.readlines()
190
- log_content = "".join(lines[-100:])
191
- if log_content:
192
- break
193
- except Exception:
194
- pass
195
-
196
- safe_logs = log_content.replace("\\", "\\\\").replace("`", "\\`").replace("$", "\\$").replace("\n", "\\n") if log_content else ""
197
- log_box = f'<pre style="text-align:left; background:#0d1117; padding:12px; border-radius:6px; color:#38bdf8; font-size:0.8rem; overflow-x:auto; max-height:200px; margin-top:1rem; border:1px solid #30363d;">{log_content}</pre>' if log_content else ''
198
- log_script = f'<script>console.error("=== OMNIROUTE LOGS ==="); console.error(`{safe_logs}`);</script>' if safe_logs else ''
199
 
200
  html_retry = f"""<!DOCTYPE html>
201
  <html>
@@ -205,27 +188,25 @@ async def proxy_http_request(
205
 
206
  <style>
207
  body {{ font-family: system-ui, -apple-system, sans-serif; background: #0d1117; color: #c9d1d9; display: flex; align-items: center; justify-content: center; min-height: 100vh; margin: 0; padding: 20px; box-sizing: border-box; }}
208
- .card {{ text-align: center; background: #161b22; padding: 2.5rem; border-radius: 12px; border: 1px solid #30363d; max-width: 700px; width: 100%; }}
209
  .spinner {{ width: 40px; height: 40px; border: 4px solid #30363d; border-top-color: #58a6ff; border-radius: 50%; animation: spin 1s linear infinite; margin: 0 auto 1.5rem; }}
210
  @keyframes spin {{ to {{ transform: rotate(360deg); }} }}
211
  h2 {{ margin: 0 0 0.5rem; color: #f0f6fc; font-size: 1.25rem; }}
212
  p {{ color: #8b949e; font-size: 0.9rem; margin: 0; }}
213
  </style>
214
- {log_script}
215
  </head>
216
  <body>
217
  <div class="card">
218
  <div class="spinner"></div>
219
  <h2>Initializing {service_name}...</h2>
220
  <p>The service is completing backend startup. This page will refresh automatically in 3 seconds.</p>
221
- {log_box}
222
  </div>
223
  </body>
224
  </html>"""
225
  return Response(content=html_retry, status_code=200, headers={"Retry-After": "3", "Refresh": "3"}, media_type="text/html")
226
 
227
  return Response(
228
- content=f"<h2>502 Service Unavailable</h2><p>Upstream starting: {exc}</p>",
229
  status_code=502,
230
  media_type="text/html",
231
  )
 
179
  continue
180
  if method in ("GET", "HEAD") and ("html" in request.headers.get("accept", "").lower() or request.url.path in ("/", "/index.html", "/healthz", "/health", "/omniroute", "/dashboard", "/jellyfin") or request.url.path.startswith("/dashboard") or request.url.path.startswith("/omniroute") or request.url.path.startswith("/jellyfin")):
181
  service_name = "OmniRoute AI Gateway" if default_prefix == "/omniroute" or "omniroute" in request.url.path or "dashboard" in request.url.path else ("Jellyfin Media Server" if default_prefix == "/jellyfin" or "jellyfin" in request.url.path else "Open WebUI")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
182
 
183
  html_retry = f"""<!DOCTYPE html>
184
  <html>
 
188
 
189
  <style>
190
  body {{ font-family: system-ui, -apple-system, sans-serif; background: #0d1117; color: #c9d1d9; display: flex; align-items: center; justify-content: center; min-height: 100vh; margin: 0; padding: 20px; box-sizing: border-box; }}
191
+ .card {{ text-align: center; background: #161b22; padding: 2.5rem; border-radius: 12px; border: 1px solid #30363d; max-width: 500px; width: 100%; }}
192
  .spinner {{ width: 40px; height: 40px; border: 4px solid #30363d; border-top-color: #58a6ff; border-radius: 50%; animation: spin 1s linear infinite; margin: 0 auto 1.5rem; }}
193
  @keyframes spin {{ to {{ transform: rotate(360deg); }} }}
194
  h2 {{ margin: 0 0 0.5rem; color: #f0f6fc; font-size: 1.25rem; }}
195
  p {{ color: #8b949e; font-size: 0.9rem; margin: 0; }}
196
  </style>
 
197
  </head>
198
  <body>
199
  <div class="card">
200
  <div class="spinner"></div>
201
  <h2>Initializing {service_name}...</h2>
202
  <p>The service is completing backend startup. This page will refresh automatically in 3 seconds.</p>
 
203
  </div>
204
  </body>
205
  </html>"""
206
  return Response(content=html_retry, status_code=200, headers={"Retry-After": "3", "Refresh": "3"}, media_type="text/html")
207
 
208
  return Response(
209
+ content=f"<h2>502 Service Unavailable</h2><p>Service initializing. Please retry in a few seconds.</p>",
210
  status_code=502,
211
  media_type="text/html",
212
  )
tg_streamer.py CHANGED
@@ -194,11 +194,18 @@ async def trigger_jellyfin_scan():
194
  continue
195
  logger.warning(f"Could not trigger Jellyfin library refresh after retries: {e}")
196
 
 
 
197
  @routes.post("/")
198
  @routes.post("/telegram-webhook")
199
  @routes.post("/webhook")
200
  async def telegram_webhook(request):
201
  try:
 
 
 
 
 
202
  data = await request.json()
203
  post = data.get("channel_post") or data.get("message")
204
  if not post:
 
194
  continue
195
  logger.warning(f"Could not trigger Jellyfin library refresh after retries: {e}")
196
 
197
+ WEBHOOK_SECRET = os.environ.get("TG_WEBHOOK_SECRET")
198
+
199
  @routes.post("/")
200
  @routes.post("/telegram-webhook")
201
  @routes.post("/webhook")
202
  async def telegram_webhook(request):
203
  try:
204
+ if WEBHOOK_SECRET:
205
+ token = request.headers.get("X-Telegram-Bot-Api-Secret-Token")
206
+ if token != WEBHOOK_SECRET:
207
+ return web.json_response({"ok": False, "error": "Unauthorized"}, status=401)
208
+
209
  data = await request.json()
210
  post = data.get("channel_post") or data.get("message")
211
  if not post: