This is an automated email from the ASF dual-hosted git repository. ashishtiwari 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 06981b18 fix: hmac-auth plugin spec compatibility with latest apisix (#2528) 06981b18 is described below commit 06981b18d3f089b14e4ead306d22a6f34d958fea Author: Ashish Tiwari <ashishjaitiwari15112...@gmail.com> AuthorDate: Thu Aug 28 14:37:32 2025 +0530 fix: hmac-auth plugin spec compatibility with latest apisix (#2528) --- api/adc/plugin_types.go | 7 +- api/v2/apisixconsumer_types.go | 21 +-- .../bases/apisix.apache.org_apisixconsumers.yaml | 26 +-- config/crd/kustomization.yaml | 7 +- config/crd/patches/hmac_auth_validation.yaml | 8 + docs/en/latest/reference/api-reference.md | 17 +- docs/en/latest/upgrade-guide.md | 4 + internal/adc/translator/apisixconsumer.go | 32 ++-- test/e2e/crds/v2/consumer.go | 177 +++++++++++++++++++++ 9 files changed, 259 insertions(+), 40 deletions(-) diff --git a/api/adc/plugin_types.go b/api/adc/plugin_types.go index a6e5ba06..1c2cd888 100644 --- a/api/adc/plugin_types.go +++ b/api/adc/plugin_types.go @@ -77,8 +77,11 @@ type JwtAuthConsumerConfig struct { // used in Consumer object. // +k8s:deepcopy-gen=true type HMACAuthConsumerConfig struct { - AccessKey string `json:"access_key" yaml:"access_key"` - SecretKey string `json:"secret_key" yaml:"secret_key"` + KeyID string `json:"key_id,omitempty" yaml:"key_id"` + SecretKey string `json:"secret_key" yaml:"secret_key"` + + // Deprecated + AccessKey string `json:"access_key,omitempty" yaml:"access_key"` Algorithm string `json:"algorithm,omitempty" yaml:"algorithm,omitempty"` ClockSkew int64 `json:"clock_skew,omitempty" yaml:"clock_skew,omitempty"` SignedHeaders []string `json:"signed_headers,omitempty" yaml:"signed_headers,omitempty"` diff --git a/api/v2/apisixconsumer_types.go b/api/v2/apisixconsumer_types.go index 89163c5b..c9e4afba 100644 --- a/api/v2/apisixconsumer_types.go +++ b/api/v2/apisixconsumer_types.go @@ -160,23 +160,26 @@ type ApisixConsumerHMACAuth struct { // ApisixConsumerHMACAuthValue defines configuration for HMAC authentication. type ApisixConsumerHMACAuthValue struct { - // AccessKey is the identifier used to look up the HMAC secret. - AccessKey string `json:"access_key" yaml:"access_key"` + // KeyID is the identifier used to look up the HMAC secret. + KeyID string `json:"key_id,omitempty" yaml:"key_id"` // SecretKey is the HMAC secret used to sign the request. SecretKey string `json:"secret_key" yaml:"secret_key"` - // Algorithm specifies the hashing algorithm (e.g., "hmac-sha256"). + + // AccessKey is the identifier used to look up the HMAC secret. Deprecated from consumer configuration + AccessKey string `json:"access_key,omitempty" yaml:"access_key"` + // Algorithm specifies the hashing algorithm (e.g., "hmac-sha256"). Deprecated from consumer configuration Algorithm string `json:"algorithm,omitempty" yaml:"algorithm,omitempty"` - // ClockSkew is the allowed time difference (in seconds) between client and server clocks. + // ClockSkew is the allowed time difference (in seconds) between client and server clocks. Deprecated from consumer configuration ClockSkew int64 `json:"clock_skew,omitempty" yaml:"clock_skew,omitempty"` - // SignedHeaders lists the headers that must be included in the signature. + // SignedHeaders lists the headers that must be included in the signature. Deprecated from consumer configuration SignedHeaders []string `json:"signed_headers,omitempty" yaml:"signed_headers,omitempty"` - // KeepHeaders determines whether the HMAC signature headers are preserved after verification. + // KeepHeaders determines whether the HMAC signature headers are preserved after verification. Deprecated from consumer configuration KeepHeaders bool `json:"keep_headers,omitempty" yaml:"keep_headers,omitempty"` - // EncodeURIParams indicates whether URI parameters are encoded when calculating the signature. + // EncodeURIParams indicates whether URI parameters are encoded when calculating the signature. Deprecated from consumer configuration EncodeURIParams bool `json:"encode_uri_params,omitempty" yaml:"encode_uri_params,omitempty"` - // ValidateRequestBody enables HMAC validation of the request body. + // ValidateRequestBody enables HMAC validation of the request body. Deprecated from consumer configuration ValidateRequestBody bool `json:"validate_request_body,omitempty" yaml:"validate_request_body,omitempty"` - // MaxReqBody sets the maximum size (in bytes) of the request body that can be validated. + // MaxReqBody sets the maximum size (in bytes) of the request body that can be validated. Deprecated from consumer configuration MaxReqBody int64 `json:"max_req_body,omitempty" yaml:"max_req_body,omitempty"` } diff --git a/config/crd/bases/apisix.apache.org_apisixconsumers.yaml b/config/crd/bases/apisix.apache.org_apisixconsumers.yaml index ed40241b..9235ef27 100644 --- a/config/crd/bases/apisix.apache.org_apisixconsumers.yaml +++ b/config/crd/bases/apisix.apache.org_apisixconsumers.yaml @@ -101,28 +101,36 @@ spec: properties: access_key: description: AccessKey is the identifier used to look - up the HMAC secret. + up the HMAC secret. Deprecated from consumer configuration type: string algorithm: description: Algorithm specifies the hashing algorithm - (e.g., "hmac-sha256"). + (e.g., "hmac-sha256"). Deprecated from consumer configuration type: string clock_skew: description: ClockSkew is the allowed time difference - (in seconds) between client and server clocks. + (in seconds) between client and server clocks. Deprecated + from consumer configuration format: int64 type: integer encode_uri_params: description: EncodeURIParams indicates whether URI parameters - are encoded when calculating the signature. + are encoded when calculating the signature. Deprecated + from consumer configuration type: boolean keep_headers: description: KeepHeaders determines whether the HMAC signature - headers are preserved after verification. + headers are preserved after verification. Deprecated + from consumer configuration type: boolean + key_id: + description: KeyID is the identifier used to look up the + HMAC secret. + type: string max_req_body: description: MaxReqBody sets the maximum size (in bytes) - of the request body that can be validated. + of the request body that can be validated. Deprecated + from consumer configuration format: int64 type: integer secret_key: @@ -131,16 +139,16 @@ spec: type: string signed_headers: description: SignedHeaders lists the headers that must - be included in the signature. + be included in the signature. Deprecated from consumer + configuration items: type: string type: array validate_request_body: description: ValidateRequestBody enables HMAC validation - of the request body. + of the request body. Deprecated from consumer configuration type: boolean required: - - access_key - secret_key type: object type: object diff --git a/config/crd/kustomization.yaml b/config/crd/kustomization.yaml index 31b55ab7..2f12a658 100644 --- a/config/crd/kustomization.yaml +++ b/config/crd/kustomization.yaml @@ -22,7 +22,12 @@ patches: name: consumers.apisix.apache.org group: apiextensions.k8s.io version: v1 - +- path: patches/hmac_auth_validation.yaml + target: + kind: CustomResourceDefinition + name: apisixconsumers.apisix.apache.org + group: apiextensions.k8s.io + version: v1 # [WEBHOOK] To enable webhook, uncomment all the sections with [WEBHOOK] prefix. # patches here are for enabling the conversion webhook for each CRD #- path: patches/webhook_in_gatewayproxies.yaml diff --git a/config/crd/patches/hmac_auth_validation.yaml b/config/crd/patches/hmac_auth_validation.yaml new file mode 100644 index 00000000..8beb752e --- /dev/null +++ b/config/crd/patches/hmac_auth_validation.yaml @@ -0,0 +1,8 @@ +- op: replace + path: /spec/versions/0/schema/openAPIV3Schema/properties/spec/properties/authParameter/properties/hmacAuth/properties/value/required + value: ["secret_key"] +- op: add + path: /spec/versions/0/schema/openAPIV3Schema/properties/spec/properties/authParameter/properties/hmacAuth/properties/value/oneOf + value: + - required: ["key_id", "secret_key"] + - required: ["access_key", "secret_key"] diff --git a/docs/en/latest/reference/api-reference.md b/docs/en/latest/reference/api-reference.md index bd360a84..914742fd 100644 --- a/docs/en/latest/reference/api-reference.md +++ b/docs/en/latest/reference/api-reference.md @@ -745,15 +745,16 @@ ApisixConsumerHMACAuthValue defines configuration for HMAC authentication. | Field | Description | | --- | --- | -| `access_key` _string_ | AccessKey is the identifier used to look up the HMAC secret. | +| `key_id` _string_ | KeyID is the identifier used to look up the HMAC secret. | | `secret_key` _string_ | SecretKey is the HMAC secret used to sign the request. | -| `algorithm` _string_ | Algorithm specifies the hashing algorithm (e.g., "hmac-sha256"). | -| `clock_skew` _integer_ | ClockSkew is the allowed time difference (in seconds) between client and server clocks. | -| `signed_headers` _string array_ | SignedHeaders lists the headers that must be included in the signature. | -| `keep_headers` _boolean_ | KeepHeaders determines whether the HMAC signature headers are preserved after verification. | -| `encode_uri_params` _boolean_ | EncodeURIParams indicates whether URI parameters are encoded when calculating the signature. | -| `validate_request_body` _boolean_ | ValidateRequestBody enables HMAC validation of the request body. | -| `max_req_body` _integer_ | MaxReqBody sets the maximum size (in bytes) of the request body that can be validated. | +| `access_key` _string_ | AccessKey is the identifier used to look up the HMAC secret. Deprecated from consumer configuration | +| `algorithm` _string_ | Algorithm specifies the hashing algorithm (e.g., "hmac-sha256"). Deprecated from consumer configuration | +| `clock_skew` _integer_ | ClockSkew is the allowed time difference (in seconds) between client and server clocks. Deprecated from consumer configuration | +| `signed_headers` _string array_ | SignedHeaders lists the headers that must be included in the signature. Deprecated from consumer configuration | +| `keep_headers` _boolean_ | KeepHeaders determines whether the HMAC signature headers are preserved after verification. Deprecated from consumer configuration | +| `encode_uri_params` _boolean_ | EncodeURIParams indicates whether URI parameters are encoded when calculating the signature. Deprecated from consumer configuration | +| `validate_request_body` _boolean_ | ValidateRequestBody enables HMAC validation of the request body. Deprecated from consumer configuration | +| `max_req_body` _integer_ | MaxReqBody sets the maximum size (in bytes) of the request body that can be validated. Deprecated from consumer configuration | _Appears in:_ diff --git a/docs/en/latest/upgrade-guide.md b/docs/en/latest/upgrade-guide.md index 6b3fa7f9..e892e789 100644 --- a/docs/en/latest/upgrade-guide.md +++ b/docs/en/latest/upgrade-guide.md @@ -150,6 +150,10 @@ More details: [ADC Backend Differences](https://github.com/api7/adc/blob/2449ca8 The `ApisixClusterConfig` CRD has been removed in 2.0.0. global rules and configurations should now be managed through the `ApisixGlobalRule` CRDs. +#### `ApisixConsumer` - `hmac-auth` + +In apisix >= 3.11, most of the hmac-auth related configuration has been deprecated from consumer and moved to service/route level. The name of a `required` field has also been changed from `access_key` to `key_id`. If you have ApisixConsumer configuration with hmac-auth plugin compatible with <3.11, they will not be compatible with newer versions of APISIX. Since all 3+ versions of apisix are supported by ingress controller, if you dont upgrade APISIX, you don't need to change your Apisi [...] + #### Ingress ##### API Version Support diff --git a/internal/adc/translator/apisixconsumer.go b/internal/adc/translator/apisixconsumer.go index cae838dc..406f1c2c 100644 --- a/internal/adc/translator/apisixconsumer.go +++ b/internal/adc/translator/apisixconsumer.go @@ -31,11 +31,14 @@ import ( ) var ( - _errKeyNotFoundOrInvalid = errors.New("key \"key\" not found or invalid in secret") _errUsernameNotFoundOrInvalid = errors.New("key \"username\" not found or invalid in secret") _errPasswordNotFoundOrInvalid = errors.New("key \"password\" not found or invalid in secret") ) +func errKeyNotFoundOrInvalid(key string) error { + return errors.New(fmt.Sprintf("key \"%s\" not found or invalid in secret", key)) +} + const ( _jwtAuthExpDefaultValue = 86400 @@ -114,7 +117,7 @@ func (t *Translator) translateConsumerKeyAuthPlugin(tctx *provider.TranslateCont } raw, ok := sec.Data["key"] if !ok || len(raw) == 0 { - return nil, _errKeyNotFoundOrInvalid + return nil, errKeyNotFoundOrInvalid("key") } return &adctypes.KeyAuthConsumerConfig{Key: string(raw)}, nil } @@ -200,7 +203,7 @@ func (t *Translator) translateConsumerJwtAuthPlugin(tctx *provider.TranslateCont } keyRaw, ok := sec.Data["key"] if !ok || len(keyRaw) == 0 { - return nil, _errKeyNotFoundOrInvalid + return nil, errKeyNotFoundOrInvalid("key") } base64SecretRaw := sec.Data["base64_secret"] var base64Secret bool @@ -244,6 +247,7 @@ func (t *Translator) translateConsumerHMACAuthPlugin(tctx *provider.TranslateCon EncodeURIParams: cfg.Value.EncodeURIParams, ValidateRequestBody: cfg.Value.ValidateRequestBody, MaxReqBody: cfg.Value.MaxReqBody, + KeyID: cfg.Value.KeyID, }, nil } @@ -254,15 +258,19 @@ func (t *Translator) translateConsumerHMACAuthPlugin(tctx *provider.TranslateCon if sec == nil { return nil, fmt.Errorf("secret %s/%s not found", consumerNamespace, cfg.SecretRef.Name) } - - accessKeyRaw, ok := sec.Data["access_key"] - if !ok || len(accessKeyRaw) == 0 { - return nil, _errKeyNotFoundOrInvalid + var accessKeyRaw []byte + keyIDRaw, ok := sec.Data["key_id"] + if !ok || len(keyIDRaw) == 0 { + // For backward compatibility with older versions + accessKeyRaw, ok = sec.Data["access_key"] + if !ok || len(accessKeyRaw) == 0 { + return nil, errKeyNotFoundOrInvalid("access_key/key_id") + } } secretKeyRaw, ok := sec.Data["secret_key"] if !ok || len(secretKeyRaw) == 0 { - return nil, _errKeyNotFoundOrInvalid + return nil, errKeyNotFoundOrInvalid("secret_key") } algorithmRaw, ok := sec.Data["algorithm"] @@ -326,10 +334,12 @@ func (t *Translator) translateConsumerHMACAuthPlugin(tctx *provider.TranslateCon if maxReqBody < 0 { maxReqBody = _hmacAuthMaxReqBodyDefaultValue } - return &adctypes.HMACAuthConsumerConfig{ + KeyID: string(keyIDRaw), + SecretKey: string(secretKeyRaw), + + // Deprecated fields supported for backwards compatibility AccessKey: string(accessKeyRaw), - SecretKey: string(secretKeyRaw), Algorithm: algorithm, ClockSkew: clockSkew, SignedHeaders: signedHeaders, @@ -356,7 +366,7 @@ func (t *Translator) translateConsumerLDAPAuthPlugin(tctx *provider.TranslateCon } userDNRaw, ok := sec.Data["user_dn"] if !ok || len(userDNRaw) == 0 { - return nil, _errKeyNotFoundOrInvalid + return nil, errKeyNotFoundOrInvalid("user_dn") } return &adctypes.LDAPAuthConsumerConfig{ diff --git a/test/e2e/crds/v2/consumer.go b/test/e2e/crds/v2/consumer.go index 0169d20b..a670c71a 100644 --- a/test/e2e/crds/v2/consumer.go +++ b/test/e2e/crds/v2/consumer.go @@ -18,6 +18,9 @@ package v2 import ( + "crypto/hmac" + "crypto/sha256" + "encoding/base64" "fmt" "net/http" "time" @@ -33,6 +36,28 @@ import ( type Headers map[string]string +func generateHMACHeaders(keyID, secretKey, method, path string) map[string]string { + gmtTime := time.Now().UTC().Format("Mon, 02 Jan 2006 15:04:05 GMT") + signingString := fmt.Sprintf("%s\n%s %s\ndate: %s\n", keyID, method, path, gmtTime) + + // Create HMAC signature + mac := hmac.New(sha256.New, []byte(secretKey)) + mac.Write([]byte(signingString)) + signature := mac.Sum(nil) + signatureBase64 := base64.StdEncoding.EncodeToString(signature) + + // Construct Authorization header + authHeader := fmt.Sprintf( + `Signature keyId="%s",algorithm="hmac-sha256",headers="@request-target date",signature="%s"`, + keyID, signatureBase64, + ) + + return map[string]string{ + "Date": gmtTime, + "Authorization": authHeader, + } +} + var _ = Describe("Test ApisixConsumer", Label("apisix.apache.org", "v2", "apisixconsumer"), func() { var ( s = scaffold.NewDefaultScaffold() @@ -409,4 +434,156 @@ spec: }) }) }) + + Context("Test HMACAuth", func() { + const ( + hmacAuthConsumer = ` +apiVersion: apisix.apache.org/v2 +kind: ApisixConsumer +metadata: + name: hmac-consumer +spec: + ingressClassName: %s + authParameter: + hmacAuth: + value: + key_id: papa + secret_key: fatpa +` + + hmacAuthConsumerInvalid = ` +apiVersion: apisix.apache.org/v2 +kind: ApisixConsumer +metadata: + name: hmac-consumer +spec: + ingressClassName: %s + authParameter: + hmacAuth: + value: + secret_key: fatpa +` + hmacRoute = ` +apiVersion: apisix.apache.org/v2 +kind: ApisixRoute +metadata: + name: hmac-route +spec: + ingressClassName: %s + http: + - name: rule1 + match: + hosts: + - httpbin.org + paths: + - /ip + backends: + - serviceName: httpbin-service-e2e-test + servicePort: 80 + authentication: + enable: true + type: hmacAuth +` + hmacSecret = ` +apiVersion: v1 +kind: Secret +metadata: + name: hmac +data: + key_id: cGFwYQ== + secret_key: ZmF0cGE= +` + hmacAuthWithSecret = ` +apiVersion: apisix.apache.org/v2 +kind: ApisixConsumer +metadata: + name: hmac-consumer +spec: + ingressClassName: %s + authParameter: + hmacAuth: + secretRef: + name: hmac +` + ) + + It("Basic tests", func() { + By("apply ApisixRoute") + applier.MustApplyAPIv2(types.NamespacedName{Namespace: s.Namespace(), Name: "hmac-route"}, + &apiv2.ApisixRoute{}, fmt.Sprintf(hmacRoute, s.Namespace())) + + By("apply Invalid ApisixConsumer with missing required field") + err := s.CreateResourceFromString(hmacAuthConsumerInvalid) + Expect(err).Should(HaveOccurred(), "creating invalid ApisixConsumer") + + By("apply ApisixConsumer") + applier.MustApplyAPIv2(types.NamespacedName{Namespace: s.Namespace(), Name: "hmac-consumer"}, + &apiv2.ApisixConsumer{}, fmt.Sprintf(hmacAuthConsumer, s.Namespace())) + + By("verify ApisixRoute with ApisixConsumer") + // Generate HMAC headers dynamically + hmacHeaders := generateHMACHeaders("papa", "fatpa", "GET", "/ip") + + // Test valid HMAC authentication + s.RequestAssert(&scaffold.RequestAssert{ + Method: "GET", + Path: "/ip", + Host: "httpbin.org", + Headers: hmacHeaders, + Check: scaffold.WithExpectedStatus(http.StatusOK), + }) + + // Test missing authorization + s.RequestAssert(&scaffold.RequestAssert{ + Method: "GET", + Path: "/ip", + Host: "httpbin.org", + Check: scaffold.WithExpectedStatus(http.StatusUnauthorized), + }) + + By("Delete resources") + err = s.DeleteResource("ApisixConsumer", "hmac-consumer") + Expect(err).ShouldNot(HaveOccurred(), "deleting ApisixConsumer") + + err = s.DeleteResource("ApisixRoute", "hmac-route") + Expect(err).ShouldNot(HaveOccurred(), "deleting ApisixRoute") + }) + + It("SecretRef tests", func() { + By("apply ApisixRoute") + applier.MustApplyAPIv2(types.NamespacedName{Namespace: s.Namespace(), Name: "hmac-route"}, + &apiv2.ApisixRoute{}, fmt.Sprintf(hmacRoute, s.Namespace())) + + By("apply Secret") + err := s.CreateResourceFromString(hmacSecret) + Expect(err).ShouldNot(HaveOccurred(), "creating Secret for ApisixConsumer") + + By("apply ApisixConsumer") + applier.MustApplyAPIv2(types.NamespacedName{Namespace: s.Namespace(), Name: "hmac-consumer"}, + &apiv2.ApisixConsumer{}, fmt.Sprintf(hmacAuthWithSecret, s.Namespace())) + + By("verify ApisixRoute with ApisixConsumer") + // Generate HMAC headers dynamically + hmacHeaders := generateHMACHeaders("papa", "fatpa", "GET", "/ip") + + // Test valid HMAC authentication + s.RequestAssert(&scaffold.RequestAssert{ + Method: "GET", + Path: "/ip", + Host: "httpbin.org", + Headers: hmacHeaders, + Check: scaffold.WithExpectedStatus(http.StatusOK), + }) + + By("Delete resources") + err = s.DeleteResource("ApisixConsumer", "hmac-consumer") + Expect(err).ShouldNot(HaveOccurred(), "deleting ApisixConsumer") + + err = s.DeleteResource("ApisixRoute", "hmac-route") + Expect(err).ShouldNot(HaveOccurred(), "deleting ApisixRoute") + + err = s.DeleteResource("Secret", "hmac") + Expect(err).ShouldNot(HaveOccurred(), "deleting Secret") + }) + }) })