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 src/ /app/src/
|
||||||
COPY wordlists.json /app/
|
COPY wordlists.json /app/
|
||||||
COPY entrypoint.sh /app/
|
COPY entrypoint.sh /app/
|
||||||
|
COPY start.sh /app/
|
||||||
COPY config.yaml /app/
|
COPY config.yaml /app/
|
||||||
|
|
||||||
RUN useradd -m -u 1000 krawl && \
|
RUN useradd -m -u 1000 krawl && \
|
||||||
mkdir -p /app/logs /app/data /app/exports && \
|
mkdir -p /app/logs /app/data /app/exports && \
|
||||||
chown -R krawl:krawl /app && \
|
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
|
ENV PYTHONUNBUFFERED=1
|
||||||
|
|
||||||
ENTRYPOINT ["/app/entrypoint.sh"]
|
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
|
# Krawl Honeypot Configuration
|
||||||
|
|
||||||
server:
|
server:
|
||||||
port: 5000
|
port: 5010
|
||||||
delay: 100 # Response delay in milliseconds
|
delay: 100 # Response delay in milliseconds
|
||||||
|
|
||||||
# manually set the server header, if null a random one will be used.
|
# manually set the server header, if null a random one will be used.
|
||||||
@@ -20,10 +20,9 @@ canary:
|
|||||||
token_tries: 10
|
token_tries: 10
|
||||||
|
|
||||||
dashboard:
|
dashboard:
|
||||||
# if set to "null" this will Auto-generates random path if not set
|
port: 5123
|
||||||
# can be set to "/dashboard" or similar <-- note this MUST include a forward slash
|
# Set to empty string "" to serve dashboard at root "/" on its own dedicated port
|
||||||
# secret_path: super-secret-dashboard-path
|
secret_path: ""
|
||||||
secret_path: null
|
|
||||||
|
|
||||||
# Password for accessing protected dashboard panels.
|
# Password for accessing protected dashboard panels.
|
||||||
# If null, a random password will be generated and printed in the logs.
|
# If null, a random password will be generated and printed in the logs.
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
---
|
---
|
||||||
# THIS IS FOR DEVELOPMENT PURPOSES
|
|
||||||
services:
|
services:
|
||||||
krawl:
|
krawl:
|
||||||
build:
|
build:
|
||||||
@@ -7,12 +6,15 @@ services:
|
|||||||
dockerfile: Dockerfile
|
dockerfile: Dockerfile
|
||||||
container_name: krawl-server
|
container_name: krawl-server
|
||||||
ports:
|
ports:
|
||||||
- "5000:5000"
|
- "5010:5010" # Honeypot (crawler trap)
|
||||||
|
- "5123:5123" # Dashboard (public UI)
|
||||||
environment:
|
environment:
|
||||||
- CONFIG_LOCATION=config.yaml
|
- CONFIG_LOCATION=config.yaml
|
||||||
# Uncomment to set a custom dashboard password (auto-generated if not set)
|
# Uncomment to set a custom dashboard password (auto-generated if not set)
|
||||||
# - KRAWL_DASHBOARD_PASSWORD=your-secret-password
|
# - 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}
|
# - TZ=${TZ}
|
||||||
volumes:
|
volumes:
|
||||||
- ./wordlists.json:/app/wordlists.json:ro
|
- ./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 contextlib import asynccontextmanager
|
||||||
|
|
||||||
from fastapi import FastAPI, Request, Response
|
from fastapi import FastAPI, Request, Response
|
||||||
from fastapi.staticfiles import StaticFiles
|
|
||||||
|
|
||||||
from config import get_config
|
from config import get_config
|
||||||
from tracker import AccessTracker, set_tracker
|
from tracker import AccessTracker, set_tracker
|
||||||
@@ -72,21 +71,7 @@ async def lifespan(app: FastAPI):
|
|||||||
tasks_master = get_tasksmaster()
|
tasks_master = get_tasksmaster()
|
||||||
tasks_master.run_scheduled_tasks()
|
tasks_master.run_scheduled_tasks()
|
||||||
|
|
||||||
password_line = ""
|
app_logger.info(f"Starting honeypot deception server on port {config.port}...")
|
||||||
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}...")
|
|
||||||
if config.canary_token_url:
|
if config.canary_token_url:
|
||||||
app_logger.info(
|
app_logger.info(
|
||||||
f"Canary token will appear after {config.canary_token_tries} tries"
|
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}")
|
access_logger.info(f"[{method}] {client_ip} - {path} - {status}")
|
||||||
return response
|
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
|
# Import and include routers
|
||||||
from routes.honeypot import router as honeypot_router
|
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)
|
# Honeypot routes (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)
|
|
||||||
application.include_router(honeypot_router)
|
application.include_router(honeypot_router)
|
||||||
|
|
||||||
return application
|
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:
|
class Config:
|
||||||
"""Configuration class for the deception server"""
|
"""Configuration class for the deception server"""
|
||||||
|
|
||||||
port: int = 5000
|
port: int = 5010
|
||||||
|
dashboard_port: int = 5123
|
||||||
delay: int = 100 # milliseconds
|
delay: int = 100 # milliseconds
|
||||||
server_header: str = ""
|
server_header: str = ""
|
||||||
links_length_range: Tuple[int, int] = (5, 15)
|
links_length_range: Tuple[int, int] = (5, 15)
|
||||||
@@ -173,6 +174,8 @@ class Config:
|
|||||||
dashboard_path = dashboard.get("secret_path")
|
dashboard_path = dashboard.get("secret_path")
|
||||||
if dashboard_path is None:
|
if dashboard_path is None:
|
||||||
dashboard_path = f"/{os.urandom(16).hex()}"
|
dashboard_path = f"/{os.urandom(16).hex()}"
|
||||||
|
elif dashboard_path == "":
|
||||||
|
dashboard_path = ""
|
||||||
else:
|
else:
|
||||||
# ensure the dashboard path starts with a /
|
# ensure the dashboard path starts with a /
|
||||||
if dashboard_path[:1] != "/":
|
if dashboard_path[:1] != "/":
|
||||||
@@ -186,7 +189,8 @@ class Config:
|
|||||||
dashboard_password_generated = True
|
dashboard_password_generated = True
|
||||||
|
|
||||||
return cls(
|
return cls(
|
||||||
port=server.get("port", 5000),
|
port=server.get("port", 5010),
|
||||||
|
dashboard_port=dashboard.get("port", 5123),
|
||||||
delay=server.get("delay", 100),
|
delay=server.get("delay", 100),
|
||||||
server_header=server.get("server_header", ""),
|
server_header=server.get("server_header", ""),
|
||||||
links_length_range=(
|
links_length_range=(
|
||||||
|
|||||||
@@ -19,7 +19,8 @@ router = APIRouter()
|
|||||||
@router.get("/")
|
@router.get("/")
|
||||||
async def dashboard_page(request: Request):
|
async def dashboard_page(request: Request):
|
||||||
config = request.app.state.config
|
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
|
# Serve from pre-computed cache when available, fall back to live queries
|
||||||
if is_warm():
|
if is_warm():
|
||||||
@@ -50,7 +51,8 @@ async def ip_page(ip_address: str, request: Request):
|
|||||||
try:
|
try:
|
||||||
stats = db.get_ip_stats_by_ip(ip_address)
|
stats = db.get_ip_stats_by_ip(ip_address)
|
||||||
config = request.app.state.config
|
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:
|
if stats:
|
||||||
# Transform fields for template compatibility
|
# Transform fields for template compatibility
|
||||||
|
|||||||
@@ -17,7 +17,8 @@ router = APIRouter()
|
|||||||
|
|
||||||
def _dashboard_path(request: Request) -> str:
|
def _dashboard_path(request: Request) -> str:
|
||||||
config = request.app.state.config
|
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 ────────────────────────────────────────────────
|
# ── 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