feat: add SQLite storage for login attempts and sessions

Adds persistent storage using modernc.org/sqlite (pure Go). Login
attempts are deduplicated by (username, password, ip) with counts.
Sessions and session logs are tracked with UUID IDs. Includes embedded
SQL migrations, configurable retention with background pruning, and
an in-memory store for tests.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-02-14 17:33:45 +01:00
parent 75bac814d4
commit d655968216
21 changed files with 1131 additions and 10 deletions

View File

@@ -14,6 +14,7 @@ import (
"git.t-juice.club/torjus/oubliette/internal/auth"
"git.t-juice.club/torjus/oubliette/internal/config"
"git.t-juice.club/torjus/oubliette/internal/storage"
"golang.org/x/crypto/ssh"
)
@@ -21,15 +22,17 @@ const sessionTimeout = 30 * time.Second
type Server struct {
cfg config.Config
store storage.Store
authenticator *auth.Authenticator
sshConfig *ssh.ServerConfig
logger *slog.Logger
connSem chan struct{} // semaphore limiting concurrent connections
}
func New(cfg config.Config, logger *slog.Logger) (*Server, error) {
func New(cfg config.Config, store storage.Store, logger *slog.Logger) (*Server, error) {
s := &Server{
cfg: cfg,
store: store,
authenticator: auth.NewAuthenticator(cfg.Auth),
logger: logger,
connSem: make(chan struct{}, cfg.SSH.MaxConnections),
@@ -123,6 +126,18 @@ func (s *Server) handleConn(conn net.Conn) {
func (s *Server) handleSession(channel ssh.Channel, requests <-chan *ssh.Request, conn *ssh.ServerConn) {
defer channel.Close()
ip := extractIP(conn.RemoteAddr())
sessionID, err := s.store.CreateSession(context.Background(), ip, conn.User(), "")
if err != nil {
s.logger.Error("failed to create session", "err", err)
} else {
defer func() {
if err := s.store.EndSession(context.Background(), sessionID, time.Now()); err != nil {
s.logger.Error("failed to end session", "err", err)
}
}()
}
// Handle session requests (pty-req, shell, etc.)
go func() {
for req := range requests {
@@ -179,6 +194,10 @@ func (s *Server) passwordCallback(conn ssh.ConnMetadata, password []byte) (*ssh.
"reason", d.Reason,
)
if err := s.store.RecordLoginAttempt(context.Background(), conn.User(), string(password), ip); err != nil {
s.logger.Error("failed to record login attempt", "err", err)
}
if d.Accepted {
return nil, nil
}