Migrate config to cameleer.saas.* naming convention
All checks were successful
CI / build (push) Successful in 1m49s
CI / docker (push) Successful in 55s

Move all SaaS configuration properties under the cameleer.saas.*
namespace with all-lowercase dot-separated names and mechanical env var
mapping. Aligns with the server (cameleer.server.*) and agent
(cameleer.agent.*) conventions.

Changes:
- Move cameleer.identity.* → cameleer.saas.identity.*
- Move cameleer.provisioning.* → cameleer.saas.provisioning.*
- Move cameleer.certs.* → cameleer.saas.certs.*
- Rename kebab-case properties to concatenated lowercase
- Update all env vars to CAMELEER_SAAS_* mechanical mapping
- Update DockerTenantProvisioner to pass CAMELEER_SERVER_* env vars
  to provisioned server containers (matching server's new convention)
- Spring JWT config now derives from SaaS properties via cross-reference
- Clean up orphaned properties in application-local.yml
- Update docker-compose.yml, docker-compose.dev.yml, .env.example
- Update CLAUDE.md, HOWTO.md, architecture.md, user-manual.md

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
hsiegeln
2026-04-11 18:11:21 +02:00
parent 5e69628a51
commit 8cf44f6e2c
15 changed files with 147 additions and 128 deletions

View File

@@ -9,24 +9,19 @@ POSTGRES_USER=cameleer
POSTGRES_PASSWORD=change_me_in_production POSTGRES_PASSWORD=change_me_in_production
POSTGRES_DB=cameleer_saas POSTGRES_DB=cameleer_saas
# Logto Identity Provider # Public domain (used by Traefik, Logto, and SaaS provisioning)
PUBLIC_HOST=localhost
PUBLIC_PROTOCOL=https
# Logto Identity Provider (infrastructure — used by logto-bootstrap init container)
LOGTO_ENDPOINT=http://logto:3001 LOGTO_ENDPOINT=http://logto:3001
LOGTO_PUBLIC_ENDPOINT=http://localhost:3001
LOGTO_ISSUER_URI=http://localhost:3001/oidc
LOGTO_JWK_SET_URI=http://logto:3001/oidc/jwks
LOGTO_DB_PASSWORD=change_me_in_production LOGTO_DB_PASSWORD=change_me_in_production
LOGTO_M2M_CLIENT_ID=
LOGTO_M2M_CLIENT_SECRET=
LOGTO_SPA_CLIENT_ID=
# Ed25519 Keys (mount PEM files) # SaaS Identity (Logto M2M credentials — usually auto-provisioned by bootstrap)
CAMELEER_JWT_PRIVATE_KEY_PATH=/etc/cameleer/keys/ed25519.key CAMELEER_SAAS_IDENTITY_M2MCLIENTID=
CAMELEER_JWT_PUBLIC_KEY_PATH=/etc/cameleer/keys/ed25519.pub CAMELEER_SAAS_IDENTITY_M2MCLIENTSECRET=
CAMELEER_SAAS_IDENTITY_SPACLIENTID=
# Domain (for Traefik TLS) # SaaS Provisioning
DOMAIN=localhost CAMELEER_SAAS_PROVISIONING_SERVERIMAGE=gitea.siegeln.net/cameleer/cameleer3-server:latest
CAMELEER_SAAS_PROVISIONING_SERVERUIIMAGE=gitea.siegeln.net/cameleer/cameleer3-server-ui:latest
CAMELEER_AUTH_TOKEN=change_me_bootstrap_token
CAMELEER_CONTAINER_MEMORY_LIMIT=512m
CAMELEER_CONTAINER_CPU_SHARES=512
CAMELEER_TENANT_SLUG=default

View File

@@ -98,7 +98,7 @@ The SaaS platform is a **vendor management plane**. It does not proxy requests t
### Routing (single-domain, path-based via Traefik) ### Routing (single-domain, path-based via Traefik)
All services on one hostname. Two env vars control everything: `PUBLIC_HOST` + `PUBLIC_PROTOCOL`. All services on one hostname. Infrastructure containers (Traefik, Logto) use `PUBLIC_HOST` + `PUBLIC_PROTOCOL` env vars directly. The SaaS app reads these via `CAMELEER_SAAS_PROVISIONING_PUBLICHOST` / `CAMELEER_SAAS_PROVISIONING_PUBLICPROTOCOL` (Spring Boot properties `cameleer.saas.provisioning.publichost` / `cameleer.saas.provisioning.publicprotocol`).
| Path | Target | Notes | | Path | Target | Notes |
|------|--------|-------| |------|--------|-------|
@@ -175,17 +175,20 @@ These env vars are injected into provisioned per-tenant server containers:
| Env var | Value | Purpose | | Env var | Value | Purpose |
|---------|-------|---------| |---------|-------|---------|
| `CAMELEER_OIDC_ISSUER_URI` | `${PUBLIC_PROTOCOL}://${PUBLIC_HOST}/oidc` | Token issuer claim validation | | `CAMELEER_SERVER_SECURITY_OIDCISSUERURI` | `${PUBLIC_PROTOCOL}://${PUBLIC_HOST}/oidc` | Token issuer claim validation |
| `CAMELEER_OIDC_JWK_SET_URI` | `http://logto:3001/oidc/jwks` | Docker-internal JWK fetch | | `CAMELEER_SERVER_SECURITY_OIDCJWKSETURI` | `http://logto:3001/oidc/jwks` | Docker-internal JWK fetch |
| `CAMELEER_OIDC_TLS_SKIP_VERIFY` | `true` (conditional) | Skip cert verify for OIDC discovery; only set when no `/certs/ca.pem` exists. When ca.pem exists, the server's `docker-entrypoint.sh` imports it into the JVM truststore instead. | | `CAMELEER_SERVER_SECURITY_OIDCTLSSKIPVERIFY` | `true` (conditional) | Skip cert verify for OIDC discovery; only set when no `/certs/ca.pem` exists. When ca.pem exists, the server's `docker-entrypoint.sh` imports it into the JVM truststore instead. |
| `CAMELEER_CORS_ALLOWED_ORIGINS` | `${PUBLIC_PROTOCOL}://${PUBLIC_HOST}` | Allow browser requests through Traefik | | `CAMELEER_SERVER_SECURITY_OIDCAUDIENCE` | `https://api.cameleer.local` | JWT audience validation for OIDC tokens |
| `CAMELEER_RUNTIME_ENABLED` | `true` | Enable Docker orchestration | | `CAMELEER_SERVER_SECURITY_CORSALLOWEDORIGINS` | `${PUBLIC_PROTOCOL}://${PUBLIC_HOST}` | Allow browser requests through Traefik |
| `CAMELEER_SERVER_URL` | `http://cameleer3-server-{slug}:8081` | Per-tenant server URL (DNS alias on tenant network) | | `CAMELEER_SERVER_SECURITY_BOOTSTRAPTOKEN` | (generated) | Bootstrap auth token for M2M communication |
| `CAMELEER_ROUTING_DOMAIN` | `${PUBLIC_HOST}` | Domain for Traefik routing labels | | `CAMELEER_SERVER_RUNTIME_ENABLED` | `true` | Enable Docker orchestration |
| `CAMELEER_ROUTING_MODE` | `path` | `path` or `subdomain` routing | | `CAMELEER_SERVER_RUNTIME_SERVERURL` | `http://cameleer3-server-{slug}:8081` | Per-tenant server URL (DNS alias on tenant network) |
| `CAMELEER_JAR_STORAGE_PATH` | `/data/jars` | Directory for uploaded JARs | | `CAMELEER_SERVER_RUNTIME_ROUTINGDOMAIN` | `${PUBLIC_HOST}` | Domain for Traefik routing labels |
| `CAMELEER_DOCKER_NETWORK` | `cameleer-tenant-{slug}` | Primary network for deployed app containers | | `CAMELEER_SERVER_RUNTIME_ROUTINGMODE` | `path` | `path` or `subdomain` routing |
| `CAMELEER_JAR_DOCKER_VOLUME` | `cameleer-jars-{slug}` | Docker volume name for JAR sharing between server and deployed containers | | `CAMELEER_SERVER_RUNTIME_JARSTORAGEPATH` | `/data/jars` | Directory for uploaded JARs |
| `CAMELEER_SERVER_RUNTIME_DOCKERNETWORK` | `cameleer-tenant-{slug}` | Primary network for deployed app containers |
| `CAMELEER_SERVER_RUNTIME_JARDOCKERVOLUME` | `cameleer-jars-{slug}` | Docker volume name for JAR sharing between server and deployed containers |
| `CAMELEER_SERVER_TENANT_ID` | (tenant UUID) | Tenant identifier for data isolation |
| `BASE_PATH` (server-ui) | `/t/{slug}` | React Router basename + `<base>` tag | | `BASE_PATH` (server-ui) | `/t/{slug}` | React Router basename + `<base>` tag |
| `CAMELEER_API_URL` (server-ui) | `http://cameleer-server-{slug}:8081` | Nginx upstream proxy target (NOT `API_URL` — image uses `${CAMELEER_API_URL}`) | | `CAMELEER_API_URL` (server-ui) | `http://cameleer-server-{slug}:8081` | Nginx upstream proxy target (NOT `API_URL` — image uses `${CAMELEER_API_URL}`) |
@@ -194,9 +197,29 @@ These env vars are injected into provisioned per-tenant server containers:
| Mount | Container path | Purpose | | Mount | Container path | Purpose |
|-------|---------------|---------| |-------|---------------|---------|
| `/var/run/docker.sock` | `/var/run/docker.sock` | Docker socket for app deployment orchestration | | `/var/run/docker.sock` | `/var/run/docker.sock` | Docker socket for app deployment orchestration |
| `cameleer-jars-{slug}` (volume) | `/data/jars` | Shared JAR storage — server writes, deployed app containers read | | `cameleer-jars-{slug}` (volume, via `CAMELEER_SERVER_RUNTIME_JARDOCKERVOLUME`) | `/data/jars` | Shared JAR storage — server writes, deployed app containers read |
| `cameleer-saas_certs` (volume, ro) | `/certs` | Platform TLS certs + CA bundle for OIDC trust | | `cameleer-saas_certs` (volume, ro) | `/certs` | Platform TLS certs + CA bundle for OIDC trust |
### SaaS app configuration (env vars for cameleer-saas itself)
SaaS properties use the `cameleer.saas.*` prefix (env vars: `CAMELEER_SAAS_*`). Two groups:
**Identity** (`cameleer.saas.identity.*` / `CAMELEER_SAAS_IDENTITY_*`):
- Logto endpoint, M2M credentials, bootstrap file path — used by `LogtoConfig.java`
**Provisioning** (`cameleer.saas.provisioning.*` / `CAMELEER_SAAS_PROVISIONING_*`):
| Env var | Spring property | Purpose |
|---------|----------------|---------|
| `CAMELEER_SAAS_PROVISIONING_SERVERIMAGE` | `cameleer.saas.provisioning.serverimage` | Docker image for per-tenant server containers |
| `CAMELEER_SAAS_PROVISIONING_SERVERUIIMAGE` | `cameleer.saas.provisioning.serveruiimage` | Docker image for per-tenant UI containers |
| `CAMELEER_SAAS_PROVISIONING_NETWORKNAME` | `cameleer.saas.provisioning.networkname` | Shared services Docker network (compose default) |
| `CAMELEER_SAAS_PROVISIONING_TRAEFIKNETWORK` | `cameleer.saas.provisioning.traefiknetwork` | Traefik Docker network for routing |
| `CAMELEER_SAAS_PROVISIONING_PUBLICHOST` | `cameleer.saas.provisioning.publichost` | Public hostname (same value as infrastructure `PUBLIC_HOST`) |
| `CAMELEER_SAAS_PROVISIONING_PUBLICPROTOCOL` | `cameleer.saas.provisioning.publicprotocol` | Public protocol (same value as infrastructure `PUBLIC_PROTOCOL`) |
**Note:** `PUBLIC_HOST` and `PUBLIC_PROTOCOL` remain as infrastructure env vars for Traefik and Logto containers. The SaaS app reads its own copies via the `CAMELEER_SAAS_PROVISIONING_*` prefix. `LOGTO_ENDPOINT` and `LOGTO_DB_PASSWORD` are infrastructure env vars for the Logto service and are unchanged.
### Server OIDC role extraction (two paths) ### Server OIDC role extraction (two paths)
| Path | Token type | Role source | How it works | | Path | Token type | Role source | How it works |
@@ -308,7 +331,7 @@ PostgreSQL (Flyway): `src/main/resources/db/migration/`
- `cameleer-saas` — SaaS vendor management plane (frontend + JAR baked in) - `cameleer-saas` — SaaS vendor management plane (frontend + JAR baked in)
- `cameleer-logto` — custom Logto with sign-in UI baked in - `cameleer-logto` — custom Logto with sign-in UI baked in
- `cameleer3-server` / `cameleer3-server-ui` — provisioned per-tenant (not in compose, created by `DockerTenantProvisioner`) - `cameleer3-server` / `cameleer3-server-ui` — provisioned per-tenant (not in compose, created by `DockerTenantProvisioner`)
- `cameleer-runtime-base` — base image for deployed apps (agent JAR + JRE). CI downloads latest agent SNAPSHOT from Gitea Maven registry. Uses `CAMELEER_SERVER_URL` env var (not CAMELEER_EXPORT_ENDPOINT). - `cameleer-runtime-base` — base image for deployed apps (agent JAR + JRE). CI downloads latest agent SNAPSHOT from Gitea Maven registry. Uses `CAMELEER_SERVER_RUNTIME_SERVERURL` env var (not CAMELEER_EXPORT_ENDPOINT).
- Docker builds: `--no-cache`, `--provenance=false` for Gitea compatibility - Docker builds: `--no-cache`, `--provenance=false` for Gitea compatibility
- `docker-compose.dev.yml` — exposes ports for direct access, sets `SPRING_PROFILES_ACTIVE: dev`, `VENDOR_SEED_ENABLED: true`. Volume-mounts `./ui/dist` into the container so local UI builds are served without rebuilding the Docker image (`SPRING_WEB_RESOURCES_STATIC_LOCATIONS` overrides classpath). Adds Docker socket mount for tenant provisioning. - `docker-compose.dev.yml` — exposes ports for direct access, sets `SPRING_PROFILES_ACTIVE: dev`, `VENDOR_SEED_ENABLED: true`. Volume-mounts `./ui/dist` into the container so local UI builds are served without rebuilding the Docker image (`SPRING_WEB_RESOURCES_STATIC_LOCATIONS` overrides classpath). Adds Docker socket mount for tenant provisioning.
- Design system: import from `@cameleer/design-system` (Gitea npm registry) - Design system: import from `@cameleer/design-system` (Gitea npm registry)

View File

@@ -63,12 +63,12 @@ Edit `.env` and set at minimum:
```bash ```bash
# Change in production # Change in production
POSTGRES_PASSWORD=<strong-password> POSTGRES_PASSWORD=<strong-password>
CAMELEER_AUTH_TOKEN=<random-string-for-agent-bootstrap> CAMELEER_SERVER_SECURITY_BOOTSTRAPTOKEN=<random-string-for-agent-bootstrap>
CAMELEER_TENANT_SLUG=<your-tenant-slug> # e.g., "acme" — tags all observability data CAMELEER_TENANT_SLUG=<your-tenant-slug> # e.g., "acme" — tags all observability data
# Logto M2M credentials (get from Logto admin console after first boot) # Logto M2M credentials (get from Logto admin console after first boot)
LOGTO_M2M_CLIENT_ID= CAMELEER_SAAS_IDENTITY_M2MCLIENTID=
LOGTO_M2M_CLIENT_SECRET= CAMELEER_SAAS_IDENTITY_M2MCLIENTSECRET=
``` ```
### 2. Ed25519 Keys ### 2. Ed25519 Keys
@@ -139,8 +139,8 @@ On first boot, Logto seeds its database automatically. Access the admin console
- Assign the **Logto Management API** resource with all scopes - Assign the **Logto Management API** resource with all scopes
4. Update `.env`: 4. Update `.env`:
``` ```
LOGTO_M2M_CLIENT_ID=<app-id> CAMELEER_SAAS_IDENTITY_M2MCLIENTID=<app-id>
LOGTO_M2M_CLIENT_SECRET=<app-secret> CAMELEER_SAAS_IDENTITY_M2MCLIENTSECRET=<app-secret>
``` ```
5. Restart cameleer-saas: `docker compose restart cameleer-saas` 5. Restart cameleer-saas: `docker compose restart cameleer-saas`

View File

@@ -24,12 +24,12 @@ services:
environment: environment:
SPRING_PROFILES_ACTIVE: dev SPRING_PROFILES_ACTIVE: dev
SPRING_WEB_RESOURCES_STATIC_LOCATIONS: file:/app/static/,classpath:/static/ SPRING_WEB_RESOURCES_STATIC_LOCATIONS: file:/app/static/,classpath:/static/
PUBLIC_HOST: ${PUBLIC_HOST:-localhost} CAMELEER_SAAS_PROVISIONING_PUBLICHOST: ${PUBLIC_HOST:-localhost}
PUBLIC_PROTOCOL: ${PUBLIC_PROTOCOL:-https} CAMELEER_SAAS_PROVISIONING_PUBLICPROTOCOL: ${PUBLIC_PROTOCOL:-https}
CAMELEER_SERVER_IMAGE: gitea.siegeln.net/cameleer/cameleer3-server:${VERSION:-latest} CAMELEER_SAAS_PROVISIONING_SERVERIMAGE: gitea.siegeln.net/cameleer/cameleer3-server:${VERSION:-latest}
CAMELEER_SERVER_UI_IMAGE: gitea.siegeln.net/cameleer/cameleer3-server-ui:${VERSION:-latest} CAMELEER_SAAS_PROVISIONING_SERVERUIIMAGE: gitea.siegeln.net/cameleer/cameleer3-server-ui:${VERSION:-latest}
CAMELEER_NETWORK: cameleer-saas_cameleer CAMELEER_SAAS_PROVISIONING_NETWORKNAME: cameleer-saas_cameleer
CAMELEER_TRAEFIK_NETWORK: cameleer-traefik CAMELEER_SAAS_PROVISIONING_TRAEFIKNETWORK: cameleer-traefik
clickhouse: clickhouse:
ports: ports:

View File

@@ -178,12 +178,12 @@ services:
SPRING_DATASOURCE_URL: jdbc:postgresql://postgres:5432/${POSTGRES_DB:-cameleer_saas} SPRING_DATASOURCE_URL: jdbc:postgresql://postgres:5432/${POSTGRES_DB:-cameleer_saas}
SPRING_DATASOURCE_USERNAME: ${POSTGRES_USER:-cameleer} SPRING_DATASOURCE_USERNAME: ${POSTGRES_USER:-cameleer}
SPRING_DATASOURCE_PASSWORD: ${POSTGRES_PASSWORD:-cameleer_dev} SPRING_DATASOURCE_PASSWORD: ${POSTGRES_PASSWORD:-cameleer_dev}
LOGTO_ENDPOINT: ${LOGTO_ENDPOINT:-http://logto:3001} CAMELEER_SAAS_IDENTITY_LOGTOENDPOINT: ${LOGTO_ENDPOINT:-http://logto:3001}
LOGTO_PUBLIC_ENDPOINT: ${PUBLIC_PROTOCOL:-https}://${PUBLIC_HOST:-localhost} CAMELEER_SAAS_IDENTITY_LOGTOPUBLICENDPOINT: ${PUBLIC_PROTOCOL:-https}://${PUBLIC_HOST:-localhost}
LOGTO_ISSUER_URI: ${PUBLIC_PROTOCOL:-https}://${PUBLIC_HOST:-localhost}/oidc CAMELEER_SAAS_PROVISIONING_PUBLICPROTOCOL: ${PUBLIC_PROTOCOL:-https}
LOGTO_JWK_SET_URI: ${LOGTO_ENDPOINT:-http://logto:3001}/oidc/jwks CAMELEER_SAAS_PROVISIONING_PUBLICHOST: ${PUBLIC_HOST:-localhost}
LOGTO_M2M_CLIENT_ID: ${LOGTO_M2M_CLIENT_ID:-} CAMELEER_SAAS_IDENTITY_M2MCLIENTID: ${LOGTO_M2M_CLIENT_ID:-}
LOGTO_M2M_CLIENT_SECRET: ${LOGTO_M2M_CLIENT_SECRET:-} CAMELEER_SAAS_IDENTITY_M2MCLIENTSECRET: ${LOGTO_M2M_CLIENT_SECRET:-}
labels: labels:
- traefik.enable=true - traefik.enable=true
- traefik.http.routers.saas.rule=PathPrefix(`/platform`) - traefik.http.routers.saas.rule=PathPrefix(`/platform`)

View File

@@ -193,7 +193,7 @@ the bootstrap script (`docker/logto-bootstrap.sh`):
**Agent -> cameleer3-server:** **Agent -> cameleer3-server:**
1. Agent reads `CAMELEER_AUTH_TOKEN` environment variable (API key). 1. Agent reads `CAMELEER_SERVER_SECURITY_BOOTSTRAPTOKEN` environment variable (API key).
2. Calls `POST /api/v1/agents/register` with the key as Bearer token. 2. Calls `POST /api/v1/agents/register` with the key as Bearer token.
3. Server validates via `BootstrapTokenValidator` (constant-time comparison). 3. Server validates via `BootstrapTokenValidator` (constant-time comparison).
4. Server issues internal HMAC JWT (access + refresh) + Ed25519 public key. 4. Server issues internal HMAC JWT (access + refresh) + Ed25519 public key.
@@ -493,9 +493,9 @@ The deployment lifecycle is managed by `DeploymentService`:
| Variable | Value | | Variable | Value |
|-----------------------------|----------------------------------------| |-----------------------------|----------------------------------------|
| `CAMELEER_AUTH_TOKEN` | API key for agent registration | | `CAMELEER_SERVER_SECURITY_BOOTSTRAPTOKEN` | API key for agent registration |
| `CAMELEER_EXPORT_TYPE` | `HTTP` | | `CAMELEER_EXPORT_TYPE` | `HTTP` |
| `CAMELEER_SERVER_URL` | cameleer3-server internal URL | | `CAMELEER_SERVER_RUNTIME_SERVERURL` | cameleer3-server internal URL |
| `CAMELEER_APPLICATION_ID` | App slug | | `CAMELEER_APPLICATION_ID` | App slug |
| `CAMELEER_ENVIRONMENT_ID` | Environment slug | | `CAMELEER_ENVIRONMENT_ID` | Environment slug |
| `CAMELEER_DISPLAY_NAME` | `{tenant}-{env}-{app}` | | `CAMELEER_DISPLAY_NAME` | `{tenant}-{env}-{app}` |
@@ -529,7 +529,7 @@ aspects relevant to the SaaS platform.
### 6.1 Agent Registration ### 6.1 Agent Registration
1. Agent starts with `CAMELEER_AUTH_TOKEN` environment variable (an API key 1. Agent starts with `CAMELEER_SERVER_SECURITY_BOOTSTRAPTOKEN` environment variable (an API key
generated by the SaaS platform, prefixed with `cmk_`). generated by the SaaS platform, prefixed with `cmk_`).
2. Agent calls `POST /api/v1/agents/register` on the cameleer3-server with the 2. Agent calls `POST /api/v1/agents/register` on the cameleer3-server with the
API key as a Bearer token. API key as a Bearer token.
@@ -862,17 +862,15 @@ state (`currentTenantId`). Provides `logout` and `signIn` callbacks.
| `SPRING_DATASOURCE_USERNAME`| `cameleer` | PostgreSQL user | | `SPRING_DATASOURCE_USERNAME`| `cameleer` | PostgreSQL user |
| `SPRING_DATASOURCE_PASSWORD`| `cameleer_dev` | PostgreSQL password | | `SPRING_DATASOURCE_PASSWORD`| `cameleer_dev` | PostgreSQL password |
**Logto / OIDC:** **Identity / OIDC:**
| Variable | Default | Description | | Variable | Default | Description |
|---------------------------|------------|--------------------------------------------| |---------------------------|------------|--------------------------------------------|
| `LOGTO_ENDPOINT` | (empty) | Logto internal URL (Docker-internal) | | `CAMELEER_SAAS_IDENTITY_LOGTOENDPOINT` | (empty) | Logto internal URL (Docker-internal) |
| `LOGTO_PUBLIC_ENDPOINT` | (empty) | Logto public URL (browser-accessible) | | `CAMELEER_SAAS_IDENTITY_LOGTOPUBLICENDPOINT` | (empty) | Logto public URL (browser-accessible) |
| `LOGTO_ISSUER_URI` | (empty) | OIDC issuer URI for JWT validation | | `CAMELEER_SAAS_IDENTITY_M2MCLIENTID` | (empty) | M2M app client ID (from bootstrap) |
| `LOGTO_JWK_SET_URI` | (empty) | JWKS endpoint for JWT signature validation | | `CAMELEER_SAAS_IDENTITY_M2MCLIENTSECRET` | (empty) | M2M app client secret (from bootstrap) |
| `LOGTO_M2M_CLIENT_ID` | (empty) | M2M app client ID (from bootstrap) | | `CAMELEER_SAAS_IDENTITY_SPACLIENTID` | (empty) | SPA app client ID (fallback; bootstrap preferred) |
| `LOGTO_M2M_CLIENT_SECRET` | (empty) | M2M app client secret (from bootstrap) |
| `LOGTO_SPA_CLIENT_ID` | (empty) | SPA app client ID (fallback; bootstrap preferred) |
**Runtime / Deployment:** **Runtime / Deployment:**
@@ -898,11 +896,11 @@ state (`currentTenantId`). Provides `logout` and `signIn` callbacks.
| `SPRING_DATASOURCE_USERNAME`| `cameleer` | PostgreSQL user | | `SPRING_DATASOURCE_USERNAME`| `cameleer` | PostgreSQL user |
| `SPRING_DATASOURCE_PASSWORD`| `cameleer_dev` | PostgreSQL password | | `SPRING_DATASOURCE_PASSWORD`| `cameleer_dev` | PostgreSQL password |
| `CLICKHOUSE_URL` | `jdbc:clickhouse://clickhouse:8123/cameleer` | ClickHouse JDBC URL | | `CLICKHOUSE_URL` | `jdbc:clickhouse://clickhouse:8123/cameleer` | ClickHouse JDBC URL |
| `CAMELEER_AUTH_TOKEN` | `default-bootstrap-token` | Agent bootstrap token | | `CAMELEER_SERVER_SECURITY_BOOTSTRAPTOKEN` | `default-bootstrap-token` | Agent bootstrap token |
| `CAMELEER_JWT_SECRET` | `cameleer-dev-jwt-secret-...` | HMAC secret for internal JWTs | | `CAMELEER_JWT_SECRET` | `cameleer-dev-jwt-secret-...` | HMAC secret for internal JWTs |
| `CAMELEER_TENANT_ID` | `default` | Tenant slug for data isolation | | `CAMELEER_SERVER_TENANT_ID` | `default` | Tenant slug for data isolation |
| `CAMELEER_OIDC_ISSUER_URI` | (empty) | Logto issuer for M2M token validation | | `CAMELEER_SERVER_SECURITY_OIDCISSUERURI` | (empty) | Logto issuer for M2M token validation |
| `CAMELEER_OIDC_AUDIENCE` | (empty) | Expected JWT audience | | `CAMELEER_SERVER_SECURITY_OIDCAUDIENCE` | (empty) | Expected JWT audience |
### 10.3 logto ### 10.3 logto
@@ -927,7 +925,7 @@ state (`currentTenantId`). Provides `logout` and `signIn` callbacks.
| `SAAS_ADMIN_PASS` | `admin` | Platform admin password | | `SAAS_ADMIN_PASS` | `admin` | Platform admin password |
| `TENANT_ADMIN_USER` | `camel` | Default tenant admin username | | `TENANT_ADMIN_USER` | `camel` | Default tenant admin username |
| `TENANT_ADMIN_PASS` | `camel` | Default tenant admin password | | `TENANT_ADMIN_PASS` | `camel` | Default tenant admin password |
| `CAMELEER_AUTH_TOKEN`| `default-bootstrap-token` | Agent bootstrap token | | `CAMELEER_SERVER_SECURITY_BOOTSTRAPTOKEN`| `default-bootstrap-token` | Agent bootstrap token |
### 10.6 Bootstrap Output ### 10.6 Bootstrap Output

View File

@@ -435,14 +435,12 @@ Copy `.env.example` to `.env` and configure as needed:
| `POSTGRES_USER` | PostgreSQL username | `cameleer` | | `POSTGRES_USER` | PostgreSQL username | `cameleer` |
| `POSTGRES_PASSWORD` | PostgreSQL password | `change_me_in_production` | | `POSTGRES_PASSWORD` | PostgreSQL password | `change_me_in_production` |
| `POSTGRES_DB` | PostgreSQL database name | `cameleer_saas` | | `POSTGRES_DB` | PostgreSQL database name | `cameleer_saas` |
| `LOGTO_ENDPOINT` | Internal Logto URL (container-to-container) | `http://logto:3001` | | `CAMELEER_SAAS_IDENTITY_LOGTOENDPOINT` | Internal Logto URL (container-to-container) | `http://logto:3001` |
| `LOGTO_PUBLIC_ENDPOINT` | Public-facing Logto URL | `http://localhost:3001` | | `CAMELEER_SAAS_IDENTITY_LOGTOPUBLICENDPOINT` | Public-facing Logto URL | `http://localhost:3001` |
| `LOGTO_ISSUER_URI` | OIDC issuer URI | `http://localhost:3001/oidc` | | `CAMELEER_SAAS_IDENTITY_M2MCLIENTID` | Machine-to-machine client ID (auto-set by bootstrap) | _(empty)_ |
| `LOGTO_JWK_SET_URI` | OIDC JWK set URI | `http://logto:3001/oidc/jwks` | | `CAMELEER_SAAS_IDENTITY_M2MCLIENTSECRET` | Machine-to-machine client secret (auto-set by bootstrap) | _(empty)_ |
| `LOGTO_M2M_CLIENT_ID` | Machine-to-machine client ID (auto-set by bootstrap) | _(empty)_ | | `CAMELEER_SAAS_IDENTITY_SPACLIENTID` | SPA client ID for the frontend | _(empty)_ |
| `LOGTO_M2M_CLIENT_SECRET` | Machine-to-machine client secret (auto-set by bootstrap) | _(empty)_ | | `CAMELEER_SERVER_SECURITY_BOOTSTRAPTOKEN` | Bootstrap token for agent registration | `change_me_bootstrap_token` |
| `LOGTO_SPA_CLIENT_ID` | SPA client ID for the frontend | _(empty)_ |
| `CAMELEER_AUTH_TOKEN` | Bootstrap token for agent registration | `change_me_bootstrap_token` |
| `CAMELEER_CONTAINER_MEMORY_LIMIT` | Memory limit for deployed containers | `512m` | | `CAMELEER_CONTAINER_MEMORY_LIMIT` | Memory limit for deployed containers | `512m` |
| `CAMELEER_CONTAINER_CPU_SHARES` | CPU shares for deployed containers | `512` | | `CAMELEER_CONTAINER_CPU_SHARES` | CPU shares for deployed containers | `512` |
| `CAMELEER_TENANT_SLUG` | Default tenant slug | `default` | | `CAMELEER_TENANT_SLUG` | Default tenant slug | `default` |
@@ -550,7 +548,7 @@ The Cameleer SaaS application itself does not need any changes -- all identity c
**Resolution:** **Resolution:**
1. Check backend logs: `docker compose logs cameleer-saas`. 1. Check backend logs: `docker compose logs cameleer-saas`.
2. Verify that `LOGTO_ISSUER_URI` and `LOGTO_JWK_SET_URI` in `.env` are correct. 2. Verify that `CAMELEER_SAAS_IDENTITY_LOGTOENDPOINT` in `.env` is correct (the OIDC issuer and JWK set URIs are derived from it automatically).
3. If the issue persists, restart the services: `docker compose restart cameleer-saas logto`. 3. If the issue persists, restart the services: `docker compose restart cameleer-saas logto`.
### Deployment Stuck in BUILDING ### Deployment Stuck in BUILDING
@@ -577,14 +575,14 @@ The Cameleer SaaS application itself does not need any changes -- all identity c
**Possible causes:** **Possible causes:**
- The agent cannot reach the cameleer3-server endpoint. Check network connectivity between the deployed container and the observability server. - The agent cannot reach the cameleer3-server endpoint. Check network connectivity between the deployed container and the observability server.
- The bootstrap token does not match. The agent uses `CAMELEER_AUTH_TOKEN` to register with the server. - The bootstrap token does not match. The agent uses `CAMELEER_SERVER_SECURITY_BOOTSTRAPTOKEN` to register with the server.
- The cameleer3-server is not healthy. - The cameleer3-server is not healthy.
**Resolution:** **Resolution:**
1. Check cameleer3-server health: `docker compose logs cameleer3-server`. 1. Check cameleer3-server health: `docker compose logs cameleer3-server`.
2. Verify the app container's logs for agent connection errors (use the Logs tab on the app detail page). 2. Verify the app container's logs for agent connection errors (use the Logs tab on the app detail page).
3. Confirm that `CAMELEER_AUTH_TOKEN` is the same in both the `cameleer-saas` and `cameleer3-server` service configurations. 3. Confirm that `CAMELEER_SERVER_SECURITY_BOOTSTRAPTOKEN` is the same in both the `cameleer-saas` and `cameleer3-server` service configurations.
### Container Health Check Failing ### Container Health Check Failing

View File

@@ -18,10 +18,10 @@ public class PublicConfigController {
private static final Logger log = LoggerFactory.getLogger(PublicConfigController.class); private static final Logger log = LoggerFactory.getLogger(PublicConfigController.class);
private static final String BOOTSTRAP_FILE = "/data/bootstrap/logto-bootstrap.json"; private static final String BOOTSTRAP_FILE = "/data/bootstrap/logto-bootstrap.json";
@Value("${cameleer.identity.logto-public-endpoint:${cameleer.identity.logto-endpoint:}}") @Value("${cameleer.saas.identity.logtopublicendpoint:${cameleer.saas.identity.logtoendpoint:}}")
private String logtoPublicEndpoint; private String logtoPublicEndpoint;
@Value("${cameleer.identity.spa-client-id:}") @Value("${cameleer.saas.identity.spaclientid:}")
private String spaClientId; private String spaClientId;
private final ObjectMapper objectMapper = new ObjectMapper(); private final ObjectMapper objectMapper = new ObjectMapper();

View File

@@ -80,7 +80,7 @@ public class SecurityConfig {
public JwtDecoder jwtDecoder( public JwtDecoder jwtDecoder(
@Value("${spring.security.oauth2.resourceserver.jwt.jwk-set-uri}") String jwkSetUri, @Value("${spring.security.oauth2.resourceserver.jwt.jwk-set-uri}") String jwkSetUri,
@Value("${spring.security.oauth2.resourceserver.jwt.issuer-uri:}") String issuerUri, @Value("${spring.security.oauth2.resourceserver.jwt.issuer-uri:}") String issuerUri,
@Value("${cameleer.identity.audience:}") String audience) throws Exception { @Value("${cameleer.saas.identity.audience:}") String audience) throws Exception {
var jwkSource = JWKSourceBuilder.create(new URL(jwkSetUri)).build(); var jwkSource = JWKSourceBuilder.create(new URL(jwkSetUri)).build();
var keySelector = new JWSVerificationKeySelector<SecurityContext>( var keySelector = new JWSVerificationKeySelector<SecurityContext>(
JWSAlgorithm.ES384, jwkSource); JWSAlgorithm.ES384, jwkSource);

View File

@@ -16,16 +16,16 @@ public class LogtoConfig {
private static final Logger log = LoggerFactory.getLogger(LogtoConfig.class); private static final Logger log = LoggerFactory.getLogger(LogtoConfig.class);
private static final String BOOTSTRAP_FILE = "/data/bootstrap/logto-bootstrap.json"; private static final String BOOTSTRAP_FILE = "/data/bootstrap/logto-bootstrap.json";
@Value("${cameleer.identity.logto-endpoint:}") @Value("${cameleer.saas.identity.logtoendpoint:}")
private String logtoEndpoint; private String logtoEndpoint;
@Value("${cameleer.identity.m2m-client-id:}") @Value("${cameleer.saas.identity.m2mclientid:}")
private String m2mClientId; private String m2mClientId;
@Value("${cameleer.identity.m2m-client-secret:}") @Value("${cameleer.saas.identity.m2mclientsecret:}")
private String m2mClientSecret; private String m2mClientSecret;
@Value("${cameleer.identity.server-endpoint:http://cameleer3-server:8081}") @Value("${cameleer.saas.identity.serverendpoint:http://cameleer3-server:8081}")
private String serverEndpoint; private String serverEndpoint;
private String tradAppId; private String tradAppId;

View File

@@ -17,7 +17,7 @@ public class CertificateManagerAutoConfig {
@Bean @Bean
CertificateManager certificateManager( CertificateManager certificateManager(
@Value("${cameleer.certs.path:/certs}") String certsPath) { @Value("${cameleer.saas.certs.path:/certs}") String certsPath) {
Path path = Path.of(certsPath); Path path = Path.of(certsPath);
if (Files.isDirectory(path)) { if (Files.isDirectory(path)) {
log.info("Certs directory found at {} — enabling Docker certificate manager", certsPath); log.info("Certs directory found at {} — enabling Docker certificate manager", certsPath);

View File

@@ -195,27 +195,27 @@ public class DockerTenantProvisioner implements TenantProvisioner {
"SPRING_DATASOURCE_URL=" + props.datasourceUrl(), "SPRING_DATASOURCE_URL=" + props.datasourceUrl(),
"SPRING_DATASOURCE_USERNAME=cameleer", "SPRING_DATASOURCE_USERNAME=cameleer",
"SPRING_DATASOURCE_PASSWORD=cameleer_dev", "SPRING_DATASOURCE_PASSWORD=cameleer_dev",
"CLICKHOUSE_URL=jdbc:clickhouse://clickhouse:8123/cameleer", "CAMELEER_SERVER_CLICKHOUSE_URL=jdbc:clickhouse://clickhouse:8123/cameleer",
"CAMELEER_TENANT_ID=" + slug, "CAMELEER_SERVER_TENANT_ID=" + slug,
"CAMELEER_AUTH_TOKEN=" + req.licenseToken(), "CAMELEER_SERVER_SECURITY_BOOTSTRAPTOKEN=" + req.licenseToken(),
"CAMELEER_JWT_SECRET=cameleer-dev-jwt-secret-change-in-production", "CAMELEER_SERVER_SECURITY_JWTSECRET=cameleer-dev-jwt-secret-change-in-production",
"CAMELEER_OIDC_ISSUER_URI=" + props.oidcIssuerUri(), "CAMELEER_SERVER_SECURITY_OIDCISSUERURI=" + props.oidcIssuerUri(),
"CAMELEER_OIDC_JWK_SET_URI=" + props.oidcJwkSetUri(), "CAMELEER_SERVER_SECURITY_OIDCJWKSETURI=" + props.oidcJwkSetUri(),
"CAMELEER_OIDC_AUDIENCE=https://api.cameleer.local", "CAMELEER_SERVER_SECURITY_OIDCAUDIENCE=https://api.cameleer.local",
"CAMELEER_CORS_ALLOWED_ORIGINS=" + props.corsOrigins(), "CAMELEER_SERVER_SECURITY_CORSALLOWEDORIGINS=" + props.corsOrigins(),
"CAMELEER_LICENSE_TOKEN=" + req.licenseToken(), "CAMELEER_SERVER_LICENSE_TOKEN=" + req.licenseToken(),
"CAMELEER_RUNTIME_ENABLED=true", "CAMELEER_SERVER_RUNTIME_ENABLED=true",
"CAMELEER_SERVER_URL=http://" + name + ":8081", "CAMELEER_SERVER_RUNTIME_SERVERURL=http://" + name + ":8081",
"CAMELEER_ROUTING_DOMAIN=" + props.publicHost(), "CAMELEER_SERVER_RUNTIME_ROUTINGDOMAIN=" + props.publicHost(),
"CAMELEER_ROUTING_MODE=path", "CAMELEER_SERVER_RUNTIME_ROUTINGMODE=path",
"CAMELEER_JAR_STORAGE_PATH=/data/jars", "CAMELEER_SERVER_RUNTIME_JARSTORAGEPATH=/data/jars",
// Apps deployed by this server join the tenant network (isolated) // Apps deployed by this server join the tenant network (isolated)
"CAMELEER_DOCKER_NETWORK=" + tenantNetwork, "CAMELEER_SERVER_RUNTIME_DOCKERNETWORK=" + tenantNetwork,
"CAMELEER_JAR_DOCKER_VOLUME=cameleer-jars-" + slug "CAMELEER_SERVER_RUNTIME_JARDOCKERVOLUME=cameleer-jars-" + slug
)); ));
// If no CA bundle exists, fall back to TLS skip for OIDC (self-signed dev) // If no CA bundle exists, fall back to TLS skip for OIDC (self-signed dev)
if (!java.nio.file.Files.exists(java.nio.file.Path.of("/certs/ca.pem"))) { if (!java.nio.file.Files.exists(java.nio.file.Path.of("/certs/ca.pem"))) {
env.add("CAMELEER_OIDC_TLS_SKIP_VERIFY=true"); env.add("CAMELEER_SERVER_SECURITY_OIDCTLSSKIPVERIFY=true");
} }
// Primary network = tenant-isolated network // Primary network = tenant-isolated network

View File

@@ -2,7 +2,7 @@ package net.siegeln.cameleer.saas.provisioning;
import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.boot.context.properties.ConfigurationProperties;
@ConfigurationProperties(prefix = "cameleer.provisioning") @ConfigurationProperties(prefix = "cameleer.saas.provisioning")
public record ProvisioningProperties( public record ProvisioningProperties(
String serverImage, String serverImage,
String serverUiImage, String serverUiImage,

View File

@@ -17,7 +17,9 @@ spring:
jwk-set-uri: http://localhost:3001/oidc/jwks jwk-set-uri: http://localhost:3001/oidc/jwks
cameleer: cameleer:
clickhouse: saas:
url: jdbc:clickhouse://localhost:8123/cameleer identity:
runtime: logtoendpoint: http://localhost:3001
cameleer3-server-endpoint: http://localhost:8081 serverendpoint: http://localhost:8081
provisioning:
clickhouseurl: jdbc:clickhouse://localhost:8123/cameleer

View File

@@ -20,8 +20,8 @@ spring:
oauth2: oauth2:
resourceserver: resourceserver:
jwt: jwt:
issuer-uri: ${LOGTO_ISSUER_URI:} issuer-uri: ${cameleer.saas.provisioning.publicprotocol:https}://${cameleer.saas.provisioning.publichost:localhost}/oidc
jwk-set-uri: ${LOGTO_JWK_SET_URI:} jwk-set-uri: ${cameleer.saas.identity.logtoendpoint:http://logto:3001}/oidc/jwks
management: management:
endpoints: endpoints:
@@ -33,23 +33,26 @@ management:
show-details: when-authorized show-details: when-authorized
cameleer: cameleer:
identity: saas:
logto-endpoint: ${LOGTO_ENDPOINT:} identity:
logto-public-endpoint: ${LOGTO_PUBLIC_ENDPOINT:} logtoendpoint: ${CAMELEER_SAAS_IDENTITY_LOGTOENDPOINT:}
m2m-client-id: ${LOGTO_M2M_CLIENT_ID:} logtopublicendpoint: ${CAMELEER_SAAS_IDENTITY_LOGTOPUBLICENDPOINT:}
m2m-client-secret: ${LOGTO_M2M_CLIENT_SECRET:} m2mclientid: ${CAMELEER_SAAS_IDENTITY_M2MCLIENTID:}
spa-client-id: ${LOGTO_SPA_CLIENT_ID:} m2mclientsecret: ${CAMELEER_SAAS_IDENTITY_M2MCLIENTSECRET:}
audience: ${CAMELEER_OIDC_AUDIENCE:https://api.cameleer.local} spaclientid: ${CAMELEER_SAAS_IDENTITY_SPACLIENTID:}
server-endpoint: ${CAMELEER3_SERVER_ENDPOINT:http://cameleer3-server:8081} audience: ${CAMELEER_SAAS_IDENTITY_AUDIENCE:https://api.cameleer.local}
provisioning: serverendpoint: ${CAMELEER_SAAS_IDENTITY_SERVERENDPOINT:http://cameleer3-server:8081}
server-image: ${CAMELEER_SERVER_IMAGE:gitea.siegeln.net/cameleer/cameleer3-server:latest} provisioning:
server-ui-image: ${CAMELEER_SERVER_UI_IMAGE:gitea.siegeln.net/cameleer/cameleer3-server-ui:latest} serverimage: ${CAMELEER_SAAS_PROVISIONING_SERVERIMAGE:gitea.siegeln.net/cameleer/cameleer3-server:latest}
network-name: ${CAMELEER_NETWORK:cameleer-saas_cameleer} serveruiimage: ${CAMELEER_SAAS_PROVISIONING_SERVERUIIMAGE:gitea.siegeln.net/cameleer/cameleer3-server-ui:latest}
traefik-network: ${CAMELEER_TRAEFIK_NETWORK:cameleer-traefik} networkname: ${CAMELEER_SAAS_PROVISIONING_NETWORKNAME:cameleer-saas_cameleer}
public-host: ${PUBLIC_HOST:localhost} traefiknetwork: ${CAMELEER_SAAS_PROVISIONING_TRAEFIKNETWORK:cameleer-traefik}
public-protocol: ${PUBLIC_PROTOCOL:https} publichost: ${CAMELEER_SAAS_PROVISIONING_PUBLICHOST:localhost}
datasource-url: ${CAMELEER_SERVER_DB_URL:jdbc:postgresql://postgres:5432/cameleer3} publicprotocol: ${CAMELEER_SAAS_PROVISIONING_PUBLICPROTOCOL:https}
clickhouse-url: ${CLICKHOUSE_URL:jdbc:clickhouse://clickhouse:8123/cameleer} datasourceurl: ${CAMELEER_SAAS_PROVISIONING_DATASOURCEURL:jdbc:postgresql://postgres:5432/cameleer3}
oidc-issuer-uri: ${PUBLIC_PROTOCOL:https}://${PUBLIC_HOST:localhost}/oidc clickhouseurl: ${CAMELEER_SAAS_PROVISIONING_CLICKHOUSEURL:jdbc:clickhouse://clickhouse:8123/cameleer}
oidc-jwk-set-uri: http://logto:3001/oidc/jwks oidcissueruri: ${cameleer.saas.provisioning.publicprotocol}://${cameleer.saas.provisioning.publichost}/oidc
cors-origins: ${PUBLIC_PROTOCOL:https}://${PUBLIC_HOST:localhost} oidcjwkseturi: http://logto:3001/oidc/jwks
corsorigins: ${cameleer.saas.provisioning.publicprotocol}://${cameleer.saas.provisioning.publichost}
certs:
path: ${CAMELEER_SAAS_CERTS_PATH:/certs}