📝 Read the blog post to find an example of configuring and running the Honey: https://pgj11.com/posts/FortiGate-VPN-SSL-Honeypot/
- Introduction
- Key Features
- Architecture & Data Flow
- Project Structure
- Quick Start
- Log Parsing
- Reporting & Alerting
- Counter-Intelligence
- Production Deployment
- Security Considerations
- TODO
- License
- Credits
- Donate
A deception honeypot that mimics FortiGate VPN-SSL devices to trap brute force attempts, detect deliberately exfiltrated credentials for counter‑intelligence, and report malicious activity to external intelligence feeds (VT, OTX, AbuseIPDB).
- Python & Flask for a pixel-perfect FortiGate SSL-VPN login portal.
- Nginx fronts the portal with TLS and JSON-structured access logs.
- Docker environment via
docker compose. - SQLite stores raw telemetry (credentials & symlink‑exploit probes).
- A suite of helper scripts automate parsing, reporting, and alerting.
- AI-powered summaries via OpenRouter LLM integration (optional).
| 🚩 Feature | Description |
|---|---|
| 📧 Report to Email | HTML dashboard with summary of Honeypot traps, optional AI-generated executive summary via OpenRouter |
| 🌐 Report to OTX | Posts new bad IPs to AlienVault OTX pulses |
| 🔍 Report to VT | Down‑votes & comments IPs on VirusTotal, optionally adds to VT collections |
| 🛡️ Report to AbuseIPDB | Reports malicious IPs to AbuseIPDB with configurable abuse categories |
| 🔐 Check in LDAP | Validates captured credentials against Active Directory / LDAP and raises high-priority alerts if any are valid |
| 🙈 Counter‑intelligence | Flags any password present in exfiltrated_passwords.txt — deliberately exfiltrate credentials and detect attempts to use them |
| Catches FortiGate symlink persistence exploit attempts via Nginx logs. Blog post | |
| 🤖 LLM Summary | Optional AI-generated executive summary in email reports via OpenRouter (any model) |
flowchart LR
A["Attacker"] -->|"HTTPS :10443"| B["Nginx\n(TLS termination,\nJSON access logs)"]
B -->|"Proxy to :5000"| C["Flask Honeypot\n(honey.py)"]
C -->|"Logs credentials"| D["creds.log"]
B -->|"Logs requests"| E["access.log\n(JSON)"]
D --> F["parse.py"]
E --> F
F -->|"Imports & truncates logs"| G["SQLite DB\n(honeypot.db)"]
F -->|"Writes"| H["output_parsing/\n(report.json,\nbad_ips.txt,\nbad_ips_symlink.txt)"]
G --> I["report_to_email"]
G --> J["report_to_vt"]
G --> K["report_to_abuseipdb"]
G --> L["check_in_ldap"]
H --> M["report_to_otx"]
- Nginx terminates TLS on port 10443 and reverse-proxies to Flask. It logs all requests in JSON format and passes the real client IP via
X-Real-IP. - Flask (
honey.py) serves a pixel-perfect FortiGate VPN-SSL login page. Every login attempt is logged tocreds.log(TSV: username, password, IP, timestamp). Login always fails with "Permission denied". - Symlink detection: Nginx logs requests to
/lang/custom/...paths (the symlink persistence exploit vector).parse.pyextracts these into thesymlink_exploitstable. parse.pyreads both log files, inserts records into SQLite, writes output reports, and truncates the logs after processing.- Reporters query the SQLite DB (or output files for OTX) and send data to external services.
Fortigate.VPN-SSL.Honeypot/
├── honey/ # Flask honeypot app
│ ├── Dockerfile # Python 3.14 slim + Flask
│ ├── honey.py # Main app (routes, credential capture)
│ ├── requirements.txt # Flask
│ ├── assets/ # SVG brand assets
│ ├── css/ # FortiGate stylesheets
│ ├── fonts/ # WOFF/WOFF2 fonts
│ └── js/ # Login JS, language bundle
├── nginx/ # Nginx reverse proxy
│ └── dist/conf/
│ ├── nginx.conf # Main config (JSON logging)
│ ├── honey.conf # Site config (TLS, proxy)
│ └── ssl/ # TLS cert/key generation scripts
├── parse.py # Log parser → SQLite + reports
├── report_to_email/ # Email reporter (+ LLM summary)
├── report_to_vt/ # VirusTotal reporter
├── report_to_otx/ # AlienVault OTX reporter
├── report_to_abuseipdb/ # AbuseIPDB reporter
├── check_in_ldap/ # LDAP credential validator
├── docker-compose.yml # Build from source
├── docker-compose-hub.yml # Pull from Docker Hub
├── parse_and_report.service.template # systemd service template
├── parse_and_report.timer.template # systemd timer template
├── exfiltrated_passwords.txt # Counter-intel watch list (create manually)
└── .github/workflows/ # CI: Docker build + Python syntax
There are two options available: pulling the images from Docker Hub or building from the repository.
The Docker images pushed to the repository contain a preloaded TLS configuration.
The nginx image contains an RSA key pair and DH params configured.
This is necessary for the image to be ready to run.
This has two cons:
1. The Honeypot can be fingerprinted.
2. The private key is known to everyone, so consider the communications to your Honeypot compromised.
Because of that I recommend that you modify the TLS configuration in the nginx container or build from the repository.
Use docker-compose-hub.yml to get the images from Docker Hub.
$ sudo docker compose -f ./docker-compose-hub.yml up# 1. Clone & enter repo
$ git clone https://github.com/PeterGabaldon/Fortigate.VPN-SSL.Honeypot.git
$ cd Fortigate.VPN-SSL.Honeypot
# 2. Generate TLS material (one‑off) (Modify certificate data in gen-cert.sh for OPSEC)
$ cd ./nginx/dist/conf/ssl/
$ bash gen-cert.sh
$ bash gen-dhparam.sh 2048
(Ensure that the generated certificates are placed under ./nginx/dist/conf/ssl/)
# 3. Run with docker compose
$ cd ../../../../
$ docker compose up # 🔥 boots nginx & honeypot
# 4. Parse logs & load SQLite
$ ./parse.py # ➜ db/honeypot.db gets populatedThe portal will go live on port 10443 (host network) by default.
To change the port, modify nginx/dist/conf/honey.conf.
Logins are stored in data/log/honey/creds.log until parse.py moves them into SQLite.
Nginx access logs are stored in data/log/nginx/access.log.
parse.py is the central pipeline that moves raw log data into the SQLite database.
What it does:
- Reads
data/log/honey/creds.log(TSV: username, password, IP, timestamp) and imports into thehoneypot_credstable. - Reads
data/log/nginx/access.log(JSON), extracts requests to/lang/custom/...(symlink exploit attempts), and imports into thesymlink_exploitstable. - Writes aggregated output files to
output_parsing/:report.json— Full JSON report with all aggregations.bad_ips.txt— Unique IPs with first-seen timestamp.bad_ips_symlink.txt— IPs that attempted symlink exploits.
- Truncates both log files after processing.
Environment variables (all optional, with defaults):
| Variable | Default | Description |
|---|---|---|
LOG_CREDS |
data/log/honey/creds.log |
Path to honeypot creds log |
LOG_NGINX |
data/log/nginx/access.log |
Path to Nginx access log |
REPORT_DIR |
output_parsing/ |
Output directory for reports |
DB_DIR |
db/ |
Directory for SQLite DB |
DB_FILE |
db/honeypot.db |
SQLite database file path |
SQLite schema:
-- Captured login attempts
CREATE TABLE honeypot_creds (
id INTEGER PRIMARY KEY AUTOINCREMENT,
user TEXT,
password TEXT,
ip TEXT,
ts TEXT
);
-- Symlink persistence exploit attempts (from Nginx logs)
CREATE TABLE symlink_exploits (
id INTEGER PRIMARY KEY AUTOINCREMENT,
ip TEXT,
path TEXT,
ts TEXT
);
-- Created by check_in_ldap when valid credentials are found
CREATE TABLE valid_ldap_creds (
user TEXT,
password TEXT,
ts TEXT
);The recommended way to use the reporting scripts is installing the dependencies for each one in a virtual environment.
$ cd report_to_<service>
$ python3 -m venv .venv
$ . .venv/bin/activate
$ pip install -r requirements.txtEach reporter has its own requirements.txt:
| Script | Dependencies |
|---|---|
report_to_email |
Jinja2, PyYAML, requests, openrouter |
report_to_vt |
vt-py, PyYAML |
report_to_otx |
OTXv2, PyYAML |
report_to_abuseipdb |
requests, PyYAML |
check_in_ldap |
ldap3, PyYAML, Jinja2 |
Sends an HTML email report with honeypot statistics for a configurable time window (default: last 24h).
The email report includes:
- Attempts by IP / by user / by password
- Symlink exploit attempts
- Bad IPs list
- 💥 Exfiltrated credentials (counter-intelligence matches)
- 🚨 Compromised LDAP credentials (if
check_in_ldaphas been run) - 🤖 AI-generated executive summary (optional, via OpenRouter)
Configuration template (report_to_email/email_config.yaml.template):
# LLM Summary (optional – leave openrouter_api_key empty to disable)
openrouter_api_key: ""
openrouter_model: "openai/gpt-3.5-turbo"
system_prompt: "You are a cybersecurity analyst. Summarize the following honeypot data..."
subject: "📊 FortiGate VPN-SSL Honeypot – Daily Report"
from: "Honeypot <honeypot@example.com>" # optional; defaults to username if omitted
to:
- security@example.com
- soc@example.com
smtp:
host: smtp.example.com
port: 465 # 465 = SMTPS, 587 = STARTTLS, 25 = opportunistic
username: honeypot@example.com
password: "s3cr3tP@ssw0rd"
use_ssl: true # true ⇒ implicit TLS (465); false ⇒ STARTTLS (587/25)The LLM summary uses OpenRouter to generate an AI executive summary of the honeypot data. You can use any model available on OpenRouter (e.g., openai/gpt-4o, anthropic/claude-sonnet-4, google/gemini-2.5-flash). Leave openrouter_api_key empty to disable this feature.
The email template can be customized by editing report_to_email/email_template.html.jinja.
Down-votes IPs as malicious and posts comments on VirusTotal. Optionally adds IPs to a VT collection. Uses a per-tag state file to avoid re-reporting.
Configuration template (report_to_vt/vt_config/report_to_vt_bad_ips.yaml.template):
vt_api_key: "<YOUR VT API KEY>"
tag: "FortiGate VPN‑SSL Honeypot" # used for per‑tag state tracking
comment: "IP {ip} was seen bruteforcing FortiGate VPN-SSL at {seen} 🛡️"
collection_id: "" # optional: VT collection ID to add IPs to
honeypot:
ip_file: "../honeypot_bad_ips.txt"Creates or syncs an AlienVault OTX pulse with malicious IPs. If a pulse with the configured name already exists, new IPs are added as indicators; otherwise a new pulse is created.
Configuration template (report_to_otx/otx_config/report_to_otx.config.yaml.template):
otx_api_key: YOUR_OTX_API_KEY_HERE
pulse:
name: "FortiGate VPN-SSL Honeypot"
description: "IPs bruteforcing FortiGate VPN-SSL gathered from Honeypot"
type: "blacklist" # or "threat", "vulnerability", etc.
public: true
tlp: "WHITE" # TLP: WHITE, GREEN, AMBER, RED
honeypot:
ip_file: "./honeypot_bad_ips.txt"Reports malicious IPs to AbuseIPDB with configurable abuse categories. Uses a per-tag state file to avoid re-reporting.
Configuration template (report_to_abuseipdb/abuseipdb_config/report_to_abuseipdb.config.yaml.template):
abuseipdb_api_key: "<YOUR ABUSEIPDB API KEY>"
tag: "FortiGate VPN-SSL Honeypot"
comment: "IP {ip} was seen bruteforcing FortiGate VPN-SSL at {seen} 🛡️"
categories: "18,21"
hours: 24Validates captured credentials against an Active Directory / LDAP server. If a bind succeeds (meaning the credentials are valid), the script:
- Stores the valid credentials in the
valid_ldap_credsSQLite table (picked up by the email reporter as "🚨 Compromised LDAP Credentials"). - Sends a high-priority alert email immediately.
Configuration template (check_in_ldap/ldap_config/ldap_config.yaml.template):
ldap:
# LDAP Server URI.
# - LDAPS is recommended for Windows Active Directory: "ldaps://dc01.example.local:636"
# - StartTLS is also supported: "ldap://dc01.example.local:389" (with start_tls: true)
# - Plain LDAP without StartTLS: "ldap://ldap01.example.local:389" (refused by default unless allow_cleartext_simple_bind: true)
server: "ldaps://dc01.example.local:636"
# Authentication Method: "simple" (default) or "ntlm".
# NTLM requires pycryptodome installed (pip install pycryptodome) and does not need OpenSSL legacy providers.
auth_method: "simple"
# For auth_method: "simple"
domain: "example.local" # DNS domain name for Simple Bind (e.g. user@domain)
# For auth_method: "ntlm"
ntlm_domain: "EXAMPLE" # NetBIOS domain name for NTLM Bind (e.g. DOMAIN\user)
# TLS & Security Configuration
validate_cert: true # Set to false only for testing/lab use to ignore TLS certificate verification
ca_certs_file: null # Path to the CA certificate bundle (PEM format) if using an internal private CA
start_tls: false # Enable StartTLS (must use 'ldap://' and port 389/3268)
allow_cleartext_simple_bind: false # Refuses plain 'ldap://' cleartext binds if false. Enable only for OpenLDAP or lab use.
timeout: 10 # Connection timeout in seconds
# Rate limiting and account lockout protection
max_failures_per_user: 3 # Max allowed failed LDAP binds per user in the tracking window
lockout_window_seconds: 86400 # Tracking window in seconds (e.g., 86400 for 24 hours)
alert_email:
subject: "🚨 HIGH ALERT: Valid Credentials Compromised!"
from: "honeypot@example.com"
to:
- soc@example.com
smtp_host: "smtp.example.com"
smtp_port: 587
smtp_user: "honeypot@example.com"
smtp_pass: "secret"
use_ssl: falseThe following examples use absolute paths to the venv Python. Alternatively, activate the venv first with . .venv/bin/activate.
Report to Email
$ report_to_email/.venv/bin/python3 report_to_email/report_to_email.py --help
usage: report_to_email.py [-h] [--config CONFIG] [--hours HOURS] [--template TEMPLATE]
Send honeypot e‑mail report from SQLite data
optional arguments:
-h, --help show this help message and exit
--config CONFIG
--hours HOURS Time window (h) – default last 24h
--template TEMPLATEReport to VirusTotal
$ report_to_vt/.venv/bin/python3 report_to_vt/report_to_vt.py --help
usage: report_to_vt.py [-h] -c CONFIG [--db DB]
Report new malicious IPs to VirusTotal
optional arguments:
-h, --help show this help message and exit
-c CONFIG, --config CONFIG
vt_config.yaml path
--db DB SQLite DB pathReport to OTX
$ report_to_otx/.venv/bin/python3 report_to_otx/report_to_otx.py --help
usage: report_to_otx.py [-h] [-c CONFIG]
Sync honeypot IPs into an OTX Pulse
optional arguments:
-h, --help show this help message and exit
-c CONFIG, --config CONFIG
path to YAML configReport to AbuseIPDB
$ report_to_abuseipdb/.venv/bin/python3 report_to_abuseipdb/report_to_abuseipdb.py --help
usage: report_to_abuseipdb.py [-h] -c CONFIG [--db DB]
Report new malicious IPs to AbuseIPDB
optional arguments:
-h, --help show this help message and exit
-c CONFIG, --config CONFIG
abuseipdb config path
--db DB SQLite DB pathCheck in LDAP (no CLI arguments – uses config file at check_in_ldap/ldap_config/ldap_config.yaml):
$ check_in_ldap/.venv/bin/python3 check_in_ldap/check_ldap.py# Parse logs into SQLite
$ ./parse.py
# Email report (last 24h)
$ report_to_email/.venv/bin/python3 report_to_email/report_to_email.py --config report_to_email/email_config.yaml
# VirusTotal
$ report_to_vt/.venv/bin/python3 report_to_vt/report_to_vt.py -c report_to_vt/vt_config/report_to_vt_bad_ips.yaml
# OTX
$ report_to_otx/.venv/bin/python3 report_to_otx/report_to_otx.py -c report_to_otx/otx_config/report_to_otx.config.yaml
# AbuseIPDB
$ report_to_abuseipdb/.venv/bin/python3 report_to_abuseipdb/report_to_abuseipdb.py -c report_to_abuseipdb/abuseipdb_config/report_to_abuseipdb.config.yaml
# LDAP Check
$ check_in_ldap/.venv/bin/python3 check_in_ldap/check_ldap.pyThe honeypot isn't just for telemetry—it actively hunts for leaked or deliberately planted passwords you care about.
- Watch-list file
Drop one password per line into
exfiltrated_passwords.txtat the repo root. Example:S3cr3tP@ssw0rd th1sIsB41t
The email‐report script loads that watch-list and flags any match during the selected time-window.
A dedicated table "💥 Exfiltrated Credentials" appears in the email report, showing the IP addresses along with the timestamp that used the credentials that were deliberately exfiltrated.
Ideas to exfiltrate credentials:
- Run stealer in a controlled environment
- Write the credentials in pastebin
- Sell the credentials in a forum
- ...
In the repository there are templates ready to schedule parsing using systemd service and timer (parse_and_report.service.template & parse_and_report.timer.template).
$ sudo tee /etc/systemd/system/fortihoney-parse.service <<'EOF'
[Unit]
Description=Parse and report FortiGate VPN-SSL Honeypot service
[Service]
User=fortihoney
Group=fortihoney
WorkingDirectory=/home/fortihoney/Fortigate.VPN-SSL.Honeypot/
Type=oneshot
ExecStart=/home/fortihoney/Fortigate.VPN-SSL.Honeypot/parse.py
ExecStart=-/home/fortihoney/Fortigate.VPN-SSL.Honeypot/report_to_otx/.venv/bin/python3 report_to_otx/report_to_otx.py -c report_to_otx/otx_config/report_to_otx.config.yaml
ExecStart=-/home/fortihoney/Fortigate.VPN-SSL.Honeypot/report_to_otx/.venv/bin/python3 report_to_otx/report_to_otx.py -c report_to_otx/otx_config/report_to_otx.config.symlink.yaml
ExecStart=-/home/fortihoney/Fortigate.VPN-SSL.Honeypot/report_to_vt/.venv/bin/python3 report_to_vt/report_to_vt.py -c report_to_vt/vt_config/report_to_vt_bad_ips.yaml
ExecStart=-/home/fortihoney/Fortigate.VPN-SSL.Honeypot/report_to_vt/.venv/bin/python3 report_to_vt/report_to_vt.py -c report_to_vt/vt_config/report_to_vt_bad_ips.symlink.yaml
EOF
$ sudo tee /etc/systemd/system/fortihoney-parse.timer <<'EOF'
[Unit]
Description=Parse and report FortiGate VPN-SSL Honeypot service timer
[Timer]
OnCalendar=*:0/30
Persistent=true
[Install]
WantedBy=timers.target
EOF
sudo systemctl enable --now fortihoney-parse.timerThe logs are written as root (inside the container). Allow the service user write access in order for the parser script (parse.py) to be able to clear the logs:
sudo setfacl -m u:fortihoney:rw data/log/honey/creds.log data/log/nginx/access.log- Read-only containers: Both Docker containers run with
read_only: truefor defense in depth. - Input sanitization: The honeypot sanitizes all captured inputs before logging (prevents log injection and shell metacharacter attacks). IP addresses are validated with a strict regex.
- Security headers: All responses include HSTS, CSP, X-Frame-Options, X-Content-Type-Options, and X-XSS-Protection headers.
- Network isolation: The Flask app only binds to
127.0.0.1:5000and is only accessible through the Nginx reverse proxy. - Secrets management: Never commit API keys, SMTP/LDAP credentials, generated TLS private keys, SQLite databases, or production logs. Start from
*.templateconfig files and keep real configs local. - TLS material: If using Docker Hub images, replace the bundled TLS certificates and keys for production deployment to avoid fingerprinting and key exposure.
- Report to OTX
- Report to VT
- Report to Email
- Store data in SQLite and clear logs when done
- Documentation
- Github Actions - Docker
- Github Actions - Python
- Report to AbuseIPDB
- VT collection
- Detect deliberately exfiltrated credentials
- Check credentials in LDAP and raise High Alert
- Report summary with LLM (OpenRouter)
- In some cases, jq may fail if so many credentials are to parse
- ...
Any ideas and PRs are welcome!
This project is licensed under the MIT License.
Please consider supporting its development — every coffee fuels more open-source defense!
