diff --git a/.claude/settings.local.json b/.claude/settings.local.json deleted file mode 100644 index 18d0b7c..0000000 --- a/.claude/settings.local.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "permissions": { - "allow": [ - "Bash(kubectl describe *)", - "Bash(helm show *)", - "Bash(kubectl get *)", - "Bash(kubectl logs *)", - "Bash(curl -s -o /dev/null -w \"%{http_code} %{content_type} %{size_download}\\\\n\" -H \"Accept-Encoding: gzip\" http://10.42.0.44:8080/styles.cc735d5acda5c758.css)", - "Bash(curl -s -o /dev/null -w \"%{http_code} %{content_type} %{size_download}\\\\n\" -H \"Accept-Encoding: gzip\" -H \"X-Forwarded-Proto: https\" http://10.42.0.44:8080/styles.cc735d5acda5c758.css)", - "Bash(curl -sv http://10.42.0.44:8080/styles.cc735d5acda5c758.css)", - "Bash(curl -sk -o /dev/null -w \"%{http_code} %{content_type}\\\\n\" --resolve console.gravitee.sttlab.pc:443:192.168.1.18 https://console.gravitee.sttlab.pc/styles.cc735d5acda5c758.css)", - "Bash(kill %1)" - ] - } -} diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..c816185 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +.claude \ No newline at end of file diff --git a/apis/task-management-api.yaml b/apis/task-management-api.yaml new file mode 100644 index 0000000..931ba4b --- /dev/null +++ b/apis/task-management-api.yaml @@ -0,0 +1,198 @@ +apiVersion: gravitee.io/v1alpha1 +kind: ApiV4Definition +metadata: + name: task-management-api + namespace: gravitee-apim +spec: + name: Task Management API + description: Task Management microservice — CRUD tasks with PostgreSQL backend + version: 1.0.0 + type: PROXY + state: STARTED + visibility: PUBLIC + lifecycleState: PUBLISHED + contextRef: + name: gravitee-management-context + namespace: gravitee-apim + listeners: + - type: HTTP + hosts: + - gateway.gravitee.sttlab.pc + paths: + - path: /tasks-management + entrypoints: + - type: http-proxy + endpointGroups: + - name: default + type: http-proxy + endpoints: + - name: task-management + type: http-proxy + secondary: false + inheritConfiguration: false + configuration: + target: "[[ configmap `task-management-config/backend-url` ]]" + sharedConfigurationOverride: + ssl: + trustAll: false + hostnameVerifier: true + trustStore: + type: PEM + content: "[[ secret `task-management-tls/tls.crt` ]]" + sharedConfiguration: {} + plans: + API_KEY_PLAN: + name: API Key Plan + description: Access secured by API Key + security: + type: API_KEY + status: PUBLISHED + flows: [] + JWT_PLAN_FREE: + name: "Free" + description: "JWT — 100 requests per day" + security: + type: JWT + configuration: + signature: RSA_RS256 + publicKeyResolver: JWKS_URL + resolverParameter: "[[ configmap `task-management-config/keycloak-jwks-url` ]]" + useSystemProxy: false + extractClaims: true + userClaim: sub + clientIdClaim: azp + checkRequiredClaims: true + requiredClaims: + - name: iss + value: "[[ configmap `task-management-config/keycloak-issuer` ]]" + status: PUBLISHED + flows: + - name: quota + enabled: true + request: + - name: Quota + policy: quota + enabled: true + configuration: + addHeaders: true + quota: + limit: 100 + periodTime: 1 + periodTimeUnit: DAYS + response: [] + JWT_PLAN_STANDARD: + name: "Standard" + description: "JWT — 10 000 requests per day" + security: + type: JWT + configuration: + signature: RSA_RS256 + publicKeyResolver: JWKS_URL + resolverParameter: "[[ configmap `task-management-config/keycloak-jwks-url` ]]" + useSystemProxy: false + extractClaims: true + userClaim: sub + clientIdClaim: azp + checkRequiredClaims: true + requiredClaims: + - name: iss + value: "[[ configmap `task-management-config/keycloak-issuer` ]]" + status: PUBLISHED + flows: + - name: quota + enabled: true + request: + - name: Quota + policy: quota + enabled: true + configuration: + addHeaders: true + quota: + limit: 10000 + periodTime: 1 + periodTimeUnit: DAYS + response: [] + JWT_PLAN_PREMIUM: + name: "Premium" + description: "JWT — unlimited" + security: + type: JWT + configuration: + signature: RSA_RS256 + publicKeyResolver: JWKS_URL + resolverParameter: "[[ configmap `task-management-config/keycloak-jwks-url` ]]" + useSystemProxy: false + extractClaims: true + userClaim: sub + clientIdClaim: azp + checkRequiredClaims: true + requiredClaims: + - name: iss + value: "[[ configmap `task-management-config/keycloak-issuer` ]]" + status: PUBLISHED + flows: [] + analytics: + enabled: true + logging: + mode: + entrypoint: true + endpoint: true + phase: + request: true + response: true + content: + headers: true + payload: true + messageHeaders: false + messagePayload: false + messageMetadata: false + flows: + - name: traffic-management + enabled: true + request: + - name: Rate Limit + policy: rate-limit + enabled: true + configuration: + addHeaders: true + rate: + limit: 100 + periodTime: 1 + periodTimeUnit: MINUTES + response: [] + - name: request-transformation + enabled: true + request: + - name: inject-application-name + policy: transform-headers + enabled: true + configuration: + addHeaders: + - name: X-Application-Id + value: "{#context.attributes['application']}" + response: [] + - name: response-transformation + enabled: true + request: [] + response: + - name: headers-cleaning + policy: transform-headers + enabled: true + configuration: + removeHeaders: + - server + pages: + specifications: + name: specifications + type: FOLDER + published: true + swagger: + name: OpenAPI Specification + type: SWAGGER + parent: specifications + source: + type: http-fetcher + configuration: + url: "[[ configmap `task-management-config/openapi-url` ]]" + useSystemProxy: false + published: true diff --git a/apis/task-management-config.yaml b/apis/task-management-config.yaml new file mode 100644 index 0000000..c456c41 --- /dev/null +++ b/apis/task-management-config.yaml @@ -0,0 +1,10 @@ +apiVersion: v1 +kind: ConfigMap +metadata: + name: task-management-config + namespace: gravitee-apim +data: + keycloak-issuer: "https://keycloak.sttlab.eu/realms/sttlab" + keycloak-jwks-url: "https://keycloak.sttlab.eu/realms/sttlab/protocol/openid-connect/certs" + backend-url: "https://task-management.tasks.svc.cluster.local:8443" + openapi-url: "http://task-management.tasks.svc.cluster.local:8080/openapi.json" diff --git a/apis/task-management.md b/apis/task-management.md new file mode 100644 index 0000000..8425d6f --- /dev/null +++ b/apis/task-management.md @@ -0,0 +1,156 @@ +# Task Management API — Test Procedure + +## Overview + +The API is exposed at `https://gateway.gravitee.sttlab.pc/tasks-management`. +Two OAuth2 / OIDC flows are supported, both validated via Keycloak realm `sttlab`. + +| Flow | Client | Grant type | Use case | +|---|---|---|---| +| OAuth2 client credentials | `test-backend` | `client_credentials` | Service-to-service | +| OIDC Authorization Code + PKCE | `test-app` | `authorization_code` | User-facing app | + +--- + +## Prerequisites + +- `/etc/hosts` entry: `192.168.1.18 gateway.gravitee.sttlab.pc` +- Keycloak reachable at `https://keycloak.sttlab.eu` +- Gravitee Gateway running (`gravitee-apim` namespace) + +Set the following environment variables before running the commands below: + +```bash +export TEST_BACKEND_SECRET= +export TEST_USER_PASSWORD= +``` + +--- + +## Test 1 — OAuth2 client_credentials (test-backend) + +### Step 1 — Obtain a token + +```bash +TOKEN=$(curl -s -X POST https://keycloak.sttlab.eu/realms/sttlab/protocol/openid-connect/token \ + -d "client_id=test-backend" \ + -d "client_secret=${TEST_BACKEND_SECRET}" \ + -d "grant_type=client_credentials" \ + -d "scope=tasks-full" \ + | jq -r '.access_token') +``` + +To request read-only access, replace `tasks-full` with `tasks-read`. + +### Step 2 — Call the API + +**List tasks (GET):** + +```bash +curl -sk https://gateway.gravitee.sttlab.pc/tasks-management/tasks \ + -H "Authorization: Bearer ${TOKEN}" +``` + +Expected: `HTTP 200` with a JSON array of tasks. + +**Create a task (POST):** + +```bash +curl -sk -X POST https://gateway.gravitee.sttlab.pc/tasks-management/tasks \ + -H "Authorization: Bearer ${TOKEN}" \ + -H "Content-Type: application/json" \ + -d '{"description": "Test task"}' +``` + +Expected: `HTTP 201` with the created task. + +### Step 3 — Verify rejection without token + +```bash +curl -sk -o /dev/null -w "%{http_code}" \ + https://gateway.gravitee.sttlab.pc/tasks-management/tasks +``` + +Expected: `401` + +--- + +## Test 2 — OIDC Authorization Code + PKCE (test-app / test-user) + +Headless flow — no browser required. Keycloak's login form is submitted directly via curl. + +### Step 1 — Generate PKCE parameters + +```bash +CODE_VERIFIER=$(openssl rand -base64 32 | tr -d '=\n' | tr '+/' '-_') +CODE_CHALLENGE=$(echo -n "${CODE_VERIFIER}" | openssl dgst -sha256 -binary | base64 | tr -d '=\n' | tr '+/' '-_') +``` + +### Step 2 — Fetch the login form and extract the action URL + +```bash +curl -sc /tmp/kc-cookies.txt \ + "https://keycloak.sttlab.eu/realms/sttlab/protocol/openid-connect/auth?response_type=code&client_id=test-app&redirect_uri=http://localhost:3000/callback&code_challenge=${CODE_CHALLENGE}&code_challenge_method=S256&scope=openid%20tasks-read" \ + -o /tmp/kc-login.html + +LOGIN_URL=$(grep -o 'action="[^"]*"' /tmp/kc-login.html | sed 's/action="//;s/"$//;s/&/\&/g') +``` + +### Step 3 — Submit credentials and capture the authorization code + +```bash +REDIRECT=$(curl -s -b /tmp/kc-cookies.txt -c /tmp/kc-cookies.txt \ + -X POST "${LOGIN_URL}" \ + -d "username=test-user&password=${TEST_USER_PASSWORD}&credentialId=" \ + -D - -o /dev/null | grep -i "^location:" | tr -d '\r' | sed 's/location: //i') + +CODE=$(echo "${REDIRECT}" | grep -o 'code=[^&]*' | sed 's/code=//') +``` + +### Step 4 — Exchange the authorization code for a token + +```bash +TOKEN=$(curl -s -X POST \ + https://keycloak.sttlab.eu/realms/sttlab/protocol/openid-connect/token \ + -d "grant_type=authorization_code" \ + -d "code=${CODE}" \ + -d "redirect_uri=http://localhost:3000/callback" \ + -d "client_id=test-app" \ + -d "code_verifier=${CODE_VERIFIER}" \ + | jq -r '.access_token') +``` + +### Step 5 — Inspect the token + +```bash +jq -R 'split(".") | .[1] | @base64d | fromjson' <<< "${TOKEN}" +``` + +Verify the following claims: + +| Claim | Expected value | +|---|---| +| `iss` | `https://keycloak.sttlab.eu/realms/sttlab` | +| `azp` | `test-app` | +| `preferred_username` | `test-user` | +| `scope` | contains `tasks-read` | + +### Step 6 — Call the API + +```bash +curl -sk https://gateway.gravitee.sttlab.pc/tasks-management/tasks \ + -H "Authorization: Bearer ${TOKEN}" +``` + +Expected: `HTTP 200` with a JSON array of tasks. + +--- + +## Troubleshooting + +| Symptom | Likely cause | +|---|---| +| `401 Unauthorized` | Missing or expired token — request a new one | +| `401 Unauthorized` | Application not subscribed to plan — check GKO subscription | +| `invalid_scope` error from Keycloak | Scope not assigned as optional on the client — check Keycloak client scopes | +| Token obtained but gateway returns `401` | `azp` claim not matching any subscribed application in Gravitee | diff --git a/apps/test-app.yaml b/apps/test-app.yaml new file mode 100644 index 0000000..126c20a --- /dev/null +++ b/apps/test-app.yaml @@ -0,0 +1,30 @@ +--- +apiVersion: gravitee.io/v1alpha1 +kind: Application +metadata: + name: test-app + namespace: gravitee-apim +spec: + name: test-app + description: "OIDC Authorization Code + PKCE — Keycloak realm sttlab" + contextRef: + name: gravitee-management-context + namespace: gravitee-apim + settings: + app: + type: JWT + clientId: test-app +--- +apiVersion: gravitee.io/v1alpha1 +kind: Subscription +metadata: + name: test-app-keycloak-jwt + namespace: gravitee-apim +spec: + api: + name: task-management-api + namespace: gravitee-apim + application: + name: test-app + namespace: gravitee-apim + plan: JWT_PLAN_PREMIUM diff --git a/apps/test-backend.yaml b/apps/test-backend.yaml new file mode 100644 index 0000000..d00270e --- /dev/null +++ b/apps/test-backend.yaml @@ -0,0 +1,30 @@ +--- +apiVersion: gravitee.io/v1alpha1 +kind: Application +metadata: + name: test-backend + namespace: gravitee-apim +spec: + name: test-backend + description: "OAuth2 client_credentials — Keycloak realm sttlab" + contextRef: + name: gravitee-management-context + namespace: gravitee-apim + settings: + app: + type: JWT + clientId: test-backend +--- +apiVersion: gravitee.io/v1alpha1 +kind: Subscription +metadata: + name: test-backend-keycloak-jwt + namespace: gravitee-apim +spec: + api: + name: task-management-api + namespace: gravitee-apim + application: + name: test-backend + namespace: gravitee-apim + plan: JWT_PLAN_PREMIUM diff --git a/README.md b/platform/README.md similarity index 65% rename from README.md rename to platform/README.md index b58138b..5dce90e 100644 --- a/README.md +++ b/platform/README.md @@ -23,6 +23,7 @@ Internal backends: **Stack:** - **Gravitee APIM 4.x** — Management API (Jetty), Gateway (Vert.x/Netty), Console UI and Developer Portal (Angular/nginx) +- **Gravitee Kubernetes Operator (GKO)** — GitOps management of APIs via CRDs - **MongoDB 8.x** (Bitnami) — management and rate limiting persistence - **Elasticsearch 8.x** (Elastic) — analytics and reporting - **cert-manager** — internal self-signed PKI with automatic renewal @@ -84,35 +85,33 @@ All secrets are created by `secrets-create.sh` before the first deployment. | Secret | Contents | Consumed by | |--------|----------|-------------| | `mongodb-credentials` | root/gravitee MongoDB passwords | Bitnami MongoDB chart | -| `gravitee-mongodb-uri` | `GRAVITEE_MANAGEMENT_MONGODB_URI`, `GRAVITEE_RATELIMIT_MONGODB_URI` | api, gateway (env) | -| `gravitee-jwt` | `GRAVITEE_JWT_SECRET` | api (env) | -| `gravitee-admin` | `admin-password-plain`, `admin-password-bcrypt` | api (env) | -| `gravitee-es-master-credentials` | ES `username`, `password` | api, gateway (env) | -| `gravitee-jks-password` | keystore password | api, gateway (env) + cert-manager | +| `gravitee-mongodb-uri` | `GRAVITEE_MANAGEMENT_MONGODB_URI`, `GRAVITEE_RATELIMIT_MONGODB_URI` | api, gateway (Secret Manager) | +| `gravitee-jwt` | `GRAVITEE_JWT_SECRET` | api (Secret Manager) | +| `gravitee-admin` | `admin-password-plain`, `admin-password-bcrypt` | api (Secret Manager) | +| `gravitee-es-master-credentials` | ES `username`, `password` | api, gateway (Secret Manager) | +| `gravitee-encryption` | `api-properties-encryption-secret` | api, gateway (Secret Manager) | +| `gravitee-jks-password` | keystore password | api, gateway (`JAVA_OPTS` env + Secret Manager for `ssl.keystore.password`) + cert-manager | | `gravitee-ca-trust` | `ca.crt` only — no tls.crt/key | nginx ingress `proxy-ssl-secret` | > `gravitee-ca-trust` is created by `secrets-create.sh` after the certificates are ready (it reads `ca.crt` from `gravitee-ca-tls`). It must contain **only** `ca.crt` — if `tls.crt`/`tls.key` were present, nginx would present them as a client certificate, triggering unintended mTLS toward backends. -> `gravitee-ca-trust` is a dedicated secret containing only `ca.crt`. The `proxy-ssl-secret` nginx annotation presents `tls.crt`/`tls.key` as a client certificate if they exist, which would unintentionally trigger mTLS toward backends. - ### Credential injection -All credentials are injected via `env[].valueFrom.secretKeyRef` (no `envFrom`). The rule is: -- **Non-sensitive config** (endpoints, flags) → `value:` directly in `env` -- **Secrets** (passwords, tokens, URIs with credentials) → `valueFrom.secretKeyRef` +Credentials are resolved at runtime by the **Gravitee Secret Manager** (Kubernetes provider, enabled via `secrets.kubernetes`). Helm values reference secrets using the `secret://kubernetes/:` syntax, which is rendered verbatim into `gravitee.yml` and resolved by Gravitee on startup. -Example from `apim-values.yml`: ```yaml -env: - - name: GRAVITEE_MANAGEMENT_MONGODB_URI # credentials + TLS embedded in URI - valueFrom: - secretKeyRef: - name: gravitee-mongodb-uri - key: GRAVITEE_MANAGEMENT_MONGODB_URI - - name: GRAVITEE_ANALYTICS_ELASTICSEARCH_ENDPOINTS_0 # non-sensitive - value: "https://gravitee-es-master.gravitee-apim.svc.cluster.local:9200" +# In apim-values.yml — resolved by Gravitee Secret Manager at runtime +jwtSecret: "secret://kubernetes/gravitee-jwt:GRAVITEE_JWT_SECRET" +mongo: + uri: "secret://kubernetes/gravitee-mongodb-uri:GRAVITEE_MANAGEMENT_MONGODB_URI" +es: + security: + username: "secret://kubernetes/gravitee-es-master-credentials:username" + password: "secret://kubernetes/gravitee-es-master-credentials:password" ``` +The only secret still injected as a Kubernetes env var is `gravitee-jks-password` (`JKS_PASSWORD`), because it is needed in `JAVA_OPTS` before the JVM starts — before the Gravitee Secret Manager is available. + --- ## Internal TLS @@ -131,9 +130,7 @@ MongoDB runs with `--tlsAllowConnectionsWithoutCertificates` (no client mTLS) bu ### Elasticsearch -HTTPS with basic auth. Server certificate signed by the Gravitee CA, validated by the same JVM truststore. Username/password injected via Gravitee property-path env vars: -- `GRAVITEE_ANALYTICS_ELASTICSEARCH_SECURITY_USERNAME/PASSWORD` → Management API -- `GRAVITEE_REPORTERS_ELASTICSEARCH_SECURITY_USERNAME/PASSWORD` → Gateway +HTTPS with basic auth. Server certificate signed by the Gravitee CA, validated by the same JVM truststore. The HTTPS endpoint and credentials are configured in `apim-values.yml` via `es.endpoints` / `es.security.*` and resolved at runtime by the Gravitee Secret Manager. ### nginx → backends (proxy TLS) @@ -208,6 +205,18 @@ helm upgrade --install elasticsearch elastic/elasticsearch -n gravitee-apim -f e # 6. Deploy Gravitee helm upgrade --install graviteeio-apim graviteeio/apim -n gravitee-apim -f apim-values.yml + +# 7. Install GKO (after APIM is ready) +# Create GKO credentials secret first (see GKO section below) +helm upgrade --install gko graviteeio/gko \ + --namespace gravitee-apim \ + --set manager.httpClient.trustStore.path=/etc/ssl/certs/gravitee-ca.crt \ + --set-json 'manager.volumes=[{"name":"gravitee-ca","secret":{"secretName":"gravitee-ca-tls","items":[{"key":"ca.crt","path":"gravitee-ca.crt"}]}}]' \ + --set-json 'manager.volumeMounts=[{"name":"gravitee-ca","mountPath":"/etc/ssl/certs/gravitee-ca.crt","subPath":"gravitee-ca.crt","readOnly":true}]' + +# 8. Apply GKO resources +kubectl apply -f gko-management-context.yaml +kubectl apply -f gko-api.yaml ``` ### Rebuild workloads only (secrets and certs preserved) @@ -250,6 +259,77 @@ kubectl get secret gravitee-admin -n gravitee-apim \ --- +## Gravitee Kubernetes Operator (GKO) + +GKO enables GitOps-driven API management: APIs, plans and subscriptions are declared as Kubernetes CRDs and automatically synced to the Management API. + +### Installation + +```bash +helm upgrade --install gko graviteeio/gko \ + --namespace gravitee-apim \ + --set manager.httpClient.trustStore.path=/etc/ssl/certs/gravitee-ca.crt \ + --set-json 'manager.volumes=[{"name":"gravitee-ca","secret":{"secretName":"gravitee-ca-tls","items":[{"key":"ca.crt","path":"gravitee-ca.crt"}]}}]' \ + --set-json 'manager.volumeMounts=[{"name":"gravitee-ca","mountPath":"/etc/ssl/certs/gravitee-ca.crt","subPath":"gravitee-ca.crt","readOnly":true}]' +``` + +The Gravitee CA is mounted into the GKO pod so its HTTP client trusts the Management API's certificate (port 83, HTTPS). Without this, GKO would fail to connect when validating or reconciling CRDs. + +### GKO secrets + +| Secret | Contents | Purpose | +|--------|----------|---------| +| `gko-management-credentials` | `username`, `password` | GKO → Management API auth via `ManagementContext.spec.auth.secretRef` | + +Create after deploying APIM: +```bash +ADMIN_PLAIN=$(kubectl get secret gravitee-admin -n gravitee-apim \ + -o jsonpath='{.data.admin-password-plain}' | base64 -d) +kubectl create secret generic gko-management-credentials -n gravitee-apim \ + --from-literal=username=admin \ + --from-literal=password="${ADMIN_PLAIN}" +``` + +### CRDs + +| CRD | Description | +|-----|-------------| +| `ManagementContext` | Connection to a Gravitee Management API instance | +| `ApiV4Definition` | Gravitee v4 API (proxy, message, native) | +| `ApiDefinition` | Gravitee v2 API (legacy) | +| `Application` | Consumer application | +| `ApiResource` | Shared resource (cache, OAuth2, etc.) | +| `SharedPolicyGroup` | Reusable policy group | +| `Subscription` | API subscription | + +### ManagementContext + +`gko-management-context.yaml` — connects GKO to the local Management API: +```yaml +spec: + baseUrl: https://graviteeio-apim-api.gravitee-apim.svc.cluster.local:83 + environmentId: DEFAULT + organizationId: DEFAULT + auth: + secretRef: + name: gko-management-credentials + namespace: gravitee-apim +``` + +> Port 83 is the Kubernetes service port (maps to container port 8083). + +### Deploying an API + +```bash +kubectl apply -f gko-management-context.yaml +kubectl apply -f gko-api.yaml +kubectl get apiv4definition -n gravitee-apim +``` + +The `processingStatus: Completed` and `state: STARTED` in the status confirm the API is live on the gateway. + +--- + ## Security notes - The Gravitee CA is self-signed. Replace with a Vault PKI Issuer in production (see comment in `certificates.yml`). diff --git a/apim-values.yml b/platform/apim-values-env-var.yml similarity index 92% rename from apim-values.yml rename to platform/apim-values-env-var.yml index b833505..cf71fa3 100644 --- a/apim-values.yml +++ b/platform/apim-values-env-var.yml @@ -7,6 +7,15 @@ adminAccountEnable: true adminPasswordBcrypt: "${GRAVITEE_ADMIN_PASSWORD_BCRYPT}" +# ============================================================ +# Kubernetes Secret Provider +# ============================================================ +secrets: + kubernetes: + enabled: true + namespace: gravitee-apim + timeoutMs: 3000 + # ============================================================ # API Gateway (data plane) - 2 replicas # ============================================================ @@ -58,6 +67,11 @@ gateway: secretKeyRef: name: gravitee-jks-password key: password + - name: GRAVITEE_API_PROPERTIES_ENCRYPTION_SECRET + valueFrom: + secretKeyRef: + name: gravitee-encryption + key: api-properties-encryption-secret - name: JAVA_OPTS value: "-Djavax.net.ssl.trustStoreType=PKCS12 -Djavax.net.ssl.trustStore=/run/secrets/tls/truststore.p12 -Djavax.net.ssl.trustStorePassword=$(JKS_PASSWORD)" @@ -164,6 +178,11 @@ api: secretKeyRef: name: gravitee-jks-password key: password + - name: GRAVITEE_API_PROPERTIES_ENCRYPTION_SECRET + valueFrom: + secretKeyRef: + name: gravitee-encryption + key: api-properties-encryption-secret - name: JAVA_OPTS value: "-Djavax.net.ssl.trustStoreType=PKCS12 -Djavax.net.ssl.trustStore=/run/secrets/tls/truststore.p12 -Djavax.net.ssl.trustStorePassword=$(JKS_PASSWORD)" @@ -186,7 +205,7 @@ api: resources: requests: cpu: 200m - memory: 768Mi + memory: 2Gi limits: cpu: 1000m memory: 2Gi diff --git a/platform/apim-values.yml b/platform/apim-values.yml new file mode 100644 index 0000000..8577e1f --- /dev/null +++ b/platform/apim-values.yml @@ -0,0 +1,268 @@ +# Gravitee APIM OSS - prod-like single-node k3s deployment +# - Domain: gravitee.sttlab.pc +# - Ingress: nginx +# - TLS everywhere (ingress + internal component HTTPS) +# - Credentials resolved at runtime via Gravitee Secret Manager (kubernetes provider) + +adminAccountEnable: true +adminPasswordBcrypt: "secret://kubernetes/gravitee-admin:admin-password-bcrypt" + +jwtSecret: "secret://kubernetes/gravitee-jwt:GRAVITEE_JWT_SECRET" + +# ============================================================ +# MongoDB (management + ratelimit, same URI) +# ============================================================ +mongo: + uri: "secret://kubernetes/gravitee-mongodb-uri:GRAVITEE_MANAGEMENT_MONGODB_URI" + +# ============================================================ +# Elasticsearch +# ============================================================ +es: + endpoints: + - "https://gravitee-es-master.gravitee-apim.svc.cluster.local:9200" + security: + enabled: true + username: "secret://kubernetes/gravitee-es-master-credentials:username" + password: "secret://kubernetes/gravitee-es-master-credentials:password" + +# ============================================================ +# Kubernetes Secret Provider +# ============================================================ +secrets: + kubernetes: + enabled: true + namespace: gravitee-apim + timeoutMs: 3000 + +# ============================================================ +# API Gateway (data plane) - 2 replicas +# ============================================================ +gateway: + enabled: true + replicaCount: 2 + + api: + properties: + encryption: + secret: "secret://kubernetes/gravitee-encryption:api-properties-encryption-secret" + + extraVolumes: | + - name: gateway-internal-tls + secret: + secretName: gateway-internal-tls + items: + - key: keystore.p12 + path: keystore.p12 + - key: truststore.p12 + path: truststore.p12 + extraVolumeMounts: | + - name: gateway-internal-tls + mountPath: /run/secrets/tls + readOnly: true + + env: + - name: JKS_PASSWORD + valueFrom: + secretKeyRef: + name: gravitee-jks-password + key: password + - name: JAVA_OPTS + value: "-Djavax.net.ssl.trustStoreType=PKCS12 -Djavax.net.ssl.trustStore=/run/secrets/tls/truststore.p12 -Djavax.net.ssl.trustStorePassword=$(JKS_PASSWORD)" + + # Enable HTTPS on the gateway listener (port 8082) + ssl: + enabled: true + keystore: + type: pkcs12 + path: /run/secrets/tls/keystore.p12 + password: "secret://kubernetes/gravitee-jks-password:password" + + resources: + requests: + cpu: 200m + memory: 512Mi + limits: + cpu: 1000m + memory: 1Gi + + service: + type: ClusterIP + externalPort: 443 + internalPort: 8082 + + ingress: + enabled: true + ingressClassName: nginx + annotations: + nginx.ingress.kubernetes.io/backend-protocol: "HTTPS" + nginx.ingress.kubernetes.io/proxy-ssl-verify: "on" + nginx.ingress.kubernetes.io/proxy-ssl-secret: "gravitee-apim/gravitee-ca-trust" + nginx.ingress.kubernetes.io/configuration-snippet: | + proxy_ssl_name gateway.gravitee.sttlab.pc; + hosts: + - gateway.gravitee.sttlab.pc + path: / + pathType: Prefix + tls: + - hosts: + - gateway.gravitee.sttlab.pc + secretName: gateway-tls + + autoscaling: + enabled: false + +# ============================================================ +# Management API (control plane) - 1 replica +# ============================================================ +api: + enabled: true + replicaCount: 1 + + api: + properties: + encryption: + secret: "secret://kubernetes/gravitee-encryption:api-properties-encryption-secret" + + extraVolumes: | + - name: api-internal-tls + secret: + secretName: api-internal-tls + items: + - key: keystore.p12 + path: keystore.p12 + - key: truststore.p12 + path: truststore.p12 + extraVolumeMounts: | + - name: api-internal-tls + mountPath: /run/secrets/tls + readOnly: true + + env: + - name: JKS_PASSWORD + valueFrom: + secretKeyRef: + name: gravitee-jks-password + key: password + - name: JAVA_OPTS + value: "-Djavax.net.ssl.trustStoreType=PKCS12 -Djavax.net.ssl.trustStore=/run/secrets/tls/truststore.p12 -Djavax.net.ssl.trustStorePassword=$(JKS_PASSWORD)" + + # Enable HTTPS on Management API + Portal API listeners + http: + services: + core: + http: + enabled: true + port: 18083 + host: 0.0.0.0 + + ssl: + enabled: true + keystore: + type: pkcs12 + path: /run/secrets/tls/keystore.p12 + password: "secret://kubernetes/gravitee-jks-password:password" + + resources: + requests: + cpu: 200m + memory: 2Gi + limits: + cpu: 1000m + memory: 2Gi + + ingress: + management: + enabled: true + ingressClassName: nginx + annotations: + nginx.ingress.kubernetes.io/backend-protocol: "HTTPS" + nginx.ingress.kubernetes.io/proxy-ssl-verify: "on" + nginx.ingress.kubernetes.io/proxy-ssl-secret: "gravitee-apim/gravitee-ca-trust" + nginx.ingress.kubernetes.io/configuration-snippet: | + proxy_ssl_name api.gravitee.sttlab.pc; + path: /management + pathType: Prefix + hosts: + - api.gravitee.sttlab.pc + tls: + - hosts: + - api.gravitee.sttlab.pc + secretName: api-tls + portal: + enabled: true + ingressClassName: nginx + annotations: + nginx.ingress.kubernetes.io/backend-protocol: "HTTPS" + nginx.ingress.kubernetes.io/proxy-ssl-verify: "on" + nginx.ingress.kubernetes.io/proxy-ssl-secret: "gravitee-apim/gravitee-ca-trust" + nginx.ingress.kubernetes.io/configuration-snippet: | + proxy_ssl_name api.gravitee.sttlab.pc; + path: /portal + pathType: Prefix + hosts: + - api.gravitee.sttlab.pc + tls: + - hosts: + - api.gravitee.sttlab.pc + secretName: api-tls + +# ============================================================ +# Management UI (Console) - 1 replica +# ============================================================ +ui: + enabled: true + replicaCount: 1 + + resources: + requests: + cpu: 100m + memory: 128Mi + limits: + cpu: 500m + memory: 256Mi + + ingress: + enabled: true + ingressClassName: nginx + annotations: + nginx.ingress.kubernetes.io/use-regex: "true" + hosts: + - console.gravitee.sttlab.pc + path: /(.*) + pathType: ImplementationSpecific + tls: + - hosts: + - console.gravitee.sttlab.pc + secretName: console-tls + + +# ============================================================ +# Developer Portal UI - 1 replica +# ============================================================ +portal: + enabled: true + replicaCount: 1 + + resources: + requests: + cpu: 100m + memory: 128Mi + limits: + cpu: 500m + memory: 256Mi + + ingress: + enabled: true + ingressClassName: nginx + annotations: + nginx.ingress.kubernetes.io/use-regex: "true" + nginx.ingress.kubernetes.io/rewrite-target: /$1 + hosts: + - portal.gravitee.sttlab.pc + path: /(.*) + pathType: ImplementationSpecific + tls: + - hosts: + - portal.gravitee.sttlab.pc + secretName: portal-tls diff --git a/certificates.yml b/platform/certificates.yml similarity index 100% rename from certificates.yml rename to platform/certificates.yml diff --git a/deploy.sh b/platform/deploy.sh similarity index 94% rename from deploy.sh rename to platform/deploy.sh index 95479c4..d6c4da1 100644 --- a/deploy.sh +++ b/platform/deploy.sh @@ -32,8 +32,12 @@ kubectl get ingressclass nginx >/dev/null 2>&1 || { echo "==> Step 1/6 : Create namespace" kubectl create namespace "${NAMESPACE}" --dry-run=client -o yaml | kubectl apply -f - -echo "==> Step 2/6 : Create credential secrets (idempotent)" -"${SCRIPT_DIR}/secrets-create.sh" +echo "==> Step 2/6 : Create credential secrets (first install only)" +if kubectl -n "${NAMESPACE}" get secret gravitee-admin >/dev/null 2>&1; then + echo " Secrets already exist — skipping. Delete them manually to recreate." +else + "${SCRIPT_DIR}/secrets-create.sh" +fi echo "==> Step 3/6 : Apply cert-manager Issuers + Certificates" kubectl apply -f "${SCRIPT_DIR}/certificates.yml" diff --git a/elastic-values.yml b/platform/elastic-values.yml similarity index 100% rename from elastic-values.yml rename to platform/elastic-values.yml diff --git a/platform/gko-init-settings.yaml b/platform/gko-init-settings.yaml new file mode 100644 index 0000000..2fcf8ef --- /dev/null +++ b/platform/gko-init-settings.yaml @@ -0,0 +1,55 @@ +apiVersion: batch/v1 +kind: Job +metadata: + name: gravitee-init-settings + namespace: gravitee-apim + annotations: + helm.sh/hook: post-install,post-upgrade + helm.sh/hook-weight: "10" + helm.sh/hook-delete-policy: before-hook-creation +spec: + ttlSecondsAfterFinished: 300 + template: + spec: + restartPolicy: OnFailure + containers: + - name: init-settings + image: curlimages/curl:latest + command: + - /bin/sh + - -c + - | + set -e + MGMT_URL="https://graviteeio-apim-api.gravitee-apim.svc.cluster.local:83" + + echo "Waiting for Management API..." + until curl -sk -u "${ADMIN_USER}:${ADMIN_PASSWORD}" \ + "${MGMT_URL}/management/v2/ui/bootstrap" | grep -q "baseURL"; do + sleep 5 + done + + echo "Fetching current settings..." + SETTINGS=$(curl -sk -u "${ADMIN_USER}:${ADMIN_PASSWORD}" \ + "${MGMT_URL}/management/organizations/DEFAULT/environments/DEFAULT/settings") + + echo "Updating portal entrypoint..." + UPDATED=$(echo "$SETTINGS" | sed 's|"entrypoint":"[^"]*"|"entrypoint":"https://gateway.gravitee.sttlab.pc"|') + + curl -sk -u "${ADMIN_USER}:${ADMIN_PASSWORD}" \ + -X POST \ + -H "Content-Type: application/json" \ + -d "$UPDATED" \ + "${MGMT_URL}/management/organizations/DEFAULT/environments/DEFAULT/settings" + + echo "Done." + env: + - name: ADMIN_USER + valueFrom: + secretKeyRef: + name: gravitee-admin + key: admin-username + - name: ADMIN_PASSWORD + valueFrom: + secretKeyRef: + name: gravitee-admin + key: admin-password-plain diff --git a/platform/gko-management-context.yaml b/platform/gko-management-context.yaml new file mode 100644 index 0000000..67da52a --- /dev/null +++ b/platform/gko-management-context.yaml @@ -0,0 +1,13 @@ +apiVersion: gravitee.io/v1alpha1 +kind: ManagementContext +metadata: + name: gravitee-management-context + namespace: gravitee-apim +spec: + baseUrl: https://graviteeio-apim-api.gravitee-apim.svc.cluster.local:83 + environmentId: DEFAULT + organizationId: DEFAULT + auth: + secretRef: + name: gko-management-credentials + namespace: gravitee-apim diff --git a/mongo-values.yml b/platform/mongo-values.yml similarity index 95% rename from mongo-values.yml rename to platform/mongo-values.yml index 5f61ded..3919bae 100644 --- a/mongo-values.yml +++ b/platform/mongo-values.yml @@ -23,6 +23,9 @@ tls: extraFlags: - "--tlsAllowConnectionsWithoutCertificates" +updateStrategy: + type: Recreate + persistence: enabled: true size: 8Gi diff --git a/platform/secrets-create.sh b/platform/secrets-create.sh new file mode 100755 index 0000000..5c0a740 --- /dev/null +++ b/platform/secrets-create.sh @@ -0,0 +1,82 @@ +#!/usr/bin/env bash +# Create all credential secrets before the first helm install. +# Skips any secret that already exists — delete it first to regenerate. + +set -euo pipefail + +NS="gravitee-apim" + +# Ensure namespace exists +kubectl create namespace "${NS}" --dry-run=client -o yaml | kubectl apply -f - + +secret_exists() { + kubectl -n "${NS}" get secret "$1" >/dev/null 2>&1 +} + +echo "==> Creating MongoDB credentials" +if secret_exists mongodb-credentials; then + echo " mongodb-credentials already exists, skipping" +else + MONGO_ROOT_PASSWORD=$(openssl rand -base64 24 | tr -dc 'A-Za-z0-9' | head -c 16) + MONGO_GRAVITEE_PASSWORD=$(openssl rand -base64 24 | tr -dc 'A-Za-z0-9' | head -c 16) + kubectl -n "${NS}" create secret generic mongodb-credentials \ + --from-literal=mongodb-root-password="${MONGO_ROOT_PASSWORD}" \ + --from-literal=mongodb-passwords="${MONGO_GRAVITEE_PASSWORD}" \ + --from-literal=mongodb-replica-set-key='' +fi + +echo "==> Creating MongoDB URI secret" +if secret_exists gravitee-mongodb-uri; then + echo " gravitee-mongodb-uri already exists, skipping" +else + MONGO_GRAVITEE_PASSWORD=$(kubectl -n "${NS}" get secret mongodb-credentials \ + -o jsonpath='{.data.mongodb-passwords}' | base64 -d) + MONGO_URI="mongodb://gravitee:${MONGO_GRAVITEE_PASSWORD}@mongodb.gravitee-apim.svc.cluster.local:27017/gravitee?tls=true&authSource=gravitee" + kubectl -n "${NS}" create secret generic gravitee-mongodb-uri \ + --from-literal=GRAVITEE_MANAGEMENT_MONGODB_URI="${MONGO_URI}" \ + --from-literal=GRAVITEE_RATELIMIT_MONGODB_URI="${MONGO_URI}" +fi + +echo "==> Creating Gravitee admin credentials" +if secret_exists gravitee-admin; then + echo " gravitee-admin already exists, skipping" +else + GRAVITEE_ADMIN_PASSWORD=$(openssl rand -base64 24 | tr -dc 'A-Za-z0-9' | head -c 16) + ADMIN_BCRYPT=$(htpasswd -bnBC 10 "" "${GRAVITEE_ADMIN_PASSWORD}" | tr -d ':\n') + kubectl -n "${NS}" create secret generic gravitee-admin \ + --from-literal=admin-username='admin' \ + --from-literal=admin-password-plain="${GRAVITEE_ADMIN_PASSWORD}" \ + --from-literal=admin-password-bcrypt="${ADMIN_BCRYPT}" +fi + +echo "==> Creating JKS keystore password" +if secret_exists gravitee-jks-password; then + echo " gravitee-jks-password already exists, skipping" +else + JKS_PASSWORD=$(openssl rand -base64 24 | tr -dc 'A-Za-z0-9' | head -c 20) + kubectl -n "${NS}" create secret generic gravitee-jks-password \ + --from-literal=password="${JKS_PASSWORD}" +fi + +echo "==> Creating JWT signing secret" +if secret_exists gravitee-jwt; then + echo " gravitee-jwt already exists, skipping" +else + JWT_SECRET=$(openssl rand -base64 48 | tr -d '\n') + kubectl -n "${NS}" create secret generic gravitee-jwt \ + --from-literal=GRAVITEE_JWT_SECRET="${JWT_SECRET}" +fi + +echo "==> Creating API properties encryption key" +if secret_exists gravitee-encryption; then + echo " gravitee-encryption already exists, skipping" +else + ENCRYPTION_KEY=$(openssl rand -hex 16) + kubectl -n "${NS}" create secret generic gravitee-encryption \ + --from-literal=api-properties-encryption-secret="${ENCRYPTION_KEY}" +fi + +echo "" +echo "==> Done. Secrets in namespace ${NS}:" +kubectl -n "${NS}" get secrets | grep -E 'mongodb-credentials|gravitee-mongodb-uri|gravitee-admin|gravitee-jwt|gravitee-jks-password|gravitee-ca-trust' +echo "" diff --git a/secrets-create.sh b/secrets-create.sh deleted file mode 100644 index a053a5a..0000000 --- a/secrets-create.sh +++ /dev/null @@ -1,64 +0,0 @@ -#!/usr/bin/env bash -# Create all credential secrets manually before helm install. -# Run once. Re-running with new values requires `kubectl delete secret` first. - -set -euo pipefail - -NS="gravitee-apim" -MONGO_ROOT_PASSWORD=$(openssl rand -base64 24 | tr -dc 'A-Za-z0-9' | head -c 16) -MONGO_GRAVITEE_PASSWORD=$(openssl rand -base64 24 | tr -dc 'A-Za-z0-9' | head -c 16) -GRAVITEE_ADMIN_PASSWORD=$(openssl rand -base64 24 | tr -dc 'A-Za-z0-9' | head -c 16) - -# Ensure namespace exists -kubectl create namespace "${NS}" --dry-run=client -o yaml | kubectl apply -f - - -echo "==> Creating MongoDB credentials" -# Used by both the MongoDB chart and the Gravitee chart (consumer) -kubectl -n "${NS}" create secret generic mongodb-credentials \ - --from-literal=mongodb-root-password=${MONGO_ROOT_PASSWORD} \ - --from-literal=mongodb-passwords=${MONGO_GRAVITEE_PASSWORD} \ - --from-literal=mongodb-replica-set-key='' \ - --dry-run=client -o yaml | kubectl apply -f - - -# Full MongoDB URIs injected via env var override into Gravitee components. -# GRAVITEE_MANAGEMENT_MONGODB_URI overrides management.mongodb.uri in api. -# GRAVITEE_RATELIMIT_MONGODB_URI overrides ratelimit.mongodb.uri in gateway. -MONGO_URI="mongodb://gravitee:${MONGO_GRAVITEE_PASSWORD}@mongodb.gravitee-apim.svc.cluster.local:27017/gravitee?tls=true&authSource=gravitee" -kubectl -n "${NS}" create secret generic gravitee-mongodb-uri \ - --from-literal=GRAVITEE_MANAGEMENT_MONGODB_URI="${MONGO_URI}" \ - --from-literal=GRAVITEE_RATELIMIT_MONGODB_URI="${MONGO_URI}" \ - --dry-run=client -o yaml | kubectl apply -f - - -echo "==> Creating Gravitee admin credentials" -ADMIN_BCRYPT=$(htpasswd -bnBC 10 "" "${GRAVITEE_ADMIN_PASSWORD}" | tr -d ':\n') - -kubectl -n "${NS}" create secret generic gravitee-admin \ - --from-literal=admin-username='admin' \ - --from-literal=admin-password-plain="${GRAVITEE_ADMIN_PASSWORD}" \ - --from-literal=admin-password-bcrypt="${ADMIN_BCRYPT}" \ - --dry-run=client -o yaml | kubectl apply -f - - -echo "==> Creating JKS keystore password (used by cert-manager keystores and JAVA_OPTS)" -JKS_PASSWORD=$(openssl rand -base64 24 | tr -dc 'A-Za-z0-9' | head -c 20) -kubectl -n "${NS}" create secret generic gravitee-jks-password \ - --from-literal=password="${JKS_PASSWORD}" \ - --dry-run=client -o yaml | kubectl apply -f - - -echo "==> Creating JWT signing secret (used by Management API)" -JWT_SECRET=$(openssl rand -base64 48 | tr -d '\n') -kubectl -n "${NS}" create secret generic gravitee-jwt \ - --from-literal=GRAVITEE_JWT_SECRET="${JWT_SECRET}" \ - --dry-run=client -o yaml | kubectl apply -f - - -echo "==> Creating CA trust secret for nginx ingress proxy-ssl-secret" -# Contains only ca.crt (no tls.crt/key) to avoid nginx presenting the CA as a client cert. -kubectl -n "${NS}" get secret gravitee-ca-tls -o jsonpath='{.data.ca\.crt}' | base64 -d | \ - kubectl -n "${NS}" create secret generic gravitee-ca-trust \ - --from-file=ca.crt=/dev/stdin \ - --dry-run=client -o yaml | kubectl apply -f - - -echo "" -echo "==> Done. Secrets created in namespace ${NS}:" -kubectl -n "${NS}" get secrets | grep -E 'mongodb-credentials|gravitee-mongodb-uri|gravitee-admin|gravitee-jwt|gravitee-jks-password|gravitee-ca-trust' -echo "" -