diff --git a/.golangci.yml b/.golangci.yml
index 3141302de..1c0e40d80 100644
--- a/.golangci.yml
+++ b/.golangci.yml
@@ -44,10 +44,12 @@ linters:
alias: metav1
- pkg: k8s.io/api/apps/v1
alias: appsv1
- - pkg: k8s.io/api/autoscaling/v2"
+ - pkg: k8s.io/api/autoscaling/v2
alias: autoscalingv2
- pkg: k8s.io/apimachinery/pkg/api/errors
alias: k8serrors
+ - pkg: k8s.io/api/networking/v1
+ alias: networkingv1
formatters:
enable:
- gofmt
diff --git a/api/operator/v1beta1/vmauth_types.go b/api/operator/v1beta1/vmauth_types.go
index f2239f8bc..27002d907 100644
--- a/api/operator/v1beta1/vmauth_types.go
+++ b/api/operator/v1beta1/vmauth_types.go
@@ -7,7 +7,8 @@ import (
"regexp"
"strings"
- v12 "k8s.io/api/networking/v1"
+ corev1 "k8s.io/api/core/v1"
+ networkingv1 "k8s.io/api/networking/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/labels"
"k8s.io/utils/ptr"
@@ -84,6 +85,9 @@ type VMAuthSpec struct {
// will be added after removal of VMUserConfigOptions
// currently it has collision with inlined fields
// IPFilters VMUserIPFilters `json:"ip_filters,omitempty"`
+ // OODC represents configuration section for OIDC authorization
+ // +optional
+ OIDC []*VMAuthOIDCRealm `json:"jwt,omitempty"`
// will be removed at v1.0 release
// +deprecated
// +kubebuilder:validation:Schemaless
@@ -125,6 +129,28 @@ type VMAuthSpec struct {
UseProxyProtocol bool `json:"useProxyProtocol,omitempty"`
}
+// VMAuthOIDCRealm defines OIDC realm parameters
+type VMAuthOIDCRealm struct {
+ // EnforcePrefix requires JWT token to start with "Bearer: "
+ // +optional
+ EnforcePrefix bool `json:"enforce_prefix,omitempty"`
+ // IssuerURL is OpenID Connect issuer URL
+ // +optional
+ IssuerURL string `json:"issuer_url,omitempty"`
+ // JWKsURL is the OpenID Connect JWKS URL
+ // +optional
+ JWKsURL string `json:"jwks_url,omitempty"`
+ // SkipDiscovery allows to skip OIDC discovery and use manually supplied Endpoints
+ // +optional
+ SkipDiscovery bool `json:"skip_discovery,omitempty"`
+ // PublicKeyFiles is a list of paths pointing to public key files in PEM format to use
+ // for verifying JWT tokens
+ PublicKeyFiles []string `json:"public_key_files,omitempty"`
+ // PublicKeySecrets is a list of k8s Secret selectors pointing to public key files in PEM format to use
+ // for verifying JWT tokens
+ PublicKeySecrets []*corev1.SecretKeySelector `json:"public_key_secrets,omitempty"`
+}
+
// VMAuthUnauthorizedUserAccessSpec defines unauthorized_user section configuration for vmauth
type VMAuthUnauthorizedUserAccessSpec struct {
// URLPrefix defines prefix prefix for destination
@@ -425,7 +451,9 @@ func (cr *VMAuth) Validate() error {
return fmt.Errorf("incorrect cr.spec UnauthorizedAccessConfig options: %w", err)
}
}
-
+ if len(cr.Spec.OIDC) > 0 && !cr.Spec.License.IsProvided() {
+ return fmt.Errorf("spec.jwt is only allowed in enterprise mode, but no license provided")
+ }
if cr.Spec.UnauthorizedUserAccessSpec != nil {
if err := cr.Spec.UnauthorizedUserAccessSpec.Validate(); err != nil {
return fmt.Errorf("incorrect cr.spec.UnauthorizedUserAccess syntax: %w", err)
@@ -461,11 +489,11 @@ type EmbeddedIngress struct {
// ExtraRules - additional rules for ingress,
// must be checked for correctness by user.
// +optional
- ExtraRules []v12.IngressRule `json:"extraRules,omitempty" yaml:"extraRules,omitempty"`
+ ExtraRules []networkingv1.IngressRule `json:"extraRules,omitempty" yaml:"extraRules,omitempty"`
// ExtraTLS - additional TLS configuration for ingress
// must be checked for correctness by user.
// +optional
- ExtraTLS []v12.IngressTLS `json:"extraTls,omitempty" yaml:"extraTls,omitempty"`
+ ExtraTLS []networkingv1.IngressTLS `json:"extraTls,omitempty" yaml:"extraTls,omitempty"`
// Host defines ingress host parameter for default rule
// It will be used, only if TlsHosts is empty
// +optional
diff --git a/api/operator/v1beta1/vmuser_types.go b/api/operator/v1beta1/vmuser_types.go
index ecc1de214..56fb0c1e7 100644
--- a/api/operator/v1beta1/vmuser_types.go
+++ b/api/operator/v1beta1/vmuser_types.go
@@ -15,6 +15,9 @@ type VMUserSpec struct {
// Name of the VMUser object.
// +optional
Name *string `json:"name,omitempty"`
+ // ClientID extracted from JWT token
+ // +optional
+ ClientID *string `json:"client_id,omitempty"`
// UserName basic auth user name for accessing protected endpoint,
// will be replaced with metadata.name of VMUser if omitted.
// +optional
diff --git a/api/operator/v1beta1/zz_generated.deepcopy.go b/api/operator/v1beta1/zz_generated.deepcopy.go
index a31a3206a..ba568015b 100644
--- a/api/operator/v1beta1/zz_generated.deepcopy.go
+++ b/api/operator/v1beta1/zz_generated.deepcopy.go
@@ -5007,6 +5007,37 @@ func (in *VMAuthLoadBalancerSpec) DeepCopy() *VMAuthLoadBalancerSpec {
return out
}
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *VMAuthOIDCRealm) DeepCopyInto(out *VMAuthOIDCRealm) {
+ *out = *in
+ if in.PublicKeyFiles != nil {
+ in, out := &in.PublicKeyFiles, &out.PublicKeyFiles
+ *out = make([]string, len(*in))
+ copy(*out, *in)
+ }
+ if in.PublicKeySecrets != nil {
+ in, out := &in.PublicKeySecrets, &out.PublicKeySecrets
+ *out = make([]*v1.SecretKeySelector, len(*in))
+ for i := range *in {
+ if (*in)[i] != nil {
+ in, out := &(*in)[i], &(*out)[i]
+ *out = new(v1.SecretKeySelector)
+ (*in).DeepCopyInto(*out)
+ }
+ }
+ }
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new VMAuthOIDCRealm.
+func (in *VMAuthOIDCRealm) DeepCopy() *VMAuthOIDCRealm {
+ if in == nil {
+ return nil
+ }
+ out := new(VMAuthOIDCRealm)
+ in.DeepCopyInto(out)
+ return out
+}
+
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *VMAuthSpec) DeepCopyInto(out *VMAuthSpec) {
*out = *in
@@ -5067,6 +5098,17 @@ func (in *VMAuthSpec) DeepCopyInto(out *VMAuthSpec) {
*out = new(VMAuthUnauthorizedUserAccessSpec)
(*in).DeepCopyInto(*out)
}
+ if in.OIDC != nil {
+ in, out := &in.OIDC, &out.OIDC
+ *out = make([]*VMAuthOIDCRealm, len(*in))
+ for i := range *in {
+ if (*in)[i] != nil {
+ in, out := &(*in)[i], &(*out)[i]
+ *out = new(VMAuthOIDCRealm)
+ (*in).DeepCopyInto(*out)
+ }
+ }
+ }
in.VMUserConfigOptions.DeepCopyInto(&out.VMUserConfigOptions)
if in.License != nil {
in, out := &in.License, &out.License
@@ -6751,6 +6793,11 @@ func (in *VMUserSpec) DeepCopyInto(out *VMUserSpec) {
*out = new(string)
**out = **in
}
+ if in.ClientID != nil {
+ in, out := &in.ClientID, &out.ClientID
+ *out = new(string)
+ **out = **in
+ }
if in.UserName != nil {
in, out := &in.UserName, &out.UserName
*out = new(string)
diff --git a/config/crd/overlay/crd.yaml b/config/crd/overlay/crd.yaml
index d8ba7e876..d51198267 100644
--- a/config/crd/overlay/crd.yaml
+++ b/config/crd/overlay/crd.yaml
@@ -23651,6 +23651,69 @@ spec:
and v1.111.0 vmauth version
related doc https://docs.victoriametrics.com/victoriametrics/vmauth/#security
type: string
+ jwt:
+ description: |-
+ IPFilters global access ip filters
+ supported only with enterprise version of [vmauth](https://docs.victoriametrics.com/victoriametrics/vmauth/#ip-filters)
+ will be added after removal of VMUserConfigOptions
+ currently it has collision with inlined fields
+ IPFilters VMUserIPFilters `json:"ip_filters,omitempty"`
+ OODC represents configuration section for OIDC authorization
+ items:
+ description: VMAuthOIDCRealm defines OIDC realm parameters
+ properties:
+ enforce_prefix:
+ description: 'EnforcePrefix requires JWT token to start with
+ "Bearer: "'
+ type: boolean
+ issuer_url:
+ description: IssuerURL is OpenID Connect issuer URL
+ type: string
+ jwks_url:
+ description: JWKsURL is the OpenID Connect JWKS URL
+ type: string
+ public_key_files:
+ description: |-
+ PublicKeyFiles is a list of paths pointing to public key files in PEM format to use
+ for verifying JWT tokens
+ items:
+ type: string
+ type: array
+ public_key_secrets:
+ description: |-
+ PublicKeySecrets is a list of k8s Secret selectors pointing to public key files in PEM format to use
+ for verifying JWT tokens
+ items:
+ description: SecretKeySelector selects a key of a Secret.
+ properties:
+ key:
+ description: The key of the secret to select from. Must
+ be a valid secret key.
+ type: string
+ name:
+ default: ""
+ description: |-
+ Name of the referent.
+ This field is effectively required, but due to backwards compatibility is
+ allowed to be empty. Instances of this type with an empty value here are
+ almost certainly wrong.
+ More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names
+ type: string
+ optional:
+ description: Specify whether the Secret or its key must
+ be defined
+ type: boolean
+ required:
+ - key
+ type: object
+ x-kubernetes-map-type: atomic
+ type: array
+ skip_discovery:
+ description: SkipDiscovery allows to skip OIDC discovery and
+ use manually supplied Endpoints
+ type: boolean
+ type: object
+ type: array
license:
description: |-
License allows to configure license key to be used for enterprise features.
@@ -38811,6 +38874,9 @@ spec:
description: BearerToken Authorization header value for accessing
protected endpoint.
type: string
+ client_id:
+ description: ClientID extracted from JWT token
+ type: string
default_url:
description: |-
DefaultURLs backend url for non-matching paths filter
diff --git a/docs/api.md b/docs/api.md
index 84421bb12..8713bcdab 100644
--- a/docs/api.md
+++ b/docs/api.md
@@ -3547,6 +3547,24 @@ Appears in: [VMAuthLoadBalancer](#vmauthloadbalancer)
| volumes#
_[Volume](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.30/#volume-v1-core) array_ | _(Required)_
Volumes allows configuration of additional volumes on the output Deployment/StatefulSet definition.
Volumes specified will be appended to other volumes that are generated.
/ +optional |
+#### VMAuthOIDCRealm
+
+
+
+VMAuthOIDCRealm defines OIDC realm parameters
+
+Appears in: [VMAuthSpec](#vmauthspec)
+
+| Field | Description |
+| --- | --- |
+| enforce_prefix#
_boolean_ | _(Optional)_
EnforcePrefix requires JWT token to start with "Bearer: " |
+| issuer_url#
_string_ | _(Optional)_
IssuerURL is OpenID Connect issuer URL |
+| jwks_url#
_string_ | _(Optional)_
JWKsURL is the OpenID Connect JWKS URL |
+| public_key_files#
_string array_ | _(Required)_
PublicKeyFiles is a list of paths pointing to public key files in PEM format to use
for verifying JWT tokens |
+| public_key_secrets#
_[SecretKeySelector](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.30/#secretkeyselector-v1-core) array_ | _(Required)_
PublicKeySecrets is a list of k8s Secret selectors pointing to public key files in PEM format to use
for verifying JWT tokens |
+| skip_discovery#
_boolean_ | _(Optional)_
SkipDiscovery allows to skip OIDC discovery and use manually supplied Endpoints |
+
+
#### VMAuthSpec
@@ -3587,6 +3605,7 @@ Appears in: [VMAuth](#vmauth)
| initContainers#
_[Container](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.30/#container-v1-core) array_ | _(Optional)_
InitContainers allows adding initContainers to the pod definition.
Any errors during the execution of an initContainer will lead to a restart of the Pod.
More info: https://kubernetes.io/docs/concepts/workloads/pods/init-containers/ |
| internalListenPort#
_string_ | _(Optional)_
InternalListenPort instructs vmauth to serve internal routes at given port
available from v0.56.0 operator
and v1.111.0 vmauth version
related doc https://docs.victoriametrics.com/victoriametrics/vmauth/#security |
| ip_filters#
_[VMUserIPFilters](#vmuseripfilters)_ | _(Optional)_
IPFilters defines per target src ip filters
supported only with enterprise version of [vmauth](https://docs.victoriametrics.com/victoriametrics/vmauth/#ip-filters) |
+| jwt#
_[VMAuthOIDCRealm](#vmauthoidcrealm) array_ | _(Optional)_
IPFilters global access ip filters
supported only with enterprise version of [vmauth](https://docs.victoriametrics.com/victoriametrics/vmauth/#ip-filters)
will be added after removal of VMUserConfigOptions
currently it has collision with inlined fields
IPFilters VMUserIPFilters `json:"ip_filters,omitempty"`
OODC represents configuration section for OIDC authorization |
| license#
_[License](#license)_ | _(Optional)_
License allows to configure license key to be used for enterprise features.
Using license key is supported starting from VictoriaMetrics v1.94.0.
See [here](https://docs.victoriametrics.com/victoriametrics/enterprise/) |
| load_balancing_policy#
_string_ | _(Optional)_
LoadBalancingPolicy defines load balancing policy to use for backend urls.
Supported policies: least_loaded, first_available.
See [here](https://docs.victoriametrics.com/victoriametrics/vmauth#load-balancing) for more details (default "least_loaded") |
| logFormat#
_string_ | _(Optional)_
LogFormat for VMAuth to be configured with. |
@@ -4476,6 +4495,7 @@ Appears in: [VMUser](#vmuser)
| Field | Description |
| --- | --- |
| bearerToken#
_string_ | _(Optional)_
BearerToken Authorization header value for accessing protected endpoint. |
+| client_id#
_string_ | _(Optional)_
ClientID extracted from JWT token |
| default_url#
_string array_ | _(Required)_
DefaultURLs backend url for non-matching paths filter
usually used for default backend with error message |
| disable_secret_creation#
_boolean_ | _(Required)_
DisableSecretCreation skips related secret creation for vmuser |
| discover_backend_ips#
_boolean_ | _(Required)_
DiscoverBackendIPs instructs discovering URLPrefix backend IPs via DNS. |
diff --git a/internal/controller/operator/factory/reconcile/hpa.go b/internal/controller/operator/factory/reconcile/hpa.go
index 69b64e3e4..f4a5e56b0 100644
--- a/internal/controller/operator/factory/reconcile/hpa.go
+++ b/internal/controller/operator/factory/reconcile/hpa.go
@@ -4,7 +4,7 @@ import (
"context"
"fmt"
- v2 "k8s.io/api/autoscaling/v2"
+ autoscalingv2 "k8s.io/api/autoscaling/v2"
"k8s.io/apimachinery/pkg/api/equality"
k8serrors "k8s.io/apimachinery/pkg/api/errors"
"k8s.io/apimachinery/pkg/types"
@@ -16,9 +16,9 @@ import (
)
// HPA creates or update horizontalPodAutoscaler object
-func HPA(ctx context.Context, rclient client.Client, newHPA, prevHPA *v2.HorizontalPodAutoscaler) error {
+func HPA(ctx context.Context, rclient client.Client, newHPA, prevHPA *autoscalingv2.HorizontalPodAutoscaler) error {
return retry.RetryOnConflict(retry.DefaultRetry, func() error {
- var currentHPA v2.HorizontalPodAutoscaler
+ var currentHPA autoscalingv2.HorizontalPodAutoscaler
if err := rclient.Get(ctx, types.NamespacedName{Name: newHPA.GetName(), Namespace: newHPA.GetNamespace()}, ¤tHPA); err != nil {
if k8serrors.IsNotFound(err) {
logger.WithContext(ctx).Info(fmt.Sprintf("creating HPA %s configuration", newHPA.Name))
diff --git a/internal/controller/operator/factory/vmauth/vmusers_config.go b/internal/controller/operator/factory/vmauth/vmusers_config.go
index 48f602dc4..354e4dd4c 100644
--- a/internal/controller/operator/factory/vmauth/vmusers_config.go
+++ b/internal/controller/operator/factory/vmauth/vmusers_config.go
@@ -424,6 +424,42 @@ func generateVMAuthConfig(cr *vmv1beta1.VMAuth, sus *skipableVMUsers, crdCache m
return nil, fmt.Errorf("cannot build unauthorized_user config section: %w", err)
}
+ var oidcCfg []yaml.MapSlice
+ for _, realm := range cr.Spec.OIDC {
+ if realm == nil {
+ continue
+ }
+ var oidcItem yaml.MapSlice
+ if realm.SkipDiscovery {
+ if len(realm.JWKsURL) > 0 {
+ oidcItem = append(oidcItem, yaml.MapItem{Key: "jwks_url", Value: realm.JWKsURL})
+ }
+ var publicKeyFiles []string
+ if len(realm.PublicKeyFiles) > 0 {
+ publicKeyFiles = append(publicKeyFiles, realm.PublicKeyFiles...)
+ }
+ for _, ref := range realm.PublicKeySecrets {
+ file, err := ac.LoadPathFromSecret(build.TLSAssetsResourceKind, cr.Namespace, ref)
+ if err != nil {
+ return nil, fmt.Errorf("cannot build jwt config section: %w", err)
+ }
+ publicKeyFiles = append(publicKeyFiles, file)
+ }
+ if len(publicKeyFiles) > 0 {
+ oidcItem = append(oidcItem, yaml.MapItem{Key: "public_key_files", Value: publicKeyFiles})
+ }
+ } else {
+ oidcItem = append(oidcItem, yaml.MapItem{Key: "issuer_url", Value: realm.IssuerURL})
+ }
+ if realm.EnforcePrefix {
+ oidcItem = append(oidcItem, yaml.MapItem{Key: "enforce_prefix", Value: realm.EnforcePrefix})
+ }
+ oidcCfg = append(oidcCfg, oidcItem)
+ }
+ if len(oidcCfg) > 0 {
+ cfg = append(cfg, yaml.MapItem{Key: "oidc", Value: oidcCfg})
+ }
+
if len(unAuthorizedAccessValue) > 0 {
cfg = append(cfg, yaml.MapItem{Key: "unauthorized_user", Value: unAuthorizedAccessValue})
}
@@ -789,7 +825,7 @@ func genUserCfg(user *vmv1beta1.VMUser, crdURLCache map[string]string, cr *vmv1b
}
// generate user access config.
- var name, username, password, token string
+ var name, username, password, token, clientID string
if user.Spec.Name != nil {
name = *user.Spec.Name
}
@@ -807,6 +843,9 @@ func genUserCfg(user *vmv1beta1.VMUser, crdURLCache map[string]string, cr *vmv1b
password = *user.Spec.Password
}
+ if cr.Spec.License.IsProvided() && user.Spec.ClientID != nil {
+ clientID = *user.Spec.ClientID
+ }
if user.Spec.BearerToken != nil {
token = *user.Spec.BearerToken
}
@@ -829,6 +868,15 @@ func genUserCfg(user *vmv1beta1.VMUser, crdURLCache map[string]string, cr *vmv1b
})
return r, nil
}
+
+ if clientID != "" {
+ r = append(r, yaml.MapItem{
+ Key: "client_id",
+ Value: clientID,
+ })
+ return r, nil
+ }
+
// mutate vmuser
if username == "" {
username = user.Name