optimisations

* use pyke's re cache
* get an unlimited number of ldap attributes
* get a perturbator for the OTP secret, in case of stolen phone
* lowercase the username, to avoid strange behaviour with the OTP
This commit is contained in:
Xavier Henner 2019-07-12 22:33:22 +02:00
parent 3d1801ee50
commit 24544a6260
7 changed files with 96 additions and 84 deletions

View File

@ -115,7 +115,7 @@ func (h *HttpServer) ajaxHandler(w http.ResponseWriter, r *http.Request) {
return
}
profile, _ := h.ovpn.AuthLoop(h.minProfile,
profile, _, _ := h.ovpn.AuthLoop(h.minProfile,
strings.Replace(r.TLS.PeerCertificates[0].Subject.CommonName, " ", "", -1), "", false)
if profile != h.neededProfile {
http.Error(w, fmt.Sprintf("You need the %s profile", h.neededProfile), 403)

102
ldap.go
View File

@ -6,28 +6,27 @@ import (
"fmt"
"log"
"net"
"regexp"
"strings"
"time"
"github.com/pyke369/golang-support/rcache"
"gopkg.in/ldap.v2"
)
type ldapConfig struct {
servers []string
baseDN string
bindCn string
bindPw string
searchFilter string
primaryAttribute string
secondaryAttribute string
validGroups []string
mfaType string
certAuth string
ipMin net.IP
ipMax net.IP
upgradeFrom string
routes []string
servers []string
baseDN string
bindCn string
bindPw string
searchFilter string
attributes []string
validGroups []string
mfaType string
certAuth string
ipMin net.IP
ipMax net.IP
upgradeFrom string
routes []string
}
func (l *ldapConfig) addIPRange(s string) error {
@ -46,24 +45,28 @@ func (l *ldapConfig) addIPRange(s string) error {
// auth loop. Try all auth profiles from startProfile
// return the last possible profile and the mail if we found a mail like login
func (s *OpenVpnMgt) AuthLoop(startProfile, user, pass string, overridePwdCheck bool) (string, string) {
func (s *OpenVpnMgt) AuthLoop(startProfile, user, pass string, overridePwdCheck bool) (string, string, string) {
login := []string{user}
profile := startProfile
mail := ""
otpSalt := ""
re := regexp.MustCompile("^[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+.[a-zA-Z0-9-.]+$")
re := rcache.Get("^[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+.[a-zA-Z0-9-.]+$")
for {
if re.MatchString(login[0]) && mail == "" {
if mail == "" && re.MatchString(login[0]) {
mail = login[0]
}
n := profile
for k, ldap := range s.ldap {
if ldap.upgradeFrom != profile {
continue
}
err, userOk, passOk, secondary := ldap.Auth(login, pass)
err, userOk, passOk, attributes := ldap.Auth(login, pass)
if otpSalt == "" && len(attributes) > 1 && len(attributes[1]) > 0 {
otpSalt = attributes[1][0]
}
// if there is an error, try the other configurations
if err != nil {
@ -74,7 +77,7 @@ func (s *OpenVpnMgt) AuthLoop(startProfile, user, pass string, overridePwdCheck
// we did find a valid User
if userOk {
// the login for the new auth level is given by the current one
login = secondary
login = attributes[0]
if passOk && profile != "" {
// it's at least the second auth level, and we have a valid
@ -96,7 +99,7 @@ func (s *OpenVpnMgt) AuthLoop(startProfile, user, pass string, overridePwdCheck
}
}
return profile, mail
return profile, mail, otpSalt
}
// override the real DialTLS function
@ -116,13 +119,11 @@ func myDialTLS(network, addr string, config *tls.Config) (*ldap.Conn, error) {
return conn, nil
}
func (conf *ldapConfig) Auth(logins []string, pass string) (e error, userOk, passOk bool, attributes []string) {
var primary, secondary []string
func (conf *ldapConfig) Auth(logins []string, pass string) (e error, userOk, passOk bool, attributes [][]string) {
// special case. This configuration is a filter on the previous one
if len(conf.servers) == 0 && len(conf.validGroups) > 0 {
if inArray(logins, conf.validGroups) {
return nil, true, false, logins
return nil, true, false, [][]string{logins}
}
}
@ -131,7 +132,7 @@ func (conf *ldapConfig) Auth(logins []string, pass string) (e error, userOk, pas
return nil, false, false, nil
}
attributes = logins
attributes = [][]string{logins}
for _, s := range conf.servers {
// we force ldaps because we can
l, err := myDialTLS("tcp", s+":636", &tls.Config{ServerName: s})
@ -147,10 +148,7 @@ func (conf *ldapConfig) Auth(logins []string, pass string) (e error, userOk, pas
return err, false, false, nil
}
search := []string{"dn", conf.primaryAttribute}
if conf.secondaryAttribute != "" {
search = append(search, conf.secondaryAttribute)
}
search := append(conf.attributes, "dn")
// search the user
searchRequest := ldap.NewSearchRequest(
@ -172,39 +170,51 @@ func (conf *ldapConfig) Auth(logins []string, pass string) (e error, userOk, pas
// check the attributes requested in the search
// a valid account must be part of the correct group (per instance)
for _, attribute := range sr.Entries[0].Attributes {
if (*attribute).Name == conf.primaryAttribute {
primary = attribute.Values
ret := [][]string{}
for _, needed := range conf.attributes {
ok := false
for _, attribute := range sr.Entries[0].Attributes {
if (*attribute).Name == needed {
ret = append(ret, attribute.Values)
ok = true
}
}
if (*attribute).Name == conf.secondaryAttribute {
secondary = attribute.Values
if !ok {
ret = append(ret, []string{})
}
}
// user must have both primary and secondary attributes
if len(primary) == 0 {
log.Printf("User %s has no %s attribute", logins[0], conf.primaryAttribute)
// user must have both the first and second attributes
if len(ret[0]) == 0 {
log.Printf("User %s has no %s attribute", logins[0], conf.attributes[0])
return nil, false, false, nil
}
if len(secondary) == 0 {
log.Printf("User %s has no %s attribute", logins[0], conf.secondaryAttribute)
if len(ret[1]) == 0 {
log.Printf("User %s has no %s attribute", logins[0], conf.attributes[1])
return nil, false, false, nil
}
// check if the primary attributes are in the validGroups list
if len(conf.validGroups) > 0 && !inArray(conf.validGroups, primary) {
// check if the first attribute valus are in the validGroups list
if len(conf.validGroups) > 0 && !inArray(conf.validGroups, ret[0]) {
return nil, false, false, nil
}
// if there is no validGroups check, pass the primary attributes to the
// if there is no validGroups check, pass the first attribute values to the
// next level
if len(conf.validGroups) == 0 {
attributes = primary
attributes = [][]string{ret[0]}
} else {
attributes = secondary
attributes = [][]string{ret[1]}
}
if len(ret) > 2 {
attributes = append(attributes, ret[2:]...)
}
log.Println(attributes)
log.Printf("User %s has a valid account on %s", logins[0], s)
userdn := sr.Entries[0].DN

28
main.go
View File

@ -64,21 +64,25 @@ func main() {
for _, profile := range config.GetPaths("config.profiles") {
profileName := strings.Split(profile, ".")[2]
ldapConf := ldapConfig{
servers: parseConfigArray(config, profile+".servers"),
baseDN: config.GetString(profile+".baseDN", ""),
bindCn: config.GetString(profile+".bindCn", ""),
bindPw: config.GetString(profile+".bindPw", ""),
searchFilter: config.GetString(profile+".searchFilter", ""),
primaryAttribute: config.GetString(profile+".primaryAttribute", ""),
secondaryAttribute: config.GetString(profile+".secondaryAttribute", ""),
validGroups: parseConfigArray(config, profile+".validGroups"),
routes: parseConfigArray(config, profile+".routes"),
mfaType: config.GetString(profile+".mfa", ""),
certAuth: config.GetString(profile+".cert", "optionnal"),
upgradeFrom: config.GetString(profile+".upgradeFrom", ""),
servers: parseConfigArray(config, profile+".servers"),
baseDN: config.GetString(profile+".baseDN", ""),
bindCn: config.GetString(profile+".bindCn", ""),
bindPw: config.GetString(profile+".bindPw", ""),
searchFilter: config.GetString(profile+".searchFilter", ""),
attributes: parseConfigArray(config, profile+".attributes"),
validGroups: parseConfigArray(config, profile+".validGroups"),
routes: parseConfigArray(config, profile+".routes"),
mfaType: config.GetString(profile+".mfa", ""),
certAuth: config.GetString(profile+".cert", "optionnal"),
upgradeFrom: config.GetString(profile+".upgradeFrom", ""),
}
ldapConf.addIPRange(config.GetString(profile+".IPRange", ""))
if len(ldapConf.servers) > 0 && len(ldapConf.attributes) < 2 {
log.Println("valud ldap configuration must have 2 attributes")
os.Exit(1)
}
server.ldap[profileName] = ldapConf
}

View File

@ -9,8 +9,7 @@ config
bindCn: "CN=VPN Service,OU=Services,OU=Dailymotion,DC=office,DC=daily",
bindPw: "********************",
searchFilter: "(&(sAMAccountName=%s))"
primaryAttribute: "memberOf"
secondaryAttribute: "mail"
attributes: [ "memberOf", "mail" ]
validGroups:
[
"CN=SEC_VPN_Users_External,OU=Security,OU=Groups,OU=Dailymotion,DC=office,DC=daily",
@ -39,8 +38,7 @@ config
bindCn: "CN=VPN Service,OU=Services,OU=Dailymotion,DC=office,DC=daily",
bindPw: "********************",
searchFilter: "(&(sAMAccountName=%s))"
primaryAttribute: "memberOf"
secondaryAttribute: "mail"
attributes: [ "memberOf", "mail" ]
validGroups:
[
"CN=SEC_VPN,OU=Security,OU=Groups,OU=Dailymotion,DC=office,DC=daily",
@ -56,8 +54,7 @@ config
bindCn: "cn=readonly,dc=dailymotion,dc=com"
bindPw: "**********"
searchFilter: "(&(mail=%s))"
primaryAttribute: "description"
secondaryAttribute: "sshPublicKey"
attributes: [ "description", "sshPublicKey" ]
upgradeFrom: "CORP"
mfa: ""
cert: "optionnal"
@ -67,7 +64,7 @@ config
{
validGroups:
[
"infra2",
"infra",
"net",
"datacenter",
]

2
vendor/modules.txt vendored
View File

@ -1,8 +1,8 @@
# github.com/mattevans/pwned-passwords v0.0.0-20190611210716-1da592be4a34
github.com/mattevans/pwned-passwords
# github.com/pyke369/golang-support v0.0.0-20190703174728-34ca97aa79e9
github.com/pyke369/golang-support/uconfig
github.com/pyke369/golang-support/rcache
github.com/pyke369/golang-support/uconfig
# gopkg.in/asn1-ber.v1 v1.0.0-20181015200546-f715ec2f112d
gopkg.in/asn1-ber.v1
# gopkg.in/ldap.v2 v2.5.1

View File

@ -8,12 +8,12 @@ import (
"log"
"net"
"os"
"regexp"
"strconv"
"strings"
"sync"
hibp "github.com/mattevans/pwned-passwords"
"github.com/pyke369/golang-support/rcache"
)
// Server represents the server
@ -136,7 +136,7 @@ func (s *OpenVpnMgt) Kill(session string, id int) error {
// send the help command on all vpn servers. Kind of useless
func (s *OpenVpnMgt) Help() (error, map[string]map[string]string) {
ret := make(map[string]map[string]string)
re := regexp.MustCompile("^(.*[^ ]) *: (.*)$")
re := rcache.Get("^(.*[^ ]) *: (.*)$")
for remote := range s.buf {
help := make(map[string]string)
err, msg := s.sendCommand([]string{"help"}, remote)
@ -247,7 +247,7 @@ func (s *OpenVpnMgt) ClientReAuth(line, remote string) {
// find a client among all registered sessions
func (s *OpenVpnMgt) getClient(line, remote string) (error, *vpnSession) {
re := regexp.MustCompile("^[^0-9]*,([0-9]+)[^0-9]*")
re := rcache.Get("^[^0-9]*,([0-9]+)[^0-9]*")
match := re.FindStringSubmatch(line)
if len(match) == 0 {
return errors.New("invalid message"), nil

View File

@ -5,9 +5,9 @@ import (
"encoding/json"
"errors"
"fmt"
"github.com/pyke369/golang-support/rcache"
"os"
"os/exec"
"regexp"
"strconv"
"strings"
"time"
@ -84,7 +84,7 @@ func (c *vpnSession) AddRoute(ip string) error {
func (c *vpnSession) ParseSessionId(line string) error {
var err error
re := regexp.MustCompile("^>CLIENT:[^,]*,([0-9]+),([0-9]+)$")
re := rcache.Get("^>CLIENT:[^,]*,([0-9]+),([0-9]+)$")
match := re.FindStringSubmatch(line)
if len(match) == 0 {
return errors.New("invalid message")
@ -101,8 +101,8 @@ func (c *vpnSession) ParseSessionId(line string) error {
func (c *vpnSession) ParseEnv(s *OpenVpnMgt, infos *[]string) error {
var err error
r := regexp.MustCompile("[^a-zA-Z0-9./_@-]")
renv := regexp.MustCompile("^>CLIENT:ENV,([^=]*)=(.*)$")
r := rcache.Get("[^a-zA-Z0-9./_@-]")
renv := rcache.Get("^>CLIENT:ENV,([^=]*)=(.*)$")
for _, line := range *infos {
p := renv.FindStringSubmatch(line)
if len(p) != 3 {
@ -182,7 +182,7 @@ func (c *vpnSession) ParseEnv(s *OpenVpnMgt, infos *[]string) error {
}
case "username":
c.Login = r.ReplaceAllString(p[2], "")
c.Login = strings.ToLower(r.ReplaceAllString(p[2], ""))
case "dev":
c.dev = r.ReplaceAllString(p[2], "")
case "ifconfig_netmask":
@ -260,11 +260,20 @@ func (c *vpnSession) auth(s *OpenVpnMgt) (error, int) {
c.password = ""
}
otpSalt := ""
c.Profile, c.Mail, otpSalt = s.AuthLoop("", c.Login, c.password, tokenPasswordOk)
// no profile validated, we stop here
if c.Profile == "" {
c.Status = "fail (password)"
return errors.New("Authentication Failed"), -3
}
// if the otp is not empty, we check it against the valid codes as soon as
// possible
otpvalidated := false
if c.otpCode != "" {
codes, err := s.GenerateOTP(c.Login)
codes, err := s.GenerateOTP(c.Login + otpSalt)
if err != nil {
return err, -2
}
@ -275,14 +284,6 @@ func (c *vpnSession) auth(s *OpenVpnMgt) (error, int) {
}
}
c.Profile, c.Mail = s.AuthLoop("", c.Login, c.password, tokenPasswordOk)
// no profile validated, we stop here
if c.Profile == "" {
c.Status = "fail (password)"
return errors.New("Authentication Failed"), -3
}
// check the MFA requested by the secured profile
c.TwoFA = true
switch s.ldap[c.Profile].mfaType {