HTTP Skin Templates Enterprise Community
HTTP skins let you control exactly what the honeypot serves to a visitor. Each skin is a folder inside trapster/data/http/ with a config.yaml, optional Jinja2 templates, and optional static files.
Folder structure
trapster/data/http/
└── my_skin/
├── config.yaml # required - route definitions and headers
├── templates/ # Jinja2 templates (.html, .j2, etc.)
│ ├── index.html
│ ├── 404.html
│ └── login.j2
└── files/ # static assets served as-is
├── logo.png
└── styles.cssPoint the honeypot at your skin by setting "skin": "my_skin" in trapster.conf.
config.yaml reference
Top-level fields
| Field | Required | Description |
|---|---|---|
name | yes | Skin identifier, must match the folder name |
description | no | Human-readable description |
headers | no | HTTP headers added to every response |
endpoints | yes | List of route definitions |
default | no | Catch-all response when no route matches |
errors | no | Custom responses for specific error codes |
Global headers
Headers defined at the top level are added to every response. Use them to set a realistic server fingerprint:
headers:
Server: Apache/2.4.41 (Ubuntu)
X-Powered-By: PHP/7.4.3
X-Frame-Options: SAMEORIGINPer-route headers are merged on top of global headers and take precedence.
Endpoint definition
endpoints is an ordered list. The first route whose pattern matches the request URL and method is used.
endpoints:
- "/path/or/regex":
- method: GET
status_code: 200
file: page.html # render a template
headers:
Content-Type: text/htmlEach route maps a URL pattern to one or more method handlers. A single URL can have different handlers for different HTTP methods:
endpoints:
- "/login":
- method: GET
status_code: 200
file: login.html
- method: POST
status_code: 302
headers:
Location: /dashboard
content: ''Route handler fields
| Field | Description |
|---|---|
method | HTTP method: GET, POST, PUT, DELETE, PATCH, HEAD, OPTIONS |
status_code | HTTP response status code |
reason | Custom HTTP reason phrase, e.g. Access Denied instead of the default Unauthorized |
file | Template file to render (from templates/). Supports Jinja2. |
content | Inline response body. Overrides file if both are set. |
ai | AI prompt - Trapster generates a dynamic response (see AI responses) |
headers | Per-route headers, merged with global headers. Values support Jinja2 expressions. |
query | Query parameter matching rules (regex per parameter) |
Header values support the same Jinja2 context as templates, including request and uuid(). Only values containing {{ are rendered at request time; others are resolved once at startup.
headers:
request-id: "{{ uuid() }}"
location: '/auth/login?url={{ request.url | quote }}&reason=0'
www-authenticate: 'Basic realm="{{ request.host }}"'URL patterns
Routes use regular expressions that must match the full path. Use standard regex syntax:
endpoints:
# Exact match
- "/login":
- method: GET
status_code: 200
file: login.html
# Any path starting with /api/
- "/api/(.*)":
- method: GET
status_code: 401
content: '{"error": "unauthorized"}'
# Regex exclusion - serve index.html for everything except /static/ and /favicon/
- "/(?!(static|favicon)($|/))(.*)":
- method: GET
status_code: 200
file: index.htmlQuery parameter matching
Use the query field to match requests with specific query parameters. Values are regex patterns:
endpoints:
- "/api/v1/user":
- method: GET
query:
id: '[0-9]+' # matches /api/v1/user?id=42
status_code: 200
file: user.j2
headers:
Content-Type: application/jsonIf the URL has query parameters but no query rule matches, the first handler without a query rule is used as a fallback.
Default response
The default block is returned when no endpoint matches the request path:
default:
status_code: 404
file: 404.htmlCustom error pages
Override responses for specific error codes using the errors block:
errors:
"401":
file: 401.html
headers:
WWW-Authenticate: 'Basic realm="Restricted Area"'
"403":
file: 403.htmlIf no entry exists for an error code, Trapster looks for a file named <code>.html in templates/ automatically.
Jinja2 templates
Files referenced via file: are rendered through Jinja2 before being sent. The template receives a request object and two built-in functions.
request object
| Field | Type | Description |
|---|---|---|
request.path | string | URL path, e.g. /login |
request.method | string | HTTP method |
request.headers | dict | Request headers |
request.body | string / null | Request body (POST/PUT/PATCH only) |
request.form | dict | Parsed form fields (POST application/x-www-form-urlencoded) |
request.query_string | dict | Parsed query parameters (values are strings) |
request.remote | string | Client IP address |
request.cookies | dict | Request cookies |
request.host | string | Host header, including port if non-standard (e.g. 192.168.1.1:8443) |
request.scheme | string | http or https |
request.url | object | Full URL object (cast to string for the complete URL) |
request.path_qs | string | Path with query string |
Building redirect URLs
request.host already includes the port, so {{ request.scheme }}😕/{{ request.host }} gives the full origin (e.g. https://192.168.1.1:8443). Use the quote filter to URL-encode values inside query parameters:
location: '/auth/login?url={{ request.url | quote }}'Built-in functions
uuid() - generates a random UUID4 per request. Useful for realistic request-id or X-Request-ID headers:
{{ uuid() }}
{# outputs: 4a128772-e3c9-4f6e-be26-2549681c4d17 #}random(seed, alphabet, length) - generates a deterministic random string. Useful for creating realistic-looking but reproducible fake data:
{{ random(seed=request.query_string.get('id', '1'), alphabet='abcdefghijklmnopqrstuvwxyz', length=8) }}| Argument | Default | Description |
|---|---|---|
seed | None (truly random) | Seed value for reproducibility |
alphabet | hex characters | Characters to draw from |
length | 36 | Output length |
get_current_time() - returns the current UTC time formatted as an HTTP date string:
{{ get_current_time() }}
{# outputs: Mon, 12 Jun 2026 14:23:05 GMT #}Dynamic status code override
A template can override its own status code by starting with a front matter block. This lets a single route return different codes based on the request content:
{% if 'settings' not in request.body %}
---
status_code: 400
---
{"error": "missing required field"}
{% else %}
---
status_code: 200
---
{"ok": "settings saved"}
{% endif %}Example: fake REST API endpoint
templates/user.j2:
{% set id = request.query_string.get('id', '1') %}
{% set username = random(seed=id, alphabet='abcdefghijklmnopqrstuvwxyz', length=8) %}
{
"id": "{{ id }}",
"username": "{{ username }}",
"email": "{{ username }}@example.com",
"created_at": "{{ get_current_time() }}"
}Static files
Any file placed in files/ is served directly without processing. Trapster resolves the request path against this folder before falling through to the default response.
files/
├── favicon.ico
├── logo.png
└── css/
└── main.cssA request to /logo.png will serve files/logo.png with the correct MIME type, without needing a matching endpoint rule.
AI-powered responses
Set ai: on a route instead of file: or content: to have Trapster generate a response dynamically using an LLM. Provide a prompt describing what the response should look like:
endpoints:
- "/api/v1/user/(.*)":
- method: GET
status_code: 200
ai: "Respond with JSON containing realistic user account information"
headers:
Content-Type: application/json
- "/admin/":
- method: GET
status_code: 200
ai: "Respond with an HTML admin login page"
headers:
Content-Type: text/html; charset=utf-8The request path is appended to your prompt automatically so the AI can tailor the response to the specific URL being requested.
AI responses require the optional AI dependencies. If they are not installed, the route returns a 404.
YAML anchors
Large configs with repeated header sets can use YAML anchors to avoid duplication:
common_headers: &common_headers
X-Frame-Options: SAMEORIGIN
X-Content-Type-Options: nosniff
Cache-Control: no-cache, no-store, must-revalidate
endpoints:
- "/login":
- method: GET
status_code: 200
file: login.html
headers:
<<: *common_headers
Expires: '0'
- "/loginError":
- method: GET
status_code: 401
file: loginError.html
headers:
<<: *common_headersBuilt-in skins
Enterprise Edition Enterprise
Enterprise VMs ship with additional skins for common internal targets (login pages for CI servers, NAS, network appliances, cloud consoles and more). The full list is available in the Skins section of your dashboard.
Community Edition
| Skin | Description |
|---|---|
default_apache | Apache 2.4 default page |
default_nginx | Nginx welcome page |
default_iis | IIS 10 default page |
fortigate | Fortinet FortiGate login page |
demo_api | Sample REST API with user and settings endpoints |
demo_ai | AI-powered API that generates dynamic responses per route |
Complete example
A minimal skin that emulates a basic login portal:
config.yaml:
name: my_portal
description: Custom internal portal honeypot
headers:
Server: nginx/1.18.0
X-Frame-Options: DENY
endpoints:
- "/":
- method: GET
status_code: 302
headers:
Location: /login
- "/login":
- method: GET
status_code: 200
file: login.html
- method: POST
status_code: 302
headers:
Location: /login?error=1
- "/robots.txt":
- method: GET
status_code: 200
content: "User-agent: *\nDisallow: /"
headers:
Content-Type: text/plain
default:
status_code: 404
file: 404.htmltemplates/login.html (simplified):
<!DOCTYPE html>
<html>
<head><title>Portal Login</title></head>
<body>
<form method="POST" action="/login">
<input type="text" name="username" placeholder="Username">
<input type="password" name="password" placeholder="Password">
<button type="submit">Sign in</button>
</form>
{% if request.query_string.get('error') %}
<p>Invalid credentials.</p>
{% endif %}
</body>
</html>Any POST to /login is automatically logged with the submitted username and password fields - no extra configuration needed.
