ObservabilityLXXVIII · Securing PrometheusSecurePrometheus
Prometheus Authentication
What you'll learn
- Configure HTTP basic_auth and bearer_token credentials per scrape job without leaking them into the config file or process arguments
- Distinguish basic_auth (for scrape targets that challenge the client) from bearer_token (for federation and remote-write)
- Harden the server-side authentication of Prometheus itself using basic_auth_users in the web configuration file
- Recognise the symptoms of a misconfigured credential file path, a wrong hash, or a default-credentials exporter
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 team scrapes a SaaS partner’s API for latency metrics. The
partner gave them a bearer token, tk_live_8f3a2c.... The team
pastes the token directly into prometheus.yml under
bearer_token: and commits the config to Git. A former
contractor still has read access to the repository. Two months
later the partner rotates the token and asks “who leaked this?”
The audit trail points at the commit hash. The token in the
config file was the leak.
Prometheus has two different authentication concepts that look similar and behave differently: client-side authentication, which is what Prometheus uses to authenticate itself to a scrape target, and server-side authentication, which is what Prometheus uses to authenticate the callers of its own API and UI. This lesson covers both, the right approach per scrape, and the most common ways the credentials leak.
What it is
Prometheus 2.55.x supports three client-side credential shapes on every scrape target and federation endpoint:
- HTTP Basic Auth —
usernameandpassword(orpassword_file) sent as aAuthorization: Basic <b64>header on every scrape. Use when the target expects a username and password (most common with exporters that support--web.config.fileand traditional services that speak HTTP basic). - Bearer Token —
bearer_token(literal string) orbearer_token_file(path to a file containing the token) sent asAuthorization: Bearer <token>. Use when the target expects a token (most modern SaaS metrics endpoints, the federation/federateendpoint, and many managed observability services). - mTLS — client certificate authentication via the
tls_configblock. Covered in lesson 03 (TLS).
The server side has its own authentication:
- Basic auth users for the Prometheus HTTP API, configured
in the web configuration file (
--web.config.file) underbasic_auth_users. The passwords are bcrypt hashes.
scrape target Prometheus callers of /api
+------------+ GET /metrics +------------+ GET /api/v1/...
| user: |<--Basic---------| |<--Basic----------<operator>
| pass: | | |
+------------+ | | GET /federate
| |<--Bearer----------<peer Prometheus>
+------------+ GET /metrics +------------+
| bearer: |<--Bearer--------| |
| tk_live... | | |
+------------+ +------------+
The mental model: scrape credentials live with the scrape job; API credentials live in the web configuration file; and neither should appear in a Git-tracked file.
Why a sysadmin cares
Credentials in Prometheus configs leak in three well-trodden ways:
- The config file is in a Git repository. Anyone with read access to the repo sees every scrape credential. A former contractor or a compromised CI token is enough.
- The config is exposed via
/api/v1/status/config. An authenticated or unauthenticated caller who can reach the Prometheus API gets the entire scrape config including any literalpassword:orbearer_token:. Lesson 04 covers the admin API surface; the takeaway is that every credential reachable from the Prometheus HTTP port is one access away from the config endpoint. - The credential is in process arguments. A
prometheus --bearer-token=tk_live_...invocation puts the token in/proc/<pid>/cmdlineand visible to any user with read access to/proc. The right answer ispassword_file/bearer_token_file.
The most expensive credential failures in real installs are not “Prometheus could not authenticate”; they are “an attacker authenticated because the credential was visible”.
How it works
Client-side: the scrape request
For every scrape target, Prometheus builds the HTTP request from
the scrape job config. If the job has basic_auth set,
Prometheus adds Authorization: Basic base64(user:password) to
the Accept-Encoding / User-Agent headers it always sends. If
the job has bearer_token or bearer_token_file, Prometheus
adds Authorization: Bearer <token> instead. The two blocks are
mutually exclusive at the scrape job level; setting both
produces a parse error at config reload.
The credentials are evaluated in this order:
- The
basic_authblock (if set). - The
authorizationblock (if set, for custom credentials). - The
bearer_token/bearer_token_file(if set). - The
oauth2block (if set, for token exchange).
For TLS-encrypted scrapes, the tls_config block controls
client certificate authentication and CA validation. Lesson 03
covers that path.
Server-side: the web configuration file
Prometheus’s own HTTP listener can require authentication when
the operator passes --web.config.file=/etc/prometheus/web.yml.
The web config file has its own schema:
# /etc/prometheus/web.yml
basic_auth_users:
alice: $2y$10$bcrypt-hash-of-password-here
tls_server_config:
cert_file: /etc/prometheus/server.crt
key_file: /etc/prometheus/server.key
The basic_auth_users map accepts usernames mapped to bcrypt
password hashes. The format is the same one htpasswd -B emits.
Plaintext passwords are not accepted at config-load time;
promtool check web-config validates the hash format.
When basic_auth_users is set, every request to the Prometheus
HTTP listener (UI, API, /-/reload, /metrics) requires an
Authorization: Basic header from a user whose bcrypt hash is
in the map. Without the header, Prometheus answers 401 Unauthorized. The /-/quit and /-/reload endpoints behave
the same way when authentication is required.
Federation and remote write
Federation (/federate) and remote write use the same
bearer_token / basic_auth blocks under their respective
configuration stanzas. A typical federation job:
# /etc/prometheus/prometheus.yml
scrape_configs:
- job_name: 'federate'
honor_labels: true
metrics_path: '/federate'
params:
'match[]':
- '{job="prometheus"}'
- '{__name__=~"job:.*"}'
static_configs:
- targets:
- 'prometheus-b.internal:9090'
bearer_token_file: /etc/prometheus/federation-token
The bearer_token_file points at a file Prometheus reads on
every scrape; the file must be readable by the Prometheus
process user only. A common mistake is to give the file world-
readable permissions and defeat the purpose.
How to configure it
Scrape job with basic auth
# /etc/prometheus/prometheus.yml
scrape_configs:
- job_name: 'application_exporter'
metrics_path: /metrics
static_configs:
- targets:
- 'app-svc-1.internal:9100'
- 'app-svc-2.internal:9100'
basic_auth:
username: monitoring
password_file: /etc/prometheus/secrets/app-exporter.pass
# tls_config covered in lesson 03
# tls_config:
# ca_file: /etc/prometheus/ca/internal-ca.crt
# server_name: metrics.internal
The password_file directive tells Prometheus to read the file
on every scrape. The file should be 0640, owned by the
Prometheus user, and stored on the same volume as the rest of
the secrets. Production secret managers (Vault, AWS Secrets
Manager, Kubernetes secrets) typically render the file at boot
via an init container or a sidecar.
Scrape job with bearer token
# /etc/prometheus/prometheus.yml
scrape_configs:
- job_name: 'saas_metrics'
metrics_path: /api/v1/metrics
scheme: https
static_configs:
- targets:
- 'metrics.partner.example.com'
bearer_token_file: /etc/prometheus/secrets/partner-token
The token file should contain only the token, with a trailing newline optional. A multi-line file or a JSON object produces an HTTP 401 with no useful diagnostics.
Server-side authentication (the Prometheus API)
# /etc/default/prometheus
ARGS="--config.file=/etc/prometheus/prometheus.yml \
--storage.tsdb.path=/var/lib/prometheus \
--web.listen-address=127.0.0.1:9090 \
--web.config.file=/etc/prometheus/web.yml \
--web.external-url=https://prometheus.example.com"
# /etc/prometheus/web.yml
basic_auth_users:
grafana: $2y$10$N9qo8uLOickgx2ZMRZoMyeIjZAgcfl7p92ldGxad68LJZdL17lhWy
oncall: $2y$10$kE3gZ3F0m3n0Yt8aP0QkC.4pRqW6bV5sG2m8aN0vT0n1kS5bU7wCe
Generate the bcrypt hash for a new user:
# READ-ONLY: bcrypt-hash a password for inclusion in web.yml.
htpasswd -nB oncall
# New password: ********
# Re-type new password: ********
# oncall:$2y$10$kE3gZ3F0m3n0Yt8aP0QkC.4pRqW6bV5sG2m8aN0vT0n1kS5bU7wCe
Copy only the hash (the part after the colon) into web.yml.
Then validate the web config before restarting:
# READ-ONLY: validate the web config schema.
promtool check web-config /etc/prometheus/web.yml
# web.yml: VALID
Remote write with basic auth
# /etc/prometheus/prometheus.yml
remote_write:
- url: 'https://prometheus-remote.internal/api/v1/write'
basic_auth:
username: remote-write
password_file: /etc/prometheus/secrets/remote-write.pass
write_relabel_configs:
- source_labels: [__name__]
regex: 'go_.*'
action: drop
Remote write with AWS SigV4 (the SigV4 alternative)
For managed Prometheus receivers that use AWS signature
version 4 (Amazon Managed Prometheus, Mimir on AWS with IAM
auth), Prometheus 2.55.x supports a separate sigv4 block
that does not require a bearer token:
remote_write:
- url: 'https://aps-workspaces.us-east-1.amazonaws.com/workspaces/ws-.../api/v1/remote_write'
sigv4:
region: us-east-1
access_key: ${AWS_ACCESS_KEY_ID} # not the right shape; use secret_file
secret_key: ${AWS_SECRET_ACCESS_KEY} # see note
The access_key / secret_key fields accept literal strings
only. For production, prefer AWS IAM roles for service accounts
or the file-based credentials_file if the receiver supports
it. The credential shape is fundamentally different from
basic_auth and bearer_token; it does not derive from
HTTPS client auth.
How to validate it
# READ-ONLY: confirm the scrape credentials are working.
curl -fsS -u monitoring:$PASS \
http://app-svc-1.internal:9100/metrics | head -5
# # HELP go_gc_duration_seconds A summary of the GC invocation durations.
# # TYPE go_gc_duration_seconds summary
# go_gc_duration_seconds{quantile="0"} 1.23e-05
# READ-ONLY: confirm Prometheus's own API requires authentication.
curl -sI http://127.0.0.1:9090/api/v1/targets
# HTTP/1.1 401 Unauthorized
# www-authenticate: Basic realm="Prometheus"
# READ-ONLY: confirm the same call works with credentials.
curl -fsS -u grafana:$PASS \
http://127.0.0.1:9090/api/v1/status/config | jq '.data | keys'
# [
# "yaml"
# ]
Three things should be true after this validation: the scrape target responds to the credentials Prometheus sends, the Prometheus API rejects unauthenticated calls, and the config endpoint is reachable only with credentials.
How it can fail
The five most expensive authentication failures in real Prometheus installs.
- Literal
password:inprometheus.yml. The config is in Git. The credential is in the diff. The symptom is agit grepfinding the password in a historical commit. The fix is to rotate the password and rewrite the config to usepassword_file. password_filepoints at a wrong path. Prometheus answers the scrape with a 401 and the UI showscontext deadline exceededfor the affected targets. The symptom in the targets page islast error: ... basic auth credentials missing or empty. The fix is to render the file at boot and validate withcat /etc/prometheus/secrets/... | wc -c.- Bearer token rotated by the partner, Prometheus still
using the old one. Every scrape returns 401. The targets
page shows all targets
up = 0. The fix is to update the secret file andcurl -X POST http://127.0.0.1:9090/-/reload(lesson 04 covers the lifecycle controls). - Server-side
basic_auth_usersmisconfigured. A user pasted the literal password instead of the bcrypt hash. Thepromtool check web-configcatches this. The symptom on a live install is401 Unauthorizedfor every credential the user thought was correct. bearer_token_fileworld-readable. The token leaks to every user on the host. The symptom is that the token appears in/etcdumps taken by a backup agent. The fix ischmod 0640 /etc/prometheus/secrets/*andchown prometheus:prometheus.
How to troubleshoot it
Diagnostic order: which credential shape, which direction (client or server), what does the error say.
- Inspect the targets page.
GET /api/v1/targetsshowslastErrorper target. Common strings:server returned HTTP status 401 Unauthorized,context deadline exceeded,basic auth credentials missing or empty. - Reproduce the scrape manually.
Thecurl -v -u monitoring:$PASS http://app-svc:9100/metrics-voutput shows the TLS handshake and theAuthorizationheader. A successful response means Prometheus should also succeed; a 401 means the credentials or the target’s user database is wrong. - Inspect the file permissions.
Prometheus needs read access; everyone else should not.ls -l /etc/prometheus/secrets/ # -rw-r----- 1 prometheus prometheus 42 Aug 14 10:00 app-exporter.pass - Validate the server-side config.
A failed check points at the line and column of the problem.promtool check web-config /etc/prometheus/web.yml # web.yml: VALID - Reload carefully. A
SIGHUPreloads the scrape config but not the--web.config.file. Changes toweb.ymlrequire a Prometheus restart. (CONFIGURATION.)
Security implications
The most important security property of authentication in Prometheus 2.55.x is that file-based credentials are read on every scrape. This means rotating a credential is a matter of replacing the file’s contents and reloading Prometheus; the scrape will pick up the new value on the next interval. The downside is that the file is read continuously and must be secured continuously: a permissive file mode is a permanent leak.
Two specific attack classes:
- Config endpoint disclosure. A Prometheus with no
server-side auth and a permissive bind address lets any
caller read every scrape credential via
/api/v1/status/config. Lesson 01 covers the bind; this lesson covers the auth requirement. Both must hold. - Credential reuse. A token used for federation that has
read access to
/federateon a higher-privilege Prometheus should be treated as a high-privilege credential. Rotate it on the same schedule as any other high-privilege credential.
Verification
You should now be able to answer:
- What is the difference between
basic_authandbearer_tokenin a Prometheus scrape config, and which one does the federation endpoint use? - Why is
password_filepreferable to a literalpassword:inprometheus.yml? - What does the
--web.config.fileflag do, and where does Prometheus look for it? - Which endpoint exposes the resolved scrape configuration
(including the contents of
password_file) to any authenticated caller?
Quiz
Knowledge check · 8 questions
Q1. Which Prometheus 2.55.x scrape config directive reads the credential from a file on every scrape?
Q2. Setting both basic_auth and bearer_token on the same scrape job is allowed and Prometheus sends both headers to the target.
Q3. What does the --web.config.file flag control?
Q4. Which of these are valid ways to keep a scrape credential out of a Git-tracked prometheus.yml?
Q5. A scrape target returns 401 to every Prometheus request. The targets page shows last error: server returned HTTP status 401 Unauthorized. What is the first thing to check?
Q6. The /api/v1/status/config endpoint returns the resolved scrape configuration including the contents of any password_file.
Q7. Name the Prometheus command-line flag that points at the file holding basic_auth_users for the Prometheus HTTP API.
Q8. Which of these are reasonable production practices for scrape credentials in Prometheus?
Passing score: 75%. Answers are checked in this browser.