Skip to content

BUG/MEDIUM: prometheus: reject passwords that are not SHA-256 crypt hashes - #841

Open
PixiBixi wants to merge 2 commits into
haproxytech:masterfrom
PixiBixi:bug/prometheus-crypt-hash-validation
Open

PixiBixi wants to merge 2 commits into
haproxytech:masterfrom
PixiBixi:bug/prometheus-crypt-hash-validation

Conversation

@PixiBixi

Copy link
Copy Markdown
Contributor

What

The prometheus endpoint panics when the secret named by
prometheus-endpoint-auth-secret holds a password that is not a SHA-256 crypt hash.

pkg/handler/prometheus.go split the stored value on $ and read fields 1 and 2 to
rebuild the crypt salt, without checking that these fields existed:

partsPass := strings.Split(string(password), "$")
salt := fmt.Sprintf("$%s$%s$", partsPass[1], partsPass[2])

A plaintext password, which is what an operator writes when they skip the mkpasswd
step of documentation/prometheus.md, gives a slice of one element. Reading field 1
panics. The handler runs in the sync goroutine, so the panic takes down the controller,
and it comes back at every sync as long as the secret stays in place. Any user allowed
to write a secret in a watched namespace can stop the controller.

Reproduce

apiVersion: v1
kind: Secret
metadata:
  name: prometheus-credentials
  namespace: haproxy-controller
type: Opaque
stringData:
  admin: hunter2
prometheus-endpoint-auth-secret: haproxy-controller/prometheus-credentials

With --prometheus on:

panic: runtime error: index out of range [1] with length 1
  pkg/handler/prometheus.go:108
  pkg/controller/controller.go:158

Fix

The salt is read through cryptSalt, which refuses anything that does not carry the
SHA-256 identifier. A rejected user is skipped with an error in the logs and the endpoint
keeps answering 401 for them, which is the outcome they already had: a value that is not
a hash never matches what crypt computes. The other users of the secret keep their access.

Requiring the identifier, and not only a leading $, also separates a hash from a
plaintext password that contains $. $ecret$pass$word is shaped like a hash; kept, it
would lock the user out with nothing in the logs to say why.

Second bug, same expression

cryptSalt cuts the hash at its last $ instead of at a fixed field index. A hash
carrying a rounds= parameter, which mkpasswd writes with -R, used to give
$5$rounds=N$ as the salt, losing the salt itself. That user could never authenticate,
in silence.

Going through crypt.Verify(hash, password) instead was tried and does not work here:
common.Salt.Decode does bytes.SplitN(raw, '$', 4), so on $5$rounds=N$salt$digest
the fourth field absorbs salt$digest as a whole and verification always fails. The
salt has to be extracted before the call.

Affected versions

The faulty expression was introduced by 279d16d and is present from v3.1.11 and
v3.2.0 onwards. v3.1.10 and earlier build a HAProxy userlist instead and are not
affected.

Tests

pkg/handler/prometheus_test.go, 20 subcases. The one that matters replays the
comparison prometheusHandler performs on every request: it hashes the password with
the stored salt and expects the stored hash, with and without rounds=. A salt cut at
the wrong $ passes an equality assertion on its own but fails this one.

Checked that the tests fail against the previous implementation: the first one panics
again, and the rounds= case fails.

go test ./pkg/handler/ -race     PASS
go test ./... (without e2e)      PASS
go vet ./pkg/...                 clean
make check-commit                clean

@PixiBixi
PixiBixi force-pushed the bug/prometheus-crypt-hash-validation branch from bc36a67 to acef066 Compare August 26, 2026 22:28
…ashes

The prometheus endpoint reads its basic auth users from the secret named by
the prometheus-endpoint-auth-secret annotation. For each user it split the
stored value on '$' and read fields 1 and 2 to rebuild the crypt salt.

Nothing checked that these fields existed. A value that is not a crypt hash,
which is what an operator writes when they skip the mkpasswd step described
in documentation/prometheus.md, produced a slice of one element. Reading
field 1 panicked. The handler runs in the sync goroutine, so the panic took
down the controller, and it came back at every sync as long as the secret
stayed in place. Any user allowed to write a secret in a watched namespace
could stop the controller.

This change reads the salt through cryptSalt, which refuses anything that
does not carry the SHA-256 identifier. A rejected user is skipped with an
error in the logs and the endpoint keeps answering 401 for them, which is
the outcome they already had: a value that is not a hash never matches what
crypt computes. The other users of the secret keep their access.

Requiring the identifier, and not only a leading '$', also separates a hash
from a plaintext password that contains '$'. A value like "$ecret$pass$word"
is shaped like a hash. Kept, it would lock the user out with nothing in the
logs to say why.

cryptSalt also cuts the hash at its last '$' instead of at a fixed field
index. A hash carrying a rounds= parameter, which mkpasswd writes with -R,
used to yield "$5$rounds=N$" as the salt, losing the salt itself. That user
could never authenticate, in silence. The salt now keeps the parameter,
where crypt needs it.

Backport this fix to 3.2 and 3.1.
These tests drive PrometheusEndpoint.Update with a store holding a main
configmap and the secret it names.

The first test gives a plaintext password. Without the fix it panics with
an index out of range, which is what the controller did in production. The
second test walks the shapes a purely syntactic check lets through, among
them a plaintext password carrying '$' and a hash from another algorithm.

The third test reads back the registered user and replays the comparison
that prometheusHandler performs on every request: it hashes the password
with the stored salt and expects the stored hash. It runs once with a plain
salt and once with a rounds= salt. A salt cut at the wrong '$' passes an
equality assertion on its own but fails this one.

The last test puts a good user and a bad one in the same secret, and checks
that the good one keeps its access.
@oktalz
oktalz force-pushed the bug/prometheus-crypt-hash-validation branch from acef066 to d68605d Compare September 3, 2026 12:01
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant