App and API Protection
App and API Protection (AAP) finds attacks and abuse in the traces your services already send. It can also block them inside your app through the Watchtower SDKs, and it lists the API endpoints your services expose. Everything runs on the server spans you already send. Watchtower adds no agent, sidecar or proxy.
The console page is App protection (/appsec). It has four tabs: Signals, Rules, Denylist and API inventory.
Detection from traces
Every minute the worker judges the newest closed five-minute bucket of server spans. It waits 90 seconds after the bucket ends, so late spans still count.
Pattern rules run on each request. They read these fields, each percent-decoded once:
the URL path and the query string;
the
User-Agent;the other request header values (
http.request.header.*);the request body, when a capture rule keeps
http.request.body.
Threshold rules count requests per client IP or per user in each bucket.
The shipped default ruleset:
Id | Detects | Kind | Severity |
|---|---|---|---|
| SQL injection | pattern | high (metadata: medium) |
| Cross-site scripting | pattern | high / medium |
| Path traversal, sensitive files | pattern | high |
| SSRF to internal or metadata addresses, dangerous schemes | pattern | high / medium |
| Command injection | pattern | high |
| Log4Shell lookups, template injection | pattern | critical / high |
| Security scanners, vulnerability probes | pattern | medium / low |
| 30+ failed logins (401-403) from one IP | threshold | high |
| 10+ failed logins over 5+ accounts from one IP | threshold | critical |
| 10+ failed logins on one account from 3+ IPs | threshold | high |
| 20+ successful signups ( | threshold | medium |
| 200+ client errors or 50+ server errors from one IP | threshold | medium / high |
A login route is any route matching login, signin, session, oauth, auth or token. A login that answers 200 but fails in your code counts as a 401 when the SDK marks it with trackLogin(login, false).
Signals
Each match becomes a signal, stored in appsec_signals_v1 for 90 days. A signal groups one rule's hits by one actor (a client IP, else the user) across buckets. It carries:
hit and blocked counts;
the routes, client IPs and users involved;
the matched text;
up to 10 attack traces.
High and critical signals raise the alert external:appsec. It is keyed by the signal and labelled with rule_id, category, service, actor and trace_id. It routes through receivers, routing trees and escalation policies like any alert. The alert resolves after 30 minutes without new hits. Low and medium signals are stored but do not alert.
Client IP comes from these span attributes, in this order: client.address, http.client_ip, appsec.client_ip (set by the SDKs), network.peer.address, net.sock.peer.addr. The user comes from enduser.id, user.id or appsec.login.
Rules and mode
GET /v1/projects/{prj}/appsec/rules returns the effective ruleset.
GET|PUT /v1/projects/{prj}/appsec/config holds three settings:
mode:monitororblock;disabled: default rule ids that are turned off;custom: up to 50 custom rules.
A write needs ManageProject and is audited (appsec.config.updated).
Custom rules use the same shape as the default rules:
pattern rules:
fieldsandpattern;threshold rules:
group_by(iporuser), optionalmethodandrouteregexes,status_min,status_max,min_countand optionalmin_distinct.
Patterns are matched case-insensitively. They must stay within the regex subset that Rust, RE2 (Go, ClickHouse), JavaScript and Python all support. These are refused: inline flags and any (? group other than (?:, look-around, \A, \z, Unicode and POSIX classes, and \Q…\E.
In-app WAF (SDKs)
The SDKs poll GET /v1/appsec/ruleset?service= on the edge with an ingestion key that has scope probes:read. They poll every 30 seconds with an ETag. The response has the mode, the enabled pattern rules and the denylist entries in force. Threshold rules stay on the server.
The SDKs check each request before your handler runs:
Match in
monitormode: the server span getsappsec.event,appsec.rule_id,appsec.categoryandappsec.blocked=false, and the request goes through.Match in
blockmode: the SDK answers403 application/problem+jsonand your handler never runs.Denylisted IP or user: the SDK answers 403 in both modes.
Until the first ruleset arrives, every request goes through.
SDK | Start | Hook |
|---|---|---|
Node ( |
|
|
Python ( |
|
|
Go ( |
| built into |
All three read WT_API_URL and WT_INGEST_KEY by default.
Each SDK also has these helpers:
trackLogin/track_login/TrackLogin: marks a login attempt and its result;isUserBlocked/is_user_blocked/AppsecUserBlocked: checks a user after authentication.
Install the middleware inside the OpenTelemetry server instrumentation, so the server span is the current span.
The SDKs inspect the path, query, User-Agent and header values. The Node middleware also reads a body that a body parser has already parsed. The Python and Go middleware do not read the body. The worker still checks bodies that capture rules keep.
The SDKs take the client IP from the first X-Forwarded-For hop, then X-Real-IP, then the socket peer. Run them behind a proxy that overwrites these headers.
Denylist
The denylist API needs ManageProject for writes, and every write is audited:
GET /v1/projects/{prj}/appsec/denylistlists entries (cursor pagination).POST /v1/projects/{prj}/appsec/denylistadds an entry:{kind: "ip"|"user", value, reason?, ttl_s?, signal_key?}.ttl_sis 60 seconds to 1 year; leave it out for no expiry.DELETE /v1/projects/{prj}/appsec/denylist/{id}removes an entry.
Adding the same kind and value again replaces its reason and expiry. A project holds up to 10,000 entries. Expired entries stop applying at once and are pruned a day later. A signal's detail page can deny its actor in one click. IP entries are exact addresses (IPv4 or IPv6).
API inventory
GET /v1/projects/{prj}/appsec/inventory?from&to&service lists every HTTP endpoint (method and http.route) seen in server spans. It covers the last 7 days by default, up to 2,000 endpoints. Each endpoint shows:
Calls and errors.
Authentication. Whether requests carried an identity (
enduser.id,user.id, a capturedAuthorizationheader, orappsec.authfromtrackLogin), or whether the endpoint answered 401 or 403.PII in responses. These kinds of the gateway's PII detectors, found in
http.response.*attributes (headers, and the body excerpt that capture rules keep):email,card,ssn,phone,iban,jwt.First and last seen.
documented. Whether the service's imported OpenAPI spec (services/{svc}/api-spec) lists the endpoint. It is null when the service has no spec.
Signals API
GET /v1/projects/{prj}/appsec/signals?from&to&severity&category&servicelists signals, at most 500, most recently seen first.GET /v1/projects/{prj}/appsec/signals/{key}returns one signal's summary, its hits per bucket and the actor's active denylist entry.
Both cover the last 7 days by default.
Limits
Delivery is at least once. A bucket that is judged twice writes the same rows, and the
ReplacingMergeTreekeeps one.Each bucket reads at most 20,000 spans that pass the pattern pre-filter, plus 200,000 count groups.
The
cardcheck in the inventory is regex-only (no Luhn check).