From e8abd27f3c38d6b244520a12f3e35c38cc7e5619 Mon Sep 17 00:00:00 2001 From: Nathan Coad Date: Thu, 4 Jan 2024 11:42:04 +1100 Subject: [PATCH] initial work on adding LDAP integration --- README.md | 4 +- controllers/store_secrets.go | 2 +- go.mod | 12 +-- go.sum | 25 +++--- main.go | 5 ++ models/ldap.go | 146 +++++++++++++++++++++++++++++++++++ models/user.go | 7 +- test.env | 4 +- 8 files changed, 186 insertions(+), 19 deletions(-) create mode 100644 models/ldap.go diff --git a/README.md b/README.md index c28ef37..09a8148 100644 --- a/README.md +++ b/README.md @@ -29,7 +29,9 @@ Written by Nathan Coad (nathan.coad@dell.com) | LOG_FILE | Specify the name/path of file to write log messages to | /var/log/smt.log | ./smt.log | BIND_IP | Specify the local IP address to bind to. | 127.0.0.1 | Primary IPv4 address | | BIND_PORT | Specify the TCP/IP port to bind to. | 443 | 8443 | -| LDAP_BIND_ADDRESS | If LDAP integration is needed, specify the LDAP Bind address | ldaps://dc.domain.com:636 | No default specified | +| LDAP_BIND_ADDRESS | If LDAP integration is needed, specify the LDAP Bind address. Only LDAPS on port 636 is supported. Do not specify port 636 in the bind address | ldaps://dc.example.com | No default specified | +| LDAP_BASE_DN | If LDAP integration is needed, specify the base DN to use when binding to AD | "OU=Users,DC=example,DC=com" | No default specified | +| LDAP_TRUST_CERT_FILE | If LDAP integration is needed, specify filepath to PEM format public certificate of Certificate Authority signing LDAPS communications | caroot.pem | No default specified, must define this value | | TLS_KEY_FILE | Specify the filename of the TLS certificate private key (must be unencrypted) in PEM format | key.pem | privkey.pem | | TLS_CERT_FILE | Specify the filename of the TLS certificate file in PEM format | cert.pem | cert.pem | | TOKEN_HOUR_LIFESPAN | Number of hours that the JWT token returned at login is valid | 12 | No default specified, must define this value | diff --git a/controllers/store_secrets.go b/controllers/store_secrets.go index b2d9eb7..0c5e331 100644 --- a/controllers/store_secrets.go +++ b/controllers/store_secrets.go @@ -59,7 +59,7 @@ func StoreSecret(c *gin.Context) { if len(checkExists) > 0 { log.Printf("StoreSecret not storing secret with '%d' already matching secrets.\n", len(checkExists)) - c.JSON(http.StatusBadRequest, gin.H{"error": "StoreSecret attempting to store secret already defined. API calls for update/delete don't yet exist"}) + c.JSON(http.StatusBadRequest, gin.H{"error": "StoreSecret attempting to store secret already defined. Use update API call instead"}) return } diff --git a/go.mod b/go.mod index edc2e9b..f1fe987 100644 --- a/go.mod +++ b/go.mod @@ -5,9 +5,10 @@ go 1.19 require ( github.com/dgrijalva/jwt-go v3.2.0+incompatible github.com/gin-gonic/gin v1.9.0 + github.com/go-ldap/ldap v3.0.3+incompatible github.com/jmoiron/sqlx v1.3.5 github.com/joho/godotenv v1.5.1 - golang.org/x/crypto v0.7.0 + golang.org/x/crypto v0.13.0 modernc.org/sqlite v1.21.0 ) @@ -20,7 +21,7 @@ require ( github.com/go-playground/universal-translator v0.18.1 // indirect github.com/go-playground/validator/v10 v10.12.0 // indirect github.com/goccy/go-json v0.10.2 // indirect - github.com/google/uuid v1.3.0 // indirect + github.com/google/uuid v1.3.1 // indirect github.com/json-iterator/go v1.1.12 // indirect github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 // indirect github.com/klauspost/cpuid/v2 v2.2.4 // indirect @@ -34,11 +35,12 @@ require ( github.com/ugorji/go/codec v1.2.11 // indirect golang.org/x/arch v0.3.0 // indirect golang.org/x/mod v0.8.0 // indirect - golang.org/x/net v0.8.0 // indirect - golang.org/x/sys v0.6.0 // indirect - golang.org/x/text v0.8.0 // indirect + golang.org/x/net v0.10.0 // indirect + golang.org/x/sys v0.12.0 // indirect + golang.org/x/text v0.13.0 // indirect golang.org/x/tools v0.6.0 // indirect google.golang.org/protobuf v1.30.0 // indirect + gopkg.in/asn1-ber.v1 v1.0.0-20181015200546-f715ec2f112d // indirect gopkg.in/yaml.v3 v3.0.1 // indirect lukechampine.com/uint128 v1.2.0 // indirect modernc.org/cc/v3 v3.40.0 // indirect diff --git a/go.sum b/go.sum index 0d71680..9fed516 100644 --- a/go.sum +++ b/go.sum @@ -1,3 +1,5 @@ +github.com/Azure/go-ntlmssp v0.0.0-20221128193559-754e69321358 h1:mFRzDkZVAjdal+s7s0MwaRv9igoPqLRdzOLzw/8Xvq8= +github.com/Azure/go-ntlmssp v0.0.0-20221128193559-754e69321358/go.mod h1:chxPXzSsl7ZWRAuOIE23GDNzjWuZquvFlgA8xmpunjU= github.com/bytedance/sonic v1.5.0/go.mod h1:ED5hyg4y6t3/9Ku1R6dU/4KyJ48DZ4jPhfY1O2AihPM= github.com/bytedance/sonic v1.8.6 h1:aUgO9S8gvdN6SyW2EhIpAw5E4ChworywIEndZCkCVXk= github.com/bytedance/sonic v1.8.6/go.mod h1:i736AoUSYt75HyZLoJW9ERYxcy6eaN6h4BZXU064P/U= @@ -15,6 +17,8 @@ github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI= github.com/gin-gonic/gin v1.9.0 h1:OjyFBKICoexlu99ctXNR2gg+c5pKrKMuyjgARg9qeY8= github.com/gin-gonic/gin v1.9.0/go.mod h1:W1Me9+hsUSyj3CePGrd1/QrKJMSJ1Tu/0hFEH89961k= +github.com/go-ldap/ldap v3.0.3+incompatible h1:HTeSZO8hWMS1Rgb2Ziku6b8a7qRIZZMHjsvuZyatzwk= +github.com/go-ldap/ldap v3.0.3+incompatible/go.mod h1:qfd9rJvER9Q0/D/Sqn1DfHRoBp40uXYvFoEVrNEPqRc= github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s= github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= @@ -31,8 +35,8 @@ github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/ github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/pprof v0.0.0-20221118152302-e6195bd50e26 h1:Xim43kblpZXfIBQsbuBVKCudVG457BR2GZFIz3uw3hQ= -github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= -github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/google/uuid v1.3.1 h1:KjJaJ9iWZ3jOFZIf1Lqf4laDRCasjl0BCmnEGxkdLb4= +github.com/google/uuid v1.3.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/jmoiron/sqlx v1.3.5 h1:vFFPA71p1o5gAeqtEAwLU4dnX2napprKtHr7PYIcN3g= github.com/jmoiron/sqlx v1.3.5/go.mod h1:nRVWtLre0KfCLJvgxzCsLVMogSvQ1zNJtpYr2Ccp0mQ= github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= @@ -82,24 +86,27 @@ github.com/ugorji/go/codec v1.2.11/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZ golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8= golang.org/x/arch v0.3.0 h1:02VY4/ZcO/gBOH6PUaoiptASxtXU10jazRCP865E97k= golang.org/x/arch v0.3.0/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8= -golang.org/x/crypto v0.7.0 h1:AvwMYaRytfdeVt3u6mLaxYtErKYjxA2OXjJ1HHq6t3A= -golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU= +golang.org/x/crypto v0.13.0 h1:mvySKfSWJ+UKUii46M40LOvyWfN0s2U+46/jDd0e6Ck= +golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc= golang.org/x/mod v0.8.0 h1:LUYupSeNrTNCGzR/hVBk2NHZO4hXcVaW1k4Qx7rjPx8= golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= -golang.org/x/net v0.8.0 h1:Zrh2ngAOFYneWTAIAPethzeaQLuHwhuBkuV6ZiRnUaQ= -golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc= +golang.org/x/net v0.10.0 h1:X2//UzNDwYmtCLn7To6G58Wr6f5ahEAQgKNzv9Y951M= +golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= golang.org/x/sync v0.1.0 h1:wsuoTGHzEhffawBOhz5CYhcrV4IdKZbEyZjBMuTp12o= golang.org/x/sys v0.0.0-20220704084225-05e143d24a9e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.6.0 h1:MVltZSvRTcU2ljQOhs94SXPftV6DCNnZViHeQps87pQ= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/text v0.8.0 h1:57P1ETyNKtuIjB4SRd15iJxuhj8Gc416Y78H3qgMh68= -golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= +golang.org/x/sys v0.12.0 h1:CM0HF96J0hcLAwsHPJZjfdNzs0gftsLfgKt57wWHJ0o= +golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/text v0.13.0 h1:ablQoSUd0tRdKxZewP80B+BaqeKJuVhuRxj/dkrun3k= +golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= golang.org/x/tools v0.6.0 h1:BOw41kyTf3PuCW1pVQf8+Cyg8pMlkYB1oo9iJ6D/lKM= golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= google.golang.org/protobuf v1.30.0 h1:kPPoIgf3TsEvrm0PFe15JQ+570QVxYzEvvHqChK+cng= google.golang.org/protobuf v1.30.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= +gopkg.in/asn1-ber.v1 v1.0.0-20181015200546-f715ec2f112d h1:TxyelI5cVkbREznMhfzycHdkp5cLA7DpE+GKjSslYhM= +gopkg.in/asn1-ber.v1 v1.0.0-20181015200546-f715ec2f112d/go.mod h1:cuepJuh7vyXfUyUwEgHQXw849cJrilpS5NeIjOWESAw= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/main.go b/main.go index 2561236..a1c3b15 100644 --- a/main.go +++ b/main.go @@ -147,6 +147,9 @@ func main() { models.ReceiveKey(keyString) } + // Load certificate for LDAP connectivy + models.LoadLdapCert() + // Create context that listens for the interrupt signal from the OS. ctx, stop := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM) defer stop() @@ -265,6 +268,8 @@ func main() { protected.POST("/retrieveMultiple", controllers.RetrieveMultpleSecrets) protected.POST("/store", controllers.StoreSecret) protected.POST("/update", controllers.UpdateSecret) + // TODO + //protected.POST("/delete", controllers.DeleteSecret) // Support parameters in path // See https://gin-gonic.com/docs/examples/param-in-path/ diff --git a/models/ldap.go b/models/ldap.go new file mode 100644 index 0000000..28c927a --- /dev/null +++ b/models/ldap.go @@ -0,0 +1,146 @@ +package models + +import ( + "crypto/tls" + "crypto/x509" + "encoding/pem" + "fmt" + "os" + "path/filepath" + "strings" + + "github.com/go-ldap/ldap" +) + +// Code relating to AD integration + +type LdapConfig struct { + LdapBindAddress string + LdapBaseDn string + LdapCertFile string +} + +var systemCA *x509.CertPool +var certLoaded bool + +func GetFilePath(path string) string { + // Check for empty filename + if len(path) == 0 { + return "" + } + + // check if filename exists + if _, err := os.Stat(path); os.IsNotExist((err)) { + fmt.Printf("File '%s' not found, searching in same directory as binary\n", path) + // if not, check that it exists in the same directory as the currently executing binary + ex, err2 := os.Executable() + if err2 != nil { + //log.Printf("Error determining binary path : '%s'", err) + return "" + } + binaryPath := filepath.Dir(ex) + path = filepath.Join(binaryPath, path) + } + return path +} + +func LoadLdapCert() { + var err error + // Get a copy of the system defined CA's + systemCA, err = x509.SystemCertPool() + if err != nil { + fmt.Printf("LoadLdapCert error getting system certificate pool : '%s'\n", err) + return + } + + // only try to load certificate from file if the command line argument was specified + ldapCertFile := os.Getenv("LDAP_TRUST_CERT_FILE") + if ldapCertFile != "" { + // Try to read the file + cf, err := os.ReadFile(GetFilePath(ldapCertFile)) + if err != nil { + fmt.Printf("LoadLdapCert error opening LDAP certificate file '%s' : '%s'\n", ldapCertFile, err) + return + } + + // Get the certificate from the file + cpb, _ := pem.Decode(cf) + crt, err := x509.ParseCertificate(cpb.Bytes) + //fmt.Printf("Loaded certificate with subject %s\n", crt.Subject) + + if err != nil { + fmt.Printf("LoadLdapCert error processing LDAP certificate file '%s' : '%s'\n", ldapCertFile, err) + return + } + + // Add custom certificate to the system cert pool + systemCA.AddCert(crt) + + certLoaded = true + } +} + +func VerifyLdapCreds(username string, password string) bool { + var ldaps *ldap.Conn + var err error + ldapServer := os.Getenv("LDAP_BIND_ADDRESS") + if ldapServer == "" { + fmt.Printf("VerifyLdapCreds no LDAP bind address supplied\n") + return false + } + + ldapBaseDn := os.Getenv("LDAP_BASE_DN") + if ldapBaseDn == "" { + fmt.Printf("VerifyLdapCreds no LDAP base DN supplied\n") + return false + } + + // Set up TLS to use our custom certificate authority passed in cli argument + tlsConfig := &tls.Config{ + RootCAs: systemCA, + InsecureSkipVerify: true, + } + + // try connecting to AD via TLS and our custom certificate authority + // Add port if not specified in .env file + if strings.HasSuffix(ldapServer, ":636") { + ldaps, err = ldap.DialTLS("tcp", ldapServer, tlsConfig) + } else { + ldaps, err = ldap.DialTLS("tcp", fmt.Sprintf("%s:636", ldapServer), tlsConfig) + } + + if err != nil { + fmt.Printf("VerifyLdapCreds error connecting to LDAP bind address '%s' : '%s'\n", ldapServer, err) + return false + } + + defer ldaps.Close() + + // try to bind to AD + err = ldaps.Bind(username, password) + if err != nil { + fmt.Printf("VerifyLdapCreds error binding to LDAP with supplied credentials : '%s'\n", err) + return false + } + + searchReq := ldap.NewSearchRequest( + ldapBaseDn, + ldap.ScopeBaseObject, // you can also use ldap.ScopeWholeSubtree + ldap.NeverDerefAliases, + 0, + 0, + false, + "(objectClass=*)", + []string{}, + nil, + ) + result, err := ldaps.Search(searchReq) + if err != nil { + fmt.Printf("VerifyLdapCreds search error : '%s'\n", err) + return false + } + + fmt.Printf("result: %v\n", result) + + return true +} diff --git a/models/user.go b/models/user.go index d9ff116..4e3de73 100644 --- a/models/user.go +++ b/models/user.go @@ -88,12 +88,15 @@ func LoginCheck(username string, password string) (string, error) { // Query database for matching user object err = db.QueryRowx("SELECT * FROM Users WHERE Username=?", username).StructScan(&u) - log.Printf("LoginCheck retrieved user '%v' from database\n", u) - if err != nil { return "", err + } else { + log.Printf("LoginCheck retrieved user '%v' from database\n", u) } + // TODO : attempt ldap bind + VerifyLdapCreds(username, password) + err = VerifyPassword(password, u.Password) if err != nil && err == bcrypt.ErrMismatchedHashAndPassword { diff --git a/test.env b/test.env index b794f2b..cd9d75a 100644 --- a/test.env +++ b/test.env @@ -4,6 +4,8 @@ INITIAL_PASSWORD=Password123 TOKEN_HOUR_LIFESPAN=168 BIND_IP= BIND_PORT=8443 -LDAP_BIND_ADDRESS= +LDAP_BIND_ADDRESS=ldaps://dc.lab.local +LDAP_BASE_DN=OU=Users,DC=lab,DC=local +LDAP_TRUST_CERT_FILE= TLS_KEY_FILE=key.pem TLS_CERT_FILE=cert.pem \ No newline at end of file