Key TakeawaysLegacy search clients like elasticsearch==7.10.1 hard-cap underlying transport libraries (urllib3=1.26.19), requiring an explicit application-level pin of urllib3>=2.5.0.When third-party packages like django-elasticsearch-dsl-drf are abandoned, vendoring a lean (~190 LOC) project-local filter and pagination layer prevents breaking client-facing REST API response contracts.Verifying a search client migration requires separating metadata ping traffic (size=0) from document retrieval queries and verifying vulnerability remediation with automated security auditing tools.When a security advisory hits a core library like urllib3, upgrading is usually a routine dependency bump. However, when legacy search clients hard-cap underlying transport libraries, security remediations become full-scale client migrations.This is the story of that digging: how a security patch that looked like a one-line version bump turned into a full client-library migration, what part of that migration was genuinely mechanical, and what part wasn't. If you're running an older elasticsearch Python client against an AWS OpenSearch domain, there's a decent chance you're sitting on the same trap.The Anatomy of a Transitive CVE TrapIn modern Python backend architectures, transitive dependencies can quietly turn into security bottlenecks. Our production Django application relied on elasticsearch==7.10.1 to interact with event and audit-log indices hosted on AWS OpenSearch 1.3.A transitive dependency is a package your code never imports directly, it gets pulled in because something you depend on needs it. That distance is exactly why a cap like this goes unnoticed for years: nobody on the team ever wrote import urllib3, so nobody was watching its version.When CVE-2025-50181 (an open-redirect / SSRF vulnerability in urllib3, affecting everything before 2.5.0, details here) was disclosed, security compliance required updating urllib3 to version 2.5.0 or higher. However, running a dependency audit revealed a hard blocker:# pyproject.toml (legacy state)elasticsearch = "7.10.1" # Pinned: requires urllib3 >= 1.21.1, < 2The elasticsearch 7.10.1 package explicitly capped urllib3 < 2. Because 1.26.20 was the final release on the urllib3 1.26.x line, no backport was available. Upgrading elasticsearch-py to 7.14+ or 8.x was not an option due to the introduction of client-side product checks (UnsupportedProductError), which intentionally refuse connection to AWS OpenSearch or Elasticsearch OSS endpoints.The client library itself was forcing the application to remain vulnerable. The only remediation path was swapping the underlying search client entirely.Client Selection: Why opensearch-py, Not a Newer Elasticsearch ClientOpenSearch is AWS's fork of Elasticsearch: a separately maintained, Apache-licensed search engine with (mostly) the same API, created specifically so companies could keep running an open-source-licensed alternative after Elastic changed its own license.The elasticsearch==7.10.1 pin predates a fight that had nothing to do with us. Elastic changed its licensing in January 2021 after AWS forked Elasticsearch into what became OpenSearch, and that July elasticsearch-py 7.14.0 quietly added a check that raises UnsupportedProductError on any server missing Elastic's own X-Elastic-Product header (issue #1639). AWS OpenSearch doesn't send it. Elastic called the change an enhancement; the backlash was loud enough that Elastic locked the GitHub thread. Practically, it meant any elasticsearch-py release from 7.14 onward refuses to talk to our production database, so upgrading the vulnerable client in place was never on the table.opensearch-py sidesteps that fight by construction: it's OpenSearch's own client, forked from elasticsearch-py at the 7.10.x line before the check existed. It never inherited it, and from a recent enough release, it drops the urllib3 ceiling too. COMPATIBILITY.md confirms it supports OpenSearch 1.0 through 2.x, our production range, and since OpenSearch's wire protocol is still the Elasticsearch 7.x REST API, the same client talks cleanly to both our production domain and the legacy elasticsearch-oss:7.10.2 container we ran in CI.Picking a Version Was Its Own Research ProjectHere's the part that surprised me: even after deciding on opensearch-py, picking which version wasn't obvious, and getting it wrong would have shipped a "fix" that didn't actually fix anything.Not every opensearch-py release frees urllib3. Early 1.x and 2.x releases inherited the exact same =1.21.1,=1.21.1,=1.26.18 (cap dropped, undeclared)Unofficially, briefly2.5.0>=1.26.18,=1.26.19 is a floor: it says "at least this version," not "exactly this version." Package maintainers almost always publish floors rather than pins, because a pin would make their library incompatible with anything else in your project that needs a different exact version. That's exactly why a floor alone can't guarantee what actually gets installed.The gotcha I'd flag loudest: that urllib3 constraint is a floor, not a pin. 1.26.19 satisfies >=1.26.19 just as well as 2.7.0 does, so installing opensearch-py 2.8 doesn't guarantee your resolver picks a patched urllib3, it just stops forbidding it. Skip a separate urllib3>=2.5.0 pin in your own dependency file, and you can finish this entire migration still shipping the vulnerable version, because the old version still satisfies every constraint in the graph. We pinned it explicitly, with a comment citing the CVE, so the reason survives whoever edits that line next.# pyproject.toml[tool.poetry.dependencies]python = "^3.12"opensearch-py = "^2.8"# Explicitly floor urllib3 >= 2.5.0 for CVE-2025-50181 remediation.# opensearch-py only floors at >=1.26.19, so explicit flooring forces the resolver to upgrade.urllib3 = ">=2.5.0,