diff --git a/.agents/skills/gateway-debug/SKILL.md b/.agents/skills/gateway-debug/SKILL.md index 4cb5d66b4b..92dc1877fb 100644 --- a/.agents/skills/gateway-debug/SKILL.md +++ b/.agents/skills/gateway-debug/SKILL.md @@ -117,12 +117,20 @@ cd "$REPO_ROOT/gateway" && docker compose down ## Step 2: Prepare the environment -> **One-time provisioning (required before the first `docker compose up`).** Run `./scripts/setup.sh` -> once from `/gateway` — it generates `api-platform.env` (required runtime defaults), the -> router's HTTPS listener certificate, and the AES-256 at-rest encryption key. The gateway never -> auto-generates keys/certs and has no demo mode: the compose `env_file:` is now `required: true` -> (a missing `api-platform.env` fails `docker compose up`), and the controller exits at startup if the -> encryption key is missing. Full reference: [Gateway Quick Start](../../../docs/gateway/quick-start-guide.md). +> **One-time provisioning (required before the first `docker compose up`).** Run setup once from +> `/gateway`. It generates `api-platform.env` (required runtime defaults), the router's HTTPS +> listener certificate, the AES-256 at-rest encryption key, and the gateway-controller **admin +> credentials**. Startup fails if basic auth is enabled with no credential, so run it non-interactively +> with fixed credentials to keep the `-u admin:admin` examples in this skill valid: +> +> ```bash +> ADMIN_USERNAME=admin ADMIN_PASSWORD=admin ./scripts/setup.sh +> ``` +> +> The gateway never auto-generates keys/certs and has no demo mode: the compose `env_file:` is now +> `required: true` (a missing `api-platform.env` fails `docker compose up`), and the controller exits at +> startup if the encryption key is missing. Full reference: +> [Gateway Quick Start](../../../docs/gateway/quick-start-guide.md). > **Compose target — pick one before editing anything.** Step 2 (and Step 8 > cleanup) operates on **one** compose file. Subsequent substeps reference @@ -372,7 +380,7 @@ is Go-only. This skill's `dlv` flow does not cover Python; see the ## Step 4: Reproduce the issue against the management REST API The management API runs on `http://localhost:9090/api/management/v0.9` -(basic auth `admin:admin`). Full endpoint list: +(basic auth — `admin:admin` if you provisioned it as recommended in Step 2). Full endpoint list: `/gateway/gateway-controller/api/management-openapi.yaml`. Most common: | Resource | Method + path | Example YAML / response | diff --git a/.agents/skills/gateway-integration-tests/SKILL.md b/.agents/skills/gateway-integration-tests/SKILL.md index 7b1d58f222..9937e137d3 100644 --- a/.agents/skills/gateway-integration-tests/SKILL.md +++ b/.agents/skills/gateway-integration-tests/SKILL.md @@ -140,8 +140,10 @@ grep -iE '' "$latest" | grep -iE 'error|warn|fail|panic|reje ### 4. Reproduce manually for assertion failures Bring up only the services you need, deploy the artifact by hand, and inspect. -The controller exposes two REST APIs (basic auth `admin:admin`); the router is -`http://localhost:8080`; Envoy admin is `http://localhost:9901`. +The controller exposes two REST APIs (basic auth `admin:admin` — the plaintext credential hardcoded in +the IT fixture `gateway/it/test-config.toml`, distinct from the shipped `config.toml`, which provisions +its admin credential via `setup.sh`); the router is `http://localhost:8080`; Envoy admin is +`http://localhost:9901`. - Management API — `http://localhost:9090/api/management/v0.9` — data-plane artifacts: REST APIs, LLM providers, LLM proxies, MCP servers, … diff --git a/.github/workflows/ai-workspace-pr-check.yml b/.github/workflows/ai-workspace-pr-check.yml index dc8b79b77e..3c7a14766c 100644 --- a/.github/workflows/ai-workspace-pr-check.yml +++ b/.github/workflows/ai-workspace-pr-check.yml @@ -56,8 +56,9 @@ jobs: run: echo "127.0.0.1 host.docker.internal" | sudo tee -a /etc/hosts - name: Generate quickstart keys, credentials and certificates - # setup.sh writes api-platform.env (encryption/JWT keys + admin credentials) and - # the TLS certs both services now require. Credentials are pinned to the + # setup.sh writes the admin credentials into api-platform.env, and provisions the + # at-rest encryption key + RS256 JWT keypair as files under resources/keys (read via + # {{ file }}) plus the TLS certs both services require. Credentials are pinned to the # Cypress defaults (admin/admin) for the E2E suite. run: ADMIN_USERNAME=admin ADMIN_PASSWORD=admin ./setup.sh working-directory: portals/ai-workspace diff --git a/.github/workflows/perf-gateway.yml b/.github/workflows/perf-gateway.yml index 00a5bc6343..e2c66cf17d 100644 --- a/.github/workflows/perf-gateway.yml +++ b/.github/workflows/perf-gateway.yml @@ -10,6 +10,10 @@ jobs: runs-on: ubuntu-latest timeout-minutes: 60 + env: + ADMIN_USERNAME: admin + ADMIN_PASSWORD: perf-admin + steps: - name: Checkout uses: actions/checkout@v4 @@ -31,6 +35,15 @@ jobs: test -f gateway/configs/config.toml echo "✅ gateway/configs/config.toml exists and is a file" + - name: Provision gateway secrets + admin credentials + run: | + cd gateway + # Non-interactive: ADMIN_USERNAME/ADMIN_PASSWORD come from the job env. + # setup.sh writes api-platform.env (listener cert, AES-256 encryption key, + # and the bcrypt-hashed admin credential the controller reads via the + # {{ env }} tokens in config.toml). Required now that config.toml + ./scripts/setup.sh + - name: Start gateway stack run: | cd gateway diff --git a/common/authenticators/authn.go b/common/authenticators/authn.go index 3388dbfcd0..9770702117 100644 --- a/common/authenticators/authn.go +++ b/common/authenticators/authn.go @@ -79,6 +79,8 @@ func AuthMiddleware(config models.AuthConfig, logger *slog.Logger) (func(http.Ha // No authenticators configured => run in no-auth mode. // This disables both authentication and authorization (via authzSkipKey). if len(authenticators) == 0 { + logger.Warn("no authentication method is configured — running with authentication and authorization DISABLED; " + + "every request is treated as an authenticated admin. Enable basic auth or an IDP to secure this service.") return func(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { authCtx := models.AuthContext{ diff --git a/distribution/all-in-one/docker-compose.yaml b/distribution/all-in-one/docker-compose.yaml index afe66130dd..30f2d52678 100644 --- a/distribution/all-in-one/docker-compose.yaml +++ b/distribution/all-in-one/docker-compose.yaml @@ -94,7 +94,8 @@ services: - platform-api-certs:/certs # One-shot init container: generates the RS256 keypair platform-api's - # auth.jwt config requires (config.toml reads it via {{ file }}). Tokens are + # auth.jwt config requires AND the 32-byte at-rest encryption key its + # security config requires — config.toml reads both via {{ file }}. Tokens are # signed asymmetrically — there is no shared HMAC secret anymore. platform-api-jwtkeygen: image: alpine/openssl @@ -102,15 +103,21 @@ services: command: - | set -e - [ -f /keys/jwt_private.pem ] && [ -f /keys/jwt_public.pem ] && exit 0 - openssl genpkey -algorithm RSA -pkeyopt rsa_keygen_bits:2048 \ + # Generate only what's missing — never overwrite an existing JWT keypair + # (that would invalidate already-issued tokens) or an existing encryption + # key (that would make previously-encrypted data unreadable). + [ -f /keys/jwt_private.pem ] || openssl genpkey -algorithm RSA -pkeyopt rsa_keygen_bits:2048 \ -out /keys/jwt_private.pem 2>/dev/null - openssl rsa -in /keys/jwt_private.pem -pubout \ + [ -f /keys/jwt_public.pem ] || openssl rsa -in /keys/jwt_private.pem -pubout \ -out /keys/jwt_public.pem 2>/dev/null + # At-rest encryption key: 32 bytes as 64 hex chars, read by config.toml + # via {{ file "/etc/platform-api/keys/encryption.key" }}. + [ -f /keys/encryption.key ] || openssl rand -hex 32 > /keys/encryption.key + # Applied every run to both freshly-generated and pre-existing keys. # jwtkeygen runs as root; platform-api runs as uid 10001, so the private - # key must be owned by that uid, not made world-readable. - chown 10001 /keys/jwt_private.pem - chmod 0600 /keys/jwt_private.pem + # key and encryption key must be owned by that uid, not world-readable. + chown 10001 /keys/jwt_private.pem /keys/encryption.key + chmod 0600 /keys/jwt_private.pem /keys/encryption.key chmod 0644 /keys/jwt_public.pem volumes: - platform-api-jwt-keys:/keys @@ -148,9 +155,6 @@ services: - APIP_CP_DATABASE_MAX_IDLE_CONNS=10 - APIP_CP_DATABASE_CONN_MAX_LIFETIME=300 - APIP_CP_DATABASE_EXECUTE_SCHEMA_DDL=true - # Required — set before `docker compose up`, e.g.: - # export APIP_CP_ENCRYPTION_KEY=$(openssl rand -hex 32) - - APIP_CP_ENCRYPTION_KEY=${APIP_CP_ENCRYPTION_KEY:-} depends_on: postgres: condition: service_healthy diff --git a/docs/ai-gateway/llm/guardrails/aws-bedrock-guardrail.md b/docs/ai-gateway/llm/guardrails/aws-bedrock-guardrail.md index a7dd7ac49d..22204c21ca 100644 --- a/docs/ai-gateway/llm/guardrails/aws-bedrock-guardrail.md +++ b/docs/ai-gateway/llm/guardrails/aws-bedrock-guardrail.md @@ -113,7 +113,7 @@ Deploy an LLM provider with AWS Bedrock Guardrail validation: ```bash curl -X POST http://localhost:9090/api/management/v0.9/llm-providers \ -H "Content-Type: application/yaml" \ - -H "Authorization: Basic YWRtaW46YWRtaW4=" \ + -u "$ADMIN_USERNAME:$ADMIN_PASSWORD" \ --data-binary @- <<'EOF' apiVersion: gateway.api-platform.wso2.com/v1 kind: LlmProvider diff --git a/docs/ai-gateway/llm/guardrails/azure-content-safety.md b/docs/ai-gateway/llm/guardrails/azure-content-safety.md index a385210bba..a38464ac02 100644 --- a/docs/ai-gateway/llm/guardrails/azure-content-safety.md +++ b/docs/ai-gateway/llm/guardrails/azure-content-safety.md @@ -104,7 +104,7 @@ Deploy an LLM provider with Azure Content Safety validation: ```bash curl -X POST http://localhost:9090/api/management/v0.9/llm-providers \ -H "Content-Type: application/yaml" \ - -H "Authorization: Basic YWRtaW46YWRtaW4=" \ + -u "$ADMIN_USERNAME:$ADMIN_PASSWORD" \ --data-binary @- <<'EOF' apiVersion: gateway.api-platform.wso2.com/v1 kind: LlmProvider diff --git a/docs/ai-gateway/llm/guardrails/content-length.md b/docs/ai-gateway/llm/guardrails/content-length.md index 742a7e89b7..903f839a40 100644 --- a/docs/ai-gateway/llm/guardrails/content-length.md +++ b/docs/ai-gateway/llm/guardrails/content-length.md @@ -56,7 +56,7 @@ Deploy an LLM provider that limits request payloads to between 100 bytes and 1MB ```bash curl -X POST http://localhost:9090/api/management/v0.9/llm-providers \ -H "Content-Type: application/yaml" \ - -H "Authorization: Basic YWRtaW46YWRtaW4=" \ + -u "$ADMIN_USERNAME:$ADMIN_PASSWORD" \ --data-binary @- <<'EOF' apiVersion: gateway.api-platform.wso2.com/v1 kind: LlmProvider diff --git a/docs/ai-gateway/llm/guardrails/json-schema.md b/docs/ai-gateway/llm/guardrails/json-schema.md index 547ac96197..eb9712ccc0 100644 --- a/docs/ai-gateway/llm/guardrails/json-schema.md +++ b/docs/ai-gateway/llm/guardrails/json-schema.md @@ -65,7 +65,7 @@ Deploy an LLM provider that validates that request contains a user object with r ```bash curl -X POST http://localhost:9090/api/management/v0.9/llm-providers \ -H "Content-Type: application/yaml" \ - -H "Authorization: Basic YWRtaW46YWRtaW4=" \ + -u "$ADMIN_USERNAME:$ADMIN_PASSWORD" \ --data-binary @- <<'EOF' apiVersion: gateway.api-platform.wso2.com/v1 kind: LlmProvider diff --git a/docs/ai-gateway/llm/guardrails/pii-masking-regex.md b/docs/ai-gateway/llm/guardrails/pii-masking-regex.md index c0e671dd5c..f9c41a6887 100644 --- a/docs/ai-gateway/llm/guardrails/pii-masking-regex.md +++ b/docs/ai-gateway/llm/guardrails/pii-masking-regex.md @@ -66,7 +66,7 @@ Deploy an LLM provider that masks email addresses and phone numbers in requests ```bash curl -X POST http://localhost:9090/api/management/v0.9/llm-providers \ -H "Content-Type: application/yaml" \ - -H "Authorization: Basic YWRtaW46YWRtaW4=" \ + -u "$ADMIN_USERNAME:$ADMIN_PASSWORD" \ --data-binary @- <<'EOF' apiVersion: gateway.api-platform.wso2.com/v1 kind: LlmProvider diff --git a/docs/ai-gateway/llm/guardrails/regex.md b/docs/ai-gateway/llm/guardrails/regex.md index 2b22be7c2d..5c3be319f4 100644 --- a/docs/ai-gateway/llm/guardrails/regex.md +++ b/docs/ai-gateway/llm/guardrails/regex.md @@ -65,7 +65,7 @@ Deploy an LLM provider that protects against sensitive data leaks by blocking an ```bash curl -X POST http://localhost:9090/api/management/v0.9/llm-providers \ -H "Content-Type: application/yaml" \ - -H "Authorization: Basic YWRtaW46YWRtaW4=" \ + -u "$ADMIN_USERNAME:$ADMIN_PASSWORD" \ --data-binary @- <<'EOF' apiVersion: gateway.api-platform.wso2.com/v1 kind: LlmProvider diff --git a/docs/ai-gateway/llm/guardrails/semantic-prompt-guard.md b/docs/ai-gateway/llm/guardrails/semantic-prompt-guard.md index ce2f7f5d3a..9bfa19175c 100644 --- a/docs/ai-gateway/llm/guardrails/semantic-prompt-guard.md +++ b/docs/ai-gateway/llm/guardrails/semantic-prompt-guard.md @@ -115,7 +115,7 @@ Deploy an LLM provider that blocks prompts similar to prohibited phrases: ```bash curl -X POST http://localhost:9090/api/management/v0.9/llm-providers \ -H "Content-Type: application/yaml" \ - -H "Authorization: Basic YWRtaW46YWRtaW4=" \ + -u "$ADMIN_USERNAME:$ADMIN_PASSWORD" \ --data-binary @- <<'EOF' apiVersion: gateway.api-platform.wso2.com/v1 kind: LlmProvider @@ -195,7 +195,7 @@ Deploy an LLM provider that only allows prompts similar to approved phrases: ```bash curl -X POST http://localhost:9090/api/management/v0.9/llm-providers \ -H "Content-Type: application/yaml" \ - -H "Authorization: Basic YWRtaW46YWRtaW4=" \ + -u "$ADMIN_USERNAME:$ADMIN_PASSWORD" \ --data-binary @- <<'EOF' apiVersion: gateway.api-platform.wso2.com/v1 kind: LlmProvider diff --git a/docs/ai-gateway/llm/guardrails/sentence-count.md b/docs/ai-gateway/llm/guardrails/sentence-count.md index bcaa01e35f..6cd4a5f121 100644 --- a/docs/ai-gateway/llm/guardrails/sentence-count.md +++ b/docs/ai-gateway/llm/guardrails/sentence-count.md @@ -65,7 +65,7 @@ Deploy an LLM provider that ensures requests contain between 1 and 10 sentences: ```bash curl -X POST http://localhost:9090/api/management/v0.9/llm-providers \ -H "Content-Type: application/yaml" \ - -H "Authorization: Basic YWRtaW46YWRtaW4=" \ + -u "$ADMIN_USERNAME:$ADMIN_PASSWORD" \ --data-binary @- <<'EOF' apiVersion: gateway.api-platform.wso2.com/v1 kind: LlmProvider diff --git a/docs/ai-gateway/llm/guardrails/url.md b/docs/ai-gateway/llm/guardrails/url.md index 7fc98122f6..e955e5dc2e 100644 --- a/docs/ai-gateway/llm/guardrails/url.md +++ b/docs/ai-gateway/llm/guardrails/url.md @@ -73,7 +73,7 @@ Deploy an LLM provider that validates URLs in request content using HTTP HEAD re ```bash curl -X POST http://localhost:9090/api/management/v0.9/llm-providers \ -H "Content-Type: application/yaml" \ - -H "Authorization: Basic YWRtaW46YWRtaW4=" \ + -u "$ADMIN_USERNAME:$ADMIN_PASSWORD" \ --data-binary @- <<'EOF' apiVersion: gateway.api-platform.wso2.com/v1 kind: LlmProvider diff --git a/docs/ai-gateway/llm/guardrails/word-count.md b/docs/ai-gateway/llm/guardrails/word-count.md index 6e0611ddb8..45e791a15c 100644 --- a/docs/ai-gateway/llm/guardrails/word-count.md +++ b/docs/ai-gateway/llm/guardrails/word-count.md @@ -56,7 +56,7 @@ Deploy an LLM provider that validates request messages contain between 10 and 50 ```bash curl -X POST http://localhost:9090/api/management/v0.9/llm-providers \ -H "Content-Type: application/yaml" \ - -H "Authorization: Basic YWRtaW46YWRtaW4=" \ + -u "$ADMIN_USERNAME:$ADMIN_PASSWORD" \ --data-binary @- <<'EOF' apiVersion: gateway.api-platform.wso2.com/v1 kind: LlmProvider diff --git a/docs/ai-gateway/llm/llm-templates.md b/docs/ai-gateway/llm/llm-templates.md index 81138484a1..a2a37e17f2 100644 --- a/docs/ai-gateway/llm/llm-templates.md +++ b/docs/ai-gateway/llm/llm-templates.md @@ -284,7 +284,7 @@ To create an LLM provider using any of the out-of-the-box templates: ```bash curl -X POST http://localhost:9090/api/management/v0.9/llm-providers \ -H "Content-Type: application/yaml" \ - -H "Authorization: Basic YWRtaW46YWRtaW4=" \ + -u "$ADMIN_USERNAME:$ADMIN_PASSWORD" \ --data-binary @- <<'EOF' apiVersion: gateway.api-platform.wso2.com/v1 kind: LlmProvider @@ -333,7 +333,7 @@ To list all available LLM provider templates: ```bash curl -X GET http://localhost:9090/api/management/v0.9/llm-provider-templates \ - -H "Authorization: Basic YWRtaW46YWRtaW4=" + -u "$ADMIN_USERNAME:$ADMIN_PASSWORD" ``` ### Retrieving a Specific Template @@ -342,7 +342,7 @@ To retrieve details of a specific template: ```bash curl -X GET http://localhost:9090/api/management/v0.9/llm-provider-templates/openai \ - -H "Authorization: Basic YWRtaW46YWRtaW4=" + -u "$ADMIN_USERNAME:$ADMIN_PASSWORD" ``` ### Creating Custom Templates @@ -352,7 +352,7 @@ Platform administrators can create custom templates for LLM providers not covere ```bash curl -X POST http://localhost:9090/api/management/v0.9/llm-provider-templates \ -H "Content-Type: application/yaml" \ - -H "Authorization: Basic YWRtaW46YWRtaW4=" \ + -u "$ADMIN_USERNAME:$ADMIN_PASSWORD" \ --data-binary @- <<'EOF' apiVersion: gateway.api-platform.wso2.com/v1 kind: LlmProviderTemplate @@ -373,7 +373,7 @@ To update an existing custom template: ```bash curl -X PUT http://localhost:9090/api/management/v0.9/llm-provider-templates/custom-provider \ -H "Content-Type: application/yaml" \ - -H "Authorization: Basic YWRtaW46YWRtaW4=" \ + -u "$ADMIN_USERNAME:$ADMIN_PASSWORD" \ --data-binary @- <<'EOF' apiVersion: gateway.api-platform.wso2.com/v1 kind: LlmProviderTemplate @@ -394,7 +394,7 @@ To delete a custom template: ```bash curl -X DELETE http://localhost:9090/api/management/v0.9/llm-provider-templates/custom-provider \ - -H "Authorization: Basic YWRtaW46YWRtaW4=" + -u "$ADMIN_USERNAME:$ADMIN_PASSWORD" ``` **Note**: Out-of-the-box templates cannot be deleted or modified. Only custom templates created by platform administrators can be updated or deleted. diff --git a/docs/ai-gateway/llm/load-balancing/model-round-robin.md b/docs/ai-gateway/llm/load-balancing/model-round-robin.md index 04bb536cfc..07cfadefb1 100644 --- a/docs/ai-gateway/llm/load-balancing/model-round-robin.md +++ b/docs/ai-gateway/llm/load-balancing/model-round-robin.md @@ -55,7 +55,7 @@ Deploy an LLM provider with round-robin load balancing across multiple models: ```bash curl -X POST http://localhost:9090/api/management/v0.9/llm-providers \ -H "Content-Type: application/yaml" \ - -H "Authorization: Basic YWRtaW46YWRtaW4=" \ + -u "$ADMIN_USERNAME:$ADMIN_PASSWORD" \ --data-binary @- <<'EOF' apiVersion: gateway.api-platform.wso2.com/v1 kind: LlmProvider diff --git a/docs/ai-gateway/llm/load-balancing/model-weighted-round-robin.md b/docs/ai-gateway/llm/load-balancing/model-weighted-round-robin.md index 333885111c..75fdfe53cd 100644 --- a/docs/ai-gateway/llm/load-balancing/model-weighted-round-robin.md +++ b/docs/ai-gateway/llm/load-balancing/model-weighted-round-robin.md @@ -70,7 +70,7 @@ Deploy an LLM provider with weighted round-robin load balancing: ```bash curl -X POST http://localhost:9090/api/management/v0.9/llm-providers \ -H "Content-Type: application/yaml" \ - -H "Authorization: Basic YWRtaW46YWRtaW4=" \ + -u "$ADMIN_USERNAME:$ADMIN_PASSWORD" \ --data-binary @- <<'EOF' apiVersion: gateway.api-platform.wso2.com/v1 kind: LlmProvider diff --git a/docs/ai-gateway/llm/prompt-management/prompt-decorator.md b/docs/ai-gateway/llm/prompt-management/prompt-decorator.md index 251f5e3b92..80bf27cca4 100644 --- a/docs/ai-gateway/llm/prompt-management/prompt-decorator.md +++ b/docs/ai-gateway/llm/prompt-management/prompt-decorator.md @@ -84,7 +84,7 @@ Add a summarization instruction to user prompts: ```bash curl -X POST http://localhost:9090/api/management/v0.9/llm-providers \ -H "Content-Type: application/yaml" \ - -H "Authorization: Basic YWRtaW46YWRtaW4=" \ + -u "$ADMIN_USERNAME:$ADMIN_PASSWORD" \ --data-binary @- <<'EOF' apiVersion: gateway.api-platform.wso2.com/v1 kind: LlmProvider @@ -157,7 +157,7 @@ Add a system message to define AI behavior: ```bash curl -X POST http://localhost:9090/api/management/v0.9/llm-providers \ -H "Content-Type: application/yaml" \ - -H "Authorization: Basic YWRtaW46YWRtaW4=" \ + -u "$ADMIN_USERNAME:$ADMIN_PASSWORD" \ --data-binary @- <<'EOF' apiVersion: gateway.api-platform.wso2.com/v1 kind: LlmProvider diff --git a/docs/ai-gateway/llm/prompt-management/prompt-template.md b/docs/ai-gateway/llm/prompt-management/prompt-template.md index c9acdc5813..0d47559dd9 100644 --- a/docs/ai-gateway/llm/prompt-management/prompt-template.md +++ b/docs/ai-gateway/llm/prompt-management/prompt-template.md @@ -81,7 +81,7 @@ Deploy an LLM provider with a translation prompt template: ```bash curl -X POST http://localhost:9090/api/management/v0.9/llm-providers \ -H "Content-Type: application/yaml" \ - -H "Authorization: Basic YWRtaW46YWRtaW4=" \ + -u "$ADMIN_USERNAME:$ADMIN_PASSWORD" \ --data-binary @- <<'EOF' apiVersion: gateway.api-platform.wso2.com/v1 kind: LlmProvider @@ -154,7 +154,7 @@ Create a template for summarizing content with configurable length: ```bash curl -X POST http://localhost:9090/api/management/v0.9/llm-providers \ -H "Content-Type: application/yaml" \ - -H "Authorization: Basic YWRtaW46YWRtaW4=" \ + -u "$ADMIN_USERNAME:$ADMIN_PASSWORD" \ --data-binary @- <<'EOF' apiVersion: gateway.api-platform.wso2.com/v1 kind: LlmProvider diff --git a/docs/ai-gateway/llm/quick-start-guide.md b/docs/ai-gateway/llm/quick-start-guide.md index 6d40a09689..4962016cfe 100644 --- a/docs/ai-gateway/llm/quick-start-guide.md +++ b/docs/ai-gateway/llm/quick-start-guide.md @@ -40,10 +40,18 @@ curl http://localhost:9094/health The API Platform Gateway currently includes first-class support for the OpenAI LLM provider. As a platform administrator, replace `` with your openai API key and run the following command to deploy a sample OpenAI LLM provider. +> The management API uses basic auth. Export the admin credentials `scripts/setup.sh` provisioned — the +> username defaults to `admin`; use the password it printed: +> +> ```bash +> export ADMIN_USERNAME=admin +> export ADMIN_PASSWORD='' +> ``` + ```bash curl -X POST http://localhost:9090/api/management/v0.9/llm-providers \ -H "Content-Type: application/yaml" \ - -H "Authorization: Basic YWRtaW46YWRtaW4=" \ + -u "$ADMIN_USERNAME:$ADMIN_PASSWORD" \ --data-binary @- <<'EOF' apiVersion: gateway.api-platform.wso2.com/v1 kind: LlmProvider @@ -95,7 +103,7 @@ The API Platform Gateway provides first-class support for configuring and deploy ```bash curl -X POST http://localhost:9090/api/management/v0.9/llm-proxies \ -H "Content-Type: application/yaml" \ - -H "Authorization: Basic YWRtaW46YWRtaW4=" \ + -u "$ADMIN_USERNAME:$ADMIN_PASSWORD" \ --data-binary @- <<'EOF' apiVersion: gateway.api-platform.wso2.com/v1 kind: LlmProxy diff --git a/docs/ai-gateway/llm/semantic-caching.md b/docs/ai-gateway/llm/semantic-caching.md index 15c69c34d5..68a39be451 100644 --- a/docs/ai-gateway/llm/semantic-caching.md +++ b/docs/ai-gateway/llm/semantic-caching.md @@ -118,7 +118,7 @@ Deploy an LLM provider with semantic caching using OpenAI embeddings and Redis v ```bash curl -X POST http://localhost:9090/api/management/v0.9/llm-providers \ -H "Content-Type: application/yaml" \ - -H "Authorization: Basic YWRtaW46YWRtaW4=" \ + -u "$ADMIN_USERNAME:$ADMIN_PASSWORD" \ --data-binary @- <<'EOF' apiVersion: gateway.api-platform.wso2.com/v1 kind: LlmProvider diff --git a/docs/ai-gateway/mcp/quick-start-guide.md b/docs/ai-gateway/mcp/quick-start-guide.md index 2eb663317d..74cf54aa2a 100644 --- a/docs/ai-gateway/mcp/quick-start-guide.md +++ b/docs/ai-gateway/mcp/quick-start-guide.md @@ -47,10 +47,18 @@ docker run -p 3001:3001 --name everything --network ai-gateway_gateway-network r Run the following command to deploy the MCP proxy. +> The management API uses basic auth. Export the admin credentials `scripts/setup.sh` provisioned — the +> username defaults to `admin`; use the password it printed: +> +> ```bash +> export ADMIN_USERNAME=admin +> export ADMIN_PASSWORD='' +> ``` + ```bash curl -X POST http://localhost:9090/api/management/v0.9/mcp-proxies \ -H "Content-Type: application/yaml" \ - -H "Authorization: Basic YWRtaW46YWRtaW4=" \ + -u "$ADMIN_USERNAME:$ADMIN_PASSWORD" \ --data-binary @- <<'EOF' apiVersion: gateway.api-platform.wso2.com/v1 kind: Mcp diff --git a/docs/ai-workspace/configuration.md b/docs/ai-workspace/configuration.md index 049dea7706..7c46c2f9b1 100644 --- a/docs/ai-workspace/configuration.md +++ b/docs/ai-workspace/configuration.md @@ -184,8 +184,8 @@ Instead, reference each in the TOML with an interpolation token that is resolved startup — from an environment variable, or preferably from a mounted secret file: ```toml -encryption_key = '{{ env "APIP_CP_ENCRYPTION_KEY" }}' # from an env var -secret_key = '{{ file "/secrets/platform-api/jwt_secret" }}' # from a file (preferred) +encryption_key = '{{ file "/etc/platform-api/keys/encryption.key" }}' # shipped default — from a mounted file +# encryption_key = '{{ env "APIP_CP_ENCRYPTION_KEY" }}' # alternatively, from an env var ``` Supply the env values from a git-ignored `api-platform.env` and start with `docker compose up` diff --git a/docs/ai-workspace/features/secrets-management.md b/docs/ai-workspace/features/secrets-management.md index 66586b1c83..fa9d746f5d 100644 --- a/docs/ai-workspace/features/secrets-management.md +++ b/docs/ai-workspace/features/secrets-management.md @@ -333,21 +333,19 @@ Attempting to delete a secret that is still in use returns HTTP 409. To remove i ## Configuration -The following environment variable controls encryption for the Platform API. It is -required and never auto-generated — the Platform API fails to start if it is missing or -malformed. +The at-rest encryption key controls encryption for the Platform API. It is required and never +auto-generated by the server — the Platform API fails to start if it is missing or malformed. -| Key | Purpose | -|-----|---------| -| `APIP_CP_ENCRYPTION_KEY` | Encrypts secrets, subscription tokens, and WebSub HMAC secrets at rest. | +| Setting | Purpose | +|---------|---------| +| `security.encryption_key` | Encrypts secrets, subscription tokens, and WebSub HMAC secrets at rest. | -Generate a stable key with: - -```sh -openssl rand -hex 32 -``` +For Docker Compose deployments, the shipped config provisions this key as a **mounted file** — the +default: `setup.sh` writes `resources/keys/encryption.key` (mounted at `/etc/platform-api/keys`) and +`config.toml` reads it via `{{ file }}`. As an alternative, you can supply the value through the +`APIP_CP_ENCRYPTION_KEY` environment variable by switching the token to the `{{ env }}` form (see below). -For Docker Compose deployments, set the key in a `api-platform.env` file next to `docker-compose.yaml`. First generate a key: +Generate a stable key with: ```sh openssl rand -hex 32 @@ -363,10 +361,11 @@ preferably, from a mounted file: ```toml # config-platform-api.toml -encryption_key = '{{ env "APIP_CP_ENCRYPTION_KEY" }}' # from an environment variable +# From a mounted file provisioned by setup.sh: +encryption_key = '{{ file "/etc/platform-api/keys/encryption.key" }}' -# or (preferred) — from a mounted secret file: -encryption_key = '{{ file "/secrets/platform-api/encryption_key" }}' +# or — from an environment variable supplied via a git-ignored env file: +encryption_key = '{{ env "APIP_CP_ENCRYPTION_KEY" }}' ``` Resolution fails closed: if a referenced env var is unset/empty or a referenced file is diff --git a/docs/gateway/bottom-up-api-deployment-guide.md b/docs/gateway/bottom-up-api-deployment-guide.md index 49373d95a5..b3fc5b0253 100644 --- a/docs/gateway/bottom-up-api-deployment-guide.md +++ b/docs/gateway/bottom-up-api-deployment-guide.md @@ -117,9 +117,10 @@ gateway_name = "onprem-gw" enabled = true [[controller.auth.basic.users]] -username = "admin" -password = "admin" -roles = ["admin"] +username = '{{ env "APIP_GW_CONTROLLER_AUTH_BASIC_ADMIN_USERNAME" "" }}' +password = '{{ env "APIP_GW_CONTROLLER_AUTH_BASIC_ADMIN_PASSWORD_HASH" "" }}' +password_hashed = true +roles = ["admin"] ``` --- @@ -359,10 +360,18 @@ Save the API definition as `petstore-api.json`: **Step 2: Deploy to Gateway** +The management API uses basic auth. Export the admin credentials `scripts/setup.sh` provisioned (the +username defaults to `admin`; use the password it printed) so the calls below can authenticate: + +```bash +export ADMIN_USERNAME=admin +export ADMIN_PASSWORD='' +``` + ```bash curl -X POST http://localhost:9090/rest-apis \ -H "Content-Type: application/json" \ - -H "Authorization: Basic YWRtaW46YWRtaW4=" \ + -u "$ADMIN_USERNAME:$ADMIN_PASSWORD" \ -d @petstore-api.json ``` @@ -384,7 +393,7 @@ curl -X POST http://localhost:9090/rest-apis \ ```bash # Check if API synced to on-prem APIM curl -X GET http://localhost:9090/rest-apis/PetStoreAPI \ - -H "Authorization: Basic YWRtaW46YWRtaW4=" | jq '{origin, cpSyncStatus}' + -u "$ADMIN_USERNAME:$ADMIN_PASSWORD" | jq '{origin, cpSyncStatus}' ``` **Response (before sync):** @@ -409,7 +418,7 @@ curl -X GET http://localhost:9090/rest-apis/PetStoreAPI \ # Get API key curl -X POST http://localhost:9090/rest-apis/PetStoreAPI/api-keys \ -H "Content-Type: application/json" \ - -H "Authorization: Basic YWRtaW46YWRtaW4=" \ + -u "$ADMIN_USERNAME:$ADMIN_PASSWORD" \ -d '{"name": "test-key"}' # Invoke API @@ -488,7 +497,7 @@ Update your API definition to add a rate limit policy: ```bash curl -X PUT http://localhost:9090/rest-apis/PetStoreAPI \ -H "Content-Type: application/json" \ - -H "Authorization: Basic YWRtaW46YWRtaW4=" \ + -u "$ADMIN_USERNAME:$ADMIN_PASSWORD" \ -d @petstore-api-updated.json ``` @@ -503,7 +512,7 @@ curl -X PUT http://localhost:9090/rest-apis/PetStoreAPI \ ```bash curl -X GET http://localhost:9090/rest-apis/PetStoreAPI \ - -H "Authorization: Basic YWRtaW46YWRtaW4=" | jq '{cpSyncStatus, policies}' + -u "$ADMIN_USERNAME:$ADMIN_PASSWORD" | jq '{cpSyncStatus, policies}' ``` --- @@ -546,7 +555,7 @@ To undeploy an API, set `desiredState: undeployed` and update: ```bash curl -X PUT http://localhost:9090/rest-apis/PetStoreAPI \ -H "Content-Type: application/json" \ - -H "Authorization: Basic YWRtaW46YWRtaW4=" \ + -u "$ADMIN_USERNAME:$ADMIN_PASSWORD" \ -d @petstore-api-undeploy.json ``` @@ -561,7 +570,7 @@ curl -X PUT http://localhost:9090/rest-apis/PetStoreAPI \ ```bash # Check API state curl -X GET http://localhost:9090/rest-apis/PetStoreAPI \ - -H "Authorization: Basic YWRtaW46YWRtaW4=" | jq '{desiredState, cpSyncStatus}' + -u "$ADMIN_USERNAME:$ADMIN_PASSWORD" | jq '{desiredState, cpSyncStatus}' ``` **Response:** @@ -583,7 +592,7 @@ curl -X GET http://localhost:9090/rest-apis/PetStoreAPI \ ```bash curl -X POST http://localhost:9090/rest-apis/PetStoreAPI/api-keys \ -H "Content-Type: application/json" \ - -H "Authorization: Basic YWRtaW46YWRtaW4=" \ + -u "$ADMIN_USERNAME:$ADMIN_PASSWORD" \ -d '{ "name": "my-api-key" }' @@ -610,14 +619,14 @@ curl http://localhost:8080/petstore/pet/1 \ ```bash curl -X GET http://localhost:9090/rest-apis/PetStoreAPI/api-keys \ - -H "Authorization: Basic YWRtaW46YWRtaW4=" + -u "$ADMIN_USERNAME:$ADMIN_PASSWORD" ``` ### Revoke API Key ```bash curl -X DELETE http://localhost:9090/rest-apis/PetStoreAPI/api-keys/key-uuid-12345 \ - -H "Authorization: Basic YWRtaW46YWRtaW4=" + -u "$ADMIN_USERNAME:$ADMIN_PASSWORD" ``` --- @@ -642,7 +651,7 @@ If sync fails, the gateway automatically retries up to 3 times. To manually trig ```bash curl -X PUT http://localhost:9090/rest-apis/PetStoreAPI \ -H "Content-Type: application/json" \ - -H "Authorization: Basic YWRtaW46YWRtaW4=" \ + -u "$ADMIN_USERNAME:$ADMIN_PASSWORD" \ -d @petstore-api.json ``` @@ -711,7 +720,7 @@ curl -k https://192.168.0.102:9443/internal/gateway/.well-known ```bash curl -X GET http://localhost:9090/rest-apis/PetStoreAPI \ - -H "Authorization: Basic YWRtaW46YWRtaW4=" | jq '.spec.upstream' + -u "$ADMIN_USERNAME:$ADMIN_PASSWORD" | jq '.spec.upstream' ``` **Fix:** Ensure `upstream.main.url` is set to a valid backend service @@ -734,7 +743,7 @@ APIP_GW_CONTROLLER_LOGGING_LEVEL=debug ```bash curl -X PUT http://localhost:9090/rest-apis/PetStoreAPI \ -H "Content-Type: application/json" \ - -H "Authorization: Basic YWRtaW46YWRtaW4=" \ + -u "$ADMIN_USERNAME:$ADMIN_PASSWORD" \ -d @api-definition.json ``` diff --git a/docs/gateway/kubernetes/gateway-operator.md b/docs/gateway/kubernetes/gateway-operator.md index 975bdb529a..d6adf84922 100644 --- a/docs/gateway/kubernetes/gateway-operator.md +++ b/docs/gateway/kubernetes/gateway-operator.md @@ -709,9 +709,18 @@ curl -X GET "https://raw.githubusercontent.com/wso2/api-platform/refs/heads/main ### 2. Add Certificate to Gateway +The management API uses basic auth with the credentials from your Helm values +(`controller.auth.basic.users`; the chart default is `admin` / `admin`). Export them, changing them if +you overrode the chart defaults: + +```sh +export ADMIN_USERNAME=admin +export ADMIN_PASSWORD=admin +``` + ```sh cert_path="/tmp/test-backend.crt" -curl -X POST http://localhost:9090/api/management/v0.9/certificates -u "admin:admin" \ +curl -X POST http://localhost:9090/api/management/v0.9/certificates -u "$ADMIN_USERNAME:$ADMIN_PASSWORD" \ -H "Content-Type: application/json" \ -d "{\"certificate\":$(jq -Rs . < $cert_path),\"filename\":\"my-cert.pem\", \"name\":\"test\"}" ``` diff --git a/docs/gateway/kubernetes/kubernetes-standalone.md b/docs/gateway/kubernetes/kubernetes-standalone.md index c8a9b1f13f..e3347baa4d 100644 --- a/docs/gateway/kubernetes/kubernetes-standalone.md +++ b/docs/gateway/kubernetes/kubernetes-standalone.md @@ -203,9 +203,18 @@ curl http://localhost:9094/api/admin/v0.9/health ### Deploy an API configuration +The management API uses basic auth with the credentials from your Helm values +(`controller.auth.basic.users`; the chart default is `admin` / `admin`). Export them for the calls below, +changing them if you overrode the chart defaults: + +```bash +export ADMIN_USERNAME=admin +export ADMIN_PASSWORD=admin +``` + ```bash curl -X POST http://localhost:9090/api/management/v0.9/rest-apis \ - -u admin:admin \ + -u "$ADMIN_USERNAME:$ADMIN_PASSWORD" \ -H "Content-Type: application/yaml" \ --data-binary @- <<'EOF' apiVersion: gateway.api-platform.wso2.com/v1 diff --git a/docs/gateway/quick-start-guide.md b/docs/gateway/quick-start-guide.md index b34b0f76cd..7143fb77e7 100644 --- a/docs/gateway/quick-start-guide.md +++ b/docs/gateway/quick-start-guide.md @@ -30,9 +30,16 @@ unzip wso2apip-api-gateway-1.2.0-beta.zip cd wso2apip-api-gateway-v1.2.0-beta/ -# One-time setup: provision the HTTPS listener certificate and api-platform.env. +# One-time setup: provision the HTTPS listener certificate, the encryption key, +# api-platform.env, and the gateway-controller admin credentials. setup.sh prints +# the admin password once — copy it now. ./scripts/setup.sh +# Export the admin credentials so the management-API calls below can authenticate. +# The username defaults to "admin"; use the password setup.sh just printed. +export ADMIN_USERNAME=admin +export ADMIN_PASSWORD='' + # Start the complete stack docker compose up -d @@ -41,7 +48,7 @@ curl http://localhost:9094/api/admin/v0.9/health # Deploy an API configuration curl -X POST http://localhost:9090/api/management/v0.9/rest-apis \ - -u admin:admin \ + -u "$ADMIN_USERNAME:$ADMIN_PASSWORD" \ -H "Content-Type: application/yaml" \ --data-binary @- <<'EOF' apiVersion: gateway.api-platform.wso2.com/v1 @@ -103,14 +110,39 @@ It provisions, idempotently (existing files are kept unless `--force`): |---|---| | `listener-certs/default-listener.crt` / `.key` | Self-signed certificate for the router's HTTPS ingress listener (`:8443`). | | `aesgcm-keys/default-aesgcm256-v1.bin` | AES-256 key for at-rest encryption of stored secrets (bind-mounted into the controller). | -| `api-platform.env` | Required runtime settings, loaded into both containers via docker-compose `env_file:` — `GATEWAY_CONTROLLER_HOST` and `LOG_LEVEL`. | +| `api-platform.env` | Required runtime settings, loaded into both containers via docker-compose `env_file:` — `GATEWAY_CONTROLLER_HOST`, `LOG_LEVEL`, and the gateway-controller admin credentials (`APIP_GW_CONTROLLER_AUTH_BASIC_ADMIN_USERNAME` and the bcrypt `APIP_GW_CONTROLLER_AUTH_BASIC_ADMIN_PASSWORD_HASH`). | + +**Admin credentials.** The gateway-controller REST/management API is protected by basic auth. +Two sets of variables are involved, and `setup.sh` bridges them: + +- **What you provide** — the plaintext inputs to `setup.sh` (also what you pass to `curl -u`): + - `ADMIN_USERNAME` — defaults to `admin` (override via the environment or the interactive prompt). + - `ADMIN_PASSWORD` — used if set; otherwise you are prompted; otherwise a strong random one is generated. +- **What `setup.sh` writes** into `api-platform.env` — the variables `config.toml` actually reads via its + `{{ env }}` tokens (you normally never set these by hand): + - `APIP_GW_CONTROLLER_AUTH_BASIC_ADMIN_USERNAME` ← your `ADMIN_USERNAME`. + - `APIP_GW_CONTROLLER_AUTH_BASIC_ADMIN_PASSWORD_HASH` ← the **bcrypt hash** of your `ADMIN_PASSWORD`. + +The plaintext password is printed to the console **once** and never stored — copy it, and use it with the +username for `curl -u "$ADMIN_USERNAME:$ADMIN_PASSWORD"` against the management API. + +For non-interactive use (CI), set the plaintext inputs up front: + +```bash +ADMIN_USERNAME=admin ADMIN_PASSWORD='choose-a-strong-password' ./scripts/setup.sh +``` + +If the controller starts with the shipped `config.toml` while `APIP_GW_CONTROLLER_AUTH_BASIC_ADMIN_USERNAME` +/ `..._PASSWORD_HASH` are unset (i.e. you never ran `setup.sh`), it **refuses to start** rather than coming +up on an empty credential — so always run `setup.sh` first. To populate those two config variables by hand +instead, set the username and a **bcrypt hash** of the password (never the plaintext). **Options:** | Flag | Effect | |---|---| -| `--force` | Regenerate the certificate and encryption key, and rewrite `api-platform.env` (rotates them). | -| `--certs-only` | Generate only the listener TLS certificate (skip the encryption key and `api-platform.env`). | +| `--force` | Regenerate the certificate and encryption key, and rewrite `api-platform.env` — this **rotates the admin password** too (a new one is generated/prompted and printed). | +| `--certs-only` | Generate only the listener TLS certificate (skip the encryption key, admin credentials, and `api-platform.env`). | | `--help` | Print usage. | Then start the stack: diff --git a/event-gateway/docker-compose.test.postgres.yaml b/event-gateway/docker-compose.test.postgres.yaml index be1425b0c6..0527c1ac72 100644 --- a/event-gateway/docker-compose.test.postgres.yaml +++ b/event-gateway/docker-compose.test.postgres.yaml @@ -57,6 +57,8 @@ services: - APIP_GW_CONTROLLER_STORAGE_POSTGRES_PASSWORD=gateway - APIP_GW_CONTROLLER_STORAGE_POSTGRES_SSLMODE=disable - APIP_GW_CONTROLLER_LOGGING_LEVEL=info + - APIP_GW_CONTROLLER_AUTH_BASIC_ADMIN_USERNAME=admin + - APIP_GW_CONTROLLER_AUTH_BASIC_ADMIN_PASSWORD_HASH=$$2y$$10$$KZx7AodnpgOthAweR/k2P.xu4kLY7UiFY8Lt2D76mpzzEA6wv7qUO - APIP_GW_ROUTER_EVENT__GATEWAY_ENABLED=true - APIP_GW_ROUTER_EVENT__GATEWAY_WEBSUB__HUB_URL=http://event-gateway:8080 - APIP_GW_ROUTER_EVENT__GATEWAY_WEBSUB__HUB_PORT=8080 diff --git a/event-gateway/docker-compose.test.sqlserver.yaml b/event-gateway/docker-compose.test.sqlserver.yaml index c73259328a..0e4a70f8d5 100644 --- a/event-gateway/docker-compose.test.sqlserver.yaml +++ b/event-gateway/docker-compose.test.sqlserver.yaml @@ -77,6 +77,8 @@ services: - APIP_GW_CONTROLLER_STORAGE_TYPE=sqlserver - APIP_GW_CONTROLLER_STORAGE_DATABASE_DSN=sqlserver://sa:${MSSQL_SA_PASSWORD:-EGW_Strong!Pass123}@sqlserver:1433?database=gateway_test&encrypt=disable&TrustServerCertificate=true&app+name=gateway-controller - APIP_GW_CONTROLLER_LOGGING_LEVEL=info + - APIP_GW_CONTROLLER_AUTH_BASIC_ADMIN_USERNAME=admin + - APIP_GW_CONTROLLER_AUTH_BASIC_ADMIN_PASSWORD_HASH=$$2y$$10$$KZx7AodnpgOthAweR/k2P.xu4kLY7UiFY8Lt2D76mpzzEA6wv7qUO - APIP_GW_ROUTER_EVENT__GATEWAY_ENABLED=true - APIP_GW_ROUTER_EVENT__GATEWAY_WEBSUB__HUB_URL=http://event-gateway:8080 - APIP_GW_ROUTER_EVENT__GATEWAY_WEBSUB__HUB_PORT=8080 diff --git a/gateway/README.md b/gateway/README.md index a7d7bbd7d4..76a671881a 100644 --- a/gateway/README.md +++ b/gateway/README.md @@ -35,7 +35,8 @@ make build-gateway-builder ### Run -Run the one-time setup (generates `api-platform.env` and the router listener TLS certificate), then start the stack: +Run the one-time setup (generates `api-platform.env`, the router listener TLS certificate, the AES-256 +encryption key, and the gateway-controller admin credentials), then start the stack: ```bash ./scripts/setup.sh @@ -43,7 +44,13 @@ docker compose up -d curl http://localhost:9092/api/admin/v0.9/health ``` -`setup.sh` is idempotent (rerun with `--force` to rotate certs and rewrite `api-platform.env`). +`setup.sh` prints the admin password **once** — copy it. The username defaults to `admin`; set +`ADMIN_USERNAME`/`ADMIN_PASSWORD` up front for non-interactive runs. Only the bcrypt hash is stored (in +`api-platform.env`), and — because basic auth is enabled in the shipped config — the controller refuses +to start if the credential is missing. + +`setup.sh` is idempotent (rerun with `--force` to rotate certs, the encryption key, and the admin +password, and rewrite `api-platform.env`). For the full setup reference — flags (`--force`, `--certs-only`, `--with-encryption`), control-plane connection, at-rest encryption, and how configuration is delivered — see diff --git a/gateway/configs/config-template.toml b/gateway/configs/config-template.toml index 35c3c4c240..fddc616c66 100644 --- a/gateway/configs/config-template.toml +++ b/gateway/configs/config-template.toml @@ -120,10 +120,10 @@ definitions_path = '{{ env "APIP_GW_CONTROLLER_POLICIES_DEFINITIONS_PATH" "./def enabled = true [[controller.auth.basic.users]] -username = "admin" -password = "admin" -password_hashed = false -roles = ["admin"] +username = '{{ env "APIP_GW_CONTROLLER_AUTH_BASIC_ADMIN_USERNAME" "" }}' +password = '{{ env "APIP_GW_CONTROLLER_AUTH_BASIC_ADMIN_PASSWORD_HASH" "" }}' +password_hashed = true +roles = ["admin"] [controller.auth.idp] enabled = false diff --git a/gateway/configs/config.toml b/gateway/configs/config.toml index cc552c3c94..d2cb9a2aa8 100644 --- a/gateway/configs/config.toml +++ b/gateway/configs/config.toml @@ -66,9 +66,10 @@ definitions_path = '{{ env "APIP_GW_CONTROLLER_POLICIES_DEFINITIONS_PATH" "./def enabled = true [[controller.auth.basic.users]] -username = "admin" -password = "admin" -roles = ["admin"] +username = '{{ env "APIP_GW_CONTROLLER_AUTH_BASIC_ADMIN_USERNAME" "" }}' +password = '{{ env "APIP_GW_CONTROLLER_AUTH_BASIC_ADMIN_PASSWORD_HASH" "" }}' +password_hashed = true +roles = ["admin"] [policy_configurations.llm_cost_v1] pricing_file = "/etc/policy-engine/llm-pricing/model_prices.json" diff --git a/gateway/distribution/README.md b/gateway/distribution/README.md index a698755c7e..03ca9c37a6 100644 --- a/gateway/distribution/README.md +++ b/gateway/distribution/README.md @@ -34,14 +34,19 @@ No other tools are required to run the gateway. ## Quick Start -Run the one-time setup (generates `api-platform.env` and the router listener TLS certificate), then -start the stack: +Run the one-time setup (generates `api-platform.env`, the router listener TLS certificate, the AES-256 +encryption key, and the gateway-controller admin credentials), then start the stack: ```bash ./scripts/setup.sh docker compose up -d ``` +`setup.sh` prints the admin password **once** — copy it. The username defaults to `admin`; set +`ADMIN_USERNAME`/`ADMIN_PASSWORD` beforehand to run non-interactively. Only the bcrypt hash is stored, +and — because basic auth is enabled in the shipped config — the controller refuses to start without a +credential. + Verify the gateway is healthy: ```bash @@ -70,7 +75,7 @@ Edit `configs/config.toml` before starting the gateway. The most commonly change | Setting | Description | Default | |---------|-------------|---------| -| `controller.auth.basic.users` | Admin credentials for the controller REST API | `admin` / `admin` | +| `controller.auth.basic.users` | Admin credentials for the controller REST API — `setup.sh` turns the plaintext `ADMIN_USERNAME`/`ADMIN_PASSWORD` you give it into `api-platform.env`'s `APIP_GW_CONTROLLER_AUTH_BASIC_ADMIN_USERNAME` + bcrypt `..._PASSWORD_HASH`, which `config.toml` reads. Not a hardcoded default | generated by `setup.sh` | | `controller.server.gateway_id` | Unique identifier for this gateway instance | `platform-gateway-id` | | `controller.logging.level` | Controller log level (`debug`, `info`, `warn`, `error`) | `info` | | `policy_engine.logging.level` | Policy engine log level | `info` | diff --git a/gateway/docker-compose.yaml b/gateway/docker-compose.yaml index 71b1fdb3bf..989e2d0827 100644 --- a/gateway/docker-compose.yaml +++ b/gateway/docker-compose.yaml @@ -24,9 +24,9 @@ services: gateway-controller: image: ghcr.io/wso2/api-platform/gateway-controller:1.2.0-beta-SNAPSHOT - mem_limit: 60m - mem_reservation: 60m - cpus: 0.025 + mem_limit: 600m + mem_reservation: 600m + cpus: 0.25 command: ["-config", "/etc/gateway-controller/config.toml"] ports: - "9090:9090" # REST API @@ -49,9 +49,9 @@ services: gateway-runtime: image: ghcr.io/wso2/api-platform/gateway-runtime:1.2.0-beta-SNAPSHOT - mem_limit: 180m - mem_reservation: 180m - cpus: 0.175 + mem_limit: 1800m + mem_reservation: 1800m + cpus: 1.75 command: ["--pol.config", "/etc/policy-engine/config.toml"] ports: # Router (Envoy) diff --git a/gateway/gateway-controller/README.md b/gateway/gateway-controller/README.md index 1333f40ef0..1760515da4 100644 --- a/gateway/gateway-controller/README.md +++ b/gateway/gateway-controller/README.md @@ -175,8 +175,13 @@ APIP_GW_CONTROLLER_STORAGE_DATABASE_DSN=sqlserver://sa:secret@sqlserver:1433?dat APIP_GW_CONTROLLER_CONTROLPLANE_HOST=connect.example.com:9243 APIP_GW_CONTROLLER_CONTROLPLANE_TOKEN= APIP_GW_CONTROLLER_LOGGING_LEVEL=debug +APIP_GW_CONTROLLER_AUTH_BASIC_ADMIN_USERNAME=admin +APIP_GW_CONTROLLER_AUTH_BASIC_ADMIN_PASSWORD_HASH= ``` +> When delivering the bcrypt hash through docker-compose, use `env_file` with `format: raw` (as the +> shipped composes do) so the `$` characters in the hash are not treated as compose interpolation. + To configure a key that has no token in the shipped config, add the `{{ env }}` token to your config file for that key, or set the value in the file directly. @@ -308,6 +313,21 @@ Admin API: http://localhost:9092/api/admin/v0.9 The paths shown in the examples below are relative to these base URLs. +### Authentication + +The Management API is protected by HTTP basic auth (see [authentication.md](./authentication.md)). Use the +admin credential provisioned by `scripts/setup.sh` — the username defaults to `admin` and the password is +the one setup.sh printed. Export them once and pass with `curl -u`: + +```bash +export ADMIN_USERNAME=admin +export ADMIN_PASSWORD='' +# then, on each management call: +# curl -u "$ADMIN_USERNAME:$ADMIN_PASSWORD" ... +``` + +The Admin API (health, config dump) is reached on its own port and is governed separately. + ### Endpoints #### Health Check (Admin API) diff --git a/gateway/gateway-controller/authentication.md b/gateway/gateway-controller/authentication.md index c2e3508122..5701416ace 100644 --- a/gateway/gateway-controller/authentication.md +++ b/gateway/gateway-controller/authentication.md @@ -22,6 +22,24 @@ Gateway Controller routes are protected using **local roles** (for example `admi ## Configuration In the umbrella gateway config, these settings live under `controller.auth`. (If you run the controller standalone, the same structure applies under the controller's config root.) +> **Shipped default admin user.** The bundled `config.toml` defines a single basic-auth admin user whose +> username and password (bcrypt-hashed) are **not hardcoded** — they are interpolated from environment +> variables provisioned by `scripts/setup.sh`: +> ```toml +> [[controller.auth.basic.users]] +> username = '{{ env "APIP_GW_CONTROLLER_AUTH_BASIC_ADMIN_USERNAME" "" }}' +> password = '{{ env "APIP_GW_CONTROLLER_AUTH_BASIC_ADMIN_PASSWORD_HASH" "" }}' +> password_hashed = true +> roles = ["admin"] +> ``` +> You provide the plaintext credential to `setup.sh` as `ADMIN_USERNAME` / `ADMIN_PASSWORD` (prompted, or +> randomly generated if omitted); it then writes `APIP_GW_CONTROLLER_AUTH_BASIC_ADMIN_USERNAME` and the +> bcrypt `APIP_GW_CONTROLLER_AUTH_BASIC_ADMIN_PASSWORD_HASH` into `api-platform.env` (the two tokens above), +> and prints the plaintext password once. If basic auth is enabled but a configured user has an empty +> username or password (e.g. those tokens are unset), the controller **fails startup** rather than running +> on an unenforceable credential. To add more users or run standalone, configure them explicitly as shown +> below. See the [Gateway Quick Start](../../docs/gateway/quick-start-guide.md) for the full credential flow. + ### Option A: Basic Auth (local users) ```yaml controller: diff --git a/gateway/gateway-controller/pkg/config/config.go b/gateway/gateway-controller/pkg/config/config.go index c9ee4d4cbe..0da076d571 100644 --- a/gateway/gateway-controller/pkg/config/config.go +++ b/gateway/gateway-controller/pkg/config/config.go @@ -594,10 +594,10 @@ type VHostEntry struct { // HTTPListenerConfig holds HTTP listener related configuration of an API type HTTPListenerConfig struct { - ServerHeaderTransformation string `koanf:"server_header_transformation"` // Options: "APPEND_IF_ABSENT", "OVERWRITE", "PASS_THROUGH" - ServerHeaderValue string `koanf:"server_header_value"` // Custom value for the Server header - Timeouts HCMTimeouts `koanf:"timeouts"` // HTTP Connection Manager (downstream) timeouts - PerConnectionBufferLimitBytes uint32 `koanf:"per_connection_buffer_limit_bytes"` // Downstream per-connection buffer limit in bytes + ServerHeaderTransformation string `koanf:"server_header_transformation"` // Options: "APPEND_IF_ABSENT", "OVERWRITE", "PASS_THROUGH" + ServerHeaderValue string `koanf:"server_header_value"` // Custom value for the Server header + Timeouts HCMTimeouts `koanf:"timeouts"` // HTTP Connection Manager (downstream) timeouts + PerConnectionBufferLimitBytes uint32 `koanf:"per_connection_buffer_limit_bytes"` // Downstream per-connection buffer limit in bytes } // HCMTimeouts holds HTTP Connection Manager (downstream/connection) timeouts. @@ -1891,6 +1891,18 @@ func validateGRPCEventServerConfig(cfg GRPCEventServerConfig) error { // validateAuthConfig validates the authentication configuration func (c *Config) validateAuthConfig() error { + if c.Controller.Auth.Basic.Enabled { + for i := range c.Controller.Auth.Basic.Users { + u := &c.Controller.Auth.Basic.Users[i] + if strings.TrimSpace(u.Username) == "" || strings.TrimSpace(u.Password) == "" { + return fmt.Errorf("auth.basic user #%d has an empty username or password — "+ + "refusing to start with an unenforceable basic-auth config; "+ + "run scripts/setup.sh to provision admin credentials "+ + "(sets APIP_GW_CONTROLLER_AUTH_BASIC_ADMIN_USERNAME and APIP_GW_CONTROLLER_AUTH_BASIC_ADMIN_PASSWORD_HASH)", i+1) + } + } + } + // Validate IDP role mapping for multiple wildcards if c.Controller.Auth.IDP.Enabled && len(c.Controller.Auth.IDP.RoleMapping) > 0 { wildcardRoles := []string{} diff --git a/gateway/gateway-controller/pkg/config/interpolate_test.go b/gateway/gateway-controller/pkg/config/interpolate_test.go index 796d9877ef..f5dca6c35b 100644 --- a/gateway/gateway-controller/pkg/config/interpolate_test.go +++ b/gateway/gateway-controller/pkg/config/interpolate_test.go @@ -86,3 +86,51 @@ token = "{{ file \"`+secret+`\" }}" require.NoError(t, err) assert.Equal(t, "file-token", cfg.Controller.ControlPlane.Token) } + +// TestLoadConfig_Interpolation_BasicAuthAdminCreds exercises the full path the +// shipped config.toml uses for its admin credentials: {{ env }} tokens resolved +// from the environment (as scripts/setup.sh writes them into api-platform.env). +func TestLoadConfig_Interpolation_BasicAuthAdminCreds(t *testing.T) { + t.Setenv("APIP_GW_CONTROLLER_AUTH_BASIC_ADMIN_USERNAME", "gwadmin") + t.Setenv("APIP_GW_CONTROLLER_AUTH_BASIC_ADMIN_PASSWORD_HASH", "$2y$10$C6UzMDM.H6dfI/f/IKcEeO3JxpH3nZ0z8oJ0kQ1yQ2wRxYzAbCdEe") + path := writeCtlInterpConfig(t, ` +[controller.auth.basic] +enabled = true + +[[controller.auth.basic.users]] +username = '{{ env "APIP_GW_CONTROLLER_AUTH_BASIC_ADMIN_USERNAME" "" }}' +password = '{{ env "APIP_GW_CONTROLLER_AUTH_BASIC_ADMIN_PASSWORD_HASH" "" }}' +password_hashed = true +roles = ["admin"] +`) + cfg, err := LoadConfig(path) + require.NoError(t, err) + require.Len(t, cfg.Controller.Auth.Basic.Users, 1) + u := cfg.Controller.Auth.Basic.Users[0] + assert.Equal(t, "gwadmin", u.Username) + assert.Equal(t, "$2y$10$C6UzMDM.H6dfI/f/IKcEeO3JxpH3nZ0z8oJ0kQ1yQ2wRxYzAbCdEe", u.Password) + assert.True(t, u.PasswordHashed) +} + +// TestLoadConfig_Interpolation_BasicAuthAdminCredsUnset_FailsClosed verifies the +// shipped-config scenario with the credential env vars unset: the {{ env }} +// tokens resolve to their empty defaults, leaving a user present but empty-valued, +// which validateAuthConfig rejects so the controller refuses to start. +func TestLoadConfig_Interpolation_BasicAuthAdminCredsUnset_FailsClosed(t *testing.T) { + os.Unsetenv("APIP_GW_CONTROLLER_AUTH_BASIC_ADMIN_USERNAME") + os.Unsetenv("APIP_GW_CONTROLLER_AUTH_BASIC_ADMIN_PASSWORD_HASH") + path := writeCtlInterpConfig(t, ` +[controller.auth.basic] +enabled = true + +[[controller.auth.basic.users]] +username = '{{ env "APIP_GW_CONTROLLER_AUTH_BASIC_ADMIN_USERNAME" "" }}' +password = '{{ env "APIP_GW_CONTROLLER_AUTH_BASIC_ADMIN_PASSWORD_HASH" "" }}' +password_hashed = true +roles = ["admin"] +`) + cfg, err := LoadConfig(path) + require.Error(t, err) + assert.Nil(t, cfg) + assert.Contains(t, err.Error(), "empty username or password") +} diff --git a/gateway/gateway-controller/pkg/config/validator_test.go b/gateway/gateway-controller/pkg/config/validator_test.go index 6a867a98be..2d95b46a6a 100644 --- a/gateway/gateway-controller/pkg/config/validator_test.go +++ b/gateway/gateway-controller/pkg/config/validator_test.go @@ -233,6 +233,56 @@ func TestValidateAuthConfig_BothAuthEnabled(t *testing.T) { assert.NoError(t, err) } +func TestValidateAuthConfig_BasicAuthEnabledNoUsers_AllowsNoAuthMode(t *testing.T) { + // Basic auth enabled with an empty user list is allowed: it degrades to the + // auth middleware's no-auth passthrough (which logs its own warning). Only a + // user that is present but empty-valued is rejected — see the empty-credential + // test below. The shipped config always defines a user, so its unset-env case + // hits that check, not this one. + config := &Config{ + Controller: Controller{ + Auth: AuthConfig{ + Basic: BasicAuth{ + Enabled: true, + Users: []AuthUser{}, + }, + }, + }, + } + + err := config.validateAuthConfig() + assert.NoError(t, err) +} + +func TestValidateAuthConfig_BasicAuthEnabledEmptyCredential_FailsClosed(t *testing.T) { + // A user present but with an empty username or password (e.g. an unset + // {{ env }} token) is just as unenforceable as no user at all. + for _, tc := range []struct { + name string + user AuthUser + }{ + {"empty password", AuthUser{Username: "admin", Password: "", Roles: []string{"admin"}}}, + {"empty username", AuthUser{Username: "", Password: "hash", Roles: []string{"admin"}}}, + } { + t.Run(tc.name, func(t *testing.T) { + config := &Config{ + Controller: Controller{ + Auth: AuthConfig{ + Basic: BasicAuth{ + Enabled: true, + Users: []AuthUser{tc.user}, + }, + }, + }, + } + + err := config.validateAuthConfig() + require.Error(t, err) + assert.Contains(t, err.Error(), "empty username or password") + }) + } +} + func TestValidator_LabelsValidation(t *testing.T) { validator := NewAPIValidator() diff --git a/gateway/perf/create_apis_and_capture_stats.sh b/gateway/perf/create_apis_and_capture_stats.sh index 2ef4684f4b..3eda92df7f 100755 --- a/gateway/perf/create_apis_and_capture_stats.sh +++ b/gateway/perf/create_apis_and_capture_stats.sh @@ -13,6 +13,12 @@ API_MGR_URL="http://localhost:9090/api/management/v1/rest-apis" TOTAL="${1:-50}" OUT="${2:-stats.csv}" +# ---------------- admin credentials ---------------- +# Gateway-controller admin credentials (provisioned by scripts/setup.sh). No hardcoded +# default password: set ADMIN_PASSWORD (and optionally ADMIN_USERNAME) before running. +ADMIN_USERNAME="${ADMIN_USERNAME:-admin}" +: "${ADMIN_PASSWORD:?set ADMIN_PASSWORD to the gateway admin password (see scripts/setup.sh output)}" + # ---------------- CSV header ---------------- echo "api_count,router_cpu_pct,router_mem_used,controller_cpu_pct,controller_mem_used,http_code" > "$OUT" @@ -40,7 +46,7 @@ for i in $(seq 1 "$TOTAL"); do http_code=$( curl -s -o "$resp_file" -w "%{http_code}" \ -X POST "$API_MGR_URL" \ - -u admin:admin \ + -u "$ADMIN_USERNAME:$ADMIN_PASSWORD" \ -H "Content-Type: application/yaml" \ --data-binary @- </dev/null 2>&1 || { echo "error: openssl is required" >&2; e log() { echo "[setup] $*"; } +# bcrypt isn't in openssl; use htpasswd when available, else the httpd image. +# The gateway-controller's basic authenticator accepts bcrypt ($2a/$2b/$2y$) hashes. +bcrypt_hash() { + local password="$1" + if command -v htpasswd >/dev/null 2>&1; then + printf '%s' "$password" | htpasswd -niB -C 10 "" | cut -d: -f2 | tr -d '\r\n' + elif command -v docker >/dev/null 2>&1; then + printf '%s' "$password" | docker run --rm -i httpd:2.4-alpine htpasswd -niB -C 10 "" | cut -d: -f2 | tr -d '\r\n' + else + echo "error: need either htpasswd (apache2-utils / httpd-tools) or docker to bcrypt-hash the admin password" >&2 + exit 1 + fi +} + gen_cert() { if [[ "$FORCE" == false && -f "$CERTS_DIR/default-listener.crt" && -f "$CERTS_DIR/default-listener.key" ]]; then log " - $CERTS_DIR/default-listener.crt already exists — keeping it" @@ -114,15 +135,38 @@ if [[ "$FORCE" == false && -f "$ENV_FILE" ]]; then exit 0 fi +# Admin credentials for the gateway-controller REST/management API (basic auth). +# Precedence: ADMIN_USERNAME/ADMIN_PASSWORD env vars > interactive prompt > defaults. +# Only the bcrypt hash is persisted; the plaintext password is shown once at the end. +GENERATED_PASSWORD="$(openssl rand -base64 24 | tr -d '/+=' | cut -c1-20)" + +if [[ -z "${ADMIN_USERNAME:-}" && -t 0 ]]; then + read -r -p "Admin username [admin]: " ADMIN_USERNAME +fi +ADMIN_USERNAME="${ADMIN_USERNAME:-admin}" + +if [[ -z "${ADMIN_PASSWORD:-}" && -t 0 ]]; then + read -r -s -p "Admin password [press Enter to generate one]: " ADMIN_PASSWORD + echo +fi +ADMIN_PASSWORD="${ADMIN_PASSWORD:-$GENERATED_PASSWORD}" + +log "Provisioning admin credentials ..." +ADMIN_PASSWORD_HASH="$(bcrypt_hash "$ADMIN_PASSWORD")" +log " - APIP_GW_CONTROLLER_AUTH_BASIC_ADMIN_PASSWORD_HASH generated (bcrypt)" + log "Writing $ENV_FILE ..." umask 177 cat > "$ENV_FILE" < /etc/platform-api/keys/encryption.key. The field below reads it via a +# {{ file "..." }} token — never paste raw key values into this file. # 3. Generate an RS256 JWT keypair and mount it at the paths named by # auth.jwt.public_key_file / private_key_file below: # openssl genpkey -algorithm RSA -pkeyopt rsa_keygen_bits:2048 \ diff --git a/platform-api/config/config.toml b/platform-api/config/config.toml index 165c0c6601..1dbadfd378 100644 --- a/platform-api/config/config.toml +++ b/platform-api/config/config.toml @@ -13,7 +13,7 @@ level = '{{ env "APIP_CP_LOGGING_LEVEL" "info" }}' [platform_api.security] -encryption_key = '{{ env "APIP_CP_ENCRYPTION_KEY" }}' +encryption_key = '{{ file "/etc/platform-api/keys/encryption.key" }}' [platform_api.database] driver = "sqlite3" diff --git a/platform-api/config/config_test.go b/platform-api/config/config_test.go index 37a6f4647a..d1e36f5e3b 100644 --- a/platform-api/config/config_test.go +++ b/platform-api/config/config_test.go @@ -118,6 +118,32 @@ func TestLoadConfig_ValidKeys_Succeeds(t *testing.T) { assert.Equal(t, validInlineKey, cfg.Security.EncryptionKey) } +// Issue #2835: the at-rest encryption key loads from a mounted file via a {{ file }} +// token — the mechanism the shipped config.toml now uses (setup.sh writes the key to +// /etc/platform-api/keys/encryption.key instead of an env var). The file is written +// with a trailing newline (as `openssl rand -hex 32 > file` produces); {{ file }} trims it. +func TestLoadConfig_EncryptionKeyFromFile_Succeeds(t *testing.T) { + dir := t.TempDir() + keyPath := filepath.Join(dir, "encryption.key") + require.NoError(t, os.WriteFile(keyPath, []byte(validInlineKey+"\n"), 0o600)) + + // Permit {{ file }} reads from the temp dir (default allowlist is /etc/platform-api, + // /secrets/platform-api). Constant: configinterpolate.EnvFileSourceAllowlist. + t.Setenv("APIP_CONFIG_FILE_SOURCE_ALLOWLIST", dir) + // The JWT public-key path is an {{ env }} token (unaffected by the {{ file }} allowlist). + t.Setenv("APIP_CP_AUTH_JWT_PUBLIC_KEY_FILE", validJWTPublicKeyFile) + + cfg, err := loadTOML(t, ` +[platform_api.security] +encryption_key = '{{ file "`+keyPath+`" }}' + +[platform_api.auth.jwt] +public_key_file = '{{ env "APIP_CP_AUTH_JWT_PUBLIC_KEY_FILE" }}' +`) + require.NoError(t, err) + assert.Equal(t, validInlineKey, cfg.Security.EncryptionKey) +} + // A merged multi-component config file also carries a foreign [developer_portal] // section with its own interpolation tokens — here deliberately poisonous ones: an // {{ env }} with no default that is left unset, and a {{ file }} path outside diff --git a/portals/ai-workspace/README.md b/portals/ai-workspace/README.md index 3508aedf33..55875def96 100644 --- a/portals/ai-workspace/README.md +++ b/portals/ai-workspace/README.md @@ -475,7 +475,7 @@ credentials, and self-signed certificates); for production, provide real values: | Requirement | Quickstart (`setup.sh`) | Production | |---|---|---| -| **Platform API** — `APIP_CP_ENCRYPTION_KEY` | Generated into `api-platform.env` | Manage as real secrets; prefer mounting files and referencing them with `{{ file "..." }}` in the config TOML | +| **Platform API** — at-rest encryption key | Generated into `resources/keys/encryption.key` (mounted at `/etc/platform-api/keys`) and read by `config.toml` via `{{ file "..." }}` | Manage as a real secret; a stable value must persist across restarts/replicas | | **Platform API** — RS256 JWT signing keypair | Generated as PEM files in `resources/keys/` (`jwt_private.pem` / `jwt_public.pem`), read by `config.toml` via `{{ file }}` | Mount your own RSA keypair at the same paths; rotate as real signing keys | | **Platform API** — admin credentials | Generated into `api-platform.env` (bcrypt hash); password printed once | Use OIDC (`auth.idp`) instead of file-based auth | | **TLS certificates (both services)** | One self-signed pair in `resources/certificates/`, shared by both services | Certificates from your CA (same file names), or terminate TLS at an ingress and disable the listeners' TLS | diff --git a/portals/ai-workspace/distribution/README.md b/portals/ai-workspace/distribution/README.md index d796bf03b3..47b5e348d5 100644 --- a/portals/ai-workspace/distribution/README.md +++ b/portals/ai-workspace/distribution/README.md @@ -41,7 +41,8 @@ docker compose up -d | Output | Contents | |---|---| -| `api-platform.env` (git-ignored) | `APIP_CP_ENCRYPTION_KEY` (at-rest encryption), `APIP_CP_ADMIN_USERNAME`, `APIP_CP_ADMIN_PASSWORD_HASH` (bcrypt) | +| `api-platform.env` (git-ignored) | `APIP_CP_ADMIN_USERNAME`, `APIP_CP_ADMIN_PASSWORD_HASH` (bcrypt) | +| `resources/keys/encryption.key` (git-ignored) | Platform API at-rest encryption key (32 bytes, 64 hex chars); read by `config.toml` via `{{ file }}`. **Retain it** — losing or changing it makes previously-encrypted secrets unreadable. | | `resources/keys/jwt_private.pem` + `jwt_public.pem` (git-ignored) | RS256 keypair signing/verifying login JWTs; read by `config.toml` via `{{ file }}` | | `resources/certificates/cert.pem` + `key.pem` | Self-signed TLS pair shared by both services (SAN: `localhost`, `platform-api`, `ai-workspace`) | diff --git a/portals/ai-workspace/production/README.md b/portals/ai-workspace/production/README.md index b10dc019f9..60104c286b 100644 --- a/portals/ai-workspace/production/README.md +++ b/portals/ai-workspace/production/README.md @@ -238,8 +238,8 @@ There is no demo mode: startup checks are always on for **both** the `platform-a `ai-workspace` services and fail fast when a requirement is missing. For production, replace the quickstart's `setup.sh` outputs with real values: use OIDC (sections 1–3) instead of the generated file-based admin user, mount TLS certificates from your CA instead of the generated -self-signed pairs, and manage `APIP_CP_ENCRYPTION_KEY` and the RS256 JWT signing keypair -(`resources/keys/jwt_private.pem` / `jwt_public.pem`, read via `{{ file }}`) as stable, real +self-signed pairs, and manage the at-rest encryption key and the RS256 JWT signing keypair +(`resources/keys/encryption.key`, `resources/keys/jwt_private.pem` / `jwt_public.pem`, all read via `{{ file }}`) as stable, real secrets (prefer `{{ file }}` tokens over environment variables). See [Production hardening](../README.md#production-hardening) in the main README for the diff --git a/portals/ai-workspace/setup.sh b/portals/ai-workspace/setup.sh index 0741ba5b84..6be8abba85 100755 --- a/portals/ai-workspace/setup.sh +++ b/portals/ai-workspace/setup.sh @@ -105,10 +105,6 @@ if [[ -z "${ADMIN_PASSWORD:-}" && -t 0 ]]; then fi ADMIN_PASSWORD="${ADMIN_PASSWORD:-$GENERATED_PASSWORD}" -log "Generating secrets into $ENV_FILE ..." -ENCRYPTION_KEY="$(openssl rand -hex 32)" -log " - APIP_CP_ENCRYPTION_KEY generated" - log "Provisioning Platform API JWT signing keypair (RS256) ..." # Tokens are signed asymmetrically (RS256), not with a shared HMAC secret. The # Platform API mints login tokens with the RSA private key and verifies every @@ -131,6 +127,21 @@ openssl rsa -in "$KEYS_DIR/jwt_private.pem" -pubout \ chmod 644 "$KEYS_DIR/jwt_private.pem" "$KEYS_DIR/jwt_public.pem" log " - RS256 JWT keypair generated at $KEYS_DIR" +# Platform API at-rest encryption key. Written to a file (not api-platform.env) and +# read by config.toml via {{ file "/etc/platform-api/keys/encryption.key" }} — the +# same mounted keys dir as the JWT keypair. 32-byte key as 64 hex chars. 644 so the +# container's non-root user can read the host-owned bind-mounted file (matching the +# JWT keys above; a tighter mode would block the non-root platform-api user). +# Preserve an existing key across reruns — regenerating it makes previously-encrypted +# data unreadable — so only create it when absent or when --force rotation is requested. +if [[ "$FORCE" == true || ! -f "$KEYS_DIR/encryption.key" ]]; then + openssl rand -hex 32 > "$KEYS_DIR/encryption.key" + chmod 644 "$KEYS_DIR/encryption.key" + log " - at-rest encryption key generated at $KEYS_DIR/encryption.key" +else + log " - $KEYS_DIR/encryption.key already exists — keeping it" +fi + log "Provisioning admin credentials ..." ADMIN_PASSWORD_HASH="$(bcrypt_hash "$ADMIN_PASSWORD")" log " - APIP_CP_ADMIN_PASSWORD_HASH generated (bcrypt)" @@ -139,7 +150,6 @@ umask 177 cat > "$ENV_FILE" < "$JWT_KEY_DIR/encryption.key" + chmod "$CERT_FILE_MODE" "$JWT_KEY_DIR/encryption.key" + log " - at-rest encryption key generated at $JWT_KEY_DIR/encryption.key" +fi + log "Provisioning Platform API admin credentials ..." CREDENTIALS_PROVISIONED=false if grep -q "^APIP_CP_ADMIN_USERNAME=" "$ENV_FILE" 2>/dev/null; then diff --git a/samples/ai-agent-with-mcp/configure-gateway.sh b/samples/ai-agent-with-mcp/configure-gateway.sh index fef40ce18a..8a7fe0a4d1 100644 --- a/samples/ai-agent-with-mcp/configure-gateway.sh +++ b/samples/ai-agent-with-mcp/configure-gateway.sh @@ -22,7 +22,7 @@ if [[ -z "${ANTHROPIC_API_KEY:-}" ]]; then fi MGMT="http://localhost:9090/api/management/v0.9" -AUTH_HEADER="Authorization: Basic YWRtaW46YWRtaW4=" # admin:admin +AUTH_HEADER="Authorization: Basic $(printf %s "${ADMIN_USERNAME:-admin}:${ADMIN_PASSWORD:-admin}" | base64 | tr -d '\r\n')" # default admin/admin; override with ADMIN_USERNAME/ADMIN_PASSWORD # Inbound API key clients use to call the LLM proxy. # The gateway's api-key-auth policy checks the x-api-key header, diff --git a/samples/ai-app-claude-code/setup.sh b/samples/ai-app-claude-code/setup.sh index 0382ed912f..f99157b386 100755 --- a/samples/ai-app-claude-code/setup.sh +++ b/samples/ai-app-claude-code/setup.sh @@ -11,7 +11,7 @@ DIST_URL="https://github.com/wso2/api-platform/releases/download/gateway/v${DIST GATEWAY_MGMT_URL="http://localhost:9090/api/management/v0.9" GATEWAY_HEALTH_URL="http://localhost:9094/health" -AUTH_HEADER="Authorization: Basic YWRtaW46YWRtaW4=" # admin:admin +AUTH_HEADER="Authorization: Basic $(printf %s "${ADMIN_USERNAME:-admin}:${ADMIN_PASSWORD:-admin}" | base64 | tr -d '\r\n')" # default admin/admin; override with ADMIN_USERNAME/ADMIN_PASSWORD API_NAME="reading-list-api" API_KEY_NAME="claude-code-key" @@ -70,6 +70,10 @@ fi info "Starting Docker Compose stack in ${DIST_NAME}/ ..." # Bring down any previous instance to avoid stale network/port conflicts. (cd "${SCRIPT_DIR}/${DIST_NAME}" && docker compose down -v --remove-orphans 2>/dev/null || true) +# Provision the gateway's listener cert, encryption key, api-platform.env, and admin credentials. +# The gateway no longer ships a default admin:admin and fails closed without a credential; this +# provisions admin/admin (matching AUTH_HEADER above). Override via ADMIN_USERNAME/ADMIN_PASSWORD. +(cd "${SCRIPT_DIR}/${DIST_NAME}" && ADMIN_USERNAME="${ADMIN_USERNAME:-admin}" ADMIN_PASSWORD="${ADMIN_PASSWORD:-admin}" ./scripts/setup.sh) (cd "${SCRIPT_DIR}/${DIST_NAME}" && docker compose up -d) success "Docker Compose stack started." diff --git a/samples/ai-app-claude-code/teardown.sh b/samples/ai-app-claude-code/teardown.sh index 6ded40fcfc..2a4a6cece9 100755 --- a/samples/ai-app-claude-code/teardown.sh +++ b/samples/ai-app-claude-code/teardown.sh @@ -9,7 +9,7 @@ DIST_NAME="wso2apip-api-gateway-${DIST_VERSION}" DIST_ZIP="${DIST_NAME}.zip" GATEWAY_MGMT_URL="http://localhost:9090/api/management/v0.9" -AUTH_HEADER="Authorization: Basic YWRtaW46YWRtaW4=" # admin:admin +AUTH_HEADER="Authorization: Basic $(printf %s "${ADMIN_USERNAME:-admin}:${ADMIN_PASSWORD:-admin}" | base64 | tr -d '\r\n')" # default admin/admin; override with ADMIN_USERNAME/ADMIN_PASSWORD API_NAME="reading-list-api" diff --git a/samples/ai-gw-llm-proxy/inject-mock.sh b/samples/ai-gw-llm-proxy/inject-mock.sh index 5fe7dbb720..af266957f8 100644 --- a/samples/ai-gw-llm-proxy/inject-mock.sh +++ b/samples/ai-gw-llm-proxy/inject-mock.sh @@ -24,7 +24,7 @@ fi MGMT_PORT="${MGMT_PORT:-9090}" INBOUND_API_KEY="${INBOUND_API_KEY:-demo-api-key}" MGMT_BASE="http://localhost:${MGMT_PORT}/api/management/v1" -AUTH="Authorization: Basic YWRtaW46YWRtaW4=" +AUTH="Authorization: Basic $(printf %s "${ADMIN_USERNAME:-admin}:${ADMIN_PASSWORD:-admin}" | base64 | tr -d '\r\n')" print_info "Registering LLM Provider (upstream: mock-llm-openai, policy: token-based-ratelimit)..." curl -sf -X POST "${MGMT_BASE}/llm-providers" \ diff --git a/samples/ai-gw-mcp-claude-desktop/inject-mock.sh b/samples/ai-gw-mcp-claude-desktop/inject-mock.sh index 0d8ad1b852..492ea4f36d 100755 --- a/samples/ai-gw-mcp-claude-desktop/inject-mock.sh +++ b/samples/ai-gw-mcp-claude-desktop/inject-mock.sh @@ -23,7 +23,7 @@ fi MGMT_PORT="${MGMT_PORT:-9090}" MGMT_BASE="http://localhost:${MGMT_PORT}/api/management/v1" -AUTH="Authorization: Basic YWRtaW46YWRtaW4=" +AUTH="Authorization: Basic $(printf %s "${ADMIN_USERNAME:-admin}:${ADMIN_PASSWORD:-admin}" | base64 | tr -d '\r\n')" print_info "Registering MCP Proxy (context: /reading-list, policy: mcp-auth)..." curl -sf -X POST "${MGMT_BASE}/mcp-proxies" \ diff --git a/samples/llm-cost-control-and-privacy-control/README.md b/samples/llm-cost-control-and-privacy-control/README.md index 53ff349592..f7b43cb90b 100644 --- a/samples/llm-cost-control-and-privacy-control/README.md +++ b/samples/llm-cost-control-and-privacy-control/README.md @@ -203,4 +203,4 @@ TEST_EMAIL="you@example.com" TEST_PHONE="+15551234567" ./test-pii-masking.sh | `setup.sh` fails at health check | Docker images are still pulling — wait and retry | | Scenario 1: no cache hit detected | `embedding_provider_api_key` is empty in `additional-config.toml`, or Redis is not reachable | | Scenario 2: original values appear in response | PII regex did not match — verify the regex patterns in `llm-proxy.yaml` | -| HTTP 401 on management API | Basic auth header mismatch; default credentials are `admin:admin` | +| HTTP 401 on management API | Basic auth header mismatch. `setup.sh` provisions the gateway admin credential (defaults to `admin`/`admin`); if you set `ADMIN_USERNAME`/`ADMIN_PASSWORD`, use the same values here. | diff --git a/samples/llm-cost-control-and-privacy-control/setup.sh b/samples/llm-cost-control-and-privacy-control/setup.sh index bd14495902..8d41bde8b7 100755 --- a/samples/llm-cost-control-and-privacy-control/setup.sh +++ b/samples/llm-cost-control-and-privacy-control/setup.sh @@ -11,7 +11,7 @@ DIST_URL="https://github.com/wso2/api-platform/releases/download/ai-gateway/v${D GATEWAY_MGMT_URL="http://localhost:9090/api/management/v1" GATEWAY_HEALTH_URL="http://localhost:9094/health" -AUTH_HEADER="Authorization: Basic YWRtaW46YWRtaW4=" # admin:admin +AUTH_HEADER="Authorization: Basic $(printf %s "${ADMIN_USERNAME:-admin}:${ADMIN_PASSWORD:-admin}" | base64 | tr -d '\r\n')" # default admin/admin; override with ADMIN_USERNAME/ADMIN_PASSWORD SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" PROVIDER_YAML="${SCRIPT_DIR}/llm-provider.yaml" @@ -148,6 +148,10 @@ success "Redis service merged into docker-compose." # Step 5 — Start the stack # --------------------------------------------------------------------------- info "Starting Docker Compose stack in ${DIST_NAME}/ ..." +# Provision the gateway's listener cert, encryption key, api-platform.env, and admin credentials. +# The gateway no longer ships a default admin:admin and fails closed without a credential; this +# provisions admin/admin (matching AUTH_HEADER above). Override via ADMIN_USERNAME/ADMIN_PASSWORD. +(cd "${DIST_NAME}" && ADMIN_USERNAME="${ADMIN_USERNAME:-admin}" ADMIN_PASSWORD="${ADMIN_PASSWORD:-admin}" ./scripts/setup.sh) (cd "${DIST_NAME}" && docker compose up -d) success "Docker Compose stack started." diff --git a/samples/llm-cost-control-and-privacy-control/teardown.sh b/samples/llm-cost-control-and-privacy-control/teardown.sh index 32ac5a8b52..b5f590bedf 100755 --- a/samples/llm-cost-control-and-privacy-control/teardown.sh +++ b/samples/llm-cost-control-and-privacy-control/teardown.sh @@ -9,7 +9,7 @@ DIST_NAME="wso2apip-ai-gateway-${DIST_VERSION}" DIST_ZIP="${DIST_NAME}.zip" GATEWAY_MGMT_URL="http://localhost:9090/api/management/v1" -AUTH_HEADER="Authorization: Basic YWRtaW46YWRtaW4=" # admin:admin +AUTH_HEADER="Authorization: Basic $(printf %s "${ADMIN_USERNAME:-admin}:${ADMIN_PASSWORD:-admin}" | base64 | tr -d '\r\n')" # default admin/admin; override with ADMIN_USERNAME/ADMIN_PASSWORD SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" PROVIDER_YAML="${SCRIPT_DIR}/llm-provider.yaml" diff --git a/samples/prompt-decorator-policy/README.md b/samples/prompt-decorator-policy/README.md index 4e47829659..f0cc29988f 100644 --- a/samples/prompt-decorator-policy/README.md +++ b/samples/prompt-decorator-policy/README.md @@ -168,7 +168,7 @@ Each scenario has its own script and can be run independently. No API key is nee | `setup.sh` fails at health check | Docker images are still pulling — wait and retry | | Scenario 1: persona not detected | The model paraphrased away the hotel name — re-run; or verify the `prompt-decorator` policy in `llm-proxy-persona.yaml` | | Scenario 2: tag not detected | The model dropped the tag — re-run; or verify the `prompt-decorator` policy in `llm-proxy-suffix.yaml` | -| HTTP 401 on management API | Basic auth header mismatch; default credentials are `admin:admin` | +| HTTP 401 on management API | Basic auth header mismatch. `setup.sh` provisions the gateway admin credential (defaults to `admin`/`admin`); if you set `ADMIN_USERNAME`/`ADMIN_PASSWORD`, use the same values here. | | Request errors with auth failure | Check that your Anthropic API key is valid | | `HTTP 400` — `credit balance is too low` | The Anthropic account has no credits — add credits in the Anthropic console | | Proxy returns `HTTP 500` — `Internal Server Error` | The `prompt-decorator` config is invalid; its `promptDecoratorConfig` must use `text` or `messages` (check the gateway-runtime logs) | diff --git a/samples/prompt-decorator-policy/setup.sh b/samples/prompt-decorator-policy/setup.sh index 52d55fe6aa..362c414519 100755 --- a/samples/prompt-decorator-policy/setup.sh +++ b/samples/prompt-decorator-policy/setup.sh @@ -11,7 +11,7 @@ DIST_URL="https://github.com/wso2/api-platform/releases/download/ai-gateway/v${D GATEWAY_MGMT_URL="http://localhost:9090/api/management/v0.9" GATEWAY_HEALTH_URL="http://localhost:9094/health" -AUTH_HEADER="Authorization: Basic YWRtaW46YWRtaW4=" # admin:admin +AUTH_HEADER="Authorization: Basic $(printf %s "${ADMIN_USERNAME:-admin}:${ADMIN_PASSWORD:-admin}" | base64 | tr -d '\r\n')" # default admin/admin; override with ADMIN_USERNAME/ADMIN_PASSWORD SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" PROVIDER_YAML="${SCRIPT_DIR}/llm-provider.yaml" @@ -77,6 +77,10 @@ fi # Step 3 — Start the stack # --------------------------------------------------------------------------- info "Starting Docker Compose stack in ${DIST_NAME}/ ..." +# Provision the gateway's listener cert, encryption key, api-platform.env, and admin credentials. +# The gateway no longer ships a default admin:admin and fails closed without a credential; this +# provisions admin/admin (matching AUTH_HEADER above). Override via ADMIN_USERNAME/ADMIN_PASSWORD. +(cd "${DIST_NAME}" && ADMIN_USERNAME="${ADMIN_USERNAME:-admin}" ADMIN_PASSWORD="${ADMIN_PASSWORD:-admin}" ./scripts/setup.sh) (cd "${DIST_NAME}" && docker compose up -d) success "Docker Compose stack started." diff --git a/samples/prompt-decorator-policy/teardown.sh b/samples/prompt-decorator-policy/teardown.sh index 2319c51ef8..2cdb050728 100755 --- a/samples/prompt-decorator-policy/teardown.sh +++ b/samples/prompt-decorator-policy/teardown.sh @@ -9,7 +9,7 @@ DIST_NAME="wso2apip-ai-gateway-${DIST_VERSION}" DIST_ZIP="${DIST_NAME}.zip" GATEWAY_MGMT_URL="http://localhost:9090/api/management/v0.9" -AUTH_HEADER="Authorization: Basic YWRtaW46YWRtaW4=" # admin:admin +AUTH_HEADER="Authorization: Basic $(printf %s "${ADMIN_USERNAME:-admin}:${ADMIN_PASSWORD:-admin}" | base64 | tr -d '\r\n')" # default admin/admin; override with ADMIN_USERNAME/ADMIN_PASSWORD SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" PROVIDER_YAML="${SCRIPT_DIR}/llm-provider.yaml"