Qdrant 1.20 Enforces Internal Auth and Adds BM25 Scoring
Qdrant 1.20, released on October 22, switches on a security setting you may not know you had. If your cluster has an API key, its nodes now check that key on each other's internal traffic. Most clusters will upgrade without noticing, but a few specific setups will lose peer-to-peer communication, so read this before you roll anything.
What Qdrant 1.20 changes
The v1.20.0 release notes list four security items:
service.enforce_internal_authnow defaults totruewhen an API key is configured.- Qdrant warns at startup when distributed mode has an API key but
enforce_internal_authis off. - Vector names that escape the segment directory, including
.and.., are rejected. rustls-webpkiandwebpki-root-certsare updated for RUSTSEC-2026-0382 and RUSTSEC-2026-0383. The notes say nothing more about the advisories.
The feature list adds BM25 scoring on the full-text payload index, turbo8 and turbo16 vector datatypes, a code tokenizer for full-text indexes, and configurable audit log output.
Qdrant 1.20 internal auth: which clusters break
In distributed mode, peers talk over an internal gRPC API on port 6335. Each peer attaches its admin api_key to outgoing internal requests, and as of 1.20 the receiving peer checks it. Single-node deployments have no internal API, and with no API key configured the setting does nothing.
The release notes contain no migration steps. The Qdrant security docs do, and they name three setups that can break:
- Only
read_only_api_keyset. The internal API accepts only read-write keys, so peers have nothing to authenticate with and stop talking. Qdrant logs a warning at startup but keeps running. - Only
alt_api_keyset, upgrading from v1.19.0 or v1.19.1. Those versions don't forwardalt_api_keyon internal requests. Upgrade to v1.19.2 first, then to v1.20.0. - Other tools calling port 6335. Anything that hits the internal port must now send the
api-keyheader.
The docs say rolling upgrades from v1.19.2 are safe, because peers have attached their key since v1.18.0.
Check your cluster config before upgrading
Every peer must carry the same key. This is the shape you want on each node:
service:
api_key: <same-admin-key-on-every-peer>
# alt_api_key: <new-key> # only while rotating keys, set on every peer
enforce_internal_auth: true # the 1.20 default, written out for clarity
The escape hatch is enforce_internal_auth: false or the environment variable QDRANT__SERVICE__ENFORCE_INTERNAL_AUTH=false. Treat it as a bridge while you fix keys, not as the end state.
The docs also say enforcement does not replace network isolation. Keep port 6335 unreachable from outside the cluster and enable peer TLS, so the key isn't sent in plain text between nodes:
cluster:
p2p:
enable_tls: true
tls:
cert: ./tls/cert.pem
key: ./tls/key.pem
ca_cert: ./tls/cacert.pem # required when cluster.p2p.enable_tls is true
After the upgrade, read each node's startup log for the new warning.
Try BM25 scoring on the full-text index
Per the full-text search docs, you create a text index with scoring set to true, then query with text:
curl -X PUT "http://localhost:6333/collections/books/index?wait=true" \
-H "api-key: <your-api-key>" \
-H "Content-Type: application/json" \
-d '{"field_name": "title", "field_schema": {"type": "text", "tokenizer": "word", "scoring": true}}'
curl -X POST "http://localhost:6333/collections/books/points/query" \
-H "api-key: <your-api-key>" \
-H "Content-Type: application/json" \
-d '{"query": {"text": {"query": "time travel", "scoring": {"bm25": {"k": 1.5, "b": 0.5}}}}, "using": "title", "limit": 10, "with_payload": true}'
The first call builds the index. The second ranks by BM25 with per-query tuning: k (term-frequency saturation, default 1.2) and b (length normalization, default 0.75). Note the parameter is k, not k1. Drop the scoring block to use the defaults.
By default a full-text index uses the word tokenizer and lowercasing, with no stemming or stopword removal. The docs recommend adding both for better BM25 quality. Re-run your relevance checks before you swap this in for an existing keyword path.
Audit log output and the other additions
Audit logging is off by default. Version 1.20 adds audit.output, so security teams can ship events straight to their log pipeline:
audit:
enabled: true
output: stdout # file (default), stdout, stderr, file_and_stdout, file_and_stderr
With stdout or stderr, Qdrant writes nothing to the audit directory, and rotation and max_log_files no longer apply. Make sure your collector keeps those lines. Each written line carries "kind": "audit", which makes them easy to filter.
The turbo8 and turbo16 datatypes (TurboQuant, described in the vectors docs) and the code tokenizer are worth a benchmark on your own data. I haven't tested either, and the release notes give no numbers.
What to do on Monday morning
- Check every node's config for
api_key,read_only_api_keyandalt_api_key. - If a node has only
alt_api_keyand runs 1.19.0 or 1.19.1, go to 1.19.2 first. - List every non-peer client of port 6335 and give it the
api-keyheader. - Confirm port 6335 is closed to the outside and peer TLS is on.
- Upgrade staging, then read the startup logs before touching production.
Secure-by-default only helps if your cluster config matches the default, so check it before 1.20 rolls through production.