diff --git a/deploy/fly/README.md b/deploy/fly/README.md index c6558c8a..5c5d1258 100644 --- a/deploy/fly/README.md +++ b/deploy/fly/README.md @@ -5,7 +5,10 @@ Deploy from a clean checkout using the root Dockerfile: ```bash flyctl auth login flyctl launch --copy-config --config deploy/fly/fly.toml --no-deploy -flyctl secrets set FALKORDB_HOST=localhost FALKORDB_PORT=6379 +# Replace with your FalkorDB Fly app name. +# Fly.io private networking uses .internal hostnames — do not use localhost +# unless FalkorDB is a co-located process inside the same Machine. +flyctl secrets set FALKORDB_HOST=.internal FALKORDB_PORT=6379 flyctl deploy --config deploy/fly/fly.toml ``` diff --git a/deploy/fly/fly.toml b/deploy/fly/fly.toml index 1e97c65a..a236cbb8 100644 --- a/deploy/fly/fly.toml +++ b/deploy/fly/fly.toml @@ -8,7 +8,9 @@ dockerfile = "Dockerfile" [env] ALLOWED_ORIGINS = "https://semantica-knowledge-explorer.fly.dev" -FALKORDB_HOST = "localhost" +# Set via: flyctl secrets set FALKORDB_HOST=.internal FALKORDB_PORT=6379 +# Do not use localhost unless FalkorDB runs as a co-located process in the same Machine. +FALKORDB_HOST = "falkordb-REPLACE_ME.internal" FALKORDB_PORT = "6379" [http_service] diff --git a/deploy/gcp/README.md b/deploy/gcp/README.md index 59e413c1..572f896d 100644 --- a/deploy/gcp/README.md +++ b/deploy/gcp/README.md @@ -6,7 +6,9 @@ Create the Secret Manager entries, then submit the Cloud Build pipeline: gcloud services enable cloudbuild.googleapis.com run.googleapis.com secretmanager.googleapis.com printf "falkordb-host.example.internal" | gcloud secrets create falkordb-host --data-file=- printf "6379" | gcloud secrets create falkordb-port --data-file=- -gcloud builds submit --config deploy/gcp/cloudbuild.yaml --substitutions _REGION=us-central1,_SERVICE_NAME=knowledge-explorer +# Set _ALLOWED_ORIGINS to your actual service URL after the first deploy. +gcloud builds submit --config deploy/gcp/cloudbuild.yaml \ + --substitutions _REGION=us-central1,_SERVICE_NAME=knowledge-explorer,_ALLOWED_ORIGINS=https://knowledge-explorer-REPLACE_ME.a.run.app ``` For declarative deploys, replace `PROJECT_ID` in `cloudrun-service.yaml`, then run: diff --git a/deploy/gcp/cloudbuild.yaml b/deploy/gcp/cloudbuild.yaml index e6efe572..674d7d2e 100644 --- a/deploy/gcp/cloudbuild.yaml +++ b/deploy/gcp/cloudbuild.yaml @@ -2,6 +2,8 @@ substitutions: _REGION: us-central1 _SERVICE_NAME: knowledge-explorer _IMAGE: gcr.io/$PROJECT_ID/knowledge-explorer + # Set to your actual service URL — do not use '*' in production. + _ALLOWED_ORIGINS: https://knowledge-explorer-REPLACE_ME.a.run.app steps: - name: gcr.io/cloud-builders/docker @@ -35,7 +37,12 @@ steps: - ${_REGION} - --platform - managed - - --allow-unauthenticated + # SECURITY: Remove --allow-unauthenticated and restrict ingress for + # production; add IAP or a load balancer with auth before enabling + # unauthenticated access. See: cloud.google.com/run/docs/authenticating + - --no-allow-unauthenticated + - --ingress + - internal-and-cloud-load-balancing - --port - "8000" - --min-instances @@ -43,7 +50,9 @@ steps: - --max-instances - "10" - --set-env-vars - - ALLOWED_ORIGINS=* + # Replace with your actual Cloud Run service URL after first deploy, + # e.g. ALLOWED_ORIGINS=https://knowledge-explorer-abc123-uc.a.run.app + - ALLOWED_ORIGINS=${_ALLOWED_ORIGINS} - --set-secrets - FALKORDB_HOST=falkordb-host:latest,FALKORDB_PORT=falkordb-port:latest diff --git a/deploy/gcp/cloudrun-service.yaml b/deploy/gcp/cloudrun-service.yaml index ce1c871d..afee9c69 100644 --- a/deploy/gcp/cloudrun-service.yaml +++ b/deploy/gcp/cloudrun-service.yaml @@ -3,7 +3,9 @@ kind: Service metadata: name: knowledge-explorer annotations: - run.googleapis.com/ingress: all + # Use 'internal-and-cloud-load-balancing' or 'internal' in production. + # 'all' permits direct unauthenticated public internet access. + run.googleapis.com/ingress: internal-and-cloud-load-balancing spec: template: metadata: @@ -20,7 +22,8 @@ spec: containerPort: 8000 env: - name: ALLOWED_ORIGINS - value: "*" + # Replace with your actual service URL — do not use '*' in production. + value: "https://knowledge-explorer-REPLACE_ME.a.run.app" - name: FALKORDB_HOST valueFrom: secretKeyRef: diff --git a/deploy/helm/knowledge-explorer/templates/deployment.yaml b/deploy/helm/knowledge-explorer/templates/deployment.yaml index 7015fb23..2df76459 100644 --- a/deploy/helm/knowledge-explorer/templates/deployment.yaml +++ b/deploy/helm/knowledge-explorer/templates/deployment.yaml @@ -75,6 +75,12 @@ spec: failureThreshold: {{ .Values.readinessProbe.failureThreshold }} resources: {{- toYaml .Values.resources | nindent 12 }} + volumeMounts: + - name: tmp + mountPath: /tmp + volumes: + - name: tmp + emptyDir: {} {{- with .Values.nodeSelector }} nodeSelector: {{- toYaml . | nindent 8 }} diff --git a/deploy/helm/knowledge-explorer/values.yaml b/deploy/helm/knowledge-explorer/values.yaml index cc5b4995..ee179986 100644 --- a/deploy/helm/knowledge-explorer/values.yaml +++ b/deploy/helm/knowledge-explorer/values.yaml @@ -19,6 +19,8 @@ podSecurityContext: securityContext: allowPrivilegeEscalation: false + readOnlyRootFilesystem: true + runAsUser: 1000 capabilities: drop: - ALL diff --git a/deploy/kubernetes/deployment.yaml b/deploy/kubernetes/deployment.yaml index 1cdb4730..61c87c34 100644 --- a/deploy/kubernetes/deployment.yaml +++ b/deploy/kubernetes/deployment.yaml @@ -64,6 +64,14 @@ spec: memory: 512Mi securityContext: allowPrivilegeEscalation: false + readOnlyRootFilesystem: true + runAsUser: 1000 capabilities: drop: - ALL + volumeMounts: + - name: tmp + mountPath: /tmp + volumes: + - name: tmp + emptyDir: {} diff --git a/docker-compose.dev.yml b/docker-compose.dev.yml index 5162d6d3..5ff13b8d 100644 --- a/docker-compose.dev.yml +++ b/docker-compose.dev.yml @@ -38,5 +38,9 @@ services: networks: - semantica +networks: + semantica: + driver: bridge + volumes: explorer_node_modules: diff --git a/docs/cli-setup.md b/docs/cli-setup.md index 588ef980..3abe4d90 100644 --- a/docs/cli-setup.md +++ b/docs/cli-setup.md @@ -69,7 +69,7 @@ python -c "import semantica; print(semantica.__version__)" ```bash curl http://localhost:8000/health - # {"status": "healthy"} + # {"status": "ok"} curl http://localhost:8000/api/info # {"name": "Semantica API", "version": "...", "status": "active"} diff --git a/docs/explorer-setup.md b/docs/explorer-setup.md index 6714c084..f023730e 100644 --- a/docs/explorer-setup.md +++ b/docs/explorer-setup.md @@ -56,7 +56,7 @@ The browser opens at `http://127.0.0.1:8000`. The health endpoint confirms the s ```bash curl http://127.0.0.1:8000/api/health -# {"status": "healthy"} +# {"status": "ok"} ``` @@ -174,7 +174,7 @@ Once the server is running: | :--- | :------------ | | `http://127.0.0.1:8000` | Interactive dashboard | | `http://127.0.0.1:8000/docs` | Swagger UI: every REST endpoint, interactive | -| `http://127.0.0.1:8000/api/health` | Health check: `{"status": "healthy"}` | +| `http://127.0.0.1:8000/api/health` | Health check: `{"status": "ok"}` | The browser tab opens shortly after startup. If it does not open, navigate to the URL manually or pass `--no-browser` and open it yourself. diff --git a/docs/reference/explorer.md b/docs/reference/explorer.md index 2bccb30e..578ca316 100644 --- a/docs/reference/explorer.md +++ b/docs/reference/explorer.md @@ -306,7 +306,7 @@ Full interactive docs at `http://localhost:8000/docs`. All endpoints accept and | Endpoint | Method | Description | | :-------- | :------ | :----------- | - | `/api/health` | `GET` | Returns `{"status": "healthy"}` | + | `/api/health` | `GET` | Returns `{"status": "ok"}` | | `/api/info` | `GET` | Server name, version, status | | `/docs` | `GET` | Interactive Swagger UI: all endpoints | diff --git a/semantica/explorer/app.py b/semantica/explorer/app.py index d6a8f888..93c9ec54 100644 --- a/semantica/explorer/app.py +++ b/semantica/explorer/app.py @@ -30,11 +30,12 @@ def _read_int_env(name: str, default: int) -> int: def _read_explorer_settings() -> dict: - raw_origins = ( - os.environ.get("ALLOWED_ORIGINS") - or os.environ.get("EXPLORER_CORS_ORIGINS") - or "http://localhost:5173,http://127.0.0.1:5173" - ) + if "ALLOWED_ORIGINS" in os.environ: + raw_origins = os.environ["ALLOWED_ORIGINS"] + elif "EXPLORER_CORS_ORIGINS" in os.environ: + raw_origins = os.environ["EXPLORER_CORS_ORIGINS"] + else: + raw_origins = "http://localhost:5173,http://127.0.0.1:5173" return { "allowed_origins": [ origin.strip() for origin in raw_origins.split(",") if origin.strip() @@ -88,8 +89,6 @@ def create_app(session: Optional[GraphSession] = None) -> FastAPI: ) app.state.explorer_settings = settings - app.state.falkordb_host = settings["falkordb_host"] - app.state.falkordb_port = settings["falkordb_port"] app.state.allowed_origins = settings["allowed_origins"] # allow_credentials lets browsers send cookies/auth headers cross-origin. diff --git a/tests/explorer/test_explorer_api.py b/tests/explorer/test_explorer_api.py index 16a89f1b..a83022b0 100644 --- a/tests/explorer/test_explorer_api.py +++ b/tests/explorer/test_explorer_api.py @@ -175,8 +175,8 @@ class TestHealthInfo: "https://app.example.com", "https://team.example.com", ] - assert app.state.falkordb_host == "falkordb.internal" - assert app.state.falkordb_port == 6380 + assert app.state.explorer_settings["falkordb_host"] == "falkordb.internal" + assert app.state.explorer_settings["falkordb_port"] == 6380 def test_env_settings_fall_back_to_legacy_cors_name(self, monkeypatch): monkeypatch.delenv("ALLOWED_ORIGINS", raising=False)