Skip to content

Commit

Permalink
feat: implement API Key Auth security policy
Browse files Browse the repository at this point in the history
Signed-off-by: Kensei Nakada <[email protected]>
  • Loading branch information
sanposhiho committed Jan 1, 2025
1 parent f71fa99 commit 999022c
Show file tree
Hide file tree
Showing 28 changed files with 1,333 additions and 12 deletions.
63 changes: 63 additions & 0 deletions api/v1alpha1/api_key_auth_types.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
// Copyright Envoy Gateway Authors
// SPDX-License-Identifier: Apache-2.0
// The full text of the Apache license is available in the LICENSE file at
// the root of the repo.

package v1alpha1

import (
gwapiv1 "sigs.k8s.io/gateway-api/apis/v1"
)

const APIKeysSecretKey = "credentials"

// APIKeyAuth defines the configuration for the API Key Authentication.
type APIKeyAuth struct {
// Credentials is the Kubernetes secret which contains the API keys.
// This is an Opaque secret.
// Each API key is stored in the key representing the client id,
// which can be used in AllowedClients to authorize the client in a simple way.
// If this is attached to a HTTPRoute while the other SecurityPolicies with APIKeyAuth is attached to a Gateway,
// this credentials takes precedence.
Credentials gwapiv1.SecretObjectReference `json:"credentials"`

// KeySources is where to fetch the key from the coming request.
// The value from the first source that has a key will be used.
// If this is attached to a HTTPRoute while the other SecurityPolicies with APIKeyAuth is attached to a Gateway,
// this key sources takes precedence.
KeySources []*KeySource `json:"keySources"`

// AllowedClients is a list of clients that are allowed to access the route or vhost.
// The clients listed here should be subset of the clients listed in the `Credentials` to provide authorization control
// after the authentication is successful. If the list is empty, then all authenticated clients
// are allowed. This provides very limited but simple authorization.
//
// +optional
AllowedClients []string `json:"allowedClients,omitempty"`
}

// KeySource is where to fetch the key from the coming request.
// Only one of header, query or cookie is supposed to be specified.
//
// Note: we intentionally don't add the validation for the only one of header, query or cookie is supposed to be specified with +kubebuilder:validation:XValidation:rule.
// Instead, we add the validation in the controller reconciliation.
// Technically we can define CEL, but the CEL estimated cost exceeds the threshold and it wouldn't be accepted.
//
// +kubebuilder:validation:XValidation:rule="(has(self.header) || has(self.query) || has(self.cookie))",message="one of header, query or cookie must be specified"
type KeySource struct {
// Header is the name of the header to fetch the key from.
// This field is optional, but only one of header, query or cookie is supposed to be specified.
//
// +optional
Header *string `json:"header,omitempty"`
// Query is the name of the query parameter to fetch the key from.
// This field is optional, but only one of header, query or cookie is supposed to be specified.
//
// +optional
Query *string `json:"query,omitempty"`
// Cookie is the name of the cookie to fetch the key from.
// This field is optional, but only one of header, query or cookie is supposed to be specified.
//
// +optional
Cookie *string `json:"cookie,omitempty"`
}
4 changes: 4 additions & 0 deletions api/v1alpha1/envoyproxy_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -203,6 +203,10 @@ const (
// EnvoyFilterExtAuthz defines the Envoy HTTP external authorization filter.
EnvoyFilterExtAuthz EnvoyFilter = "envoy.filters.http.ext_authz"

// EnvoyFilterAPIKeyAuth defines the Envoy HTTP api key authentication filter.
//nolint:gosec // this is not an API key credential.
EnvoyFilterAPIKeyAuth EnvoyFilter = "envoy.filters.http.api_key_auth"

// EnvoyFilterBasicAuth defines the Envoy HTTP basic authentication filter.
EnvoyFilterBasicAuth EnvoyFilter = "envoy.filters.http.basic_auth"

Expand Down
5 changes: 5 additions & 0 deletions api/v1alpha1/securitypolicy_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,11 @@ type SecurityPolicy struct {
type SecurityPolicySpec struct {
PolicyTargetReferences `json:",inline"`

// APIKeyAuth defines the configuration for the API Key Authentication.
//
// +optional
APIKeyAuth *APIKeyAuth `json:"apiKeyAuth,omitempty"`

// CORS defines the configuration for Cross-Origin Resource Sharing (CORS).
//
// +optional
Expand Down
19 changes: 19 additions & 0 deletions api/v1alpha1/validation/securitypolicy_validate.go
Original file line number Diff line number Diff line change
Expand Up @@ -57,9 +57,28 @@ func validateSecurityPolicySpec(spec *egv1a1.SecurityPolicySpec) error {
errs = append(errs, err)
}

if err := ValidateAPIKeyAuth(spec.APIKeyAuth); err != nil {
errs = append(errs, err)
}

return utilerrors.NewAggregate(errs)
}

func ValidateAPIKeyAuth(p *egv1a1.APIKeyAuth) error {
if p == nil {
return nil
}

for _, keySource := range p.KeySources {
if (keySource.Header != nil && keySource.Query != nil) ||
(keySource.Header != nil && keySource.Cookie != nil) ||
(keySource.Query != nil && keySource.Cookie != nil) {
return errors.New("only one of header, query or cookie is supposed to be specified")
}
}
return nil
}

// ValidateJWTProvider validates the provided JWT authentication configuration.
func ValidateJWTProvider(providers []egv1a1.JWTProvider) error {
var errs []error
Expand Down
26 changes: 26 additions & 0 deletions api/v1alpha1/validation/securitypolicy_validate_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -491,6 +491,32 @@ func TestValidateSecurityPolicy(t *testing.T) {
},
expected: true,
},
{
name: "only one of header, query or cookie is supposed to be specified",
policy: &egv1a1.SecurityPolicy{
TypeMeta: metav1.TypeMeta{
Kind: egv1a1.KindSecurityPolicy,
APIVersion: egv1a1.GroupVersion.String(),
},
ObjectMeta: metav1.ObjectMeta{
Namespace: "test",
Name: "test",
},
Spec: egv1a1.SecurityPolicySpec{
JWT: &egv1a1.JWT{
Providers: []egv1a1.JWTProvider{
{
Name: "test",
Issuer: "https://www.test.local",
RemoteJWKS: egv1a1.RemoteJWKS{
URI: "https://test.local/jwt/public-key/jwks.json",
},
},
},
},
},
},
},
}

for i := range testCases {
Expand Down
67 changes: 67 additions & 0 deletions api/v1alpha1/zz_generated.deepcopy.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,104 @@ spec:
spec:
description: Spec defines the desired state of SecurityPolicy.
properties:
apiKeyAuth:
description: APIKeyAuth defines the configuration for the API Key
Authentication.
properties:
allowedClients:
description: |-
AllowedClients is a list of clients that are allowed to access the route or vhost.
The clients listed here should be subset of the clients listed in the `Credentials` to provide authorization control
after the authentication is successful. If the list is empty, then all authenticated clients
are allowed. This provides very limited but simple authorization.
items:
type: string
type: array
credentials:
description: |-
Credentials is the Kubernetes secret which contains the API keys.
This is an Opaque secret.
Each API key is stored in the key representing the client id,
which can be used in AllowedClients to authorize the client in a simple way.
If this is attached to a HTTPRoute while the other SecurityPolicies with APIKeyAuth is attached to a Gateway,
this credentials takes precedence.
This field is optional, but it doesn't make sense that any attached APIKeyAuth for the route/gateway doesn't have credentials at all.
properties:
group:
default: ""
description: |-
Group is the group of the referent. For example, "gateway.networking.k8s.io".
When unspecified or empty string, core API group is inferred.
maxLength: 253
pattern: ^$|^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$
type: string
kind:
default: Secret
description: Kind is kind of the referent. For example "Secret".
maxLength: 63
minLength: 1
pattern: ^[a-zA-Z]([-a-zA-Z0-9]*[a-zA-Z0-9])?$
type: string
name:
description: Name is the name of the referent.
maxLength: 253
minLength: 1
type: string
namespace:
description: |-
Namespace is the namespace of the referenced object. When unspecified, the local
namespace is inferred.
Note that when a namespace different than the local namespace is specified,
a ReferenceGrant object is required in the referent namespace to allow that
namespace's owner to accept the reference. See the ReferenceGrant
documentation for details.
Support: Core
maxLength: 63
minLength: 1
pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?$
type: string
required:
- name
type: object
keySources:
description: |-
KeySources is where to fetch the key from the coming request.
The value from the first source that has a key will be used.
If this is attached to a HTTPRoute while the other SecurityPolicies with APIKeyAuth is attached to a Gateway,
this key sources takes precedence.
This field is optional, but it doesn't make sense that any attached APIKeyAuth for the route/gateway doesn't have key sources at all.
items:
description: |-
KeySource is where to fetch the key from the coming request.
Only one of header, query or cookie is supposed to be specified.
Note: we intentionally don't add the validation for the only one of header, query or cookie is supposed to be specified with +kubebuilder:validation:XValidation:rule.
Instead, we add the validation in the webhook.
Technically we can define CEL, but the CEL estimated cost exceeds the threshold and it wouldn't be accepted.
properties:
cookie:
description: |-
Cookie is the name of the cookie to fetch the key from.
This field is optional, but only one of header, query or cookie is supposed to be specified.
type: string
header:
description: |-
Header is the name of the header to fetch the key from.
This field is optional, but only one of header, query or cookie is supposed to be specified.
type: string
query:
description: |-
Query is the name of the query parameter to fetch the key from.
This field is optional, but only one of header, query or cookie is supposed to be specified.
type: string
type: object
x-kubernetes-validations:
- message: one of header, query or cookie must be specified
rule: (has(self.header) || has(self.query) || has(self.cookie))
type: array
type: object
authorization:
description: Authorization defines the authorization configuration.
properties:
Expand Down
6 changes: 4 additions & 2 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,10 @@ require (
github.com/docker/cli v27.4.1+incompatible
github.com/docker/docker v27.4.1+incompatible
github.com/dominikbraun/graph v0.23.0
github.com/envoyproxy/go-control-plane v0.13.1
github.com/envoyproxy/go-control-plane v0.13.3-0.20241223225832-3bbb1657744f
github.com/envoyproxy/go-control-plane/contrib v1.32.2
github.com/envoyproxy/go-control-plane/envoy v1.32.2
github.com/envoyproxy/go-control-plane/ratelimit v0.1.0
github.com/envoyproxy/ratelimit v1.4.1-0.20230427142404-e2a87f41d3a7
github.com/evanphx/json-patch v5.9.0+incompatible
github.com/evanphx/json-patch/v5 v5.9.0
Expand Down Expand Up @@ -101,7 +104,6 @@ require (
github.com/beorn7/perks v1.0.1 // indirect
github.com/blang/semver/v4 v4.0.0 // indirect
github.com/c9s/goprocinfo v0.0.0-20170724085704-0010a05ce49f // indirect
github.com/census-instrumentation/opencensus-proto v0.4.1 // indirect
github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/chai2010/gettext-go v1.0.2 // indirect
github.com/cilium/ebpf v0.16.0 // indirect
Expand Down
12 changes: 8 additions & 4 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -112,8 +112,6 @@ github.com/c9s/goprocinfo v0.0.0-20170724085704-0010a05ce49f/go.mod h1:uEyr4WpAH
github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8=
github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE=
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
github.com/census-instrumentation/opencensus-proto v0.4.1 h1:iKLQ0xPNFxR/2hzXZMrBo8f1j86j5WHzznCCQxV/b8g=
github.com/census-instrumentation/opencensus-proto v0.4.1/go.mod h1:4T9NM4+4Vw91VeyqjLS6ao50K5bOcLKN6Q42XnYaRYw=
github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc=
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
Expand Down Expand Up @@ -223,8 +221,14 @@ github.com/emicklei/go-restful/v3 v3.12.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRr
github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=
github.com/envoyproxy/go-control-plane v0.13.1 h1:vPfJZCkob6yTMEgS+0TwfTUfbHjfy/6vOJ8hUWX/uXE=
github.com/envoyproxy/go-control-plane v0.13.1/go.mod h1:X45hY0mufo6Fd0KW3rqsGvQMw58jvjymeCzBU3mWyHw=
github.com/envoyproxy/go-control-plane v0.13.3-0.20241223225832-3bbb1657744f h1:MSDvWi3WoCXCIkvTNt+ZNZHgq+VBd4VYlSARDXKKPJY=
github.com/envoyproxy/go-control-plane v0.13.3-0.20241223225832-3bbb1657744f/go.mod h1:mcYj6+AKxG86c/jKeZsCIWv8oLzhR+SJynG0TB94Xw8=
github.com/envoyproxy/go-control-plane/contrib v1.32.2 h1:zt3NQQpUn9ZiS1Yl4W9n4+p3rrTCHzR5RHJWwk98Mg0=
github.com/envoyproxy/go-control-plane/contrib v1.32.2/go.mod h1:36lHpshtTe1qSBKm21yhJyMkGDiQvk91IWr4Fdj4GcQ=
github.com/envoyproxy/go-control-plane/envoy v1.32.2 h1:zidqwmijfcbyKqVxjQDFx042PgX+p9U+/fu/f9VtSk8=
github.com/envoyproxy/go-control-plane/envoy v1.32.2/go.mod h1:eR2SOX2IedqlPvmiKjUH7Wu//S602JKI7HPC/L3SRq8=
github.com/envoyproxy/go-control-plane/ratelimit v0.1.0 h1:/G9QYbddjL25KvtKTv3an9lx6VBE2cnb8wp1vEGNYGI=
github.com/envoyproxy/go-control-plane/ratelimit v0.1.0/go.mod h1:Wk+tMFAFbCXaJPzVVHnPgRKdUdwW/KdbRt94AzgRee4=
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
github.com/envoyproxy/protoc-gen-validate v1.1.0 h1:tntQDh69XqOCOZsDz0lVJQez/2L6Uu2PdjCQwWCJ3bM=
github.com/envoyproxy/protoc-gen-validate v1.1.0/go.mod h1:sXRDRVmzEbkM7CVcM06s9shE/m23dg3wzjl0UWqJ2q4=
Expand Down
Loading

0 comments on commit 999022c

Please sign in to comment.