diff --git a/.env.example b/.env.example index 842c80a..2450f61 100644 --- a/.env.example +++ b/.env.example @@ -5,6 +5,7 @@ DOMAIN=bracc.example.com # Dev defaults — for production with 40M+ nodes, use: # NEO4J_HEAP_INITIAL=4G NEO4J_HEAP_MAX=8G NEO4J_PAGECACHE=12G # Requires server with 32GB+ RAM (recommended: 64GB) +# Do not use default password in production; set a strong NEO4J_PASSWORD. NEO4J_PASSWORD=changeme NEO4J_URI=bolt://localhost:7687 NEO4J_DATABASE=neo4j @@ -17,9 +18,13 @@ API_HOST=0.0.0.0 API_PORT=8000 LOG_LEVEL=info APP_ENV=dev +# In production use a secret with >= 32 characters (e.g. openssl rand -hex 32). JWT_SECRET_KEY=change-me-generate-with-openssl-rand-hex-32 INVITE_CODE= +# Do not use CORS_ORIGINS=* when using cookies/credentials (allow_credentials). CORS_ORIGINS=http://localhost:3000 +# In production use true so the session cookie is sent only over HTTPS. +AUTH_COOKIE_SECURE=false PRODUCT_TIER=community PATTERNS_ENABLED=false PUBLIC_MODE=false diff --git a/api/src/bracc/main.py b/api/src/bracc/main.py index d9db376..2eb404c 100644 --- a/api/src/bracc/main.py +++ b/api/src/bracc/main.py @@ -43,6 +43,11 @@ async def lifespan(app: FastAPI) -> AsyncIterator[None]: else: _logger.critical(msg) raise RuntimeError(msg) + app_env = settings.app_env.strip().lower() + if app_env not in {"dev", "test"} and settings.neo4j_password == "changeme": + msg = "Neo4j default password not allowed in production — set NEO4J_PASSWORD" + _logger.critical(msg) + raise RuntimeError(msg) driver = await init_driver() app.state.neo4j_driver = driver await ensure_schema(driver) diff --git a/api/src/bracc/routers/auth.py b/api/src/bracc/routers/auth.py index 1b8922f..ee7859e 100644 --- a/api/src/bracc/routers/auth.py +++ b/api/src/bracc/routers/auth.py @@ -48,12 +48,15 @@ async def login( if user is None: raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Invalid credentials") token = auth_service.create_access_token(user.id) + effective_secure = settings.auth_cookie_secure or ( + settings.app_env.strip().lower() == "prod" + ) response.set_cookie( key=settings.auth_cookie_name, value=token, max_age=settings.jwt_expire_minutes * 60, httponly=True, - secure=settings.auth_cookie_secure, + secure=effective_secure, samesite=settings.auth_cookie_samesite, path="/", ) diff --git a/infra/docker-compose.prod.yml b/infra/docker-compose.prod.yml index fa0c000..7c119b7 100644 --- a/infra/docker-compose.prod.yml +++ b/infra/docker-compose.prod.yml @@ -46,6 +46,7 @@ services: JWT_SECRET_KEY: ${JWT_SECRET_KEY} INVITE_CODE: ${INVITE_CODE:-} CORS_ORIGINS: https://${DOMAIN} + AUTH_COOKIE_SECURE: "true" LOG_LEVEL: ${LOG_LEVEL:-info} networks: - bracc