From 43bcd2db7b83247b3a7738c1b1a6fc715c37e154 Mon Sep 17 00:00:00 2001 From: Jarvis Date: Fri, 16 Oct 2026 08:37:15 +0000 Subject: [PATCH 1/8] chore(aisix): drop the retired observability.usage_event.request_headers --- charts/aisix/README.md | 1 - charts/aisix/ci/config-values.yaml | 2 -- charts/aisix/values.yaml | 3 --- 3 files changed, 6 deletions(-) diff --git a/charts/aisix/README.md b/charts/aisix/README.md index 8b84cc0..8733937 100644 --- a/charts/aisix/README.md +++ b/charts/aisix/README.md @@ -564,7 +564,6 @@ rejects. New installs set the value in its own key in `values.yaml` or under | config.observability.metrics.prometheus.enabled | bool | `true` | Serve Prometheus metrics on the metrics listener (`containerPorts.metrics`) | | config.observability.metrics.prometheus.path | string | `"/metrics"` | Metrics path | | config.observability.service_name | string | `"aisix"` | Service name reported in telemetry | -| config.observability.usage_event.request_headers | list | `[]` | Request headers whose values every usage event records (lowercase names; credential headers and duplicates are refused at startup) | | config.proxy.real_ip.header | string | `"x-forwarded-for"` | Header carrying the client IP | | config.proxy.real_ip.recursive | bool | `false` | Walk the header right to left past every trusted proxy | | config.proxy.real_ip.trusted_proxies | list | `[]` | CIDRs (or bare IPs) of proxies trusted to set the client-IP header | diff --git a/charts/aisix/ci/config-values.yaml b/charts/aisix/ci/config-values.yaml index 3099f45..41c620c 100644 --- a/charts/aisix/ci/config-values.yaml +++ b/charts/aisix/ci/config-values.yaml @@ -18,8 +18,6 @@ config: heap_profiling: auto_dump: thresholds: [0.7, 0.95] - usage_event: - request_headers: [x-sub-user, x-department] proxy: real_ip: trusted_proxies: ["10.0.0.0/8", "127.0.0.1/32"] diff --git a/charts/aisix/values.yaml b/charts/aisix/values.yaml index 72f21de..7bd7674 100644 --- a/charts/aisix/values.yaml +++ b/charts/aisix/values.yaml @@ -300,9 +300,6 @@ config: dir: /var/lib/aisix/heap # -- Newest profiles kept keep: 5 - usage_event: - # -- Request headers whose values every usage event records (lowercase names; credential headers and duplicates are refused at startup) - request_headers: [] cache: # -- Legacy cache backend switch, `memory` or `redis`. `redis` requires # `cache.redis`. Which cache serves a request is chosen per cache policy From 85f86a8d79c1187b62061249d78bb9c15c17a5a0 Mon Sep 17 00:00:00 2001 From: Jarvis Date: Tue, 20 Oct 2026 02:32:17 +0000 Subject: [PATCH 2/8] chore(aisix-cp): sync from AISIX-Cloud v1.7.0-rc.1 Takes helm/aisix-cp at v1.7.0-rc.1: a checksum/secret pod annotation on cp-api and dp-manager (with its aisix-cp.secretChecksum helper) so a change to a value in a chart-rendered Secret rolls the pods, and updated values descriptions (api.dpImage, ui.defaultLocale now a fallback behind the language picker, AISIX Enterprise / AISIX Cloud naming). Public adaptations are kept: docker.io registries, empty image tags defaulting to the chart appVersion, the real gateway image in NOTES.txt, the 1.5.0 DP image default in _legacy-env.tpl, and the public README. --- charts/aisix-cp/README.md | 6 ++--- charts/aisix-cp/templates/_helpers.tpl | 17 ++++++++++++ charts/aisix-cp/templates/api-deployment.yaml | 5 ++++ charts/aisix-cp/templates/dpm-deployment.yaml | 5 ++++ charts/aisix-cp/values.yaml | 26 ++++++++++++------- 5 files changed, 46 insertions(+), 13 deletions(-) diff --git a/charts/aisix-cp/README.md b/charts/aisix-cp/README.md index c872cf7..d48b6bb 100644 --- a/charts/aisix-cp/README.md +++ b/charts/aisix-cp/README.md @@ -408,7 +408,7 @@ plane and hands out the gateway image from the same release as the chart. | api.bootstrapAdmin.password | string | `""` | Initial password (12 to 128 characters), stored in the chart's Secret. Ignored when existingSecret is set. Empty generates one (see above) | | api.corsAllowedOrigins | list | `[]` | Browser origins allowed to call cp-api cross-origin, as bare origins (scheme://host[:port]). Empty — the default — writes no CORS headers at all, which is what a normal install wants: cp-api serves the API and proxies the dashboard, so the browser has only one origin and nothing is ever cross-origin. Populate it only to let a dashboard served from somewhere else reach this API directly, such as a frontend-only PR preview. An entry is a bare origin (https://host[:port]), or `https://*` plus a suffix naming at least three labels (`https://*-api7ai.vercel.app`) — a PR preview's host carries the branch name, so an exact list would mean redeploying cp-api for every pull request. Read the suffix as collision-resistance, NOT as a guarantee: it is a byte suffix, and on a shared host like `.vercel.app` another account can claim a name ending the same way, so a grant that needs a real boundary belongs on a domain you control. Every entry must be spelled the way a BROWSER serializes an origin, and the chart refuses one that is not, naming the spelling to use — a bare `*`, a path, query, fragment or userinfo, a trailing dot, a non-https origin (loopback may use http), an internationalized host given as anything but punycode, a numeric last label that is not a dotted quad, an IPv6 address in any spelling but the one it is sent in, the scheme's default port written out, a port with a leading zero, and a port above 65535. cp-api applies the same rules at startup, so a value this chart accepts is one it will start with. | | api.deploymentMode | string | `"on-premises"` | How this control plane is deployed: `on-premises` or `saas`. `on-premises` serves one company's organizations: nobody signs up on their own (accounts come from invitations), organizations cannot be created, billing is off, and cp-api's outbound calls may reach private addresses unless an outbound value below says otherwise. `saas` is the multi-tenant hosted service — open sign-up, organization creation and billing — and requires twoDSN.enabled | -| api.dpImage | string | `""` | Gateway image the console hands out in the install snippets it generates for a new data plane. Empty uses cp-api's own default: the gateway of the same release, docker.io/api7/aisix:, or ghcr.io/api7/aisix:dev on a build that is not a release | +| api.dpImage | string | `""` | Gateway image the console hands out in the install snippets it generates for a new data plane. Empty uses cp-api's own default: the gateway of the same release, docker.io/api7/aisix:, or ghcr.io/api7/aisix:dev on a build that is not a release. The Helm snippet takes only its repository; the gateway chart's appVersion picks the tag | | api.dpmgrBaseURL | string | `""` | dp-manager /dp/* mTLS endpoint that data-plane hosts dial, as baked into the generated install snippets. Also passed to the dpm deployment, which seeds this host into the TLS server certificate it presents — so an IP address works here, not only a DNS name. When dpm.service.type is NodePort, use https://:. | | api.extraEnvVars | list | `[]` | Extra environment for cp-api: system-level settings only (for example TZ). Every cp-api setting has its own value in this section; an env var set here for one of them still overrides the rendered config file, which is how values set this way before those keys existed keep working. Overriding a name the chart itself sets is only a compatibility path for existing deployments. For a name chart 1.5.0 already set, the chart keeps its own entry ahead of yours, so the name appears twice and Helm 4's server-side apply rejects the install. New installs set the value in this file instead. | | api.guardrailTestConnectionAllowInsecure | bool | `nil` | Whether the guardrail "test connection" probe may reach plain http, private addresses and non-Azure hosts (an internal or Private Link endpoint). Unset follows deploymentMode: allowed on-premises, blocked on saas | @@ -509,11 +509,11 @@ plane and hands out the gateway image from the same release as the chart. | serviceAccount.annotations | object | `{}` | Annotations for the ServiceAccount, e.g. a cloud IAM role binding | | serviceAccount.create | bool | `true` | Create a ServiceAccount for the control-plane pods | | serviceAccount.name | string | `""` | Name of the ServiceAccount. Empty derives one from the release name; set it to use a ServiceAccount you manage yourself (create: false) | -| twoDSN.enabled | bool | `false` | Open a second, Row-Level-Security-enforced pool for tenant queries. Leave off for an On-Premises deployment | +| twoDSN.enabled | bool | `false` | Open a second, Row-Level-Security-enforced pool for tenant queries. Leave off for AISIX Enterprise | | twoDSN.existingSecret | string | `""` | Optionally source the serving password from an existing Secret instead of servingPassword above. Key defaults to "serving-password". | | twoDSN.existingSecretKey | string | `"serving-password"` | Key inside twoDSN.existingSecret holding the serving password | | twoDSN.servingPassword | string | `""` | Password cp-api assigns to the cp_api_app serving role and uses in the serving DSN. Required when enabled unless existingSecret is set. Use a URL-safe value, from `openssl rand -hex 24`: it is embedded in a `postgres://` DSN, so `+`, `/` and `=` corrupt URL parsing | -| ui.defaultLocale | string | `"en"` | Fixed language for this deployment. Supported: "en", "zh". There is no in-UI language switcher; the whole console renders in this locale (default English), and the emails cp-api sends are written in it too. | +| ui.defaultLocale | string | `"en"` | Default language for this deployment. Supported: "en", "zh". It is the fallback the console and the emails cp-api sends use when none of the console's language picker, the organization's language setting or the browser's own language names a supported language (default English). | | ui.extraEnvVars | list | `[]` | Kept for installs upgraded from chart 1.5.0, which ran a separate dashboard. Of what that release was told to set here, AISIX_TRUSTED_ORIGINS and AISIX_AUTH_BEARER_SESSIONS are carried to cp-api (unless api.extraEnvVars sets the same name); nothing else is read. Set api.extraEnvVars instead. | | ui.service.nodePort | string | `""` | Optional fixed NodePort. Used only when type is NodePort; leave empty to let Kubernetes allocate a port dynamically. This plain-HTTP endpoint reaches cp-api exactly as the api Service does; terminate TLS in front of it unless it is on a trusted network. | | ui.service.port | int | `3000` | Port of the dashboard Service kept from chart 1.5.0; it forwards to cp-api | diff --git a/charts/aisix-cp/templates/_helpers.tpl b/charts/aisix-cp/templates/_helpers.tpl index a0309dd..a6720c8 100644 --- a/charts/aisix-cp/templates/_helpers.tpl +++ b/charts/aisix-cp/templates/_helpers.tpl @@ -336,3 +336,20 @@ those deployments set api.bootstrapAdmin.password or existingSecret. {{- end -}} {{- end -}} {{- end }} + +{{/* +Hash of the values in the Secrets this chart renders and the pods read at +boot (secret.yaml and external-db-secret.yaml), for the checksum/secret pod +annotation. Only `data` is hashed, so a chart version bump alone rolls +nothing. The +first administrator's password is left out: it is read once, when no +administrator exists yet, and a generated one differs on every render +where lookup answers nothing (helm template, GitOps), which would roll the +pods on every sync. +*/}} +{{- define "aisix-cp.secretChecksum" -}} +{{- $data := (include (print $.Template.BasePath "/secret.yaml") . | fromYaml).data | default dict -}} +{{- $_ := unset $data "bootstrap-admin-password" -}} +{{- $db := (include (print $.Template.BasePath "/external-db-secret.yaml") . | fromYaml).data | default dict -}} +{{- toYaml (list $data $db) | sha256sum -}} +{{- end }} diff --git a/charts/aisix-cp/templates/api-deployment.yaml b/charts/aisix-cp/templates/api-deployment.yaml index 41a8e31..0d43d02 100644 --- a/charts/aisix-cp/templates/api-deployment.yaml +++ b/charts/aisix-cp/templates/api-deployment.yaml @@ -16,7 +16,12 @@ spec: {{- include "aisix-cp.labels" . | nindent 8 }} {{- include "aisix-cp.selectorLabels" (dict "root" . "component" "api") | nindent 8 }} annotations: + # Roll the pods when the rendered config file or a value in a Secret + # this chart renders changes: both are read at boot only. A Secret + # supplied by name (existingSecret) or rendered by the PostgreSQL + # subchart is not hashed — restart the Deployment after editing it. checksum/config: {{ include (print $.Template.BasePath "/api-configmap.yaml") . | sha256sum }} + checksum/secret: {{ include "aisix-cp.secretChecksum" . }} spec: {{- include "aisix-cp.imagePullSecrets" . | nindent 6 }} serviceAccountName: {{ include "aisix-cp.serviceAccountName" . }} diff --git a/charts/aisix-cp/templates/dpm-deployment.yaml b/charts/aisix-cp/templates/dpm-deployment.yaml index e25b608..538cd99 100644 --- a/charts/aisix-cp/templates/dpm-deployment.yaml +++ b/charts/aisix-cp/templates/dpm-deployment.yaml @@ -16,7 +16,12 @@ spec: {{- include "aisix-cp.labels" . | nindent 8 }} {{- include "aisix-cp.selectorLabels" (dict "root" . "component" "dpm") | nindent 8 }} annotations: + # Roll the pods when the rendered config file or a value in a Secret + # this chart renders changes: both are read at boot only. A Secret + # supplied by name (existingSecret) or rendered by the PostgreSQL + # subchart is not hashed — restart the Deployment after editing it. checksum/config: {{ include (print $.Template.BasePath "/dpm-configmap.yaml") . | sha256sum }} + checksum/secret: {{ include "aisix-cp.secretChecksum" . }} spec: {{- include "aisix-cp.imagePullSecrets" . | nindent 6 }} serviceAccountName: {{ include "aisix-cp.serviceAccountName" . }} diff --git a/charts/aisix-cp/values.yaml b/charts/aisix-cp/values.yaml index 0a94c1d..b0d153f 100644 --- a/charts/aisix-cp/values.yaml +++ b/charts/aisix-cp/values.yaml @@ -159,7 +159,9 @@ api: # -- Gateway image the console hands out in the install snippets it # generates for a new data plane. Empty uses cp-api's own default: the # gateway of the same release, docker.io/api7/aisix:, or - # ghcr.io/api7/aisix:dev on a build that is not a release + # ghcr.io/api7/aisix:dev on a build that is not a release. The Helm + # snippet takes only its repository; the gateway chart's appVersion + # picks the tag dpImage: "" # -- How this control plane is deployed: `on-premises` or `saas`. # `on-premises` serves one company's organizations: nobody signs up on @@ -225,7 +227,7 @@ api: # an existing catalog, and refreshes every model a later sync returns pricesyncSnapshotPath: "" - ## The first administrator of an on-premises install. When cp-api starts + ## The first administrator of an AISIX Enterprise install. When cp-api starts ## on a database with no users yet, it creates the organization `Default` ## (rename it later in the organization settings) with this account as its ## owner, who must change the password at the first sign-in. It happens @@ -254,7 +256,8 @@ api: # -- Key inside bootstrapAdmin.existingSecret holding the password existingSecretKey: "bootstrap-admin-password" - ## Billing — SaaS only. On-premises (api.deploymentMode: on-premises) has + ## Billing — AISIX Cloud only. AISIX Enterprise (`api.deploymentMode: + ## on-premises`) has ## no billing, and the chart refuses to render with a provider other than ## `mock` or with usagePush on, as cp-api refuses to start. Every value ## here is left out of cp-api's config file while it is empty. @@ -288,7 +291,8 @@ api: stripeWebhookSecret: "stripe-webhook-secret" lagoAPIKey: "lago-api-key" - ## Organization creation — SaaS only; on-premises creates no organizations. + ## Self-service organization creation — AISIX Cloud only. AISIX Enterprise + ## bootstraps one `Default` organization and does not allow creating more. # @ignored organization: # -- Every new organization needs a one-time invite code, including a @@ -300,7 +304,7 @@ api: # organizations requireInviteCode: true - ## Organization domain verification — SaaS only. An organization owner + ## Organization domain verification — AISIX Cloud only. An organization owner ## proves the organization controls a domain by publishing a DNS TXT ## record; cp-api looks it up when the owner verifies and about once a ## day after that. @@ -443,9 +447,11 @@ dpm: ## The console. cp-api serves it from its own image, so there is no separate ## dashboard Deployment: the console is reached wherever the api Service is. ui: - # -- Fixed language for this deployment. Supported: "en", "zh". There is - # no in-UI language switcher; the whole console renders in this locale - # (default English), and the emails cp-api sends are written in it too. + # -- Default language for this deployment. Supported: "en", "zh". It is + # the fallback the console and the emails cp-api sends use when none of + # the console's language picker, the organization's language setting or + # the browser's own language names a supported language (default + # English). defaultLocale: "en" ## Chart 1.5.0 ran the dashboard behind its own Service, and a same-origin ## proxy may still send pages to it. The Service stays, under the same @@ -623,12 +629,12 @@ externalDatabase: ## cp-api provisions cp_api_app's LOGIN + password (from servingPassword) ## on the admin pool at boot — you do NOT pre-create the role. ## -## Leave DISABLED for single-tenant / private (On-Premises) deployments: there +## Leave DISABLED for AISIX Enterprise deployments: there ## every organization is one trust domain, RLS is inert, and the single ## privileged DSN handles everything. twoDSN: # -- Open a second, Row-Level-Security-enforced pool for tenant queries. - # Leave off for an On-Premises deployment + # Leave off for AISIX Enterprise enabled: false # -- Password cp-api assigns to the cp_api_app serving role and uses in the # serving DSN. Required when enabled unless existingSecret is set. From aab690740e9e793c4d80aa24796c249f49527a11 Mon Sep 17 00:00:00 2001 From: Jarvis Date: Tue, 20 Oct 2026 11:13:54 +0000 Subject: [PATCH 3/8] feat(aisix): declare upstream.media_fetch in values.yaml --- charts/aisix/README.md | 3 +++ charts/aisix/values.yaml | 12 ++++++++++++ 2 files changed, 15 insertions(+) diff --git a/charts/aisix/README.md b/charts/aisix/README.md index 8b84cc0..c07fc30 100644 --- a/charts/aisix/README.md +++ b/charts/aisix/README.md @@ -577,6 +577,9 @@ rejects. New installs set the value in its own key in `values.yaml` or under | config.ratelimit.redis | object | `nil` | Extra settings for the shared rate-limit Redis selected by `rateLimit.backend: redis`, e.g. `{timeout_secs: 2}` or `{mode: cluster, nodes: [...]}`. Same keys as `cache.redis`; the URL comes from `rateLimit.redis`, `username` and `password` go through `configSecrets` | | config.shutdown.min_drain_secs | int | `30` | Seconds the gateway keeps accepting after SIGTERM while `/readyz` reports 503. `terminationGracePeriodSeconds` must cover it | | config.upstream.connect_timeout_ms | int | `5000` | Connect timeout in milliseconds | +| config.upstream.media_fetch.allowed_hosts | list | `[]` | Hosts the download may reach although they resolve to a non-public address, as `host` or `host:port` (IPv6 with a port as `[addr]:port`). Every other non-public address is refused | +| config.upstream.media_fetch.enabled | bool | `true` | Download a chat request's media URL when the target upstream cannot read it (Bedrock, Claude on Vertex AI, http:// toward Anthropic or Gemini) and send the bytes inline. false refuses such parts with a 400 | +| config.upstream.media_fetch.max_bytes | int | `52428800` | Largest download accepted per media URL, in bytes | | config.upstream.pool_idle_timeout_secs | int | `30` | Seconds an idle pooled connection is kept. Lower it when a load balancer or NAT between the gateway and the provider closes idle connections sooner | | config.upstream.pool_max_idle_per_host | int | `nil` | Idle pooled connections kept per host. Null means unbounded | | config.upstream.retries | int | `2` | Retry budget every dispatch starts from | diff --git a/charts/aisix/values.yaml b/charts/aisix/values.yaml index 72f21de..e482e13 100644 --- a/charts/aisix/values.yaml +++ b/charts/aisix/values.yaml @@ -343,6 +343,18 @@ config: pool_max_idle_per_host: null # -- Retry budget every dispatch starts from retries: 2 + media_fetch: + # -- Download a chat request's media URL when the target upstream + # cannot read it (Bedrock, Claude on Vertex AI, http:// toward + # Anthropic or Gemini) and send the bytes inline. false refuses such + # parts with a 400 + enabled: true + # -- Hosts the download may reach although they resolve to a + # non-public address, as `host` or `host:port` (IPv6 with a port as + # `[addr]:port`). Every other non-public address is refused + allowed_hosts: [] + # -- Largest download accepted per media URL, in bytes + max_bytes: 52428800 tls: # -- Extra CA bundle, added to the platform trust store, for every # upstream TLS handshake. Mount the file through `extraVolumes` From 196f423903610dc44345415e91501c4c04079251 Mon Sep 17 00:00:00 2001 From: Jarvis Date: Tue, 20 Oct 2026 16:07:17 +0000 Subject: [PATCH 4/8] feat(aisix): declare upstream.bedrock.mantle_endpoint Pairs with api7/aisix#1318: the Bedrock Mantle endpoint count_tokens falls back to for Claude models bedrock-runtime cannot count. Null derives https://bedrock-mantle.{region}.api.aws from the provider key's region. --- charts/aisix/README.md | 1 + charts/aisix/values.yaml | 7 +++++++ 2 files changed, 8 insertions(+) diff --git a/charts/aisix/README.md b/charts/aisix/README.md index c07fc30..8423719 100644 --- a/charts/aisix/README.md +++ b/charts/aisix/README.md @@ -576,6 +576,7 @@ rejects. New installs set the value in its own key in `values.yaml` or under | config.ratelimit.concurrency_ttl_secs | int | `300` | Seconds a concurrency-limit slot is held at most | | config.ratelimit.redis | object | `nil` | Extra settings for the shared rate-limit Redis selected by `rateLimit.backend: redis`, e.g. `{timeout_secs: 2}` or `{mode: cluster, nodes: [...]}`. Same keys as `cache.redis`; the URL comes from `rateLimit.redis`, `username` and `password` go through `configSecrets` | | config.shutdown.min_drain_secs | int | `30` | Seconds the gateway keeps accepting after SIGTERM while `/readyz` reports 503. `terminationGracePeriodSeconds` must cover it | +| config.upstream.bedrock.mantle_endpoint | string | `nil` | Bedrock Mantle base URL where `/v1/messages/count_tokens` for a Claude model is counted when bedrock-runtime's CountTokens refuses that model. Null derives the public regional host `https://bedrock-mantle.{region}.api.aws` from the provider key's region; set it for a VPC endpoint or GovCloud | | config.upstream.connect_timeout_ms | int | `5000` | Connect timeout in milliseconds | | config.upstream.media_fetch.allowed_hosts | list | `[]` | Hosts the download may reach although they resolve to a non-public address, as `host` or `host:port` (IPv6 with a port as `[addr]:port`). Every other non-public address is refused | | config.upstream.media_fetch.enabled | bool | `true` | Download a chat request's media URL when the target upstream cannot read it (Bedrock, Claude on Vertex AI, http:// toward Anthropic or Gemini) and send the bytes inline. false refuses such parts with a 400 | diff --git a/charts/aisix/values.yaml b/charts/aisix/values.yaml index e482e13..2656bb1 100644 --- a/charts/aisix/values.yaml +++ b/charts/aisix/values.yaml @@ -355,6 +355,13 @@ config: allowed_hosts: [] # -- Largest download accepted per media URL, in bytes max_bytes: 52428800 + bedrock: + # -- (string) Bedrock Mantle base URL where `/v1/messages/count_tokens` + # for a Claude model is counted when bedrock-runtime's CountTokens + # refuses that model. Null derives the public regional host + # `https://bedrock-mantle.{region}.api.aws` from the provider key's + # region; set it for a VPC endpoint or GovCloud + mantle_endpoint: null tls: # -- Extra CA bundle, added to the platform trust store, for every # upstream TLS handshake. Mount the file through `extraVolumes` From 3fe20a7c0cb01972f8e38bd0341b1a4c8e49e2d7 Mon Sep 17 00:00:00 2001 From: Jarvis Date: Wed, 21 Oct 2026 03:00:45 +0000 Subject: [PATCH 5/8] feat(aisix): declare outbound.ssrf_protection, drop media_fetch.allowed_hosts The gateway moved the media download's allow-list to a product-wide outbound.ssrf_protection block (enabled, allowed_hosts) in api7/aisix#1327. --- charts/aisix/README.md | 3 ++- charts/aisix/values.yaml | 15 +++++++++++---- 2 files changed, 13 insertions(+), 5 deletions(-) diff --git a/charts/aisix/README.md b/charts/aisix/README.md index 8423719..ebbcfa6 100644 --- a/charts/aisix/README.md +++ b/charts/aisix/README.md @@ -565,6 +565,8 @@ rejects. New installs set the value in its own key in `values.yaml` or under | config.observability.metrics.prometheus.path | string | `"/metrics"` | Metrics path | | config.observability.service_name | string | `"aisix"` | Service name reported in telemetry | | config.observability.usage_event.request_headers | list | `[]` | Request headers whose values every usage event records (lowercase names; credential headers and duplicates are refused at startup) | +| config.outbound.ssrf_protection.allowed_hosts | list | `[]` | Hosts such a fetch may reach although they resolve to a non-public address, as `host` or `host:port` (IPv6 with a port as `[addr]:port`). Behind a fake-ip DNS that maps public names into 198.18.0.0/15, list those hosts here | +| config.outbound.ssrf_protection.enabled | bool | `true` | Only reach publicly routable addresses when fetching a URL an API caller supplied (today the media download). false turns the check off and ignores `allowed_hosts` | | config.proxy.real_ip.header | string | `"x-forwarded-for"` | Header carrying the client IP | | config.proxy.real_ip.recursive | bool | `false` | Walk the header right to left past every trusted proxy | | config.proxy.real_ip.trusted_proxies | list | `[]` | CIDRs (or bare IPs) of proxies trusted to set the client-IP header | @@ -578,7 +580,6 @@ rejects. New installs set the value in its own key in `values.yaml` or under | config.shutdown.min_drain_secs | int | `30` | Seconds the gateway keeps accepting after SIGTERM while `/readyz` reports 503. `terminationGracePeriodSeconds` must cover it | | config.upstream.bedrock.mantle_endpoint | string | `nil` | Bedrock Mantle base URL where `/v1/messages/count_tokens` for a Claude model is counted when bedrock-runtime's CountTokens refuses that model. Null derives the public regional host `https://bedrock-mantle.{region}.api.aws` from the provider key's region; set it for a VPC endpoint or GovCloud | | config.upstream.connect_timeout_ms | int | `5000` | Connect timeout in milliseconds | -| config.upstream.media_fetch.allowed_hosts | list | `[]` | Hosts the download may reach although they resolve to a non-public address, as `host` or `host:port` (IPv6 with a port as `[addr]:port`). Every other non-public address is refused | | config.upstream.media_fetch.enabled | bool | `true` | Download a chat request's media URL when the target upstream cannot read it (Bedrock, Claude on Vertex AI, http:// toward Anthropic or Gemini) and send the bytes inline. false refuses such parts with a 400 | | config.upstream.media_fetch.max_bytes | int | `52428800` | Largest download accepted per media URL, in bytes | | config.upstream.pool_idle_timeout_secs | int | `30` | Seconds an idle pooled connection is kept. Lower it when a load balancer or NAT between the gateway and the provider closes idle connections sooner | diff --git a/charts/aisix/values.yaml b/charts/aisix/values.yaml index 2656bb1..78ceeac 100644 --- a/charts/aisix/values.yaml +++ b/charts/aisix/values.yaml @@ -349,10 +349,6 @@ config: # Anthropic or Gemini) and send the bytes inline. false refuses such # parts with a 400 enabled: true - # -- Hosts the download may reach although they resolve to a - # non-public address, as `host` or `host:port` (IPv6 with a port as - # `[addr]:port`). Every other non-public address is refused - allowed_hosts: [] # -- Largest download accepted per media URL, in bytes max_bytes: 52428800 bedrock: @@ -373,6 +369,17 @@ config: # -- Verify upstream certificates. false accepts any certificate — test # use only verify: true + outbound: + ssrf_protection: + # -- Only reach publicly routable addresses when fetching a URL an + # API caller supplied (today the media download). false turns the + # check off and ignores `allowed_hosts` + enabled: true + # -- Hosts such a fetch may reach although they resolve to a + # non-public address, as `host` or `host:port` (IPv6 with a port as + # `[addr]:port`). Behind a fake-ip DNS that maps public names into + # 198.18.0.0/15, list those hosts here + allowed_hosts: [] downstream: # -- Seconds an idle client connection is held. 0 never closes it idle_timeout_secs: 0 From b0625669c2e0bd2653a6fdb99724b7738ea7f8df Mon Sep 17 00:00:00 2001 From: Copilot <198982749+Copilot@users.noreply.github.com> Date: Wed, 21 Oct 2026 23:53:15 +0200 Subject: [PATCH 6/8] aisix: support Deployment metadata annotations --- charts/aisix/README.md | 1 + charts/aisix/templates/deployment.yaml | 4 ++++ charts/aisix/values.yaml | 2 ++ 3 files changed, 7 insertions(+) diff --git a/charts/aisix/README.md b/charts/aisix/README.md index 8b84cc0..325b13a 100644 --- a/charts/aisix/README.md +++ b/charts/aisix/README.md @@ -531,6 +531,7 @@ rejects. New installs set the value in its own key in `values.yaml` or under | admin.service.annotations | object | `{}` | Extra annotations for the admin Service | | admin.service.port | int | `3001` | Admin Service port | | affinity | object | `{}` | Affinity rules for the gateway pods | +| annotations | object | `{}` | Extra annotations for the Deployment | | autoscaling.behavior | object | `{}` | `spec.behavior` for the HPA. Empty uses the Kubernetes defaults (immediate scale-up, 5-minute scale-down stabilization). A gateway that carries long streaming responses usually wants a gentler scale-down, e.g. `scaleDown: {policies: [{type: Pods, value: 1, periodSeconds: 60}]}` | | autoscaling.enabled | bool | `false` | Create a HorizontalPodAutoscaler for the gateway Deployment | | autoscaling.extraMetrics | list | `[]` | Extra `spec.metrics` entries appended verbatim — Pods / Object / External metrics such as a Prometheus adapter series | diff --git a/charts/aisix/templates/deployment.yaml b/charts/aisix/templates/deployment.yaml index cf40fd1..003e07a 100644 --- a/charts/aisix/templates/deployment.yaml +++ b/charts/aisix/templates/deployment.yaml @@ -2,6 +2,10 @@ apiVersion: apps/v1 kind: Deployment metadata: name: {{ include "aisix.fullname" . }} + {{- with .Values.annotations }} + annotations: + {{- toYaml . | nindent 4 }} + {{- end }} labels: {{- include "aisix.labels" . | nindent 4 }} {{- include "aisix.selectorLabels" . | nindent 4 }} diff --git a/charts/aisix/values.yaml b/charts/aisix/values.yaml index 72f21de..9dcb258 100644 --- a/charts/aisix/values.yaml +++ b/charts/aisix/values.yaml @@ -588,6 +588,8 @@ extraVolumes: [] # -- Extra volume mounts for the gateway container extraVolumeMounts: [] +# -- Extra annotations for the Deployment +annotations: {} # -- Annotations for the gateway pods podAnnotations: {} # -- Labels for the gateway pods From 20b96dac4525286d07753138b3423995b9c36eb2 Mon Sep 17 00:00:00 2001 From: Jarvis Date: Thu, 22 Oct 2026 02:28:18 +0000 Subject: [PATCH 7/8] chore(aisix-cp): sync from AISIX-Cloud v1.7.0-rc.11 cp-api's per-site private-address switches (api.playgroundAllowPrivateIPs, notifyAllowPrivateURLs, mcpSpecAllowPrivateURLs, guardrailTestConnectionAllowInsecure) are replaced by one api.outbound.ssrfProtection block (enabled, allowedHosts). A release that still carries one of the old values renders it so cp-api warns that it is ignored. --- charts/aisix-cp/README.md | 8 ++--- charts/aisix-cp/templates/api-configmap.yaml | 26 ++++++++------ charts/aisix-cp/values.yaml | 36 ++++++++++---------- 3 files changed, 37 insertions(+), 33 deletions(-) diff --git a/charts/aisix-cp/README.md b/charts/aisix-cp/README.md index d48b6bb..52a16cf 100644 --- a/charts/aisix-cp/README.md +++ b/charts/aisix-cp/README.md @@ -407,15 +407,13 @@ plane and hands out the gateway image from the same release as the chart. | api.bootstrapAdmin.existingSecretKey | string | `"bootstrap-admin-password"` | Key inside bootstrapAdmin.existingSecret holding the password | | api.bootstrapAdmin.password | string | `""` | Initial password (12 to 128 characters), stored in the chart's Secret. Ignored when existingSecret is set. Empty generates one (see above) | | api.corsAllowedOrigins | list | `[]` | Browser origins allowed to call cp-api cross-origin, as bare origins (scheme://host[:port]). Empty — the default — writes no CORS headers at all, which is what a normal install wants: cp-api serves the API and proxies the dashboard, so the browser has only one origin and nothing is ever cross-origin. Populate it only to let a dashboard served from somewhere else reach this API directly, such as a frontend-only PR preview. An entry is a bare origin (https://host[:port]), or `https://*` plus a suffix naming at least three labels (`https://*-api7ai.vercel.app`) — a PR preview's host carries the branch name, so an exact list would mean redeploying cp-api for every pull request. Read the suffix as collision-resistance, NOT as a guarantee: it is a byte suffix, and on a shared host like `.vercel.app` another account can claim a name ending the same way, so a grant that needs a real boundary belongs on a domain you control. Every entry must be spelled the way a BROWSER serializes an origin, and the chart refuses one that is not, naming the spelling to use — a bare `*`, a path, query, fragment or userinfo, a trailing dot, a non-https origin (loopback may use http), an internationalized host given as anything but punycode, a numeric last label that is not a dotted quad, an IPv6 address in any spelling but the one it is sent in, the scheme's default port written out, a port with a leading zero, and a port above 65535. cp-api applies the same rules at startup, so a value this chart accepts is one it will start with. | -| api.deploymentMode | string | `"on-premises"` | How this control plane is deployed: `on-premises` or `saas`. `on-premises` serves one company's organizations: nobody signs up on their own (accounts come from invitations), organizations cannot be created, billing is off, and cp-api's outbound calls may reach private addresses unless an outbound value below says otherwise. `saas` is the multi-tenant hosted service — open sign-up, organization creation and billing — and requires twoDSN.enabled | +| api.deploymentMode | string | `"on-premises"` | How this control plane is deployed: `on-premises` or `saas`. `on-premises` serves one company's organizations: nobody signs up on their own (accounts come from invitations), organizations cannot be created, billing is off, and cp-api's outbound calls may reach private addresses unless api.outbound.ssrfProtection says otherwise. `saas` is the multi-tenant hosted service — open sign-up, organization creation and billing — and requires twoDSN.enabled | | api.dpImage | string | `""` | Gateway image the console hands out in the install snippets it generates for a new data plane. Empty uses cp-api's own default: the gateway of the same release, docker.io/api7/aisix:, or ghcr.io/api7/aisix:dev on a build that is not a release. The Helm snippet takes only its repository; the gateway chart's appVersion picks the tag | | api.dpmgrBaseURL | string | `""` | dp-manager /dp/* mTLS endpoint that data-plane hosts dial, as baked into the generated install snippets. Also passed to the dpm deployment, which seeds this host into the TLS server certificate it presents — so an IP address works here, not only a DNS name. When dpm.service.type is NodePort, use https://:. | | api.extraEnvVars | list | `[]` | Extra environment for cp-api: system-level settings only (for example TZ). Every cp-api setting has its own value in this section; an env var set here for one of them still overrides the rendered config file, which is how values set this way before those keys existed keep working. Overriding a name the chart itself sets is only a compatibility path for existing deployments. For a name chart 1.5.0 already set, the chart keeps its own entry ahead of yours, so the name appears twice and Helm 4's server-side apply rejects the install. New installs set the value in this file instead. | -| api.guardrailTestConnectionAllowInsecure | bool | `nil` | Whether the guardrail "test connection" probe may reach plain http, private addresses and non-Azure hosts (an internal or Private Link endpoint). Unset follows deploymentMode: allowed on-premises, blocked on saas | | api.image.pullPolicy | string | `"IfNotPresent"` | Image pull policy | | api.image.repository | string | `"docker.io/api7/aisix-cp-api"` | cp-api image repository | | api.image.tag | string | `""` | Image tag | -| api.mcpSpecAllowPrivateURLs | bool | `nil` | Whether an MCP server's OpenAPI spec_url may be fetched from a private / internal address. Unset follows deploymentMode: allowed on-premises, blocked on saas, where pasting the spec content still works | | api.metrics.enabled | bool | `true` | Serve Prometheus metrics from cp-api. On by default, matching the gateway chart so both planes are scraped the same way; the port is reachable only inside the cluster (ClusterIP). cp-api binds nothing unless this sets the address, so turning it off leaves the port genuinely unbound inside the pod rather than merely unexposed by the Service. | | api.metrics.port | int | `9090` | Port the metrics listener binds inside the container | | api.metrics.service.annotations | object | `{}` | Extra annotations for the metrics Service, e.g. scrape hints for a Prometheus that discovers by annotation rather than ServiceMonitor | @@ -428,8 +426,8 @@ plane and hands out the gateway image from the same release as the chart. | api.metrics.serviceMonitor.relabelings | list | `[]` | Scrape-time relabeling rules | | api.metrics.serviceMonitor.scrapeTimeout | string | `""` | Scrape timeout. Empty leaves the Prometheus default | | api.nodeSelector | object | `{}` | Node selector for the cp-api pod | -| api.notifyAllowPrivateURLs | bool | `nil` | Whether notification channels (budget alert webhooks / Slack) may deliver to private / internal addresses. Unset follows deploymentMode: allowed on-premises, blocked on saas | -| api.playgroundAllowPrivateIPs | bool | `nil` | Whether the dashboard playground may call LLM endpoints on private / internal addresses. Unset follows deploymentMode: allowed on-premises, blocked on saas (SSRF guard). Set true or false to override | +| api.outbound.ssrfProtection.allowedHosts | list | `[]` | Hosts exempt from SSRF protection, each `host`, `host:port` or `[v6addr]:port` (no port matches any port). cp-api refuses to start on an entry it cannot parse | +| api.outbound.ssrfProtection.enabled | bool | `nil` | Whether SSRF protection is on. Unset follows deploymentMode: off on-premises, on for saas. Set true or false to override | | api.podSecurityContext.runAsNonRoot | bool | `true` | Refuse to run the cp-api pod as root | | api.podSecurityContext.seccompProfile.type | string | `"RuntimeDefault"` | Seccomp profile for the cp-api pod | | api.pricesyncSnapshotPath | string | `""` | Offline pricing: seed the catalog once at startup from this models.dev api.json capture (plain or .gz) and never fetch. The cp-api image ships one at /etc/aisix/models-dev-snapshot.json.gz. Empty selects online sync, which loads that bundled snapshot when a sync fails while the catalog is still empty (an install without internet access), keeps an existing catalog, and refreshes every model a later sync returns | diff --git a/charts/aisix-cp/templates/api-configmap.yaml b/charts/aisix-cp/templates/api-configmap.yaml index ad0eca7..1307b5f 100644 --- a/charts/aisix-cp/templates/api-configmap.yaml +++ b/charts/aisix-cp/templates/api-configmap.yaml @@ -93,21 +93,27 @@ data: pricesync: url: {{ .Values.api.pricesyncURL | default "" | quote }} snapshot_path: {{ .Values.api.pricesyncSnapshotPath | default "" | quote }} - {{- /* Unset values stay out of the file, which is what lets - deployment.mode decide them. */}} - {{- $outbound := dict + {{- /* An unset enabled stays out of the file, which is what lets + deployment.mode decide it. dig: --reuse-values from an older + chart carries no api.outbound. */}} + {{- $ssrf := dig "outbound" "ssrfProtection" (dict) .Values.api }} + {{- $ssrfSet := dict }} + {{- if not (kindIs "invalid" $ssrf.enabled) }}{{ $_ := set $ssrfSet "enabled" $ssrf.enabled }}{{ end }} + {{- with $ssrf.allowedHosts }}{{ $_ := set $ssrfSet "allowed_hosts" . }}{{ end }} + {{- $outbound := dict }} + {{- if $ssrfSet }}{{ $_ := set $outbound "ssrf_protection" $ssrfSet }}{{ end }} + {{- /* The per-site switches ssrf_protection replaced. A release that still + carries one (--reuse-values from chart 1.5.0 carries two as false) + renders it, so cp-api warns that it is ignored. */}} + {{- range $key, $v := dict "notify_allow_private_urls" .Values.api.notifyAllowPrivateURLs "playground_allow_private_ips" .Values.api.playgroundAllowPrivateIPs "mcp_spec_allow_private_urls" .Values.api.mcpSpecAllowPrivateURLs "guardrail_test_connection_allow_insecure" .Values.api.guardrailTestConnectionAllowInsecure }} - {{- $set := dict }} - {{- range $k, $v := $outbound }} - {{- if not (kindIs "invalid" $v) }} - {{- $_ := set $set $k $v }} + {{- if not (kindIs "invalid" $v) }}{{ $_ := set $outbound $key $v }}{{ end }} {{- end }} - {{- end }} - {{- if $set }} - outbound: {{ $set | toJson }} + {{- if $outbound }} + outbound: {{ $outbound | toJson }} {{- end }} {{- $billingSet := dict }} {{- with $billingProvider }}{{ $_ := set $billingSet "provider" . }}{{ end }} diff --git a/charts/aisix-cp/values.yaml b/charts/aisix-cp/values.yaml index b0d153f..b98d07f 100644 --- a/charts/aisix-cp/values.yaml +++ b/charts/aisix-cp/values.yaml @@ -167,27 +167,27 @@ api: # `on-premises` serves one company's organizations: nobody signs up on # their own (accounts come from invitations), organizations cannot be # created, billing is off, and cp-api's outbound calls may reach private - # addresses unless an outbound value below says otherwise. `saas` is the + # addresses unless api.outbound.ssrfProtection says otherwise. `saas` is the # multi-tenant hosted service — open sign-up, organization creation and # billing — and requires twoDSN.enabled deploymentMode: "on-premises" - # -- (bool) Whether the dashboard playground may call LLM endpoints on - # private / internal addresses. Unset follows deploymentMode: allowed - # on-premises, blocked on saas (SSRF guard). Set true or false to override - playgroundAllowPrivateIPs: null - # -- (bool) Whether notification channels (budget alert webhooks / Slack) - # may deliver to private / internal addresses. Unset follows - # deploymentMode: allowed on-premises, blocked on saas - notifyAllowPrivateURLs: null - # -- (bool) Whether an MCP server's OpenAPI spec_url may be fetched from a - # private / internal address. Unset follows deploymentMode: allowed - # on-premises, blocked on saas, where pasting the spec content still works - mcpSpecAllowPrivateURLs: null - # -- (bool) Whether the guardrail "test connection" probe may reach plain - # http, private addresses and non-Azure hosts (an internal or Private Link - # endpoint). Unset follows deploymentMode: allowed on-premises, blocked on - # saas - guardrailTestConnectionAllowInsecure: null + outbound: + # SSRF protection for cp-api's own outbound calls to operator- or + # tenant-supplied URLs: the dashboard playground and its semantic + # helpers, notification channel deliveries, an MCP server's OpenAPI + # spec_url fetch, and the guardrail "test connection" probe. On, they may + # not reach private / internal addresses, the connection goes to the + # addresses checked, and the test-connection probe also requires an https + # endpoint on an official Azure domain. These calls honour HTTPS_PROXY / + # HTTP_PROXY / NO_PROXY. + ssrfProtection: + # -- (bool) Whether SSRF protection is on. Unset follows deploymentMode: + # off on-premises, on for saas. Set true or false to override + enabled: null + # -- Hosts exempt from SSRF protection, each `host`, `host:port` or + # `[v6addr]:port` (no port matches any port). cp-api refuses to start on + # an entry it cannot parse + allowedHosts: [] # -- Let cp-api migrate a database last run by a release older than this one # supports upgrading from. cp-api refuses such an upgrade at startup and # names the release to pass through first; set this only after reading that From c625670fd67c9cbc12ee029a6257c9702a8b26cd Mon Sep 17 00:00:00 2001 From: Jarvis Date: Thu, 22 Oct 2026 02:31:19 +0000 Subject: [PATCH 8/8] release(aisix): 1.7.0 Both charts move to version and appVersion 1.7.0. --- charts/aisix-cp/Chart.yaml | 4 ++-- charts/aisix-cp/README.md | 10 +++++----- charts/aisix/Chart.yaml | 4 ++-- charts/aisix/README.md | 2 +- 4 files changed, 10 insertions(+), 10 deletions(-) diff --git a/charts/aisix-cp/Chart.yaml b/charts/aisix-cp/Chart.yaml index 8721dec..75aef71 100644 --- a/charts/aisix-cp/Chart.yaml +++ b/charts/aisix-cp/Chart.yaml @@ -2,8 +2,8 @@ apiVersion: v2 name: aisix-cp description: Helm chart for AISIX control plane (cp-api, dp-manager, dashboard) type: application -version: 1.6.0 -appVersion: "1.6.0" +version: 1.7.0 +appVersion: "1.7.0" maintainers: - name: API7 diff --git a/charts/aisix-cp/README.md b/charts/aisix-cp/README.md index 52a16cf..b74e381 100644 --- a/charts/aisix-cp/README.md +++ b/charts/aisix-cp/README.md @@ -1,6 +1,6 @@ # aisix-cp -![Version: 1.6.0](https://img.shields.io/badge/Version-1.6.0-informational?style=flat-square) ![Type: application](https://img.shields.io/badge/Type-application-informational?style=flat-square) ![AppVersion: 1.6.0](https://img.shields.io/badge/AppVersion-1.6.0-informational?style=flat-square) +![Version: 1.7.0](https://img.shields.io/badge/Version-1.7.0-informational?style=flat-square) ![Type: application](https://img.shields.io/badge/Type-application-informational?style=flat-square) ![AppVersion: 1.7.0](https://img.shields.io/badge/AppVersion-1.7.0-informational?style=flat-square) Helm chart for AISIX control plane (cp-api, dp-manager, dashboard) @@ -102,7 +102,7 @@ helm repo add api7 https://charts.api7.ai helm repo update helm install aisix-cp api7/aisix-cp --namespace aisix --create-namespace \ - --version 1.6.0 \ + --version 1.7.0 \ -f cp-values.yaml ``` @@ -111,7 +111,7 @@ same install, as long as you record what you generated: ```sh helm install aisix-cp api7/aisix-cp --namespace aisix --create-namespace \ - --version 1.6.0 \ + --version 1.7.0 \ --set secrets.masterKey="$(openssl rand -base64 32)" \ --set postgresql.auth.postgresPassword="$(openssl rand -hex 24)" \ --set postgresql.auth.password="$(openssl rand -hex 24)" @@ -181,7 +181,7 @@ fails the handshake with nothing logged to say why. The console's generated commands carry a gateway image, taken from `api.dpImage`. Left empty — the default — `cp-api` hands out the gateway of its -own release, so this release hands out `docker.io/api7/aisix:1.6.0`. +own release, so this release hands out `docker.io/api7/aisix:1.7.0`. Set it only to pin a different image, and remember that a value set explicitly carries forward across upgrades, so gateways added later come up on the old image. @@ -343,7 +343,7 @@ install time: ```sh helm install aisix-cp api7/aisix-cp --namespace aisix --create-namespace \ - --version 1.6.0 \ + --version 1.7.0 \ -f cp-values.yaml \ --set postgresql.primary.podSecurityContext.enabled=false \ --set postgresql.primary.containerSecurityContext.enabled=false diff --git a/charts/aisix/Chart.yaml b/charts/aisix/Chart.yaml index 0a7ec0d..28f60ce 100644 --- a/charts/aisix/Chart.yaml +++ b/charts/aisix/Chart.yaml @@ -2,8 +2,8 @@ apiVersion: v2 name: aisix description: Helm chart for the AISIX AI gateway data plane type: application -version: 1.6.0 -appVersion: "1.6.0" +version: 1.7.0 +appVersion: "1.7.0" keywords: - ai-gateway diff --git a/charts/aisix/README.md b/charts/aisix/README.md index 7b546ce..5edcc8e 100644 --- a/charts/aisix/README.md +++ b/charts/aisix/README.md @@ -1,6 +1,6 @@ # aisix -![Version: 1.6.0](https://img.shields.io/badge/Version-1.6.0-informational?style=flat-square) ![Type: application](https://img.shields.io/badge/Type-application-informational?style=flat-square) ![AppVersion: 1.6.0](https://img.shields.io/badge/AppVersion-1.6.0-informational?style=flat-square) +![Version: 1.7.0](https://img.shields.io/badge/Version-1.7.0-informational?style=flat-square) ![Type: application](https://img.shields.io/badge/Type-application-informational?style=flat-square) ![AppVersion: 1.7.0](https://img.shields.io/badge/AppVersion-1.7.0-informational?style=flat-square) Helm chart for the AISIX AI gateway data plane