Introduce support for OIDC workload identity federation (#3711)

* feat(oidc): introduce support for OIDC workload identity federation

Signed-off-by: Matheus Pimenta <matheuscscp@gmail.com>

* feat(oidc): add e2e test for bearer OIDC and a kind cluster

Signed-off-by: Matheus Pimenta <matheuscscp@gmail.com>

* feat(oidc): make OIDC workload identity federation its own feature

Signed-off-by: Matheus Pimenta <matheuscscp@gmail.com>

* feat(oidc): move errors to the errors package

Signed-off-by: Matheus Pimenta <matheuscscp@gmail.com>

* feat(oidc): fix race in cel package

Signed-off-by: Matheus Pimenta <matheuscscp@gmail.com>

* feat(oidc): compile cel expressions

Signed-off-by: Matheus Pimenta <matheuscscp@gmail.com>

---------

Signed-off-by: Matheus Pimenta <matheuscscp@gmail.com>
This commit is contained in:
Matheus Pimenta
2026-01-25 05:03:53 +00:00
committed by GitHub
parent ba3436c57e
commit bf619c570e
25 changed files with 5151 additions and 52 deletions
+86
View File
@@ -129,9 +129,19 @@ func (a *AuthConfig) IsHtpasswdAuthEnabled() bool {
// IsBearerAuthEnabled checks if Bearer authentication is enabled in this auth config.
func (a *AuthConfig) IsBearerAuthEnabled() bool {
return a.IsTraditionalBearerAuthEnabled() || a.IsOIDCBearerAuthEnabled()
}
// IsTraditionalBearerAuthEnabled checks if traditional Bearer authentication is enabled in this auth config.
func (a *AuthConfig) IsTraditionalBearerAuthEnabled() bool {
return a != nil && a.Bearer != nil && a.Bearer.Cert != "" && a.Bearer.Realm != "" && a.Bearer.Service != ""
}
// IsOIDCBearerAuthEnabled checks if OIDC Bearer authentication is enabled in this auth config.
func (a *AuthConfig) IsOIDCBearerAuthEnabled() bool {
return a != nil && a.Bearer != nil && a.Bearer.OIDC.IsEnabled()
}
// IsOpenIDAuthEnabled checks if OpenID authentication is enabled in this auth config.
func (a *AuthConfig) IsOpenIDAuthEnabled() bool {
if a == nil || a.OpenID == nil {
@@ -183,6 +193,42 @@ type BearerConfig struct {
Realm string
Service string
Cert string
// OIDC configuration for workload identity authentication
OIDC BearerOIDCConfigs `json:"oidc,omitempty" mapstructure:"oidc,omitempty"`
}
// BearerOIDCConfigs is a slice of BearerOIDCConfig.
type BearerOIDCConfigs []BearerOIDCConfig
func (b BearerOIDCConfigs) IsEnabled() bool {
for i := range b {
if b[i].Issuer != "" && len(b[i].Audiences) > 0 {
return true
}
}
return false
}
// BearerOIDCConfig configures OIDC token validation for workload identity.
// This enables workloads to authenticate using OIDC ID tokens in the Authorization header.
type BearerOIDCConfig struct {
// Issuer is the OIDC issuer URL. Required for OIDC workload identity.
// Example: "https://kubernetes.default.svc.cluster.local"
Issuer string `json:"issuer" mapstructure:"issuer"`
// Audiences is a list of acceptable audiences for the OIDC token.
// At least one audience must be specified.
// Example: ["zot", "https://zot.example.com"]
Audiences []string `json:"audiences" mapstructure:"audiences"`
// ClaimMapping specifies how OIDC claims are validated and mapped to Zot identities.
// Default: {"username":"claims.iss + '/' + claims.sub"}
ClaimMapping *CELClaimValidationAndMapping `json:"claimMapping,omitempty" mapstructure:"claimMapping,omitempty"`
// SkipIssuerVerification skips issuer verification (for testing only).
// Default: false
SkipIssuerVerification bool `json:"skipIssuerVerification,omitempty" mapstructure:"skipIssuerVerification,omitempty"`
}
type SessionKeys struct {
@@ -221,6 +267,46 @@ type ClaimMapping struct {
Username string `mapstructure:"username,omitempty"`
}
// CELClaimValidationAndMapping specifies Common Expression Language (CEL) expressions
// for validating and mapping OIDC claims.
type CELClaimValidationAndMapping struct {
// Variables is a list of CELVariable definitions used to extract variables from OIDC claims.
Variables []CELVariable `mapstructure:"variables,omitempty"`
// Validations is a list of CELValidation definitions used to validate OIDC claims.
Validations []CELValidation `mapstructure:"validations,omitempty"`
// Username is the CEL expression used to extract the username from OIDC claims.
// This expression should evaluate to a string value.
// Default: "claims.iss + '/' + claims.sub"
Username string `mapstructure:"username,omitempty"`
// Groups is the CEL expression used to extract groups from OIDC claims.
// This expression should evaluate to a list of strings.
// Default: "" (no groups extracted)
Groups string `mapstructure:"groups,omitempty"`
}
// CELVariable represents a CEL expression to extract a variable from OIDC claims.
type CELVariable struct {
// Name is the name of the variable to be extracted.
Name string `mapstructure:"name"`
// Expression is the CEL expression that will extract the variable from the OIDC claims.
Expression string `mapstructure:"expression"`
}
// CELValidation represents a CEL expression used for validating OIDC claims.
type CELValidation struct {
// Expression is the CEL expression used for validation. It should evaluate to a boolean value.
// If the expression evaluates to false, the validation fails and the associated error message
// is returned.
Expression string `mapstructure:"expression"`
// Message is the error message returned if the expression evaluates to false.
Message string `mapstructure:"message"`
}
type MethodRatelimitConfig struct {
Method string
Rate int
+62
View File
@@ -707,6 +707,66 @@ func TestConfig(t *testing.T) {
So(authConfig.IsBearerAuthEnabled(), ShouldBeTrue)
})
Convey("Test IsTraditionalBearerAuthEnabled()", func() {
// Test with nil AuthConfig
var authConfig *config.AuthConfig = nil
So(authConfig.IsTraditionalBearerAuthEnabled(), ShouldBeFalse)
// Test with AuthConfig but nil Bearer
authConfig = &config.AuthConfig{}
So(authConfig.IsTraditionalBearerAuthEnabled(), ShouldBeFalse)
// Test with AuthConfig and Bearer configured with all required fields
authConfig = &config.AuthConfig{
Bearer: &config.BearerConfig{
Cert: "/path/to/cert.pem",
Realm: "test-realm",
Service: "test-service",
},
}
So(authConfig.IsTraditionalBearerAuthEnabled(), ShouldBeTrue)
// Test with partial config (missing Cert)
authConfig = &config.AuthConfig{
Bearer: &config.BearerConfig{
Realm: "test-realm",
Service: "test-service",
},
}
So(authConfig.IsTraditionalBearerAuthEnabled(), ShouldBeFalse)
})
Convey("Test IsOIDCBearerAuthEnabled()", func() {
// Test with nil AuthConfig
var authConfig *config.AuthConfig = nil
So(authConfig.IsOIDCBearerAuthEnabled(), ShouldBeFalse)
// Test with AuthConfig but nil Bearer
authConfig = &config.AuthConfig{}
So(authConfig.IsOIDCBearerAuthEnabled(), ShouldBeFalse)
// Test with AuthConfig and OIDC Bearer configured
authConfig = &config.AuthConfig{
Bearer: &config.BearerConfig{
OIDC: []config.BearerOIDCConfig{{
Issuer: "https://issuer.example.com",
Audiences: []string{"zot"},
}},
},
}
So(authConfig.IsOIDCBearerAuthEnabled(), ShouldBeTrue)
// Test with invalid OIDC config (missing audiences)
authConfig = &config.AuthConfig{
Bearer: &config.BearerConfig{
OIDC: []config.BearerOIDCConfig{{
Issuer: "https://issuer.example.com",
}},
},
}
So(authConfig.IsOIDCBearerAuthEnabled(), ShouldBeFalse)
})
Convey("Test IsOpenIDAuthEnabled()", func() {
// Test with nil AuthConfig
var authConfig *config.AuthConfig = nil
@@ -2152,6 +2212,8 @@ func TestConfig(t *testing.T) {
So(authConfig.IsLdapAuthEnabled(), ShouldBeFalse)
So(authConfig.IsHtpasswdAuthEnabled(), ShouldBeFalse)
So(authConfig.IsBearerAuthEnabled(), ShouldBeFalse)
So(authConfig.IsTraditionalBearerAuthEnabled(), ShouldBeFalse)
So(authConfig.IsOIDCBearerAuthEnabled(), ShouldBeFalse)
So(authConfig.IsOpenIDAuthEnabled(), ShouldBeFalse)
So(authConfig.IsAPIKeyEnabled(), ShouldBeFalse)
So(authConfig.IsBasicAuthnEnabled(), ShouldBeFalse)