mirror of
https://github.com/project-zot/zot.git
synced 2026-06-20 06:37:56 +08:00
feat: support mTLS-only authn/authz with AccessControl and allow combining mTLS with other auth mechanisms (#3624)
* feat: support mTLS-only authn/authz with AccessControl and allow combining mTLS with other auth mechanisms Signed-off-by: Ivan Arkhipov <me@endevir.ru> * refactor: improve authentication logic and TLS certificate generation - Fix mTLS authentication to use only leaf certificate instead of iterating through all certificates in the chain - Reject Authorization headers when corresponding auth method is disabled, regardless of mTLS status (security improvement) - Simplify authentication switch statement ordering and logic - Move ErrUserDataNotFound error handling into sessionAuthn method - Refactor TLS certificate generation to use Options pattern with CertificateOptions struct for better extensibility - Consolidate duplicate certificate generation code into helper functions (generateCertificate, parseCA, initializeTemplate, applyOptions) - Rename certificate generation functions for clarity: - GenerateCertWithCN -> GenerateClientCert - GenerateSelfSignedCertWithCN -> GenerateClientSelfSignedCert - Add support for SAN settings including email addresses in certificates - Update tests to reflect new authentication behavior and certificate API This commit improves both the security posture (rejecting disabled auth methods) and code maintainability (consolidated certificate generation). Signed-off-by: Andrei Aaron <andreifdaaron@gmail.com> * fix: guard against multiple Authorization headers Signed-off-by: Andrei Aaron <andreifdaaron@gmail.com> --------- Signed-off-by: Ivan Arkhipov <me@endevir.ru> Signed-off-by: Andrei Aaron <andreifdaaron@gmail.com> Co-authored-by: Ivan Arkhipov <me@endevir.ru>
This commit is contained in:
@@ -524,57 +524,6 @@ func (c *Config) isTagsRetentionEnabled(tagRetentionPolicy KeepTagsPolicy) bool
|
||||
return false
|
||||
}
|
||||
|
||||
// isBasicAuthnEnabled checks if any basic authentication method is enabled (internal, no locking).
|
||||
func (c *Config) isBasicAuthnEnabled() bool {
|
||||
if c == nil {
|
||||
return false
|
||||
}
|
||||
|
||||
// Check HTPasswd
|
||||
if c.HTTP.Auth != nil && c.HTTP.Auth.HTPasswd.Path != "" {
|
||||
return true
|
||||
}
|
||||
|
||||
// Check LDAP
|
||||
if c.HTTP.Auth != nil && c.HTTP.Auth.LDAP != nil {
|
||||
return true
|
||||
}
|
||||
|
||||
// Check API Key
|
||||
if c.HTTP.Auth != nil && c.HTTP.Auth.APIKey {
|
||||
return true
|
||||
}
|
||||
|
||||
// Check OpenID
|
||||
if c.HTTP.Auth != nil && c.HTTP.Auth.OpenID != nil {
|
||||
for provider := range c.HTTP.Auth.OpenID.Providers {
|
||||
if isOpenIDAuthProviderEnabled(c, provider) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// isOpenIDAuthProviderEnabled checks if a specific OpenID provider is enabled (internal use only).
|
||||
func isOpenIDAuthProviderEnabled(config *Config, provider string) bool {
|
||||
if providerConfig, ok := config.HTTP.Auth.OpenID.Providers[provider]; ok {
|
||||
if IsOpenIDSupported(provider) {
|
||||
if providerConfig.ClientID != "" || providerConfig.Issuer != "" ||
|
||||
len(providerConfig.Scopes) > 0 {
|
||||
return true
|
||||
}
|
||||
} else if IsOauth2Supported(provider) {
|
||||
if providerConfig.ClientID != "" || len(providerConfig.Scopes) > 0 {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// Sanitize makes a sanitized copy of the config removing any secrets.
|
||||
func (c *Config) Sanitize() *Config {
|
||||
if c == nil {
|
||||
@@ -999,12 +948,11 @@ func (c *Config) IsMTLSAuthEnabled() bool {
|
||||
c.mu.RLock()
|
||||
defer c.mu.RUnlock()
|
||||
|
||||
// mTLS is enabled if TLS is configured with client CA certificates
|
||||
if c.HTTP.TLS != nil &&
|
||||
c.HTTP.TLS.Key != "" &&
|
||||
c.HTTP.TLS.Cert != "" &&
|
||||
c.HTTP.TLS.CACert != "" &&
|
||||
!c.isBasicAuthnEnabled() &&
|
||||
!c.HTTP.AccessControl.AnonymousPolicyExists() {
|
||||
c.HTTP.TLS.CACert != "" {
|
||||
return true
|
||||
}
|
||||
|
||||
|
||||
@@ -1760,237 +1760,6 @@ func TestConfig(t *testing.T) {
|
||||
},
|
||||
}
|
||||
So(cfg.IsMTLSAuthEnabled(), ShouldBeTrue)
|
||||
|
||||
// Test with HTPasswd enabled (should disable mTLS)
|
||||
cfg = &config.Config{
|
||||
HTTP: config.HTTPConfig{
|
||||
Auth: &config.AuthConfig{
|
||||
HTPasswd: config.AuthHTPasswd{
|
||||
Path: "/path/to/htpasswd",
|
||||
},
|
||||
},
|
||||
TLS: &config.TLSConfig{
|
||||
Cert: "/path/to/cert.pem",
|
||||
Key: "/path/to/key.pem",
|
||||
CACert: "/path/to/ca-cert.pem",
|
||||
},
|
||||
},
|
||||
}
|
||||
So(cfg.IsMTLSAuthEnabled(), ShouldBeFalse) // Basic auth enabled, so mTLS disabled
|
||||
|
||||
// Test with LDAP enabled (should disable mTLS)
|
||||
cfg = &config.Config{
|
||||
HTTP: config.HTTPConfig{
|
||||
Auth: &config.AuthConfig{
|
||||
LDAP: &config.LDAPConfig{},
|
||||
},
|
||||
TLS: &config.TLSConfig{
|
||||
Cert: "/path/to/cert.pem",
|
||||
Key: "/path/to/key.pem",
|
||||
CACert: "/path/to/ca-cert.pem",
|
||||
},
|
||||
},
|
||||
}
|
||||
So(cfg.IsMTLSAuthEnabled(), ShouldBeFalse) // Basic auth enabled, so mTLS disabled
|
||||
|
||||
// Test with API Key enabled (should disable mTLS)
|
||||
cfg = &config.Config{
|
||||
HTTP: config.HTTPConfig{
|
||||
Auth: &config.AuthConfig{
|
||||
APIKey: true,
|
||||
},
|
||||
TLS: &config.TLSConfig{
|
||||
Cert: "/path/to/cert.pem",
|
||||
Key: "/path/to/key.pem",
|
||||
CACert: "/path/to/ca-cert.pem",
|
||||
},
|
||||
},
|
||||
}
|
||||
So(cfg.IsMTLSAuthEnabled(), ShouldBeFalse) // Basic auth enabled, so mTLS disabled
|
||||
|
||||
// Test with OpenID enabled (valid config - should disable mTLS)
|
||||
cfg = &config.Config{
|
||||
HTTP: config.HTTPConfig{
|
||||
Auth: &config.AuthConfig{
|
||||
OpenID: &config.OpenIDConfig{
|
||||
Providers: map[string]config.OpenIDProviderConfig{
|
||||
"google": {
|
||||
ClientID: "client-id",
|
||||
Issuer: "",
|
||||
Scopes: []string{},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
TLS: &config.TLSConfig{
|
||||
Cert: "/path/to/cert.pem",
|
||||
Key: "/path/to/key.pem",
|
||||
CACert: "/path/to/ca-cert.pem",
|
||||
},
|
||||
},
|
||||
}
|
||||
So(cfg.IsMTLSAuthEnabled(), ShouldBeFalse) // Basic auth enabled, so mTLS disabled
|
||||
|
||||
// Test with OpenID enabled (with Issuer - should disable mTLS)
|
||||
cfg = &config.Config{
|
||||
HTTP: config.HTTPConfig{
|
||||
Auth: &config.AuthConfig{
|
||||
OpenID: &config.OpenIDConfig{
|
||||
Providers: map[string]config.OpenIDProviderConfig{
|
||||
"google": {
|
||||
ClientID: "",
|
||||
Issuer: "https://accounts.google.com",
|
||||
Scopes: []string{},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
TLS: &config.TLSConfig{
|
||||
Cert: "/path/to/cert.pem",
|
||||
Key: "/path/to/key.pem",
|
||||
CACert: "/path/to/ca-cert.pem",
|
||||
},
|
||||
},
|
||||
}
|
||||
So(cfg.IsMTLSAuthEnabled(), ShouldBeFalse) // Basic auth enabled, so mTLS disabled
|
||||
|
||||
// Test with OpenID enabled (with Scopes - should disable mTLS)
|
||||
cfg = &config.Config{
|
||||
HTTP: config.HTTPConfig{
|
||||
Auth: &config.AuthConfig{
|
||||
OpenID: &config.OpenIDConfig{
|
||||
Providers: map[string]config.OpenIDProviderConfig{
|
||||
"google": {
|
||||
ClientID: "",
|
||||
Issuer: "",
|
||||
Scopes: []string{"openid", "email"},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
TLS: &config.TLSConfig{
|
||||
Cert: "/path/to/cert.pem",
|
||||
Key: "/path/to/key.pem",
|
||||
CACert: "/path/to/ca-cert.pem",
|
||||
},
|
||||
},
|
||||
}
|
||||
So(cfg.IsMTLSAuthEnabled(), ShouldBeFalse) // Basic auth enabled, so mTLS disabled
|
||||
|
||||
// Test with OAuth2 provider (github) with ClientID (should disable mTLS)
|
||||
cfg = &config.Config{
|
||||
HTTP: config.HTTPConfig{
|
||||
Auth: &config.AuthConfig{
|
||||
OpenID: &config.OpenIDConfig{
|
||||
Providers: map[string]config.OpenIDProviderConfig{
|
||||
"github": {
|
||||
ClientID: "github-client-id",
|
||||
Scopes: []string{},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
TLS: &config.TLSConfig{
|
||||
Cert: "/path/to/cert.pem",
|
||||
Key: "/path/to/key.pem",
|
||||
CACert: "/path/to/ca-cert.pem",
|
||||
},
|
||||
},
|
||||
}
|
||||
So(cfg.IsMTLSAuthEnabled(), ShouldBeFalse) // Basic auth enabled, so mTLS disabled
|
||||
|
||||
// Test with OAuth2 provider (github) with Scopes (should disable mTLS)
|
||||
cfg = &config.Config{
|
||||
HTTP: config.HTTPConfig{
|
||||
Auth: &config.AuthConfig{
|
||||
OpenID: &config.OpenIDConfig{
|
||||
Providers: map[string]config.OpenIDProviderConfig{
|
||||
"github": {
|
||||
ClientID: "",
|
||||
Scopes: []string{"user:email"},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
TLS: &config.TLSConfig{
|
||||
Cert: "/path/to/cert.pem",
|
||||
Key: "/path/to/key.pem",
|
||||
CACert: "/path/to/ca-cert.pem",
|
||||
},
|
||||
},
|
||||
}
|
||||
So(cfg.IsMTLSAuthEnabled(), ShouldBeFalse) // Basic auth enabled, so mTLS disabled
|
||||
|
||||
// Test with OpenID but empty config (should enable mTLS)
|
||||
cfg = &config.Config{
|
||||
HTTP: config.HTTPConfig{
|
||||
Auth: &config.AuthConfig{
|
||||
OpenID: &config.OpenIDConfig{
|
||||
Providers: map[string]config.OpenIDProviderConfig{
|
||||
"google": {
|
||||
ClientID: "",
|
||||
Issuer: "",
|
||||
Scopes: []string{},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
TLS: &config.TLSConfig{
|
||||
Cert: "/path/to/cert.pem",
|
||||
Key: "/path/to/key.pem",
|
||||
CACert: "/path/to/ca-cert.pem",
|
||||
},
|
||||
},
|
||||
}
|
||||
So(cfg.IsMTLSAuthEnabled(), ShouldBeTrue) // No basic auth, so mTLS enabled
|
||||
|
||||
// Test with OpenID but unsupported provider (should enable mTLS)
|
||||
cfg = &config.Config{
|
||||
HTTP: config.HTTPConfig{
|
||||
Auth: &config.AuthConfig{
|
||||
OpenID: &config.OpenIDConfig{
|
||||
Providers: map[string]config.OpenIDProviderConfig{
|
||||
"unsupported": {
|
||||
ClientID: "client-id",
|
||||
Scopes: []string{"scope"},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
TLS: &config.TLSConfig{
|
||||
Cert: "/path/to/cert.pem",
|
||||
Key: "/path/to/key.pem",
|
||||
CACert: "/path/to/ca-cert.pem",
|
||||
},
|
||||
},
|
||||
}
|
||||
So(cfg.IsMTLSAuthEnabled(), ShouldBeTrue) // No basic auth, so mTLS enabled
|
||||
|
||||
// Test with no authentication methods (should enable mTLS)
|
||||
cfg = &config.Config{
|
||||
HTTP: config.HTTPConfig{
|
||||
Auth: &config.AuthConfig{},
|
||||
TLS: &config.TLSConfig{
|
||||
Cert: "/path/to/cert.pem",
|
||||
Key: "/path/to/key.pem",
|
||||
CACert: "/path/to/ca-cert.pem",
|
||||
},
|
||||
},
|
||||
}
|
||||
So(cfg.IsMTLSAuthEnabled(), ShouldBeTrue) // No basic auth, so mTLS enabled
|
||||
|
||||
// Test with nil Auth (should enable mTLS)
|
||||
cfg = &config.Config{
|
||||
HTTP: config.HTTPConfig{
|
||||
Auth: nil,
|
||||
TLS: &config.TLSConfig{
|
||||
Cert: "/path/to/cert.pem",
|
||||
Key: "/path/to/key.pem",
|
||||
CACert: "/path/to/ca-cert.pem",
|
||||
},
|
||||
},
|
||||
}
|
||||
So(cfg.IsMTLSAuthEnabled(), ShouldBeTrue) // No basic auth, so mTLS enabled
|
||||
})
|
||||
|
||||
Convey("Test UseSecureSession()", func() {
|
||||
@@ -2341,88 +2110,6 @@ func TestConfig(t *testing.T) {
|
||||
})
|
||||
})
|
||||
|
||||
Convey("Test isOpenIDAuthProviderEnabled indirectly via IsMTLSAuthEnabled", t, func() {
|
||||
Convey("Test with OpenID provider with empty config", func() {
|
||||
cfg := &config.Config{
|
||||
HTTP: config.HTTPConfig{
|
||||
TLS: &config.TLSConfig{
|
||||
Key: "key",
|
||||
Cert: "cert",
|
||||
CACert: "cacert",
|
||||
},
|
||||
Auth: &config.AuthConfig{
|
||||
OpenID: &config.OpenIDConfig{
|
||||
Providers: map[string]config.OpenIDProviderConfig{
|
||||
"google": {
|
||||
ClientID: "",
|
||||
Issuer: "",
|
||||
Scopes: []string{},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
AccessControl: &config.AccessControlConfig{},
|
||||
},
|
||||
}
|
||||
// This should return true because isOpenIDAuthProviderEnabled returns false for empty config,
|
||||
// so isBasicAuthnEnabled returns false, making IsMTLSAuthEnabled return true
|
||||
So(cfg.IsMTLSAuthEnabled(), ShouldBeTrue)
|
||||
})
|
||||
|
||||
Convey("Test with OpenID provider with valid config", func() {
|
||||
cfg := &config.Config{
|
||||
HTTP: config.HTTPConfig{
|
||||
TLS: &config.TLSConfig{
|
||||
Key: "key",
|
||||
Cert: "cert",
|
||||
CACert: "cacert",
|
||||
},
|
||||
Auth: &config.AuthConfig{
|
||||
OpenID: &config.OpenIDConfig{
|
||||
Providers: map[string]config.OpenIDProviderConfig{
|
||||
"google": {
|
||||
ClientID: "client-id",
|
||||
Issuer: "",
|
||||
Scopes: []string{},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
AccessControl: &config.AccessControlConfig{},
|
||||
},
|
||||
}
|
||||
// This should return false because isOpenIDAuthProviderEnabled returns true for valid config,
|
||||
// so isBasicAuthnEnabled returns true, making IsMTLSAuthEnabled return false
|
||||
So(cfg.IsMTLSAuthEnabled(), ShouldBeFalse)
|
||||
})
|
||||
|
||||
Convey("Test with unsupported OpenID provider", func() {
|
||||
cfg := &config.Config{
|
||||
HTTP: config.HTTPConfig{
|
||||
TLS: &config.TLSConfig{
|
||||
Key: "key",
|
||||
Cert: "cert",
|
||||
CACert: "cacert",
|
||||
},
|
||||
Auth: &config.AuthConfig{
|
||||
OpenID: &config.OpenIDConfig{
|
||||
Providers: map[string]config.OpenIDProviderConfig{
|
||||
"unsupported": {
|
||||
ClientID: "client-id",
|
||||
Scopes: []string{"scope"},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
AccessControl: &config.AccessControlConfig{},
|
||||
},
|
||||
}
|
||||
// This should return true because isOpenIDAuthProviderEnabled returns false for unsupported provider,
|
||||
// so isBasicAuthnEnabled returns false, making IsMTLSAuthEnabled return true
|
||||
So(cfg.IsMTLSAuthEnabled(), ShouldBeTrue)
|
||||
})
|
||||
})
|
||||
|
||||
Convey("Test nil receiver coverage for all methods", t, func() {
|
||||
Convey("Test AuthConfig methods with nil receiver", func() {
|
||||
var authConfig *config.AuthConfig = nil
|
||||
|
||||
Reference in New Issue
Block a user