mirror of
https://github.com/project-zot/zot.git
synced 2026-06-20 06:37:56 +08:00
feat: add configurable mTLS identity extraction with fallback chain (#3640)
Add support for configurable identity attributes in mTLS authentication, allowing identity extraction from CommonName, Subject DN, Email SAN, URI SAN, or DNSName SAN with fallback chain support. Includes regex pattern matching for URI SANs (e.g., SPIFFE workload IDs). - Add MTLSConfig with identity attributes, URISANPattern, and index fields - Implement extractMTLSIdentity with fallback chain logic - Move the mtls tests in the api package to pkg/api/mtls_test.go Signed-off-by: Andrei Aaron <andreifdaaron@gmail.com>
This commit is contained in:
@@ -70,6 +70,34 @@ type TLSConfig struct {
|
||||
CACert string
|
||||
}
|
||||
|
||||
type MTLSConfig struct {
|
||||
// IdentityAttibutes is an ordered list of identity attributes to try
|
||||
// Options: "CommonName", "Subject", "Email", "URI", "DNSName" (case-insensitive)
|
||||
// Default: ["CommonName"] (backward compatible)
|
||||
IdentityAttibutes []string `json:"identityAttributes,omitempty" mapstructure:"identityAttributes,omitempty"`
|
||||
|
||||
// URISANPattern is a regex pattern to extract identity from URI SAN
|
||||
// Only used when IdentityAttibutes contains "URI"
|
||||
// Example: "spiffe://example.org/workload/(.*)" extracts the workload ID
|
||||
// If empty, uses the full URI SAN value
|
||||
URISANPattern string `json:"uriSanPattern,omitempty" mapstructure:"uriSanPattern,omitempty"`
|
||||
|
||||
// URISANIndex specifies which URI SAN to use if multiple exist (0-based)
|
||||
// Maps to cert.URIs[index] - the URIs field is a slice, so index is needed
|
||||
// Default: 0 (first URI)
|
||||
URISANIndex int `json:"uriSanIndex,omitempty" mapstructure:"uriSanIndex,omitempty"`
|
||||
|
||||
// DNSANIndex specifies which DNS SAN to use if multiple exist (0-based)
|
||||
// Maps to cert.DNSNames[index] - the DNSNames field is a slice, so index is needed
|
||||
// Default: 0 (first DNS name)
|
||||
DNSANIndex int `json:"dnsSanIndex,omitempty" mapstructure:"dnsSanIndex,omitempty"`
|
||||
|
||||
// EmailSANIndex specifies which Email SAN to use if multiple exist (0-based)
|
||||
// Maps to cert.EmailAddresses[index] - the EmailAddresses field is a slice, so index is needed
|
||||
// Default: 0 (first email)
|
||||
EmailSANIndex int `json:"emailSanIndex,omitempty" mapstructure:"emailSanIndex,omitempty"`
|
||||
}
|
||||
|
||||
type AuthHTPasswd struct {
|
||||
Path string
|
||||
}
|
||||
@@ -86,6 +114,7 @@ type AuthConfig struct {
|
||||
SessionEncryptKey []byte `json:"-"`
|
||||
SessionDriver map[string]any `mapstructure:",omitempty"`
|
||||
SecureSession *bool `json:"secureSession,omitempty" mapstructure:"secureSession,omitempty"`
|
||||
MTLS *MTLSConfig `json:"mtls,omitempty" mapstructure:"mtls,omitempty"`
|
||||
}
|
||||
|
||||
// IsLdapAuthEnabled checks if LDAP authentication is enabled in this auth config.
|
||||
@@ -141,6 +170,15 @@ func (a *AuthConfig) GetFailDelay() int {
|
||||
return a.FailDelay
|
||||
}
|
||||
|
||||
// GetMTLSConfig returns the mTLS configuration if it exists.
|
||||
func (a *AuthConfig) GetMTLSConfig() *MTLSConfig {
|
||||
if a == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
return a.MTLS
|
||||
}
|
||||
|
||||
type BearerConfig struct {
|
||||
Realm string
|
||||
Service string
|
||||
@@ -638,6 +676,7 @@ func (c *Config) UpdateReloadableConfig(newConfig *Config) {
|
||||
c.HTTP.Auth.APIKey = newConfig.HTTP.Auth.APIKey
|
||||
c.HTTP.Auth.OpenID = newConfig.HTTP.Auth.OpenID
|
||||
c.HTTP.Auth.SecureSession = newConfig.HTTP.Auth.SecureSession
|
||||
c.HTTP.Auth.MTLS = newConfig.HTTP.Auth.MTLS
|
||||
}
|
||||
|
||||
// Initialize and update AccessControlConfig
|
||||
|
||||
@@ -873,6 +873,31 @@ func TestConfig(t *testing.T) {
|
||||
}
|
||||
So(authConfig.GetFailDelay(), ShouldEqual, 5)
|
||||
})
|
||||
|
||||
Convey("Test GetMTLSConfig()", func() {
|
||||
// Test with nil AuthConfig
|
||||
var authConfig *config.AuthConfig = nil
|
||||
|
||||
So(authConfig.GetMTLSConfig(), ShouldBeNil)
|
||||
|
||||
// Test with AuthConfig but nil MTLS
|
||||
authConfig = &config.AuthConfig{}
|
||||
So(authConfig.GetMTLSConfig(), ShouldBeNil)
|
||||
|
||||
// Test with AuthConfig and MTLS configured
|
||||
authConfig = &config.AuthConfig{
|
||||
MTLS: &config.MTLSConfig{
|
||||
IdentityAttibutes: []string{"CommonName", "URI"},
|
||||
URISANPattern: "spiffe://example.org/workload/(.*)",
|
||||
},
|
||||
}
|
||||
mtlsConfig := authConfig.GetMTLSConfig()
|
||||
So(mtlsConfig, ShouldNotBeNil)
|
||||
So(len(mtlsConfig.IdentityAttibutes), ShouldEqual, 2)
|
||||
So(mtlsConfig.IdentityAttibutes[0], ShouldEqual, "CommonName")
|
||||
So(mtlsConfig.IdentityAttibutes[1], ShouldEqual, "URI")
|
||||
So(mtlsConfig.URISANPattern, ShouldEqual, "spiffe://example.org/workload/(.*)")
|
||||
})
|
||||
})
|
||||
|
||||
Convey("Test LDAPConfig methods", t, func() {
|
||||
@@ -1036,6 +1061,10 @@ func TestConfig(t *testing.T) {
|
||||
"type": "redis",
|
||||
"host": "localhost",
|
||||
},
|
||||
MTLS: &config.MTLSConfig{
|
||||
IdentityAttibutes: []string{"CommonName"},
|
||||
URISANPattern: "spiffe://example.org/workload/(.*)",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -1049,6 +1078,8 @@ func TestConfig(t *testing.T) {
|
||||
So(authConfig.IsBearerAuthEnabled(), ShouldBeTrue)
|
||||
So(authConfig.IsOpenIDAuthEnabled(), ShouldBeTrue)
|
||||
So(authConfig.IsAPIKeyEnabled(), ShouldBeFalse)
|
||||
So(authConfig.GetMTLSConfig(), ShouldNotBeNil)
|
||||
So(authConfig.GetMTLSConfig().IdentityAttibutes[0], ShouldEqual, "CommonName")
|
||||
|
||||
// Test deep copy isolation by modifying nested structures
|
||||
authConfig.LDAP.Address = "modified-ldap.example.com"
|
||||
@@ -1056,6 +1087,8 @@ func TestConfig(t *testing.T) {
|
||||
authConfig.OpenID.Providers["google"].Scopes[0] = "modified-scope"
|
||||
authConfig.SessionHashKey[0] = 'M'
|
||||
authConfig.SessionDriver["type"] = "modified-driver"
|
||||
authConfig.MTLS.IdentityAttibutes[0] = "URI"
|
||||
authConfig.MTLS.URISANPattern = "modified-pattern"
|
||||
|
||||
// Verify original is unchanged
|
||||
So(cfg.HTTP.Auth.LDAP.Address, ShouldEqual, "ldap.example.com")
|
||||
@@ -1063,6 +1096,8 @@ func TestConfig(t *testing.T) {
|
||||
So(cfg.HTTP.Auth.OpenID.Providers["google"].Scopes[0], ShouldEqual, "openid")
|
||||
So(cfg.HTTP.Auth.SessionHashKey[0], ShouldEqual, byte('h'))
|
||||
So(cfg.HTTP.Auth.SessionDriver["type"], ShouldEqual, "redis")
|
||||
So(cfg.HTTP.Auth.MTLS.IdentityAttibutes[0], ShouldEqual, "CommonName")
|
||||
So(cfg.HTTP.Auth.MTLS.URISANPattern, ShouldEqual, "spiffe://example.org/workload/(.*)")
|
||||
})
|
||||
|
||||
Convey("Test that returned AuthConfig is isolated when config is updated via UpdateReloadableConfig", func() {
|
||||
@@ -2852,6 +2887,109 @@ func TestConfig(t *testing.T) {
|
||||
})
|
||||
})
|
||||
|
||||
Convey("Test UpdateReloadableConfig MTLS config updates", t, func() {
|
||||
Convey("Test MTLS config is updated in UpdateReloadableConfig", func() {
|
||||
// Create initial config with MTLS
|
||||
cfg := &config.Config{
|
||||
HTTP: config.HTTPConfig{
|
||||
Auth: &config.AuthConfig{
|
||||
MTLS: &config.MTLSConfig{
|
||||
IdentityAttibutes: []string{"CommonName"},
|
||||
URISANPattern: "spiffe://old.example.org/workload/(.*)",
|
||||
URISANIndex: 0,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
// Create new config with updated MTLS
|
||||
newConfig := &config.Config{
|
||||
HTTP: config.HTTPConfig{
|
||||
Auth: &config.AuthConfig{
|
||||
MTLS: &config.MTLSConfig{
|
||||
IdentityAttibutes: []string{"URI", "CommonName"},
|
||||
URISANPattern: "spiffe://new.example.org/workload/(.*)",
|
||||
URISANIndex: 1,
|
||||
DNSANIndex: 2,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
// Update the config
|
||||
cfg.UpdateReloadableConfig(newConfig)
|
||||
|
||||
// Verify MTLS config was updated
|
||||
So(cfg.HTTP.Auth.MTLS, ShouldNotBeNil)
|
||||
So(len(cfg.HTTP.Auth.MTLS.IdentityAttibutes), ShouldEqual, 2)
|
||||
So(cfg.HTTP.Auth.MTLS.IdentityAttibutes[0], ShouldEqual, "URI")
|
||||
So(cfg.HTTP.Auth.MTLS.IdentityAttibutes[1], ShouldEqual, "CommonName")
|
||||
So(cfg.HTTP.Auth.MTLS.URISANPattern, ShouldEqual, "spiffe://new.example.org/workload/(.*)")
|
||||
So(cfg.HTTP.Auth.MTLS.URISANIndex, ShouldEqual, 1)
|
||||
So(cfg.HTTP.Auth.MTLS.DNSANIndex, ShouldEqual, 2)
|
||||
})
|
||||
|
||||
Convey("Test MTLS config is set to nil when new config has nil MTLS", func() {
|
||||
// Create initial config with MTLS
|
||||
cfg := &config.Config{
|
||||
HTTP: config.HTTPConfig{
|
||||
Auth: &config.AuthConfig{
|
||||
MTLS: &config.MTLSConfig{
|
||||
IdentityAttibutes: []string{"CommonName"},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
// Create new config with nil MTLS
|
||||
newConfig := &config.Config{
|
||||
HTTP: config.HTTPConfig{
|
||||
Auth: &config.AuthConfig{
|
||||
MTLS: nil,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
// Update the config
|
||||
cfg.UpdateReloadableConfig(newConfig)
|
||||
|
||||
// Verify MTLS config was set to nil
|
||||
So(cfg.HTTP.Auth.MTLS, ShouldBeNil)
|
||||
})
|
||||
|
||||
Convey("Test MTLS config is created when going from nil to non-nil", func() {
|
||||
// Create initial config with nil MTLS
|
||||
cfg := &config.Config{
|
||||
HTTP: config.HTTPConfig{
|
||||
Auth: &config.AuthConfig{
|
||||
MTLS: nil,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
// Create new config with MTLS
|
||||
newConfig := &config.Config{
|
||||
HTTP: config.HTTPConfig{
|
||||
Auth: &config.AuthConfig{
|
||||
MTLS: &config.MTLSConfig{
|
||||
IdentityAttibutes: []string{"URI"},
|
||||
URISANPattern: "spiffe://new.example.org/workload/(.*)",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
// Update the config
|
||||
cfg.UpdateReloadableConfig(newConfig)
|
||||
|
||||
// Verify MTLS config was created
|
||||
So(cfg.HTTP.Auth.MTLS, ShouldNotBeNil)
|
||||
So(len(cfg.HTTP.Auth.MTLS.IdentityAttibutes), ShouldEqual, 1)
|
||||
So(cfg.HTTP.Auth.MTLS.IdentityAttibutes[0], ShouldEqual, "URI")
|
||||
So(cfg.HTTP.Auth.MTLS.URISANPattern, ShouldEqual, "spiffe://new.example.org/workload/(.*)")
|
||||
})
|
||||
})
|
||||
|
||||
Convey("Test UpdateReloadableConfig Storage.SubPaths logic", t, func() {
|
||||
Convey("Test existing SubPaths are updated", func() {
|
||||
// Create initial config with SubPaths
|
||||
|
||||
Reference in New Issue
Block a user