Blog

Production Hardening: Access Control, TLS and What to Alert On

Qdrant starts with no authentication at all. What to turn on before it faces anything, how read-only and scoped access work, which metrics are worth an alert, and how to plan capacity from the arithmetic in this series.

Everything up to here has been about making search good. This part is about making it safe to run, and it starts with the most important sentence in the series:

A default Qdrant has no authentication. Start the container as every example in this series has, and anyone who can reach the port can read every vector, delete every collection, and create new ones. There is no password to get wrong because there is no password.

That’s a reasonable default for a local container on localhost. It is a catastrophe the first time that port is reachable from anywhere else, and the failure mode is silent: everything works perfectly, for everyone.


Try this first

You’ve deployed Qdrant to a cloud VM and your application can reach it. Search works.

List everything an attacker who can reach that port can do.

Then answer: how would you find out if someone had?

The second question is usually the harder one, and it’s the reason the observability section is in this part rather than a different one.


The API key

The minimum, and it takes one environment variable:

docker run -p 6333:6333 \
  -e QDRANT__SERVICE__API_KEY="a-long-random-string" \
  qdrant/qdrant@sha256:12364fe851b9f17356fc88189fc06d1b521262e04659ec7345975b00c9246a10
client = QdrantClient(url="https://qdrant.internal:6333", api_key=os.environ["QDRANT_API_KEY"])

There is also a read-only key, which can search but cannot write or administer:

-e QDRANT__SERVICE__API_KEY="full-access-key"
-e QDRANT__SERVICE__READ_ONLY_API_KEY="read-only-key"

That distinction is worth using even in a single-application deployment. The search path of your service almost certainly doesn’t need to be able to delete a collection, and giving it a key that can is how a bug in an unrelated code path becomes a data-loss incident.

An API key is only meaningful over TLS. Sent over plain HTTP it travels in the clear on every request, so anyone in a position to watch traffic has it. Key and TLS are one decision, not two.


TLS

-e QDRANT__SERVICE__ENABLE_TLS=1
-e QDRANT__TLS__CERT=/qdrant/tls/cert.pem
-e QDRANT__TLS__KEY=/qdrant/tls/key.pem

Terminating TLS at a proxy in front of Qdrant is equally fine, and often easier to operate. What matters is that the hop carrying the API key is encrypted — including inside your network, if you take the position that the network is not trustworthy, which by now most people do.


Finer-grained access with JWT

An API key is all-or-nothing, and one read-only variant isn’t enough for a multi-tenant product. Qdrant supports JWT-based access control: you sign a token that grants specific access, and the server validates it against the API key as the signing secret.

That lets you issue a token scoped to particular collections, to read-only operations, or — most usefully — restricted to a payload filter, so a token can only ever see points matching it.

This is the piece that changes the multitenancy picture from Part 8. There, tenant isolation was a filter your application had to remember to add, and one missing filter was a cross-tenant leak. With a payload-scoped token, the restriction is enforced by the server, on a token your application cannot widen. That turns a discipline problem into an architectural one, which is a much better kind of problem.

Tokens expire, which also means a leaked one stops working — unlike a static API key, which is valid until somebody notices and rotates it.

Who can do what search all search own tenant write delete collection your search path your ingest job tenant A's token anyone on the network

Schematic — a table of rules, not measurements. The important column is the last one: a default Qdrant lets anyone who can reach the port delete a collection. The important row is the third: with a payload-scoped token, a forgotten tenant filter returns nothing rather than another tenant’s data.


Network posture

Access control is the second line. The first is not being reachable.

  • Do not expose Qdrant to the internet. It belongs on a private network, reachable only by the services that use it. Most Qdrant incidents are not clever; they are an open port.
  • Two ports. REST on 6333 and gRPC on 6334. Firewalling one and forgetting the other is a common and complete failure.
  • The dashboard. Qdrant serves a web UI. Convenient in development, and one more thing not to expose.
  • Rate limiting belongs at the proxy. An expensive query is genuinely expensive — a high ef with a large limit and rescoring at high oversampling costs real CPU and I/O — so an unauthenticated or under-authenticated endpoint is a denial-of-service surface as well as a data one.

What to alert on

Qdrant exposes Prometheus metrics at /metrics. The useful alerts follow from the rest of this series rather than from a generic list:

Collection not fully indexed. From Part 10: indexed_vectors_count well below points_count, for longer than an optimiser pass should take, means searches are scanning. It’s the single most informative check and it costs one API call.

Memory, including the page cache. From Part 10: with mmap the working set lives in the OS page cache, so process RSS is the wrong thing to watch. Watch page faults and disk read throughput as well, or you’ll see healthy memory while the machine thrashes.

Latency percentiles, per collection. p95 and p99, not the mean. And remember Part 1’s finding: a lot of what a client observes is round trip, so measure at the client and at the server, because they answer different questions.

Filtered recall, sampled. From Part 7: filtered search can degrade silently as data grows and selectivity shifts. Nothing surfaces it. A periodic shadow query with exact=True, compared against the indexed result, is the only thing that will tell you.

Disk space, against the deletion lag. Deleted points are flagged and reclaimed later, so logical size and physical size diverge. A collection with heavy churn can fill a disk that looks half empty by point count.

Snapshot age and size. From Part 11: a backup that quietly stopped being taken is only discovered when you need it. Alert on age, and on a sudden change in size.


Capacity planning, from this series

You can plan a deployment from the arithmetic without measuring anything, then measure to confirm. For N points at d dimensions:

raw vectors     = N × d × 4 bytes                        (Part 5)
scalar quantized= N × d × 1 byte
binary quantized= N × d / 8 bytes
HNSW graph      ≈ N × m × 2 × 4 bytes                    (Part 3)
× replication_factor for all of it                       (Part 12)

Worked through for 50 million points at 768 dimensions, m=16, replication factor 2:

  • raw vectors: 50e6 × 768 × 4 = about 143 GiB
  • scalar quantized: about 35.8 GiB
  • graph: 50e6 × 16 × 2 × 4 = about 6 GiB
  • replicated ×2: about 84 GiB of quantized vectors and graph

So a hot path of quantized vectors plus graph is roughly 84 GiB across the cluster, with the originals on disk for rescoring. That is a planning number you can take to a procurement conversation, and it required no benchmark — only the counts and the multiplications.

What the arithmetic cannot tell you is whether quantization at that level keeps your recall acceptable. That is Part 4, on your own vectors, and it is the one number in the plan you have to earn.


Explain it like I’m ten

Imagine a warehouse full of valuable things.

No API key is leaving the doors open with no lock. Everything works beautifully and anyone can walk in. Nothing tells you they did.

An API key is one key that opens everything. Better than nothing, and everyone who needs to get in has the same key, so if one person loses theirs, everyone’s lock has to change.

A read-only key is a key that opens the door but not the tills. Give it to the people who only ever need to look.

A scoped token is a key that opens one specific room, and stops working on Friday. If it leaks, the damage is one room and it expires by itself.

And the alarms are the part people forget. A lock tells you who should get in. An alarm tells you what actually happened. Without one you find out about the break-in when you notice something missing, which could be months.

Where the analogy breaks: a burgled warehouse looks burgled. A database that has been read looks exactly like one that hasn’t — the vectors are all still there. That’s why the answer to “how would you know” is logging and monitoring, and why it can’t be retrofitted after the fact.

The precise version

Qdrant’s default is unauthenticated access to the full API surface on both the REST and gRPC listeners. service.api_key enables a static bearer credential checked per request; service.read_only_api_key provides a second credential restricted to non-mutating operations.

JWT support uses the API key as the HMAC signing secret, so the server validates tokens without additional key distribution. A token’s claims can restrict it to particular collections, to read-only access, and to a payload filter — the last being an access filter applied server-side and combined with the query’s own filter, so a token cannot widen its own scope. This is the mechanism that turns Part 8’s application-enforced tenancy into a server-enforced one, changing the failure mode of a forgotten filter from a data leak to an empty result set.

Note what the static key does not give you: no identity, no per-caller audit trail, no revocation short of rotation. Tokens address the first and third; the second is the proxy’s job.


Trade-offs

Static API key against JWT. A key is one environment variable and gives all-or-nothing access with no expiry. Tokens give scope and expiry, and need issuing infrastructure.

TLS at Qdrant against at a proxy. Terminating at a proxy is usually easier to operate and leaves the last hop unencrypted unless you handle it. Either is defensible; pretending the question doesn’t exist is not.

Read-only key for the search path. Costs nothing, removes a whole class of accident. There is no good argument against it.

Alerting depth against noise. The six checks above are the ones that follow from how this system actually fails. A generic dashboard will show you CPU and miss silently degraded filtered recall.

Rate limiting against legitimate heavy queries. A high ef with rescoring is expensive and sometimes exactly what a user needs. Limit per caller rather than rejecting expensive queries.


Common mistakes

Running with no API key because it’s “internal”. Internal networks are reachable by more than you think, and the failure is silent.

An API key over plain HTTP. It travels in the clear on every request. Key and TLS are one decision.

Firewalling 6333 and forgetting 6334. REST and gRPC are separate listeners.

Exposing the dashboard. Useful in development, another door in production.

Using the full-access key for search. The search path doesn’t need to delete collections.

Monitoring process memory with mmap on. The working set is in the page cache. RSS looks fine while the machine thrashes.

No alert on indexed_vectors_count. Searches silently scanning is the most common quiet degradation, and it’s one API call to detect.

Assuming a backup exists. Part 11: alert on snapshot age, and restore one occasionally.

Planning capacity from raw vector size alone. Add the graph, the payloads and the replication factor, all of which are arithmetic.


Interview questions

1. What’s the default authentication on a fresh Qdrant?

Answer None. Anyone who can reach the port has full API access: read every vector, delete every collection, create new ones. There is no default password because there is no authentication layer enabled. That’s fine for a container bound to localhost and catastrophic the moment the port is reachable more widely — and it fails silently, because everything works correctly for everyone. **Follow-up:** what’s the minimum fix? `QDRANT__SERVICE__API_KEY`, plus TLS, because a key over plain HTTP is sent in the clear on every request.

2. How would you give a multi-tenant application tenant isolation you can rely on?

Answer JWT tokens scoped to a payload filter. The restriction is applied server-side and combined with the query’s filter, so the token cannot widen its own scope. That’s a genuine improvement over Part 8’s payload multitenancy, where isolation depended on your application remembering to add a filter and one omission was a cross-tenant leak. With a scoped token, a forgotten filter returns nothing rather than someone else’s data. **Follow-up:** when would you still use separate collections? When you need the boundary to be physical — different storage, different nodes — rather than enforced by the query layer.

3. Which metrics would you alert on?

Answer `indexed_vectors_count` falling behind `points_count` for longer than an optimiser pass, because that means searches are scanning. Latency percentiles per collection, measured at both client and server. Memory including page-fault rate if anything is memory-mapped. Disk space against the deletion lag. Snapshot age and size. And one that isn’t a metric: sampled filtered recall, compared against `exact=True`, because filtered search degrades silently as selectivity shifts and nothing else will tell you. **Follow-up:** why measure latency in two places? The client sees round trip plus search; the server sees search. Part 1 showed the round trip can dominate, and you can’t tell which you’re fixing from one number.

4. How much memory for 50 million vectors at 768 dimensions, replicated twice?

Answer Raw vectors are 50e6 × 768 × 4, about 143 GiB per copy. Scalar quantized, about 35.8 GiB. The HNSW graph at `m=16` is roughly 50e6 × 16 × 2 × 4, about 6 GiB. Replicated twice, the quantized hot path plus graph is about 84 GiB across the cluster. The shape of the answer matters as much as the number: it’s all arithmetic, so it belongs in the plan before any hardware is bought. **Follow-up:** what can’t you compute? Whether quantization at that level keeps recall acceptable. That’s measured on your own vectors, and the whole plan depends on it.

5. Why is a read-only key worth using in a single-application deployment?

Answer Because the search path doesn’t need to delete collections, and giving it a credential that can means any bug or injection in that path has full destructive reach. It costs one environment variable and removes a class of accident. There’s no real argument against it. **Follow-up:** what does it not protect against? Reading data it shouldn’t — it’s read-only, not scoped. For that you need payload-scoped tokens.

6. Someone had access to your Qdrant for a month. How would you know what they did?

Answer From a static API key, you wouldn’t. There’s no identity, no per-caller audit trail, and reading data leaves everything exactly as it was — unlike a theft, a copied database looks untouched. What gives you a chance is what sits in front: a proxy logging authenticated requests per caller, tokens carrying an identity rather than one shared secret, and network-level records of who connected. All of which have to be in place beforehand. **Follow-up:** what would you do differently having learned that? Per-caller credentials with expiry, and request logging at the proxy, both from the start.

7. What’s the risk of rate limiting by query cost rather than per caller?

Answer You end up rejecting exactly the queries some users legitimately need — a high `ef` with rescoring at high oversampling is expensive precisely because it’s doing careful work. Limiting per caller keeps one client from consuming the cluster while letting an individual expensive query through. The real defence against expensive queries is bounding the parameters your API exposes, rather than punishing the ones that use them. **Follow-up:** which parameters would you bound? `limit`, `ef`, and oversampling — the three that multiply work per query.

8. Is it safe to expose Qdrant’s dashboard internally?

Answer Treat it as another full-access door. It’s genuinely useful in development, and in production it’s an additional surface with the same authority as the API. The broader point: Qdrant listens on two ports, REST and gRPC, and serves a UI. Any hardening that covers one and not the others isn’t hardening. **Follow-up:** what if you want it for debugging production? Reach it through the same authenticated, logged path as everything else — a bastion or an authenticating proxy — rather than opening a port.

Sources

What to remember

A default Qdrant has no authentication. Set an API key, put TLS around it, keep the port off the public internet, and give the search path a read-only key. That’s an afternoon and it removes almost all of the risk.

Then use scoped tokens if you’re multi-tenant, because they move tenant isolation from something your code must remember into something the server enforces.

Alert on the things this system actually does wrong: unindexed vectors, page-cache pressure, filtered recall drifting, snapshots quietly stopping. A generic dashboard will show you CPU and miss every one of them.

You can plan the memory with arithmetic and you cannot plan the recall that way. Everything else in hardening is one afternoon of configuration you will otherwise do during an incident.

How useful was this post?

Click on a heart to rate it!

Average rating 0 / 5. Vote count: 0

No votes so far! Be the first to rate this post.