Security & Access Control
Securing a Qdrant deployment means controlling who can access your data, encrypting traffic, and keeping an audit trail for compliance. To secure your deployments, Qdrant supports API key authentication (including read-only API keys for query-only consumers and granular access API keys with per-collection read/write scoping), network binding, TLS for encrypted connections, and audit logging for compliance. On Qdrant Cloud, these features are enabled by default. On self-hosted open source deployments, they must be explicitly configured before going to production.
Secure Your Instance
By default, all self-deployed Qdrant instances are not secure. They are open to all network interfaces and do not have any kind of authentication configured. They may be open to everybody on the internet without any restrictions. You must therefore take security measures to make your instance production-ready. Please read this section carefully for instructions on how to secure your instance.
Qdrant Cloud deployments are always secure by default. Refer to Authentication and Client IP Restrictions.
To properly secure your own instance, we strongly recommend taking the following steps:
- Authentication: Set up an API key to prevent unauthorized access.
- Audit Logging: Record all API operations to a log file for compliance and forensics.
- Network Bind: Bind to a specific network interface or IP address.
When developing locally, bind to127.0.0.1to prevent all external access. When deploying to production, bind to a private network interface or IP. - TLS: Encrypt traffic everywhere using TLS.
Authentication
By default, an open source Qdrant deployment accepts requests from anyone who can reach it. To secure your instance, enable API key authentication. On Qdrant Cloud, API key authentication is enabled by default.
Qdrant supports three types of API key:
- Admin API Key: Grants full access to all operations and collections.
- Read-Only API Key: Grants read-only access to all operations and collections. This key can be used for services or users that only need to query data.
- Granular Access API Keys: For more granular access control, you can use API keys that specify read or write permissions on individual collections.
Authenticate with an API Key
To authenticate with an API key, whether it’s an admin key, read-only key, or granular access token, provide it in the api-key request header:
curl -X GET https://xyz-example.eu-central.aws.cloud.qdrant.io:6333 \
--header 'api-key: your_api_key_here'
from qdrant_client import QdrantClient
client = QdrantClient(
url="https://xyz-example.eu-central.aws.cloud.qdrant.io:6333",
api_key="your_api_key_here",
)
import { QdrantClient } from "@qdrant/js-client-rest";
const client = new QdrantClient({
url: "https://xyz-example.eu-central.aws.cloud.qdrant.io",
port: 6333,
apiKey: "your_api_key_here",
});
use qdrant_client::Qdrant;
let client = Qdrant::from_url("https://xyz-example.eu-central.aws.cloud.qdrant.io:6334")
.api_key("your_api_key_here")
.build()?;
import io.qdrant.client.QdrantClient;
import io.qdrant.client.QdrantGrpcClient;
QdrantClient client = new QdrantClient(
QdrantGrpcClient.newBuilder("xyz-example.eu-central.aws.cloud.qdrant.io", 6334, true)
.withApiKey("your_api_key_here")
.build());
using Qdrant.Client;
var client = new QdrantClient(
host: "xyz-example.eu-central.aws.cloud.qdrant.io",
port: 6334,
https: true,
apiKey: "your_api_key_here");
import (
"github.com/qdrant/go-client/qdrant"
)
client, err := qdrant.NewClient(&qdrant.Config{
Host: "xyz-example.eu-central.aws.cloud.qdrant.io",
Port: 6334,
APIKey: "your_api_key_here",
UseTLS: true,
})
Alternatively, use the Authorization: Bearer header:
curl -X GET https://xyz-example.eu-central.aws.cloud.qdrant.io:6333 \
--header 'Authorization: Bearer your_token_here'
from qdrant_client import QdrantClient
client = QdrantClient(
url="https://xyz-example.eu-central.aws.cloud.qdrant.io:6333",
auth_token_provider=lambda: "your_token_here",
)
import { QdrantClient } from "@qdrant/js-client-rest";
const client = new QdrantClient({
url: "https://xyz-example.eu-central.aws.cloud.qdrant.io",
port: 6333,
headers: {
authorization: "Bearer your_token_here",
},
});
use qdrant_client::Qdrant;
let client = Qdrant::from_url("https://xyz-example.eu-central.aws.cloud.qdrant.io:6334")
.header("authorization", "Bearer your_token_here")
.build()?;
import io.qdrant.client.QdrantClient;
import io.qdrant.client.QdrantGrpcClient;
import java.util.Map;
QdrantClient client = new QdrantClient(
QdrantGrpcClient.newBuilder("xyz-example.eu-central.aws.cloud.qdrant.io", 6334, true)
.withHeaders(Map.of("authorization", "Bearer your_token_here"))
.build());
using Qdrant.Client;
var client = new QdrantClient(
host: "xyz-example.eu-central.aws.cloud.qdrant.io",
port: 6334,
https: true,
apiKey: null,
grpcTimeout: default,
loggerFactory: null,
headers: new Dictionary<string, string>
{
{ "authorization", "Bearer your_token_here" }
});
import (
"github.com/qdrant/go-client/qdrant"
)
client, err := qdrant.NewClient(&qdrant.Config{
Host: "xyz-example.eu-central.aws.cloud.qdrant.io",
Port: 6334,
UseTLS: true,
Headers: map[string]string{
"authorization": "Bearer your_token_here",
},
})
Admin API Key
Available as of v1.2.0
The admin API key is the primary key that grants full access to all operations and collections. It is configured with the api_key setting in the configuration file.
service:
# Set an api-key.
# If set, all requests must include a header with the api-key.
# example header: `api-key: <API-KEY>`
#
# If you enable this you should also enable TLS.
# (Either above or via an external service like nginx.)
# Sending an api-key over an unencrypted channel is insecure.
api_key: your_secret_api_key_here
Or alternatively, you can use the QDRANT__SERVICE__API_KEY environment variable:
docker run -p 6333:6333 \
-e QDRANT__SERVICE__API_KEY=your_secret_api_key_here \
qdrant/qdrant
For using API key based authentication on Qdrant Cloud, see the Cloud Authentication section.
Rotate an Admin API Key
Available as of v1.17.0
In a distributed deployment, you can rotate an admin API key without downtime. Use the alt_api_key setting to temporarily configure a second API key that acts identically to the primary api_key, allowing both the old and new API keys to be active at the same time.
service:
api_key: your_current_api_key_here
alt_api_key: your_new_api_key_here
To rotate an API key without downtime:
- Configure each peer with the new key set as
alt_api_key. Restart only one peer at a time to avoid downtime (rolling restart). During the rotation window, requests authenticated with either key are accepted. - Switch clients to the new key.
- Perform another rolling restart of the peers, promoting the new key to
api_keyand removingalt_api_key.
Read-Only API Key
Available as of v1.7.0
Qdrant also supports a read-only API key. This key can be used to access read-only operations on the instance.
service:
read_only_api_key: your_secret_read_only_api_key_here
Or with the environment variable:
export QDRANT__SERVICE__READ_ONLY_API_KEY=your_secret_read_only_api_key_here
Admin and read-only API keys can be used simultaneously.
Granular Access API Keys
Available as of v1.9.0
Granular access API keys let you assign read or write permissions on individual collections, enabling Role-based access control (RBAC). They’re built on the JSON Web Tokens (JWT) standard.
On Qdrant Cloud, granular access API key authentication is enabled by default. To enable granular access API key authentication on open source Qdrant instances, specify an api-key and enable the jwt_rbac feature in the configuration:
service:
api_key: you_secret_api_key_here
jwt_rbac: true
Or with environment variables:
export QDRANT__SERVICE__API_KEY=your_secret_api_key_here
export QDRANT__SERVICE__JWT_RBAC=true
The api_key you set in the configuration will be used to encode and decode the JWTs, so –needless to say– keep it secure. If your api_key changes, all existing tokens will be invalid.
Generating JSON Web Tokens
JWTs can be generated with the admin API key. You can use any of the existing libraries and tools to generate tokens. You can also use the Qdrant Web UI to generate JWTs by selecting Access Tokens.
JWT Header - Qdrant uses the
HS256algorithm to decode the tokens.{ "alg": "HS256", "typ": "JWT" }JWT Payload - You can include any combination of the available parameters in the payload.
{ "exp": 1640995200, // Expiration time "value_exists": ..., // Validate this token by looking for a point with a payload value "access": "r", // Define the access level. }
Signing the token - To confirm that the generated token is valid, it needs to be signed with the api_key set in the configuration.
This means that someone who knows the admin API key can authorize the new token for use with the Qdrant instance.
Qdrant can validate the signature because it knows the admin API key and can decode the token.
The process of token generation can be done on the client side offline and doesn’t require any communication with the Qdrant instance.
Here is an example of libraries that can be used to generate JWT tokens:
- Python: PyJWT
- JavaScript: jsonwebtoken
- Rust: jsonwebtoken
- CLI: jwt-cli
Here is an example using jwt-cli:
jwt encode --payload '{
"access": "r",
"exp": 1766055305
}' --secret 'your-api-key'
JWT Configuration
These are the available options, or claims in the JWT lingo. You can use them in the JWT payload to define its functionality.
exp- The expiration time of the token. This is a Unix timestamp in seconds. The token will be invalid after this time. The check for this claim includes a 30-second leeway to account for clock skew.{ "exp": 1640995200, // Expiration time }value_exists- This is a claim that can be used to validate the token against the data stored in a collection. The structure of this claim is as follows:{ "value_exists": { "collection": "my_validation_collection", "matches": [ { "key": "my_key", "value": "value_that_must_exist" } ], }, }If this claim is present, Qdrant will check if there is a point in the collection with the specified key-values. If such a point exists, the token is valid.
This claim is especially useful if you want to have an ability to revoke tokens without changing the
api_key. Consider a case where you have a collection of users, and you want to revoke access to a specific user.{ "value_exists": { "collection": "users", "matches": [ { "key": "user_id", "value": "andrey" }, { "key": "role", "value": "manager" } ], }, }You can create a token with this claim, and when you want to revoke access, you can change the
roleof the user to something else, and the token will become invalid.access- This claim defines the access level of the token. If this claim is present, Qdrant will check if the token has the required access level to perform the operation. If this claim is not present, manage access is assumed.It can provide global access with
rfor read-only, ormfor manage. For example:{ "access": "r" }It can also be specific to one or more collections. The
accesslevel for each collection isrfor read-only, orrwfor read-write, like this:{ "access": [ { "collection": "my_collection", "access": "rw" } ] }
Table of Access
Check out this table to see which actions are allowed or denied based on the access level.
This is also applicable to using api keys instead of tokens. In that case, api_key maps to manage, while read_only_api_key maps to read-only.
| Action | manage | read-only | collection read-write | collection read-only |
|---|---|---|---|---|
| list collections | ✅ | ✅ | 🟡 | 🟡 |
| get collection info | ✅ | ✅ | ✅ | ✅ |
| create collection | ✅ | ❌ | ❌ | ❌ |
| delete collection | ✅ | ❌ | ❌ | ❌ |
| update collection params | ✅ | ❌ | ❌ | ❌ |
| get collection cluster info | ✅ | ✅ | ✅ | ✅ |
| collection exists | ✅ | ✅ | ✅ | ✅ |
| update collection cluster setup | ✅ | ❌ | ❌ | ❌ |
| update aliases | ✅ | ❌ | ❌ | ❌ |
| list collection aliases | ✅ | ✅ | 🟡 | 🟡 |
| list aliases | ✅ | ✅ | 🟡 | 🟡 |
| create shard key | ✅ | ❌ | ❌ | ❌ |
| delete shard key | ✅ | ❌ | ❌ | ❌ |
| create payload index | ✅ | ❌ | ✅ | ❌ |
| delete payload index | ✅ | ❌ | ✅ | ❌ |
| list collection snapshots | ✅ | ✅ | ✅ | ✅ |
| create collection snapshot | ✅ | ❌ | ✅ | ❌ |
| delete collection snapshot | ✅ | ❌ | ✅ | ❌ |
| download collection snapshot | ✅ | ✅ | ✅ | ✅ |
| upload collection snapshot | ✅ | ❌ | ❌ | ❌ |
| recover collection snapshot | ✅ | ❌ | ❌ | ❌ |
| list shard snapshots | ✅ | ✅ | ✅ | ✅ |
| create shard snapshot | ✅ | ❌ | ✅ | ❌ |
| delete shard snapshot | ✅ | ❌ | ✅ | ❌ |
| download shard snapshot | ✅ | ✅ | ✅ | ✅ |
| upload shard snapshot | ✅ | ❌ | ❌ | ❌ |
| recover shard snapshot | ✅ | ❌ | ❌ | ❌ |
| list full snapshots | ✅ | ✅ | ❌ | ❌ |
| create full snapshot | ✅ | ❌ | ❌ | ❌ |
| delete full snapshot | ✅ | ❌ | ❌ | ❌ |
| download full snapshot | ✅ | ✅ | ❌ | ❌ |
| get cluster info | ✅ | ✅ | ❌ | ❌ |
| recover raft state | ✅ | ❌ | ❌ | ❌ |
| delete peer | ✅ | ❌ | ❌ | ❌ |
| get point | ✅ | ✅ | ✅ | ✅ |
| get points | ✅ | ✅ | ✅ | ✅ |
| upsert points | ✅ | ❌ | ✅ | ❌ |
| update points batch | ✅ | ❌ | ✅ | ❌ |
| delete points | ✅ | ❌ | ✅ | ❌ |
| update vectors | ✅ | ❌ | ✅ | ❌ |
| delete vectors | ✅ | ❌ | ✅ | ❌ |
| set payload | ✅ | ❌ | ✅ | ❌ |
| overwrite payload | ✅ | ❌ | ✅ | ❌ |
| delete payload | ✅ | ❌ | ✅ | ❌ |
| clear payload | ✅ | ❌ | ✅ | ❌ |
| scroll points | ✅ | ✅ | ✅ | ✅ |
| query points | ✅ | ✅ | ✅ | ✅ |
| search points | ✅ | ✅ | ✅ | ✅ |
| search groups | ✅ | ✅ | ✅ | ✅ |
| recommend points | ✅ | ✅ | ✅ | ✅ |
| recommend groups | ✅ | ✅ | ✅ | ✅ |
| discover points | ✅ | ✅ | ✅ | ✅ |
| count points | ✅ | ✅ | ✅ | ✅ |
| version | ✅ | ✅ | ✅ | ✅ |
| readyz, healthz, livez | ✅ | ✅ | ✅ | ✅ |
| telemetry | ✅ | ✅ | ❌ | ❌ |
| metrics | ✅ | ✅ | ❌ | ❌ |
Audit Logging
Available as of v1.17.0
Audit logging records all API operations that require authentication or authorization, and writes them to a log file in JSON format.
Audit logging is not enabled by default. To enable it, use the following configuration options:
audit:
enabled: false
dir: ./storage/audit
rotation: daily
max_log_files: 7
# Only enable when Qdrant is behind a trusted reverse proxy or load balancer.
# When true, the client IP is taken from the X-Forwarded-For header instead of
# the TCP connection. Enabling this on a publicly reachable instance allows
# clients to spoof their IP address in audit logs.
trust_forwarded_headers: false
By default, audit logs are rotated daily, and the seven most recent log files are kept. To configure hourly rotation, set rotation to hourly. When the number of log files exceeds max_log_files, the oldest log file is deleted.
Tracing IDs
Available as of v1.18.0
You can attach a tracing ID to individual requests. When audit logging is enabled, Qdrant includes the tracing ID in the audit log entry, enabling the correlation of client-side operations with their corresponding log entries.
Qdrant reads the tracing ID from the first matching header in the following order: x-request-id, x-tracing-id, traceparent. Tracing IDs longer than 256 characters are truncated.
curl -X GET http://localhost:6333/collections \
--header 'api-key: your_api_key_here' \
--header 'x-request-id: my-trace-id'
from qdrant_client import QdrantClient
from qdrant_client.context_headers import headers
with headers({"x-request-id": "my-trace-id"}):
client.get_collections()
import { QdrantClient, withHeaders } from "@qdrant/js-client-rest";
const result = await withHeaders({ "x-request-id": "my-trace-id" }, () =>
client.getCollections()
);
use qdrant_client::Qdrant;
client
.with_header("x-request-id", "my-trace-id")
.list_collections()
.await?;
import io.qdrant.client.QdrantClient;
import io.qdrant.client.QdrantGrpcClient;
import io.qdrant.client.RequestHeaders;
import io.grpc.Context;
Context ctx = RequestHeaders.withHeader(Context.current(), "x-request-id", "my-trace-id");
ctx.run(() -> client.listCollectionsAsync());
using Qdrant.Client;
using (RequestHeaders.Use("x-request-id", "my-trace-id"))
await client.ListCollectionsAsync();
import (
"context"
"github.com/qdrant/go-client/qdrant"
)
ctx := qdrant.WithHeader(context.Background(), "x-request-id", "my-trace-id")
client.ListCollections(ctx)
Query Audit Logs
Available as of v1.18.0
The audit log can be queried via the /audit/logs API (requires manage-level access). For example:
curl -X POST 'https://YOUR-CLUSTER-URL:6333/audit/logs' \
-H 'api-key: QDRANT_API_KEY' \
-H 'Content-Type: application/json' \
-d '{}'
By default, the API returns the 100 most recent entries, but you can change this number with the limit parameter (max 10,000).
In a distributed cluster, the API aggregates results from all nodes before returning them. An optional timeout (seconds) query parameter controls how long to wait for remote peers in a cluster.
Entries are returned in reverse-chronological order (newest first). Each entry has the following fields:
| Field | Type | Description |
|---|---|---|
timestamp | ISO-8601 | When the access check occurred. |
method | string | API method name, for example upsert_points, search_points. |
auth_type | "Jwt" | "ApiKey" | "None" | How the request was authenticated. |
result | "ok" | "denied" | Whether access was granted. |
subject | string | JWT sub claim. Only present for JWT-authenticated requests. |
remote | string | Client IP address, if available. |
collection | string | Collection name, for collection-scoped operations. |
tracing_id | string | Value of the x-request-id, x-tracing-id, or traceparent request header. |
error | string | Reason access was denied. Only present when result is "denied". |
Narrowing Results with Time Ranges and Filters
To narrow results to a specific time range, use the time_from (inclusive) and time_to (exclusive) parameters.
The filters parameter enables exact-match filtering of entries based on specific field values. The parameter accepts a dictionary of field-value pairs. When specifying more than one pair, only entries that match all specified criteria are returned (logical AND).
Unknown filter fields silently return no matches. Filter field names are case-sensitive: filtering on a field name with incorrect casing silently returns no matches.
You can filter on any field in the entry fields table except timestamp. Use time_from and time_to for time-range filtering instead.
For example, to retrieve the 50 most recent denied requests to the my_collection collection on March 26, 2026:
curl -X POST 'https://YOUR-CLUSTER-URL:6333/audit/logs' \
-H 'api-key: QDRANT_API_KEY' \
-H 'Content-Type: application/json' \
-d '{
"limit": 50,
"time_from": "2026-03-26T00:00:00Z",
"time_to": "2026-03-27T00:00:00Z",
"filters": {
"result": "denied",
"collection": "my_collection"
}
}'
Network Bind
By default, a custom Qdrant deployment binds to all network interfaces. Your instance may be open to everybody on the internet. On a local development machine you likely have a firewall in place to prevent public access, but that may not be the case on a public VPS or dedicated server.
It is highly recommended to bind to a specific interface or IP address to prevent unwanted access:
- when developing locally, bind to
127.0.0.1so no external access is possible - or, when deploying to production, bind to a private network interface or IP
When using Docker, you may use the publish flag to bind to a specific interface. For example:
docker run -p 127.0.0.1:6333:6333 qdrant/qdrant
If using another type of deployment you may configure the bind address in Qdrant
itself. Either set service.host: 127.0.0.1 in the configuration, or use an
environment variable like this:
QDRANT__SERVICE__HOST=127.0.0.1 ./qdrant
Managed Qdrant Cloud deployments are always secure by default. They are publicly accessible and bound to the endpoint that is assigned to the cluster. You may configure authentication with API keys, and restrict access to specific IP addresses through Client IP Restrictions. Hybrid Cloud and Private Cloud deployments have their own kind of configuration.
TLS
Available as of v1.2.0
TLS for encrypted connections can be enabled on your Qdrant instance to secure connections.
First make sure you have a certificate and private key for TLS, usually in
.pem format. On your local machine you may use
mkcert to generate a self signed
certificate.
To enable TLS, set the following properties in the Qdrant configuration with the correct paths and restart:
service:
# Enable HTTPS for the REST and gRPC API
enable_tls: true
# TLS configuration.
# Required if either service.enable_tls or cluster.p2p.enable_tls is true.
tls:
# Server certificate chain file
cert: ./tls/cert.pem
# Server private key file
key: ./tls/key.pem
For internal communication when running cluster mode, TLS can be enabled with:
cluster:
# Configuration of the inter-cluster communication
p2p:
# Use TLS for communication between peers
enable_tls: true
With TLS enabled, you must start using HTTPS connections. For example:
curl -X GET https://localhost:6333
from qdrant_client import QdrantClient
client = QdrantClient(
url="https://localhost:6333",
)
import { QdrantClient } from "@qdrant/js-client-rest";
const client = new QdrantClient({ url: "https://localhost", port: 6333 });
use qdrant_client::Qdrant;
let client = Qdrant::from_url("http://localhost:6334").build()?;
Certificate rotation is enabled with a default refresh time of one hour. This
reloads certificate files every hour while Qdrant is running. This way changed
certificates are picked up when they get updated externally. The refresh time
can be tuned by changing the tls.cert_ttl setting. You can leave this on, even
if you don’t plan to update your certificates. Currently this is only supported
for the REST API.
Optionally, you can enable client certificate validation on the server against a local certificate authority. Set the following properties and restart:
service:
# Check user HTTPS client certificate against CA file specified in tls config
verify_https_client_certificate: false
# TLS configuration.
# Required if either service.enable_tls or cluster.p2p.enable_tls is true.
tls:
# Certificate authority certificate file.
# This certificate will be used to validate the certificates
# presented by other nodes during inter-cluster communication.
#
# If verify_https_client_certificate is true, it will verify
# HTTPS client certificate
#
# Required if cluster.p2p.enable_tls is true.
ca_cert: ./tls/cacert.pem
Hardening
We recommend reducing the amount of permissions granted to Qdrant containers so that you can reduce the risk of exploitation. Here are some ways to reduce the permissions of a Qdrant container:
Run Qdrant as a non-root user. This can help mitigate the risk of future container breakout vulnerabilities. Qdrant does not need the privileges of the root user for any purpose.
- You can use the image
qdrant/qdrant:<version>-unprivilegedinstead of the default Qdrant image. - You can use the flag
--user=1000:2000when runningdocker run. - You can set
user: 1000when using Docker Compose. - You can set
runAsUser: 1000when running in Kubernetes (our Helm chart does this by default).
- You can use the image
Run Qdrant with a read-only root filesystem. This can help mitigate vulnerabilities that require the ability to modify system files, which is a permission Qdrant does not need. As long as the container uses mounted volumes for storage (
/qdrant/storageand/qdrant/snapshotsby default), Qdrant can continue to operate while being prevented from writing data outside of those volumes.- You can use the flag
--read-onlywhen runningdocker run. - You can set
read_only: truewhen using Docker Compose. - You can set
readOnlyRootFilesystem: truewhen running in Kubernetes (our Helm chart does this by default).
- You can use the flag
Block Qdrant’s external network access. This can help mitigate server side request forgery attacks, like via the snapshot recovery API. Single-node Qdrant clusters do not require any outbound network access. Multi-node Qdrant clusters only need the ability to connect to other Qdrant nodes via TCP ports 6333, 6334, and 6335.
- You can use
docker network create --internal <name>and use that network when runningdocker run --network <name>. - You can create an internal network when using Docker Compose.
- You can create a NetworkPolicy when using Kubernetes. Note that multi-node Qdrant clusters will also need access to cluster DNS in Kubernetes.
- You can use
There are other techniques for reducing the permissions such as dropping Linux capabilities depending on your deployment method, but the methods mentioned above are the most important.
