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:
Andrei Aaron
2025-12-11 20:08:32 +02:00
committed by GitHub
parent e7b73b6c2d
commit 08fae9104d
11 changed files with 2066 additions and 536 deletions
+2 -54
View File
@@ -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
}
-313
View File
@@ -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