108 lines
2.8 KiB
Go
108 lines
2.8 KiB
Go
|
package main
|
||
|
|
||
|
import (
|
||
|
"log/slog"
|
||
|
"net"
|
||
|
"strings"
|
||
|
|
||
|
"github.com/alexedwards/argon2id"
|
||
|
"github.com/gliderlabs/ssh"
|
||
|
"go.elara.ws/seashell/internal/config"
|
||
|
"go.elara.ws/seashell/internal/fail2ban"
|
||
|
"go.elara.ws/seashell/internal/sshctx"
|
||
|
)
|
||
|
|
||
|
// passwordHandler returns a handler that checks password authentication attempts against
|
||
|
// fail2ban and the configured argon2id password hash.
|
||
|
func passwordHandler(f2b *fail2ban.Fail2Ban, cfg config.Config) ssh.PasswordHandler {
|
||
|
return func(ctx ssh.Context, password string) (ok bool) {
|
||
|
if !f2b.LoginAllowed(ctx.RemoteAddr()) {
|
||
|
log.Warn(
|
||
|
"Login attempt blocked by fail2ban policy",
|
||
|
slog.String("username", ctx.User()),
|
||
|
slog.String("addr", ctx.RemoteAddr().String()),
|
||
|
)
|
||
|
return false
|
||
|
}
|
||
|
|
||
|
user, ok := getUser(ctx, cfg)
|
||
|
if !ok {
|
||
|
return false
|
||
|
}
|
||
|
|
||
|
ok, err := argon2id.ComparePasswordAndHash(password, user.Password)
|
||
|
return err == nil && ok
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// pubkeyHandler returns a handler that checks public key authentication attempts against
|
||
|
// fail2ban and the configures authorized public keys.
|
||
|
func pubkeyHandler(f2b *fail2ban.Fail2Ban, cfg config.Config) ssh.PublicKeyHandler {
|
||
|
return func(ctx ssh.Context, key ssh.PublicKey) (ok bool) {
|
||
|
if !f2b.LoginAllowed(ctx.RemoteAddr()) {
|
||
|
log.Warn(
|
||
|
"Login attempt blocked by fail2ban policy",
|
||
|
slog.String("username", ctx.User()),
|
||
|
slog.String("addr", ctx.RemoteAddr().String()),
|
||
|
)
|
||
|
return false
|
||
|
}
|
||
|
|
||
|
user, ok := getUser(ctx, cfg)
|
||
|
if !ok {
|
||
|
return false
|
||
|
}
|
||
|
|
||
|
for i, pubkeyStr := range user.Pubkeys {
|
||
|
pubkey, _, _, _, err := ssh.ParseAuthorizedKey([]byte(pubkeyStr))
|
||
|
if err != nil {
|
||
|
log.Warn("Invalid pubkey", slog.String("user", user.Name), slog.Int("index", i))
|
||
|
continue
|
||
|
}
|
||
|
|
||
|
if ssh.KeysEqual(key, pubkey) {
|
||
|
return true
|
||
|
}
|
||
|
}
|
||
|
|
||
|
return false
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// failedConnHandler returns a handler that reports failed login attempts
|
||
|
// to the rate limiter.
|
||
|
func failedConnHandler(f2b *fail2ban.Fail2Ban) ssh.ConnectionFailedCallback {
|
||
|
return func(conn net.Conn, err error) {
|
||
|
if strings.Contains(err.Error(), "permission denied") {
|
||
|
log.Warn("Failed login attempt", slog.Any("addr", conn.RemoteAddr()))
|
||
|
f2b.AddFailedLogin(conn.RemoteAddr())
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// getUser uses information from the request to retrieve the seashell user
|
||
|
// that is attempting to authenticate.
|
||
|
func getUser(ctx ssh.Context, cfg config.Config) (config.User, bool) {
|
||
|
user, ok := sshctx.GetUser(ctx)
|
||
|
if ok {
|
||
|
return user, true
|
||
|
} else {
|
||
|
username, arg, ok := strings.Cut(ctx.User(), ":")
|
||
|
if !ok {
|
||
|
username, arg, ok = strings.Cut(ctx.User(), "~")
|
||
|
if !ok {
|
||
|
return config.User{}, false
|
||
|
}
|
||
|
}
|
||
|
sshctx.SetArg(ctx, arg)
|
||
|
|
||
|
for _, user := range cfg.Auth.Users {
|
||
|
if user.Name == username {
|
||
|
sshctx.SetUser(ctx, user)
|
||
|
return user, true
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
return config.User{}, false
|
||
|
}
|