Skip to main content
Cognee exposes several environment variables that let you harden a self-hosted deployment for production use. Some controls are enforced by default, while others remain permissive for local development, so you should review each setting before exposing Cognee to untrusted users or networks.

Security Controls

Require authentication for all API requests

ENABLE_BACKEND_ACCESS_CONTROL is the canonical posture switch; REQUIRE_AUTHENTICATION is an optional override on the auth requirement alone.When neither variable is set, the default is multi-tenant mode with authentication required.
At startup, Cognee logs an auth posture: ... line summarizing the resolved decision and the reason (default, inherited from ENABLE_BACKEND_ACCESS_CONTROL, explicit REQUIRE_AUTHENTICATION, or forced on by multi-tenant mode). Use this log line to verify what is actually in effect after deployment.
If ENABLE_BACKEND_ACCESS_CONTROL=true (the default), authentication is enforced automatically regardless of the value of REQUIRE_AUTHENTICATION. Setting REQUIRE_AUTHENTICATION=false in this mode is ignored and a warning is logged at startup.

JWT token settings

There is no fixed default. When the variable is unset or blank, Cognee generates a random secret once per process instead of falling back to a shared, publicly known value. A generated secret is known only to that process, so tokens signed with it stop verifying after a restart and are rejected by every other process. Any deployment running more than one process — uvicorn workers, several replicas, or separate API and worker processes — must set the variable explicitly to the same securely generated value everywhere, and never commit the real value to version control.FASTAPI_USERS_JWT_SECRET must be the same across all instances (e.g., all Kubernetes pods) so that a token issued by one pod is accepted by another.The two sibling token secrets behave identically, and are also generated per process when unset:
With those unset, password reset links and email verification links stop working when the server restarts.
At startup, the API server logs a warning for each of these three variables it had to generate a secret for, naming the variable and what stops working without it. Startup is silent on this when all three are set. Only the server reports it — SDK, CLI and MCP usage never verifies tokens.
JWT_LIFETIME_SECONDS controls how long a bearer token or cookie remains valid before the user must log in again.

Default user credentials

When an operation runs without an explicit authenticated user, Cognee falls back to a built-in default user — created on first use as a superuser. This happens for SDK/library calls, and for HTTP requests when authentication is off (single-user mode). Configure its credentials with:
The default user has no password unless you set one. With DEFAULT_USER_PASSWORD unset, the account is created password-less: SDK, CLI, and the API’s own user resolution still use it (none of them authenticate as it), but POST /api/v1/auth/login rejects every password for it. There is no built-in fallback password — earlier releases used the literal default_password, which gave every install a superuser with publicly known credentials.DEFAULT_USER_PASSWORD is applied once. When the API server starts with it set, it creates the default user if needed and gives a password-less default user that password. It never changes a password the account already has: if the value does not match the stored password, the server logs a warning and leaves the account alone — change an existing password through the API instead (log in as the account, then PATCH /api/v1/users/me with a new password; see Deploy REST API Server). When the variable is unset, the server does not create the default user at startup and logs a warning that no default-user login is configured.The local stacks supply a password for you: cognee-cli -ui (which binds the backend to localhost) and docker-compose.yml both set DEFAULT_USER_PASSWORD=default_password unless you provide your own value, so the UI login works out of the box. Do not reuse that value on a server reachable from a network — leave it unset there, or set a strong one.
Upgrading: an existing default user keeps the password it already has, including the old default_password. The upgrade does not remove that known credential, and setting DEFAULT_USER_PASSWORD to a new value will not replace it, so on any server reachable from a network, change it through the API. A fresh database gets a password-less default user, so if you still want a default-user login there, set DEFAULT_USER_PASSWORD on the server.
When ENABLE_BACKEND_ACCESS_CONTROL=true, HTTP endpoints require an authenticated user, so the default user is not used to serve unauthenticated requests — but SDK calls still fall back to it. DEFAULT_USER_EMAIL only takes effect before the default user is first created; changing it later does not rename an existing user. See Users for the full permission model.

API Key Storage

When false, API keys are stored as plaintext in the relational database. When true, each key is hashed with SHA-256 before storage. The raw key is shown to the user only once at creation time and cannot be recovered afterward.
Migration note: Enabling HASH_API_KEY on a running system that already has plaintext API keys stored will break those existing keys immediately — the lookup hashes the incoming value and finds no match. You must either delete and re-issue all existing keys, or run a one-off migration to SHA-256-hash the existing api_key column values.

Local File System Access

When true, Cognee accepts local filesystem paths as data sources (e.g., /etc/passwd). This is convenient for local development but dangerous when Cognee is exposed as a multi-user backend — an authenticated user could read arbitrary files that the Cognee process has access to.Set to false when running Cognee as a backend service:
The flag governs file:// URIs and path strings that resolve to an existing local file; those inputs raise IngestionError: Local files are not accepted. when it is disabled. It is not a filter on submitted text: an absolute-looking string that does not point to an existing file is ingested as text content either way, since Cognee never treats it as a path.

Allowed local roots

Independently of ACCEPT_LOCAL_FILE_PATH, you can confine every local path Cognee dereferences to a set of allowed roots:
The allowlist is opt-in. When the variable is unset — the default — there is no root restriction at all: any local path is readable, subject only to ACCEPT_LOCAL_FILE_PATH. That is what lets a local install ingest a repository or document tree from wherever it happens to live on the machine. Set this variable on any deployment reachable by untrusted callers; leaving it unset there means an authenticated caller can name any path the Cognee process can read.When it is set, its entries are the allowed roots, separated by the platform path separator (: on Linux/macOS, ; on Windows). Cognee’s own DATA_ROOT_DIRECTORY, SYSTEM_ROOT_DIRECTORY, cache, log, and repository-clone (COGNEE_REPOS_DIR) directories are always appended to that list, so a restrictive value cannot lock Cognee out of its own storage. The clone root has to be on the list because a cloned repository’s document files (README, docs) are ingested by path exactly like a local project’s, and pass through this same check.Paths are canonicalized with realpath before the containment check, so a symlink inside an allowed root that points outside it is rejected rather than followed. Once the variable is set, a path outside every allowed root raises:
On the ordinary add() / remember() ingestion path, a path-looking string outside the allowed roots is never read from disk — it falls through and is ingested as plain text instead, though an explicit file:// URI raises IngestionError with the same message rather than falling through. Callers that read a file explicitly, such as the memory-migration sources, surface the error directly. A local repository path handed to the code-graph pipeline reports the same rejection as a CodeRepositoryError instead:

Presort scan roots

Folder presort is the one path that does not inherit the permissive default above. A scan opens every candidate file in a folder to hash it and sample it for personal data, so it always requires a bounded root set — even when COGNEE_ALLOWED_LOCAL_FILE_ROOTS is unset.When the variable is set, its entries are the permitted roots, exactly as for ordinary ingestion. When it is unset, presort falls back to its own bounded default instead of “anywhere”:
  • the current working directory
  • the system temporary directory
  • Cognee’s own DATA_ROOT_DIRECTORY, SYSTEM_ROOT_DIRECTORY, cache, log, and repository-clone roots
Paths are canonicalized with realpath before the containment check, so a symlink pointing out of a permitted root is rejected rather than followed. A folder outside every permitted root is refused rather than falling through to text ingestion — the scan raises ValueError: Path is outside the allowed local file roots. Add the folder to COGNEE_ALLOWED_LOCAL_FILE_ROOTS (os.pathsep-separated list) to allow it, or use the CLI's --allow-root flag. The same check guards reading a saved *.presort.json report back in, which surfaces the shorter ValueError: Local file path is outside allowed roots.The CLI’s --allow-root flag is the explicit opt-in for a folder outside those roots:
--allow-root appends the folder you named to COGNEE_ALLOWED_LOCAL_FILE_ROOTS for the lifetime of that command, which widens what presort — and everything else running in that process — may read from disk. On a shared or untrusted machine, prefer setting COGNEE_ALLOWED_LOCAL_FILE_ROOTS deliberately to the exact folders you intend to scan.
Presort scanning is read-only: it never moves, renames, or deletes anything in the folder. Personal-data findings in the report are stored redacted (j***@example.com); the raw matched values are never written into the report.

Cypher Query Access

When true, users can execute raw Cypher queries against the graph database (SearchType.CYPHER) and use natural language-to-Cypher translation (SearchType.NATURAL_LANGUAGE). Disable this to limit users to higher-level semantic search only:

Outbound HTTP Requests (SSRF Protection)

When Cognee ingests an http(s) URL (via add() → save_data_item_to_storage) or crawls a page, it fetches that URL server-side. To prevent Server-Side Request Forgery (SSRF, CWE-918), every user-supplied outbound URL is now validated before any request is made.ALLOW_HTTP_REQUESTS (default true) gates outbound HTTP(S) fetching. Set it to false to disable all remote URL ingestion — any http(s) URL is then rejected:
Falsey values are false, 0, no, and off (case-insensitive); any other value leaves outbound fetching enabled.When outbound requests are allowed, each URL still passes through these checks before it is fetched:
  • Scheme — only http and https are permitted. Other schemes (e.g. ftp://, gopher://) are rejected.
  • Host — the URL must contain a host, and that host must resolve. Hostless or unresolvable URLs are rejected.
  • Resolved address — the host is resolved and every resolved IP is checked. The request is blocked if any address is loopback, private, link-local, reserved, multicast, unspecified, or IPv6 site-local. This blocks internal and cloud-metadata targets such as 127.0.0.1, ::1, 169.254.169.254, 10.0.0.0/8, 172.16.0.0/12, and 192.168.0.0/16. IP-literal URLs (e.g. http://[::1]) are validated directly, so DNS-rebinding and IP-literal bypasses are also caught.
To keep public URLs from redirecting the server to an internal address, the crawler’s HTTP client runs with follow_redirects=False — redirects are not followed automatically.A GitHub/GitLab repository URL is fetched server-side too, by git rather than by the HTTP client: add() and remember() shallow-clone it into COGNEE_REPOS_DIR (default ~/.cognee/repos) and index it as a code graph. The clone URL clears the same outbound checks as any other http(s) ingestion, and ALLOW_HTTP_REQUESTS=false blocks it. On a deployment reachable by untrusted callers, size the disk behind COGNEE_REPOS_DIR for the clones it will accumulate — they are kept and reused across calls, not deleted after ingestion.
A blocked or disabled request raises SSRFProtectionError (an HTTP 403 CogneeValidationError) instead of silently fetching the internal target. If you see this during ingestion, check that the URL is a public http(s) address that resolves to a routable IP, and that ALLOW_HTTP_REQUESTS is not set to false.

Encrypting Neo4j Credentials

When using the neo4j_aura_dev or neo4j_community dataset database handler for multi-user mode, Cognee stores per-dataset Neo4j connection info in the relational database — for neo4j_aura_dev the provisioned Aura instance, for neo4j_community the per-dataset Docker container. The stored database password is encrypted with Fernet symmetric encryption; both handlers derive the encryption key from the same NEO4J_ENCRYPTION_KEY:
The default value "test_key" is intentionally insecure. Replace it with a long random string in any environment that stores real Neo4j credentials.
The Aura API credentials used to create or delete instances (NEO4J_CLIENT_ID, NEO4J_CLIENT_SECRET, and NEO4J_TENANT_ID) are read from environment variables when needed and are not stored in the relational database by this handler.

Encrypting Integration Credentials

Third-party OAuth integrations (Slack, GitHub, and Linear) store their tokens in the relational integration_credentials table. The token payload is encrypted with AES-256-GCM under a fresh random 96-bit nonce per write; only non-secret metadata — the workspace label, Slack team and enterprise ids, the bot and installing member’s user ids, the granted OAuth scopes, the allowed-channel list, and (for Slack history import) the saved sync selection and last-run import reports — is stored in the clear.Keys are configured as a keyring — a set of 32-byte keys, each addressed by a short key id — so that keys can be rotated without re-encrypting existing rows:
There is no default key and no derived fallback: if neither variable is set, or a key does not decode to exactly 32 bytes, credential writes fail with a RuntimeError rather than producing ciphertext nobody can decrypt later. Generate a key with:
Rotation. Add the new key to INTEGRATION_CREDENTIALS_KEYS alongside the old one under a new id, then point INTEGRATION_CREDENTIALS_ACTIVE_KEY_ID at it. Every row records the key id it was written with, so existing rows keep decrypting under their original key; reconnecting an integration rewrites its row under the active key. Remove an old key from the ring only once no row still references it.Revocation. Disconnecting an integration marks the stored credential revoked and makes a best-effort call to the provider’s revoke endpoint. Provider-side removal has the same effect: for Slack, an app_uninstalled or tokens_revoked event deactivates the stored installation, so no usable token survives an uninstall.

Dataset & Multi-User Isolation

When enabled, Cognee creates isolated storage per user + dataset combination and enforces permission checks on every read and write operation. This is the primary control for preventing cross-tenant data leakage in multi-user deployments.Database support requirements:If you configure an unsupported backend (e.g., Qdrant, Weaviate), disable access control to avoid runtime errors:
Setting ENABLE_BACKEND_ACCESS_CONTROL=false alone also disables the auth requirement (single-user mode). You only need to add REQUIRE_AUTHENTICATION if you want to override that default — for example, REQUIRE_AUTHENTICATION=true to keep auth on for a single-user deployment behind a shared token.See Dataset Database Handlers for the full list of supported handlers.
When running Cognee locally for development or testing, you can disable authentication so that API calls succeed without a bearer token. Setting the single posture switch is enough:
With ENABLE_BACKEND_ACCESS_CONTROL=false and REQUIRE_AUTHENTICATION unset, REQUIRE_AUTHENTICATION inherits from the posture switch and the auth requirement is turned off automatically. (Previously, both variables had to be set to false independently; that is no longer required.)These values are read once when the server process starts, so you must restart the server after changing them. Check the auth posture: ... line in the startup logs to confirm the resolved decision.When authentication is disabled, unauthenticated requests are automatically served under a built-in default user (default_user@example.com). The relational database must be initialized before the first request so that this default user can be looked up or created.Troubleshooting 401 errors after setting the variables
Only use ENABLE_BACKEND_ACCESS_CONTROL=false in local or trusted environments unless you have another protection layer in place. It disables multi-user isolation, and it also disables the HTTP auth requirement unless you explicitly set REQUIRE_AUTHENTICATION=true.

Permissions Setup

Enable dataset isolation and access control

Multi-User Mode

Understand multi-tenant architecture