mirror of
https://github.com/project-zot/zot.git
synced 2026-06-17 12:58:02 +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:
@@ -567,6 +567,10 @@ func validateConfiguration(config *config.Config, logger zlog.Logger) error {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := validateMTLS(config, logger); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := validateOpenIDConfig(config, logger); err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -1268,6 +1272,100 @@ func validateLDAP(config *config.Config, logger zlog.Logger) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// validateMTLS checks if the authentication settings for MTLS are valid.
|
||||
func validateMTLS(config *config.Config, logger zlog.Logger) error {
|
||||
mtlsConfig := config.CopyAuthConfig().GetMTLSConfig()
|
||||
if mtlsConfig == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
// If mTLS config is present, TLS must be properly configured
|
||||
if !config.IsMTLSAuthEnabled() {
|
||||
msg := "mTLS configuration requires TLS to be enabled with CA certificate"
|
||||
logger.Error().Msg(msg)
|
||||
|
||||
return fmt.Errorf("%w: %s", zerr.ErrBadConfig, msg)
|
||||
}
|
||||
|
||||
if len(mtlsConfig.IdentityAttibutes) > 0 {
|
||||
validIdentityAttributes := []string{
|
||||
"CommonName", "CN", "Subject", "DN", "Email", "rfc822name", "URI", "URL", "DNSName", "DNS",
|
||||
}
|
||||
|
||||
var unrecognizedIdentityAttributes []string
|
||||
|
||||
for _, source := range mtlsConfig.IdentityAttibutes {
|
||||
idx := slices.IndexFunc(validIdentityAttributes,
|
||||
func(s string) bool {
|
||||
return strings.EqualFold(strings.TrimSpace(source), strings.TrimSpace(s))
|
||||
},
|
||||
)
|
||||
|
||||
if idx < 0 {
|
||||
unrecognizedIdentityAttributes = append(unrecognizedIdentityAttributes, source)
|
||||
}
|
||||
}
|
||||
|
||||
if len(unrecognizedIdentityAttributes) > 0 {
|
||||
logger.Error().Strs("identityAttributes", unrecognizedIdentityAttributes).Msg("unsupported identityAttributes")
|
||||
|
||||
return fmt.Errorf("%w: %s", zerr.ErrUnsupportedIdentityAttribute, strings.Join(unrecognizedIdentityAttributes, ","))
|
||||
}
|
||||
}
|
||||
|
||||
idx := slices.IndexFunc(mtlsConfig.IdentityAttibutes,
|
||||
func(s string) bool {
|
||||
return strings.ToLower(strings.TrimSpace(s)) == "uri" || strings.ToLower(strings.TrimSpace(s)) == "url"
|
||||
},
|
||||
)
|
||||
|
||||
useSan := idx >= 0
|
||||
|
||||
if mtlsConfig.DNSANIndex != 0 && !useSan {
|
||||
logger.Error().Int("dnsSanIndex", mtlsConfig.DNSANIndex).Strs("identityAttributes", mtlsConfig.IdentityAttibutes).
|
||||
Msg("dnsSanIndex is only supported for URI/URL MTLS identity attribute")
|
||||
|
||||
return fmt.Errorf("%w: dnsSanIndex is only supported for URI/URL MTLS identity attribute",
|
||||
zerr.ErrBadConfig)
|
||||
}
|
||||
|
||||
if mtlsConfig.EmailSANIndex != 0 && !useSan {
|
||||
logger.Error().Int("emailSanIndex", mtlsConfig.EmailSANIndex).
|
||||
Strs("identityAttributes", mtlsConfig.IdentityAttibutes).
|
||||
Msg("emailSanIndex is only supported for URI/URL MTLS identity attribute")
|
||||
|
||||
return fmt.Errorf("%w: emailSanIndex is only supported for URI/URL MTLS identity attribute",
|
||||
zerr.ErrBadConfig)
|
||||
}
|
||||
|
||||
if mtlsConfig.URISANIndex != 0 && !useSan {
|
||||
logger.Error().Int("uriSanIndex", mtlsConfig.URISANIndex).Strs("identityAttributes", mtlsConfig.IdentityAttibutes).
|
||||
Msg("uriSanIndex is only supported for URI/URL MTLS identity attribute")
|
||||
|
||||
return fmt.Errorf("%w: uriSanIndex is only supported for URI/URL MTLS identity attribute",
|
||||
zerr.ErrBadConfig)
|
||||
}
|
||||
|
||||
if mtlsConfig.URISANPattern != "" {
|
||||
if !useSan {
|
||||
logger.Error().Str("uriSanPattern", mtlsConfig.URISANPattern).
|
||||
Strs("identityAttributes", mtlsConfig.IdentityAttibutes).
|
||||
Msg("uriSanPattern is only supported for URI/URL MTLS identity attribute")
|
||||
|
||||
return fmt.Errorf("%w: uriSanPattern is only supported for URI/URL MTLS identity attribute",
|
||||
zerr.ErrBadConfig)
|
||||
}
|
||||
|
||||
if _, err := regexp.Compile(mtlsConfig.URISANPattern); err != nil {
|
||||
logger.Error().Str("uriSanPattern", mtlsConfig.URISANPattern).Msg("invalid regex pattern")
|
||||
|
||||
return fmt.Errorf("%w: %s", zerr.ErrInvalidURISANPattern, mtlsConfig.URISANPattern)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func validateHTTP(config *config.Config, logger zlog.Logger) error {
|
||||
port := config.GetHTTPPort()
|
||||
if port != "" {
|
||||
|
||||
@@ -1769,6 +1769,372 @@ storage:
|
||||
So(err, ShouldNotBeNil)
|
||||
So(err.Error(), ShouldContainSubstring, "invalid server config")
|
||||
})
|
||||
|
||||
Convey("Test verify mTLS config validation", t, func(c C) {
|
||||
Convey("Test valid mTLS config with CommonName", func() {
|
||||
content := `{
|
||||
"distSpecVersion": "1.1.1",
|
||||
"storage": {
|
||||
"rootDirectory": "/tmp/zot"
|
||||
},
|
||||
"http": {
|
||||
"address": "127.0.0.1",
|
||||
"port": "8080",
|
||||
"realm": "zot",
|
||||
"tls": {
|
||||
"cert": "test/data/server.cert",
|
||||
"key": "test/data/server.key",
|
||||
"cacert": "test/data/ca.crt"
|
||||
},
|
||||
"auth": {
|
||||
"mtls": {
|
||||
"identityAttributes": ["CommonName"]
|
||||
}
|
||||
}
|
||||
},
|
||||
"log": {
|
||||
"level": "debug"
|
||||
}
|
||||
}`
|
||||
tmpfile := MakeTempFileWithContent(t, "zot-test.json", content)
|
||||
|
||||
os.Args = []string{"cli_test", "verify", tmpfile}
|
||||
err := cli.NewServerRootCmd().Execute()
|
||||
So(err, ShouldBeNil)
|
||||
})
|
||||
|
||||
Convey("Test valid mTLS config with URI and pattern", func() {
|
||||
content := `{
|
||||
"distSpecVersion": "1.1.1",
|
||||
"storage": {
|
||||
"rootDirectory": "/tmp/zot"
|
||||
},
|
||||
"http": {
|
||||
"address": "127.0.0.1",
|
||||
"port": "8080",
|
||||
"realm": "zot",
|
||||
"tls": {
|
||||
"cert": "test/data/server.cert",
|
||||
"key": "test/data/server.key",
|
||||
"cacert": "test/data/ca.crt"
|
||||
},
|
||||
"auth": {
|
||||
"mtls": {
|
||||
"identityAttributes": ["URI", "CommonName"],
|
||||
"uriSanPattern": "spiffe://example.org/workload/(.*)"
|
||||
}
|
||||
}
|
||||
},
|
||||
"log": {
|
||||
"level": "debug"
|
||||
}
|
||||
}`
|
||||
tmpfile := MakeTempFileWithContent(t, "zot-test.json", content)
|
||||
|
||||
os.Args = []string{"cli_test", "verify", tmpfile}
|
||||
err := cli.NewServerRootCmd().Execute()
|
||||
So(err, ShouldBeNil)
|
||||
})
|
||||
|
||||
Convey("Test valid mTLS config with all valid identity attributes", func() {
|
||||
content := `{
|
||||
"distSpecVersion": "1.1.1",
|
||||
"storage": {
|
||||
"rootDirectory": "/tmp/zot"
|
||||
},
|
||||
"http": {
|
||||
"address": "127.0.0.1",
|
||||
"port": "8080",
|
||||
"realm": "zot",
|
||||
"tls": {
|
||||
"cert": "test/data/server.cert",
|
||||
"key": "test/data/server.key",
|
||||
"cacert": "test/data/ca.crt"
|
||||
},
|
||||
"auth": {
|
||||
"mtls": {
|
||||
"identityAttributes": ["CommonName", "CN", "Subject", "DN", "Email",
|
||||
"rfc822name", "URI", "URL", "DNSName", "DNS"]
|
||||
}
|
||||
}
|
||||
},
|
||||
"log": {
|
||||
"level": "debug"
|
||||
}
|
||||
}`
|
||||
tmpfile := MakeTempFileWithContent(t, "zot-test.json", content)
|
||||
|
||||
os.Args = []string{"cli_test", "verify", tmpfile}
|
||||
err := cli.NewServerRootCmd().Execute()
|
||||
So(err, ShouldBeNil)
|
||||
})
|
||||
|
||||
Convey("Test invalid identity attribute", func() {
|
||||
content := `{
|
||||
"distSpecVersion": "1.1.1",
|
||||
"storage": {
|
||||
"rootDirectory": "/tmp/zot"
|
||||
},
|
||||
"http": {
|
||||
"address": "127.0.0.1",
|
||||
"port": "8080",
|
||||
"realm": "zot",
|
||||
"tls": {
|
||||
"cert": "test/data/server.cert",
|
||||
"key": "test/data/server.key",
|
||||
"cacert": "test/data/ca.crt"
|
||||
},
|
||||
"auth": {
|
||||
"mtls": {
|
||||
"identityAttributes": ["InvalidAttribute"]
|
||||
}
|
||||
}
|
||||
},
|
||||
"log": {
|
||||
"level": "debug"
|
||||
}
|
||||
}`
|
||||
tmpfile := MakeTempFileWithContent(t, "zot-test.json", content)
|
||||
|
||||
os.Args = []string{"cli_test", "verify", tmpfile}
|
||||
err := cli.NewServerRootCmd().Execute()
|
||||
So(err, ShouldNotBeNil)
|
||||
So(err.Error(), ShouldContainSubstring, "unsupported identity attribute")
|
||||
So(err.Error(), ShouldContainSubstring, "InvalidAttribute")
|
||||
})
|
||||
|
||||
Convey("Test DNSANIndex without URI/URL identity attribute", func() {
|
||||
content := `{
|
||||
"distSpecVersion": "1.1.1",
|
||||
"storage": {
|
||||
"rootDirectory": "/tmp/zot"
|
||||
},
|
||||
"http": {
|
||||
"address": "127.0.0.1",
|
||||
"port": "8080",
|
||||
"realm": "zot",
|
||||
"tls": {
|
||||
"cert": "test/data/server.cert",
|
||||
"key": "test/data/server.key",
|
||||
"cacert": "test/data/ca.crt"
|
||||
},
|
||||
"auth": {
|
||||
"mtls": {
|
||||
"identityAttributes": ["CommonName"],
|
||||
"dnsSanIndex": 1
|
||||
}
|
||||
}
|
||||
},
|
||||
"log": {
|
||||
"level": "debug"
|
||||
}
|
||||
}`
|
||||
tmpfile := MakeTempFileWithContent(t, "zot-test.json", content)
|
||||
|
||||
os.Args = []string{"cli_test", "verify", tmpfile}
|
||||
err := cli.NewServerRootCmd().Execute()
|
||||
So(err, ShouldNotBeNil)
|
||||
So(err.Error(), ShouldContainSubstring, "dnsSanIndex is only supported for URI/URL MTLS identity attribute")
|
||||
})
|
||||
|
||||
Convey("Test EmailSANIndex without URI/URL identity attribute", func() {
|
||||
content := `{
|
||||
"distSpecVersion": "1.1.1",
|
||||
"storage": {
|
||||
"rootDirectory": "/tmp/zot"
|
||||
},
|
||||
"http": {
|
||||
"address": "127.0.0.1",
|
||||
"port": "8080",
|
||||
"realm": "zot",
|
||||
"tls": {
|
||||
"cert": "test/data/server.cert",
|
||||
"key": "test/data/server.key",
|
||||
"cacert": "test/data/ca.crt"
|
||||
},
|
||||
"auth": {
|
||||
"mtls": {
|
||||
"identityAttributes": ["CommonName"],
|
||||
"emailSanIndex": 1
|
||||
}
|
||||
}
|
||||
},
|
||||
"log": {
|
||||
"level": "debug"
|
||||
}
|
||||
}`
|
||||
tmpfile := MakeTempFileWithContent(t, "zot-test.json", content)
|
||||
|
||||
os.Args = []string{"cli_test", "verify", tmpfile}
|
||||
err := cli.NewServerRootCmd().Execute()
|
||||
So(err, ShouldNotBeNil)
|
||||
So(err.Error(), ShouldContainSubstring, "emailSanIndex is only supported for URI/URL MTLS identity attribute")
|
||||
})
|
||||
|
||||
Convey("Test URISANIndex without URI/URL identity attribute", func() {
|
||||
content := `{
|
||||
"distSpecVersion": "1.1.1",
|
||||
"storage": {
|
||||
"rootDirectory": "/tmp/zot"
|
||||
},
|
||||
"http": {
|
||||
"address": "127.0.0.1",
|
||||
"port": "8080",
|
||||
"realm": "zot",
|
||||
"tls": {
|
||||
"cert": "test/data/server.cert",
|
||||
"key": "test/data/server.key",
|
||||
"cacert": "test/data/ca.crt"
|
||||
},
|
||||
"auth": {
|
||||
"mtls": {
|
||||
"identityAttributes": ["CommonName"],
|
||||
"uriSanIndex": 1
|
||||
}
|
||||
}
|
||||
},
|
||||
"log": {
|
||||
"level": "debug"
|
||||
}
|
||||
}`
|
||||
tmpfile := MakeTempFileWithContent(t, "zot-test.json", content)
|
||||
|
||||
os.Args = []string{"cli_test", "verify", tmpfile}
|
||||
err := cli.NewServerRootCmd().Execute()
|
||||
So(err, ShouldNotBeNil)
|
||||
So(err.Error(), ShouldContainSubstring, "uriSanIndex is only supported for URI/URL MTLS identity attribute")
|
||||
})
|
||||
|
||||
Convey("Test URISANPattern without URI/URL identity attribute", func() {
|
||||
content := `{
|
||||
"distSpecVersion": "1.1.1",
|
||||
"storage": {
|
||||
"rootDirectory": "/tmp/zot"
|
||||
},
|
||||
"http": {
|
||||
"address": "127.0.0.1",
|
||||
"port": "8080",
|
||||
"realm": "zot",
|
||||
"tls": {
|
||||
"cert": "test/data/server.cert",
|
||||
"key": "test/data/server.key",
|
||||
"cacert": "test/data/ca.crt"
|
||||
},
|
||||
"auth": {
|
||||
"mtls": {
|
||||
"identityAttributes": ["CommonName"],
|
||||
"uriSanPattern": "spiffe://example.org/workload/(.*)"
|
||||
}
|
||||
}
|
||||
},
|
||||
"log": {
|
||||
"level": "debug"
|
||||
}
|
||||
}`
|
||||
tmpfile := MakeTempFileWithContent(t, "zot-test.json", content)
|
||||
|
||||
os.Args = []string{"cli_test", "verify", tmpfile}
|
||||
err := cli.NewServerRootCmd().Execute()
|
||||
So(err, ShouldNotBeNil)
|
||||
So(err.Error(), ShouldContainSubstring, "uriSanPattern is only supported for URI/URL MTLS identity attribute")
|
||||
})
|
||||
|
||||
Convey("Test invalid regex pattern for URISANPattern", func() {
|
||||
content := `{
|
||||
"distSpecVersion": "1.1.1",
|
||||
"storage": {
|
||||
"rootDirectory": "/tmp/zot"
|
||||
},
|
||||
"http": {
|
||||
"address": "127.0.0.1",
|
||||
"port": "8080",
|
||||
"realm": "zot",
|
||||
"tls": {
|
||||
"cert": "test/data/server.cert",
|
||||
"key": "test/data/server.key",
|
||||
"cacert": "test/data/ca.crt"
|
||||
},
|
||||
"auth": {
|
||||
"mtls": {
|
||||
"identityAttributes": ["URI"],
|
||||
"uriSanPattern": "[invalid(regex"
|
||||
}
|
||||
}
|
||||
},
|
||||
"log": {
|
||||
"level": "debug"
|
||||
}
|
||||
}`
|
||||
tmpfile := MakeTempFileWithContent(t, "zot-test.json", content)
|
||||
|
||||
os.Args = []string{"cli_test", "verify", tmpfile}
|
||||
err := cli.NewServerRootCmd().Execute()
|
||||
So(err, ShouldNotBeNil)
|
||||
So(err.Error(), ShouldContainSubstring, "invalid URI SAN pattern")
|
||||
})
|
||||
|
||||
Convey("Test valid mTLS config with URL identity attribute", func() {
|
||||
content := `{
|
||||
"distSpecVersion": "1.1.1",
|
||||
"storage": {
|
||||
"rootDirectory": "/tmp/zot"
|
||||
},
|
||||
"http": {
|
||||
"address": "127.0.0.1",
|
||||
"port": "8080",
|
||||
"realm": "zot",
|
||||
"tls": {
|
||||
"cert": "test/data/server.cert",
|
||||
"key": "test/data/server.key",
|
||||
"cacert": "test/data/ca.crt"
|
||||
},
|
||||
"auth": {
|
||||
"mtls": {
|
||||
"identityAttributes": ["URL"],
|
||||
"uriSanPattern": "spiffe://example.org/workload/(.*)",
|
||||
"uriSanIndex": 0
|
||||
}
|
||||
}
|
||||
},
|
||||
"log": {
|
||||
"level": "debug"
|
||||
}
|
||||
}`
|
||||
tmpfile := MakeTempFileWithContent(t, "zot-test.json", content)
|
||||
|
||||
os.Args = []string{"cli_test", "verify", tmpfile}
|
||||
err := cli.NewServerRootCmd().Execute()
|
||||
So(err, ShouldBeNil)
|
||||
})
|
||||
|
||||
Convey("Test mTLS config without TLS (should fail - mTLS requires TLS)", func() {
|
||||
content := `{
|
||||
"distSpecVersion": "1.1.1",
|
||||
"storage": {
|
||||
"rootDirectory": "/tmp/zot"
|
||||
},
|
||||
"http": {
|
||||
"address": "127.0.0.1",
|
||||
"port": "8080",
|
||||
"realm": "zot",
|
||||
"auth": {
|
||||
"mtls": {
|
||||
"identityAttributes": ["CommonName"]
|
||||
}
|
||||
}
|
||||
},
|
||||
"log": {
|
||||
"level": "debug"
|
||||
}
|
||||
}`
|
||||
tmpfile := MakeTempFileWithContent(t, "zot-test.json", content)
|
||||
|
||||
os.Args = []string{"cli_test", "verify", tmpfile}
|
||||
err := cli.NewServerRootCmd().Execute()
|
||||
So(err, ShouldNotBeNil)
|
||||
So(err.Error(), ShouldContainSubstring, "mTLS configuration requires TLS to be enabled with CA certificate")
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func TestApiKeyConfig(t *testing.T) {
|
||||
|
||||
Reference in New Issue
Block a user