Group 11 · Howest Hogeschool ·
group11.hp.edu.technet.howest.be
Lars Nolf, René Ruts, Giel Van Poppel and Dylan Van Goethem
The project contains two distinct honeypots: a web-based admin panel decoy and a fake MySQL database server.
The /adminpanel route is a honeypot a convincing but completely fake admin panel designed to lure and log attackers. The attacker will find the path at "robots.txt"
- Looks identical to the real
/adminpanel - Touches zero real data because everything is hardcoded
- Every action (delete, edit, create) is simulated
- All suspicious activity is logged to Kibana via the security log channel
Visitor hits /adminpanel
│
▼
HoneypotLogger middleware runs
│
├── Not honeypot-authenticated? → redirect to login wall
│
└── Authenticated? → serve fake panel + log the visit
The honeypot has its own separate session-based authentication, completely independent of real Laravel auth.
Logging into the honeypot does not grant any real access.
The fake login form accepts and reacts to several input types:
| Input | Result | Kibana event |
|---|---|---|
admin / B4n44n! |
Access granted | login_success |
SQLi pattern (e.g. ' OR 1=1 --) |
Simulated bypass | login_sqli_bypass |
XSS payload (e.g. <script>alert(1)</script>) |
Blocked warning shown | login_xss_attempt |
| Wrong credentials | Rejected attempts counted | login_failed |
| 5 failed attempts | IP locked for 10 minutes | ip_blocked_fail2ban |
| Retry during ban | +1 minute added to ban | login_blocked_retry |
After 5 failed login attempts, the attacker's IP is blocked for 10 minutes using Laravel's cache.
Every retry during the ban:
- Adds +1 minute to the lockout timer
- Shows the message:
"your ban just increased to X minutes."
A live countdown timer (MM:SS) ticks down in the browser. The lockout duration and hit count are both recorded in Kibana.
app/
└── Http/
├── Controllers/
│ └── Honeypot/
│ └── HoneypotController.php ← all honeypot logic + fake data
└── Middleware/
└── HoneypotLogger.php ← auth guard + security logging
resources/views/
└── adminpanel/
├── index.blade.php ← login wall / dashboard
├── metrics.blade.php ← fake site metrics page
├── products/
│ ├── index.blade.php
│ ├── create.blade.php
│ └── edit.blade.php
├── users/
│ └── index.blade.php
└── orders/
├── index.blade.php
└── show.blade.php
All data is hardcoded in the controller no database queries are ever made.
| Data | Source |
|---|---|
| Products | 60 real product names, brands & prices hardcoded as PHP arrays |
| Users | 4 fake admins (@pharmaproject.com) + the real logged-in user as a non-admin |
| Orders | Generated in-memory from the fake users and products |
Deleting a product or user only removes it from the PHP session.
On the next page load or new session, everything reappears automatically.
Nothing is ever written to or deleted from the real database.
Every honeypot visit and action is logged to the security log channel using the same LogsActivity trait as the rest of the application keeping log shape consistent in Kibana.
| Event | Level | Trigger |
|---|---|---|
HONEYPOT access |
info |
Any GET request to /adminpanel/* |
HONEYPOT access |
warning |
Any POST / PATCH / DELETE request |
HONEYPOT: login_success |
info |
Correct credentials entered |
HONEYPOT: login_failed |
warning |
Wrong credentials |
HONEYPOT: login_sqli_bypass |
warning |
SQLi pattern matched |
HONEYPOT: login_xss_attempt |
warning |
XSS pattern detected |
HONEYPOT: ip_blocked_fail2ban |
warning |
IP locked after 5 failures |
HONEYPOT: login_blocked_retry |
warning |
Retry attempt while banned |
Logs are shipped via Filebeat from storage/logs/security.log to Elasticsearch.
All routes live inside the auth middleware group, prefixed with /adminpanel.
Route::prefix('adminpanel')->middleware(HoneypotLogger::class)->group(function () {
Route::get('/', [HoneypotController::class, 'index']);
Route::post('/login', [HoneypotController::class, 'loginPost']);
// Products
Route::get('/products', [HoneypotController::class, 'products']);
Route::get('/products/create', [HoneypotController::class, 'productCreate']);
Route::post('/products', [HoneypotController::class, 'productStore']);
Route::get('/products/{id}/edit', [HoneypotController::class, 'productEdit']);
Route::patch('/products/{id}', [HoneypotController::class, 'productUpdate']);
Route::delete('/products/{id}', [HoneypotController::class, 'productDestroy']);
Route::patch('/products/{id}/restore', [HoneypotController::class, 'productRestore']);
// Orders
Route::get('/orders', [HoneypotController::class, 'orders']);
Route::get('/orders/{id}', [HoneypotController::class, 'orderShow']);
// Users
Route::get('/users', [HoneypotController::class, 'users']);
Route::delete('/users/{id}', [HoneypotController::class, 'userDestroy']);
Route::patch('/users/{id}/restore', [HoneypotController::class, 'userRestore']);
Route::patch('/users/{id}/toggle-admin', [HoneypotController::class, 'userToggleAdmin']);
// Metrics
Route::get('/metrics', [HoneypotController::class, 'metrics']);
});A fake MySQL server runs on port 3306 using the mysql-mimic library. It presents itself as a real MySQL instance and accepts all credentials — every login attempt succeeds to maximise attacker engagement. The server logs:
- Every login attempt with the supplied username and password.
- Every SQL query with the source IP, username, active database, and the full query text.
- Disconnection events.
All events go to
/var/log/mysql-honeypot.log.
To make the deception convincing, the server returns plausible fake data for common reconnaissance queries. Some examples are listed below:
| Query | Fake response |
|---|---|
SHOW DATABASES |
pharmacy_db |
SHOW TABLES |
products, users, logs, priv_prod |
SELECT on products |
honey pharmacy products featured on our main webshop |
SELECT on users |
Fake usernames with activity logs |
SELECT on priv_prod |
Fictitious controlled substances (to lure illegal intent) |
SELECT on logs |
Fabricated activity log with realistic IPs and timestamps |
On top of that the honeypot also provides interactive commands the attacker can run. These commands will also result in the honeypot database being altered:
| Query | Fake response |
|---|---|
DROP DATABASE <database_name> |
drops all contents of the given database |
DELETE FROM <table_name> WHERE <condition> |
delete selected content from a provided table |
Other commands the attacker tries to execute will result in a custom given error message:
ERROR 1045 (28000): Access not permitted from this IP address
Beyond the honeypot itself, three other mechanisms contribute to threat detection:
Failed login tracking (LogFailedLogin listener): Every failed login attempt is logged as a warning. If the same email address fails 5 or more times within 10 minutes, an alert-level brute-force event is emitted.
Session IP monitoring (SecurityMonitor middleware): The user's IP is stored in the session on each request. If it changes mid-session, an alert is logged — catching session hijacking attempts.
Admin panel scanning detection (EnsureAdmin middleware): The real admin panel logs every access and tracks per-IP request counts per route. Exceeding 20 requests to the same admin route triggers a scanning alert.
The application is a Laravel 12 pharmacy web shop. The following security measures are in place across the stack.
The Nginx configuration (config-main/) enforces a strict transport policy:
- HTTPS-only: HTTP on port 80 is permanently redirected (
301) to HTTPS. - TLS 1.3 only: Older TLS versions are disabled; server cipher preference is off, deferring to the client's ordering.
- HTTP/3 (QUIC): Enabled alongside HTTP/2 on port 443 via
Alt-Svcheader advertisement. server_tokens off: Nginx version is not disclosed in responses or error pages.
The following headers are set globally in nginx.conf and repeated for the /static/ location block:
| Header | Value | Purpose |
|---|---|---|
Cache-Control |
no-cache, no-store, must-revalidate |
Prevents browsers from caching sensitive responses |
X-Content-Type-Options |
nosniff |
Blocks MIME-type sniffing |
X-Frame-Options |
DENY |
Prevents clickjacking via iframes |
Content-Security-Policy |
default-src 'self' |
Restricts resource loading to same origin |
Static assets use Cache-Control: public, immutable with a 1-year expires header for aggressive caching, while maintaining the security headers above.
A login rate limit zone is defined in nginx.conf:
limit_req_zone $binary_remote_addr zone=mylimit:10m rate=10r/s;
This zone is applied to the /login/ location with a burst of 20 and nodelay, capping each IP to 10 login requests per second before requests are rejected.
Authentication & authorisation
- Role-based access control via an
is_adminflag on theUsermodel. - The
EnsureAdminmiddleware protects all/adminroutes and returns a404(not403) for non-admin users, avoiding route disclosure. - Sessions are stored server-side in the database driver with a 120-minute lifetime. Password policy
A custom StrongPassword validation rule (app/Rules/StrongPassword.php) enforces a minimum entropy of 72 bits, calculated from character set diversity (lowercase, uppercase, digits, symbols) and password length. This goes beyond simple length or complexity rules.
Image upload hardening
The ImageSanitizer service (app/Services/ImageSanitizer.php) prevents polyglot file attacks and steganography payloads:
- Magic-byte validation: only files starting with the JPEG (
\xFF\xD8\xFF) or PNG (\x89PNG) signatures are accepted. - Full decode and re-encode via PHP's GD library: the image is decoded to a pixel buffer and then written back as a new file, stripping any embedded scripts, metadata, or payloads in the original binary.
- Output is saved as WebP, so the extension cannot be used as an attack vector. Structured activity logging
All controllers use the LogsActivity trait to write structured JSON events to dedicated Monolog channels:
| Channel | Log file | Events |
|---|---|---|
security |
storage/logs/security.log |
Failed logins, brute force, honeypot hits, session anomalies |
admin |
storage/logs/admin.log |
Admin panel access and mutations |
orders |
storage/logs/orders.log |
Order creation and viewing |
products |
storage/logs/products.log |
Product CRUD actions |
users |
storage/logs/users.log |
User CRUD and role changes |
Every log entry includes a timestamp, a per-request UUID (from X-Request-ID or generated), the authenticated user ID, IP address, user agent, route, and HTTP method.
CSRF protection
Laravel's built-in CSRF middleware is active on all state-changing routes. The _token field is excluded from honeypot log output to avoid capturing it.
SSL configuration
The server uses a TLS certificate issued for group11.hp.edu.technet.howest.be, stored at /etc/ssl/certs/group11-csr.crt with its key at /etc/ssl/private/server.key.
Five dashboards are configured in Kibana to monitor the web server, application layer, and honeypot activity. All dashboards are built on the group-11-filebeat index, fed by Filebeat running on the web server.
Monitors all incoming HTTP traffic to the Nginx web server. Built using the Filebeat Nginx module which automatically parses access and error logs into structured fields.
| Panel | Type | Description |
|---|---|---|
| Requests over time | Line chart | Total HTTP requests per 30 seconds — useful for spotting traffic spikes during pentesting |
| HTTP status codes breakdown | Donut chart | Distribution of 200, 404, 500 and other response codes |
| Top IP addresses | Table | Most active client IPs by request count |
| Top requested URLs | Table | Most frequently accessed paths |
| Top user agents | Table | Browsers and tools making requests — reveals automated scanners |
Focuses on suspicious Nginx activity, particularly directory scanning and application errors.
| Panel | Type | Description |
|---|---|---|
| 404 errors over time | Line chart | Filtered to 404 responses — spikes indicate directory brute forcing |
| 500 errors over time | Line chart | Filtered to 500 responses — spikes indicate something breaking under attack |
| Top 404 URLs | Table | Paths returning 404 — reveals what attackers are scanning for |
| Top user agents on 404s | Table | Tools generating 404s — identifies automated scanners like Nikto or dirb |
Monitors application-level events from Laravel's structured JSON logs, shipped from storage/logs/security.log, admin.log, orders.log, products.log, and users.log. All panels are filtered on fields.type: laravel.
| Panel | Type | Description |
|---|---|---|
| Security events over time | Line chart | All Laravel log events over time — baseline of application activity |
| Security events by message type | Table | Distribution of event types such as failed logins, brute force detections, and session anomalies |
| Top IPs hitting the application | Table | Clients generating the most application-level events |
| Events by log channel | Table | Activity split across security, admin, orders, products and users channels |
| Failed logins over time | Line chart | Filtered to failed login messages — shows brute force attempts as they happen |
| Top users triggering security events | Table | Accounts most involved in security events |
Monitors the fake MySQL server running on port 3306. Fed from /var/log/mysql-honeypot.log with a Filebeat dissect processor that parses each line into structured fields (dissect.honeypot.ip, dissect.honeypot.user, dissect.honeypot.database, dissect.honeypot.query). All panels are filtered on fields.type: honeypot.
| Panel | Type | Description |
|---|---|---|
| Honeypot connections over time | Line chart | All honeypot events over time — shows when attackers discover and engage with the fake database |
| Top attacking IPs | Table | Hosts connecting to the fake MySQL server |
| Top queries executed | Table | SQL commands attackers are running |
| Top usernames used | Table | Credentials attackers are trying |
Monitors activity on the fake admin panel at /adminpanel. Fed from storage/logs/security.log and filtered on context.honeypot: true.
| Panel | Type | Description |
|---|---|---|
| Honeypot visits over time | Line chart | When attackers discover and probe the fake admin panel |
| GET vs mutating actions | Donut chart | Distinguishes passive browsing from active attacks such as deleting products or toggling admin status |
| Top attacking IPs | Table | Hosts interacting with the honeypot |
| Most visited honeypot routes | Table | Which fake admin pages attract the most attention |
| Attackers by email | Table | Authenticated users who access the honeypot |
| Severity over time | Line chart | Escalation from INFO browsing to WARNING mutations over time |
| Requirement | Version |
|---|---|
| PHP | 8.4 |
| Composer | 2.x |
| Node.js + npm | LTS |
| Nginx | With QUIC/HTTP3 support |
| Python | 3.10+ |
| SQLite | 3.x (default DB) |
git clone <your-gitlab-repo-url>
cd <repo-directory>composer installnpm install
npm run buildcp .env.example .env
php artisan key:generateOpen .env and set at minimum:
APP_URL=https://group11.hp.edu.technet.howest.be
APP_DOMAIN=group11.hp.edu.technet.howest.be
APP_ENV=production
APP_DEBUG=falseThe application uses SQLite by default (DB_CONNECTION=sqlite). No additional database host configuration is needed unless you switch to MySQL.
php artisan migrate
php artisan db:seeddb:seed runs UserSeeder (creates default admin and test users) and ProductSeeder (populates the pharmacy product catalogue).
php artisan storage:linkThis makes storage/app/public/ accessible via the web at /storage/.
Ensure Nginx's worker process can read and write the necessary directories:
chown -R www-data:www-data storage bootstrap/cache
chmod -R 775 storage bootstrap/cacheCopy the provided Nginx configuration files to your server:
cp config-main/nginx.conf /etc/nginx/nginx.conf
cp config-main/default.conf /etc/nginx/conf.d/default.confPlace your TLS certificate and key:
/etc/ssl/certs/group11-csr.crt
/etc/ssl/private/server.key
Test and reload Nginx:
nginx -t
systemctl reload nginxThe Nginx config expects PHP 8.4 FPM on a Unix socket:
/run/php/php8.4-fpm.sock
Ensure the php8.4-fpm service is running:
systemctl enable php8.4-fpm
systemctl start php8.4-fpmInstall the Python dependency and start the fake MySQL server:
pip install mysql-mimic
python3 scripts/database.py &The honeypot will listen on 0.0.0.0:3306 and write all activity to /var/log/mysql-honeypot.log. Make sure port 3306 is reachable from outside (or inside your test network as appropriate).
To run it as a persistent background service, create a systemd unit:
# /etc/systemd/system/mysql-honeypot.service
[Unit]
Description=MySQL Honeypot
After=network.target
[Service]
Type=simple
User=root
ExecStart=/usr/bin/python3 /opt/mysql-honeypot/mysql-honeypot.py
Restart=always
RestartSec=5
[Install]
WantedBy=multi-user.targetsystemctl enable mysql-honeypot
systemctl start mysql-honeypotEnsure the storage/logs/ directory is writable and pre-create the log files if needed:
touch storage/logs/security.log
touch storage/logs/admin.log
touch storage/logs/orders.log
touch storage/logs/products.log
touch storage/logs/users.log
chown www-data:www-data storage/logs/*.logOpen a browser and navigate to https://group11.hp.edu.technet.howest.be. You should see the pharmacy storefront login page.
To verify the honeypot, log in with any account and navigate to /adminpanel. Check that a new entry appears in storage/logs/security.log:
tail -f storage/logs/security.logTo verify the MySQL honeypot:
mysql -h <server-ip> -u anyuser -p
# Enter any password — connection should succeed
SHOW DATABASES;
SELECT * FROM pharmacy_db.priv_prod;Check /var/log/mysql-honeypot.log for the recorded credentials and queries.