This is an automated email from the ASF dual-hosted git repository.
AlinsRan pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/apisix-ingress-controller.git
The following commit(s) were added to refs/heads/master by this push:
new e89f0f24 fix: relax jwtAuth private_key requirement and add CEL
validation (#2759)
e89f0f24 is described below
commit e89f0f24ec4f6086b4539feb12a786eca6e079f3
Author: AlinsRan <[email protected]>
AuthorDate: Sat May 9 12:45:32 2026 +0800
fix: relax jwtAuth private_key requirement and add CEL validation (#2759)
---
api/adc/plugin_types.go | 2 +-
api/v2/apisixconsumer_types.go | 9 +-
api/v2/apisixconsumer_validation_test.go | 337 +++++++++++++++++++++
.../bases/apisix.apache.org_apisixconsumers.yaml | 11 +-
docs/en/latest/reference/api-reference.md | 5 +-
5 files changed, 358 insertions(+), 6 deletions(-)
diff --git a/api/adc/plugin_types.go b/api/adc/plugin_types.go
index 6d230886..ee68a746 100644
--- a/api/adc/plugin_types.go
+++ b/api/adc/plugin_types.go
@@ -68,7 +68,7 @@ type JwtAuthConsumerConfig struct {
Key string `json:"key" yaml:"key"`
Secret string `json:"secret,omitempty"
yaml:"secret,omitempty"`
PublicKey string `json:"public_key,omitempty"
yaml:"public_key,omitempty"`
- PrivateKey string `json:"private_key"
yaml:"private_key,omitempty"`
+ PrivateKey string `json:"private_key,omitempty"
yaml:"private_key,omitempty"`
Algorithm string `json:"algorithm,omitempty"
yaml:"algorithm,omitempty"`
Exp int64 `json:"exp,omitempty" yaml:"exp,omitempty"`
Base64Secret bool `json:"base64_secret,omitempty"
yaml:"base64_secret,omitempty"`
diff --git a/api/v2/apisixconsumer_types.go b/api/v2/apisixconsumer_types.go
index c9e4afba..7a05f9ca 100644
--- a/api/v2/apisixconsumer_types.go
+++ b/api/v2/apisixconsumer_types.go
@@ -130,6 +130,11 @@ type ApisixConsumerJwtAuth struct {
}
// ApisixConsumerJwtAuthValue defines configuration for JWT authentication.
+// For asymmetric algorithms (RS*, ES*, PS*, EdDSA), at least one of public_key
+// or private_key must be provided. Symmetric algorithms (HS256, HS384, HS512)
+// and unset algorithm do not require any key field.
+//
+// +kubebuilder:validation:XValidation:rule="!has(self.algorithm) ||
size(self.algorithm) == 0 || self.algorithm in ['HS256','HS384','HS512'] ||
(has(self.public_key) && size(self.public_key.trim()) > 0) ||
(has(self.private_key) && size(self.private_key.trim()) >
0)",message="algorithms other than HS256/HS384/HS512 require at least one
non-empty public_key or private_key"
type ApisixConsumerJwtAuthValue struct {
// Key is the unique identifier for the JWT credential.
Key string `json:"key" yaml:"key"`
@@ -138,9 +143,9 @@ type ApisixConsumerJwtAuthValue struct {
// PublicKey is the public key used to verify JWT signatures (for
asymmetric algorithms).
PublicKey string `json:"public_key,omitempty"
yaml:"public_key,omitempty"`
// PrivateKey is the private key used to sign the JWT (for asymmetric
algorithms).
- PrivateKey string `json:"private_key" yaml:"private_key,omitempty"`
+ PrivateKey string `json:"private_key,omitempty"
yaml:"private_key,omitempty"`
// Algorithm specifies the signing algorithm.
- // Can be `HS256`, `HS512`, `RS256`, or `ES256`.
+ // Can be `HS256`, `HS384`, `HS512`, `RS256`, `RS384`, `RS512`,
`ES256`, `ES384`, `ES512`, `PS256`, `PS384`, `PS512`, or `EdDSA`.
Algorithm string `json:"algorithm,omitempty" yaml:"algorithm,omitempty"`
// Exp is the token expiration period in seconds.
Exp int64 `json:"exp,omitempty" yaml:"exp,omitempty"`
diff --git a/api/v2/apisixconsumer_validation_test.go
b/api/v2/apisixconsumer_validation_test.go
new file mode 100644
index 00000000..88fdd1d6
--- /dev/null
+++ b/api/v2/apisixconsumer_validation_test.go
@@ -0,0 +1,337 @@
+// Licensed to the Apache Software Foundation (ASF) under one or more
+// contributor license agreements. See the NOTICE file distributed with
+// this work for additional information regarding copyright ownership.
+// The ASF licenses this file to You under the Apache License, Version 2.0
+// (the "License"); you may not use this file except in compliance with
+// the License. You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package v2_test
+
+import (
+ "context"
+ "encoding/json"
+ "os"
+ "path/filepath"
+ "runtime"
+ "testing"
+
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+ apiextensions "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions"
+ apiextensionsv1
"k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
+ structuralschema "k8s.io/apiextensions-apiserver/pkg/apiserver/schema"
+ "k8s.io/apiextensions-apiserver/pkg/apiserver/schema/cel"
+ "k8s.io/apiextensions-apiserver/pkg/apiserver/validation"
+ celconfig "k8s.io/apiserver/pkg/apis/cel"
+ sigsyaml "sigs.k8s.io/yaml"
+
+ apisixv2 "github.com/apache/apisix-ingress-controller/api/v2"
+)
+
+// consumerSchemaValidator holds the parsed CRD schema for ApisixConsumer
+// and provides a Validate method for use in tests.
+type consumerSchemaValidator struct {
+ structural *structuralschema.Structural
+ internal *apiextensions.JSONSchemaProps
+}
+
+func (v *consumerSchemaValidator) Validate(t *testing.T, ac
*apisixv2.ApisixConsumer) error {
+ t.Helper()
+
+ data, err := json.Marshal(ac)
+ require.NoError(t, err, "failed to marshal ApisixConsumer")
+
+ var obj map[string]interface{}
+ require.NoError(t, json.Unmarshal(data, &obj), "failed to unmarshal to
map")
+
+ schemaValidator, _, err := validation.NewSchemaValidator(v.internal)
+ require.NoError(t, err, "failed to build schema validator")
+
+ if errs := validation.ValidateCustomResource(nil, obj,
schemaValidator); len(errs) > 0 {
+ return errs.ToAggregate()
+ }
+
+ celValidator := cel.NewValidator(v.structural, false,
celconfig.PerCallLimit)
+ celErrs, _ := celValidator.Validate(context.Background(), nil,
v.structural, obj, nil, celconfig.RuntimeCELCostBudget)
+ if len(celErrs) > 0 {
+ return celErrs.ToAggregate()
+ }
+ return nil
+}
+
+// loadApisixConsumerSchema reads the ApisixConsumer CRD YAML and returns a
+// validator backed by the real generated schema.
+func loadApisixConsumerSchema(t *testing.T) *consumerSchemaValidator {
+ t.Helper()
+
+ _, thisFile, _, _ := runtime.Caller(0)
+ crdPath := filepath.Join(filepath.Dir(thisFile), "..", "..",
+ "config", "crd", "bases",
"apisix.apache.org_apisixconsumers.yaml")
+
+ data, err := os.ReadFile(crdPath)
+ require.NoError(t, err, "failed to read CRD file: %s", crdPath)
+
+ jsonData, err := sigsyaml.YAMLToJSON(data)
+ require.NoError(t, err, "failed to convert CRD YAML to JSON")
+
+ var crd apiextensionsv1.CustomResourceDefinition
+ require.NoError(t, json.Unmarshal(jsonData, &crd), "failed to unmarshal
CRD")
+
+ var v1Schema *apiextensionsv1.JSONSchemaProps
+ for _, v := range crd.Spec.Versions {
+ if v.Name == "v2" {
+ v1Schema = v.Schema.OpenAPIV3Schema
+ break
+ }
+ }
+ require.NotNil(t, v1Schema, "v2 schema not found in CRD")
+
+ var internal apiextensions.JSONSchemaProps
+ require.NoError(t,
+
apiextensionsv1.Convert_v1_JSONSchemaProps_To_apiextensions_JSONSchemaProps(v1Schema,
&internal, nil),
+ "failed to convert v1 schema to internal",
+ )
+
+ structural, err := structuralschema.NewStructural(&internal)
+ require.NoError(t, err, "failed to build structural schema")
+ return &consumerSchemaValidator{structural: structural, internal:
&internal}
+}
+
+func TestApisixConsumer_JwtAuth_SymmetricHS256(t *testing.T) {
+ v := loadApisixConsumerSchema(t)
+ ac := &apisixv2.ApisixConsumer{
+ Spec: apisixv2.ApisixConsumerSpec{
+ AuthParameter: apisixv2.ApisixConsumerAuthParameter{
+ JwtAuth: &apisixv2.ApisixConsumerJwtAuth{
+ Value:
&apisixv2.ApisixConsumerJwtAuthValue{
+ Key: "my-key",
+ Secret: "my-secret",
+ Algorithm: "HS256",
+ },
+ },
+ },
+ },
+ }
+ assert.NoError(t, v.Validate(t, ac))
+}
+
+// TestApisixConsumer_JwtAuth_AsymmetricWithWhitespaceOnlyPublicKey verifies
+// that a whitespace-only public_key is treated as absent and rejected for
+// asymmetric algorithms.
+func TestApisixConsumer_JwtAuth_AsymmetricWithWhitespaceOnlyPublicKey(t
*testing.T) {
+ v := loadApisixConsumerSchema(t)
+ ac := &apisixv2.ApisixConsumer{
+ Spec: apisixv2.ApisixConsumerSpec{
+ AuthParameter: apisixv2.ApisixConsumerAuthParameter{
+ JwtAuth: &apisixv2.ApisixConsumerJwtAuth{
+ Value:
&apisixv2.ApisixConsumerJwtAuthValue{
+ Key: "my-key",
+ Algorithm: "RS256",
+ PublicKey: " ",
+ },
+ },
+ },
+ },
+ }
+ err := v.Validate(t, ac)
+ require.Error(t, err)
+ assert.Contains(t, err.Error(), "algorithms other than
HS256/HS384/HS512")
+}
+
+func TestApisixConsumer_JwtAuth_SymmetricHS512(t *testing.T) {
+ v := loadApisixConsumerSchema(t)
+ ac := &apisixv2.ApisixConsumer{
+ Spec: apisixv2.ApisixConsumerSpec{
+ AuthParameter: apisixv2.ApisixConsumerAuthParameter{
+ JwtAuth: &apisixv2.ApisixConsumerJwtAuth{
+ Value:
&apisixv2.ApisixConsumerJwtAuthValue{
+ Key: "my-key",
+ Secret: "my-secret",
+ Algorithm: "HS512",
+ },
+ },
+ },
+ },
+ }
+ assert.NoError(t, v.Validate(t, ac))
+}
+
+func TestApisixConsumer_JwtAuth_NoAlgorithmDefaultsToSymmetric(t *testing.T) {
+ v := loadApisixConsumerSchema(t)
+ ac := &apisixv2.ApisixConsumer{
+ Spec: apisixv2.ApisixConsumerSpec{
+ AuthParameter: apisixv2.ApisixConsumerAuthParameter{
+ JwtAuth: &apisixv2.ApisixConsumerJwtAuth{
+ Value:
&apisixv2.ApisixConsumerJwtAuthValue{
+ Key: "my-key",
+ Secret: "my-secret",
+ },
+ },
+ },
+ },
+ }
+ assert.NoError(t, v.Validate(t, ac))
+}
+
+func TestApisixConsumer_JwtAuth_AsymmetricRS256WithPublicKey(t *testing.T) {
+ v := loadApisixConsumerSchema(t)
+ ac := &apisixv2.ApisixConsumer{
+ Spec: apisixv2.ApisixConsumerSpec{
+ AuthParameter: apisixv2.ApisixConsumerAuthParameter{
+ JwtAuth: &apisixv2.ApisixConsumerJwtAuth{
+ Value:
&apisixv2.ApisixConsumerJwtAuthValue{
+ Key: "my-key",
+ PublicKey: "test-public-key",
+ Algorithm: "RS256",
+ },
+ },
+ },
+ },
+ }
+ assert.NoError(t, v.Validate(t, ac))
+}
+
+func TestApisixConsumer_JwtAuth_AsymmetricRS256WithPrivateKey(t *testing.T) {
+ v := loadApisixConsumerSchema(t)
+ ac := &apisixv2.ApisixConsumer{
+ Spec: apisixv2.ApisixConsumerSpec{
+ AuthParameter: apisixv2.ApisixConsumerAuthParameter{
+ JwtAuth: &apisixv2.ApisixConsumerJwtAuth{
+ Value:
&apisixv2.ApisixConsumerJwtAuthValue{
+ Key: "my-key",
+ PrivateKey: "test-private-key",
+ Algorithm: "RS256",
+ },
+ },
+ },
+ },
+ }
+ assert.NoError(t, v.Validate(t, ac))
+}
+
+func TestApisixConsumer_JwtAuth_AsymmetricRS256WithBothKeys(t *testing.T) {
+ v := loadApisixConsumerSchema(t)
+ ac := &apisixv2.ApisixConsumer{
+ Spec: apisixv2.ApisixConsumerSpec{
+ AuthParameter: apisixv2.ApisixConsumerAuthParameter{
+ JwtAuth: &apisixv2.ApisixConsumerJwtAuth{
+ Value:
&apisixv2.ApisixConsumerJwtAuthValue{
+ Key: "my-key",
+ PublicKey: "test-public-key",
+ PrivateKey: "test-private-key",
+ Algorithm: "RS256",
+ },
+ },
+ },
+ },
+ }
+ assert.NoError(t, v.Validate(t, ac))
+}
+
+func TestApisixConsumer_JwtAuth_AsymmetricRS256WithoutAnyKey(t *testing.T) {
+ v := loadApisixConsumerSchema(t)
+ ac := &apisixv2.ApisixConsumer{
+ Spec: apisixv2.ApisixConsumerSpec{
+ AuthParameter: apisixv2.ApisixConsumerAuthParameter{
+ JwtAuth: &apisixv2.ApisixConsumerJwtAuth{
+ Value:
&apisixv2.ApisixConsumerJwtAuthValue{
+ Key: "my-key",
+ Algorithm: "RS256",
+ },
+ },
+ },
+ },
+ }
+ err := v.Validate(t, ac)
+ require.Error(t, err)
+ assert.Contains(t, err.Error(), "algorithms other than
HS256/HS384/HS512")
+}
+
+func TestApisixConsumer_JwtAuth_AsymmetricES256WithoutAnyKey(t *testing.T) {
+ v := loadApisixConsumerSchema(t)
+ ac := &apisixv2.ApisixConsumer{
+ Spec: apisixv2.ApisixConsumerSpec{
+ AuthParameter: apisixv2.ApisixConsumerAuthParameter{
+ JwtAuth: &apisixv2.ApisixConsumerJwtAuth{
+ Value:
&apisixv2.ApisixConsumerJwtAuthValue{
+ Key: "my-key",
+ Algorithm: "ES256",
+ },
+ },
+ },
+ },
+ }
+ err := v.Validate(t, ac)
+ require.Error(t, err)
+ assert.Contains(t, err.Error(), "algorithms other than
HS256/HS384/HS512")
+}
+
+func TestApisixConsumer_JwtAuth_AsymmetricEdDSAWithoutAnyKey(t *testing.T) {
+ v := loadApisixConsumerSchema(t)
+ ac := &apisixv2.ApisixConsumer{
+ Spec: apisixv2.ApisixConsumerSpec{
+ AuthParameter: apisixv2.ApisixConsumerAuthParameter{
+ JwtAuth: &apisixv2.ApisixConsumerJwtAuth{
+ Value:
&apisixv2.ApisixConsumerJwtAuthValue{
+ Key: "my-key",
+ Algorithm: "EdDSA",
+ },
+ },
+ },
+ },
+ }
+ err := v.Validate(t, ac)
+ require.Error(t, err)
+ assert.Contains(t, err.Error(), "algorithms other than
HS256/HS384/HS512")
+}
+
+func TestApisixConsumer_JwtAuth_AsymmetricWithEmptyPublicKey(t *testing.T) {
+ v := loadApisixConsumerSchema(t)
+ ac := &apisixv2.ApisixConsumer{
+ Spec: apisixv2.ApisixConsumerSpec{
+ AuthParameter: apisixv2.ApisixConsumerAuthParameter{
+ JwtAuth: &apisixv2.ApisixConsumerJwtAuth{
+ Value:
&apisixv2.ApisixConsumerJwtAuthValue{
+ Key: "my-key",
+ Algorithm: "RS256",
+ // PublicKey is empty string —
omitempty means it won't appear
+ // in the serialized JSON, same
effect as not set
+ },
+ },
+ },
+ },
+ }
+ err := v.Validate(t, ac)
+ require.Error(t, err)
+ assert.Contains(t, err.Error(), "algorithms other than
HS256/HS384/HS512")
+}
+
+// TestApisixConsumer_JwtAuth_EmptyAlgorithmTreatedAsSymmetric verifies that an
+// explicitly empty algorithm string is treated the same as an unset algorithm
+// (defaults to HS256) and does not require public_key or private_key.
+func TestApisixConsumer_JwtAuth_EmptyAlgorithmTreatedAsSymmetric(t *testing.T)
{
+ v := loadApisixConsumerSchema(t)
+ ac := &apisixv2.ApisixConsumer{
+ Spec: apisixv2.ApisixConsumerSpec{
+ AuthParameter: apisixv2.ApisixConsumerAuthParameter{
+ JwtAuth: &apisixv2.ApisixConsumerJwtAuth{
+ Value:
&apisixv2.ApisixConsumerJwtAuthValue{
+ Key: "my-key",
+ Secret: "my-secret",
+ // Algorithm is explicitly
empty string — should be treated as
+ // unset and not require
asymmetric keys.
+ },
+ },
+ },
+ },
+ }
+ assert.NoError(t, v.Validate(t, ac))
+}
diff --git a/config/crd/bases/apisix.apache.org_apisixconsumers.yaml
b/config/crd/bases/apisix.apache.org_apisixconsumers.yaml
index 9235ef27..db0ec861 100644
--- a/config/crd/bases/apisix.apache.org_apisixconsumers.yaml
+++ b/config/crd/bases/apisix.apache.org_apisixconsumers.yaml
@@ -176,7 +176,7 @@ spec:
algorithm:
description: |-
Algorithm specifies the signing algorithm.
- Can be `HS256`, `HS512`, `RS256`, or `ES256`.
+ Can be `HS256`, `HS384`, `HS512`, `RS256`,
`RS384`, `RS512`, `ES256`, `ES384`, `ES512`, `PS256`, `PS384`, `PS512`, or
`EdDSA`.
type: string
base64_secret:
description: Base64Secret indicates whether the
secret
@@ -209,8 +209,15 @@ spec:
type: string
required:
- key
- - private_key
type: object
+ x-kubernetes-validations:
+ - message: algorithms other than HS256/HS384/HS512
require
+ at least one non-empty public_key or private_key
+ rule: '!has(self.algorithm) || size(self.algorithm)
== 0
+ || self.algorithm in
[''HS256'',''HS384'',''HS512''] ||
+ (has(self.public_key) &&
size(self.public_key.trim())
+ > 0) || (has(self.private_key) &&
size(self.private_key.trim())
+ > 0)'
type: object
keyAuth:
description: KeyAuth configures the key authentication
details.
diff --git a/docs/en/latest/reference/api-reference.md
b/docs/en/latest/reference/api-reference.md
index f385468d..4ee8a633 100644
--- a/docs/en/latest/reference/api-reference.md
+++ b/docs/en/latest/reference/api-reference.md
@@ -781,6 +781,9 @@ _Appears in:_
ApisixConsumerJwtAuthValue defines configuration for JWT authentication.
+For asymmetric algorithms (RS*, ES*, PS*, EdDSA), at least one of public_key
+or private_key must be provided. Symmetric algorithms (HS256, HS384, HS512)
+and unset algorithm do not require any key field.
@@ -790,7 +793,7 @@ ApisixConsumerJwtAuthValue defines configuration for JWT
authentication.
| `secret` _string_ | Secret is the shared secret used to sign the JWT (for
symmetric algorithms). |
| `public_key` _string_ | PublicKey is the public key used to verify JWT
signatures (for asymmetric algorithms). |
| `private_key` _string_ | PrivateKey is the private key used to sign the JWT
(for asymmetric algorithms). |
-| `algorithm` _string_ | Algorithm specifies the signing algorithm. Can be
`HS256`, `HS512`, `RS256`, or `ES256`. |
+| `algorithm` _string_ | Algorithm specifies the signing algorithm. Can be
`HS256`, `HS384`, `HS512`, `RS256`, `RS384`, `RS512`, `ES256`, `ES384`,
`ES512`, `PS256`, `PS384`, `PS512`, or `EdDSA`. |
| `exp` _integer_ | Exp is the token expiration period in seconds. |
| `base64_secret` _boolean_ | Base64Secret indicates whether the secret is
base64-encoded. |
| `lifetime_grace_period` _integer_ | LifetimeGracePeriod is the allowed clock
skew in seconds for token expiration. |