initial work on adding LDAP integration
All checks were successful
continuous-integration/drone/push Build is passing

This commit is contained in:
2024-01-04 11:42:04 +11:00
parent 0619b497f7
commit e8abd27f3c
8 changed files with 186 additions and 19 deletions

View File

@@ -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 |

View File

@@ -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
}

12
go.mod
View File

@@ -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

25
go.sum
View File

@@ -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=

View File

@@ -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/

146
models/ldap.go Normal file
View File

@@ -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
}

View File

@@ -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 {

View File

@@ -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