{ "annotations": { "list": [ { "builtIn": 1, "datasource": { "type": "datasource", "uid": "grafana" }, "enable": true, "hide": true, "iconColor": "rgba(0, 211, 255, 1)", "name": "Annotations & Alerts", "type": "dashboard" } ] }, "description": "RPKI (Resource Public Key Infrastructure) validation status. Teaches BGP routing security and how RPKI prevents prefix hijacks by validating route origin.", "editable": true, "fiscalYearStartMonth": 0, "graphTooltip": 1, "id": null, "links": [ { "asDropdown": true, "icon": "external link", "includeVars": true, "keepTime": true, "tags": [ "obmp-nav" ], "title": "OBMP Dashboards", "type": "dashboards" } ], "panels": [ { "content": "## What is RPKI?\n\nRPKI (Resource Public Key Infrastructure) is a cryptographic security framework for BGP routing. It lets IP address holders publish **Route Origin Authorizations (ROAs)** stating which ASNs are authorized to originate their prefixes.\n\n### RPKI Validation States\n| State | Meaning |\n|-------|----------|\n| **Valid** | The route's origin AS matches a ROA for this prefix |\n| **Invalid** | A ROA exists but the origin AS or prefix length does NOT match \u2014 this route is potentially a hijack |\n| **NotFound** | No ROA exists for this prefix/origin \u2014 unprotected, can't be validated |\n\n### How to read this dashboard\n- **Valid %** should be as high as possible (target: 100%)\n- **Invalid routes** are critical \u2014 they indicate either a misconfiguration or a prefix hijack\n- Routes with no RPKI data show as **NotFound** \u2014 they are not necessarily invalid, just unprotected\n\n> **Lab note:** The RPKI validator table is populated by a cron job in psql-app every 2 hours. If the table shows 0 rows, wait for the cron to run or check `ENABLE_RPKI=1` in docker-compose.yml.", "datasource": { "type": "datasource", "uid": "grafana" }, "gridPos": { "h": 10, "w": 8, "x": 0, "y": 0 }, "id": 1, "options": { "content": "## What is RPKI?\n\nRPKI (Resource Public Key Infrastructure) is a cryptographic security framework for BGP routing. It lets IP address holders publish **Route Origin Authorizations (ROAs)** stating which ASNs are authorized to originate their prefixes.\n\n### RPKI Validation States\n| State | Meaning |\n|-------|----------|\n| **Valid** | The route's origin AS matches a ROA for this prefix |\n| **Invalid** | A ROA exists but the origin AS or prefix length does NOT match \u2014 this route is potentially a hijack |\n| **NotFound** | No ROA exists for this prefix/origin \u2014 unprotected, can't be validated |\n\n### How to read this dashboard\n- **Valid %** should be as high as possible (target: 100%)\n- **Invalid routes** are critical \u2014 they indicate either a misconfiguration or a prefix hijack\n- Routes with no RPKI data show as **NotFound** \u2014 they are not necessarily invalid, just unprotected\n\n> **Lab note:** The RPKI validator table is populated by a cron job in psql-app every 2 hours. If the table shows 0 rows, wait for the cron to run or check `ENABLE_RPKI=1` in docker-compose.yml.", "mode": "markdown" }, "pluginVersion": "9.1.7", "title": "RPKI Learning Guide", "type": "text" }, { "datasource": { "type": "postgres", "uid": "obmp_postgres" }, "description": "Total ROAs (Route Origin Authorizations) loaded from the RPKI validator. If 0, the cron job has not yet run.", "fieldConfig": { "defaults": { "color": { "mode": "thresholds" }, "thresholds": { "mode": "absolute", "steps": [ { "color": "red", "value": null }, { "color": "yellow", "value": 1 }, { "color": "green", "value": 100000 } ] }, "unit": "short" } }, "gridPos": { "h": 5, "w": 4, "x": 8, "y": 0 }, "id": 2, "options": { "colorMode": "background", "graphMode": "none", "justifyMode": "auto", "orientation": "auto", "reduceOptions": { "calcs": [ "lastNotNull" ], "fields": "", "values": false }, "text": {} }, "targets": [ { "datasource": { "type": "postgres", "uid": "obmp_postgres" }, "format": "time_series", "rawSql": "SELECT NOW() AS time, COUNT(*) AS \"RPKI ROAs Loaded\" FROM rpki_validator", "refId": "A" } ], "title": "RPKI ROAs Loaded", "type": "stat" }, { "datasource": { "type": "postgres", "uid": "obmp_postgres" }, "description": "Routes with a matching valid ROA \u2014 origin AS and prefix length both match.", "fieldConfig": { "defaults": { "color": { "mode": "thresholds" }, "thresholds": { "mode": "absolute", "steps": [ { "color": "red", "value": null }, { "color": "green", "value": 1 } ] }, "unit": "short" } }, "gridPos": { "h": 5, "w": 4, "x": 12, "y": 0 }, "id": 3, "options": { "colorMode": "background", "graphMode": "none", "justifyMode": "auto", "orientation": "auto", "reduceOptions": { "calcs": [ "lastNotNull" ], "fields": "", "values": false }, "text": {} }, "targets": [ { "datasource": { "type": "postgres", "uid": "obmp_postgres" }, "format": "time_series", "rawSql": "SELECT NOW() AS time, COUNT(*) AS \"Valid Routes\"\nFROM ip_rib r\nJOIN base_attrs ba ON ba.hash_id = r.base_attr_hash_id\nJOIN rpki_validator rv ON rv.prefix >>= r.prefix AND rv.origin_as = ba.origin_as AND r.prefix_len <= rv.prefix_len_max\nWHERE r.iswithdrawn = false AND r.isipv4 = true", "refId": "A" } ], "title": "RPKI Valid Routes", "type": "stat" }, { "datasource": { "type": "postgres", "uid": "obmp_postgres" }, "description": "Routes where a ROA exists but the origin AS does NOT match \u2014 high-priority investigation needed.", "fieldConfig": { "defaults": { "color": { "mode": "thresholds" }, "thresholds": { "mode": "absolute", "steps": [ { "color": "green", "value": null }, { "color": "red", "value": 1 } ] }, "unit": "short" } }, "gridPos": { "h": 5, "w": 4, "x": 16, "y": 0 }, "id": 4, "options": { "colorMode": "background", "graphMode": "none", "justifyMode": "auto", "orientation": "auto", "reduceOptions": { "calcs": [ "lastNotNull" ], "fields": "", "values": false }, "text": {} }, "targets": [ { "datasource": { "type": "postgres", "uid": "obmp_postgres" }, "format": "time_series", "rawSql": "SELECT NOW() AS time, COUNT(*) AS \"RPKI Invalid Routes\"\nFROM ip_rib r\nJOIN base_attrs ba ON ba.hash_id = r.base_attr_hash_id\nWHERE r.iswithdrawn = false AND r.isipv4 = true\n AND EXISTS (\n SELECT 1 FROM rpki_validator rv\n WHERE rv.prefix >>= r.prefix AND rv.origin_as != ba.origin_as\n )\n AND NOT EXISTS (\n SELECT 1 FROM rpki_validator rv\n WHERE rv.prefix >>= r.prefix AND rv.origin_as = ba.origin_as AND r.prefix_len <= rv.prefix_len_max\n )", "refId": "A" } ], "title": "RPKI Invalid Routes", "type": "stat" }, { "datasource": { "type": "postgres", "uid": "obmp_postgres" }, "description": "Learn: ExaBGP-injected routes (AS 65100) will be NotFound since they use synthetic ASNs not registered in RPKI. Real internet prefixes with valid ROAs will appear as Valid.", "fieldConfig": { "defaults": { "color": { "mode": "palette-classic" }, "custom": { "hideFrom": { "legend": false, "tooltip": false, "viz": false } }, "mappings": [] }, "overrides": [] }, "gridPos": { "h": 10, "w": 10, "x": 0, "y": 10 }, "id": 5, "options": { "displayLabels": [ "percent", "name" ], "legend": { "displayMode": "list", "placement": "bottom" }, "pieType": "donut", "tooltip": { "mode": "single" } }, "targets": [ { "datasource": { "type": "postgres", "uid": "obmp_postgres" }, "format": "table", "rawSql": "SELECT\n CASE\n WHEN rv_valid.prefix IS NOT NULL THEN 'Valid'\n WHEN rv_any.prefix IS NOT NULL THEN 'Invalid'\n ELSE 'NotFound'\n END AS \"RPKI Status\",\n COUNT(*) AS \"Route Count\"\nFROM ip_rib r\nJOIN base_attrs ba ON ba.hash_id = r.base_attr_hash_id\nLEFT JOIN rpki_validator rv_valid\n ON rv_valid.prefix >>= r.prefix AND rv_valid.origin_as = ba.origin_as AND r.prefix_len <= rv_valid.prefix_len_max\nLEFT JOIN rpki_validator rv_any\n ON rv_any.prefix >>= r.prefix AND rv_any.origin_as != ba.origin_as\nWHERE r.iswithdrawn = false AND r.isipv4 = true\nGROUP BY 1\nORDER BY 1", "refId": "A" } ], "title": "RPKI Validation Status Distribution", "type": "piechart" }, { "datasource": { "type": "postgres", "uid": "obmp_postgres" }, "description": "Prefixes that have a ROA but the observed origin AS does not match. These are the most security-critical routes \u2014 each one represents a potential hijack or misconfiguration.", "fieldConfig": { "defaults": { "custom": { "align": "auto", "displayMode": "auto" } }, "overrides": [ { "matcher": { "id": "byName", "options": "Status" }, "properties": [ { "id": "custom.displayMode", "value": "color-background" }, { "id": "mappings", "value": [ { "options": { "Invalid": { "color": "red", "index": 0 }, "Valid": { "color": "green", "index": 1 }, "NotFound": { "color": "yellow", "index": 2 } }, "type": "value" } ] } ] } ] }, "gridPos": { "h": 14, "w": 14, "x": 10, "y": 10 }, "id": 6, "options": { "footer": { "fields": "", "reducer": [ "sum" ], "show": false }, "showHeader": true }, "targets": [ { "datasource": { "type": "postgres", "uid": "obmp_postgres" }, "format": "table", "rawSql": "SELECT\n r.prefix AS \"Prefix\",\n ba.origin_as AS \"Observed Origin AS\",\n rv.origin_as AS \"Authorized Origin AS (ROA)\",\n 'Invalid' AS \"Status\"\nFROM ip_rib r\nJOIN base_attrs ba ON ba.hash_id = r.base_attr_hash_id\nJOIN rpki_validator rv ON rv.prefix >>= r.prefix AND rv.origin_as != ba.origin_as\nWHERE r.iswithdrawn = false AND r.isipv4 = true\n AND NOT EXISTS (\n SELECT 1 FROM rpki_validator rv2\n WHERE rv2.prefix >>= r.prefix AND rv2.origin_as = ba.origin_as AND r.prefix_len <= rv2.prefix_len_max\n )\nORDER BY r.prefix\nLIMIT 50", "refId": "A" } ], "title": "RPKI Invalid Routes \u2014 Potential Hijacks", "type": "table" } ], "schemaVersion": 36, "style": "dark", "tags": [ "obmp", "bgp", "rpki", "security", "obmp-nav" ], "time": { "from": "now-1h", "to": "now" }, "timepicker": {}, "timezone": "browser", "title": "RPKI Validation Status", "uid": "obmp-learn-04", "version": 1 }