Data Privacy¶
This exporter is primarily a fleet/topology metrics exporter: organizations, networks, devices, ports, SSIDs, sensors. That data is not personal data. The one subsystem that can touch data about identifiable people is client tracking (collectors/clients.py), which is opt-in and disabled by default. This page documents exactly what it collects, where it can end up, and how to restrict or disable it. See also Security for the endpoint threat model this page cross-references.
Off by default
Client collection is controlled by MERAKI_EXPORTER_CLIENTS__ENABLED (clients.enabled in config), which defaults to false. With the default configuration, none of the data described below is collected, stored, or exposed.
What counts as PII here¶
A Meraki "client" is a device (and by extension, usually a person) connected to the customer's network. The identifiers the Dashboard API returns for a client can include its MAC address, DHCP hostname, a user-supplied description, DNS-resolved hostname, IP address, and the SSID/VLAN it's attached to. Depending on your environment and local law, some of these (MAC address, hostname, resolved DNS name) may constitute personal data under GDPR or similar regimes.
The current data contract (post-#533): ID-only numeric series + one join carrier¶
As of #533, every numeric Prometheus client series carries only client_id — a stable Meraki identifier — as its client-scoped label, never mac/hostname/description/ssid. Confirmed in collectors/clients.py::_initialize_metrics: client_status, client_usage_sent/recv/total_bytes, client_app_usage_*_bytes, and the signal-quality gauges (wireless_client_rssi/_snr) all label only with org_id, network_id, client_id (plus type for app-usage).
The only metric allowed to carry the descriptive/PII-ish fields is a single join carrier:
meraki_client_info(ClientMetricName.CLIENT_INFO) — value always1, labelledorg_id,network_id,client_id,mac,description,hostname,ssid. It exists purely so a PromQL query can* on(client_id) group_left(mac, description, hostname, ssid)the human-readable fields onto the ID-only numeric series when you need them. Because the label values (hostname, description, SSID) are mutable, the series' label set churns and the old label combination expires whenever a client's hostname/description/SSID changes — this is expected metric-expiration behavior, not a bug.
This means the metrics plane (/metrics) already minimises PII exposure: a scraper or Prometheus retention store that never queries meraki_client_info never stores mac/hostname/description/ssid at all, only opaque client IDs. If you don't need the human-readable join, you can drop meraki_client_info at the scrape config (metric_relabel_configs action drop) to keep it out of your TSDB/WAL entirely while keeping the ID-only numeric series.
Where PII can actually land¶
Three places, only when client collection is enabled:
- The
meraki_client_infoPrometheus series (above) — scraped into your Prometheus/Mimir/Cortex TSDB and its WAL, subject to whatever retention policy that system has. This is the main metrics-plane exposure and the reason to treatmeraki_client_infoas sensitive data at the storage layer, same as any other PII you'd put in a time series. - The
/clientsHTML page. This is a full non-Prometheus, non-labelled view: rendered intemplates/clients.html, it shows description, hostname (DNS), IP address, IPv6 address, MAC address, and SSID/VLAN per client, sourced from the in-memoryClientStoreandDNSResolvercaches (not from the Prometheus registry). See Security → endpoint exposure for the full endpoint table —/clientsis flagged there as the one endpoint with a firm PII: Yes. - The OTel structured data-log channel, if separately enabled (
otel.logs.enabled, off by default) — see below.
Two in-memory caches back items 1–2 and are not exposed via /metrics themselves, but are the source data for /clients and hold PII in process memory for as long as their TTL:
ClientStore(services/client_store.py) — ID/hostname/description mappings, TTLclients.cache_ttl(MERAKI_EXPORTER_CLIENTS__CACHE_TTL, default 3600s / 1 hour).DNSResolverreverse-DNS cache (services/dns_resolver.py) — resolved hostnames keyed by client IP, TTLclients.dns_cache_ttl(MERAKI_EXPORTER_CLIENTS__DNS_CACHE_TTL, default 21600s / 6 hours), bounded byclients.dns_cache_max_entries(default 100,000 entries) so memory stays bounded under client churn.
Neither cache persists to disk; both are cleared on process restart, and the DNS cache can be cleared on demand via POST /api/clients/clear-dns-cache (itself protectable by server.api_token, see below).
Application-usage and reverse-DNS bounds¶
Client application-usage metrics retain at most clients.application_top_n application labels per client, plus any names in clients.application_allowlist and (by default) one __other__ aggregate label. Rows are ranked by total usage descending; equal totals break by the sanitized application label in ascending order. That stable tie-break prevents series from changing merely because the API returns equal-traffic applications in a different order. The maximum application-usage series per client is therefore three times application_top_n + len(application_allowlist) + 1 while the __other__ bucket is enabled (sent, received, and total); the existing client cap bounds the number of clients.
Reverse DNS is independently controlled by clients.dns_reverse_lookup_enabled (MERAKI_EXPORTER_CLIENTS__DNS_REVERSE_LOOKUP_ENABLED). It defaults to true to preserve existing hostname enrichment. When enabled, the exporter submits client IP addresses to the host resolver configured for the deployment; that resolver and its upstream query destination are chosen by the operating system or platform, not by the exporter. Set it to false to keep client collection while preventing those reverse-DNS queries. DNS work is capped by clients.dns_max_concurrent_lookups (default 32); keep DEBUG logs under the same access controls as other client data.
Client-identifying DEBUG logs¶
INFO, WARNING, and ERROR events deliberately contain counts, stable operation names, and safe error types only; they do not include client IP addresses, IDs, MAC addresses, hostnames, or descriptions. DEBUG is the intentional diagnostic surface and must receive the same access controls as client data. The DNS resolver can record queried, cached, expired, old, and new client IP addresses; resolved hostnames; and a client ID when an IP change invalidates a cached entry. The client collector can record a client ID, MAC address, description, hostname, and SSID when it emits or diagnoses client metrics. It may also record associated counts and network/organization context.
OTel data logs: the other PII surface, and its own opt-in gate¶
Independently of Prometheus metrics, the exporter has an optional OTLP data-log emitter (otel.logs, core/otel_data_logs.py, #622) for high-cardinality per-client signals (e.g. per-client wireless packet loss) that must never become a labelled Prometheus series — see OpenTelemetry → data logs vs. metrics for the full boundary rule. This channel is:
- Off by default —
otel.logs.enabled(MERAKI_EXPORTER_OTEL__LOGS__ENABLED) defaults tofalse, and is independent of bothotel.enabled(tracing) and the client collector's ownclients.enabledgate. - PII-stripped by default even when enabled —
otel.logs.include_identifiers(MERAKI_EXPORTER_OTEL__LOGS__INCLUDE_IDENTIFIERS) defaults tofalse, which dropsclient.mac/client.hostname/client.descriptionfrom every emitted record and scrubs MAC-shaped strings from data-log bodies as well as identifier keys. MAC-only client rows omitclient.id; the MAC is never substituted as an identifier. Set the flag totrueonly if you specifically want the human-readable identifiers on that channel too, and understand they'll travel to whatever OTLP endpoint you've configured.
Mitigations (by config key)¶
| Goal | Setting | Default | Effect |
|---|---|---|---|
| No client data at all | clients.enabled (MERAKI_EXPORTER_CLIENTS__ENABLED) | false | Collector is fully disabled: no client series, no /clients data, caches never populate. This is the default. |
| Keep client metrics but drop the PII join series at scrape time | n/a (Prometheus-side) | — | drop meraki_client_info in your scrape config's metric_relabel_configs; the ID-only numeric series are unaffected. |
Restrict who can view the /clients PII page | server.api_token (MERAKI_EXPORTER_SERVER__API_TOKEN) | unset (open) | Requires Authorization: Bearer <token> on /clients and the other sensitive GET UIs plus the control POSTs (/api/collectors/trigger, /api/clients/clear-dns-cache). |
Remove the /clients page (and other human UI) entirely | server.ui_enabled (MERAKI_EXPORTER_SERVER__UI_ENABLED) | true | When false, /clients (and /, /status, /config, /cardinality*) return 404; /metrics//health//ready stay open. |
| Bound in-memory PII cache lifetime | clients.cache_ttl, clients.dns_cache_ttl, clients.dns_cache_max_entries | 3600s, 21600s, 100000 | Shorter TTLs age out stale hostname/description/DNS mappings sooner; the max-entries cap bounds worst-case memory regardless of churn. |
| Disable reverse-DNS queries without disabling client metrics | clients.dns_reverse_lookup_enabled | true | When enabled, client IPs are sent to the deployment host resolver; set false to keep client collection while making no reverse-DNS queries. |
| Bound reverse-DNS work in flight | clients.dns_max_concurrent_lookups | 32 | Caps DNS workers and queue allocation, independently of the client cap. |
| Cap overall client series volume | clients.max_clients_per_network, clients.max_clients_total | 10000, 25000 | Clients beyond the cap are dropped from metric emission (counted in meraki_exporter_clients_over_cap), bounding both cardinality and PII surface area under very large client populations. |
| Avoid per-client signal-quality collection | clients.signal_quality_enabled | false | RSSI/SNR are ID-only labelled already, but this endpoint is also the most expensive per-client API call; leave off unless needed. |
| Keep the structured data-log PII-stripped if that channel is used | otel.logs.include_identifiers | false | Drops identifier keys and scrubs MAC-shaped strings from data-log bodies; MAC-only rows omit client.id rather than substituting a MAC. |
Summary¶
The safest and default posture is clients.enabled=false — no client-scoped data of any kind is collected. If you need client visibility, the ID-only-numeric-series-plus-meraki_client_info-join design (#533) already minimises what lands in your metrics TSDB by default; the remaining exposure to actively manage is the /clients HTML page (gate it with server.api_token and/or server.ui_enabled=false if the exporter is reachable by anyone other than trusted operators) and, if you separately opt into OTel data logs, the include_identifiers flag on that channel.