diff --git a/README.md b/README.md index 5f103a6..f86d411 100644 --- a/README.md +++ b/README.md @@ -7,7 +7,7 @@ Go-lang LDAP Authentication (GLAuth) is a secure, easy-to-use, LDAP server w/ co ![GitHub all releases](https://img.shields.io/github/downloads/glauth/glauth/total) ![Docker pulls](https://badgen.net/docker/pulls/glauth/glauth) -![GitHub last commit (branch)](https://img.shields.io/github/last-commit/glauth/glauth/maaster) +![GitHub last commit (branch)](https://img.shields.io/github/last-commit/glauth/glauth/master) ![Code Climate maintainability](https://img.shields.io/codeclimate/maintainability-percentage/glauth/glauth) * Centrally manage accounts across your infrastructure diff --git a/v2/glauth.go b/v2/glauth.go index d0275b9..75e3498 100644 --- a/v2/glauth.go +++ b/v2/glauth.go @@ -1,6 +1,7 @@ package main import ( + "crypto/tls" "fmt" "net/http" "os" @@ -14,6 +15,7 @@ import ( docopt "github.com/docopt/docopt-go" "github.com/fsnotify/fsnotify" "github.com/glauth/glauth/v2/internal/monitoring" + _tls "github.com/glauth/glauth/v2/internal/tls" "github.com/glauth/glauth/v2/internal/toml" "github.com/glauth/glauth/v2/internal/version" "github.com/glauth/glauth/v2/pkg/config" @@ -131,10 +133,22 @@ func startService() { startConfigWatcher() + var err error + var tlsConfig *tls.Config + if c := activeConfig.LDAP; c.Enabled && c.TLS { + // TODO check if tls params are string or bytes and change config accordingly + tlsConfig, err = _tls.MakeTLS([]byte(c.TLSCert), []byte(c.TLSKey)) + + if err != nil { + log.Warn().Err(err).Msg("unable to configure TLS, proceeding without....StartTLS won't be supported") + } + } + s, err := server.NewServer( server.Logger(log), server.Config(activeConfig), server.Monitor(monitor), + server.TLSConfig(tlsConfig), ) if err != nil { diff --git a/v2/glauth_test.go b/v2/glauth_test.go index 5703b01..e8d8e1b 100644 --- a/v2/glauth_test.go +++ b/v2/glauth_test.go @@ -1,6 +1,7 @@ package main import ( + "os" "os/exec" "strings" "testing" @@ -26,50 +27,61 @@ type testEnv struct { func TestIntegerStuff(t *testing.T) { - Convey("Testing sample-simple local file-based LDAP server", t, func() { - svc := startSvc(SD, "bin/glauth", "-c", "sample-simple.cfg") - batteryOfTests( - t, - svc, testEnv{ - checkanonymousrootDSE: true, - checkTOTP: true, - checkbindUPN: true, - expectedinfo: "supportedLDAPVersion: 3", - svcdn: "cn=serviceuser,ou=svcaccts,dc=glauth,dc=com", - svcdnnogroup: "cn=serviceuser,dc=glauth,dc=com", - otpdn: "cn=otpuser,ou=superheros,dc=glauth,dc=com", - expectedaccount: "dn: cn=hackers,ou=superheros,ou=users,dc=glauth,dc=com", - scopedaccount: "dn: cn=hackers,ou=superheros,dc=glauth,dc=com", - expectedfirstaccount: "dn: cn=hackers,ou=superheros,ou=users,dc=glauth,dc=com", - expectedgroup: "dn: ou=superheros,ou=users,dc=glauth,dc=com", - checkemployeetype: "cn=hackers", - }) - stopSvc(svc) - }) + Convey("Preflight: Testing pre-requisites", t, func() { + Convey("bin/glauth should exist", func() { + info, err := os.Stat("bin/glauth") + So(err, ShouldEqual, nil) + Convey("bin/glauth should be executable", func() { + mode := uint32(info.Mode()) + So(mode&0b001001001, ShouldNotEqual, 0) - matchingLibrary := doRunGetFirst(RD, "ls", "bin/sqlite.so") - if matchingLibrary == "bin/sqlite.so" { - Convey("Testing sample-database LDAP server", t, func() { - svc := startSvc(SD, "bin/glauth", "-c", "pkg/plugins/sample-database.cfg") - batteryOfTests( - t, - svc, testEnv{ - checkanonymousrootDSE: true, - checkTOTP: false, - checkbindUPN: true, - expectedinfo: "supportedLDAPVersion: 3", - svcdn: "cn=serviceuser,ou=svcaccts,dc=glauth,dc=com", - svcdnnogroup: "cn=serviceuser,dc=glauth,dc=com", - otpdn: "cn=otpuser,ou=superheros,dc=glauth,dc=com", - expectedaccount: "dn: cn=hackers,ou=superheros,ou=users,dc=glauth,dc=com", - scopedaccount: "dn: cn=hackers,ou=superheros,dc=glauth,dc=com", - expectedfirstaccount: "dn: cn=hackers,ou=superheros,ou=users,dc=glauth,dc=com", - expectedgroup: "dn: ou=superheros,ou=users,dc=glauth,dc=com", - checkemployeetype: "", + Convey("Testing sample-simple local file-based LDAP server", func() { + svc := startSvc(SD, "bin/glauth", "-c", "sample-simple.cfg") + batteryOfTests( + t, + svc, testEnv{ + checkanonymousrootDSE: true, + checkTOTP: true, + checkbindUPN: true, + expectedinfo: "supportedLDAPVersion: 3", + svcdn: "cn=serviceuser,ou=svcaccts,dc=glauth,dc=com", + svcdnnogroup: "cn=serviceuser,dc=glauth,dc=com", + otpdn: "cn=otpuser,ou=superheros,dc=glauth,dc=com", + expectedaccount: "dn: cn=hackers,ou=superheros,ou=users,dc=glauth,dc=com", + scopedaccount: "dn: cn=hackers,ou=superheros,dc=glauth,dc=com", + expectedfirstaccount: "dn: cn=hackers,ou=superheros,ou=users,dc=glauth,dc=com", + expectedgroup: "dn: ou=superheros,ou=users,dc=glauth,dc=com", + checkemployeetype: "cn=hackers", + }) + stopSvc(svc) }) - stopSvc(svc) + + matchingLibrary := doRunGetFirst(RD, "ls", "bin/sqlite.so") + if matchingLibrary == "bin/sqlite.so" { + Convey("Testing sample-database LDAP server", func() { + svc := startSvc(SD, "bin/glauth", "-c", "pkg/plugins/sample-database.cfg") + batteryOfTests( + t, + svc, testEnv{ + checkanonymousrootDSE: true, + checkTOTP: false, + checkbindUPN: true, + expectedinfo: "supportedLDAPVersion: 3", + svcdn: "cn=serviceuser,ou=svcaccts,dc=glauth,dc=com", + svcdnnogroup: "cn=serviceuser,dc=glauth,dc=com", + otpdn: "cn=otpuser,ou=superheros,dc=glauth,dc=com", + expectedaccount: "dn: cn=hackers,ou=superheros,ou=users,dc=glauth,dc=com", + scopedaccount: "dn: cn=hackers,ou=superheros,dc=glauth,dc=com", + expectedfirstaccount: "dn: cn=hackers,ou=superheros,ou=users,dc=glauth,dc=com", + expectedgroup: "dn: ou=superheros,ou=users,dc=glauth,dc=com", + checkemployeetype: "", + }) + stopSvc(svc) + }) + } + }) }) - } + }) matchingContainers := doRunGetFirst(RD, "sh", "-c", "docker ps | grep openldap-service | wc -l") if matchingContainers == "1" { diff --git a/v2/go.mod b/v2/go.mod index 4492aee..f816937 100644 --- a/v2/go.mod +++ b/v2/go.mod @@ -42,3 +42,7 @@ require ( golang.org/x/sys v0.13.0 // indirect google.golang.org/protobuf v1.31.0 // indirect ) + +replace github.com/nmcclain/ldap => github.com/shipperizer/ldap v0.0.0-20231117095347-c1d6afe484ca + +// replace github.com/nmcclain/ldap => ../../../ldap diff --git a/v2/go.sum b/v2/go.sum index c915205..bb36e4c 100644 --- a/v2/go.sum +++ b/v2/go.sum @@ -74,6 +74,8 @@ github.com/rogpeppe/go-internal v1.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDN github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg= github.com/rs/zerolog v1.31.0 h1:FcTR3NnLWW+NnTwwhFWiJSZr4ECLpqCm6QsEnyvbV4A= github.com/rs/zerolog v1.31.0/go.mod h1:/7mN4D5sKwJLZQ2b/znpjC3/GQWY/xaDXUM0kKWRHss= +github.com/shipperizer/ldap v0.0.0-20231117095347-c1d6afe484ca h1:KSpZYIDb2ei4d+AxsFmC9KJaTNlmSiONSl6hLd5Qmfg= +github.com/shipperizer/ldap v0.0.0-20231117095347-c1d6afe484ca/go.mod h1:/KinL0zHM54Sk4gMgvODL26YGG7u4SFBOAKNpJmsJvc= github.com/smarty/assertions v1.15.0 h1:cR//PqUBUiQRakZWqBiFFQ9wb8emQGDb0HeGdqGByCY= github.com/smarty/assertions v1.15.0/go.mod h1:yABtdzeQs6l1brC900WlRNwj6ZR55d7B+E8C6HtKdec= github.com/smartystreets/goconvey v1.8.1 h1:qGjIddxOk4grTu9JPOU31tVfq3cNdBlNa5sSznIX1xY= diff --git a/v2/internal/tls/tls.go b/v2/internal/tls/tls.go new file mode 100644 index 0000000..df7f7d5 --- /dev/null +++ b/v2/internal/tls/tls.go @@ -0,0 +1,81 @@ +package tls + +import ( + tls "crypto/tls" + "crypto/x509" + "encoding/pem" + + "github.com/rs/zerolog" +) + +var ( + log zerolog.Logger +) + +// MakeTLS generates a tls.Config +func MakeTLS(clientCert, key []byte) (*tls.Config, error) { + if clientCert == nil && key == nil { + return new(tls.Config), nil + } + + var err error + + cert, err := tls.X509KeyPair(clientCert, key) + + if err != nil { + return nil, err + } + + log.Debug().Str("key", string(key)) + log.Debug().Str("client.certificate", string(clientCert)) + log.Debug().Interface("certificate", cert) + + if err != nil { + return nil, err + } + + // Get the SystemCertPool, continue with an empty pool on error + rootCAs, err := x509.SystemCertPool() + + if rootCAs == nil { + rootCAs = x509.NewCertPool() + log.Warn().Err(err).Msg("Using empty cert-pool") + } else { + log.Info().Msg("Using system cert-pool") + } + + for _, cert := range DecodePEM(clientCert).Certificate { + x509Cert, err := x509.ParseCertificate(cert) + if err != nil { + log.Error().Err(err).Msg("issue parsing cert PEM") + } + rootCAs.AddCert(x509Cert) + } + + log.Debug().Interface("root.ca", rootCAs) + log.Debug().Interface("certificates", []tls.Certificate{cert}) + + return &tls.Config{ + RootCAs: rootCAs, + MinVersion: tls.VersionTLS10, + MaxVersion: tls.VersionTLS13, + Certificates: []tls.Certificate{cert}, + }, nil +} + +// DecodePEM builds a PEM certificate object +func DecodePEM(certPEM []byte) tls.Certificate { + var cert tls.Certificate + var certDER *pem.Block + for { + certDER, certPEM = pem.Decode(certPEM) + if certDER == nil { + break + } + if certDER.Type == "CERTIFICATE" { + cert.Certificate = append(cert.Certificate, certDER.Bytes) + } + } + + return cert +} diff --git a/v2/internal/toml/config.go b/v2/internal/toml/config.go index 8b8dc69..4a0f101 100644 --- a/v2/internal/toml/config.go +++ b/v2/internal/toml/config.go @@ -400,6 +400,25 @@ func validateConfig(cfg *config.Config) (*config.Config, error) { if len(cfg.LDAP.Listen) == 0 { return cfg, fmt.Errorf("no LDAP bind address was specified: please disable LDAP or use the 'listen' option") } + + if cfg.LDAP.TLS && cfg.LDAP.TLSCert == "" && cfg.LDAP.TLSCertPath != "" { + byteData, err := os.ReadFile(cfg.LDAP.TLSCertPath) + cfg.LDAP.TLSCert = string(byteData) + + if err != nil { + return cfg, fmt.Errorf("unable to read TLS certificate file") + } + } + + if cfg.LDAP.TLS && cfg.LDAP.TLSKey == "" && cfg.LDAP.TLSKeyPath != "" { + byteData, err := os.ReadFile(cfg.LDAP.TLSKeyPath) + cfg.LDAP.TLSKey = string(byteData) + + if err != nil { + return cfg, fmt.Errorf("unable to read TLS key file") + } + } + } //spew.Dump(cfg) diff --git a/v2/pkg/config/config.go b/v2/pkg/config/config.go index 88bc5ae..be7989a 100644 --- a/v2/pkg/config/config.go +++ b/v2/pkg/config/config.go @@ -36,6 +36,12 @@ type Frontend struct { type LDAP struct { Enabled bool Listen string + // StartTLS parameters + TLS bool + TLSCert string + TLSKey string + TLSCertPath string + TLSKeyPath string } type LDAPS struct { Enabled bool diff --git a/v2/pkg/frontend/options.go b/v2/pkg/frontend/options.go index 2d731e7..1904d74 100644 --- a/v2/pkg/frontend/options.go +++ b/v2/pkg/frontend/options.go @@ -2,6 +2,7 @@ package frontend import ( "context" + "github.com/rs/zerolog" "github.com/glauth/glauth/v2/pkg/config" diff --git a/v2/pkg/logging/loghandler_other.go b/v2/pkg/logging/loghandler_other.go index 79b50d2..e2a3d2a 100644 --- a/v2/pkg/logging/loghandler_other.go +++ b/v2/pkg/logging/loghandler_other.go @@ -5,7 +5,6 @@ package logging import ( "fmt" - "github.com/rs/zerolog" "io" "log" "log/syslog" @@ -13,6 +12,8 @@ import ( "regexp" "strings" "time" + + "github.com/rs/zerolog" ) // We will use this package to wrap log messages coming from libraries who have no interest diff --git a/v2/pkg/logging/loghandler_windows.go b/v2/pkg/logging/loghandler_windows.go index b538cc0..9bd6363 100644 --- a/v2/pkg/logging/loghandler_windows.go +++ b/v2/pkg/logging/loghandler_windows.go @@ -5,13 +5,14 @@ package logging import ( "fmt" - "github.com/rs/zerolog" "io" "log" "os" "regexp" "strings" "time" + + "github.com/rs/zerolog" ) // We will use this package to wrap log messages coming from libraries who have no interest diff --git a/v2/pkg/plugins/glauth-mysql b/v2/pkg/plugins/glauth-mysql new file mode 160000 index 0000000..8379828 --- /dev/null +++ b/v2/pkg/plugins/glauth-mysql @@ -0,0 +1 @@ +Subproject commit 8379828f13fb0b31a41f3e5f6bf5bc6a3c8d6e1a diff --git a/v2/pkg/plugins/glauth-pam b/v2/pkg/plugins/glauth-pam new file mode 160000 index 0000000..0e6e424 --- /dev/null +++ b/v2/pkg/plugins/glauth-pam @@ -0,0 +1 @@ +Subproject commit 0e6e4240ef72ffc6d09b3aba9aa130b70ae13d92 diff --git a/v2/pkg/plugins/glauth-postgres b/v2/pkg/plugins/glauth-postgres new file mode 160000 index 0000000..61fa8ee --- /dev/null +++ b/v2/pkg/plugins/glauth-postgres @@ -0,0 +1 @@ +Subproject commit 61fa8ee872e932052fbdb0d7b5dd22e53310f8f3 diff --git a/v2/pkg/plugins/glauth-sqlite b/v2/pkg/plugins/glauth-sqlite new file mode 160000 index 0000000..e8b3b6a --- /dev/null +++ b/v2/pkg/plugins/glauth-sqlite @@ -0,0 +1 @@ +Subproject commit e8b3b6a8b0541d72dacfd4678273f7c86382ef00 diff --git a/v2/pkg/server/options.go b/v2/pkg/server/options.go index 9ce0f35..14af9a7 100644 --- a/v2/pkg/server/options.go +++ b/v2/pkg/server/options.go @@ -2,6 +2,7 @@ package server import ( "context" + "crypto/tls" "github.com/rs/zerolog" @@ -14,10 +15,11 @@ type Option func(o *Options) // Options defines the available options for this package. type Options struct { - Logger zerolog.Logger - Config *config.Config - Monitor monitoring.MonitorInterface - Context context.Context + Logger zerolog.Logger + Config *config.Config + Monitor monitoring.MonitorInterface + TLSConfig *tls.Config + Context context.Context } // newOptions initializes the available default options. @@ -52,6 +54,13 @@ func Context(val context.Context) Option { } } +// TLSConfig provides a function to set the TLS config option. +func TLSConfig(val *tls.Config) Option { + return func(o *Options) { + o.TLSConfig = val + } +} + // Monitor provides a function to set the monitor option. func Monitor(val monitoring.MonitorInterface) Option { return func(o *Options) { diff --git a/v2/pkg/server/server.go b/v2/pkg/server/server.go index a3e5f30..c53730b 100644 --- a/v2/pkg/server/server.go +++ b/v2/pkg/server/server.go @@ -90,6 +90,12 @@ func NewServer(opts ...Option) (*LdapSvc, error) { // configure the backends s.l = ldap.NewServer() s.l.EnforceLDAP = true + + if tlsConfig := options.TLSConfig; tlsConfig != nil { + s.l.TLSConfig = tlsConfig + s.log.Info().Interface("tls.certificates", tlsConfig.Certificates).Msg("enabling LDAP over TLS") + } + for i, backend := range s.c.Backends { var h handler.Handler switch backend.Datastore { diff --git a/v2/sample-simple.cfg b/v2/sample-simple.cfg index 07574e2..c3420c5 100644 --- a/v2/sample-simple.cfg +++ b/v2/sample-simple.cfg @@ -21,6 +21,9 @@ debug = true enabled = true # run on a non privileged port listen = "0.0.0.0:3893" + tls = false # enable StartTLS support + tlsCertPath = "glauth.crt" + tlsKeyPath = "glauth.key" [ldaps] # to enable ldaps generate a certificate, eg. with: