From fdbb3f636e0a42a7c6a2ffda5a38b75960b5c5df Mon Sep 17 00:00:00 2001 From: Zach Kipp Date: Fri, 16 Jan 2026 15:49:12 -0700 Subject: [PATCH 1/4] feat: add Coder Agent Boundaries dashboard Add dashboard for monitoring workspace agent boundary audit logs, showing HTTP requests that are audited by boundary within workspaces. Panels: - Request Totals: count of allowed vs denied requests - Top Allowed/Denied Domains: most frequently accessed domains - Recent Allowed/Denied Requests: detailed table with time, domain, method, path, workspace owner, and workspace name Includes filters for domain and workspace owner to narrow results. --- README.md | 5 + .../dashboards/_dashboards_boundary.json.tpl | 782 +++++++++++++++++ ...configmap-dashboards-agent-boundaries.yaml | 12 + coder-observability/values.yaml | 5 + compiled/resources.yaml | 796 ++++++++++++++++++ 5 files changed, 1600 insertions(+) create mode 100644 coder-observability/templates/dashboards/_dashboards_boundary.json.tpl create mode 100644 coder-observability/templates/dashboards/configmap-dashboards-agent-boundaries.yaml diff --git a/README.md b/README.md index a952c86..0d12168 100644 --- a/README.md +++ b/README.md @@ -475,6 +475,11 @@ values which are defined [here](https://github.com/grafana/helm-charts/tree/main | grafana.extraConfigmapMounts[5].name | string | `"coder-dashboard-prebuilds"` | | | grafana.extraConfigmapMounts[5].optional | bool | `true` | | | grafana.extraConfigmapMounts[5].readOnly | bool | `false` | | +| grafana.extraConfigmapMounts[6].configMap | string | `"coder-dashboard-agent-boundaries"` | | +| grafana.extraConfigmapMounts[6].mountPath | string | `"/var/lib/grafana/dashboards/coder/6"` | | +| grafana.extraConfigmapMounts[6].name | string | `"coder-dashboard-agent-boundaries"` | | +| grafana.extraConfigmapMounts[6].optional | bool | `true` | | +| grafana.extraConfigmapMounts[6].readOnly | bool | `false` | | | grafana.fullnameOverride | string | `"grafana"` | | | grafana.image.tag | string | `"10.4.19"` | | | grafana.persistence.enabled | bool | `true` | | diff --git a/coder-observability/templates/dashboards/_dashboards_boundary.json.tpl b/coder-observability/templates/dashboards/_dashboards_boundary.json.tpl new file mode 100644 index 0000000..0b95c42 --- /dev/null +++ b/coder-observability/templates/dashboards/_dashboards_boundary.json.tpl @@ -0,0 +1,782 @@ +{{ define "agent-boundaries-dashboard.json" }} +{ + "annotations": { + "list": [ + { + "builtIn": 1, + "datasource": { + "type": "grafana", + "uid": "-- Grafana --" + }, + "enable": true, + "hide": true, + "iconColor": "rgba(0, 211, 255, 1)", + "name": "Annotations & Alerts", + "target": { + "limit": 100, + "matchAny": false, + "tags": [], + "type": "dashboard" + }, + "type": "dashboard" + } + ] + }, + "editable": true, + "fiscalYearStartMonth": 0, + "graphTooltip": 0, + "links": [], + "panels": [ + { + "datasource": { + "type": "loki", + "uid": "loki" + }, + "description": "Total number of requests allowed/denied", + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + } + ] + } + }, + "overrides": [ + { + "matcher": { + "id": "byName", + "options": "{decision=\"allow\"}" + }, + "properties": [ + { + "id": "displayName", + "value": "Allowed" + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "{decision=\"deny\"}" + }, + "properties": [ + { + "id": "displayName", + "value": "Denied" + } + ] + } + ] + }, + "gridPos": { + "h": 7, + "w": 20, + "x": 0, + "y": 0 + }, + "id": 5, + "options": { + "colorMode": "value", + "graphMode": "area", + "justifyMode": "auto", + "orientation": "auto", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "showPercentChange": false, + "textMode": "auto", + "wideLayout": true + }, + "pluginVersion": "10.4.0", + "targets": [ + { + "datasource": { + "type": "loki", + "uid": "loki" + }, + "direction": "backward", + "editorMode": "code", + "expr": "sum by (decision) (count_over_time({ {{- include "non-workspace-selector" . -}}, logger=`coderd.agentrpc`} |= `boundary_request` | logfmt | decision=~`deny|allow` | owner=~`$owner` [$__range]))", + "queryType": "range", + "refId": "A" + } + ], + "title": "Request Totals", + "type": "stat" + }, + { + "datasource": { + "type": "loki", + "uid": "loki" + }, + "description": "Top 20 allowed domains for HTTP requests", + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "custom": { + "align": "auto", + "cellOptions": { + "type": "auto" + }, + "filterable": false, + "inspect": false + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [ + { + "matcher": { + "id": "byName", + "options": "Domain" + }, + "properties": [ + { + "id": "custom.width", + "value": 340 + } + ] + } + ] + }, + "gridPos": { + "h": 12, + "w": 10, + "x": 0, + "y": 7 + }, + "id": 1, + "options": { + "cellHeight": "sm", + "footer": { + "countRows": false, + "enablePagination": false, + "fields": "", + "reducer": [ + "sum" + ], + "show": false + }, + "frameIndex": 1, + "showHeader": true, + "sortBy": [ + { + "desc": true, + "displayName": "Count" + } + ] + }, + "pluginVersion": "10.4.0", + "targets": [ + { + "datasource": { + "type": "loki", + "uid": "loki" + }, + "direction": "backward", + "editorMode": "code", + "expr": "topk(20, sum by (domain) (count_over_time({ {{- include "non-workspace-selector" . -}}, logger=`coderd.agentrpc`} |= `boundary_request` | logfmt | decision=`allow` | owner=~`$owner` | regexp `http_url=(?Phttps?)://(?P[^/:]+)` | domain=~`$domain` [$__auto])))", + "legendFormat": "", + "queryType": "instant", + "refId": "A" + } + ], + "title": "Top Allowed Domains", + "transformations": [ + { + "id": "organize", + "options": { + "excludeByName": { + "Time": true + }, + "includeByName": {}, + "indexByName": {}, + "renameByName": { + "Time": "", + "Value #A": "Count", + "domain": "Domain" + } + } + }, + { + "id": "sortBy", + "options": { + "fields": {}, + "sort": [ + { + "desc": true, + "field": "Count" + } + ] + } + } + ], + "type": "table" + }, + { + "datasource": { + "type": "loki", + "uid": "loki" + }, + "description": "Top 20 denied domains for HTTP requests", + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "custom": { + "align": "auto", + "cellOptions": { + "type": "auto" + }, + "filterable": false, + "inspect": false + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [ + { + "matcher": { + "id": "byName", + "options": "Domain" + }, + "properties": [ + { + "id": "custom.width", + "value": 382 + } + ] + } + ] + }, + "gridPos": { + "h": 12, + "w": 10, + "x": 10, + "y": 7 + }, + "id": 2, + "options": { + "cellHeight": "sm", + "footer": { + "countRows": false, + "enablePagination": false, + "fields": "", + "reducer": [ + "sum" + ], + "show": false + }, + "frameIndex": 1, + "showHeader": true, + "sortBy": [ + { + "desc": true, + "displayName": "Count" + } + ] + }, + "pluginVersion": "10.4.0", + "targets": [ + { + "datasource": { + "type": "loki", + "uid": "loki" + }, + "direction": "backward", + "editorMode": "code", + "expr": "topk(20, sum by (domain) (count_over_time({ {{- include "non-workspace-selector" . -}}, logger=`coderd.agentrpc`} |= `boundary_request` | logfmt | decision=`deny` | owner=~`$owner` | regexp `http_url=(?Phttps?)://(?P[^/:]+)` | domain=~`$domain` [$__auto])))", + "legendFormat": "", + "queryType": "instant", + "refId": "A" + } + ], + "title": "Top Denied Domains", + "transformations": [ + { + "id": "organize", + "options": { + "excludeByName": { + "Time": true + }, + "includeByName": {}, + "indexByName": {}, + "renameByName": { + "Time": "", + "Value #A": "Count", + "domain": "Domain" + } + } + }, + { + "id": "sortBy", + "options": { + "fields": {}, + "sort": [ + { + "desc": true, + "field": "Count" + } + ] + } + } + ], + "type": "table" + }, + { + "datasource": { + "type": "loki", + "uid": "loki" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "custom": { + "align": "auto", + "cellOptions": { + "type": "auto" + }, + "inspect": false + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [ + { + "matcher": { + "id": "byName", + "options": "Time" + }, + "properties": [ + { + "id": "custom.width", + "value": 282 + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Domain" + }, + "properties": [ + { + "id": "custom.width", + "value": 185 + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "method" + }, + "properties": [ + { + "id": "custom.width", + "value": 73 + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "path" + }, + "properties": [ + { + "id": "custom.width", + "value": 397 + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Workspace Owner" + }, + "properties": [ + { + "id": "custom.width", + "value": 144 + } + ] + } + ] + }, + "gridPos": { + "h": 12, + "w": 22, + "x": 0, + "y": 19 + }, + "id": 3, + "options": { + "cellHeight": "sm", + "footer": { + "countRows": false, + "fields": "", + "reducer": [ + "sum" + ], + "show": false + }, + "showHeader": true, + "sortBy": [] + }, + "pluginVersion": "10.4.0", + "targets": [ + { + "datasource": { + "type": "loki", + "uid": "loki" + }, + "direction": "backward", + "editorMode": "code", + "expr": "{ {{- include "non-workspace-selector" . -}}, logger=`coderd.agentrpc`} |= `boundary_request` | logfmt | decision=`allow` | owner=~`$owner` | regexp `http_url=https?://(?P[^/?#]+)(?P/[^?#]*)?` | domain=~`$domain` | line_format `time=\"{{ "{{" }}.event_time{{ "}}" }}\" domain={{ "{{" }}.domain{{ "}}" }} method={{ "{{" }}.http_method{{ "}}" }} path=\"{{ "{{" }}.path{{ "}}" }}\" owner={{ "{{" }}.owner{{ "}}" }} workspace_name=\"{{ "{{" }}.workspace_name{{ "}}" }}\"`", + "queryType": "range", + "refId": "A" + } + ], + "title": "Most recent allowed requests", + "transformations": [ + { + "id": "limit", + "options": { + "limitField": "10" + } + }, + { + "id": "extractFields", + "options": { + "delimiter": "|", + "format": "kvp", + "keepTime": false, + "replace": true, + "source": "Line" + } + }, + { + "id": "organize", + "options": { + "excludeByName": { + "id": true, + "labelTypes": true, + "labels": true, + "tsNs": true + }, + "includeByName": {}, + "indexByName": { + "domain": 2, + "method": 1, + "owner": 4, + "path": 3, + "time": 0, + "workspace_name": 5 + }, + "renameByName": { + "Line": "Domain", + "domain": "Domain", + "labels": "", + "method": "Method", + "owner": "Workspace Owner", + "path": "Path", + "time": "Time", + "tsNs": "", + "workspace_name": "Workspace Name" + } + } + } + ], + "type": "table" + }, + { + "datasource": { + "type": "loki", + "uid": "loki" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "custom": { + "align": "auto", + "cellOptions": { + "type": "auto" + }, + "inspect": false + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [ + { + "matcher": { + "id": "byName", + "options": "Time" + }, + "properties": [ + { + "id": "custom.width", + "value": 282 + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Domain" + }, + "properties": [ + { + "id": "custom.width", + "value": 185 + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "method" + }, + "properties": [ + { + "id": "custom.width", + "value": 73 + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "path" + }, + "properties": [ + { + "id": "custom.width", + "value": 397 + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Workspace Owner" + }, + "properties": [ + { + "id": "custom.width", + "value": 152 + } + ] + } + ] + }, + "gridPos": { + "h": 12, + "w": 22, + "x": 0, + "y": 31 + }, + "id": 6, + "options": { + "cellHeight": "sm", + "footer": { + "countRows": false, + "fields": "", + "reducer": [ + "sum" + ], + "show": false + }, + "showHeader": true, + "sortBy": [] + }, + "pluginVersion": "10.4.0", + "targets": [ + { + "datasource": { + "type": "loki", + "uid": "loki" + }, + "direction": "backward", + "editorMode": "code", + "expr": "{ {{- include "non-workspace-selector" . -}}, logger=`coderd.agentrpc`} |= `boundary_request` | logfmt | decision=`deny` | owner=~`$owner` | regexp `http_url=https?://(?P[^/?#]+)(?P/[^?#]*)?` | domain=~`$domain` | line_format `time=\"{{ "{{" }}.event_time{{ "}}" }}\" domain={{ "{{" }}.domain{{ "}}" }} method={{ "{{" }}.http_method{{ "}}" }} path=\"{{ "{{" }}.path{{ "}}" }}\" owner={{ "{{" }}.owner{{ "}}" }} workspace_name=\"{{ "{{" }}.workspace_name{{ "}}" }}\"`", + "queryType": "range", + "refId": "A" + } + ], + "title": "Most recent denied requests", + "transformations": [ + { + "id": "limit", + "options": { + "limitField": "10" + } + }, + { + "id": "extractFields", + "options": { + "delimiter": "|", + "format": "kvp", + "keepTime": false, + "replace": true, + "source": "Line" + } + }, + { + "id": "organize", + "options": { + "excludeByName": { + "id": true, + "labelTypes": true, + "labels": true, + "tsNs": true + }, + "includeByName": {}, + "indexByName": { + "domain": 2, + "method": 1, + "owner": 4, + "path": 3, + "time": 0, + "workspace_name": 5 + }, + "renameByName": { + "Line": "Domain", + "domain": "Domain", + "labels": "", + "method": "Method", + "owner": "Workspace Owner", + "path": "Path", + "time": "Time", + "tsNs": "", + "workspace_name": "Workspace Name" + } + } + } + ], + "type": "table" + } + ], + "refresh": "{{ include "dashboard-refresh" . }}", + "schemaVersion": 39, + "tags": [], + "templating": { + "list": [ + { + "current": { + "text": "", + "value": "" + }, + "description": "Search for blocked paths/methods by domain", + "label": "Domain", + "name": "domain", + "options": [ + { + "selected": true, + "text": "", + "value": "" + } + ], + "query": "", + "type": "textbox" + }, + { + "current": { + "text": "", + "value": "" + }, + "description": "Search for allowed/denied requests by workspace owner", + "label": "Workspace Owner", + "name": "owner", + "options": [ + { + "selected": true, + "text": "", + "value": "" + } + ], + "query": "", + "type": "textbox" + } + ] + }, + "time": { + "from": "now-{{ include "dashboard-range" . }}", + "to": "now" + }, + "timepicker": {}, + "timezone": "browser", + "title": "Coder Agent Boundaries", + "uid": "agent-boundaries", + "version": 1, + "weekStart": "" +} +{{ end }} diff --git a/coder-observability/templates/dashboards/configmap-dashboards-agent-boundaries.yaml b/coder-observability/templates/dashboards/configmap-dashboards-agent-boundaries.yaml new file mode 100644 index 0000000..1d5eb82 --- /dev/null +++ b/coder-observability/templates/dashboards/configmap-dashboards-agent-boundaries.yaml @@ -0,0 +1,12 @@ +{{- if .Values.global.dashboards.enabled }} +apiVersion: v1 +kind: ConfigMap +metadata: + name: coder-dashboard-agent-boundaries + namespace: {{ .Release.Namespace }} + {{- with .Values.global.dashboards.labels }} + labels: {{- toYaml . | nindent 4 }} + {{- end }} +data: + coder-agent-boundaries.json: |- {{- include "agent-boundaries-dashboard.json" . | trim | nindent 4 }} +{{- end }} diff --git a/coder-observability/values.yaml b/coder-observability/values.yaml index ae1d2e0..459cca8 100644 --- a/coder-observability/values.yaml +++ b/coder-observability/values.yaml @@ -481,6 +481,11 @@ grafana: configMap: coder-dashboard-prebuilds readOnly: false optional: true + - name: coder-dashboard-agent-boundaries + mountPath: /var/lib/grafana/dashboards/coder/6 + configMap: coder-dashboard-agent-boundaries + readOnly: false + optional: true prometheus: enabled: true diff --git a/compiled/resources.yaml b/compiled/resources.yaml index f8bb42d..56b6a79 100644 --- a/compiled/resources.yaml +++ b/compiled/resources.yaml @@ -1028,6 +1028,795 @@ data: query: | SELECT pg_notification_queue_usage() AS usage; --- +# Source: coder-observability/templates/dashboards/configmap-dashboards-agent-boundaries.yaml +apiVersion: v1 +kind: ConfigMap +metadata: + name: coder-dashboard-agent-boundaries + namespace: coder-observability +data: + coder-agent-boundaries.json: |- + { + "annotations": { + "list": [ + { + "builtIn": 1, + "datasource": { + "type": "grafana", + "uid": "-- Grafana --" + }, + "enable": true, + "hide": true, + "iconColor": "rgba(0, 211, 255, 1)", + "name": "Annotations & Alerts", + "target": { + "limit": 100, + "matchAny": false, + "tags": [], + "type": "dashboard" + }, + "type": "dashboard" + } + ] + }, + "editable": true, + "fiscalYearStartMonth": 0, + "graphTooltip": 0, + "links": [], + "panels": [ + { + "datasource": { + "type": "loki", + "uid": "loki" + }, + "description": "Total number of requests allowed/denied", + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + } + ] + } + }, + "overrides": [ + { + "matcher": { + "id": "byName", + "options": "{decision=\"allow\"}" + }, + "properties": [ + { + "id": "displayName", + "value": "Allowed" + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "{decision=\"deny\"}" + }, + "properties": [ + { + "id": "displayName", + "value": "Denied" + } + ] + } + ] + }, + "gridPos": { + "h": 7, + "w": 20, + "x": 0, + "y": 0 + }, + "id": 5, + "options": { + "colorMode": "value", + "graphMode": "area", + "justifyMode": "auto", + "orientation": "auto", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "showPercentChange": false, + "textMode": "auto", + "wideLayout": true + }, + "pluginVersion": "10.4.0", + "targets": [ + { + "datasource": { + "type": "loki", + "uid": "loki" + }, + "direction": "backward", + "editorMode": "code", + "expr": "sum by (decision) (count_over_time({namespace=~`(coder|coder)`, logger=`coderd.agentrpc`} |= `boundary_request` | logfmt | decision=~`deny|allow` | owner=~`$owner` [$__range]))", + "queryType": "range", + "refId": "A" + } + ], + "title": "Request Totals", + "type": "stat" + }, + { + "datasource": { + "type": "loki", + "uid": "loki" + }, + "description": "Top 20 allowed domains for HTTP requests", + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "custom": { + "align": "auto", + "cellOptions": { + "type": "auto" + }, + "filterable": false, + "inspect": false + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [ + { + "matcher": { + "id": "byName", + "options": "Domain" + }, + "properties": [ + { + "id": "custom.width", + "value": 340 + } + ] + } + ] + }, + "gridPos": { + "h": 12, + "w": 10, + "x": 0, + "y": 7 + }, + "id": 1, + "options": { + "cellHeight": "sm", + "footer": { + "countRows": false, + "enablePagination": false, + "fields": "", + "reducer": [ + "sum" + ], + "show": false + }, + "frameIndex": 1, + "showHeader": true, + "sortBy": [ + { + "desc": true, + "displayName": "Count" + } + ] + }, + "pluginVersion": "10.4.0", + "targets": [ + { + "datasource": { + "type": "loki", + "uid": "loki" + }, + "direction": "backward", + "editorMode": "code", + "expr": "topk(20, sum by (domain) (count_over_time({namespace=~`(coder|coder)`, logger=`coderd.agentrpc`} |= `boundary_request` | logfmt | decision=`allow` | owner=~`$owner` | regexp `http_url=(?Phttps?)://(?P[^/:]+)` | domain=~`$domain` [$__auto])))", + "legendFormat": "", + "queryType": "instant", + "refId": "A" + } + ], + "title": "Top Allowed Domains", + "transformations": [ + { + "id": "organize", + "options": { + "excludeByName": { + "Time": true + }, + "includeByName": {}, + "indexByName": {}, + "renameByName": { + "Time": "", + "Value #A": "Count", + "domain": "Domain" + } + } + }, + { + "id": "sortBy", + "options": { + "fields": {}, + "sort": [ + { + "desc": true, + "field": "Count" + } + ] + } + } + ], + "type": "table" + }, + { + "datasource": { + "type": "loki", + "uid": "loki" + }, + "description": "Top 20 denied domains for HTTP requests", + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "custom": { + "align": "auto", + "cellOptions": { + "type": "auto" + }, + "filterable": false, + "inspect": false + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [ + { + "matcher": { + "id": "byName", + "options": "Domain" + }, + "properties": [ + { + "id": "custom.width", + "value": 382 + } + ] + } + ] + }, + "gridPos": { + "h": 12, + "w": 10, + "x": 10, + "y": 7 + }, + "id": 2, + "options": { + "cellHeight": "sm", + "footer": { + "countRows": false, + "enablePagination": false, + "fields": "", + "reducer": [ + "sum" + ], + "show": false + }, + "frameIndex": 1, + "showHeader": true, + "sortBy": [ + { + "desc": true, + "displayName": "Count" + } + ] + }, + "pluginVersion": "10.4.0", + "targets": [ + { + "datasource": { + "type": "loki", + "uid": "loki" + }, + "direction": "backward", + "editorMode": "code", + "expr": "topk(20, sum by (domain) (count_over_time({namespace=~`(coder|coder)`, logger=`coderd.agentrpc`} |= `boundary_request` | logfmt | decision=`deny` | owner=~`$owner` | regexp `http_url=(?Phttps?)://(?P[^/:]+)` | domain=~`$domain` [$__auto])))", + "legendFormat": "", + "queryType": "instant", + "refId": "A" + } + ], + "title": "Top Denied Domains", + "transformations": [ + { + "id": "organize", + "options": { + "excludeByName": { + "Time": true + }, + "includeByName": {}, + "indexByName": {}, + "renameByName": { + "Time": "", + "Value #A": "Count", + "domain": "Domain" + } + } + }, + { + "id": "sortBy", + "options": { + "fields": {}, + "sort": [ + { + "desc": true, + "field": "Count" + } + ] + } + } + ], + "type": "table" + }, + { + "datasource": { + "type": "loki", + "uid": "loki" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "custom": { + "align": "auto", + "cellOptions": { + "type": "auto" + }, + "inspect": false + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [ + { + "matcher": { + "id": "byName", + "options": "Time" + }, + "properties": [ + { + "id": "custom.width", + "value": 282 + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Domain" + }, + "properties": [ + { + "id": "custom.width", + "value": 185 + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "method" + }, + "properties": [ + { + "id": "custom.width", + "value": 73 + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "path" + }, + "properties": [ + { + "id": "custom.width", + "value": 397 + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Workspace Owner" + }, + "properties": [ + { + "id": "custom.width", + "value": 144 + } + ] + } + ] + }, + "gridPos": { + "h": 12, + "w": 22, + "x": 0, + "y": 19 + }, + "id": 3, + "options": { + "cellHeight": "sm", + "footer": { + "countRows": false, + "fields": "", + "reducer": [ + "sum" + ], + "show": false + }, + "showHeader": true, + "sortBy": [] + }, + "pluginVersion": "10.4.0", + "targets": [ + { + "datasource": { + "type": "loki", + "uid": "loki" + }, + "direction": "backward", + "editorMode": "code", + "expr": "{namespace=~`(coder|coder)`, logger=`coderd.agentrpc`} |= `boundary_request` | logfmt | decision=`allow` | owner=~`$owner` | regexp `http_url=https?://(?P[^/?#]+)(?P/[^?#]*)?` | domain=~`$domain` | line_format `time=\"{{.event_time}}\" domain={{.domain}} method={{.http_method}} path=\"{{.path}}\" owner={{.owner}} workspace_name=\"{{.workspace_name}}\"`", + "queryType": "range", + "refId": "A" + } + ], + "title": "Most recent allowed requests", + "transformations": [ + { + "id": "limit", + "options": { + "limitField": "10" + } + }, + { + "id": "extractFields", + "options": { + "delimiter": "|", + "format": "kvp", + "keepTime": false, + "replace": true, + "source": "Line" + } + }, + { + "id": "organize", + "options": { + "excludeByName": { + "id": true, + "labelTypes": true, + "labels": true, + "tsNs": true + }, + "includeByName": {}, + "indexByName": { + "domain": 2, + "method": 1, + "owner": 4, + "path": 3, + "time": 0, + "workspace_name": 5 + }, + "renameByName": { + "Line": "Domain", + "domain": "Domain", + "labels": "", + "method": "Method", + "owner": "Workspace Owner", + "path": "Path", + "time": "Time", + "tsNs": "", + "workspace_name": "Workspace Name" + } + } + } + ], + "type": "table" + }, + { + "datasource": { + "type": "loki", + "uid": "loki" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "custom": { + "align": "auto", + "cellOptions": { + "type": "auto" + }, + "inspect": false + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [ + { + "matcher": { + "id": "byName", + "options": "Time" + }, + "properties": [ + { + "id": "custom.width", + "value": 282 + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Domain" + }, + "properties": [ + { + "id": "custom.width", + "value": 185 + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "method" + }, + "properties": [ + { + "id": "custom.width", + "value": 73 + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "path" + }, + "properties": [ + { + "id": "custom.width", + "value": 397 + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Workspace Owner" + }, + "properties": [ + { + "id": "custom.width", + "value": 152 + } + ] + } + ] + }, + "gridPos": { + "h": 12, + "w": 22, + "x": 0, + "y": 31 + }, + "id": 6, + "options": { + "cellHeight": "sm", + "footer": { + "countRows": false, + "fields": "", + "reducer": [ + "sum" + ], + "show": false + }, + "showHeader": true, + "sortBy": [] + }, + "pluginVersion": "10.4.0", + "targets": [ + { + "datasource": { + "type": "loki", + "uid": "loki" + }, + "direction": "backward", + "editorMode": "code", + "expr": "{namespace=~`(coder|coder)`, logger=`coderd.agentrpc`} |= `boundary_request` | logfmt | decision=`deny` | owner=~`$owner` | regexp `http_url=https?://(?P[^/?#]+)(?P/[^?#]*)?` | domain=~`$domain` | line_format `time=\"{{.event_time}}\" domain={{.domain}} method={{.http_method}} path=\"{{.path}}\" owner={{.owner}} workspace_name=\"{{.workspace_name}}\"`", + "queryType": "range", + "refId": "A" + } + ], + "title": "Most recent denied requests", + "transformations": [ + { + "id": "limit", + "options": { + "limitField": "10" + } + }, + { + "id": "extractFields", + "options": { + "delimiter": "|", + "format": "kvp", + "keepTime": false, + "replace": true, + "source": "Line" + } + }, + { + "id": "organize", + "options": { + "excludeByName": { + "id": true, + "labelTypes": true, + "labels": true, + "tsNs": true + }, + "includeByName": {}, + "indexByName": { + "domain": 2, + "method": 1, + "owner": 4, + "path": 3, + "time": 0, + "workspace_name": 5 + }, + "renameByName": { + "Line": "Domain", + "domain": "Domain", + "labels": "", + "method": "Method", + "owner": "Workspace Owner", + "path": "Path", + "time": "Time", + "tsNs": "", + "workspace_name": "Workspace Name" + } + } + } + ], + "type": "table" + } + ], + "refresh": "30s", + "schemaVersion": 39, + "tags": [], + "templating": { + "list": [ + { + "current": { + "text": "", + "value": "" + }, + "description": "Search for blocked paths/methods by domain", + "label": "Domain", + "name": "domain", + "options": [ + { + "selected": true, + "text": "", + "value": "" + } + ], + "query": "", + "type": "textbox" + }, + { + "current": { + "text": "", + "value": "" + }, + "description": "Search for allowed/denied requests by workspace owner", + "label": "Workspace Owner", + "name": "owner", + "options": [ + { + "selected": true, + "text": "", + "value": "" + } + ], + "query": "", + "type": "textbox" + } + ] + }, + "time": { + "from": "now-12h", + "to": "now" + }, + "timepicker": {}, + "timezone": "browser", + "title": "Coder Agent Boundaries", + "uid": "agent-boundaries", + "version": 1, + "weekStart": "" + } +--- # Source: coder-observability/templates/dashboards/configmap-dashboards-coderd.yaml apiVersion: v1 kind: ConfigMap @@ -11784,6 +12573,10 @@ spec: mountPath: /var/lib/grafana/dashboards/coder/5 subPath: readOnly: false + - name: coder-dashboard-agent-boundaries + mountPath: /var/lib/grafana/dashboards/coder/6 + subPath: + readOnly: false - name: storage mountPath: "/var/lib/grafana" - name: config @@ -11856,6 +12649,9 @@ spec: - name: coder-dashboard-prebuilds configMap: name: coder-dashboard-prebuilds + - name: coder-dashboard-agent-boundaries + configMap: + name: coder-dashboard-agent-boundaries - name: dashboards-infra configMap: name: grafana-dashboards-infra From fc54e6497f34b8fec4cb62d52392acf08bc00da5 Mon Sep 17 00:00:00 2001 From: Zach Kipp Date: Thu, 22 Jan 2026 10:33:15 -0700 Subject: [PATCH 2/4] Add filtering by template ID and handle trailing path slashes --- .../dashboards/_dashboards_boundary.json.tpl | 117 +++++++++++------- compiled/resources.yaml | 117 +++++++++++------- 2 files changed, 142 insertions(+), 92 deletions(-) diff --git a/coder-observability/templates/dashboards/_dashboards_boundary.json.tpl b/coder-observability/templates/dashboards/_dashboards_boundary.json.tpl index 0b95c42..0890eaf 100644 --- a/coder-observability/templates/dashboards/_dashboards_boundary.json.tpl +++ b/coder-observability/templates/dashboards/_dashboards_boundary.json.tpl @@ -22,6 +22,7 @@ } ] }, + "description": "This dashboard shows HTTP requests audited by agent boundaries within Coder workspaces to provide visibility into workspace network activity.\n\nWhat it shows:\n - Total count of allowed and denied outbound HTTP requests\n - Top 20 most frequently accessed allowed domains\n - Top 20 most frequently blocked domains\n - Recent allowed requests with details (time, domain, method, path, workspace owner, workspace name, template ID)\n - Recent denied requests with the same details\n\nWho it's for:\n - Platform administrators and template administrators who need to audit workspace network activity and define agent boundary policies\n - Security team members who want to know what HTTP requests AI agents made in Coder workspaces for security incident review\n - Agent Boundaries policy owners who want to refine network access controls/security posture\n\nFilters available:\n - Template ID\n - HTTP request domain\n - Workspace owner", "editable": true, "fiscalYearStartMonth": 0, "graphTooltip": 0, @@ -44,7 +45,7 @@ "steps": [ { "color": "green", - "value": null + "value": 0 } ] } @@ -88,6 +89,7 @@ "graphMode": "area", "justifyMode": "auto", "orientation": "auto", + "percentChangeColorMode": "standard", "reduceOptions": { "calcs": [ "lastNotNull" @@ -99,7 +101,7 @@ "textMode": "auto", "wideLayout": true }, - "pluginVersion": "10.4.0", + "pluginVersion": "12.1.0", "targets": [ { "datasource": { @@ -108,7 +110,7 @@ }, "direction": "backward", "editorMode": "code", - "expr": "sum by (decision) (count_over_time({ {{- include "non-workspace-selector" . -}}, logger=`coderd.agentrpc`} |= `boundary_request` | logfmt | decision=~`deny|allow` | owner=~`$owner` [$__range]))", + "expr": "sum by (decision) (count_over_time({ {{- include "non-workspace-selector" . -}}, logger=`coderd.agentrpc`} |= `boundary_request` | logfmt | decision=~`deny|allow` | owner=~`$owner` | domain=~`$domain` | template_id=~`$template_id` [$__range]))", "queryType": "range", "refId": "A" } @@ -141,7 +143,7 @@ "steps": [ { "color": "green", - "value": null + "value": 0 }, { "color": "red", @@ -192,7 +194,7 @@ } ] }, - "pluginVersion": "10.4.0", + "pluginVersion": "12.1.0", "targets": [ { "datasource": { @@ -201,7 +203,7 @@ }, "direction": "backward", "editorMode": "code", - "expr": "topk(20, sum by (domain) (count_over_time({ {{- include "non-workspace-selector" . -}}, logger=`coderd.agentrpc`} |= `boundary_request` | logfmt | decision=`allow` | owner=~`$owner` | regexp `http_url=(?Phttps?)://(?P[^/:]+)` | domain=~`$domain` [$__auto])))", + "expr": "topk(20, sum by (domain) (count_over_time({ {{- include "non-workspace-selector" . -}}, logger=`coderd.agentrpc`} |= `boundary_request` | logfmt | decision=`allow` | owner=~`$owner` | template_id=~`$template_id` | regexp `http_url=(?Phttps?)://(?P[^/:]+)` | domain=~`$domain` [$__auto])))", "legendFormat": "", "queryType": "instant", "refId": "A" @@ -264,7 +266,7 @@ "steps": [ { "color": "green", - "value": null + "value": 0 }, { "color": "red", @@ -315,7 +317,7 @@ } ] }, - "pluginVersion": "10.4.0", + "pluginVersion": "12.1.0", "targets": [ { "datasource": { @@ -324,7 +326,7 @@ }, "direction": "backward", "editorMode": "code", - "expr": "topk(20, sum by (domain) (count_over_time({ {{- include "non-workspace-selector" . -}}, logger=`coderd.agentrpc`} |= `boundary_request` | logfmt | decision=`deny` | owner=~`$owner` | regexp `http_url=(?Phttps?)://(?P[^/:]+)` | domain=~`$domain` [$__auto])))", + "expr": "topk(20, sum by (domain) (count_over_time({ {{- include "non-workspace-selector" . -}}, logger=`coderd.agentrpc`} |= `boundary_request` | logfmt | decision=`deny` | owner=~`$owner` | template_id=~`$template_id` | regexp `http_url=(?Phttps?)://(?P[^/:]+)` | domain=~`$domain` [$__auto])))", "legendFormat": "", "queryType": "instant", "refId": "A" @@ -385,7 +387,7 @@ "steps": [ { "color": "green", - "value": null + "value": 0 }, { "color": "red", @@ -459,7 +461,7 @@ }, "gridPos": { "h": 12, - "w": 22, + "w": 24, "x": 0, "y": 19 }, @@ -477,7 +479,7 @@ "showHeader": true, "sortBy": [] }, - "pluginVersion": "10.4.0", + "pluginVersion": "12.1.0", "targets": [ { "datasource": { @@ -486,7 +488,7 @@ }, "direction": "backward", "editorMode": "code", - "expr": "{ {{- include "non-workspace-selector" . -}}, logger=`coderd.agentrpc`} |= `boundary_request` | logfmt | decision=`allow` | owner=~`$owner` | regexp `http_url=https?://(?P[^/?#]+)(?P/[^?#]*)?` | domain=~`$domain` | line_format `time=\"{{ "{{" }}.event_time{{ "}}" }}\" domain={{ "{{" }}.domain{{ "}}" }} method={{ "{{" }}.http_method{{ "}}" }} path=\"{{ "{{" }}.path{{ "}}" }}\" owner={{ "{{" }}.owner{{ "}}" }} workspace_name=\"{{ "{{" }}.workspace_name{{ "}}" }}\"`", + "expr": "{ {{- include "non-workspace-selector" . -}}, logger=`coderd.agentrpc`} |= `boundary_request` | logfmt | decision=`allow` | owner=~`$owner` | template_id=~`$template_id` | regexp `http_url=https?://(?P[^/?# ]+)(?P/[^?# ]*)?` | domain=~`$domain` | line_format `time=\"{{ "{{" }}.event_time{{ "}}" }}\" method=\"{{ "{{" }}.http_method{{ "}}" }}\" domain=\"{{ "{{" }}.domain{{ "}}" }}\" path=\"{{ "{{" }}.path{{ "}}" }}\" owner=\"{{ "{{" }}.owner{{ "}}" }}\" workspace_name=\"{{ "{{" }}.workspace_name{{ "}}" }}\" template_id=\"{{ "{{" }}.template_id{{ "}}" }}\"`", "queryType": "range", "refId": "A" } @@ -512,30 +514,32 @@ { "id": "organize", "options": { - "excludeByName": { - "id": true, - "labelTypes": true, - "labels": true, - "tsNs": true + "excludeByName": {}, + "includeByName": { + "time": true, + "method": true, + "domain": true, + "path": true, + "owner": true, + "workspace_name": true, + "template_id": true }, - "includeByName": {}, "indexByName": { - "domain": 2, + "time": 0, "method": 1, - "owner": 4, + "domain": 2, "path": 3, - "time": 0, - "workspace_name": 5 + "owner": 4, + "workspace_name": 5, + "template_id": 6 }, "renameByName": { - "Line": "Domain", "domain": "Domain", - "labels": "", "method": "Method", "owner": "Workspace Owner", "path": "Path", + "template_id": "Template ID", "time": "Time", - "tsNs": "", "workspace_name": "Workspace Name" } } @@ -566,7 +570,7 @@ "steps": [ { "color": "green", - "value": null + "value": 0 }, { "color": "red", @@ -640,7 +644,7 @@ }, "gridPos": { "h": 12, - "w": 22, + "w": 24, "x": 0, "y": 31 }, @@ -658,7 +662,7 @@ "showHeader": true, "sortBy": [] }, - "pluginVersion": "10.4.0", + "pluginVersion": "12.1.0", "targets": [ { "datasource": { @@ -667,7 +671,7 @@ }, "direction": "backward", "editorMode": "code", - "expr": "{ {{- include "non-workspace-selector" . -}}, logger=`coderd.agentrpc`} |= `boundary_request` | logfmt | decision=`deny` | owner=~`$owner` | regexp `http_url=https?://(?P[^/?#]+)(?P/[^?#]*)?` | domain=~`$domain` | line_format `time=\"{{ "{{" }}.event_time{{ "}}" }}\" domain={{ "{{" }}.domain{{ "}}" }} method={{ "{{" }}.http_method{{ "}}" }} path=\"{{ "{{" }}.path{{ "}}" }}\" owner={{ "{{" }}.owner{{ "}}" }} workspace_name=\"{{ "{{" }}.workspace_name{{ "}}" }}\"`", + "expr": "{ {{- include "non-workspace-selector" . -}}, logger=`coderd.agentrpc`} |= `boundary_request` | logfmt | decision=`deny` | owner=~`$owner` | template_id=~`$template_id` | regexp `http_url=https?://(?P[^/?# ]+)(?P/[^?# ]*)?` | domain=~`$domain` | line_format `time=\"{{ "{{" }}.event_time{{ "}}" }}\" method=\"{{ "{{" }}.http_method{{ "}}" }}\" domain=\"{{ "{{" }}.domain{{ "}}" }}\" path=\"{{ "{{" }}.path{{ "}}" }}\" owner=\"{{ "{{" }}.owner{{ "}}" }}\" workspace_name=\"{{ "{{" }}.workspace_name{{ "}}" }}\" template_id=\"{{ "{{" }}.template_id{{ "}}" }}\"`", "queryType": "range", "refId": "A" } @@ -693,30 +697,32 @@ { "id": "organize", "options": { - "excludeByName": { - "id": true, - "labelTypes": true, - "labels": true, - "tsNs": true + "excludeByName": {}, + "includeByName": { + "time": true, + "method": true, + "domain": true, + "path": true, + "owner": true, + "workspace_name": true, + "template_id": true }, - "includeByName": {}, "indexByName": { - "domain": 2, + "time": 0, "method": 1, - "owner": 4, + "domain": 2, "path": 3, - "time": 0, - "workspace_name": 5 + "owner": 4, + "workspace_name": 5, + "template_id": 6 }, "renameByName": { - "Line": "Domain", "domain": "Domain", - "labels": "", "method": "Method", "owner": "Workspace Owner", "path": "Path", + "template_id": "Template ID", "time": "Time", - "tsNs": "", "workspace_name": "Workspace Name" } } @@ -725,8 +731,9 @@ "type": "table" } ], + "preload": false, "refresh": "{{ include "dashboard-refresh" . }}", - "schemaVersion": 39, + "schemaVersion": 41, "tags": [], "templating": { "list": [ @@ -735,7 +742,7 @@ "text": "", "value": "" }, - "description": "Search for blocked paths/methods by domain", + "description": "Filter by request domain", "label": "Domain", "name": "domain", "options": [ @@ -753,7 +760,7 @@ "text": "", "value": "" }, - "description": "Search for allowed/denied requests by workspace owner", + "description": "Filter requests by workspace owner", "label": "Workspace Owner", "name": "owner", "options": [ @@ -765,6 +772,24 @@ ], "query": "", "type": "textbox" + }, + { + "current": { + "text": "", + "value": "" + }, + "description": "Filter requests by template ID", + "label": "Template ID", + "name": "template_id", + "options": [ + { + "selected": true, + "text": "", + "value": "" + } + ], + "query": "", + "type": "textbox" } ] }, diff --git a/compiled/resources.yaml b/compiled/resources.yaml index 56b6a79..50bac6e 100644 --- a/compiled/resources.yaml +++ b/compiled/resources.yaml @@ -1059,6 +1059,7 @@ data: } ] }, + "description": "This dashboard shows HTTP requests audited by agent boundaries within Coder workspaces to provide visibility into workspace network activity.\n\nWhat it shows:\n - Total count of allowed and denied outbound HTTP requests\n - Top 20 most frequently accessed allowed domains\n - Top 20 most frequently blocked domains\n - Recent allowed requests with details (time, domain, method, path, workspace owner, workspace name, template ID)\n - Recent denied requests with the same details\n\nWho it's for:\n - Platform administrators and template administrators who need to audit workspace network activity and define agent boundary policies\n - Security team members who want to know what HTTP requests AI agents made in Coder workspaces for security incident review\n - Agent Boundaries policy owners who want to refine network access controls/security posture\n\nFilters available:\n - Template ID\n - HTTP request domain\n - Workspace owner", "editable": true, "fiscalYearStartMonth": 0, "graphTooltip": 0, @@ -1081,7 +1082,7 @@ data: "steps": [ { "color": "green", - "value": null + "value": 0 } ] } @@ -1125,6 +1126,7 @@ data: "graphMode": "area", "justifyMode": "auto", "orientation": "auto", + "percentChangeColorMode": "standard", "reduceOptions": { "calcs": [ "lastNotNull" @@ -1136,7 +1138,7 @@ data: "textMode": "auto", "wideLayout": true }, - "pluginVersion": "10.4.0", + "pluginVersion": "12.1.0", "targets": [ { "datasource": { @@ -1145,7 +1147,7 @@ data: }, "direction": "backward", "editorMode": "code", - "expr": "sum by (decision) (count_over_time({namespace=~`(coder|coder)`, logger=`coderd.agentrpc`} |= `boundary_request` | logfmt | decision=~`deny|allow` | owner=~`$owner` [$__range]))", + "expr": "sum by (decision) (count_over_time({namespace=~`(coder|coder)`, logger=`coderd.agentrpc`} |= `boundary_request` | logfmt | decision=~`deny|allow` | owner=~`$owner` | domain=~`$domain` | template_id=~`$template_id` [$__range]))", "queryType": "range", "refId": "A" } @@ -1178,7 +1180,7 @@ data: "steps": [ { "color": "green", - "value": null + "value": 0 }, { "color": "red", @@ -1229,7 +1231,7 @@ data: } ] }, - "pluginVersion": "10.4.0", + "pluginVersion": "12.1.0", "targets": [ { "datasource": { @@ -1238,7 +1240,7 @@ data: }, "direction": "backward", "editorMode": "code", - "expr": "topk(20, sum by (domain) (count_over_time({namespace=~`(coder|coder)`, logger=`coderd.agentrpc`} |= `boundary_request` | logfmt | decision=`allow` | owner=~`$owner` | regexp `http_url=(?Phttps?)://(?P[^/:]+)` | domain=~`$domain` [$__auto])))", + "expr": "topk(20, sum by (domain) (count_over_time({namespace=~`(coder|coder)`, logger=`coderd.agentrpc`} |= `boundary_request` | logfmt | decision=`allow` | owner=~`$owner` | template_id=~`$template_id` | regexp `http_url=(?Phttps?)://(?P[^/:]+)` | domain=~`$domain` [$__auto])))", "legendFormat": "", "queryType": "instant", "refId": "A" @@ -1301,7 +1303,7 @@ data: "steps": [ { "color": "green", - "value": null + "value": 0 }, { "color": "red", @@ -1352,7 +1354,7 @@ data: } ] }, - "pluginVersion": "10.4.0", + "pluginVersion": "12.1.0", "targets": [ { "datasource": { @@ -1361,7 +1363,7 @@ data: }, "direction": "backward", "editorMode": "code", - "expr": "topk(20, sum by (domain) (count_over_time({namespace=~`(coder|coder)`, logger=`coderd.agentrpc`} |= `boundary_request` | logfmt | decision=`deny` | owner=~`$owner` | regexp `http_url=(?Phttps?)://(?P[^/:]+)` | domain=~`$domain` [$__auto])))", + "expr": "topk(20, sum by (domain) (count_over_time({namespace=~`(coder|coder)`, logger=`coderd.agentrpc`} |= `boundary_request` | logfmt | decision=`deny` | owner=~`$owner` | template_id=~`$template_id` | regexp `http_url=(?Phttps?)://(?P[^/:]+)` | domain=~`$domain` [$__auto])))", "legendFormat": "", "queryType": "instant", "refId": "A" @@ -1422,7 +1424,7 @@ data: "steps": [ { "color": "green", - "value": null + "value": 0 }, { "color": "red", @@ -1496,7 +1498,7 @@ data: }, "gridPos": { "h": 12, - "w": 22, + "w": 24, "x": 0, "y": 19 }, @@ -1514,7 +1516,7 @@ data: "showHeader": true, "sortBy": [] }, - "pluginVersion": "10.4.0", + "pluginVersion": "12.1.0", "targets": [ { "datasource": { @@ -1523,7 +1525,7 @@ data: }, "direction": "backward", "editorMode": "code", - "expr": "{namespace=~`(coder|coder)`, logger=`coderd.agentrpc`} |= `boundary_request` | logfmt | decision=`allow` | owner=~`$owner` | regexp `http_url=https?://(?P[^/?#]+)(?P/[^?#]*)?` | domain=~`$domain` | line_format `time=\"{{.event_time}}\" domain={{.domain}} method={{.http_method}} path=\"{{.path}}\" owner={{.owner}} workspace_name=\"{{.workspace_name}}\"`", + "expr": "{namespace=~`(coder|coder)`, logger=`coderd.agentrpc`} |= `boundary_request` | logfmt | decision=`allow` | owner=~`$owner` | template_id=~`$template_id` | regexp `http_url=https?://(?P[^/?# ]+)(?P/[^?# ]*)?` | domain=~`$domain` | line_format `time=\"{{.event_time}}\" method=\"{{.http_method}}\" domain=\"{{.domain}}\" path=\"{{.path}}\" owner=\"{{.owner}}\" workspace_name=\"{{.workspace_name}}\" template_id=\"{{.template_id}}\"`", "queryType": "range", "refId": "A" } @@ -1549,30 +1551,32 @@ data: { "id": "organize", "options": { - "excludeByName": { - "id": true, - "labelTypes": true, - "labels": true, - "tsNs": true + "excludeByName": {}, + "includeByName": { + "time": true, + "method": true, + "domain": true, + "path": true, + "owner": true, + "workspace_name": true, + "template_id": true }, - "includeByName": {}, "indexByName": { - "domain": 2, + "time": 0, "method": 1, - "owner": 4, + "domain": 2, "path": 3, - "time": 0, - "workspace_name": 5 + "owner": 4, + "workspace_name": 5, + "template_id": 6 }, "renameByName": { - "Line": "Domain", "domain": "Domain", - "labels": "", "method": "Method", "owner": "Workspace Owner", "path": "Path", + "template_id": "Template ID", "time": "Time", - "tsNs": "", "workspace_name": "Workspace Name" } } @@ -1603,7 +1607,7 @@ data: "steps": [ { "color": "green", - "value": null + "value": 0 }, { "color": "red", @@ -1677,7 +1681,7 @@ data: }, "gridPos": { "h": 12, - "w": 22, + "w": 24, "x": 0, "y": 31 }, @@ -1695,7 +1699,7 @@ data: "showHeader": true, "sortBy": [] }, - "pluginVersion": "10.4.0", + "pluginVersion": "12.1.0", "targets": [ { "datasource": { @@ -1704,7 +1708,7 @@ data: }, "direction": "backward", "editorMode": "code", - "expr": "{namespace=~`(coder|coder)`, logger=`coderd.agentrpc`} |= `boundary_request` | logfmt | decision=`deny` | owner=~`$owner` | regexp `http_url=https?://(?P[^/?#]+)(?P/[^?#]*)?` | domain=~`$domain` | line_format `time=\"{{.event_time}}\" domain={{.domain}} method={{.http_method}} path=\"{{.path}}\" owner={{.owner}} workspace_name=\"{{.workspace_name}}\"`", + "expr": "{namespace=~`(coder|coder)`, logger=`coderd.agentrpc`} |= `boundary_request` | logfmt | decision=`deny` | owner=~`$owner` | template_id=~`$template_id` | regexp `http_url=https?://(?P[^/?# ]+)(?P/[^?# ]*)?` | domain=~`$domain` | line_format `time=\"{{.event_time}}\" method=\"{{.http_method}}\" domain=\"{{.domain}}\" path=\"{{.path}}\" owner=\"{{.owner}}\" workspace_name=\"{{.workspace_name}}\" template_id=\"{{.template_id}}\"`", "queryType": "range", "refId": "A" } @@ -1730,30 +1734,32 @@ data: { "id": "organize", "options": { - "excludeByName": { - "id": true, - "labelTypes": true, - "labels": true, - "tsNs": true + "excludeByName": {}, + "includeByName": { + "time": true, + "method": true, + "domain": true, + "path": true, + "owner": true, + "workspace_name": true, + "template_id": true }, - "includeByName": {}, "indexByName": { - "domain": 2, + "time": 0, "method": 1, - "owner": 4, + "domain": 2, "path": 3, - "time": 0, - "workspace_name": 5 + "owner": 4, + "workspace_name": 5, + "template_id": 6 }, "renameByName": { - "Line": "Domain", "domain": "Domain", - "labels": "", "method": "Method", "owner": "Workspace Owner", "path": "Path", + "template_id": "Template ID", "time": "Time", - "tsNs": "", "workspace_name": "Workspace Name" } } @@ -1762,8 +1768,9 @@ data: "type": "table" } ], + "preload": false, "refresh": "30s", - "schemaVersion": 39, + "schemaVersion": 41, "tags": [], "templating": { "list": [ @@ -1772,7 +1779,7 @@ data: "text": "", "value": "" }, - "description": "Search for blocked paths/methods by domain", + "description": "Filter by request domain", "label": "Domain", "name": "domain", "options": [ @@ -1790,7 +1797,7 @@ data: "text": "", "value": "" }, - "description": "Search for allowed/denied requests by workspace owner", + "description": "Filter requests by workspace owner", "label": "Workspace Owner", "name": "owner", "options": [ @@ -1802,6 +1809,24 @@ data: ], "query": "", "type": "textbox" + }, + { + "current": { + "text": "", + "value": "" + }, + "description": "Filter requests by template ID", + "label": "Template ID", + "name": "template_id", + "options": [ + { + "selected": true, + "text": "", + "value": "" + } + ], + "query": "", + "type": "textbox" } ] }, From 861d9227eb3522724be51ced0428623ebb008a0c Mon Sep 17 00:00:00 2001 From: Zach Kipp Date: Thu, 22 Jan 2026 15:55:56 -0700 Subject: [PATCH 3/4] Add filtering by template version ID --- .../dashboards/_dashboards_boundary.json.tpl | 98 ++++++++++++++----- compiled/resources.yaml | 98 ++++++++++++++----- 2 files changed, 144 insertions(+), 52 deletions(-) diff --git a/coder-observability/templates/dashboards/_dashboards_boundary.json.tpl b/coder-observability/templates/dashboards/_dashboards_boundary.json.tpl index 0890eaf..5f7c26b 100644 --- a/coder-observability/templates/dashboards/_dashboards_boundary.json.tpl +++ b/coder-observability/templates/dashboards/_dashboards_boundary.json.tpl @@ -110,7 +110,7 @@ }, "direction": "backward", "editorMode": "code", - "expr": "sum by (decision) (count_over_time({ {{- include "non-workspace-selector" . -}}, logger=`coderd.agentrpc`} |= `boundary_request` | logfmt | decision=~`deny|allow` | owner=~`$owner` | domain=~`$domain` | template_id=~`$template_id` [$__range]))", + "expr": "sum by (decision) (count_over_time({ {{- include "non-workspace-selector" . -}}, logger=`coderd.agentrpc`} |= `boundary_request` | logfmt | decision=~`deny|allow` | owner=~`$owner` | domain=~`$domain` | template_id=~`$template_id` | template_version_id=~`$template_version_id` [$__range]))", "queryType": "range", "refId": "A" } @@ -203,7 +203,7 @@ }, "direction": "backward", "editorMode": "code", - "expr": "topk(20, sum by (domain) (count_over_time({ {{- include "non-workspace-selector" . -}}, logger=`coderd.agentrpc`} |= `boundary_request` | logfmt | decision=`allow` | owner=~`$owner` | template_id=~`$template_id` | regexp `http_url=(?Phttps?)://(?P[^/:]+)` | domain=~`$domain` [$__auto])))", + "expr": "topk(20, sum by (domain) (count_over_time({ {{- include "non-workspace-selector" . -}}, logger=`coderd.agentrpc`} |= `boundary_request` | logfmt | decision=`allow` | owner=~`$owner` | template_id=~`$template_id` | template_version_id=~`$template_version_id` | regexp `http_url=(?Phttps?)://(?P[^/:]+)` | domain=~`$domain` [$__auto])))", "legendFormat": "", "queryType": "instant", "refId": "A" @@ -326,7 +326,7 @@ }, "direction": "backward", "editorMode": "code", - "expr": "topk(20, sum by (domain) (count_over_time({ {{- include "non-workspace-selector" . -}}, logger=`coderd.agentrpc`} |= `boundary_request` | logfmt | decision=`deny` | owner=~`$owner` | template_id=~`$template_id` | regexp `http_url=(?Phttps?)://(?P[^/:]+)` | domain=~`$domain` [$__auto])))", + "expr": "topk(20, sum by (domain) (count_over_time({ {{- include "non-workspace-selector" . -}}, logger=`coderd.agentrpc`} |= `boundary_request` | logfmt | decision=`deny` | owner=~`$owner` | template_id=~`$template_id` | template_version_id=~`$template_version_id` | regexp `http_url=(?Phttps?)://(?P[^/:]+)` | domain=~`$domain` [$__auto])))", "legendFormat": "", "queryType": "instant", "refId": "A" @@ -456,6 +456,30 @@ "value": 144 } ] + }, + { + "matcher": { + "id": "byName", + "options": "Template ID" + }, + "properties": [ + { + "id": "custom.width", + "value": 195 + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Workspace Name" + }, + "properties": [ + { + "id": "custom.width", + "value": 204 + } + ] } ] }, @@ -488,7 +512,7 @@ }, "direction": "backward", "editorMode": "code", - "expr": "{ {{- include "non-workspace-selector" . -}}, logger=`coderd.agentrpc`} |= `boundary_request` | logfmt | decision=`allow` | owner=~`$owner` | template_id=~`$template_id` | regexp `http_url=https?://(?P[^/?# ]+)(?P/[^?# ]*)?` | domain=~`$domain` | line_format `time=\"{{ "{{" }}.event_time{{ "}}" }}\" method=\"{{ "{{" }}.http_method{{ "}}" }}\" domain=\"{{ "{{" }}.domain{{ "}}" }}\" path=\"{{ "{{" }}.path{{ "}}" }}\" owner=\"{{ "{{" }}.owner{{ "}}" }}\" workspace_name=\"{{ "{{" }}.workspace_name{{ "}}" }}\" template_id=\"{{ "{{" }}.template_id{{ "}}" }}\"`", + "expr": "{ {{- include "non-workspace-selector" . -}}, logger=`coderd.agentrpc`} |= `boundary_request` | logfmt | decision=`allow` | owner=~`$owner` | template_id=~`$template_id` | template_version_id=~`$template_version_id` | regexp `http_url=https?://(?P[^/?# ]+)(?P/[^?# ]*)?` | domain=~`$domain` | line_format `time=\"{{ "{{" }}.event_time{{ "}}" }}\" method=\"{{ "{{" }}.http_method{{ "}}" }}\" domain=\"{{ "{{" }}.domain{{ "}}" }}\" path=\"{{ "{{" }}.path{{ "}}" }}\" owner=\"{{ "{{" }}.owner{{ "}}" }}\" workspace_name=\"{{ "{{" }}.workspace_name{{ "}}" }}\" template_id=\"{{ "{{" }}.template_id{{ "}}" }}\" template_version_id=\"{{ "{{" }}.template_version_id{{ "}}" }}\"`", "queryType": "range", "refId": "A" } @@ -516,22 +540,23 @@ "options": { "excludeByName": {}, "includeByName": { - "time": true, - "method": true, "domain": true, - "path": true, + "method": true, "owner": true, - "workspace_name": true, - "template_id": true + "path": true, + "template_id": true, + "template_version_id": true, + "time": true, + "workspace_name": true }, "indexByName": { - "time": 0, - "method": 1, "domain": 2, - "path": 3, + "method": 1, "owner": 4, - "workspace_name": 5, - "template_id": 6 + "path": 3, + "template_id": 6, + "time": 0, + "workspace_name": 5 }, "renameByName": { "domain": "Domain", @@ -539,6 +564,7 @@ "owner": "Workspace Owner", "path": "Path", "template_id": "Template ID", + "template_version_id": "Template Version ID", "time": "Time", "workspace_name": "Workspace Name" } @@ -671,7 +697,7 @@ }, "direction": "backward", "editorMode": "code", - "expr": "{ {{- include "non-workspace-selector" . -}}, logger=`coderd.agentrpc`} |= `boundary_request` | logfmt | decision=`deny` | owner=~`$owner` | template_id=~`$template_id` | regexp `http_url=https?://(?P[^/?# ]+)(?P/[^?# ]*)?` | domain=~`$domain` | line_format `time=\"{{ "{{" }}.event_time{{ "}}" }}\" method=\"{{ "{{" }}.http_method{{ "}}" }}\" domain=\"{{ "{{" }}.domain{{ "}}" }}\" path=\"{{ "{{" }}.path{{ "}}" }}\" owner=\"{{ "{{" }}.owner{{ "}}" }}\" workspace_name=\"{{ "{{" }}.workspace_name{{ "}}" }}\" template_id=\"{{ "{{" }}.template_id{{ "}}" }}\"`", + "expr": "{ {{- include "non-workspace-selector" . -}}, logger=`coderd.agentrpc`} |= `boundary_request` | logfmt | decision=`deny` | owner=~`$owner` | template_id=~`$template_id` | template_version_id=~`$template_version_id` | regexp `http_url=https?://(?P[^/?# ]+)(?P/[^?# ]*)?` | domain=~`$domain` | line_format `time=\"{{ "{{" }}.event_time{{ "}}" }}\" method=\"{{ "{{" }}.http_method{{ "}}" }}\" domain=\"{{ "{{" }}.domain{{ "}}" }}\" path=\"{{ "{{" }}.path{{ "}}" }}\" owner=\"{{ "{{" }}.owner{{ "}}" }}\" workspace_name=\"{{ "{{" }}.workspace_name{{ "}}" }}\" template_id=\"{{ "{{" }}.template_id{{ "}}" }}\" template_version_id=\"{{ "{{" }}.template_version_id{{ "}}" }}\"`", "queryType": "range", "refId": "A" } @@ -699,22 +725,23 @@ "options": { "excludeByName": {}, "includeByName": { - "time": true, - "method": true, "domain": true, - "path": true, + "method": true, "owner": true, - "workspace_name": true, - "template_id": true + "path": true, + "template_id": true, + "template_version_id": true, + "time": true, + "workspace_name": true }, "indexByName": { - "time": 0, - "method": 1, "domain": 2, - "path": 3, + "method": 1, "owner": 4, - "workspace_name": 5, - "template_id": 6 + "path": 3, + "template_id": 6, + "time": 0, + "workspace_name": 5 }, "renameByName": { "domain": "Domain", @@ -722,6 +749,7 @@ "owner": "Workspace Owner", "path": "Path", "template_id": "Template ID", + "template_version_id": "Template Version ID", "time": "Time", "workspace_name": "Workspace Name" } @@ -778,7 +806,7 @@ "text": "", "value": "" }, - "description": "Filter requests by template ID", + "description": "Filter requests by template ID (UUID). Template IDs can be found via the CLI with \"coder templates list\".", "label": "Template ID", "name": "template_id", "options": [ @@ -790,6 +818,24 @@ ], "query": "", "type": "textbox" + }, + { + "current": { + "text": "", + "value": "" + }, + "description": "Filter by template version ID (UUID). A templates version IDs can be found via the CLI with \"coder templates versions list