Skip to content

Install bcrypt backend for passlib - #7

Open
erkiaas wants to merge 1 commit into
codemowers:mainfrom
erkiaas:fix/bcrypt-backend
Open

Install bcrypt backend for passlib#7
erkiaas wants to merge 1 commit into
codemowers:mainfrom
erkiaas:fix/bcrypt-backend

Conversation

@erkiaas

Copy link
Copy Markdown
Contributor

Symptom

codemowers/secret-claim-operator:latest (pushed 2026-08-22 21:22 UTC, sha256:3ad6ab8c…) crash-loops on startup as soon as it reconciles its first claim:

Reconciling SecretClaim argocd/argocd-redis
Unhandled exception raised during claim reconcile: bcrypt: no backends available -- recommend you install one (e.g. 'pip install bcrypt')
File "/app/secret_claim_operator.py", line 86, in <module>
SecretClaimOperator.run()
...
File "/app/operatorlib.py", line 1274, in run_claim_reconciler_loop
await cls.reconcile_claim(api_client, co, body)

Because reconcile_claim() hashes unconditionally (secret_claim_operator.py:53), the exception escapes the reconciler loop and the process exits — so no SecretClaim gets reconciled, not just ones using %(bcrypt)s. We hit this in a live cluster: the operator had been running for months and only broke when the pod happened to restart and imagePullPolicy: Always pulled the new build.

Cause

passlib doesn't implement bcrypt, it wraps a backend. With no bcrypt package installed, passlib's only remaining option is the stdlib crypt module — and PEP 594 removed crypt in Python 3.13. FROM python:alpine is now 3.14, so the fallback that used to paper over the missing dependency is gone:

$ python3 -V
Python 3.14.7
$ python3 -c "import crypt"
ModuleNotFoundError: No module named 'crypt'
$ pip install passlib && python3 -c "from passlib.hash import bcrypt; bcrypt.hash('x')"
passlib.exc.MissingBackendError: bcrypt: no backends available

Seven sample Dockerfiles install passlib and none install a backend, so they're all affected as soon as they're rebuilt; operatorlib.py:20 builds a CryptContext(schemes=["bcrypt"]) too.

Why bcrypt is pinned <5

Adding plain bcrypt is not enough. passlib 1.7.4's backend probe hashes a >72-byte test string to detect an old wraparound bug, and bcrypt 5 turned over-long passwords into a hard error, so the probe itself raises:

File ".../passlib/handlers/bcrypt.py", line 380, in detect_wrap_bug
if verify(secret, bug_hash):
ValueError: password cannot be longer than 72 bytes, truncate manually if necessary

With bcrypt<5 (4.3.0) hashing works again on 3.14.

Verification

Built samples/secret-claim-operator/Dockerfile from this branch and ran the failing code path in the resulting image:

python 3.14.0 | bcrypt 4.3.0
ctx["bcrypt"] = $2b$12$... len 60
verify: True

Output format and cost are unchanged ($2b$12$, 60 chars), so existing hashes stay valid. bcrypt publishes abi3 musllinux wheels for x86_64 and aarch64, so the alpine build needs no Rust toolchain.

Notes / possible follow-ups (not in this PR)

  • passlib 1.7.4 (2020) is unmaintained, and with bcrypt ≥ 4.1 it prints a harmless but alarming AttributeError: module 'bcrypt' has no attribute '__about__' traceback from its version probe. libpass is a maintained drop-in fork — libpass 1.9.3 + bcrypt 5.0.0 works unmodified (import passlib and CryptContext both fine, verified) and would let the <5 pin be dropped. Happy to switch this PR over to libpass instead if you'd prefer that direction.
  • secret_claim_operator.py:53 (ctx["bcrypt"] = bcrypt.hash(...) # TODO: Use lazy getter) computes a bcrypt hash for every claim even when the mapping never references %(bcrypt)s, which is what made a missing backend fatal cluster-wide rather than only for claims that use it. Making that lazy would limit the blast radius of any future backend problem, but it is not a substitute for this fix.

passlib does not ship a bcrypt implementation, it only wraps one. Until
now these images got away with it because passlib fell back to the stdlib
crypt module, but PEP 594 removed crypt in Python 3.13, so any image
rebuilt on current python:alpine (3.14) raises
MissingBackendError: bcrypt: no backends available
on the first hash it computes. For secret-claim-operator that is fatal:
reconcile_claim() hashes on every claim, so the operator crash-loops and
stops reconciling SecretClaims entirely.
bcrypt is pinned below 5.0 on purpose. passlib 1.7.4's backend probe
hashes a >72 byte test string to detect a historical wraparound bug, and
bcrypt 5 turned over-long passwords into a hard ValueError, so
passlib 1.7.4 + bcrypt 5 fails just as reliably as no backend at all.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Sign up for freeto 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

@erkiaas