Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions internal/bootstrap/service_bootstrap.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,8 @@ func (app *BootstrapApp) initServices() (Services, error) {
BaseDN: app.config.Ldap.BaseDN,
Insecure: app.config.Ldap.Insecure,
SearchFilter: app.config.Ldap.SearchFilter,
AuthCert: app.config.Ldap.AuthCert,
AuthKey: app.config.Ldap.AuthKey,
})

err = ldapService.Init()
Expand Down
2 changes: 2 additions & 0 deletions internal/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,8 @@ type LdapConfig struct {
BaseDN string `description:"Base DN for LDAP searches." yaml:"baseDn"`
Insecure bool `description:"Allow insecure LDAP connections." yaml:"insecure"`
SearchFilter string `description:"LDAP search filter." yaml:"searchFilter"`
AuthCert string `description:"Certificate for mTLS authentication." yaml:"authCert"`
AuthKey string `description:"Certificate key for mTLS authentication." yaml:"authKey"`
}

type ExperimentalConfig struct {
Expand Down
2 changes: 1 addition & 1 deletion internal/service/auth_service.go
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,7 @@ func (auth *AuthService) VerifyUser(search config.UserSearch, password string) b
return false
}

err = auth.ldap.Bind(auth.ldap.Config.BindDN, auth.ldap.Config.BindPassword)
err = auth.ldap.BindService(true)
if err != nil {
log.Error().Err(err).Msg("Failed to rebind with service account after user authentication")
return false
Expand Down
66 changes: 53 additions & 13 deletions internal/service/ldap_service.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,21 +19,33 @@ 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)
}
Comment thread
plaes marked this conversation as resolved.
ldap.cert = &cert
log.Info().Msg("Using LDAP with mTLS authentication")
}
Comment thread
plaes marked this conversation as resolved.
_, err := ldap.connect()
if err != nil {
return fmt.Errorf("failed to connect to LDAP server: %w", err)
Expand All @@ -60,31 +72,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
// scenarion, where we first connect to plain text port (389) and
Comment thread
steveiliop56 marked this conversation as resolved.
Outdated
// 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},
Comment thread
steveiliop56 marked this conversation as resolved.
}))
} 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)
ldap.conn = conn

err = ldap.BindService(false)
if err != nil {
return nil, err
}

// Set and return the connection
ldap.conn = conn
return conn, nil
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"},
Expand All @@ -107,6 +134,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()
}
Comment thread
plaes marked this conversation as resolved.
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()
Expand Down