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

- 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:
2026-03-19 20:16:35 +01:00
parent a0700d1960
commit 50a20734ff
9 changed files with 139 additions and 52 deletions

View File

@@ -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"]

View File

@@ -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.

View File

@@ -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

View File

@@ -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
View 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()

View File

@@ -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=(

View File

@@ -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

View File

@@ -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
View 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