Bug bash fixes: special-file reads, per-client lockout, prefs validation, security headers, kernel-thread guard, login feedback, error text; plus journal-quiet sudoers + journal groups, upload overwrite guard, machines pattern, dnf freshness, cron/services speed, clean version

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
Mitchell-Johnson
2026-09-18 17:41:52 -07:00
co-authored by Claude Fable 5.1
parent 57c58ebffb
commit 27f9665606
18 changed files with 306 additions and 85 deletions
+64 -11
View File
@@ -152,39 +152,92 @@ func (s *Sessions) Delete(tok string) {
// Limiter is a single global backoff: after 5 failures, each further failure
// doubles a lockout starting at 30s, capped at 10 minutes. Single-user box.
// Limiter locks out a client after repeated failed logins, per client address, with a
// global backstop so a botnet can't burn CPU on argon2 indefinitely. A lockout for one
// address never blocks another.
type Limiter struct {
mu sync.Mutex
mu sync.Mutex
clients map[string]*bucket
global bucket
}
type bucket struct {
failures int
lockUntil time.Time
last time.Time
}
var ErrLocked = errors.New("too many failed logins")
func (l *Limiter) Allow() error {
const (
perClientLock = 5 // failures before a client is locked
globalLock = 100 // failures (all clients) in the window before everyone waits a minute
globalWindow = 5 * time.Minute
)
func (l *Limiter) get(key string) *bucket {
if l.clients == nil {
l.clients = map[string]*bucket{}
}
b, ok := l.clients[key]
if !ok {
b = &bucket{}
l.clients[key] = b
}
// forget clients that have been quiet for an hour
if len(l.clients) > 256 {
for k, x := range l.clients {
if time.Since(x.last) > time.Hour && time.Now().After(x.lockUntil) {
delete(l.clients, k)
}
}
}
return b
}
func (l *Limiter) Allow(key string) error {
l.mu.Lock()
defer l.mu.Unlock()
if time.Now().Before(l.lockUntil) {
now := time.Now()
if now.Before(l.global.lockUntil) {
return ErrLocked
}
if now.Sub(l.global.last) > globalWindow {
l.global.failures = 0
}
if b := l.get(key); now.Before(b.lockUntil) {
return ErrLocked
}
return nil
}
func (l *Limiter) Fail() {
func (l *Limiter) Fail(key string) {
l.mu.Lock()
defer l.mu.Unlock()
l.failures++
if l.failures >= 5 {
d := 30 * time.Second << uint(l.failures-5)
now := time.Now()
b := l.get(key)
b.failures++
b.last = now
if b.failures >= perClientLock {
d := 30 * time.Second << uint(b.failures-perClientLock)
if d > 10*time.Minute {
d = 10 * time.Minute
}
l.lockUntil = time.Now().Add(d)
b.lockUntil = now.Add(d)
}
if now.Sub(l.global.last) > globalWindow {
l.global.failures = 0
}
l.global.failures++
l.global.last = now
if l.global.failures >= globalLock {
l.global.lockUntil = now.Add(time.Minute)
l.global.failures = 0
}
}
func (l *Limiter) Reset() {
func (l *Limiter) Reset(key string) {
l.mu.Lock()
defer l.mu.Unlock()
l.failures = 0
l.lockUntil = time.Time{}
delete(l.clients, key)
}