Skip to content

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:

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 below

Key, 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 ​

bash
sudo openshield key

Prints 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):

bash
sudo openshield key set <your-key>   # bring your own key (min 8 chars)
sudo openshield key regen            # rotate to a fresh random key

Authentication, 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):

bash
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.whitelist is non-empty, only those IPs/CIDRs may connect at all (403 otherwise). The key is still required.
  • Rate limit — metrics.rate_limit_per_sec is a per-source-IP token bucket (burst 2× the rate, min 4). Read and control endpoints share the same bucket.

Errors ​

CodeMeaning
400Bad request — invalid JSON body, missing/invalid field, or the operation rejected the input
401Missing/invalid API key
403Source IP not in metrics.whitelist, or control API disabled
405Wrong HTTP method for the path
429Rate limit exceeded
501/metrics/setup provider not initialized in this build
503Section 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 ​

PathMethodDescription
/metricsGETFull JSON snapshot (schema below)
/healthGET{ "status": "ok", "generated_at": <unix> }
/metrics/baselineGETBaseline learner status + 30-day history
/metrics/geoGETGeo-blocking mode, country list, background jobs
/metrics/alerterGETWebhook delivery statistics
/metrics/autofetchGETBlocklist auto-fetcher status
/metrics/accessGETWhitelist + blacklist entries
/metrics/targetsGETTop 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/attachedGETAttached-IP registry summary: auto/manual/pool counts + cumulative reaps (v2.17+)
/metrics/blackholeGETTenant-blackhole status: active entries with origin/expiry, exemption count, kernel drop counter (v2.17+)
/metrics/egressGETTC egress policer status + cumulative verdict counters (v2.18+, opt-in)
/metrics/forensicsGETForensics storage: dir, size vs cap, collecting/halted state, cleanup counters
/metrics/edgeGETEdge-mitigation module status (provider-aware) — the current name of the endpoint below
/metrics/ovhGETLegacy alias of /metrics/edge — serves the identical payload
/metrics/scheduleGETActive suppression windows
/metrics/dstipsGETTracked destination IPs overview: tracked count + top-5 destinations by current pps
/metrics/dstip?ip=<addr>[&range=15m|1h|24h]GETFull per-destination analysis: registry/blackhole flags (with remaining seconds), current rates, per-second series (minute-resolution on 1h/24h), windows, attack history
/metrics/setupGETPending "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 ​

jsonc
{
  "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. The current_pps vs passed_pps pair 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 cumulative pass_rate/drop_rate mix in peacetime traffic) and live_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 — so state, duration and the end-of-attack report keep accruing — but it is fully mitigated. The TUI shows an amber CONTAINED badge in this state instead of the red UNDER ATTACK one.
  • 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, reasons like "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's top_targets entries carry state (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), or transit (unknown). On forwarding deployments (VXLAN, routed upstream) dst_stats sees every destination crossing the wire, and only pool/manual means "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 read auto and alerts render the legacy flat list.
  • config — flat key = value map of the active mitigation config. License keys, webhook URLs and other secrets are excluded by construction.

Every top-level key in snapshot:

KeyContents
timestampsnapshot build time (unix)
globalcurrent/total rates — the proof-of-mitigation pair
attackattack state machine (state, thresholds, contained)
bans / top_offendersactive ban count + recent entries; per-IP rates/suspicion
trafficper-protocol rates, peaks, top_ports, per-second history series
systemloader version, kernel, uptime, interface, xdp_mode
mapsBPF map occupancy
profkernel per-drop-path counters
recent_events / logslatest loader events and log lines
attack_timelinestate transitions, newest first
paniccircuit-breaker state
licensetier, hwid, expires_at (error when invalid)
alerterwebhook delivery health — same shape as /metrics/alerter
attachedregistry summary — same shape as /metrics/attached
blackholeblackhole status — same shape as /metrics/blackhole
egresspolicer counters — same shape as /metrics/egress
dstiptracked-destination summary — same shape as /metrics/dstips

/metrics/baseline ​

jsonc
{
  "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 ​

jsonc
{
  "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 ​

jsonc
{
  "generated_at": 1754325123,
  "data": {
    "webhook_set": true,
    "queued": 0, "dropped": 0, "sent": 137,
    "last_success_at": 1754325100,
    "last_error": ""             // omitted when empty
  }
}

/metrics/autofetch ​

jsonc
{
  "generated_at": 1754325123,
  "data": {
    "status": { "...": "fetcher state (last run, entries loaded, errors)" },
    "categories": ["c2", "botnets", "..."],
    "urls": [],
    "never_block": ["203.0.113.10"]
  }
}

/metrics/access ​

jsonc
{
  "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 ​

jsonc
{
  "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):

jsonc
{
  "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.

jsonc
{
  "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.

jsonc
{
  "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.

jsonc
{ "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):

jsonc
{
  "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:

ParamValuesDefaultNotes
ipIPv4/IPv6 address—required
range15m | 1h | 24h15m15m = 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.
jsonc
{
  "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.

jsonc
{
  "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:

  1. Set metrics.whitelist to exactly the IPs of your dashboard/management hosts.
  2. 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.
  3. Keep listen on 127.0.0.1 unless 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.

yaml
metrics:
  enabled: true
  control_enabled: true          # root-equivalent — protect the port!
  whitelist: ["198.51.100.20"]   # your dashboard host ONLY

Control 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:

jsonc
{ "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.

bash
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/config

New 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 — always applies the geo country list at all times; attack only 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:

bash
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.

jsonc
{ "success": true, "applied": 2 }

Whitelist ​

bash
# 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/import

Responses: { "success": true, "ip": "..." }, { "success": true, "removed": 42 }, { "success": true, "added": 980, "skipped": 3 }.

Blacklist ​

bash
# 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/clear

Responses 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).

bash
# 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/blackhole

Both 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.

bash
# 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/clear

CLI: 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).

bash
# 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/asn

Mutations 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.

bash
curl -X POST ... -d '{"id":"nic_tuning_hardware","value":true}' .../control/setup/answer
jsonc
{ "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 ​

bash
# 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
jsonc
{ "success": true, "country": "CN", "blocked": true }   // blocked=true: added (job started), false: removed

Geo/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.

bash
# 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/list

Requires 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):

bash
curl -H "Authorization: Bearer osk_..." .../control/baseline/export > baseline-backup.json

POST /control/baseline/import — replace the live baseline + history from an export:

bash
curl -X POST ... -d "{\"payload\": $(cat baseline-backup.json | jq -Rs .)}" .../control/baseline/import

POST /control/baseline/delete — drop one poisoned day; the merged baseline is recomputed immediately:

bash
curl -X POST ... -d '{"date":"2026-08-04"}' .../control/baseline/delete
# → { "success": true, "date": "2026-08-04", "history_days": 29 }

Auto-fetch (blocklist feeds) ​

bash
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-now

never_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):

bash
# 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
jsonc
{ "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:

bash
curl -X POST ... -d '{
  "port": 25565, "proto": 17, "offset": 0,
  "pattern": "fefd", "mask": "ffff",
  "min_payload": 0, "max_payload": 0
}' .../control/block-pattern
  • proto: 6/17 or "tcp"/"udp" (0/"any"/omitted = both).
  • pattern/mask: hex bytes, max 8 bytes each. port_is_src: true matches the source port instead of the destination.
  • name is optional (auto-generated as api-<proto>-<port>-<offset> when omitted).
jsonc
{ "success": true, "name": "api-udp-25565-0" }

Attack history & bulk-ban ​

GET /control/attacks — full attack history plus the forensics base directory:

jsonc
{
  "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):

bash
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:

jsonc
{ "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 listen on localhost unless you need it, and set whitelist to your dashboard host's IP when listening on all interfaces:
yaml
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.

Next steps ​

Config Values in Plain Language · CLI Reference · TUI Guide