ObservabilityIX · ExportersExporters
Exporter Security
What you'll learn
- Choose the right bind address and exposure path for an exporter in production
- Configure TLS, basic auth, or mTLS termination for an exporter behind a reverse proxy or service mesh
- Use metric_relabel_configs to drop sensitive labels before storage and remove topology labels that change routing
- Integrate exporter exposure with a service mesh that terminates mTLS automatically
Prerequisites
Verified against Prometheus 2.55.x · Alertmanager 0.28.x · node_exporter 1.8.x · blackbox_exporter 0.26.x · Grafana 11.x · Loki 3.x · Tempo current · OpenTelemetry Collector 0.110.x · Grafana Alloy current · Docker Engine 28.x · Ubuntu 24.04 LTS · Debian 12 (Bookworm) · RHEL / Rocky / AlmaLinux 9.x · 2026-08-13
A scan of the production network turns up 1,200 hosts on which
node_exporter is listening on 0.0.0.0:9100, with no
authentication, no TLS, and no rate limiting. Every metric
about every host — process command lines, filesystem mount
points, environment variables from /proc/<pid>/environ —
is readable by anyone who can reach port 9100. The monitoring
team thought Prometheus was the only scraper. A bot from a
penetration test reached the endpoint in five minutes.
The exporter’s listening interface is a security boundary. The exporter’s endpoint is a credential surface. The exporter’s labels are data. Each of these requires deliberate configuration.
What it is
Exporter security is the discipline of placing the exporter’s HTTP endpoint behind an authenticated, encrypted boundary, restricting the labels that reach storage, and ensuring the exporter does not expose information that should not leave the host.
Three boundaries matter:
- Network boundary. What can reach the exporter on its
port. The default bind is
0.0.0.0, which means every host on the local network can scrape the exporter. - Authentication boundary. What identity the scraper must present. Most exporters do not implement auth natively; the auth happens at a reverse proxy or service mesh.
- Label boundary. What values reach the TSDB. The contract permits any UTF-8 in label values; the security decision is what you put there.
A fourth boundary is implicit: the process privilege boundary, covered in the community-trust lesson in this module. The exporter runs as root on most deployments; that boundary is real but is configured at the systemd unit or container SecurityContext level, not at the HTTP level.
Why a sysadmin cares
The exporter endpoint exposes information about the host that
is otherwise protected by the kernel’s permission model.
Without a security boundary, the exporter becomes a way for
anyone who can reach port 9100 to read /proc, /sys, and
the network interfaces of the host.
The failure shapes:
- The exposed metrics. Process command lines that include database passwords. Filesystem mount points that include secret volumes. Network interface names that include internal subnet information.
- The sensitive labels. A custom exporter that emits a
metric labelled by
customer_emailorapi_key. The label is replicated into the TSDB, the remote-write receiver, and every backup. - The topology leak. Labels that include IP addresses or internal host names let an attacker map the network from the metrics alone.
The trade-off: every security control you add is operational friction. The cost is real; the cost of the exposure is larger.
How it works
The mental model is layers of boundary, applied independently:
+-------------------------+
| Production network |
+-------------------------+
|
v
+-------------------------+
| Network ACL / firewall | <-- only Prometheus subnet
+-------------------------+
|
v
+-------------------------+
| Reverse proxy / mesh | <-- TLS, basic auth or mTLS
+-------------------------+
|
v
+-------------------------+
| Exporter on localhost | <-- 127.0.0.1 only
+-------------------------+
|
v
+-------------------------+
| metric_relabel_configs | <-- drop sensitive labels
+-------------------------+
|
v
Prometheus TSDB
Each layer is independent. Removing the network ACL but keeping the reverse proxy still works; removing the reverse proxy but keeping the bind-to-localhost still works; removing the relabel rules but keeping the bind-to-localhost still works. The strongest posture has all three.
The reverse proxy layer is the most flexible. A nginx-ingress-controller in Kubernetes, an Envoy sidecar in a service mesh, or a simple nginx in front of a bare-metal exporter all provide TLS termination, basic auth, and request limiting. The exporter itself stays simple; the policy lives at the proxy.
The relabel layer is in the Prometheus scrape config. A
metric_relabel_configs block drops or rewrites labels
after parsing but before storage. Sensitive labels are
removed here; topology labels that change routing are removed
here; labels that bloat cardinality can also be removed here,
though cardinality limits in sample_limit are the primary
control.
How to configure it
Each boundary has a separate configuration. The three blocks below cover a production setup.
1. Bind to localhost on the exporter.
# /etc/default/prometheus-node-exporter
ARGS="--web.listen-address=127.0.0.1:9100 \
--web.config.file=/etc/prometheus/exporter-web.yml"
The exporter now listens on loopback only. Anything that needs to scrape it must be on the same host, or must go through a reverse proxy that is.
2. Reverse proxy with TLS and basic auth.
# /etc/nginx/conf.d/exporter.conf
server {
listen 9100 ssl;
server_name _;
ssl_certificate /etc/nginx/tls/exporter.crt;
ssl_certificate_key /etc/nginx/tls/exporter.key;
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256;
# Bind to localhost on the upstream side
proxy_pass http://127.0.0.1:9100/metrics;
# Basic auth via htpasswd
auth_basic "Prometheus";
auth_basic_user_file /etc/nginx/htpasswd/exporter.htpasswd;
# Limit request rate
limit_req zone=exporter burst=10 nodelay;
access_log /var/log/nginx/exporter.access.log;
}
limit_req_zone $binary_remote_addr zone=exporter:10m rate=10r/s;
The proxy terminates TLS, requires basic auth, limits request rate, and forwards to the exporter on localhost. The exporter itself stays simple.
The Prometheus scrape config:
# prometheus.yml
scrape_configs:
- job_name: node
scheme: https
basic_auth:
username: prometheus
password_file: /etc/prometheus/exporter-htpasswd
tls_config:
ca_file: /etc/prometheus/ca-bundle.crt
server_name: node-exporter.internal
static_configs:
- targets:
- node-01.internal:9100
- node-02.internal:9100
metric_relabel_configs:
# Drop labels that may leak sensitive data
- source_labels: [__name__]
regex: 'node_exporter_build_info'
action: drop
# Build info is useful in dashboards but only the version
# label is needed; the others are noise.
- source_labels: [labelname]
regex: 'cmdline|environ|secret|token|api_key|password'
action: labeldrop
# Any label matching the pattern is dropped before storage.
- source_labels: [instance]
regex: '(.*)\.internal'
replacement: '$1'
target_label: instance_short
# Create a short instance label without the internal suffix.
The labeldrop action takes a regex and drops any label
name that matches. Be careful: cmdline matches
cmdline_arguments, kernel_cmdline, and any other label
ending in cmdline if you do not anchor the regex with ^.
3. Service mesh with mTLS termination. In a Kubernetes cluster with a service mesh (Istio, Linkerd), the sidecar proxy terminates mTLS automatically. The exporter binds to localhost; the sidecar proxies traffic on the pod IP.
# exporter-pod.yaml (excerpt)
apiVersion: apps/v1
kind: Deployment
metadata:
name: node-exporter
spec:
template:
metadata:
labels:
app: node-exporter
annotations:
# Linkerd injects the proxy sidecar
linkerd.io/inject: enabled
spec:
containers:
- name: node-exporter
image: prom/node-exporter:v1.8.2
args:
- --web.listen-address=127.0.0.1:9100
ports:
- containerPort: 9100
name: metrics
securityContext:
runAsNonRoot: false # node_exporter needs root for /proc
readOnlyRootFilesystem: true
allowPrivilegeEscalation: false
# ServiceMonitor for Prometheus Operator
apiVersion: monitoring.coreos.com/v1
kind: ServiceMonitor
metadata:
name: node-exporter
labels:
app: node-exporter
spec:
selector:
matchLabels:
app: node-exporter
endpoints:
- port: metrics
scheme: https
tlsConfig:
# The mesh terminates mTLS; Prometheus talks plain HTTP
# to the sidecar which proxies to the exporter.
insecureSkipVerify: true
metricRelabelings:
- source_labels: [labelname]
regex: 'cmdline|environ|secret|token|api_key|password'
action: labeldrop
The mesh handles authentication between Prometheus and the exporter; the relabel rules drop sensitive labels.
4. Topology labels that change routing. Some labels reveal internal network topology that should not be in metrics: IP addresses, internal host names, or labels whose values change under autoscaling. Drop them at relabel time:
metric_relabel_configs:
# Drop instance IPs; keep instance names.
- source_labels: [instance]
regex: '(\d+\.\d+\.\d+\.\d+):\d+'
action: labeldrop
# Drop labels that change with every pod restart.
- regex: 'pod_ip|pod_uid|container_id'
action: labeldrop
The trade-off: dropping these labels means you cannot pivot to that dimension. The discipline is to drop only what you would not want an attacker to see, not everything.
How to validate it
Validate each boundary independently. All commands are READ-ONLY.
# 1. Confirm the exporter is bound to localhost only.
ss -tlnp | grep 9100
LISTEN 0 128 127.0.0.1:9100 0.0.0.0:* users:(("node_exporter",pid=1234,fd=6))
Anything other than 127.0.0.1:9100 in the local address is
a misconfiguration.
# 2. Confirm TLS is terminated at the proxy.
curl -sI https://node-01.internal:9100/metrics | head -5
HTTP/1.1 200 OK
Server: nginx
Content-Type: text/plain; version=0.0.4; charset=utf-8
# 3. Confirm auth is enforced.
curl -sI https://node-01.internal:9100/metrics | head -3
HTTP/1.1 401 Unauthorized
WWW-Authenticate: Basic realm="Prometheus"
# 4. Confirm the contract still holds with TLS + auth.
# The password is whatever the scrape config's password_file holds.
EXPORTER_PASSWORD=$(sudo cat /etc/prometheus/exporter-htpasswd)
curl -sf -u "prometheus:$EXPORTER_PASSWORD" \
https://node-01.internal:9100/metrics > /tmp/node.prom
promtool check metrics /tmp/node.prom
node.prom: OK
# 5. Confirm sensitive labels are dropped in Prometheus.
# The cmdline label, if exposed by an exporter, should not
# appear here.
{labelname=~".*cmdline.*"}
# 6. Confirm topology labels are dropped.
{labelname=~".*pod_ip.*"}
The outputs confirm: the exporter is loopback-only, TLS is terminated, auth is enforced, the contract holds, and sensitive labels are gone.
How it can fail
Five specific failure modes:
- Exporter bound to 0.0.0.0 by default. A new deployment
uses the package default instead of
--web.listen-address. Symptom:ss -tlnpshows0.0.0.0:9100; an external host can scrape/metricswithout auth. - No TLS termination. Prometheus scrapes the exporter in plaintext over the internal network. Symptom: a packet capture on the path shows the metric body; the metrics are readable by anyone with network access to the route.
- Sensitive label in storage. A custom exporter emits a
customer_emaillabel; the relabel rule was forgotten or matched the wrong name. Symptom: the label appears in PromQL autocomplete; a Grafana dashboard panel reveals the email address. - Topology label leaks routing. A label like
pod_ipis on every series; an attacker maps the network from the metrics alone. Symptom: the label appears in metrics and the dashboard; the metric body reveals an internal subnet. - Auth credential rotated in the wrong place. Prometheus’s
basic_auth.password_fileis updated, but the htpasswd file on the proxy is not. Symptom: Prometheus scrape starts failing with401 Unauthorized;upflips to0.
How to troubleshoot it
Diagnose in this order; it is cheapest to confirm the network boundary first.
- What address is the exporter bound to?
ss -tlnpon the host. If the answer is0.0.0.0, that is the bug. - What auth is the proxy enforcing?
curl -Iwithout credentials. If the response is200 OK, auth is not configured. - What does the body contain?
curl -sfwith creds,grepfor sensitive label names. If they appear, the relabel rule is missing or wrong. - Are labels surviving into the TSDB that should not?
count by (__name__) ({labelname=~".*secret.*"})in PromQL. A non-zero result is a leak. - Is TLS terminated correctly?
openssl s_client -connectthe endpoint; check the certificate chain and the cipher in use.
Security implications
The exporter’s security boundary is the most operationally important part of the metrics stack. The discipline:
- Bind to localhost by default. The exporter’s own process
is the only thing that should reach
/metricsdirectly. - Terminate TLS at a proxy. TLS at the exporter is fine; TLS at a proxy gives you more control.
- Require basic auth or mTLS. The proxy is the right place for auth; the exporter should not implement its own.
- Drop sensitive labels at relabel time. Anything in a label value is replicated into the TSDB, remote-write receivers, and backups.
- Restrict outbound network. A compromised exporter should not phone home; NetworkPolicy or egress firewalling.
- Restrict process privileges. Run with minimum privileges
where possible; node_exporter-class exporters need root
because of
/procaccess.
Performance implications
Security and performance interact at the proxy. A reverse proxy in front of the exporter adds a small amount of latency per scrape (typically sub-millisecond) and uses CPU for TLS termination. The cost is small relative to the cost of the exporter itself; the operational benefit is large.
The trade-off: a service mesh adds a sidecar proxy to every pod. The proxy uses CPU and memory; in return, mTLS is automatic and the bind-address problem disappears.
Production guidance
- Bind every exporter to
127.0.0.1or a private interface. - Terminate TLS at a reverse proxy or service mesh. The exporter stays simple.
- Require basic auth or mTLS. The Prometheus
basic_authblock in the scrape config usespassword_fileto keep the credential off the command line. - Use
metric_relabel_configsto drop sensitive labels before storage. Document the dropped labels in the instrumentation guide. - Audit the bind address, TLS, and relabel rules on every exporter quarterly. The defaults shift on package upgrades.
Verification
You should now be able to answer:
- What are the four security boundaries around an exporter, and how are they configured independently?
- Why is
0.0.0.0the wrong default bind address for a production exporter, and how do you fix it? - How does
metric_relabel_configsdiffer from the scraperelabel_configs, and when does each apply? - What is the right pattern for integrating an exporter with a service mesh that terminates mTLS automatically?
- Which label names should always be dropped at relabel time, and what is the regex pattern?
Quiz
Knowledge check · 8 questions
Q1. What is the correct production bind address for an exporter that will be scraped via a reverse proxy on the same host?
Q2. Where should TLS and basic auth be terminated for an exporter in production?
Q3. metric_relabel_configs runs after the parser has classified the sample, but before storage.
Q4. A custom exporter emits a metric labelled by customer email. What is the correct way to prevent the email from reaching the TSDB?
Q5. Which of these label names are reasonable candidates for a labeldrop rule at relabel time? (Select all that apply.)
Q6. Which Prometheus scrape config block applies relabel rules to drop or rewrite labels after parsing but before storage?
Q7. A team integrates an exporter with a service mesh that terminates mTLS automatically. The exporter still binds to 127.0.0.1 inside the pod. What is the right pattern?
Q8. Which command confirms that an exporter is bound to localhost only?
Passing score: 75%. Answers are checked in this browser.