Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
77 changes: 64 additions & 13 deletions internal/service/ldap_service.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
Comment thread
plaes marked this conversation as resolved.
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,
}
*/
}
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 +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},
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 +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()
}
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