From 1933e8041e37dca2a37d09f3a260197ca10ea92c Mon Sep 17 00:00:00 2001 From: Jim Schaff Date: Wed, 1 Jul 2026 12:13:32 -0400 Subject: [PATCH 1/5] feat: add Kubernetes deployment via kustomize + sealed secrets Deploy the three docker-compose services (qdrant, backend, frontend) to Kubernetes, modeled on ../sms-api/kustomize. Structure: - base/ StatefulSet+Service (qdrant) and Deployments+Services (backend :8000, frontend :3000), wired to config/ ConfigMaps and sealed secrets - config/ non-secret config -> backend-config/frontend-config ConfigMaps - overlays/ namespace, image tags, ingress, and the sealed-secret tooling for vcell-ai-rke (prod), vcell-ai-rke-dev (dev), vcell-ai-local (minikube) - scripts/ sealed_secret_{backend,frontend,ghcr}.sh + build_and_push.sh Sealed secrets: each overlay has a master secrets.sh that reads plaintext secrets.dat (from secrets.dat.template) and emits three SealedSecret manifests (backend-secrets, frontend-secrets, ghcr image-pull). secrets.dat and the generated secret-*.yaml are gitignored. Ingress serves frontend at / and proxies /api/* to the backend (prefix stripped via rewrite-target, since FastAPI routes are at the root). Validated with `kubectl kustomize` for all three overlays (13 objects each). Co-Authored-By: Claude Opus 4.8 (1M context) Claude-Session: https://claude.ai/code/session_01XpNzobVi83p3YKL9sqtGwZ --- .gitignore | 11 +- kustomize/README.md | 104 ++++++++++++++++++ kustomize/base/backend.yaml | 81 ++++++++++++++ kustomize/base/frontend.yaml | 71 ++++++++++++ kustomize/base/kustomization.yaml | 7 ++ kustomize/base/qdrant.yaml | 74 +++++++++++++ kustomize/config/vcell-ai-local/backend.env | 27 +++++ kustomize/config/vcell-ai-local/frontend.env | 14 +++ .../config/vcell-ai-local/kustomization.yaml | 10 ++ kustomize/config/vcell-ai-rke-dev/backend.env | 27 +++++ .../config/vcell-ai-rke-dev/frontend.env | 13 +++ .../vcell-ai-rke-dev/kustomization.yaml | 10 ++ kustomize/config/vcell-ai-rke/backend.env | 27 +++++ kustomize/config/vcell-ai-rke/frontend.env | 13 +++ .../config/vcell-ai-rke/kustomization.yaml | 10 ++ .../overlays/vcell-ai-local/ingress.yaml | 46 ++++++++ .../vcell-ai-local/kustomization.yaml | 26 +++++ .../vcell-ai-local/secrets.dat.template | 20 ++++ kustomize/overlays/vcell-ai-local/secrets.sh | 69 ++++++++++++ .../overlays/vcell-ai-rke-dev/ingress.yaml | 59 ++++++++++ .../vcell-ai-rke-dev/kustomization.yaml | 26 +++++ .../vcell-ai-rke-dev/secrets.dat.template | 20 ++++ .../overlays/vcell-ai-rke-dev/secrets.sh | 68 ++++++++++++ kustomize/overlays/vcell-ai-rke/ingress.yaml | 59 ++++++++++ .../overlays/vcell-ai-rke/kustomization.yaml | 26 +++++ .../vcell-ai-rke/secrets.dat.template | 20 ++++ kustomize/overlays/vcell-ai-rke/secrets.sh | 68 ++++++++++++ kustomize/scripts/build_and_push.sh | 42 +++++++ kustomize/scripts/sealed_secret_backend.sh | 65 +++++++++++ kustomize/scripts/sealed_secret_frontend.sh | 59 ++++++++++ kustomize/scripts/sealed_secret_ghcr.sh | 63 +++++++++++ 31 files changed, 1234 insertions(+), 1 deletion(-) create mode 100644 kustomize/README.md create mode 100644 kustomize/base/backend.yaml create mode 100644 kustomize/base/frontend.yaml create mode 100644 kustomize/base/kustomization.yaml create mode 100644 kustomize/base/qdrant.yaml create mode 100644 kustomize/config/vcell-ai-local/backend.env create mode 100644 kustomize/config/vcell-ai-local/frontend.env create mode 100644 kustomize/config/vcell-ai-local/kustomization.yaml create mode 100644 kustomize/config/vcell-ai-rke-dev/backend.env create mode 100644 kustomize/config/vcell-ai-rke-dev/frontend.env create mode 100644 kustomize/config/vcell-ai-rke-dev/kustomization.yaml create mode 100644 kustomize/config/vcell-ai-rke/backend.env create mode 100644 kustomize/config/vcell-ai-rke/frontend.env create mode 100644 kustomize/config/vcell-ai-rke/kustomization.yaml create mode 100644 kustomize/overlays/vcell-ai-local/ingress.yaml create mode 100644 kustomize/overlays/vcell-ai-local/kustomization.yaml create mode 100644 kustomize/overlays/vcell-ai-local/secrets.dat.template create mode 100755 kustomize/overlays/vcell-ai-local/secrets.sh create mode 100644 kustomize/overlays/vcell-ai-rke-dev/ingress.yaml create mode 100644 kustomize/overlays/vcell-ai-rke-dev/kustomization.yaml create mode 100644 kustomize/overlays/vcell-ai-rke-dev/secrets.dat.template create mode 100755 kustomize/overlays/vcell-ai-rke-dev/secrets.sh create mode 100644 kustomize/overlays/vcell-ai-rke/ingress.yaml create mode 100644 kustomize/overlays/vcell-ai-rke/kustomization.yaml create mode 100644 kustomize/overlays/vcell-ai-rke/secrets.dat.template create mode 100755 kustomize/overlays/vcell-ai-rke/secrets.sh create mode 100755 kustomize/scripts/build_and_push.sh create mode 100755 kustomize/scripts/sealed_secret_backend.sh create mode 100755 kustomize/scripts/sealed_secret_frontend.sh create mode 100755 kustomize/scripts/sealed_secret_ghcr.sh diff --git a/.gitignore b/.gitignore index 4d10b73..a4d1ab8 100644 --- a/.gitignore +++ b/.gitignore @@ -10,4 +10,13 @@ backend/app.log backend/Downloads backend/.ipynb_checkpoints backend/.venv -.idea \ No newline at end of file +.idea + +# Kubernetes / kustomize secret material +# secrets.dat holds PLAINTEXT secrets — never commit it. +kustomize/overlays/*/secrets.dat +# Generated SealedSecret manifests (encrypted, but cluster-specific — generated +# per-overlay by secrets.sh; not committed in this repo). +kustomize/overlays/*/secret-backend.yaml +kustomize/overlays/*/secret-frontend.yaml +kustomize/overlays/*/secret-ghcr.yaml \ No newline at end of file diff --git a/kustomize/README.md b/kustomize/README.md new file mode 100644 index 0000000..3189b69 --- /dev/null +++ b/kustomize/README.md @@ -0,0 +1,104 @@ +# VCell-AI — Kubernetes deployment (kustomize + sealed secrets) + +Deploys the three VCell-AI services from `docker-compose.yml` to Kubernetes: + +| Component | Image | Port | Notes | +|-----------|-------|------|-------| +| `qdrant` | `qdrant/qdrant` | 6333 / 6334 | StatefulSet + 10Gi PVC | +| `backend` | `ghcr.io/virtualcell/vcell-ai-backend` | 8000 | FastAPI | +| `frontend` | `ghcr.io/virtualcell/vcell-ai-frontend` | 3000 | Next.js | + +Structured after `../sms-api/kustomize`. + +## Layout + +``` +kustomize/ +├── base/ # Deployments/StatefulSet + Services (env-agnostic) +│ ├── qdrant.yaml +│ ├── backend.yaml +│ ├── frontend.yaml +│ └── kustomization.yaml +├── config// # non-secret config -> ConfigMaps (backend/frontend .env) +│ ├── backend.env +│ ├── frontend.env +│ └── kustomization.yaml +├── overlays// # per-environment: namespace, images, ingress, secrets +│ ├── kustomization.yaml +│ ├── ingress.yaml +│ ├── secrets.sh # master script -> generates the 3 sealed secrets +│ ├── secrets.dat.template # copy to secrets.dat (gitignored) and fill in +│ ├── secrets.dat # (gitignored) plaintext inputs +│ ├── secret-backend.yaml # (gitignored) generated SealedSecret +│ ├── secret-frontend.yaml # (gitignored) generated SealedSecret +│ └── secret-ghcr.yaml # (gitignored) generated SealedSecret +└── scripts/ # sealing + build helpers + ├── sealed_secret_backend.sh + ├── sealed_secret_frontend.sh + ├── sealed_secret_ghcr.sh + └── build_and_push.sh +``` + +Environments (``): `vcell-ai-rke` (prod, on-prem UCHC RKE), `vcell-ai-rke-dev` +(dev), `vcell-ai-local` (minikube). Each overlay's namespace equals its `` name. + +## Secrets model + +Non-secret configuration lives in `config//*.env` (committed). Only truly +sensitive values are sealed: + +- **backend-secrets** → `AZURE_API_KEY`, `LANGFUSE_SECRET_KEY`, `LANGFUSE_PUBLIC_KEY`, `SUPABASE_SERVICE_ROLE_KEY` +- **frontend-secrets** → `AUTH0_SECRET`, `AUTH0_CLIENT_SECRET` +- **ghcr-secret** → ghcr.io image-pull credentials (`.dockerconfigjson`) + +`secrets.dat` (plaintext) and the generated `secret-*.yaml` are **gitignored**. +Run `secrets.sh` to (re)generate the sealed manifests before applying an overlay. + +## One-time cluster setup (Sealed Secrets controller) + +```bash +brew install kubeseal +helm repo add sealed-secrets https://bitnami-labs.github.io/sealed-secrets +helm install sealed-secrets -n kube-system \ + --set-string fullnameOverride=sealed-secrets-controller sealed-secrets/sealed-secrets +``` + +The scripts default to controller `sealed-secrets-controller` in namespace +`kube-system` (override with `--controller-name` / `--controller-namespace`, and +pass `--cert ` for offline sealing on GKE/GovCloud). + +## Deploy an environment + +```bash +cd kustomize/overlays/vcell-ai-rke # or -rke-dev / -local + +# 1. create the namespace +kubectl create namespace vcell-ai-rke + +# 2. provide secret values +cp secrets.dat.template secrets.dat +$EDITOR secrets.dat # fill in real values (gitignored) + +# 3. generate the sealed secrets (needs kubeseal + controller reachable) +./secrets.sh + +# 4. review the rendered manifests +kubectl kustomize . + +# 5. apply +kubectl apply -k . +``` + +Update image tags per environment in `overlays//kustomization.yaml` +(`images[].newTag`). Build & push images with +`scripts/build_and_push.sh virtualcell`. + +## Notes + +- The ingress serves the frontend at `/` and proxies `/api/*` to the backend + (the `/api` prefix is stripped via `rewrite-target`, since FastAPI routes are + at the root, e.g. `/biomodel`, `/kb`, `/query`). +- `NEXT_PUBLIC_*` vars are inlined at Next.js **build** time — set + `NEXT_PUBLIC_API_URL` when building the frontend image, not only at runtime. +- Placeholder values in `config//*.env` (Azure endpoint, Auth0 domain, + Supabase URL, hostnames) must be filled in for your deployment. diff --git a/kustomize/base/backend.yaml b/kustomize/base/backend.yaml new file mode 100644 index 0000000..5a1b31c --- /dev/null +++ b/kustomize/base/backend.yaml @@ -0,0 +1,81 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: backend + labels: + app: backend +spec: + replicas: 1 + selector: + matchLabels: + app: backend + template: + metadata: + labels: + app: backend + spec: + containers: + - name: backend + image: ghcr.io/virtualcell/vcell-ai-backend:latest + imagePullPolicy: Always + ports: + - containerPort: 8000 + # Non-secret configuration (Azure endpoint/deployment names, Qdrant + # URL, Auth0 domain/audience, Langfuse host, ...) comes from the + # backend-config ConfigMap generated per-environment in config/. + envFrom: + - configMapRef: + name: backend-config + # Secret values come from the sealed "backend-secrets" secret, + # generated per-overlay by scripts/sealed_secret_backend.sh. + env: + - name: AZURE_API_KEY + valueFrom: + secretKeyRef: + name: backend-secrets + key: azure-api-key + - name: LANGFUSE_SECRET_KEY + valueFrom: + secretKeyRef: + name: backend-secrets + key: langfuse-secret-key + - name: LANGFUSE_PUBLIC_KEY + valueFrom: + secretKeyRef: + name: backend-secrets + key: langfuse-public-key + - name: SUPABASE_SERVICE_ROLE_KEY + valueFrom: + secretKeyRef: + name: backend-secrets + key: supabase-service-role-key + resources: + requests: + memory: "512Mi" + cpu: "250m" + limits: + memory: "2Gi" + cpu: "1" + readinessProbe: + httpGet: + path: /docs + port: 8000 + initialDelaySeconds: 10 + periodSeconds: 15 + imagePullSecrets: + - name: ghcr-secret + +--- +apiVersion: v1 +kind: Service +metadata: + name: backend + labels: + app: backend +spec: + ports: + - name: "8000" + port: 8000 + targetPort: 8000 + selector: + app: backend diff --git a/kustomize/base/frontend.yaml b/kustomize/base/frontend.yaml new file mode 100644 index 0000000..473042b --- /dev/null +++ b/kustomize/base/frontend.yaml @@ -0,0 +1,71 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: frontend + labels: + app: frontend +spec: + replicas: 1 + selector: + matchLabels: + app: frontend + template: + metadata: + labels: + app: frontend + spec: + containers: + - name: frontend + image: ghcr.io/virtualcell/vcell-ai-frontend:latest + imagePullPolicy: Always + ports: + - containerPort: 3000 + # Non-secret configuration (NEXT_PUBLIC_API_URL, APP_BASE_URL, + # AUTH0_DOMAIN, AUTH0_CLIENT_ID, AUTH0_AUDIENCE) comes from the + # frontend-config ConfigMap generated per-environment in config/. + envFrom: + - configMapRef: + name: frontend-config + # Secret values come from the sealed "frontend-secrets" secret, + # generated per-overlay by scripts/sealed_secret_frontend.sh. + env: + - name: AUTH0_SECRET + valueFrom: + secretKeyRef: + name: frontend-secrets + key: auth0-secret + - name: AUTH0_CLIENT_SECRET + valueFrom: + secretKeyRef: + name: frontend-secrets + key: auth0-client-secret + resources: + requests: + memory: "256Mi" + cpu: "100m" + limits: + memory: "1Gi" + cpu: "500m" + readinessProbe: + httpGet: + path: / + port: 3000 + initialDelaySeconds: 10 + periodSeconds: 15 + imagePullSecrets: + - name: ghcr-secret + +--- +apiVersion: v1 +kind: Service +metadata: + name: frontend + labels: + app: frontend +spec: + ports: + - name: "3000" + port: 3000 + targetPort: 3000 + selector: + app: frontend diff --git a/kustomize/base/kustomization.yaml b/kustomize/base/kustomization.yaml new file mode 100644 index 0000000..3db2db9 --- /dev/null +++ b/kustomize/base/kustomization.yaml @@ -0,0 +1,7 @@ +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization + +resources: + - qdrant.yaml + - backend.yaml + - frontend.yaml diff --git a/kustomize/base/qdrant.yaml b/kustomize/base/qdrant.yaml new file mode 100644 index 0000000..ee87c26 --- /dev/null +++ b/kustomize/base/qdrant.yaml @@ -0,0 +1,74 @@ +apiVersion: apps/v1 +kind: StatefulSet +metadata: + name: qdrant + labels: + app: qdrant +spec: + serviceName: qdrant + replicas: 1 + selector: + matchLabels: + app: qdrant + template: + metadata: + labels: + app: qdrant + spec: + containers: + - name: qdrant + image: qdrant/qdrant:latest + imagePullPolicy: IfNotPresent + ports: + - name: http + containerPort: 6333 + - name: grpc + containerPort: 6334 + volumeMounts: + - name: qdrant-storage + mountPath: /qdrant/storage + resources: + requests: + memory: "512Mi" + cpu: "250m" + limits: + memory: "2Gi" + cpu: "1" + readinessProbe: + httpGet: + path: /readyz + port: 6333 + initialDelaySeconds: 5 + periodSeconds: 10 + livenessProbe: + httpGet: + path: /livez + port: 6333 + initialDelaySeconds: 10 + periodSeconds: 20 + volumeClaimTemplates: + - metadata: + name: qdrant-storage + spec: + accessModes: ["ReadWriteOnce"] + resources: + requests: + storage: 10Gi + +--- +apiVersion: v1 +kind: Service +metadata: + name: qdrant + labels: + app: qdrant +spec: + ports: + - name: http + port: 6333 + targetPort: 6333 + - name: grpc + port: 6334 + targetPort: 6334 + selector: + app: qdrant diff --git a/kustomize/config/vcell-ai-local/backend.env b/kustomize/config/vcell-ai-local/backend.env new file mode 100644 index 0000000..fa78aa8 --- /dev/null +++ b/kustomize/config/vcell-ai-local/backend.env @@ -0,0 +1,27 @@ +# Non-secret backend configuration for the vcell-ai-local (minikube) environment. +# Secret values (AZURE_API_KEY, LANGFUSE_*_KEY, SUPABASE_SERVICE_ROLE_KEY) are +# injected from the sealed "backend-secrets" secret, not from this file. + +# LLM provider +PROVIDER=azure +AZURE_API_VERSION=2024-02-01 +AZURE_ENDPOINT=https://your-azure-openai.openai.azure.com +AZURE_DEPLOYMENT_NAME=gpt-4o +AZURE_EMBEDDING_DEPLOYMENT_NAME=text-embedding-3-large + +# Qdrant (in-cluster StatefulSet service) +QDRANT_URL=http://qdrant:6333 +QDRANT_COLLECTION_NAME=vcell_kb_local + +# Langfuse (host only; keys are secret) +LANGFUSE_HOST=https://cloud.langfuse.com + +# Auth0 (domain/audience are public; client/session secrets are on the frontend) +AUTH0_DOMAIN=your-tenant.us.auth0.com +AUTH0_AUDIENCE=https://vcell-ai-api + +# Supabase (URL is public; service-role key is secret) +SUPABASE_URL=https://your-project.supabase.co + +# CORS / frontend origin (add "127.0.0.1 vcell-ai.local" to /etc/hosts for minikube) +FRONTEND_URL=http://vcell-ai.local diff --git a/kustomize/config/vcell-ai-local/frontend.env b/kustomize/config/vcell-ai-local/frontend.env new file mode 100644 index 0000000..6616210 --- /dev/null +++ b/kustomize/config/vcell-ai-local/frontend.env @@ -0,0 +1,14 @@ +# Non-secret frontend configuration for the vcell-ai-local (minikube) environment. +# Secret values (AUTH0_SECRET, AUTH0_CLIENT_SECRET) are injected from the sealed +# "frontend-secrets" secret, not from this file. +# +# NOTE: NEXT_PUBLIC_* vars are inlined at Next.js *build* time. For them to take +# effect they must be set when the frontend image is built (see build_and_push.sh); +# the value here is used for SSR and as documentation of the expected origin. +# Add "127.0.0.1 vcell-ai.local" to /etc/hosts and run `minikube tunnel`. + +NEXT_PUBLIC_API_URL=http://vcell-ai.local/api +APP_BASE_URL=http://vcell-ai.local +AUTH0_DOMAIN=your-tenant.us.auth0.com +AUTH0_CLIENT_ID=your_auth0_client_id +AUTH0_AUDIENCE=https://vcell-ai-api diff --git a/kustomize/config/vcell-ai-local/kustomization.yaml b/kustomize/config/vcell-ai-local/kustomization.yaml new file mode 100644 index 0000000..3b0b7b5 --- /dev/null +++ b/kustomize/config/vcell-ai-local/kustomization.yaml @@ -0,0 +1,10 @@ +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization + +configMapGenerator: + - name: backend-config + envs: + - backend.env + - name: frontend-config + envs: + - frontend.env diff --git a/kustomize/config/vcell-ai-rke-dev/backend.env b/kustomize/config/vcell-ai-rke-dev/backend.env new file mode 100644 index 0000000..6ef6e09 --- /dev/null +++ b/kustomize/config/vcell-ai-rke-dev/backend.env @@ -0,0 +1,27 @@ +# Non-secret backend configuration for the vcell-ai-rke-dev (dev) environment. +# Secret values (AZURE_API_KEY, LANGFUSE_*_KEY, SUPABASE_SERVICE_ROLE_KEY) are +# injected from the sealed "backend-secrets" secret, not from this file. + +# LLM provider +PROVIDER=azure +AZURE_API_VERSION=2024-02-01 +AZURE_ENDPOINT=https://your-azure-openai.openai.azure.com +AZURE_DEPLOYMENT_NAME=gpt-4o +AZURE_EMBEDDING_DEPLOYMENT_NAME=text-embedding-3-large + +# Qdrant (in-cluster StatefulSet service) +QDRANT_URL=http://qdrant:6333 +QDRANT_COLLECTION_NAME=vcell_kb_dev + +# Langfuse (host only; keys are secret) +LANGFUSE_HOST=https://cloud.langfuse.com + +# Auth0 (domain/audience are public; client/session secrets are on the frontend) +AUTH0_DOMAIN=your-tenant.us.auth0.com +AUTH0_AUDIENCE=https://vcell-ai-api + +# Supabase (URL is public; service-role key is secret) +SUPABASE_URL=https://your-project.supabase.co + +# CORS / frontend origin +FRONTEND_URL=https://vcell-ai-dev.cam.uchc.edu diff --git a/kustomize/config/vcell-ai-rke-dev/frontend.env b/kustomize/config/vcell-ai-rke-dev/frontend.env new file mode 100644 index 0000000..d8afc3c --- /dev/null +++ b/kustomize/config/vcell-ai-rke-dev/frontend.env @@ -0,0 +1,13 @@ +# Non-secret frontend configuration for the vcell-ai-rke-dev (dev) environment. +# Secret values (AUTH0_SECRET, AUTH0_CLIENT_SECRET) are injected from the sealed +# "frontend-secrets" secret, not from this file. +# +# NOTE: NEXT_PUBLIC_* vars are inlined at Next.js *build* time. For them to take +# effect they must be set when the frontend image is built (see build_and_push.sh); +# the value here is used for SSR and as documentation of the expected origin. + +NEXT_PUBLIC_API_URL=https://vcell-ai-dev.cam.uchc.edu/api +APP_BASE_URL=https://vcell-ai-dev.cam.uchc.edu +AUTH0_DOMAIN=your-tenant.us.auth0.com +AUTH0_CLIENT_ID=your_auth0_client_id +AUTH0_AUDIENCE=https://vcell-ai-api diff --git a/kustomize/config/vcell-ai-rke-dev/kustomization.yaml b/kustomize/config/vcell-ai-rke-dev/kustomization.yaml new file mode 100644 index 0000000..3b0b7b5 --- /dev/null +++ b/kustomize/config/vcell-ai-rke-dev/kustomization.yaml @@ -0,0 +1,10 @@ +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization + +configMapGenerator: + - name: backend-config + envs: + - backend.env + - name: frontend-config + envs: + - frontend.env diff --git a/kustomize/config/vcell-ai-rke/backend.env b/kustomize/config/vcell-ai-rke/backend.env new file mode 100644 index 0000000..d7d5488 --- /dev/null +++ b/kustomize/config/vcell-ai-rke/backend.env @@ -0,0 +1,27 @@ +# Non-secret backend configuration for the vcell-ai-rke (prod) environment. +# Secret values (AZURE_API_KEY, LANGFUSE_*_KEY, SUPABASE_SERVICE_ROLE_KEY) are +# injected from the sealed "backend-secrets" secret, not from this file. + +# LLM provider +PROVIDER=azure +AZURE_API_VERSION=2024-02-01 +AZURE_ENDPOINT=https://your-azure-openai.openai.azure.com +AZURE_DEPLOYMENT_NAME=gpt-4o +AZURE_EMBEDDING_DEPLOYMENT_NAME=text-embedding-3-large + +# Qdrant (in-cluster StatefulSet service) +QDRANT_URL=http://qdrant:6333 +QDRANT_COLLECTION_NAME=vcell_kb + +# Langfuse (host only; keys are secret) +LANGFUSE_HOST=https://cloud.langfuse.com + +# Auth0 (domain/audience are public; client/session secrets are on the frontend) +AUTH0_DOMAIN=your-tenant.us.auth0.com +AUTH0_AUDIENCE=https://vcell-ai-api + +# Supabase (URL is public; service-role key is secret) +SUPABASE_URL=https://your-project.supabase.co + +# CORS / frontend origin +FRONTEND_URL=https://vcell-ai.cam.uchc.edu diff --git a/kustomize/config/vcell-ai-rke/frontend.env b/kustomize/config/vcell-ai-rke/frontend.env new file mode 100644 index 0000000..6a5ac29 --- /dev/null +++ b/kustomize/config/vcell-ai-rke/frontend.env @@ -0,0 +1,13 @@ +# Non-secret frontend configuration for the vcell-ai-rke (prod) environment. +# Secret values (AUTH0_SECRET, AUTH0_CLIENT_SECRET) are injected from the sealed +# "frontend-secrets" secret, not from this file. +# +# NOTE: NEXT_PUBLIC_* vars are inlined at Next.js *build* time. For them to take +# effect they must be set when the frontend image is built (see build_and_push.sh); +# the value here is used for SSR and as documentation of the expected origin. + +NEXT_PUBLIC_API_URL=https://vcell-ai.cam.uchc.edu/api +APP_BASE_URL=https://vcell-ai.cam.uchc.edu +AUTH0_DOMAIN=your-tenant.us.auth0.com +AUTH0_CLIENT_ID=your_auth0_client_id +AUTH0_AUDIENCE=https://vcell-ai-api diff --git a/kustomize/config/vcell-ai-rke/kustomization.yaml b/kustomize/config/vcell-ai-rke/kustomization.yaml new file mode 100644 index 0000000..3b0b7b5 --- /dev/null +++ b/kustomize/config/vcell-ai-rke/kustomization.yaml @@ -0,0 +1,10 @@ +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization + +configMapGenerator: + - name: backend-config + envs: + - backend.env + - name: frontend-config + envs: + - frontend.env diff --git a/kustomize/overlays/vcell-ai-local/ingress.yaml b/kustomize/overlays/vcell-ai-local/ingress.yaml new file mode 100644 index 0000000..fd88991 --- /dev/null +++ b/kustomize/overlays/vcell-ai-local/ingress.yaml @@ -0,0 +1,46 @@ +# Minikube ingress (nginx addon), plain HTTP on host vcell-ai.local. +# Add "127.0.0.1 vcell-ai.local" to /etc/hosts and run `minikube tunnel`. +# /api/* -> backend:8000 (the /api prefix is stripped via rewrite-target) +# /* -> frontend:3000 +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: backend-ingress + labels: + app: backend-ingress + annotations: + nginx.ingress.kubernetes.io/rewrite-target: /$2 + nginx.ingress.kubernetes.io/proxy-body-size: "50m" +spec: + ingressClassName: nginx + rules: + - host: vcell-ai.local + http: + paths: + - path: /api(/|$)(.*) + pathType: ImplementationSpecific + backend: + service: + name: backend + port: + number: 8000 +--- +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: frontend-ingress + labels: + app: frontend-ingress +spec: + ingressClassName: nginx + rules: + - host: vcell-ai.local + http: + paths: + - path: / + pathType: Prefix + backend: + service: + name: frontend + port: + number: 3000 diff --git a/kustomize/overlays/vcell-ai-local/kustomization.yaml b/kustomize/overlays/vcell-ai-local/kustomization.yaml new file mode 100644 index 0000000..ac64527 --- /dev/null +++ b/kustomize/overlays/vcell-ai-local/kustomization.yaml @@ -0,0 +1,26 @@ +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization + +namespace: vcell-ai-local + +images: + - name: ghcr.io/virtualcell/vcell-ai-backend + newTag: latest + - name: ghcr.io/virtualcell/vcell-ai-frontend + newTag: latest + +replicas: + - count: 1 + name: backend + - count: 1 + name: frontend + +resources: + - ingress.yaml + # Sealed secrets — generated by ./secrets.sh (see README). Gitignored; run + # secrets.sh before `kubectl apply -k` so these exist. + - secret-backend.yaml + - secret-frontend.yaml + - secret-ghcr.yaml + - ../../config/vcell-ai-local + - ../../base diff --git a/kustomize/overlays/vcell-ai-local/secrets.dat.template b/kustomize/overlays/vcell-ai-local/secrets.dat.template new file mode 100644 index 0000000..19c272b --- /dev/null +++ b/kustomize/overlays/vcell-ai-local/secrets.dat.template @@ -0,0 +1,20 @@ +# Template for secrets.dat (vcell-ai-local / minikube) +# Copy this file to secrets.dat and fill in your actual values. +# DO NOT commit secrets.dat to git — it holds plaintext secrets. +# cp secrets.dat.template secrets.dat && $EDITOR secrets.dat + +# --- Backend (backend-secrets) --- +AZURE_API_KEY=your_azure_openai_api_key +LANGFUSE_SECRET_KEY=sk-lf-your-langfuse-secret-key +LANGFUSE_PUBLIC_KEY=pk-lf-your-langfuse-public-key +SUPABASE_SERVICE_ROLE_KEY=your_supabase_service_role_key + +# --- Frontend (frontend-secrets) --- +# AUTH0_SECRET: 32-byte random string, e.g. `openssl rand -hex 32` +AUTH0_SECRET=your_auth0_session_secret +AUTH0_CLIENT_SECRET=your_auth0_client_secret + +# --- GitHub Container Registry (ghcr-secret, image pull) --- +GH_USER_NAME=your_github_username +GH_USER_EMAIL=your_email@example.com +GH_PAT=ghp_YOURPERSONALACCESSTOKEN diff --git a/kustomize/overlays/vcell-ai-local/secrets.sh b/kustomize/overlays/vcell-ai-local/secrets.sh new file mode 100755 index 0000000..2882b83 --- /dev/null +++ b/kustomize/overlays/vcell-ai-local/secrets.sh @@ -0,0 +1,69 @@ +#!/usr/bin/env bash + +set -eu # Exit on error + +# Master script: reads plaintext values from secrets.dat (gitignored) and writes +# the three sealed secret manifests this overlay references: +# secret-backend.yaml (backend-secrets) +# secret-frontend.yaml (frontend-secrets) +# secret-ghcr.yaml (ghcr-secret, image pull) + +# Get the directory where this script is located and calculate repo root +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +REPO_ROOT="$(cd "${SCRIPT_DIR}/../../../" && pwd)" + +# Set paths relative to the repository root +NAMESPACE=vcell-ai-local +SCRIPTS_DIR="${REPO_ROOT}/kustomize/scripts" +SECRETS_DIR="${REPO_ROOT}/kustomize/overlays/${NAMESPACE}" + +# Sealed secrets controller configuration (minikube install into kube-system, +# see README — `helm install sealed-secrets -n kube-system ...`) +SEALED_SECRETS_CONTROLLER_NAME=sealed-secrets-controller +SEALED_SECRETS_CONTROLLER_NAMESPACE=kube-system + +# Load secrets from data file (not committed to git) +SECRETS_DATA_FILE="${SECRETS_DIR}/secrets.dat" +if [ ! -f "$SECRETS_DATA_FILE" ]; then + echo "ERROR: Secrets data file not found: $SECRETS_DATA_FILE" + echo "Please create it from secrets.dat.template" + echo " cp ${SECRETS_DIR}/secrets.dat.template ${SECRETS_DIR}/secrets.dat" + echo " # Then edit secrets.dat with your actual values" + exit 1 +fi + +echo "Loading secrets from: $SECRETS_DATA_FILE" +source "$SECRETS_DATA_FILE" + +echo "" +echo "=== Generating Sealed Secrets for namespace ${NAMESPACE} ===" + +# backend-secrets: +echo "Generating backend secrets..." +${SCRIPTS_DIR}/sealed_secret_backend.sh \ + --controller-name ${SEALED_SECRETS_CONTROLLER_NAME} \ + --controller-namespace ${SEALED_SECRETS_CONTROLLER_NAMESPACE} \ + ${NAMESPACE} "${AZURE_API_KEY}" "${LANGFUSE_SECRET_KEY}" "${LANGFUSE_PUBLIC_KEY}" "${SUPABASE_SERVICE_ROLE_KEY}" \ + > ${SECRETS_DIR}/secret-backend.yaml +echo "✓ secret-backend.yaml generated" + +# frontend-secrets: +echo "Generating frontend secrets..." +${SCRIPTS_DIR}/sealed_secret_frontend.sh \ + --controller-name ${SEALED_SECRETS_CONTROLLER_NAME} \ + --controller-namespace ${SEALED_SECRETS_CONTROLLER_NAMESPACE} \ + ${NAMESPACE} "${AUTH0_SECRET}" "${AUTH0_CLIENT_SECRET}" \ + > ${SECRETS_DIR}/secret-frontend.yaml +echo "✓ secret-frontend.yaml generated" + +# ghcr-secret: +echo "Generating GHCR image-pull secret..." +${SCRIPTS_DIR}/sealed_secret_ghcr.sh \ + --controller-name ${SEALED_SECRETS_CONTROLLER_NAME} \ + --controller-namespace ${SEALED_SECRETS_CONTROLLER_NAMESPACE} \ + ${NAMESPACE} ${GH_USER_NAME} ${GH_USER_EMAIL} ${GH_PAT} \ + > ${SECRETS_DIR}/secret-ghcr.yaml +echo "✓ secret-ghcr.yaml generated" + +echo "" +echo "=== All sealed secrets generated successfully! ===" diff --git a/kustomize/overlays/vcell-ai-rke-dev/ingress.yaml b/kustomize/overlays/vcell-ai-rke-dev/ingress.yaml new file mode 100644 index 0000000..6b10c38 --- /dev/null +++ b/kustomize/overlays/vcell-ai-rke-dev/ingress.yaml @@ -0,0 +1,59 @@ +# Two Ingress objects share the host vcell-ai-dev.cam.uchc.edu: +# /api/* -> backend:8000 (the /api prefix is stripped via rewrite-target, +# because the FastAPI routes are served at the root, e.g. /biomodel) +# /* -> frontend:3000 (Next.js app, incl. /auth/* Auth0 routes) +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: backend-ingress + labels: + app: backend-ingress + annotations: + cert-manager.io/cluster-issuer: "letsencrypt-prod" + nginx.ingress.kubernetes.io/rewrite-target: /$2 + nginx.ingress.kubernetes.io/proxy-read-timeout: "3600" + nginx.ingress.kubernetes.io/proxy-send-timeout: "3600" + nginx.ingress.kubernetes.io/proxy-body-size: "50m" +spec: + ingressClassName: nginx + tls: + - hosts: + - vcell-ai-dev.cam.uchc.edu + secretName: letsencrypt-prod-vcell-ai-dev-tls + rules: + - host: vcell-ai-dev.cam.uchc.edu + http: + paths: + - path: /api(/|$)(.*) + pathType: ImplementationSpecific + backend: + service: + name: backend + port: + number: 8000 +--- +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: frontend-ingress + labels: + app: frontend-ingress + annotations: + cert-manager.io/cluster-issuer: "letsencrypt-prod" +spec: + ingressClassName: nginx + tls: + - hosts: + - vcell-ai-dev.cam.uchc.edu + secretName: letsencrypt-prod-vcell-ai-dev-tls + rules: + - host: vcell-ai-dev.cam.uchc.edu + http: + paths: + - path: / + pathType: Prefix + backend: + service: + name: frontend + port: + number: 3000 diff --git a/kustomize/overlays/vcell-ai-rke-dev/kustomization.yaml b/kustomize/overlays/vcell-ai-rke-dev/kustomization.yaml new file mode 100644 index 0000000..e01616a --- /dev/null +++ b/kustomize/overlays/vcell-ai-rke-dev/kustomization.yaml @@ -0,0 +1,26 @@ +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization + +namespace: vcell-ai-rke-dev + +images: + - name: ghcr.io/virtualcell/vcell-ai-backend + newTag: latest + - name: ghcr.io/virtualcell/vcell-ai-frontend + newTag: latest + +replicas: + - count: 1 + name: backend + - count: 1 + name: frontend + +resources: + - ingress.yaml + # Sealed secrets — generated by ./secrets.sh (see README). Gitignored; run + # secrets.sh before `kubectl apply -k` so these exist. + - secret-backend.yaml + - secret-frontend.yaml + - secret-ghcr.yaml + - ../../config/vcell-ai-rke-dev + - ../../base diff --git a/kustomize/overlays/vcell-ai-rke-dev/secrets.dat.template b/kustomize/overlays/vcell-ai-rke-dev/secrets.dat.template new file mode 100644 index 0000000..4f51806 --- /dev/null +++ b/kustomize/overlays/vcell-ai-rke-dev/secrets.dat.template @@ -0,0 +1,20 @@ +# Template for secrets.dat (vcell-ai-rke-dev / dev) +# Copy this file to secrets.dat and fill in your actual values. +# DO NOT commit secrets.dat to git — it holds plaintext secrets. +# cp secrets.dat.template secrets.dat && $EDITOR secrets.dat + +# --- Backend (backend-secrets) --- +AZURE_API_KEY=your_azure_openai_api_key +LANGFUSE_SECRET_KEY=sk-lf-your-langfuse-secret-key +LANGFUSE_PUBLIC_KEY=pk-lf-your-langfuse-public-key +SUPABASE_SERVICE_ROLE_KEY=your_supabase_service_role_key + +# --- Frontend (frontend-secrets) --- +# AUTH0_SECRET: 32-byte random string, e.g. `openssl rand -hex 32` +AUTH0_SECRET=your_auth0_session_secret +AUTH0_CLIENT_SECRET=your_auth0_client_secret + +# --- GitHub Container Registry (ghcr-secret, image pull) --- +GH_USER_NAME=your_github_username +GH_USER_EMAIL=your_email@example.com +GH_PAT=ghp_YOURPERSONALACCESSTOKEN diff --git a/kustomize/overlays/vcell-ai-rke-dev/secrets.sh b/kustomize/overlays/vcell-ai-rke-dev/secrets.sh new file mode 100755 index 0000000..31f8138 --- /dev/null +++ b/kustomize/overlays/vcell-ai-rke-dev/secrets.sh @@ -0,0 +1,68 @@ +#!/usr/bin/env bash + +set -eu # Exit on error + +# Master script: reads plaintext values from secrets.dat (gitignored) and writes +# the three sealed secret manifests this overlay references: +# secret-backend.yaml (backend-secrets) +# secret-frontend.yaml (frontend-secrets) +# secret-ghcr.yaml (ghcr-secret, image pull) + +# Get the directory where this script is located and calculate repo root +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +REPO_ROOT="$(cd "${SCRIPT_DIR}/../../../" && pwd)" + +# Set paths relative to the repository root +NAMESPACE=vcell-ai-rke-dev +SCRIPTS_DIR="${REPO_ROOT}/kustomize/scripts" +SECRETS_DIR="${REPO_ROOT}/kustomize/overlays/${NAMESPACE}" + +# Sealed secrets controller configuration (on-premise RKE cluster) +SEALED_SECRETS_CONTROLLER_NAME=sealed-secrets-controller +SEALED_SECRETS_CONTROLLER_NAMESPACE=kube-system + +# Load secrets from data file (not committed to git) +SECRETS_DATA_FILE="${SECRETS_DIR}/secrets.dat" +if [ ! -f "$SECRETS_DATA_FILE" ]; then + echo "ERROR: Secrets data file not found: $SECRETS_DATA_FILE" + echo "Please create it from secrets.dat.template" + echo " cp ${SECRETS_DIR}/secrets.dat.template ${SECRETS_DIR}/secrets.dat" + echo " # Then edit secrets.dat with your actual values" + exit 1 +fi + +echo "Loading secrets from: $SECRETS_DATA_FILE" +source "$SECRETS_DATA_FILE" + +echo "" +echo "=== Generating Sealed Secrets for namespace ${NAMESPACE} ===" + +# backend-secrets: +echo "Generating backend secrets..." +${SCRIPTS_DIR}/sealed_secret_backend.sh \ + --controller-name ${SEALED_SECRETS_CONTROLLER_NAME} \ + --controller-namespace ${SEALED_SECRETS_CONTROLLER_NAMESPACE} \ + ${NAMESPACE} "${AZURE_API_KEY}" "${LANGFUSE_SECRET_KEY}" "${LANGFUSE_PUBLIC_KEY}" "${SUPABASE_SERVICE_ROLE_KEY}" \ + > ${SECRETS_DIR}/secret-backend.yaml +echo "✓ secret-backend.yaml generated" + +# frontend-secrets: +echo "Generating frontend secrets..." +${SCRIPTS_DIR}/sealed_secret_frontend.sh \ + --controller-name ${SEALED_SECRETS_CONTROLLER_NAME} \ + --controller-namespace ${SEALED_SECRETS_CONTROLLER_NAMESPACE} \ + ${NAMESPACE} "${AUTH0_SECRET}" "${AUTH0_CLIENT_SECRET}" \ + > ${SECRETS_DIR}/secret-frontend.yaml +echo "✓ secret-frontend.yaml generated" + +# ghcr-secret: +echo "Generating GHCR image-pull secret..." +${SCRIPTS_DIR}/sealed_secret_ghcr.sh \ + --controller-name ${SEALED_SECRETS_CONTROLLER_NAME} \ + --controller-namespace ${SEALED_SECRETS_CONTROLLER_NAMESPACE} \ + ${NAMESPACE} ${GH_USER_NAME} ${GH_USER_EMAIL} ${GH_PAT} \ + > ${SECRETS_DIR}/secret-ghcr.yaml +echo "✓ secret-ghcr.yaml generated" + +echo "" +echo "=== All sealed secrets generated successfully! ===" diff --git a/kustomize/overlays/vcell-ai-rke/ingress.yaml b/kustomize/overlays/vcell-ai-rke/ingress.yaml new file mode 100644 index 0000000..c21aad7 --- /dev/null +++ b/kustomize/overlays/vcell-ai-rke/ingress.yaml @@ -0,0 +1,59 @@ +# Two Ingress objects share the host vcell-ai.cam.uchc.edu: +# /api/* -> backend:8000 (the /api prefix is stripped via rewrite-target, +# because the FastAPI routes are served at the root, e.g. /biomodel) +# /* -> frontend:3000 (Next.js app, incl. /auth/* Auth0 routes) +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: backend-ingress + labels: + app: backend-ingress + annotations: + cert-manager.io/cluster-issuer: "letsencrypt-prod" + nginx.ingress.kubernetes.io/rewrite-target: /$2 + nginx.ingress.kubernetes.io/proxy-read-timeout: "3600" + nginx.ingress.kubernetes.io/proxy-send-timeout: "3600" + nginx.ingress.kubernetes.io/proxy-body-size: "50m" +spec: + ingressClassName: nginx + tls: + - hosts: + - vcell-ai.cam.uchc.edu + secretName: letsencrypt-prod-vcell-ai-tls + rules: + - host: vcell-ai.cam.uchc.edu + http: + paths: + - path: /api(/|$)(.*) + pathType: ImplementationSpecific + backend: + service: + name: backend + port: + number: 8000 +--- +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: frontend-ingress + labels: + app: frontend-ingress + annotations: + cert-manager.io/cluster-issuer: "letsencrypt-prod" +spec: + ingressClassName: nginx + tls: + - hosts: + - vcell-ai.cam.uchc.edu + secretName: letsencrypt-prod-vcell-ai-tls + rules: + - host: vcell-ai.cam.uchc.edu + http: + paths: + - path: / + pathType: Prefix + backend: + service: + name: frontend + port: + number: 3000 diff --git a/kustomize/overlays/vcell-ai-rke/kustomization.yaml b/kustomize/overlays/vcell-ai-rke/kustomization.yaml new file mode 100644 index 0000000..66fe351 --- /dev/null +++ b/kustomize/overlays/vcell-ai-rke/kustomization.yaml @@ -0,0 +1,26 @@ +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization + +namespace: vcell-ai-rke + +images: + - name: ghcr.io/virtualcell/vcell-ai-backend + newTag: latest + - name: ghcr.io/virtualcell/vcell-ai-frontend + newTag: latest + +replicas: + - count: 1 + name: backend + - count: 1 + name: frontend + +resources: + - ingress.yaml + # Sealed secrets — generated by ./secrets.sh (see README). Gitignored; run + # secrets.sh before `kubectl apply -k` so these exist. + - secret-backend.yaml + - secret-frontend.yaml + - secret-ghcr.yaml + - ../../config/vcell-ai-rke + - ../../base diff --git a/kustomize/overlays/vcell-ai-rke/secrets.dat.template b/kustomize/overlays/vcell-ai-rke/secrets.dat.template new file mode 100644 index 0000000..f0feb01 --- /dev/null +++ b/kustomize/overlays/vcell-ai-rke/secrets.dat.template @@ -0,0 +1,20 @@ +# Template for secrets.dat (vcell-ai-rke / prod) +# Copy this file to secrets.dat and fill in your actual values. +# DO NOT commit secrets.dat to git — it holds plaintext secrets. +# cp secrets.dat.template secrets.dat && $EDITOR secrets.dat + +# --- Backend (backend-secrets) --- +AZURE_API_KEY=your_azure_openai_api_key +LANGFUSE_SECRET_KEY=sk-lf-your-langfuse-secret-key +LANGFUSE_PUBLIC_KEY=pk-lf-your-langfuse-public-key +SUPABASE_SERVICE_ROLE_KEY=your_supabase_service_role_key + +# --- Frontend (frontend-secrets) --- +# AUTH0_SECRET: 32-byte random string, e.g. `openssl rand -hex 32` +AUTH0_SECRET=your_auth0_session_secret +AUTH0_CLIENT_SECRET=your_auth0_client_secret + +# --- GitHub Container Registry (ghcr-secret, image pull) --- +GH_USER_NAME=your_github_username +GH_USER_EMAIL=your_email@example.com +GH_PAT=ghp_YOURPERSONALACCESSTOKEN diff --git a/kustomize/overlays/vcell-ai-rke/secrets.sh b/kustomize/overlays/vcell-ai-rke/secrets.sh new file mode 100755 index 0000000..f93d6ad --- /dev/null +++ b/kustomize/overlays/vcell-ai-rke/secrets.sh @@ -0,0 +1,68 @@ +#!/usr/bin/env bash + +set -eu # Exit on error + +# Master script: reads plaintext values from secrets.dat (gitignored) and writes +# the three sealed secret manifests this overlay references: +# secret-backend.yaml (backend-secrets) +# secret-frontend.yaml (frontend-secrets) +# secret-ghcr.yaml (ghcr-secret, image pull) + +# Get the directory where this script is located and calculate repo root +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +REPO_ROOT="$(cd "${SCRIPT_DIR}/../../../" && pwd)" + +# Set paths relative to the repository root +NAMESPACE=vcell-ai-rke +SCRIPTS_DIR="${REPO_ROOT}/kustomize/scripts" +SECRETS_DIR="${REPO_ROOT}/kustomize/overlays/${NAMESPACE}" + +# Sealed secrets controller configuration (on-premise RKE cluster) +SEALED_SECRETS_CONTROLLER_NAME=sealed-secrets-controller +SEALED_SECRETS_CONTROLLER_NAMESPACE=kube-system + +# Load secrets from data file (not committed to git) +SECRETS_DATA_FILE="${SECRETS_DIR}/secrets.dat" +if [ ! -f "$SECRETS_DATA_FILE" ]; then + echo "ERROR: Secrets data file not found: $SECRETS_DATA_FILE" + echo "Please create it from secrets.dat.template" + echo " cp ${SECRETS_DIR}/secrets.dat.template ${SECRETS_DIR}/secrets.dat" + echo " # Then edit secrets.dat with your actual values" + exit 1 +fi + +echo "Loading secrets from: $SECRETS_DATA_FILE" +source "$SECRETS_DATA_FILE" + +echo "" +echo "=== Generating Sealed Secrets for namespace ${NAMESPACE} ===" + +# backend-secrets: +echo "Generating backend secrets..." +${SCRIPTS_DIR}/sealed_secret_backend.sh \ + --controller-name ${SEALED_SECRETS_CONTROLLER_NAME} \ + --controller-namespace ${SEALED_SECRETS_CONTROLLER_NAMESPACE} \ + ${NAMESPACE} "${AZURE_API_KEY}" "${LANGFUSE_SECRET_KEY}" "${LANGFUSE_PUBLIC_KEY}" "${SUPABASE_SERVICE_ROLE_KEY}" \ + > ${SECRETS_DIR}/secret-backend.yaml +echo "✓ secret-backend.yaml generated" + +# frontend-secrets: +echo "Generating frontend secrets..." +${SCRIPTS_DIR}/sealed_secret_frontend.sh \ + --controller-name ${SEALED_SECRETS_CONTROLLER_NAME} \ + --controller-namespace ${SEALED_SECRETS_CONTROLLER_NAMESPACE} \ + ${NAMESPACE} "${AUTH0_SECRET}" "${AUTH0_CLIENT_SECRET}" \ + > ${SECRETS_DIR}/secret-frontend.yaml +echo "✓ secret-frontend.yaml generated" + +# ghcr-secret: +echo "Generating GHCR image-pull secret..." +${SCRIPTS_DIR}/sealed_secret_ghcr.sh \ + --controller-name ${SEALED_SECRETS_CONTROLLER_NAME} \ + --controller-namespace ${SEALED_SECRETS_CONTROLLER_NAMESPACE} \ + ${NAMESPACE} ${GH_USER_NAME} ${GH_USER_EMAIL} ${GH_PAT} \ + > ${SECRETS_DIR}/secret-ghcr.yaml +echo "✓ secret-ghcr.yaml generated" + +echo "" +echo "=== All sealed secrets generated successfully! ===" diff --git a/kustomize/scripts/build_and_push.sh b/kustomize/scripts/build_and_push.sh new file mode 100755 index 0000000..95e46ce --- /dev/null +++ b/kustomize/scripts/build_and_push.sh @@ -0,0 +1,42 @@ +#!/usr/bin/env bash + +set -e + +# Build and push the VCell-AI backend and frontend images to ghcr.io. +# +# Usage: +# ./build_and_push.sh [] [] +# +# tag image tag to build/push (default: "latest") +# container_org ghcr.io org (default: "virtualcell") +# +# Example: +# ./build_and_push.sh 0.1.0 virtualcell + +ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" + +tag=${1:-latest} +container_org=${2:-virtualcell} + +echo "building and pushing images to ghcr.io/${container_org} with tag ${tag}" + +# service -> build context (Dockerfile lives at /Dockerfile) +build_service() { + local service="$1" + local context="$2" + local image_name="ghcr.io/${container_org}/vcell-ai-${service}:${tag}" + + echo "==> building ${image_name} from ${context}" + docker buildx build --platform=linux/amd64 -f "${context}/Dockerfile" --tag "${image_name}" "${context}" \ + || { echo "Failed to build ${service}"; exit 1; } + + docker push "${image_name}" \ + || { echo "Failed to push ${service}"; exit 1; } + + echo "built and pushed vcell-ai-${service}:${tag}" +} + +build_service backend "${ROOT_DIR}/backend" +build_service frontend "${ROOT_DIR}/frontend" + +echo "done." diff --git a/kustomize/scripts/sealed_secret_backend.sh b/kustomize/scripts/sealed_secret_backend.sh new file mode 100755 index 0000000..937feec --- /dev/null +++ b/kustomize/scripts/sealed_secret_backend.sh @@ -0,0 +1,65 @@ +#!/usr/bin/env bash + +set -u + +# Create a SealedSecret for the VCell-AI backend's sensitive environment values. +# +# Usage: +# ./sealed_secret_backend.sh [--cert ] \ +# [--controller-name ] [--controller-namespace ] \ +# \ +# > secret-backend.yaml +# +# For GKE / AWS GovCloud the controller cert is needed and can be fetched with: +# kubeseal --fetch-cert > filename.pem +# # or +# kubectl get secret -n kube-system -l sealedsecrets.bitnami.com/sealed-secrets-key -o yaml \ +# | grep tls.crt | awk '{print $2}' | base64 --decode > filename.pem + +CERT_ARG="" +CONTROLLER_NAME="sealed-secrets-controller" +CONTROLLER_NAMESPACE="sealed-secrets" + +# Parse optional arguments +while [[ "$1" == --* ]]; do + case "$1" in + --cert) + CERT_ARG="$2" + shift 2 + ;; + --controller-name) + CONTROLLER_NAME="$2" + shift 2 + ;; + --controller-namespace) + CONTROLLER_NAMESPACE="$2" + shift 2 + ;; + *) + echo "Unknown option: $1" + exit 1 + ;; + esac +done + +# Validate the number of positional arguments +if [ "$#" -ne 5 ]; then + echo "Illegal number of parameters" + echo "Usage: ./sealed_secret_backend.sh [--cert ] " + exit 1 +fi + +SECRET_NAME="backend-secrets" +NAMESPACE=$1 +AZURE_API_KEY=$2 +LANGFUSE_SECRET_KEY=$3 +LANGFUSE_PUBLIC_KEY=$4 +SUPABASE_SERVICE_ROLE_KEY=$5 + +# Create the generic secret and seal it +kubectl create secret generic ${SECRET_NAME} --dry-run=client \ + --from-literal=azure-api-key="${AZURE_API_KEY}" \ + --from-literal=langfuse-secret-key="${LANGFUSE_SECRET_KEY}" \ + --from-literal=langfuse-public-key="${LANGFUSE_PUBLIC_KEY}" \ + --from-literal=supabase-service-role-key="${SUPABASE_SERVICE_ROLE_KEY}" \ + --namespace="${NAMESPACE}" -o yaml | kubeseal --controller-name=${CONTROLLER_NAME} --controller-namespace=${CONTROLLER_NAMESPACE} --format yaml ${CERT_ARG:+--cert=$CERT_ARG} diff --git a/kustomize/scripts/sealed_secret_frontend.sh b/kustomize/scripts/sealed_secret_frontend.sh new file mode 100755 index 0000000..7d2071e --- /dev/null +++ b/kustomize/scripts/sealed_secret_frontend.sh @@ -0,0 +1,59 @@ +#!/usr/bin/env bash + +set -u + +# Create a SealedSecret for the VCell-AI frontend's sensitive environment values +# (the Auth0 server-side session/client secrets used by @auth0/nextjs-auth0). +# +# Usage: +# ./sealed_secret_frontend.sh [--cert ] \ +# [--controller-name ] [--controller-namespace ] \ +# \ +# > secret-frontend.yaml +# +# For GKE / AWS GovCloud the controller cert is needed and can be fetched with: +# kubeseal --fetch-cert > filename.pem + +CERT_ARG="" +CONTROLLER_NAME="sealed-secrets-controller" +CONTROLLER_NAMESPACE="sealed-secrets" + +# Parse optional arguments +while [[ "$1" == --* ]]; do + case "$1" in + --cert) + CERT_ARG="$2" + shift 2 + ;; + --controller-name) + CONTROLLER_NAME="$2" + shift 2 + ;; + --controller-namespace) + CONTROLLER_NAMESPACE="$2" + shift 2 + ;; + *) + echo "Unknown option: $1" + exit 1 + ;; + esac +done + +# Validate the number of positional arguments +if [ "$#" -ne 3 ]; then + echo "Illegal number of parameters" + echo "Usage: ./sealed_secret_frontend.sh [--cert ] " + exit 1 +fi + +SECRET_NAME="frontend-secrets" +NAMESPACE=$1 +AUTH0_SECRET=$2 +AUTH0_CLIENT_SECRET=$3 + +# Create the generic secret and seal it +kubectl create secret generic ${SECRET_NAME} --dry-run=client \ + --from-literal=auth0-secret="${AUTH0_SECRET}" \ + --from-literal=auth0-client-secret="${AUTH0_CLIENT_SECRET}" \ + --namespace="${NAMESPACE}" -o yaml | kubeseal --controller-name=${CONTROLLER_NAME} --controller-namespace=${CONTROLLER_NAMESPACE} --format yaml ${CERT_ARG:+--cert=$CERT_ARG} diff --git a/kustomize/scripts/sealed_secret_ghcr.sh b/kustomize/scripts/sealed_secret_ghcr.sh new file mode 100755 index 0000000..9a92544 --- /dev/null +++ b/kustomize/scripts/sealed_secret_ghcr.sh @@ -0,0 +1,63 @@ +#!/usr/bin/env bash + +set -u + +# Create a SealedSecret holding ghcr.io registry credentials so the cluster can +# pull the private vcell-ai-backend / vcell-ai-frontend images. +# +# Usage: +# ./sealed_secret_ghcr.sh [--cert ] \ +# [--controller-name ] [--controller-namespace ] \ +# \ +# > secret-ghcr.yaml +# +# For GKE / AWS GovCloud the controller cert is needed and can be fetched with: +# kubeseal --fetch-cert > filename.pem + +CERT_ARG="" +CONTROLLER_NAME="sealed-secrets-controller" +CONTROLLER_NAMESPACE="sealed-secrets" + +# Parse optional arguments +while [[ "$1" == --* ]]; do + case "$1" in + --cert) + CERT_ARG="$2" + shift 2 + ;; + --controller-name) + CONTROLLER_NAME="$2" + shift 2 + ;; + --controller-namespace) + CONTROLLER_NAMESPACE="$2" + shift 2 + ;; + *) + echo "Unknown option: $1" + exit 1 + ;; + esac +done + +# Validate the number of positional arguments +if [ "$#" -ne 4 ]; then + echo "Illegal number of parameters" + echo "Usage: ./sealed_secret_ghcr.sh [--cert ] " + exit 1 +fi + +SECRET_NAME="ghcr-secret" +SERVER="ghcr.io" +NAMESPACE=$1 +USERNAME=$2 +EMAIL=$3 +PASSWORD=$4 + +# Create the docker-registry secret and seal it +kubectl create secret docker-registry ${SECRET_NAME} --dry-run=client \ + --docker-server="${SERVER}" \ + --docker-username="${USERNAME}" \ + --docker-email="${EMAIL}" \ + --docker-password="${PASSWORD}" \ + --namespace="${NAMESPACE}" -o yaml | kubeseal --controller-name=${CONTROLLER_NAME} --controller-namespace=${CONTROLLER_NAMESPACE} --format yaml ${CERT_ARG:+--cert=$CERT_ARG} From 7b7d935cfe709c5eaad591b5a8a81d02cf466fa0 Mon Sep 17 00:00:00 2001 From: Jim Schaff Date: Wed, 1 Jul 2026 12:40:25 -0400 Subject: [PATCH 2/5] docs: explain Node-vs-browser trust boundary for frontend secrets Clarify in the kustomize README why frontend-secrets (AUTH0_SECRET, AUTH0_CLIENT_SECRET) exist for a "frontend": the Next.js image is a trusted Node server (BFF, Authorization Code + PKCE server-side) plus an untrusted browser bundle that never receives the secrets. Note what would change if the frontend became a public SPA client. Co-Authored-By: Claude Opus 4.8 (1M context) Claude-Session: https://claude.ai/code/session_01XpNzobVi83p3YKL9sqtGwZ --- kustomize/README.md | 34 ++++++++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/kustomize/README.md b/kustomize/README.md index 3189b69..fdc49e5 100644 --- a/kustomize/README.md +++ b/kustomize/README.md @@ -54,6 +54,40 @@ sensitive values are sealed: `secrets.dat` (plaintext) and the generated `secret-*.yaml` are **gitignored**. Run `secrets.sh` to (re)generate the sealed manifests before applying an overlay. +### Why the frontend has secrets: Node vs browser trust boundary + +`frontend-secrets` (`AUTH0_SECRET`, `AUTH0_CLIENT_SECRET`) may look surprising for +a "frontend" — secrets must never reach an untrusted browser. They don't. The +`frontend` image is a **Next.js app, which is two programs**: + +- **Node server** (the pod / `frontend` container) — *trusted*. It runs SSR and the + `/auth/*` route handlers. This is the only thing that reads `AUTH0_SECRET` and + `AUTH0_CLIENT_SECRET`. +- **Browser** (React bundle on the user's machine) — *untrusted*. It gets only the + compiled JS/HTML and an httpOnly, encrypted session cookie. + +The app uses `@auth0/nextjs-auth0` v4 in the **Backend-for-Frontend (BFF)** pattern: +Authorization Code + PKCE, with the `code`→token exchange executed **server-side** by +the Node process (a confidential client / Auth0 "Regular Web Application"). The client +secret authenticates that exchange and the session key encrypts the resulting tokens +into the httpOnly cookie — all inside the Node server. + +What this means for these Kubernetes secrets: + +- `AUTH0_SECRET` / `AUTH0_CLIENT_SECRET` are consumed **only by the Node server pod**. + They are never inlined into the browser bundle (only `NEXT_PUBLIC_*` values are, and + the only one here is the non-secret `NEXT_PUBLIC_API_URL`). Sealing them and injecting + them as pod env is correct and safe. +- The refresh token stays inside the encrypted cookie (server-decryptable only). The + browser does receive a short-lived **access token** (via a server endpoint) to call + the backend directly — a scoped bearer token, not a secret. + +> If the frontend is ever re-architected to a **public SPA client** (e.g. +> `@auth0/auth0-react`, code+PKCE in the browser, Auth0 "Single-Page Application"), +> there is **no client secret at all** — delete `frontend-secrets`, drop +> `AUTH0_SECRET`/`AUTH0_CLIENT_SECRET` from `secrets.dat`/`secrets.sh`, and remove the +> `frontend-secrets` `secretKeyRef`s from `base/frontend.yaml`. + ## One-time cluster setup (Sealed Secrets controller) ```bash From 961837eff64fd03c344bc266d10f5f8a2d334072 Mon Sep 17 00:00:00 2001 From: Jim Schaff Date: Wed, 1 Jul 2026 12:55:26 -0400 Subject: [PATCH 3/5] fix(k8s): pin overlays to a real image tag; note frontend build-time URL MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - CI publishes version tags only (no :latest); pin all three overlays to the current published image tag 0.1.6.2 so pods can actually pull. - Document that NEXT_PUBLIC_API_URL is baked into the frontend image at build time (CI hardcodes a NodePort URL), so the runtime config/*.env value is ignored — with two options to make the image environment-portable. Left as a follow-up since it touches build_containers.yml and the frontend Dockerfile. Verified: server-side dry-run of the dev overlay against the live cluster validates all 13 objects; sealed-secret tooling (secrets.sh + kubeseal) works end-to-end. Co-Authored-By: Claude Opus 4.8 (1M context) Claude-Session: https://claude.ai/code/session_01XpNzobVi83p3YKL9sqtGwZ --- kustomize/README.md | 33 +++++++++++++++++-- .../vcell-ai-local/kustomization.yaml | 4 +-- .../vcell-ai-rke-dev/kustomization.yaml | 4 +-- .../overlays/vcell-ai-rke/kustomization.yaml | 4 +-- 4 files changed, 37 insertions(+), 8 deletions(-) diff --git a/kustomize/README.md b/kustomize/README.md index fdc49e5..5226c1e 100644 --- a/kustomize/README.md +++ b/kustomize/README.md @@ -124,8 +124,37 @@ kubectl apply -k . ``` Update image tags per environment in `overlays//kustomization.yaml` -(`images[].newTag`). Build & push images with -`scripts/build_and_push.sh virtualcell`. +(`images[].newTag`). Images are published to ghcr by +`.github/workflows/build_containers.yml` on **git-tag push** — CI pushes the +**git tag** as the image tag (e.g. `0.1.6.2`); there is **no `:latest`** tag, so +overlays must pin a real published tag. `scripts/build_and_push.sh +virtualcell` does the same build/push locally. + +## Known follow-up: frontend `NEXT_PUBLIC_API_URL` is baked at build time + +`NEXT_PUBLIC_*` values are inlined into the browser bundle at **Next.js build +time**, not read at runtime. The current CI (`build_containers.yml`) hardcodes + +``` +NEXT_PUBLIC_API_URL=http://k8s-wn-01.cam.uchc.edu:30001 +``` + +when building `vcell-ai-frontend`, so the published image always points the +browser at that NodePort — it will **ignore** the `NEXT_PUBLIC_API_URL` set in +`config//frontend.env` at runtime, and will not use the ingress host +(`vcell-ai[-dev].cam.uchc.edu/api`) this overlay configures. + +To make the frontend image environment-portable, one of: + +1. Turn `NEXT_PUBLIC_API_URL` into a **Docker build arg** and build a separate + frontend image per environment (CI passes the right value per tag/target), or +2. Refactor the frontend so the API base URL is resolved at **runtime** (e.g. a + server-rendered `/config` endpoint or a non-`NEXT_PUBLIC_` server var proxied + by the Node server), so one image works everywhere. + +Until then, align the CI build arg / NodePort with how each environment is +actually served. This touches `build_containers.yml` and the frontend +`Dockerfile`, so it is intentionally left out of this PR. ## Notes diff --git a/kustomize/overlays/vcell-ai-local/kustomization.yaml b/kustomize/overlays/vcell-ai-local/kustomization.yaml index ac64527..cd7c820 100644 --- a/kustomize/overlays/vcell-ai-local/kustomization.yaml +++ b/kustomize/overlays/vcell-ai-local/kustomization.yaml @@ -5,9 +5,9 @@ namespace: vcell-ai-local images: - name: ghcr.io/virtualcell/vcell-ai-backend - newTag: latest + newTag: 0.1.6.2 - name: ghcr.io/virtualcell/vcell-ai-frontend - newTag: latest + newTag: 0.1.6.2 replicas: - count: 1 diff --git a/kustomize/overlays/vcell-ai-rke-dev/kustomization.yaml b/kustomize/overlays/vcell-ai-rke-dev/kustomization.yaml index e01616a..2948ab7 100644 --- a/kustomize/overlays/vcell-ai-rke-dev/kustomization.yaml +++ b/kustomize/overlays/vcell-ai-rke-dev/kustomization.yaml @@ -5,9 +5,9 @@ namespace: vcell-ai-rke-dev images: - name: ghcr.io/virtualcell/vcell-ai-backend - newTag: latest + newTag: 0.1.6.2 - name: ghcr.io/virtualcell/vcell-ai-frontend - newTag: latest + newTag: 0.1.6.2 replicas: - count: 1 diff --git a/kustomize/overlays/vcell-ai-rke/kustomization.yaml b/kustomize/overlays/vcell-ai-rke/kustomization.yaml index 66fe351..02747eb 100644 --- a/kustomize/overlays/vcell-ai-rke/kustomization.yaml +++ b/kustomize/overlays/vcell-ai-rke/kustomization.yaml @@ -5,9 +5,9 @@ namespace: vcell-ai-rke images: - name: ghcr.io/virtualcell/vcell-ai-backend - newTag: latest + newTag: 0.1.6.2 - name: ghcr.io/virtualcell/vcell-ai-frontend - newTag: latest + newTag: 0.1.6.2 replicas: - count: 1 From d1d1700c9d22514265d9f8cd44df74630ac20cbc Mon Sep 17 00:00:00 2001 From: Jim Schaff Date: Wed, 1 Jul 2026 13:15:21 -0400 Subject: [PATCH 4/5] fix(k8s): single cert-manager issuer per host (match vcell-fluxcd) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The vcell-fluxcd ingresses (and the cluster's letsencrypt-prod ClusterIssuer, which is HTTP-01) issue one cert per host from a single Ingress. Our host is split across two Ingress objects (frontend + a backend one that only exists for the /api rewrite, since rewrite-target is ingress-wide). Both carried the cert-manager cluster-issuer annotation, which is redundant. Keep the issuer annotation on frontend-ingress only; backend-ingress still serves TLS via the same shared secret. Verified on the cluster: backend-ingress issuer -> none, one Certificate remains. Note: the dev cert stays pending until DNS for vcell-ai-dev.cam.uchc.edu resolves to the ingress (HTTP-01 self-check currently fails with "no such host") — independent of this change. Co-Authored-By: Claude Opus 4.8 (1M context) Claude-Session: https://claude.ai/code/session_01XpNzobVi83p3YKL9sqtGwZ --- kustomize/overlays/vcell-ai-rke-dev/ingress.yaml | 4 +++- kustomize/overlays/vcell-ai-rke/ingress.yaml | 4 +++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/kustomize/overlays/vcell-ai-rke-dev/ingress.yaml b/kustomize/overlays/vcell-ai-rke-dev/ingress.yaml index 6b10c38..32e1c70 100644 --- a/kustomize/overlays/vcell-ai-rke-dev/ingress.yaml +++ b/kustomize/overlays/vcell-ai-rke-dev/ingress.yaml @@ -9,7 +9,9 @@ metadata: labels: app: backend-ingress annotations: - cert-manager.io/cluster-issuer: "letsencrypt-prod" + # TLS/cert for this host is issued via frontend-ingress (one cert-manager + # cluster-issuer per host, matching the vcell-fluxcd pattern). This separate + # Ingress exists only for the /api rewrite (rewrite-target is ingress-wide). nginx.ingress.kubernetes.io/rewrite-target: /$2 nginx.ingress.kubernetes.io/proxy-read-timeout: "3600" nginx.ingress.kubernetes.io/proxy-send-timeout: "3600" diff --git a/kustomize/overlays/vcell-ai-rke/ingress.yaml b/kustomize/overlays/vcell-ai-rke/ingress.yaml index c21aad7..17af68a 100644 --- a/kustomize/overlays/vcell-ai-rke/ingress.yaml +++ b/kustomize/overlays/vcell-ai-rke/ingress.yaml @@ -9,7 +9,9 @@ metadata: labels: app: backend-ingress annotations: - cert-manager.io/cluster-issuer: "letsencrypt-prod" + # TLS/cert for this host is issued via frontend-ingress (one cert-manager + # cluster-issuer per host, matching the vcell-fluxcd pattern). This separate + # Ingress exists only for the /api rewrite (rewrite-target is ingress-wide). nginx.ingress.kubernetes.io/rewrite-target: /$2 nginx.ingress.kubernetes.io/proxy-read-timeout: "3600" nginx.ingress.kubernetes.io/proxy-send-timeout: "3600" From 43c5959f3f14bd35b58da937d7ca12d830b519dc Mon Sep 17 00:00:00 2001 From: Jim Schaff Date: Wed, 1 Jul 2026 13:18:08 -0400 Subject: [PATCH 5/5] chore(k8s): use letsencrypt-staging for the dev overlay Point the dev (vcell-ai-rke-dev) ingress at the letsencrypt-staging ClusterIssuer and a letsencrypt-staging-vcell-ai-dev-tls secret, so cert issuance during DNS/testing doesn't consume Let's Encrypt production rate limits. Prod overlay stays on letsencrypt-prod. Verified on-cluster: the ACME challenge now targets acme-staging-v02.api.letsencrypt.org. Co-Authored-By: Claude Opus 4.8 (1M context) Claude-Session: https://claude.ai/code/session_01XpNzobVi83p3YKL9sqtGwZ --- kustomize/overlays/vcell-ai-rke-dev/ingress.yaml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/kustomize/overlays/vcell-ai-rke-dev/ingress.yaml b/kustomize/overlays/vcell-ai-rke-dev/ingress.yaml index 32e1c70..aaf886e 100644 --- a/kustomize/overlays/vcell-ai-rke-dev/ingress.yaml +++ b/kustomize/overlays/vcell-ai-rke-dev/ingress.yaml @@ -21,7 +21,7 @@ spec: tls: - hosts: - vcell-ai-dev.cam.uchc.edu - secretName: letsencrypt-prod-vcell-ai-dev-tls + secretName: letsencrypt-staging-vcell-ai-dev-tls rules: - host: vcell-ai-dev.cam.uchc.edu http: @@ -41,13 +41,13 @@ metadata: labels: app: frontend-ingress annotations: - cert-manager.io/cluster-issuer: "letsencrypt-prod" + cert-manager.io/cluster-issuer: "letsencrypt-staging" spec: ingressClassName: nginx tls: - hosts: - vcell-ai-dev.cam.uchc.edu - secretName: letsencrypt-prod-vcell-ai-dev-tls + secretName: letsencrypt-staging-vcell-ai-dev-tls rules: - host: vcell-ai-dev.cam.uchc.edu http: