logger: add specific logs for failed attempts to initialize a connection

This should allow for better integration in tools like fail2ban.

Hopefully fix #59
This commit is contained in:
Nicola Murino 2019-11-11 15:20:00 +01:00
parent 191da1ecaf
commit 4ff34b3e53
5 changed files with 79 additions and 23 deletions

View file

@ -452,6 +452,15 @@ The logs can be divided into the following categories:
- `resp_size` integer. Size in bytes of the HTTP response
- `elapsed_ms` int64. Elapsed time, as milliseconds, to complete the request
- `request_id` string. Unique request identifier
- **"connection_failed"**, logs failed attempts to initialize a connection. A connection can fail for an authentication error or other errors such as a client abort or a time out if the login does not happen in two minutes
- `sender` string. `connection_failed`
- `level` string
- `username`, string. Can be empty if the client open the connection and don't try to login
- `host` string.
- `login_type` string. Can be `public_key`, `password` or `no_auth_tryed`
- `error` string. Optional error description
The `connection_failed` logs can be used for better integration in tools such as [Fail2ban](http://www.fail2ban.org/)
## Acknowledgements

View file

@ -19,7 +19,7 @@ import (
)
const (
dateFormat = "2006-01-02T15:04.05.000" // YYYY-MM-DDTHH:MM.SS.ZZZ
dateFormat = "2006-01-02T15:04:05.000" // YYYY-MM-DDTHH:MM:SS.ZZZ
)
// LogLevel defines log levels.
@ -61,7 +61,7 @@ func InitLogger(logFilePath string, logMaxSize int, logMaxBackups int, logMaxAge
lock: new(sync.Mutex)})
consoleLogger = zerolog.Nop()
}
logger = logger.With().Timestamp().Logger().Level(level)
logger.Level(level)
}
// DisableLogger disable the main logger.
@ -97,22 +97,22 @@ func Log(level LogLevel, sender string, connectionID string, format string, v ..
// Debug logs at debug level for the specified sender
func Debug(sender string, connectionID string, format string, v ...interface{}) {
logger.Debug().Str("sender", sender).Str("connection_id", connectionID).Msg(fmt.Sprintf(format, v...))
logger.Debug().Timestamp().Str("sender", sender).Str("connection_id", connectionID).Msg(fmt.Sprintf(format, v...))
}
// Info logs at info level for the specified sender
func Info(sender string, connectionID string, format string, v ...interface{}) {
logger.Info().Str("sender", sender).Str("connection_id", connectionID).Msg(fmt.Sprintf(format, v...))
logger.Info().Timestamp().Str("sender", sender).Str("connection_id", connectionID).Msg(fmt.Sprintf(format, v...))
}
// Warn logs at warn level for the specified sender
func Warn(sender string, connectionID string, format string, v ...interface{}) {
logger.Warn().Str("sender", sender).Str("connection_id", connectionID).Msg(fmt.Sprintf(format, v...))
logger.Warn().Timestamp().Str("sender", sender).Str("connection_id", connectionID).Msg(fmt.Sprintf(format, v...))
}
// Error logs at error level for the specified sender
func Error(sender string, connectionID string, format string, v ...interface{}) {
logger.Error().Str("sender", sender).Str("connection_id", connectionID).Msg(fmt.Sprintf(format, v...))
logger.Error().Timestamp().Str("sender", sender).Str("connection_id", connectionID).Msg(fmt.Sprintf(format, v...))
}
// DebugToConsole logs at debug level to stdout
@ -138,6 +138,7 @@ func ErrorToConsole(format string, v ...interface{}) {
// TransferLog logs an SFTP/SCP upload or download
func TransferLog(operation string, path string, elapsed int64, size int64, user string, connectionID string, protocol string) {
logger.Info().
Timestamp().
Str("sender", operation).
Int64("elapsed_ms", elapsed).
Int64("size_bytes", size).
@ -151,6 +152,7 @@ func TransferLog(operation string, path string, elapsed int64, size int64, user
// CommandLog logs an SFTP/SCP command
func CommandLog(command string, path string, target string, user string, connectionID string, protocol string) {
logger.Info().
Timestamp().
Str("sender", command).
Str("username", user).
Str("file_path", path).
@ -159,3 +161,18 @@ func CommandLog(command string, path string, target string, user string, connect
Str("protocol", protocol).
Msg("")
}
// ConnectionFailedLog logs failed attempts to initialize a connection.
// A connection can fail for an authentication error or other errors such as
// a client abort or a time out if the login does not happen in two minutes.
// These logs are useful for better integration with Fail2ban and similar tools.
func ConnectionFailedLog(user, ip, loginType, errorString string) {
logger.Debug().
Timestamp().
Str("sender", "connection_failed").
Str("host", ip).
Str("username", user).
Str("login_type", loginType).
Str("error", errorString).
Msg("")
}

View file

@ -56,17 +56,23 @@ func (l *StructuredLogger) NewLogEntry(r *http.Request) middleware.LogEntry {
// Write logs a new entry at the end of the HTTP request
func (l *StructuredLoggerEntry) Write(status, bytes int, elapsed time.Duration) {
metrics.HTTPRequestServed(status)
l.Logger.Info().Fields(l.fields).Int(
"resp_status", status).Int(
"resp_size", bytes).Int64(
"elapsed_ms", elapsed.Nanoseconds()/1000000).Str(
"sender", "httpd").Msg(
"")
l.Logger.Info().
Timestamp().
Str("sender", "httpd").
Fields(l.fields).
Int("resp_status", status).
Int("resp_size", bytes).
Int64("elapsed_ms", elapsed.Nanoseconds()/1000000).
Msg("")
}
// Panic logs panics
func (l *StructuredLoggerEntry) Panic(v interface{}, stack []byte) {
l.Logger.Error().Fields(l.fields).Str(
"stack", string(stack)).Str(
"panic", fmt.Sprintf("%+v", v)).Msg("")
l.Logger.Error().
Timestamp().
Str("sender", "httpd").
Fields(l.fields).
Str("stack", string(stack)).
Str("panic", fmt.Sprintf("%+v", v)).
Msg("")
}

View file

@ -7,7 +7,6 @@ import (
"encoding/hex"
"encoding/json"
"encoding/pem"
"errors"
"fmt"
"io"
"io/ioutil"
@ -87,6 +86,14 @@ type Key struct {
PrivateKey string `json:"private_key" mapstructure:"private_key"`
}
type authenticationError struct {
err string
}
func (e *authenticationError) Error() string {
return fmt.Sprintf("Authentication error: %s", e.err)
}
// Initialize the SFTP server and add a persistent listener to handle inbound SFTP connections.
func (c Configuration) Initialize(configDir string) error {
umask, err := strconv.ParseUint(c.Umask, 8, 8)
@ -102,7 +109,7 @@ func (c Configuration) Initialize(configDir string) error {
PasswordCallback: func(conn ssh.ConnMetadata, pass []byte) (*ssh.Permissions, error) {
sp, err := c.validatePasswordCredentials(conn, pass)
if err != nil {
return nil, errors.New("could not validate credentials")
return nil, &authenticationError{err: fmt.Sprintf("could not validate password credentials: %v", err)}
}
return sp, nil
@ -110,7 +117,7 @@ func (c Configuration) Initialize(configDir string) error {
PublicKeyCallback: func(conn ssh.ConnMetadata, pubKey ssh.PublicKey) (*ssh.Permissions, error) {
sp, err := c.validatePublicKeyCredentials(conn, string(pubKey.Marshal()))
if err != nil {
return nil, errors.New("could not validate credentials")
return nil, &authenticationError{err: fmt.Sprintf("could not validate public key credentials: %v", err)}
}
return sp, nil
@ -207,16 +214,18 @@ func (c Configuration) AcceptInboundConnection(conn net.Conn, config *ssh.Server
// Before beginning a handshake must be performed on the incoming net.Conn
// we'll set a Deadline for handshake to complete, the default is 2 minutes as OpenSSH
conn.SetDeadline(time.Now().Add(handshakeTimeout))
remoteAddr := conn.RemoteAddr()
sconn, chans, reqs, err := ssh.NewServerConn(conn, config)
if err != nil {
logger.Warn(logSender, "", "failed to accept an incoming connection: %v", err)
if _, ok := err.(*ssh.ServerAuthError); !ok {
logger.ConnectionFailedLog("", utils.GetIPFromRemoteAddress(remoteAddr.String()), "no_auth_tryed", err.Error())
}
return
}
// handshake completed so remove the deadline, we'll use IdleTimeout configuration from now on
conn.SetDeadline(time.Time{})
logger.Debug(logSender, "", "accepted inbound connection, ip: %v", conn.RemoteAddr().String())
var user dataprovider.User
var loginType string
@ -230,15 +239,15 @@ func (c Configuration) AcceptInboundConnection(conn net.Conn, config *ssh.Server
ID: connectionID,
User: user,
ClientVersion: string(sconn.ClientVersion()),
RemoteAddr: conn.RemoteAddr(),
RemoteAddr: remoteAddr,
StartTime: time.Now(),
lastActivity: time.Now(),
lock: new(sync.Mutex),
netConn: conn,
channel: nil,
}
connection.Log(logger.LevelInfo, logSender, "User id: %d, logged in with: %#v, username: %#v, home_dir: %#v",
user.ID, loginType, user.Username, user.HomeDir)
connection.Log(logger.LevelInfo, logSender, "User id: %d, logged in with: %#v, username: %#v, home_dir: %#v remote addr: %#v",
user.ID, loginType, user.Username, user.HomeDir, remoteAddr.String())
go ssh.DiscardRequests(reqs)
@ -385,6 +394,8 @@ func (c Configuration) validatePublicKeyCredentials(conn ssh.ConnMetadata, pubKe
metrics.AddLoginAttempt(true)
if user, keyID, err = dataprovider.CheckUserAndPubKey(dataProvider, conn.User(), pubKey); err == nil {
sshPerm, err = loginUser(user, "public_key:"+keyID)
} else {
logger.ConnectionFailedLog(conn.User(), utils.GetIPFromRemoteAddress(conn.RemoteAddr().String()), "public_key", err.Error())
}
metrics.AddLoginResult(true, err)
return sshPerm, err
@ -398,6 +409,8 @@ func (c Configuration) validatePasswordCredentials(conn ssh.ConnMetadata, pass [
metrics.AddLoginAttempt(false)
if user, err = dataprovider.CheckUserAndPass(dataProvider, conn.User(), string(pass)); err == nil {
sshPerm, err = loginUser(user, "password")
} else {
logger.ConnectionFailedLog(conn.User(), utils.GetIPFromRemoteAddress(conn.RemoteAddr().String()), "password", err.Error())
}
metrics.AddLoginResult(false, err)
return sshPerm, err

View file

@ -3,6 +3,7 @@ package utils
import (
"fmt"
"net"
"os"
"path/filepath"
"runtime"
@ -133,3 +134,13 @@ func byteCount(b int64, unit int64) string {
return fmt.Sprintf("%.1f %ciB",
float64(b)/float64(div), "KMGTPE"[exp])
}
// GetIPFromRemoteAddress returns the IP from the remote address.
// If the given remote address cannot be parsed it will be returned unchanged
func GetIPFromRemoteAddress(remoteAddress string) string {
ip, _, err := net.SplitHostPort(remoteAddress)
if err == nil {
return ip
}
return remoteAddress
}