Quickstart¶
Two ways to run HoneyDjango:
- Docker Compose — clone the repo and run the bundled demo project. Fastest way to see everything working.
- pip install — add the
honeydjpackage to an existing Django project.
Prerequisites¶
- Docker (with Compose) for the container path, or Python 3.11+ for the pip path
- PostgreSQL — required; the models use
ArrayFieldand JSONB - Redis — Channels layer, Celery broker, cache
Option A: Docker Compose¶
git clone https://github.com/bistasulove/honeydj.git
cd honeydj
cp .env.example .env
docker compose up
This starts PostgreSQL 16, Redis 7, the ASGI web server (Daphne, port 8000), a Celery worker, and Celery beat. Migrations and static collection run automatically on startup.
Then, in a second terminal:
# Create your admin login
docker compose run --rm web python manage.py createsuperuser
# Seed the six default decoy routes (/.env, /wp-admin/, /admin/, …)
docker compose run --rm web python manage.py seed_decoy_routes
# Optional: seed example alert rules (created disabled — configure & enable in admin)
docker compose run --rm web python manage.py seed_default_alert_rules
Option B: pip into an existing project¶
pip install honeydj # your project already has a Postgres driver
pip install "honeydj[postgres]" # or bring psycopg 3 along
1. Settings — at the end of your settings.py:
from honeydj.contrib.quickstart import apply_honeydj_settings
MIDDLEWARE = [
"django.middleware.security.SecurityMiddleware",
"honeydj.honeypot.middleware.HoneyMiddleware", # right after SecurityMiddleware
# ... the rest of your middleware ...
]
REDIS_URL = "redis://localhost:6379/0" # optional; defaults derive from this
apply_honeydj_settings(globals())
apply_honeydj_settings appends the honeydj apps (plus channels) to INSTALLED_APPS and fills in Channels, Celery, and honeydj runtime settings without overwriting anything you already set. It deliberately never touches MIDDLEWARE, DATABASES, or your URLconf — those three are yours:
MIDDLEWARE: addHoneyMiddlewareyourself, directly afterSecurityMiddleware, so decoy hits are captured before session/auth middleware runs.DATABASES: must point at PostgreSQL.
2. URLs — decoys go at the site root, last, so they never shadow real routes and their attack paths look authentic:
urlpatterns = [
# ... your real routes ...
path("intel/dashboard/", include("honeydj.dashboard.urls", namespace="dashboard")),
path("api/events/", include("honeydj.events.urls", namespace="events")),
path("api/feeds/", include("honeydj.feeds.urls", namespace="feeds")),
path("", include("honeydj.honeypot.urls", namespace="honeypot")), # keep last
]
Hide the dashboard
Mount the dashboard behind a non-obvious prefix — it shares the admin's auth, and this host is deliberately attracting attackers. The demo project mounts admin and dashboard at /hd-<ADMIN_URL_SUFFIX>/, with the suffix set from an environment variable.
3. Migrate and run:
python manage.py migrate
python manage.py seed_decoy_routes
celery -A yourproject worker -Q default,enrichment,alerts -l info
celery -A yourproject beat -l info # hourly threat-feed purge
The live map and event stream need an ASGI server (Daphne/Uvicorn) with a ProtocolTypeRouter routing honeydj.events.routing.websocket_urlpatterns — the repo's config/asgi.py is a working example. Everything else works under plain WSGI.
First login¶
Open the admin. In the Docker demo that's http://localhost:8000/hd-<ADMIN_URL_SUFFIX>/ (the suffix comes from your .env); in your own project it's wherever you mounted admin.site.urls. Log in with your superuser and you'll find:
- Honey events — every captured hit, newest first
- Attacker profiles — per-IP dossiers with threat score and tags
- Decoy routes — the trap surface, editable live (changes apply within seconds, no restart)
- Alert rules — notification conditions, with a "Send test alert" action
- Canary tokens — trip-wire tokens and their trigger state
The dashboard (live map + event stream) lives next to the admin — in the demo at /hd-<ADMIN_URL_SUFFIX>/dashboard/.
Link the dashboard from the admin
The stock admin has no sidebar, so nothing points at the Live Map out of the box. To add header links on every admin page, drop this override into a template directory of your own as admin/base_site.html (it must extend admin/base.html — extending admin/base_site.html from a file that shadows it would recurse). The demo project ships the same file at templates/admin/base_site.html.
{% extends "admin/base.html" %}
{% load i18n %}
{% block title %}{% if subtitle %}{{ subtitle }} | {% endif %}{{ title }} | {{ site_title|default:_('Django site admin') }}{% endblock %}
{% block branding %}
<div id="site-name"><a href="{% url 'admin:index' %}">{{ site_header|default:_('Django administration') }}</a></div>
{% if user.is_anonymous %}
{% include "admin/color_theme_toggle.html" %}
{% endif %}
{% endblock %}
{% block userlinks %}
<a href="{% url 'dashboard:map' %}">Live Map</a> /
<a href="{% url 'dashboard:export' %}">Export data</a> /
{{ block.super }}
{% endblock %}
Verify it's working¶
Fire a simulated scanner wave through the real capture pipeline:
# Docker:
docker compose run --rm web python manage.py simulate_scanner
# pip install:
python manage.py simulate_scanner
The default scenario replays ~15 events from five IPs across four decoy types — known-scanner JA3 fingerprints, a SQL-injection payload, a canary-token trip, and an attacker whose score climbs high enough to fire a demo alert rule. The command waits for the Celery worker to enrich everything, then prints a pipeline report: events captured/enriched, attacker profiles with scores and tags, canary state, and which alert rules fired.
Keep the dashboard open while it runs (--watch paces one event at a time) and you'll see rows appear in the live table and markers pulse on the map.
You can also just hit a decoy yourself:
curl -i http://localhost:8000/.env
You'll get a convincing fake .env back — and a new event in the admin.
GeoIP is optional
The map needs coordinates, which come from MaxMind's free GeoLite2-City database (not distributable on PyPI). Download it from MaxMind and set GEOIP_PATH to its directory. Without it, events are still captured and enriched — they just carry no geo data, and the map stays empty.