WebDAV: improve TLS certificate authentication

For each user you can now configure:

- TLS certificate auth
- TLS certificate auth and password
- Password auth

For TLS certificate auth, the certificate common name is used as
username
This commit is contained in:
Nicola Murino
2021-03-01 19:28:11 +01:00
parent 901cafc6da
commit 534b253c20
19 changed files with 816 additions and 151 deletions

View File

@@ -14,7 +14,7 @@ import (
"strings"
"time"
"github.com/go-chi/chi/middleware"
"github.com/go-chi/chi/v5/middleware"
"github.com/rs/cors"
"github.com/rs/xid"
"golang.org/x/net/webdav"
@@ -69,10 +69,15 @@ func (s *webDavServer) listenAndServe(compressor *middleware.Compressor) error {
}
logger.Debug(logSender, "", "configured TLS cipher suites for binding %#v: %v", s.binding.GetAddress(),
httpServer.TLSConfig.CipherSuites)
if s.binding.ClientAuthType == 1 {
if s.binding.isMutualTLSEnabled() {
httpServer.TLSConfig.ClientCAs = certMgr.GetRootCAs()
httpServer.TLSConfig.ClientAuth = tls.RequireAndVerifyClientCert
httpServer.TLSConfig.VerifyConnection = s.verifyTLSConnection
switch s.binding.ClientAuthType {
case 1:
httpServer.TLSConfig.ClientAuth = tls.RequireAndVerifyClientCert
case 2:
httpServer.TLSConfig.ClientAuth = tls.VerifyClientCertIfGiven
}
}
logger.Info(logSender, "", "starting HTTPS serving, binding: %v", s.binding.GetAddress())
return httpServer.ListenAndServeTLS("", "")
@@ -92,6 +97,9 @@ func (s *webDavServer) verifyTLSConnection(state tls.ConnectionState) error {
clientCrtName = clientCrt.Subject.String()
}
if len(state.VerifiedChains) == 0 {
if s.binding.ClientAuthType == 2 {
return nil
}
logger.Warn(logSender, "", "TLS connection cannot be verified: unable to get verification chain")
return errors.New("TLS connection cannot be verified: unable to get verification chain")
}
@@ -152,28 +160,28 @@ func (s *webDavServer) ServeHTTP(w http.ResponseWriter, r *http.Request) {
http.Error(w, common.ErrConnectionDenied.Error(), http.StatusForbidden)
return
}
user, _, lockSystem, err := s.authenticate(r, ipAddr)
user, _, lockSystem, loginMethod, err := s.authenticate(r, ipAddr)
if err != nil {
w.Header().Set("WWW-Authenticate", "Basic realm=\"SFTPGo WebDAV\"")
http.Error(w, err401.Error(), http.StatusUnauthorized)
http.Error(w, fmt.Sprintf("Authentication error: %v", err), http.StatusUnauthorized)
return
}
connectionID, err := s.validateUser(&user, r)
connectionID, err := s.validateUser(&user, r, loginMethod)
if err != nil {
updateLoginMetrics(&user, ipAddr, err)
updateLoginMetrics(&user, ipAddr, loginMethod, err)
http.Error(w, err.Error(), http.StatusForbidden)
return
}
fs, err := user.GetFilesystem(connectionID)
if err != nil {
updateLoginMetrics(&user, ipAddr, err)
updateLoginMetrics(&user, ipAddr, loginMethod, err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
updateLoginMetrics(&user, ipAddr, err)
updateLoginMetrics(&user, ipAddr, loginMethod, err)
ctx := context.WithValue(r.Context(), requestIDKey, connectionID)
ctx = context.WithValue(ctx, requestStartKey, time.Now())
@@ -202,12 +210,32 @@ func (s *webDavServer) ServeHTTP(w http.ResponseWriter, r *http.Request) {
handler.ServeHTTP(w, r.WithContext(ctx))
}
func (s *webDavServer) authenticate(r *http.Request, ip string) (dataprovider.User, bool, webdav.LockSystem, error) {
func (s *webDavServer) getCredentialsAndLoginMethod(r *http.Request) (string, string, string, *x509.Certificate, bool) {
var tlsCert *x509.Certificate
loginMethod := dataprovider.LoginMethodPassword
username, password, ok := r.BasicAuth()
if s.binding.isMutualTLSEnabled() && r.TLS != nil {
if len(r.TLS.PeerCertificates) > 0 {
tlsCert = r.TLS.PeerCertificates[0]
if ok {
loginMethod = dataprovider.LoginMethodTLSCertificateAndPwd
} else {
loginMethod = dataprovider.LoginMethodTLSCertificate
username = tlsCert.Subject.CommonName
password = ""
}
ok = true
}
}
return username, password, loginMethod, tlsCert, ok
}
func (s *webDavServer) authenticate(r *http.Request, ip string) (dataprovider.User, bool, webdav.LockSystem, string, error) {
var user dataprovider.User
var err error
username, password, ok := r.BasicAuth()
username, password, loginMethod, tlsCert, ok := s.getCredentialsAndLoginMethod(r)
if !ok {
return user, false, nil, err401
return user, false, nil, loginMethod, err401
}
result, ok := dataprovider.GetCachedWebDAVUser(username)
if ok {
@@ -215,43 +243,47 @@ func (s *webDavServer) authenticate(r *http.Request, ip string) (dataprovider.Us
if cachedUser.IsExpired() {
dataprovider.RemoveCachedWebDAVUser(username)
} else {
if password != "" && cachedUser.Password == password {
return cachedUser.User, true, cachedUser.LockSystem, nil
if !cachedUser.User.IsTLSUsernameVerificationEnabled() {
// for backward compatibility with 2.0.x we only check the password
tlsCert = nil
loginMethod = dataprovider.LoginMethodPassword
}
updateLoginMetrics(&cachedUser.User, ip, dataprovider.ErrInvalidCredentials)
return user, false, nil, dataprovider.ErrInvalidCredentials
if err := dataprovider.CheckCachedUserCredentials(cachedUser, password, loginMethod, common.ProtocolWebDAV, tlsCert); err == nil {
return cachedUser.User, true, cachedUser.LockSystem, loginMethod, nil
}
updateLoginMetrics(&cachedUser.User, ip, loginMethod, dataprovider.ErrInvalidCredentials)
return user, false, nil, loginMethod, dataprovider.ErrInvalidCredentials
}
}
user, err = dataprovider.CheckUserAndPass(username, password, ip, common.ProtocolWebDAV)
user, loginMethod, err = dataprovider.CheckCompositeCredentials(username, password, ip, loginMethod,
common.ProtocolWebDAV, tlsCert)
if err != nil {
user.Username = username
updateLoginMetrics(&user, ip, err)
return user, false, nil, err
updateLoginMetrics(&user, ip, loginMethod, err)
return user, false, nil, loginMethod, err
}
lockSystem := webdav.NewMemLS()
if password != "" {
cachedUser := &dataprovider.CachedUser{
User: user,
Password: password,
LockSystem: lockSystem,
}
if s.config.Cache.Users.ExpirationTime > 0 {
cachedUser.Expiration = time.Now().Add(time.Duration(s.config.Cache.Users.ExpirationTime) * time.Minute)
}
dataprovider.CacheWebDAVUser(cachedUser, s.config.Cache.Users.MaxSize)
if user.FsConfig.Provider != dataprovider.SFTPFilesystemProvider {
// for sftp fs check root path does nothing so don't open a useless SFTP connection
tempFs, err := user.GetFilesystem("temp")
if err == nil {
tempFs.CheckRootPath(user.Username, user.UID, user.GID)
tempFs.Close()
}
cachedUser := &dataprovider.CachedUser{
User: user,
Password: password,
LockSystem: lockSystem,
}
if s.config.Cache.Users.ExpirationTime > 0 {
cachedUser.Expiration = time.Now().Add(time.Duration(s.config.Cache.Users.ExpirationTime) * time.Minute)
}
dataprovider.CacheWebDAVUser(cachedUser, s.config.Cache.Users.MaxSize)
if user.FsConfig.Provider != dataprovider.SFTPFilesystemProvider {
// for sftp fs check root path does nothing so don't open a useless SFTP connection
tempFs, err := user.GetFilesystem("temp")
if err == nil {
tempFs.CheckRootPath(user.Username, user.UID, user.GID)
tempFs.Close()
}
}
return user, false, lockSystem, nil
return user, false, lockSystem, loginMethod, nil
}
func (s *webDavServer) validateUser(user *dataprovider.User, r *http.Request) (string, error) {
func (s *webDavServer) validateUser(user *dataprovider.User, r *http.Request, loginMethod string) (string, error) {
connID := xid.New().String()
connectionID := fmt.Sprintf("%v_%v", common.ProtocolWebDAV, connID)
@@ -264,9 +296,9 @@ func (s *webDavServer) validateUser(user *dataprovider.User, r *http.Request) (s
logger.Debug(logSender, connectionID, "cannot login user %#v, protocol DAV is not allowed", user.Username)
return connID, fmt.Errorf("Protocol DAV is not allowed for user %#v", user.Username)
}
if !user.IsLoginMethodAllowed(dataprovider.LoginMethodPassword, nil) {
logger.Debug(logSender, connectionID, "cannot login user %#v, password login method is not allowed", user.Username)
return connID, fmt.Errorf("Password login method is not allowed for user %#v", user.Username)
if !user.IsLoginMethodAllowed(loginMethod, nil) {
logger.Debug(logSender, connectionID, "cannot login user %#v, %v login method is not allowed", user.Username, loginMethod)
return connID, fmt.Errorf("Login method %v is not allowed for user %#v", loginMethod, user.Username)
}
if user.MaxSessions > 0 {
activeSessions := common.Connections.GetActiveSessions(user.Username)
@@ -335,16 +367,16 @@ func checkRemoteAddress(r *http.Request) {
}
}
func updateLoginMetrics(user *dataprovider.User, ip string, err error) {
metrics.AddLoginAttempt(dataprovider.LoginMethodPassword)
func updateLoginMetrics(user *dataprovider.User, ip, loginMethod string, err error) {
metrics.AddLoginAttempt(loginMethod)
if err != nil {
logger.ConnectionFailedLog(user.Username, ip, dataprovider.LoginMethodPassword, common.ProtocolWebDAV, err.Error())
logger.ConnectionFailedLog(user.Username, ip, loginMethod, common.ProtocolWebDAV, err.Error())
event := common.HostEventLoginFailed
if _, ok := err.(*dataprovider.RecordNotFoundError); ok {
event = common.HostEventUserNotFound
}
common.AddDefenderEvent(ip, event)
}
metrics.AddLoginResult(dataprovider.LoginMethodPassword, err)
dataprovider.ExecutePostLoginHook(user, dataprovider.LoginMethodPassword, ip, common.ProtocolWebDAV, err)
metrics.AddLoginResult(loginMethod, err)
dataprovider.ExecutePostLoginHook(user, loginMethod, ip, common.ProtocolWebDAV, err)
}