mirror of
https://github.com/semantica-agi/semantica.git
synced 2026-08-29 04:26:20 +00:00
fix(deploy): harden security in deployment templates and explorer app
- GCP: remove --allow-unauthenticated, restrict ingress to
internal-and-cloud-load-balancing, replace wildcard ALLOWED_ORIGINS=*
with a substitution variable (_ALLOWED_ORIGINS) so operators supply a
real URL at deploy time; same fix in cloudrun-service.yaml
- Fly.io: replace hardcoded FALKORDB_HOST=localhost with the correct
.internal private-network hostname pattern; update README accordingly
- docker-compose.dev.yml: add missing top-level networks: block so the
frontend service can join the semantica network without --file layering
- K8s/Helm: add readOnlyRootFilesystem: true + runAsUser: 1000 to
container securityContext; mount an emptyDir /tmp so uvicorn can write
temp files
- app.py: fix _read_explorer_settings() or-chain, use in os.environ
checks so an explicit ALLOWED_ORIGINS="" produces an empty allow-list
instead of silently falling through to localhost defaults; remove dead
app.state.falkordb_host/port attributes
- docs: update four locations that still documented {"status":"healthy"}
to reflect the new {"status":"ok"} health response
- tests: update test assertion to read falkordb settings from
app.state.explorer_settings instead of removed top-level attributes
This commit is contained in:
@@ -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 <falkordb-app-name> 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=<falkordb-app-name>.internal FALKORDB_PORT=6379
|
||||
flyctl deploy --config deploy/fly/fly.toml
|
||||
```
|
||||
|
||||
|
||||
+3
-1
@@ -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=<your-falkordb-app-name>.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]
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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 }}
|
||||
|
||||
@@ -19,6 +19,8 @@ podSecurityContext:
|
||||
|
||||
securityContext:
|
||||
allowPrivilegeEscalation: false
|
||||
readOnlyRootFilesystem: true
|
||||
runAsUser: 1000
|
||||
capabilities:
|
||||
drop:
|
||||
- ALL
|
||||
|
||||
@@ -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: {}
|
||||
|
||||
@@ -38,5 +38,9 @@ services:
|
||||
networks:
|
||||
- semantica
|
||||
|
||||
networks:
|
||||
semantica:
|
||||
driver: bridge
|
||||
|
||||
volumes:
|
||||
explorer_node_modules:
|
||||
|
||||
+1
-1
@@ -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"}
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
@@ -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 |
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user