seashell/auth.go
2024-08-03 22:31:40 -07:00

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
}