feat: split honeypot and dashboard into separate services on dedicated ports
Some checks failed
Build and Push Docker Image / build-and-push (push) Has been cancelled
Some checks failed
Build and Push Docker Image / build-and-push (push) Has been cancelled
- Honeypot listens on port 5010 - Dashboard listens on port 5123 at URL / - Add app_dashboard.py as standalone FastAPI app - Add start.sh to launch both uvicorn processes - Fix dashboard_path computation to return "" at root (avoid double-slash URLs) - Update Dockerfile, docker-compose.yaml, config.yaml, config.py Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -15,16 +15,18 @@ RUN pip install --no-cache-dir --upgrade pip && \
|
||||
COPY src/ /app/src/
|
||||
COPY wordlists.json /app/
|
||||
COPY entrypoint.sh /app/
|
||||
COPY start.sh /app/
|
||||
COPY config.yaml /app/
|
||||
|
||||
RUN useradd -m -u 1000 krawl && \
|
||||
mkdir -p /app/logs /app/data /app/exports && \
|
||||
chown -R krawl:krawl /app && \
|
||||
chmod +x /app/entrypoint.sh
|
||||
chmod +x /app/entrypoint.sh /app/start.sh
|
||||
|
||||
EXPOSE 5000
|
||||
EXPOSE 5010
|
||||
EXPOSE 5123
|
||||
|
||||
ENV PYTHONUNBUFFERED=1
|
||||
|
||||
ENTRYPOINT ["/app/entrypoint.sh"]
|
||||
CMD ["uvicorn", "app:app", "--host", "0.0.0.0", "--port", "5000", "--app-dir", "src"]
|
||||
CMD ["/app/start.sh"]
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
# Krawl Honeypot Configuration
|
||||
|
||||
server:
|
||||
port: 5000
|
||||
port: 5010
|
||||
delay: 100 # Response delay in milliseconds
|
||||
|
||||
# manually set the server header, if null a random one will be used.
|
||||
@@ -20,10 +20,9 @@ canary:
|
||||
token_tries: 10
|
||||
|
||||
dashboard:
|
||||
# if set to "null" this will Auto-generates random path if not set
|
||||
# can be set to "/dashboard" or similar <-- note this MUST include a forward slash
|
||||
# secret_path: super-secret-dashboard-path
|
||||
secret_path: null
|
||||
port: 5123
|
||||
# Set to empty string "" to serve dashboard at root "/" on its own dedicated port
|
||||
secret_path: ""
|
||||
|
||||
# Password for accessing protected dashboard panels.
|
||||
# If null, a random password will be generated and printed in the logs.
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
---
|
||||
# THIS IS FOR DEVELOPMENT PURPOSES
|
||||
services:
|
||||
krawl:
|
||||
build:
|
||||
@@ -7,12 +6,15 @@ services:
|
||||
dockerfile: Dockerfile
|
||||
container_name: krawl-server
|
||||
ports:
|
||||
- "5000:5000"
|
||||
- "5010:5010" # Honeypot (crawler trap)
|
||||
- "5123:5123" # Dashboard (public UI)
|
||||
environment:
|
||||
- CONFIG_LOCATION=config.yaml
|
||||
# Uncomment to set a custom dashboard password (auto-generated if not set)
|
||||
# - KRAWL_DASHBOARD_PASSWORD=your-secret-password
|
||||
# set this to change timezone, alternatively mount /etc/timezone or /etc/localtime based on the time system management of the host environment
|
||||
# Override ports if needed
|
||||
# - HONEYPOT_PORT=5010
|
||||
# - DASHBOARD_PORT=5123
|
||||
# - TZ=${TZ}
|
||||
volumes:
|
||||
- ./wordlists.json:/app/wordlists.json:ro
|
||||
|
||||
38
src/app.py
38
src/app.py
@@ -10,7 +10,6 @@ import os
|
||||
from contextlib import asynccontextmanager
|
||||
|
||||
from fastapi import FastAPI, Request, Response
|
||||
from fastapi.staticfiles import StaticFiles
|
||||
|
||||
from config import get_config
|
||||
from tracker import AccessTracker, set_tracker
|
||||
@@ -72,21 +71,7 @@ async def lifespan(app: FastAPI):
|
||||
tasks_master = get_tasksmaster()
|
||||
tasks_master.run_scheduled_tasks()
|
||||
|
||||
password_line = ""
|
||||
if config.dashboard_password_generated:
|
||||
password_line = (
|
||||
f"\n\nDASHBOARD PASSWORD (auto-generated)\n{config.dashboard_password}"
|
||||
)
|
||||
|
||||
banner = f"""
|
||||
|
||||
============================================================
|
||||
DASHBOARD AVAILABLE AT
|
||||
{config.dashboard_secret_path}{password_line}
|
||||
============================================================
|
||||
"""
|
||||
app_logger.info(banner)
|
||||
app_logger.info(f"Starting deception server on port {config.port}...")
|
||||
app_logger.info(f"Starting honeypot deception server on port {config.port}...")
|
||||
if config.canary_token_url:
|
||||
app_logger.info(
|
||||
f"Canary token will appear after {config.canary_token_tries} tries"
|
||||
@@ -155,29 +140,10 @@ def create_app() -> FastAPI:
|
||||
access_logger.info(f"[{method}] {client_ip} - {path} - {status}")
|
||||
return response
|
||||
|
||||
# Mount static files for the dashboard
|
||||
config = get_config()
|
||||
secret = config.dashboard_secret_path.lstrip("/")
|
||||
static_dir = os.path.join(os.path.dirname(__file__), "templates", "static")
|
||||
application.mount(
|
||||
f"/{secret}/static",
|
||||
StaticFiles(directory=static_dir),
|
||||
name="dashboard-static",
|
||||
)
|
||||
|
||||
# Import and include routers
|
||||
from routes.honeypot import router as honeypot_router
|
||||
from routes.api import router as api_router
|
||||
from routes.dashboard import router as dashboard_router
|
||||
from routes.htmx import router as htmx_router
|
||||
|
||||
# Dashboard/API/HTMX routes (prefixed with secret path, before honeypot catch-all)
|
||||
dashboard_prefix = f"/{secret}"
|
||||
application.include_router(dashboard_router, prefix=dashboard_prefix)
|
||||
application.include_router(api_router, prefix=dashboard_prefix)
|
||||
application.include_router(htmx_router, prefix=dashboard_prefix)
|
||||
|
||||
# Honeypot routes (catch-all must be last)
|
||||
# Honeypot routes (catch-all)
|
||||
application.include_router(honeypot_router)
|
||||
|
||||
return application
|
||||
|
||||
94
src/app_dashboard.py
Normal file
94
src/app_dashboard.py
Normal file
@@ -0,0 +1,94 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
"""
|
||||
FastAPI application for the Krawl dashboard.
|
||||
Runs on a dedicated port (default 5123) and serves the dashboard at /.
|
||||
"""
|
||||
|
||||
import os
|
||||
from contextlib import asynccontextmanager
|
||||
|
||||
from fastapi import FastAPI, Request, Response
|
||||
from fastapi.staticfiles import StaticFiles
|
||||
|
||||
from config import get_config
|
||||
from database import initialize_database
|
||||
from tasks_master import get_tasksmaster
|
||||
from logger import initialize_logging, get_app_logger
|
||||
|
||||
|
||||
@asynccontextmanager
|
||||
async def lifespan(app: FastAPI):
|
||||
"""Dashboard application startup and shutdown lifecycle."""
|
||||
config = get_config()
|
||||
# Force dashboard path to empty (root) for this dedicated app
|
||||
config.dashboard_secret_path = ""
|
||||
|
||||
initialize_logging(log_level=config.log_level)
|
||||
app_logger = get_app_logger()
|
||||
|
||||
try:
|
||||
app_logger.info(f"Initializing database at: {config.database_path}")
|
||||
initialize_database(config.database_path)
|
||||
app_logger.info("Database ready")
|
||||
except Exception as e:
|
||||
app_logger.warning(
|
||||
f"Database initialization failed: {e}. Continuing with in-memory only."
|
||||
)
|
||||
|
||||
app.state.config = config
|
||||
|
||||
# Start scheduled tasks (handles cache warming, backups, etc.)
|
||||
tasks_master = get_tasksmaster()
|
||||
tasks_master.run_scheduled_tasks()
|
||||
|
||||
password_line = ""
|
||||
if config.dashboard_password_generated:
|
||||
password_line = (
|
||||
f"\n\nDASHBOARD PASSWORD (auto-generated)\n{config.dashboard_password}"
|
||||
)
|
||||
|
||||
banner = f"""
|
||||
|
||||
============================================================
|
||||
DASHBOARD AVAILABLE AT http://0.0.0.0:{config.dashboard_port}/{password_line}
|
||||
============================================================
|
||||
"""
|
||||
app_logger.info(banner)
|
||||
app_logger.info(f"Starting dashboard server on port {config.dashboard_port}...")
|
||||
|
||||
yield
|
||||
|
||||
app_logger.info("Dashboard server shutting down...")
|
||||
|
||||
|
||||
def create_dashboard_app() -> FastAPI:
|
||||
"""Create and configure the dashboard FastAPI application."""
|
||||
application = FastAPI(
|
||||
docs_url=None,
|
||||
redoc_url=None,
|
||||
openapi_url=None,
|
||||
lifespan=lifespan,
|
||||
)
|
||||
|
||||
# Mount static files at /static
|
||||
static_dir = os.path.join(os.path.dirname(__file__), "templates", "static")
|
||||
application.mount(
|
||||
"/static",
|
||||
StaticFiles(directory=static_dir),
|
||||
name="dashboard-static",
|
||||
)
|
||||
|
||||
from routes.api import router as api_router
|
||||
from routes.dashboard import router as dashboard_router
|
||||
from routes.htmx import router as htmx_router
|
||||
|
||||
# All dashboard routes at root prefix
|
||||
application.include_router(dashboard_router, prefix="")
|
||||
application.include_router(api_router, prefix="")
|
||||
application.include_router(htmx_router, prefix="")
|
||||
|
||||
return application
|
||||
|
||||
|
||||
app = create_dashboard_app()
|
||||
@@ -18,7 +18,8 @@ import yaml
|
||||
class Config:
|
||||
"""Configuration class for the deception server"""
|
||||
|
||||
port: int = 5000
|
||||
port: int = 5010
|
||||
dashboard_port: int = 5123
|
||||
delay: int = 100 # milliseconds
|
||||
server_header: str = ""
|
||||
links_length_range: Tuple[int, int] = (5, 15)
|
||||
@@ -173,6 +174,8 @@ class Config:
|
||||
dashboard_path = dashboard.get("secret_path")
|
||||
if dashboard_path is None:
|
||||
dashboard_path = f"/{os.urandom(16).hex()}"
|
||||
elif dashboard_path == "":
|
||||
dashboard_path = ""
|
||||
else:
|
||||
# ensure the dashboard path starts with a /
|
||||
if dashboard_path[:1] != "/":
|
||||
@@ -186,7 +189,8 @@ class Config:
|
||||
dashboard_password_generated = True
|
||||
|
||||
return cls(
|
||||
port=server.get("port", 5000),
|
||||
port=server.get("port", 5010),
|
||||
dashboard_port=dashboard.get("port", 5123),
|
||||
delay=server.get("delay", 100),
|
||||
server_header=server.get("server_header", ""),
|
||||
links_length_range=(
|
||||
|
||||
@@ -19,7 +19,8 @@ router = APIRouter()
|
||||
@router.get("/")
|
||||
async def dashboard_page(request: Request):
|
||||
config = request.app.state.config
|
||||
dashboard_path = "/" + config.dashboard_secret_path.lstrip("/")
|
||||
_path = config.dashboard_secret_path.strip("/")
|
||||
dashboard_path = f"/{_path}" if _path else ""
|
||||
|
||||
# Serve from pre-computed cache when available, fall back to live queries
|
||||
if is_warm():
|
||||
@@ -50,7 +51,8 @@ async def ip_page(ip_address: str, request: Request):
|
||||
try:
|
||||
stats = db.get_ip_stats_by_ip(ip_address)
|
||||
config = request.app.state.config
|
||||
dashboard_path = "/" + config.dashboard_secret_path.lstrip("/")
|
||||
_path = config.dashboard_secret_path.strip("/")
|
||||
dashboard_path = f"/{_path}" if _path else ""
|
||||
|
||||
if stats:
|
||||
# Transform fields for template compatibility
|
||||
|
||||
@@ -17,7 +17,8 @@ router = APIRouter()
|
||||
|
||||
def _dashboard_path(request: Request) -> str:
|
||||
config = request.app.state.config
|
||||
return "/" + config.dashboard_secret_path.lstrip("/")
|
||||
path = config.dashboard_secret_path.strip("/")
|
||||
return f"/{path}" if path else ""
|
||||
|
||||
|
||||
# ── Honeypot Triggers ────────────────────────────────────────────────
|
||||
|
||||
17
start.sh
Executable file
17
start.sh
Executable file
@@ -0,0 +1,17 @@
|
||||
#!/bin/sh
|
||||
set -e
|
||||
|
||||
HONEYPOT_PORT=${HONEYPOT_PORT:-5010}
|
||||
DASHBOARD_PORT=${DASHBOARD_PORT:-5123}
|
||||
|
||||
echo "Starting Krawl honeypot on port $HONEYPOT_PORT..."
|
||||
uvicorn app:app --host 0.0.0.0 --port "$HONEYPOT_PORT" --app-dir src &
|
||||
HONEYPOT_PID=$!
|
||||
|
||||
echo "Starting Krawl dashboard on port $DASHBOARD_PORT..."
|
||||
uvicorn app_dashboard:app --host 0.0.0.0 --port "$DASHBOARD_PORT" --app-dir src &
|
||||
DASHBOARD_PID=$!
|
||||
|
||||
# Wait for either process to exit; if one dies, kill the other
|
||||
wait -n 2>/dev/null || wait
|
||||
kill $HONEYPOT_PID $DASHBOARD_PID 2>/dev/null || true
|
||||
Reference in New Issue
Block a user