Read against current releases on 13 August 2026, none of the four major self-hosted vector databases requires authentication out of the box: Qdrant 1.19.0 ships api_key and jwt_rbac commented out, Weaviate 1.39.0 still defines its fallback authentication scheme as anonymous access enabled, Milvus 3.0.0 ships authorizationEnabled false with a default root password of Milvus, and Chroma 1.5.9 has no credential at all. That is the least interesting finding, because a competent team closes it in the first hour. What breaks in production is the layer above the default: Qdrant treats an empty api_key string as unset and only logs a warning, jwt_rbac with no api_key disables role-based access control entirely, the Milvus standalone compose publishes its object store on 9000 and 9001 with the default credential pair still in place, and Docker's published ports are diverted in the nat table before the ufw INPUT chain ever sees them. Four CVEs set the version floor: Qdrant 1.16.0, Milvus 2.5.27 or 2.6.10 and 2.4.24 or 2.5.21 or 2.6.5, and Chroma, which has no fix listed for a pre-authentication code injection scored 10.0 and has shipped no release since 5 May 2026. Treat the store as a second copy of the source corpus rather than an index: published inversion research recovers 92 percent of 32-token inputs exactly from their embeddings. Put it on an internal segment with no published host ports and verify every control with a request sent from outside the host.
Every vector database security guide ends on the same three bullets: enable authentication, use TLS, restrict the network. Any competent team does all three in the first hour. The instance still shows up on a scan.
Here is one reason, stated plainly. CVE-2026-45829 is a pre-authentication code injection in the Chroma Python server: a request to the collections endpoint carrying a model repository reference and trust_remote_code executes attacker code before any credential is checked. It covers every release from 1.0.0 through 1.5.9, the national vulnerability database scores it 10.0, and there is still no patched version listed. Turning on a token does not close it, because the injection lands first.
So this post skips the advice you already have. Every default below was read against a specific release on 13 August 2026: Qdrant 1.19.0, Weaviate 1.39.0, Milvus 3.0.0 and Chroma 1.5.9. Those are the facts most likely to age, so they are dated. What follows is the review a regulated buyer should run before a self-hosted vector store holds a single sensitive embedding: what a fresh install accepts, which settings silently do nothing, what the quickstart publishes beyond the query port, where backups land, and the CVE floor. This is the vector store equivalent of the audit we ran on the serving layer in vLLM security hardening for CVE-2026-22778 and on the tool layer in MCP server security hardening.
What a default install of each engine accepts today
Weaviate has not changed this. In the v1.39.0 source, the fallback authentication scheme is literally a struct with anonymous access set to true, and the startup validator counts anonymous access as a valid scheme, so nothing complains. Start the container with no AUTHENTICATION_ variables and it accepts unauthenticated reads and writes. Role-based access control stays off until you set AUTHORIZATION_RBAC_ENABLED yourself.
Qdrant's shipped config/config.yaml in 1.19.0 binds 0.0.0.0 on port 6333, leaves both api_key and read_only_api_key commented out, leaves jwt_rbac commented out and sets enable_tls: false. Milvus 3.0.0 still ships authorizationEnabled: false with a default root password of Milvus, tlsMode: 0 and internal TLS off. One line there deserves its own attention: enablePublicPrivilege is true, so even after you flip authorization on, the built-in public role starts with privileges attached. Turning authentication on is step one of two.
That table is the finding everyone already expects, which is exactly why it is not the interesting part. Move on to the four things that decide whether the store is actually safe.
| Engine (version read 2026-08-13) | Authentication required | Authorization on | TLS on | The default that actually bites |
|---|---|---|---|---|
| Qdrant 1.19.0 | No. api_key and read_only_api_key ship commented out | No. jwt_rbac commented out | No. enable_tls: false | Binds host: 0.0.0.0 on 6333; peer port 6335 skips key checks unless enforce_internal_auth is on |
| Weaviate 1.39.0 | No. Fallback scheme is anonymous access enabled | No. AUTHORIZATION_RBAC_ENABLED false | No | The startup validator counts anonymous access as a configured scheme, so the server boots clean |
| Milvus 3.0.0 | No. authorizationEnabled: false, root password Milvus | Public role pre-privileged, enablePublicPrivilege: true | No. tlsMode: 0, internaltlsEnabled: false | Standalone compose publishes 9091 and the object store on 9000 and 9001 |
| Chroma 1.5.9 | No | None per collection | No | Server config listens on 0.0.0.0:8000 while the CLI defaults to localhost |
The settings you configured that silently do nothing
This is the failure mode that survives a code review, because the config file reads correctly.
Qdrant treats an empty API key as unset. In 1.19.0's settings validation, an empty key string is treated as no key at all. So QDRANT__SERVICE__API_KEY pointing at an unset shell variable, or a Kubernetes secret that resolved to an empty value, leaves the instance open while your compose file and your Helm values both say otherwise. The engine logs a warning for api_key, alt_api_key and read_only_api_key when this happens, which means the real check is a grep of the startup log, not a reading of your configuration.
**Qdrant disables RBAC entirely when jwt_rbac is on but no api_key is set.** The API key doubles as the HMAC-SHA256 signing secret for JWTs. Set jwt_rbac: true without a key and the server logs that JWT RBAC is configured but no API key is set, therefore JWT RBAC is not enabled, and then serves traffic anyway. The code also recommends a minimum of 256 bits (32 bytes), because a short api_key is a short HMAC secret.
Milvus lets you build an RBAC model that nothing consults. common.security.authorizationEnabled is the switch that decides whether users and roles are checked at all. Leave it false and you have credentials, a role graph, an audit story for the questionnaire, and no enforcement.
The correction is the same in all three cases: never verify a control by reading the config that sets it. Verify it with a request.
# Run this from a different host, not the Docker host.
# 401 or 403 means the credential is enforced. 200 means it is not.
curl -s -o /dev/null -w 'qdrant %{http_code}\n' http://vecdb.internal:6333/collections
curl -s -o /dev/null -w 'weaviate %{http_code}\n' http://vecdb.internal:8080/v1/schema
curl -s -o /dev/null -w 'chroma %{http_code}\n' http://vecdb.internal:8000/api/v2/heartbeat
# And grep the startup log for the silent-disable warnings.
docker logs qdrant 2>&1 | grep -iE 'api key|jwt rbac'Network exposure is the finding, not the software default
Most self-hosted vector store exposure does not come from the engine. It comes from the deployment, and Docker documents the mechanism plainly: when you publish a container's ports, traffic to and from that container is routed in the nat table and gets diverted before it reaches the INPUT and OUTPUT chains that ufw uses. The host firewall you configured is not in the path. A team that added a deny rule on 6333 and moved on has a Qdrant instance on the public internet and a firewall config that says the opposite.
Three specific traps follow from that.
Milvus publishes four ports and people audit two. The standalone compose exposes 19530 and 9091 for the engine, plus the object store on 9000 and 9001 with MINIO_ACCESS_KEY: minioadmin and the engine defaults minio.accessKeyID: minioadmin, minio.secretAccessKey: minioadmin, minio.useSSL: false. The segments and binlogs, meaning your actual vectors, live in that bucket. An attacker who never touches 19530 can copy the corpus out of the object store.
Qdrant's cluster port is a separate trust boundary. The internal peer-to-peer gRPC API on 6335 does not verify the API key unless enforce_internal_auth is enabled, which ships commented out. A cluster with a key on 6333 can still be joined or queried on 6335.
Chroma binds two different ways depending on how you started it. The CLI run command defaults its host to localhost. The frontend server config default, which is what the container image uses, is 0.0.0.0 on port 8000. The developer who tested locally and the operator who deployed the image are both correctly reporting what they saw, which is exactly why the check has to be a port scan from outside the host rather than a reading of the docs.
# The only exposure check that counts, run from another machine. nmap -Pn -p 6333,6335,8080,8000,9000,9001,9091,19530,50051 vecdb.internal
The clean answer for regulated deployments is to stop publishing host ports entirely: put the store on an internal container network or a private subnet, reach it only from the retrieval service, and terminate TLS and authentication at a proxy you control. That is a large part of why the cost and control comparison of cloud versus on-premise AI usually lands on the on-prem side once the data is regulated.
Multi-tenancy: what each API can and cannot enforce
Only one of these engines lets the credential itself carry the tenant predicate. Qdrant's JWT tokens can embed a payload filter that the engine applies to every request made with that token, so a leaked token is scoped rather than total. Weaviate has role-based access control but it is off until you enable it. Milvus starts the public role with privileges attached. Chroma has no per-collection authorization at all.
Everywhere except a properly configured Qdrant, the tenant filter is a query parameter your application chooses to send, and one code path that forgets it is a cross-tenant read that no database setting will catch. So the tenant predicate belongs above the query API: a retrieval service that derives the tenant from the verified session and refuses to build a query without it, plus a CI test that authenticates as tenant A and asserts zero results from tenant B.
The topology question underneath this (collection per tenant, shared collection with a filter, or a hybrid) has real recall consequences and we worked through it in multi-tenant RAG isolation. This section is only about who enforces it.
Snapshots and backups are a second copy with a different access model
A snapshot is the whole corpus, exportable, usually governed by controls nobody reviewed.
enable_snapshot_url_recovery defaults to true. The shipped config flags that in its own comments as a server-side request forgery path, because the node will fetch a URL you hand it and reach whatever it can reach.PERSISTENCE_HNSW_DISABLE_SNAPSHOTS false, a 21600 second interval, and snapshot on startup). That is a second on-disk copy of the index needing the same filesystem controls as the data directory.Two consequences follow. Snapshot artifacts inherit the classification of the source corpus, so they need the same encryption at rest, retention clock and access logging. And an erasure request that only deletes from the live index leaves those vectors in every snapshot taken before it, which is a separate problem from what DELETE physically does inside an HNSW graph, covered in vector database GDPR erasure and HNSW soft deletes.
The CVEs that set your version floor
CVE-2026-26190 is the cleanest illustration of the network point. Before 2.5.27 and 2.6.10 the entire /api/v1/ REST surface was registered on the Milvus metrics and management port 9091 with no authentication at all, and the debug expression endpoint used a token derived from the etcd root path, which defaults to the string by-dev. Port 9091 is still published by the standalone compose file, so the fix protects you only if you are actually on a fixed line.
CVE-2025-64513 should end any argument about internal trust. An unauthenticated caller forged one header, sourceID, and the Proxy treated the request as an internal component: full administrative access to every collection.
CVE-2026-25628 is why a read-only key is not a containment boundary. From 1.9.3 until the 1.16.0 fix, a caller holding only that key could point the /logger endpoint at an arbitrary path on disk and append to it. Its privileges-required rating is low rather than none, which is exactly the level most teams hand to their retrieval service.
And the Chroma dates are the story. Version 1.5.9 landed on 5 May 2026, the advisory on 18 May 2026, and there has been no release since. An unpatched critical plus three months of silence is a supportability question, not a patching backlog item, and it belongs in the vendor risk file.
One hedged note while you are auditing Chroma: on some 1.0.x builds the native auth environment variables were accepted without taking effect, so verify enforcement with an unauthenticated request rather than trusting that the variables were read.
| CVE | Engine | Affected | Fixed in | Severity | If you cannot upgrade yet |
|---|---|---|---|---|---|
| CVE-2026-45829 | Chroma (Python server) | 1.0.0 through 1.5.9 | None listed as of 2026-08-13 | 10.0 critical per the national vulnerability database (9.3 in the project advisory, with an exploit-maturity modifier) | Authentication does not help. Block collection creation at the gateway, or keep it off sensitive data |
| CVE-2026-26190 | Milvus | Before 2.5.27 and 2.6.10 | 2.5.27, 2.6.10 | 9.8 critical, no privileges required | Never publish 9091 to any reachable interface |
| CVE-2025-64513 | Milvus Proxy | Before 2.4.24, 2.5.21, 2.6.5 | 2.4.24, 2.5.21, 2.6.5 | 9.3 critical (CVSS v4.0), no privileges required | Strip the sourceID header from all incoming requests at the gateway |
| CVE-2026-25628 | Qdrant | 1.9.3 up to 1.16.0 | 1.16.0 | 8.5 high in the project advisory, 8.8 in the national vulnerability database (both CVSS v3.1), privileges required low | Treat the read-only key as a full credential and restrict /logger |
The embeddings are the regulated artifact, not just an index
The argument that turns everything above from an engineering preference into a control requirement is this: a vector store is a second copy of your source documents, not a derived index.
Published inversion work recovers 92 percent of 32-token text inputs exactly from their embeddings using a multi-step iterative method, and the same paper demonstrates extracting full patient names from a clinical notes dataset (Text Embeddings Reveal (Almost) As Much As Text). If your corpus is patient notes, deal documents, case files or HR records, the embedding collection carries the handling obligations of the source under GDPR and HIPAA: lawful basis, retention, erasure, access logging, breach notification.
That reframing changes three decisions at once. The store belongs inside the same data boundary as the source system, which for most regulated buyers means on-premise or a private subnet with no published host ports. Snapshots are regulated artifacts, not operational scratch. And an unauthenticated read of a collection is a disclosure of the underlying records, not a leak of "just vectors". If you are still choosing an engine, the Pinecone, Weaviate and Qdrant comparison covers the cost and performance side.
The pre-deployment checklist a regulated buyer should demand
Twelve checks. Each one is verified by an observation, not by a configuration file.
docker inspect the container and confirm the port bindings are absent or bound to a private interface, not 0.0.0.0.jwt_rbac on with a key of at least 32 bytes. Weaviate: AUTHORIZATION_RBAC_ENABLED true and anonymous access explicitly false. Milvus: authorizationEnabled true and enablePublicPrivilege false, with the root password rotated off Milvus.minio.accessKeyID and minio.secretAccessKey off minioadmin, set minio.useSSL: true, and confirm 9000 and 9001 are not published.enable_snapshot_url_recovery unless a named operational process needs it.enable_tls, tlsMode) and peer or internal TLS (cluster.p2p.enable_tls, internaltlsEnabled), or a proxy that terminates both.The pattern across all twelve is that the software default is the least interesting risk, because it takes an hour to fix. The deployment boundary, the silently-disabled credential, the second copy in the object store and the version floor are the ones that survive a security review and show up in an incident. For the broader control set, start at our AI security pillar.
FAQ
Quick answers to the questions this post tends to raise.



