Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@ ENABLE_GOLDEN_RECORDS=false
ENABLE_EXTERNAL_CONNECTIONS=true
ALLOW_PRIVATE_DATABASE_HOSTS=false
CONNECTION_ENCRYPTION_KEY=
AUTH_SIGNING_KEY=
DATABASE_CONNECTION_TIMEOUT_SECONDS=5
DATABASE_STATEMENT_TIMEOUT_MS=10000
DATABASE_MAX_RESULT_ROWS=500
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -112,7 +112,7 @@ For the default Compose stack, optional vector features are off so schema intros
| `EMBEDDING_PROVIDER`, `EMBEDDING_MODEL` | Local semantic-retrieval configuration |
| `ENABLE_SCHEMA_RAG`, `ENABLE_GOLDEN_RECORDS` | Optional embedding features |
| `ENABLE_EXTERNAL_CONNECTIONS`, `ALLOW_PRIVATE_DATABASE_HOSTS` | BYOD and SSRF policy flags |
| `CONNECTION_ENCRYPTION_KEY`, `AUTH_SIGNING_KEY` | Backend-only Fernet and session signing secrets |
| `CONNECTION_ENCRYPTION_KEY` | Backend-only Fernet key; session signing is derived from it with domain separation |
| `DATABASE_*` | Connect timeout, statement timeout, SQL length, and result caps |
| `SCHEMA_*_TABLE_LIMIT` | Full-context and retrieval bounds |
| `NEXT_PUBLIC_API_URL` | Browser-visible API `/api/v1` URL |
Expand Down
1 change: 0 additions & 1 deletion apps/api/.env.example
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@ ENABLE_GOLDEN_RECORDS=false
ENABLE_EXTERNAL_CONNECTIONS=false
ALLOW_PRIVATE_DATABASE_HOSTS=false
CONNECTION_ENCRYPTION_KEY=
AUTH_SIGNING_KEY=
DATABASE_CONNECTION_TIMEOUT_SECONDS=5
DATABASE_STATEMENT_TIMEOUT_MS=10000
DATABASE_MAX_RESULT_ROWS=500
Expand Down
2 changes: 1 addition & 1 deletion apps/api/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ uvicorn app.main:app --reload --port 8000

Production startup never creates tables. Alembic creates application tables and pgvector. The demo seed is idempotent. Use `../../scripts/create_readonly_role.sql` as a reviewed template and run query traffic with that role.

Saved customer credentials require `CONNECTION_ENCRYPTION_KEY` (a Fernet key). Signed workspace sessions require a separate `AUTH_SIGNING_KEY`. Generate them with the commands in `../../docs/deployment.md`. BYOD accepts URL or structured PostgreSQL input, requires SSL, blocks unsafe networks by default, and stores normalized schema snapshots without row data.
Saved customer credentials require `CONNECTION_ENCRYPTION_KEY` (a Fernet key). Workspace-session signing uses a domain-separated key derived from that Fernet key, so no second signing secret is required. Generate the key with the command in `../../docs/deployment.md`. BYOD accepts URL or structured PostgreSQL input, requires SSL, blocks unsafe networks by default, and stores normalized schema snapshots without row data.

## Safety model

Expand Down
15 changes: 12 additions & 3 deletions apps/api/app/auth/dependencies.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import base64
import binascii
import hashlib
import hmac
import json
Expand All @@ -13,9 +14,17 @@


def _key() -> bytes:
if not settings.AUTH_SIGNING_KEY:
raise DependencyError("AUTH_SIGNING_KEY is required for saved connection sessions")
return settings.AUTH_SIGNING_KEY.encode()
if not settings.CONNECTION_ENCRYPTION_KEY:
raise DependencyError("CONNECTION_ENCRYPTION_KEY is required for saved connection sessions")
try:
key_material = base64.b64decode(
settings.CONNECTION_ENCRYPTION_KEY.encode(), altchars=b"-_", validate=True
)
except (binascii.Error, ValueError) as exc:
raise DependencyError("CONNECTION_ENCRYPTION_KEY is invalid") from exc
if len(key_material) != 32:
raise DependencyError("CONNECTION_ENCRYPTION_KEY is invalid")
return hmac.new(key_material, b"querymindai/workspace-session/v1", hashlib.sha256).digest()


def issue_session() -> tuple[str, str, int]:
Expand Down
1 change: 0 additions & 1 deletion apps/api/app/core/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,6 @@ class Settings(BaseSettings):
ENABLE_EXTERNAL_CONNECTIONS: bool = False
ALLOW_PRIVATE_DATABASE_HOSTS: bool = False
CONNECTION_ENCRYPTION_KEY: str | None = None
AUTH_SIGNING_KEY: str | None = None
AUTH_SESSION_TTL_SECONDS: int = Field(2592000, ge=300, le=31536000)
DATABASE_CONNECTION_TIMEOUT_SECONDS: int = Field(5, ge=1, le=30)
DATABASE_STATEMENT_TIMEOUT_MS: int = Field(10000, ge=100, le=300000)
Expand Down
11 changes: 9 additions & 2 deletions apps/api/tests/test_auth.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,19 @@
import pytest
from cryptography.fernet import Fernet

from app.auth.dependencies import get_current_user_id, issue_session
from app.core.config import settings
from app.core.exceptions import AuthenticationError
from app.core.exceptions import AuthenticationError, DependencyError


def test_signed_session_subject_cannot_be_forged(monkeypatch):
monkeypatch.setattr(settings,"AUTH_SIGNING_KEY","test-signing-key-not-for-production")
monkeypatch.setattr(settings, "CONNECTION_ENCRYPTION_KEY", Fernet.generate_key().decode())
token,user_id,_=issue_session()
assert get_current_user_id(f"Bearer {token}")==user_id
with pytest.raises(AuthenticationError): get_current_user_id(f"Bearer {token}tampered")


def test_session_requires_connection_encryption_key(monkeypatch):
monkeypatch.setattr(settings, "CONNECTION_ENCRYPTION_KEY", None)
with pytest.raises(DependencyError, match="CONNECTION_ENCRYPTION_KEY"):
issue_session()
1 change: 0 additions & 1 deletion docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,6 @@ services:
ENABLE_EXTERNAL_CONNECTIONS: ${ENABLE_EXTERNAL_CONNECTIONS:-true}
ALLOW_PRIVATE_DATABASE_HOSTS: ${ALLOW_PRIVATE_DATABASE_HOSTS:-false}
CONNECTION_ENCRYPTION_KEY: ${CONNECTION_ENCRYPTION_KEY:-}
AUTH_SIGNING_KEY: ${AUTH_SIGNING_KEY:-}
DATABASE_CONNECTION_TIMEOUT_SECONDS: ${DATABASE_CONNECTION_TIMEOUT_SECONDS:-5}
DATABASE_STATEMENT_TIMEOUT_MS: ${DATABASE_STATEMENT_TIMEOUT_MS:-10000}
DATABASE_MAX_RESULT_ROWS: ${DATABASE_MAX_RESULT_ROWS:-500}
Expand Down
7 changes: 3 additions & 4 deletions docs/deployment.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,16 +4,15 @@

```bash
cp .env.example .env
# Generate backend-only secrets and paste them into .env:
# Generate the backend-only credential encryption key and paste it into .env:
python -c "from cryptography.fernet import Fernet; print(Fernet.generate_key().decode())"
python -c "import secrets; print(secrets.token_urlsafe(48))"
docker compose up --build
# Optional local provider:
docker compose --profile ollama up --build
docker compose exec ollama ollama pull sqlcoder
```

Use the first generated value for `CONNECTION_ENCRYPTION_KEY` and the second for `AUTH_SIGNING_KEY`. Open web at `http://localhost:4028`, API docs at `http://localhost:8000/docs`, and check `/health` then `/ready`. Configure a provider for generation. BYOD databases must be publicly reachable PostgreSQL on port 5432 with SSL and a read-only role.
Use the generated value for `CONNECTION_ENCRYPTION_KEY`. Session signing is derived from this key, so no separate signing secret is needed. Open web at `http://localhost:4028`, API docs at `http://localhost:8000/docs`, and check `/health` then `/ready`. Configure a provider for generation. BYOD databases must be publicly reachable PostgreSQL on port 5432 with SSL and a read-only role.

## Supabase application database

Expand All @@ -33,7 +32,7 @@ Enter the complete value only in Render's secret `DATABASE_URL` field. Never com

1. Push this repository to GitHub. In Render choose **New → Blueprint**, connect that repository, and select `render.yaml`.
2. Review creation or update of `querymind-api` and `querymind-web`. The Blueprint intentionally does not provision a Render PostgreSQL database.
3. Enter backend secrets in the non-synced fields: Supabase session-pooler `DATABASE_URL`, Groq `LLM_API_KEY`, a Fernet `CONNECTION_ENCRYPTION_KEY`, and an independent random `AUTH_SIGNING_KEY`. Never reuse or commit these values.
3. Enter backend secrets in the non-synced fields: Supabase session-pooler `DATABASE_URL`, Groq `LLM_API_KEY`, and a Fernet `CONNECTION_ENCRYPTION_KEY`. Never reuse or commit these values.
4. Set `CORS_ALLOW_ORIGINS` on the API to the final HTTPS web origin, without a trailing slash.
5. Deploy the API. On the free tier, the API start command runs `alembic upgrade head` before starting Uvicorn because Render does not support pre-deploy commands for free services. Confirm `https://<api-host>/health` and `/ready`.
6. Set `NEXT_PUBLIC_API_URL` on `querymind-web` to `https://<api-host>/api/v1`, then trigger a clean frontend deploy. Render does not provide a supported Blueprint interpolation from another web service’s eventual public hostname into a Next.js build variable; this manual build-time step is required.
Expand Down
9 changes: 4 additions & 5 deletions docs/local-development.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,14 +23,13 @@ cd QueryMindAI
cp .env.example .env
```

Generate two independent backend secrets:
Generate the backend credential-encryption key:

```bash
python -c "from cryptography.fernet import Fernet; print(Fernet.generate_key().decode())"
python -c "import secrets; print(secrets.token_urlsafe(48))"
```

Put the first value in `CONNECTION_ENCRYPTION_KEY` and the second in `AUTH_SIGNING_KEY` in your local `.env`. Never commit that file.
Put the value in `CONNECTION_ENCRYPTION_KEY` in your local `.env`. Workspace-session signing is derived from it, so no second signing secret is needed. Never commit that file.

### Option A: local Ollama

Expand Down Expand Up @@ -107,7 +106,7 @@ alembic upgrade head
uvicorn app.main:app --reload --port 8000
```

At minimum, set the application `DATABASE_URL`, `ENABLE_EXTERNAL_CONNECTIONS=true`, and the two generated `CONNECTION_ENCRYPTION_KEY` and `AUTH_SIGNING_KEY` values in `apps/api/.env`. Keep `CORS_ALLOW_ORIGINS=http://localhost:4028` for the default frontend port.
At minimum, set the application `DATABASE_URL`, `ENABLE_EXTERNAL_CONNECTIONS=true`, and the generated `CONNECTION_ENCRYPTION_KEY` value in `apps/api/.env`. Keep `CORS_ALLOW_ORIGINS=http://localhost:4028` for the default frontend port.

In another terminal:

Expand All @@ -123,7 +122,7 @@ The browser API URL must include `/api/v1`. `CORS_ALLOW_ORIGINS` on the API must
## Troubleshooting

- **Connections are disabled:** set `ENABLE_EXTERNAL_CONNECTIONS=true` in the API environment and restart it.
- **Encryption/signing configuration error:** provide valid, different `CONNECTION_ENCRYPTION_KEY` and `AUTH_SIGNING_KEY` values.
- **Encryption/signing configuration error:** provide a valid Fernet `CONNECTION_ENCRYPTION_KEY` value.
- **Host rejected:** public mode rejects localhost, private, link-local, reserved, and metadata IP ranges. Use a public hostname or opt into private hosts only in a trusted local environment.
- **Browser reports a network or CORS error:** verify `NEXT_PUBLIC_API_URL`, `CORS_ALLOW_ORIGINS`, and `/health`. Next.js public variables are embedded at build time, so rebuild the web image after changing the API URL.
- **Generation fails but schema browsing works:** verify the LLM provider URL, model, and backend-only key. Database credentials and LLM keys must never be added to `NEXT_PUBLIC_*` variables.
Expand Down
2 changes: 1 addition & 1 deletion docs/security.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

PostgreSQL credentials are serialized and encrypted with Fernet authenticated encryption using backend-only `CONNECTION_ENCRYPTION_KEY`. Responses contain only host, database, username, SSL mode, and status; passwords, decrypted configuration, and raw URLs are never returned or logged. Deletion cascades catalog snapshots, drafts, and BYOD history and explicitly removes connection-keyed verified examples. Audit records retain only identifiers and safe status metadata.

Application encryption is not an enterprise vault. The entire dataset must be re-encrypted to rotate the key; losing the key makes credentials unrecoverable. Mature deployments should use a managed secret store/KMS. `AUTH_SIGNING_KEY` signs browser-bound anonymous workspace sessions. These sessions are not account authentication, recovery, organization membership, MFA, or revocation infrastructure; use an external identity provider for a true multi-user deployment.
Application encryption is not an enterprise vault. The entire dataset must be re-encrypted to rotate the key; losing the key makes credentials unrecoverable. Mature deployments should use a managed secret store/KMS. Browser-bound anonymous workspace sessions are signed with a domain-separated HMAC key derived from `CONNECTION_ENCRYPTION_KEY`; the derived key is never persisted. Rotating the encryption key therefore also invalidates existing sessions. These sessions are not account authentication, recovery, organization membership, MFA, or revocation infrastructure; use an external identity provider for a true multi-user deployment.

## Network and SSRF controls

Expand Down
2 changes: 0 additions & 2 deletions render.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,6 @@ services:
sync: false
- key: CONNECTION_ENCRYPTION_KEY
sync: false
- key: AUTH_SIGNING_KEY
sync: false
- key: DATABASE_CONNECTION_TIMEOUT_SECONDS
value: "5"
- key: DATABASE_STATEMENT_TIMEOUT_MS
Expand Down
Loading