Add backups, token expiry warnings and name server checks

- Daily database backups: jarl-backup.timer runs 'jarld backup', which
  writes jarl-<UTC time>.db into backup.dir, checks it before naming it,
  and prunes by the time in the name while always keeping the newest
  keep_at_least. New backup: section in jarl.yaml; the dashboard and
  'jarl status' show the newest backup and warn when it is stale or
  failed. The Debian package enables the timer.
- 'jarld restore [FILE | latest]': checks the backup, shows it next to
  the current database, keeps the current one aside and records the
  restore. 'jarld serve' holds a database lock, so a restore can't run
  under a live jarld and two jarlds can't share a database.
- Token expiry warnings from 14 days ahead: dashboard, token lists, a
  daily log line, and a CLI warning via the Jarl-Token-Expires header;
  'jarl token list --expiring'.
- 'jarl zone serials' and the zone page's Name servers tab: compare the
  serial on a primary zone's secondaries (NS records, also-notify) or a
  secondary zone's primaries with this server's.
- 'jarl zone delegation': for zones on the internet, check the parent's
  delegation and glue against the zone; an undelegated zone gets a note.
- Server: Jarl/<version> header; CLI exit code 2 for failed checks.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
abrown38andClaude Opus 5.5 committed 2026-10-08 19:02:34 +00:00
1 parent 1afed741bf
commit 293e6ddd33
47 files changed
+3717 -95

No files matched your search

+14
View File
@@ -19,6 +19,7 @@ import (
"jarl/internal/api"
"jarl/internal/auth"
"jarl/internal/engine"
"jarl/internal/nscheck"
"jarl/internal/stats"
"jarl/internal/store"
)
@@ -42,6 +43,10 @@ type Config struct {
HTTPReq HTTPReq
// Socket is the local unix socket's path ("" for none); see socket.go.
Socket string
// BackupDir and BackupWarnAfter are the backup settings the dashboard
// reports on (backup.dir and backup.warn_after).
BackupDir string
BackupWarnAfter time.Duration
}
// Server is the HTTP server.
@@ -57,6 +62,11 @@ type Server struct {
statsQ *stats.Querier
// oldSecretWarn limits warnings about rotated tokens' old secrets.
oldSecretWarn throttle
// expiryWarn limits warnings about expiring tokens to one a day each.
expiryWarn throttle
// nsChecker, when set, replaces the default name server checker
// (tests point it at local DNS servers).
nsChecker *nscheck.Checker
mu sync.Mutex
srvs []*http.Server
@@ -80,6 +90,7 @@ func New(cfg Config, eng *engine.Engine, log *slog.Logger, web fs.FS) *Server {
log = slog.Default()
}
s := &Server{cfg: cfg, eng: eng, store: eng.Store(), log: log, mux: http.NewServeMux(), limiter: newRateLimiter(), web: web}
s.expiryWarn.every = 24 * time.Hour
if cfg.Stats != nil {
s.statsQ = stats.NewQuerier(cfg.Stats)
}
@@ -236,6 +247,7 @@ func (s *Server) housekeeping(ctx context.Context) {
s.log.Warn("purging sessions failed", "err", err)
}
s.limiter.cleanup()
s.warnExpiringTokens(ctx, time.Now())
if s.certs != nil {
s.certs.reloadIfChanged()
}
@@ -330,6 +342,8 @@ func (s *Server) recoverer(next http.Handler) http.Handler {
func (s *Server) securityHeaders(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
h := w.Header()
// The version is no secret: /healthz reports it too.
h.Set("Server", "Jarl/"+s.cfg.Version)
h.Set("X-Content-Type-Options", "nosniff")
h.Set("X-Frame-Options", "DENY")
h.Set("Referrer-Policy", "no-referrer")