Metrics & Control API
Since v2.0, OpenShield-XDP can serve everything the TUI shows as plain JSON over HTTP — traffic, mitigation, attack state, bans, top offenders/ports, behavior clusters, attack history, and the active mitigation config (secrets stripped). Use it to feed Grafana, a status page, or your own tooling. There is no Prometheus dependency; you just poll an endpoint.
Since v2.7, the same endpoint can optionally expose a full remote-control API (/control/*) — everything the TUI can do: bans, whitelist, geo blocking, runtime config edits, baseline management, L7 rules, attack bulk-bans. This is off by default and gated behind metrics.control_enabled. Read the warning before turning it on.
Both are disabled by default.
Enabling
In /etc/openshield/openshield.yaml:
metrics:
enabled: true
listen: "127.0.0.1:9100" # local only; use 0.0.0.0:9100 for remote dashboards
api_key: "osk_..." # pre-generated randomly per install
rate_limit_per_sec: 10 # per source IP; 0 = unlimited
whitelist: [] # IP/CIDR allowlist; empty = any IP (key still required)
control_enabled: false # /control/* management API — see the warning belowKey, rate-limit and listen-address changes apply without a restart (sudo openshield reload), and so does control_enabled — toggling it takes effect on the very next request. Changing whitelist requires a loader restart (sudo openshield unload && sudo openshield load).
Getting your URL and key
sudo openshield keyPrints the endpoint URL, health path, your API key, and a ready-to-paste curl example — but only when the endpoint is enabled; otherwise it tells you how to enable it.
Managing the key (both are hot-applied, no restart):
sudo openshield key set <your-key> # bring your own key (min 8 chars)
sudo openshield key regen # rotate to a fresh random keyAuthentication, allowlist, rate limits
Every request passes through the same pipeline: IP allowlist → API key → per-IP rate limit → handler. Control endpoints add a fourth step, the control_enabled gate.
Send the key as a Bearer token (or ?key= query parameter):
curl -H "Authorization: Bearer osk_1a2b3c..." http://127.0.0.1:9100/metrics
curl "http://127.0.0.1:9100/metrics/baseline?key=osk_1a2b3c..."- Allowlist — when
metrics.whitelistis non-empty, only those IPs/CIDRs may connect at all (403 otherwise). The key is still required. - Rate limit —
metrics.rate_limit_per_secis a per-source-IP token bucket (burst 2× the rate, min 4). Read and control endpoints share the same bucket.
Errors
| Code | Meaning |
|---|---|
| 400 | Bad request — invalid JSON body, missing/invalid field, or the operation rejected the input |
| 401 | Missing/invalid API key |
| 403 | Source IP not in metrics.whitelist, or control API disabled |
| 405 | Wrong HTTP method for the path |
| 429 | Rate limit exceeded |
| 501 | /metrics/setup provider not initialized in this build |
| 503 | Section not available in this build |
All errors are JSON: { "error": "..." }. A 403 from a /control/* path with body { "error": "control API disabled (metrics.control_enabled: false)" } means the gate is off.
Read endpoints
| Path | Method | Description |
|---|---|---|
/metrics | GET | Full JSON snapshot (schema below) |
/health | GET | { "status": "ok", "generated_at": <unix> } |
/metrics/baseline | GET | Baseline learner status + 30-day history |
/metrics/geo | GET | Geo-blocking mode, country list, background jobs |
/metrics/alerter | GET | Webhook delivery statistics |
/metrics/autofetch | GET | Blocklist auto-fetcher status |
/metrics/access | GET | Whitelist + blacklist entries |
/metrics/targets | GET | Top destination IPs by current rate — "which VPS IP is being attacked" on dedicated hosts. Each target carries a per-IP state (normal / elevated / under_attack) on multi-IP dedicated hosts (v2.12.0+, see tenant.mode) |
/metrics/attached | GET | Attached-IP registry summary: auto/manual/pool counts + cumulative reaps (v2.17+) |
/metrics/blackhole | GET | Tenant-blackhole status: active entries with origin/expiry, exemption count, kernel drop counter (v2.17+) |
/metrics/egress | GET | TC egress policer status + cumulative verdict counters (v2.18+, opt-in) |
/metrics/forensics | GET | Forensics storage: dir, size vs cap, collecting/halted state, cleanup counters |
/metrics/edge | GET | Edge-mitigation module status (provider-aware) — the current name of the endpoint below |
/metrics/ovh | GET | Legacy alias of /metrics/edge — serves the identical payload |
/metrics/schedule | GET | Active suppression windows |
/metrics/dstips | GET | Tracked destination IPs overview: tracked count + top-5 destinations by current pps |
/metrics/dstip?ip=<addr>[&range=15m|1h|24h] | GET | Full per-destination analysis: registry/blackhole flags (with remaining seconds), current rates, per-second series (minute-resolution on 1h/24h), windows, attack history |
/metrics/setup | GET | Pending "what's new" setup questions introduced by recent releases (the dashboard popup source) |
The sub-endpoints wrap their payload in { "generated_at": <unix>, "data": ... } (schedule uses "schedule" instead of "data"). They require the same auth as /metrics and work regardless of control_enabled.
/metrics
{
"generated_at": 1754325123,
"snapshot": { ... }, // the full TUI dashboard snapshot
"behavior": { ... }, // behavior engine state + clusters
"attacks": [ ... ], // last 50 attack records
"config": { ... } // active mitigation config, secrets stripped
}Highlights of what's inside:
snapshot.global—current_pps,current_bps,passed_pps, drop/pass rates, totals. Thecurrent_ppsvspassed_ppspair is your proof-of-mitigation: what's arriving vs what got through. Since v2.13.5:live_pass_rate/live_drop_rate(percentages for the current interval — the cumulativepass_rate/drop_ratemix in peacetime traffic) andlive_ips_passed/live_ips_blocked(distinct sources this interval that passed vs are currently blocked).snapshot.traffic— per-protocol rates, peaks,top_ports, and per-second history series (pps_history,bps_history,drop_rate_history, newest last — graph them directly).snapshot.attack—state(NORMAL/UNDER_ATTACK), type, duration, baseline + spike thresholds, packets dropped, IPs banned, new sources blocked.contained(bool) is true when an attack is active but ≥85% of arriving packets are being dropped (EMA-smoothed): the flood is still arriving — sostate, duration and the end-of-attack report keep accruing — but it is fully mitigated. The TUI shows an amberCONTAINEDbadge in this state instead of the redUNDER ATTACKone.snapshot.bans/snapshot.top_offenders— active ban count and recent entries (IP, reason, expiry, star level); per-IP rates and suspicion scores.behavior—enabled,auto_block, live clusters (state,confidence,reasonslike "machine-paced timing"), and learned per-port baselines.attacks— up to 50 recent attack records: peak/avg/p95 pps, IPs involved, protocol mix,forensics_dir. Each record'stop_targetsentries carrystate(per-tenant attack state) and, since v2.22 build 3,scope— the network-ownership classification from the attached-IP registry:pool(operator-declared CIDR pool),manual(operator-added entry),auto(wire-learned entry), ortransit(unknown). On forwarding deployments (VXLAN, routed upstream) dst_stats sees every destination crossing the wire, and onlypool/manualmeans "this network is ours" — declare your allocations (openshield ips add/ pool add) so end-of-attack reports split "your networks" from learned/transit destinations; with nothing declared, all targets readautoand alerts render the legacy flat list.config— flatkey = valuemap of the active mitigation config. License keys, webhook URLs and other secrets are excluded by construction.
Every top-level key in snapshot:
| Key | Contents |
|---|---|
timestamp | snapshot build time (unix) |
global | current/total rates — the proof-of-mitigation pair |
attack | attack state machine (state, thresholds, contained) |
bans / top_offenders | active ban count + recent entries; per-IP rates/suspicion |
traffic | per-protocol rates, peaks, top_ports, per-second history series |
system | loader version, kernel, uptime, interface, xdp_mode |
maps | BPF map occupancy |
prof | kernel per-drop-path counters |
recent_events / logs | latest loader events and log lines |
attack_timeline | state transitions, newest first |
panic | circuit-breaker state |
license | tier, hwid, expires_at (error when invalid) |
alerter | webhook delivery health — same shape as /metrics/alerter |
attached | registry summary — same shape as /metrics/attached |
blackhole | blackhole status — same shape as /metrics/blackhole |
egress | policer counters — same shape as /metrics/egress |
dstip | tracked-destination summary — same shape as /metrics/dstips |
/metrics/baseline
{
"generated_at": 1754325123,
"data": {
"success": true,
"learning": "active", // or "suppressed (schedule)", "frozen (attack active)"
"live": { "date": "...", "pps": 12400.0, "bps": 9800000.0, "tcp_pps": 0, "udp_pps": 0, "icmp_pps": 0, "syn_pps": 0 },
"effective": { "...": "same shape — live merged with the 30-day history" },
"spike_pps": 45000, "spike_bps": 38000000,
"floor_pps": 500, "floor_bps": 400000,
"history_days": 30, "oldest_day": "2026-07-07",
"history": [ { "date": "2026-08-05", "pps": 12100.0, "...": "..." } ],
"merge_live_weight": 0.4, "merge_recency_weight": 0.6
}
}/metrics/geo
{
"generated_at": 1754325123,
"data": {
"mode": "block", // "block" (listed countries dropped) or "allow" (only listed countries pass)
"countries": ["CN", "RU"],
"jobs": { "BR": "blocked 4213 prefixes" } // per-country background job state
}
}/metrics/alerter
{
"generated_at": 1754325123,
"data": {
"webhook_set": true,
"queued": 0, "dropped": 0, "sent": 137,
"last_success_at": 1754325100,
"last_error": "" // omitted when empty
}
}/metrics/autofetch
{
"generated_at": 1754325123,
"data": {
"status": { "...": "fetcher state (last run, entries loaded, errors)" },
"categories": ["c2", "botnets", "..."],
"urls": [],
"never_block": ["203.0.113.10"]
}
}/metrics/access
{
"generated_at": 1754325123,
"data": {
"whitelist": [ { "ip": "203.0.113.10", "version": 4 } ],
"blacklist": [ { "ip": "198.51.100.77", "version": 4, "note": "attack #12 bulk-ban" } ]
}
}/metrics/schedule
{
"generated_at": 1754325123,
"schedule": [ { "kind": "baseline", "until": "2026-08-06T20:00:00Z" } ]
}Empty when no suppression windows are active.
/metrics/egress
TC egress policer status (v2.18+; all zeros with active: false while the opt-in feature is off):
{
"generated_at": 1754325123,
"data": {
"active": true,
"passed_total": 1823310,
"dropped_total": 1338533,
"dropped_udp": 1338000,
"dropped_icmp": 12,
"dropped_syn": 521
}
}Counters are cumulative since attach, summed over the policer's per-CPU stats map.
/metrics/attached
Attached-IP registry summary (v2.17+) — which destination IPs the host has learned or been told are its own. Informational: it gates auto-blackhole eligibility and feeds the TUI IPs tab.
{
"generated_at": 1754325123,
"data": {
"attached_total": 12, // all entries (auto + manual)
"attached_auto": 9, // learned from traffic + local-address sweeps
"attached_manual": 3, // operator-added (never reaped)
"attached_pools": 1, // attached CIDR pools
"attached_reaped_total": 41 // cumulative auto-entry reaps (idle ≥ registry.inactive_days)
}
}/metrics/blackhole
Tenant-blackhole status (v2.17+): every active entry, where it came from, when it lapses, plus the exemption count and the kernel's cumulative drop counter.
{
"generated_at": 1754325123,
"data": {
"active": [
{ "address": "203.0.113.10", "auto": false, "seconds_remaining": -1 }
],
"active_total": 1,
"auto_events_total": 3,
"blackhole_dropped_total": 18322014, // cumulative XDP drops, all time
"exempt_sources": 2 // live (dst, src) grace exemptions
}
}auto: true marks entries from the auto-blackhole trigger vs operator-added ones; seconds_remaining: -1 = indefinite. Engage/lift via POST|DELETE /control/blackhole; per-destination state also shows in /metrics/dstip?ip=.
/metrics/edge
Edge-mitigation module status (provider-aware — OVH today): enabled from config, running while the syncer is up, plus provider-specific rule/rate-limit counters when running. /metrics/ovh is the legacy alias and serves the identical payload.
{ "generated_at": 1754325123, "data": { "enabled": true, "running": false } }/metrics/dstips
Tracked destination IPs overview — how many destinations the per-IP tracker holds, plus the top-5 by current pps (the snapshot's dstip section):
{
"generated_at": 1754325123,
"data": {
"tracked": 142,
"top": [
{ "ip": "203.0.113.10", "pps": 48200, "...": "..." },
{ "ip": "203.0.113.11", "pps": 9100, "...": "..." }
]
}
}/metrics/dstip?ip=<addr>
Full per-destination analysis for one address — current rates, a time series, window aggregates, and every recorded attack against that destination. attached/blackholed come from the attached-IP registry and tenant blackhole. Missing or invalid ip returns 400.
Query parameters:
| Param | Values | Default | Notes |
|---|---|---|---|
ip | IPv4/IPv6 address | — | required |
range | 15m | 1h | 24h | 15m | 15m = live 1-second series. 1h/24h are answered from the box-side minute-resolution history (step_sec 60) — 24h costs ~12 MB RAM on the box, nothing is streamed off it. current rates and ongoing_attack stay live-resolution on every range. Unknown values fall back to 15m. |
{
"generated_at": 1754325123,
"data": {
"ip": "203.0.113.10",
"tracked": true,
"attached": true,
"blackholed": false,
"auto_blackhole": false,
"blackhole_remaining_sec": -1, // seconds until the blackhole lapses; -1 = indefinite (only meaningful when blackholed)
"range": "15m", // echoes the served range — "15m" | "1h" | "24h"
"current": { "pps": 48200, "bps": 385600000, "dps": 12000 },
"step_sec": 1, // 60 when range is 1h/24h
"series": [ { "t": 1754325100, "pps": 46000, "bps": 368000000, "dps": 11500 } ],
"windows": {
"1m": { "peak_pps": 51000, "avg_pps": 44000, "peak_bps": 408000000, "avg_bps": 352000000, "drops": 1820000 },
"5m": { "...": "same shape" },
"15m": { "...": "same shape" }
// range=1h answers windows 1h; range=24h answers windows 1h + 24h
},
"attacks": [ { "start": 1754321000, "end": 1754321600, "type": "udp_flood", "peak_pps": 480000, "verdict": "mitigated" } ],
"ongoing_attack": false
}
}/metrics/setup
Pending "what's new" setup questions introduced by recent releases — the exact list the terminal prompt, openshield setup, and the xdp.network dashboard popup all render from. When an update adds a config value that deserves a user decision, it appears here until answered; unanswered questions run their safe default and never block the firewall.
{
"generated_at": 1754325123,
"data": {
"version": "2.19.3",
"pending": 1,
"questions": [
{
"id": "nic_tuning_hardware", // stable id — the answer key
"since": "2.19.3", // version that introduced it
"field": "dynamic.nic_tuning_hardware",
"kind": "bool", // "bool" | "choice" | "string" | "int"
"title": "Enable hardware-tier NIC tuning (queue/ring changes)?",
"description": "Queue-count and ring-buffer changes fully RESET the NIC…",
"options": [], // kind=choice only
"default": false // the safe value running right now
}
]
}
}Answer via POST /control/setup/answer (below), openshield setup on the box, or the interactive prompt on the next openshield load. Answered questions disappear from pending everywhere and are recorded on the box (/var/lib/openshield/setup-state.json).
⚠️ Control API — read this before enabling
Setting metrics.control_enabled: true turns the API key into a root-equivalent credential. Anyone holding it can:
- reconfigure every runtime-safe mitigation setting (
POST /control/config), - ban or whitelist arbitrary IPs — including locking you out or unbanning attackers,
- wipe the entire blacklist/whitelist,
- change geo-blocking mode and countries,
- import a poisoned traffic baseline (blinding spike detection),
- promote L7 drop rules, toggle the blocklist fetcher, bulk-ban attack IPs.
Only enable it when you also:
- Set
metrics.whitelistto exactly the IPs of your dashboard/management hosts. - Firewall the listen port (e.g.
iptables -A INPUT -p tcp --dport 9100 -s <dashboard> -j ACCEPT; ... -j DROP) so nothing else can even attempt a key guess. - Keep
listenon127.0.0.1unless the dashboard truly lives on another host — and never expose the port on a public interface without both protections above.
Every successful control call is written to the audit log (visible in the TUI log feed) with its endpoint and parameters. Control request bodies are capped at 2 MB.
metrics:
enabled: true
control_enabled: true # root-equivalent — protect the port!
whitelist: ["198.51.100.20"] # your dashboard host ONLYControl endpoints
All are POST unless noted, all require auth, all return { "error": "..." } with a sensible code on failure and a small JSON object on success. Toggle at runtime with sudo openshield reload after editing control_enabled.
Runtime config
GET /control/config — current value of every runtime-safe setting:
{ "settings": { "static.enabled": true, "static.pps_threshold": 5000, "metrics.control_enabled": true, "...": "..." } }POST /control/config — apply runtime settings. This is the exact same code path as the TUI/CLI config editor and openshield reload: unknown or read-only settings are rejected, changes are written to the BPF config map, and the YAML is persisted.
curl -X POST -H "Authorization: Bearer osk_..." -H "Content-Type: application/json" \
-d '{"settings":{"static.enabled":true,"dynamic.global_pps_threshold":100000}}' \
http://127.0.0.1:9100/control/config
# burst-tolerant rate gating (v2.20.0+): tolerate 5s bursts, allow 6 empties/min
curl -X POST -H "Authorization: Bearer osk_..." -H "Content-Type: application/json" \
-d '{"settings":{"dynamic.burst_allowance_sec":5,"dynamic.burst_max_per_min":6}}' \
http://127.0.0.1:9100/control/configNew runtime-safe settings (same endpoint, hot-applied):
dynamic.port_syn_pps— per-destination-port new-connection/s cap (0 = off); excess SYNs to that port are dropped.geoip.enforce_mode—alwaysapplies the geo country list at all times;attackonly while an attack is declared.dynamic.burst_allowance_sec— burst bucket depth (seconds of each per-IP rate threshold) before suspicion may score; 0 = legacy per-window scoring. Default 3.dynamic.burst_max_per_min— bucket-empty windows tolerated per rolling minute before scoring starts; 0 = score on the first empty. Default 4.
POST /control/reload (v2.18+) — hot reload over HTTP: re-reads /etc/openshield/openshield.yaml, validates it, and applies every runtime-safe setting through the exact same path as openshield reload (no restart, no re-attach). Edit the YAML by hand or from your own tooling, then:
curl -X POST -H "Authorization: Bearer osk_..." http://127.0.0.1:9100/control/reload
# => { "success": true, "applied": 123 }applied counts the runtime-safe fields pushed through the update path. Read-only fields (interface, map sizes) still require a real reload — they are reported by GET /control/config but not re-applied here.
{ "success": true, "applied": 2 }Whitelist
# Add (also unbans the IP and exempts it from auto-fetch)
curl -X POST ... -d '{"ip":"203.0.113.10"}' .../control/whitelist
# Remove
curl -X DELETE ... -d '{"ip":"203.0.113.10"}' .../control/whitelist
# Remove everything
curl -X POST ... .../control/whitelist/clear
# Import a file of IPs/CIDRs (one per line; path is on the server)
curl -X POST ... -d '{"path":"/root/my-allowlist.txt"}' .../control/whitelist/importResponses: { "success": true, "ip": "..." }, { "success": true, "removed": 42 }, { "success": true, "added": 980, "skipped": 3 }.
Blacklist
# Ban an IP (duration_sec optional; 0 = 24h default)
curl -X POST ... -d '{"ip":"198.51.100.77","duration_sec":86400,"note":"bruteforce"}' .../control/blacklist
# Unban
curl -X DELETE ... -d '{"ip":"198.51.100.77"}' .../control/blacklist
# Remove every single-IP ban
curl -X POST ... .../control/blacklist/clearResponses mirror the whitelist ones.
Tenant blackhole
Engage or lift a tenant blackhole on a destination IP — while active, all traffic to that destination is dropped at XDP (established sessions keep their grace window; see blackhole.* in the config reference).
# Engage (seconds optional; 0 or omitted = indefinite until removed)
curl -X POST ... -d '{"ip":"203.0.113.10","seconds":600}' .../control/blackhole
# Lift
curl -X DELETE ... -d '{"ip":"203.0.113.10"}' .../control/blackholeBoth return { "success": true, "ip": "203.0.113.10" } — confirm current state via GET /metrics/blackhole or GET /metrics/dstip?ip= (blackholed, blackhole_remaining_sec).
The same operations exist on the TUI socket (blackhole_add / blackhole_remove), the CLI (openshield blackhole add <ip> --seconds N), and the DstIP Analyzer tab (b key: permanent / 5m / 10m / 1h / 6h / custom 1s–30d). Live blackhole state per destination is on GET /metrics/dstip?ip= (blackholed, auto_blackhole, blackhole_remaining_sec).
Port/protocol blackholes (v2.22+) — the finer-grained variant: drop traffic to a destination on one port or a port range, TCP/UDP/both, temporary or permanent. Whole-IP blackholes and bans outrank port rules.
# Blackhole TCP+UDP to 203.0.113.10 on ports 27000-27100 for 10 minutes
curl -X POST ... -d '{"ip":"203.0.113.10","port":"27000-27100","proto":"both","seconds":600}' .../control/blackhole/port
# Single port, UDP only, permanent
curl -X POST ... -d '{"ip":"203.0.113.10","port":"19132","proto":"udp"}' .../control/blackhole/port
# Remove / list / clear
curl -X DELETE ... -d '{"ip":"203.0.113.10","port":"27000-27100","proto":"both"}' .../control/blackhole/port
curl .../control/blackhole/port/list
curl -X POST .../control/blackhole/port/clearCLI: openshield blackhole port {add,remove,list,clear}; socket: blackhole_port_*; per-destination port rules show under port_blackholes in /metrics/dstip.
Peer trust (v2.22 build 10+)
Read and manage peer trust (the TUI Peer tab's HTTP twin): explicit tunnel peers (IPs/CIDRs) and peer ASNs — both are full-bypass (never scored, banned, or capped). Peer-ASN bypass activates once the source-ASN dataset is populated (asn_db: true; builds lazily on first use) and requires the ASN license tier (asn_licensed).
# Current state
curl .../metrics/peers
# → {"peers":["203.0.113.10"],"asns":[64999],"asn_db":true,"asn_licensed":true}
# Explicit peers (IP or CIDR)
curl -X POST ... -d '{"value":"203.0.113.10"}' .../control/peers
curl -X DELETE ... -d '{"value":"203.0.113.10"}' .../control/peers
# Peer ASNs (whitelists every prefix the ASN announces)
curl -X POST ... -d '{"asn":64999}' .../control/peers/asn
curl -X DELETE ... -d '{"asn":64999}' .../control/peers/asnMutations persist to tunnels.peers / tunnels.asns in the YAML config and hot-apply to the kernel maps. Socket equivalents: peer_list, peer_add, peer_remove, peer_asn_add, peer_asn_remove.
Setup answers
Apply one "what's new" setup answer — the same effect as answering the terminal prompt or openshield setup. Get the pending list from GET /metrics/setup.
curl -X POST ... -d '{"id":"nic_tuning_hardware","value":true}' .../control/setup/answer{ "success": true, "applied": "nic_tuning_hardware" }Answers go through the same runtime config path as POST /control/config (hot-applied, persisted, audit-logged). Unknown ids, wrong value kinds, and validation failures return an error and leave the question pending for a retry.
Geo blocking
# Switch mode: "block" = listed countries are dropped, "allow" = only listed countries pass
curl -X POST ... -d '{"mode":"block"}' .../control/geo/mode
# Toggle a country (ISO 3166-1 alpha-2). Blocking runs in the background —
# watch /metrics/geo "jobs" for completion.
curl -X POST ... -d '{"country":"CN"}' .../control/geo/toggle{ "success": true, "country": "CN", "blocked": true } // blocked=true: added (job started), false: removedGeo/ASN policy (v2.22+)
Two layers: a global descriptor + global ASN rules applying everywhere, and a per-destination layer that only traffic destined to a policy-bound IP sees. Every rule carries enforce: "always" (all the time) or enforce: "attack" (only while an attack is declared). Precedence: hard bans and blackholes outrank everything; a per-destination allow never rescues a global deny; unclassified sources always pass.
# Global geo/ASN descriptor (country list + ASN list, block or allow each)
curl -X POST .../control/geoasn/global -d '{
"country_mode":"block", "country_enforce":"always", "countries":["CN","RU"],
"asn_mode":"off", "asn_enforce":"always", "asns":[] }'
# Per-destination policy: only traffic destined to 203.0.113.10 sees it
curl -X POST .../control/geoasn/dst -d '{
"ip":"203.0.113.10",
"policy":{ "country_mode":"allow", "country_enforce":"attack", "countries":["US","DE"],
"asn_mode":"block", "asn_enforce":"always", "asns":[16509] } }'
curl -X DELETE .../control/geoasn/dst -d '{"ip":"203.0.113.10"}'
# Global ASN whitelist/blacklist
curl -X POST .../control/asn -d '{"asn":15169, "rule":{"mode":"allow","enforce":"always"}}'
curl -X DELETE .../control/asn -d '{"asn":15169}'
# Per-destination ASN override (BLOCK drops that ASN to this destination
# only; ALLOW rescues it from the destination's own descriptor ASN list)
curl -X POST .../control/asn/dst -d '{"ip":"203.0.113.10","asn":16509,
"rule":{"mode":"block","enforce":"attack"}}'
curl -X DELETE .../control/asn/dst -d '{"ip":"203.0.113.10","asn":16509}'
# Snapshot: descriptors, ASN rules, classifier + gate state
curl .../control/geoasn/listRequires geoip.asn_enabled: true (the first configured policy downloads the GeoLite2 ASN dataset in the background). Per-destination policy also shows under geo_asn_policy in /metrics/dstip.
Baseline management
GET /control/baseline/export — the live baseline + 30-day history as a raw JSON document (the response body is the export — save it verbatim):
curl -H "Authorization: Bearer osk_..." .../control/baseline/export > baseline-backup.jsonPOST /control/baseline/import — replace the live baseline + history from an export:
curl -X POST ... -d "{\"payload\": $(cat baseline-backup.json | jq -Rs .)}" .../control/baseline/importPOST /control/baseline/delete — drop one poisoned day; the merged baseline is recomputed immediately:
curl -X POST ... -d '{"date":"2026-08-04"}' .../control/baseline/delete
# → { "success": true, "date": "2026-08-04", "history_days": 29 }Auto-fetch (blocklist feeds)
curl -X POST ... -d '{"enabled":true}' .../control/autofetch/toggle
curl -X POST ... -d '{"entry":"203.0.113.0/24"}' .../control/autofetch/never-add
curl -X POST ... -d '{"entry":"203.0.113.0/24"}' .../control/autofetch/never-remove
curl -X POST ... .../control/autofetch/fetch-nownever_block is a fetch-only exemption (the fetcher skips those ranges) — it is not a firewall whitelist.
Scheduled suppression
Temporarily suspend baseline learning or behavior auto-block (e.g. during a planned traffic spike or event):
# Suppress baseline learning for 1 hour
curl -X POST ... -d '{"action":"suppress","kind":"baseline","duration_sec":3600}' .../control/schedule
# Clear early ("kind" may also be "all")
curl -X POST ... -d '{"action":"clear","kind":"baseline"}' .../control/schedule{ "success": true, "schedule": [ { "kind": "baseline", "until": "2026-08-06T20:00:00Z" } ] }L7 block rules
Promote a payload signature into the live L7 drop map (16 slots) and persist it:
curl -X POST ... -d '{
"port": 25565, "proto": 17, "offset": 0,
"pattern": "fefd", "mask": "ffff",
"min_payload": 0, "max_payload": 0
}' .../control/block-patternproto:6/17or"tcp"/"udp"(0/"any"/omitted = both).pattern/mask: hex bytes, max 8 bytes each.port_is_src: truematches the source port instead of the destination.nameis optional (auto-generated asapi-<proto>-<port>-<offset>when omitted).
{ "success": true, "name": "api-udp-25565-0" }Attack history & bulk-ban
GET /control/attacks — full attack history plus the forensics base directory:
{
"forensics_dir": "/var/lib/openshield/attacks",
"records": [ { "number": 12, "forensics_dir": "12-20260806-115223-IST", "peak_pps": 480000, "...": "..." } ]
}POST /control/attacks/blacklist — bulk-ban every IP involved in an attack (async, chunked; whitelisted and proven-established sources are skipped automatically):
curl -X POST ... -d '{"forensics_dir":"12-20260806-115223-IST"}' .../control/attacks/blacklist
# → { "success": true, "started": true }GET /control/attacks/blacklist/status — poll the job:
{ "success": true, "running": true, "total": 152340, "done": 88000,
"inserted": 87112, "skipped": 888, "message": "bulk-ban running: 88000/152340 ..." }Integration notes
- Poll every 1–5 s. The payload is a cached snapshot refreshed at the telemetry poll interval (default 1 s) — polling faster just burns rate-limit tokens.
- The config file (and thus the key) is stored
0600, root-only. - Binding remotely? Keep
listenon localhost unless you need it, and setwhitelistto your dashboard host's IP when listening on all interfaces:
metrics:
listen: "0.0.0.0:9100"
whitelist: ["198.51.100.20"]Rotate the key if it ever leaks: sudo openshield key regen. If the key leaks while control_enabled is on, treat it like a root compromise: rotate immediately and review the audit log.
