LDAP: Add mTLS / client certificate authentication support (#509)

* ldap: Add mTLS authentication support to LDAP backend

* ldap: Reuse BindService() for initial bind attempt

* ldap: Make LdapService.config private

Now that we have ldap.BindService(), we don't need to access any
members of LdapService.config externally.

* ldap: Add TODO note about STARTTLS/SASL authentication

* ldap: Add TODO note about mTLS and extra CA certificates

* chore: fix typo

Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>

---------

Co-authored-by: Stavros <steveiliop56@gmail.com>
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
This commit is contained in:
Priit Laes
2025-12-31 18:01:21 +02:00
committed by GitHub
parent 1ec1f82dbd
commit f564032a11
4 changed files with 71 additions and 16 deletions

View File

@@ -19,21 +19,44 @@ type LdapServiceConfig struct {
BaseDN string
Insecure bool
SearchFilter string
AuthCert string
AuthKey string
}
type LdapService struct {
Config LdapServiceConfig // exported so as the auth service can use it
config LdapServiceConfig
conn *ldapgo.Conn
mutex sync.RWMutex
cert *tls.Certificate
}
func NewLdapService(config LdapServiceConfig) *LdapService {
return &LdapService{
Config: config,
config: config,
}
}
func (ldap *LdapService) Init() error {
// Check whether authentication with client certificate is possible
if ldap.config.AuthCert != "" && ldap.config.AuthKey != "" {
cert, err := tls.LoadX509KeyPair(ldap.config.AuthCert, ldap.config.AuthKey)
if err != nil {
return fmt.Errorf("failed to initialize LDAP with mTLS authentication: %w", err)
}
ldap.cert = &cert
log.Info().Msg("Using LDAP with mTLS authentication")
// TODO: Add optional extra CA certificates, instead of `InsecureSkipVerify`
/*
caCert, _ := ioutil.ReadFile(*caFile)
caCertPool := x509.NewCertPool()
caCertPool.AppendCertsFromPEM(caCert)
tlsConfig := &tls.Config{
...
RootCAs: caCertPool,
}
*/
}
_, err := ldap.connect()
if err != nil {
return fmt.Errorf("failed to connect to LDAP server: %w", err)
@@ -60,31 +83,46 @@ func (ldap *LdapService) connect() (*ldapgo.Conn, error) {
ldap.mutex.Lock()
defer ldap.mutex.Unlock()
conn, err := ldapgo.DialURL(ldap.Config.Address, ldapgo.DialWithTLSConfig(&tls.Config{
InsecureSkipVerify: ldap.Config.Insecure,
MinVersion: tls.VersionTLS12,
}))
var conn *ldapgo.Conn
var err error
// TODO: There's also STARTTLS (or SASL)-based mTLS authentication
// scenario, where we first connect to plain text port (389) and
// continue with a STARTTLS negotiation:
// 1. conn = ldap.DialURL("ldap://ldap.example.com:389")
// 2. conn.StartTLS(tlsConfig)
// 3. conn.externalBind()
if ldap.cert != nil {
conn, err = ldapgo.DialURL(ldap.config.Address, ldapgo.DialWithTLSConfig(&tls.Config{
MinVersion: tls.VersionTLS12,
Certificates: []tls.Certificate{*ldap.cert},
}))
} else {
conn, err = ldapgo.DialURL(ldap.config.Address, ldapgo.DialWithTLSConfig(&tls.Config{
InsecureSkipVerify: ldap.config.Insecure,
MinVersion: tls.VersionTLS12,
}))
}
if err != nil {
return nil, err
}
err = conn.Bind(ldap.Config.BindDN, ldap.Config.BindPassword)
if err != nil {
return nil, err
}
// Set and return the connection
ldap.conn = conn
return conn, nil
err = ldap.BindService(false)
if err != nil {
return nil, err
}
return ldap.conn, nil
}
func (ldap *LdapService) Search(username string) (string, error) {
// Escape the username to prevent LDAP injection
escapedUsername := ldapgo.EscapeFilter(username)
filter := fmt.Sprintf(ldap.Config.SearchFilter, escapedUsername)
filter := fmt.Sprintf(ldap.config.SearchFilter, escapedUsername)
searchRequest := ldapgo.NewSearchRequest(
ldap.Config.BaseDN,
ldap.config.BaseDN,
ldapgo.ScopeWholeSubtree, ldapgo.NeverDerefAliases, 0, 0, false,
filter,
[]string{"dn"},
@@ -107,6 +145,19 @@ func (ldap *LdapService) Search(username string) (string, error) {
return userDN, nil
}
func (ldap *LdapService) BindService(rebind bool) error {
// Locks must not be used for initial binding attempt
if rebind {
ldap.mutex.Lock()
defer ldap.mutex.Unlock()
}
if ldap.cert != nil {
return ldap.conn.ExternalBind()
}
return ldap.conn.Bind(ldap.config.BindDN, ldap.config.BindPassword)
}
func (ldap *LdapService) Bind(userDN string, password string) error {
ldap.mutex.Lock()
defer ldap.mutex.Unlock()