Skip to content

Commit f564032

Browse files
plaessteveiliop56coderabbitai[bot]
authored
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>
1 parent 1ec1f82 commit f564032

4 files changed

Lines changed: 69 additions & 14 deletions

File tree

internal/bootstrap/service_bootstrap.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,8 @@ func (app *BootstrapApp) initServices(queries *repository.Queries) (Services, er
2525
BaseDN: app.config.Ldap.BaseDN,
2626
Insecure: app.config.Ldap.Insecure,
2727
SearchFilter: app.config.Ldap.SearchFilter,
28+
AuthCert: app.config.Ldap.AuthCert,
29+
AuthKey: app.config.Ldap.AuthKey,
2830
})
2931

3032
err := ldapService.Init()

internal/config/config.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,8 @@ type LdapConfig struct {
6767
BaseDN string `description:"Base DN for LDAP searches." yaml:"baseDn"`
6868
Insecure bool `description:"Allow insecure LDAP connections." yaml:"insecure"`
6969
SearchFilter string `description:"LDAP search filter." yaml:"searchFilter"`
70+
AuthCert string `description:"Certificate for mTLS authentication." yaml:"authCert"`
71+
AuthKey string `description:"Certificate key for mTLS authentication." yaml:"authKey"`
7072
}
7173

7274
type ExperimentalConfig struct {

internal/service/auth_service.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -101,7 +101,7 @@ func (auth *AuthService) VerifyUser(search config.UserSearch, password string) b
101101
return false
102102
}
103103

104-
err = auth.ldap.Bind(auth.ldap.Config.BindDN, auth.ldap.Config.BindPassword)
104+
err = auth.ldap.BindService(true)
105105
if err != nil {
106106
log.Error().Err(err).Msg("Failed to rebind with service account after user authentication")
107107
return false

internal/service/ldap_service.go

Lines changed: 64 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -19,21 +19,44 @@ type LdapServiceConfig struct {
1919
BaseDN string
2020
Insecure bool
2121
SearchFilter string
22+
AuthCert string
23+
AuthKey string
2224
}
2325

2426
type LdapService struct {
25-
Config LdapServiceConfig // exported so as the auth service can use it
27+
config LdapServiceConfig
2628
conn *ldapgo.Conn
2729
mutex sync.RWMutex
30+
cert *tls.Certificate
2831
}
2932

3033
func NewLdapService(config LdapServiceConfig) *LdapService {
3134
return &LdapService{
32-
Config: config,
35+
config: config,
3336
}
3437
}
3538

3639
func (ldap *LdapService) Init() error {
40+
// Check whether authentication with client certificate is possible
41+
if ldap.config.AuthCert != "" && ldap.config.AuthKey != "" {
42+
cert, err := tls.LoadX509KeyPair(ldap.config.AuthCert, ldap.config.AuthKey)
43+
if err != nil {
44+
return fmt.Errorf("failed to initialize LDAP with mTLS authentication: %w", err)
45+
}
46+
ldap.cert = &cert
47+
log.Info().Msg("Using LDAP with mTLS authentication")
48+
49+
// TODO: Add optional extra CA certificates, instead of `InsecureSkipVerify`
50+
/*
51+
caCert, _ := ioutil.ReadFile(*caFile)
52+
caCertPool := x509.NewCertPool()
53+
caCertPool.AppendCertsFromPEM(caCert)
54+
tlsConfig := &tls.Config{
55+
...
56+
RootCAs: caCertPool,
57+
}
58+
*/
59+
}
3760
_, err := ldap.connect()
3861
if err != nil {
3962
return fmt.Errorf("failed to connect to LDAP server: %w", err)
@@ -60,31 +83,46 @@ func (ldap *LdapService) connect() (*ldapgo.Conn, error) {
6083
ldap.mutex.Lock()
6184
defer ldap.mutex.Unlock()
6285

63-
conn, err := ldapgo.DialURL(ldap.Config.Address, ldapgo.DialWithTLSConfig(&tls.Config{
64-
InsecureSkipVerify: ldap.Config.Insecure,
65-
MinVersion: tls.VersionTLS12,
66-
}))
86+
var conn *ldapgo.Conn
87+
var err error
88+
89+
// TODO: There's also STARTTLS (or SASL)-based mTLS authentication
90+
// scenario, where we first connect to plain text port (389) and
91+
// continue with a STARTTLS negotiation:
92+
// 1. conn = ldap.DialURL("ldap://ldap.example.com:389")
93+
// 2. conn.StartTLS(tlsConfig)
94+
// 3. conn.externalBind()
95+
if ldap.cert != nil {
96+
conn, err = ldapgo.DialURL(ldap.config.Address, ldapgo.DialWithTLSConfig(&tls.Config{
97+
MinVersion: tls.VersionTLS12,
98+
Certificates: []tls.Certificate{*ldap.cert},
99+
}))
100+
} else {
101+
conn, err = ldapgo.DialURL(ldap.config.Address, ldapgo.DialWithTLSConfig(&tls.Config{
102+
InsecureSkipVerify: ldap.config.Insecure,
103+
MinVersion: tls.VersionTLS12,
104+
}))
105+
}
67106
if err != nil {
68107
return nil, err
69108
}
70109

71-
err = conn.Bind(ldap.Config.BindDN, ldap.Config.BindPassword)
110+
ldap.conn = conn
111+
112+
err = ldap.BindService(false)
72113
if err != nil {
73114
return nil, err
74115
}
75-
76-
// Set and return the connection
77-
ldap.conn = conn
78-
return conn, nil
116+
return ldap.conn, nil
79117
}
80118

81119
func (ldap *LdapService) Search(username string) (string, error) {
82120
// Escape the username to prevent LDAP injection
83121
escapedUsername := ldapgo.EscapeFilter(username)
84-
filter := fmt.Sprintf(ldap.Config.SearchFilter, escapedUsername)
122+
filter := fmt.Sprintf(ldap.config.SearchFilter, escapedUsername)
85123

86124
searchRequest := ldapgo.NewSearchRequest(
87-
ldap.Config.BaseDN,
125+
ldap.config.BaseDN,
88126
ldapgo.ScopeWholeSubtree, ldapgo.NeverDerefAliases, 0, 0, false,
89127
filter,
90128
[]string{"dn"},
@@ -107,6 +145,19 @@ func (ldap *LdapService) Search(username string) (string, error) {
107145
return userDN, nil
108146
}
109147

148+
func (ldap *LdapService) BindService(rebind bool) error {
149+
// Locks must not be used for initial binding attempt
150+
if rebind {
151+
ldap.mutex.Lock()
152+
defer ldap.mutex.Unlock()
153+
}
154+
155+
if ldap.cert != nil {
156+
return ldap.conn.ExternalBind()
157+
}
158+
return ldap.conn.Bind(ldap.config.BindDN, ldap.config.BindPassword)
159+
}
160+
110161
func (ldap *LdapService) Bind(userDN string, password string) error {
111162
ldap.mutex.Lock()
112163
defer ldap.mutex.Unlock()

0 commit comments

Comments
 (0)