feat: add Bastillion-style SSH key enforcement worker

This commit is contained in:
2026-04-06 00:17:03 +02:00
parent 3a843354b6
commit 8b9de9e83d
11 changed files with 992 additions and 3 deletions
+50 -1
View File
@@ -39,6 +39,7 @@ import (
"git.techniverse.net/scriptos/keywarden/internal/models"
"git.techniverse.net/scriptos/keywarden/internal/security"
"git.techniverse.net/scriptos/keywarden/internal/servers"
"git.techniverse.net/scriptos/keywarden/internal/worker"
)
// sessionData holds session metadata for timeout tracking
@@ -56,6 +57,7 @@ type Handler struct {
deploy *deploy.Service
audit *audit.Service
cron *cron.Service
worker *worker.Service
mail *mail.Service
db *database.DB // direct database access for backup/restore
templates map[string]*template.Template
@@ -169,6 +171,9 @@ type PageData struct {
// System Information
SystemInfo *SystemInfo
// Key Enforcement
EnforcementStatus map[string]string
}
// SystemInfo holds runtime system information for the settings page
@@ -242,7 +247,7 @@ func formatUptime(start time.Time) string {
}
// New creates a new Handler
func New(authSvc *auth.Service, keysSvc *keys.Service, serversSvc *servers.Service, deploySvc *deploy.Service, auditSvc *audit.Service, cronSvc *cron.Service, mailSvc *mail.Service, db *database.DB, templateFS embed.FS, staticFS embed.FS, dataDir string, secureCookies bool, baseURL string) *Handler {
func New(authSvc *auth.Service, keysSvc *keys.Service, serversSvc *servers.Service, deploySvc *deploy.Service, auditSvc *audit.Service, cronSvc *cron.Service, workerSvc *worker.Service, mailSvc *mail.Service, db *database.DB, templateFS embed.FS, staticFS embed.FS, dataDir string, secureCookies bool, baseURL string) *Handler {
// Create sub-FS so /static/css/... maps to static/css/... in embed
staticSub, err := fs.Sub(staticFS, "static")
if err != nil {
@@ -262,6 +267,7 @@ func New(authSvc *auth.Service, keysSvc *keys.Service, serversSvc *servers.Servi
deploy: deploySvc,
audit: auditSvc,
cron: cronSvc,
worker: workerSvc,
mail: mailSvc,
db: db,
sessions: make(map[string]*sessionData),
@@ -432,6 +438,7 @@ func (h *Handler) RegisterRoutes(mux *http.ServeMux) {
mux.HandleFunc("/admin/masterkey/regenerate", h.requireOwner(h.handleMasterKeyRegenerate))
mux.HandleFunc("/admin/backup/export", h.requireOwner(h.handleBackupExport))
mux.HandleFunc("/admin/backup/import", h.requireOwner(h.handleBackupImport))
mux.HandleFunc("/admin/enforcement/run", h.requireOwner(h.handleEnforcementRunNow))
}
// handleAPIHealth returns a JSON health status (no auth required).
@@ -2993,6 +3000,7 @@ func (h *Handler) handleAdminSettings(w http.ResponseWriter, r *http.Request) {
EmailEnabled: h.mail.IsEnabled(),
MasterKeyPublic: masterPub,
MasterKeyFingerprint: masterFP,
EnforcementStatus: h.worker.GetStatus(),
}
// Check for flash message from query parameters (e.g. after backup restore)
@@ -3047,6 +3055,31 @@ func (h *Handler) handleAdminSettings(w http.ResponseWriter, r *http.Request) {
if len(changed) > 0 {
h.audit.Log(userID, audit.ActionPasswordPolicyChanged, fmt.Sprintf("Security settings updated: %s", strings.Join(changed, ", ")), clientIP(r))
}
case "enforcement_settings":
// Key enforcement settings
batch := make(map[string]string)
enforceMode := r.FormValue("enforce_mode")
if enforceMode == "" {
enforceMode = "disabled"
}
batch["enforce_mode"] = enforceMode
changed = append(changed, "enforce_mode="+enforceMode)
enforceInterval := r.FormValue("enforce_interval")
if enforceInterval == "" {
enforceInterval = "15"
}
batch["enforce_interval"] = enforceInterval
changed = append(changed, "enforce_interval="+enforceInterval)
if err := h.auth.SetSettingsBatch(batch); err != nil {
logging.Error("Failed to save enforcement settings: %v", err)
http.Redirect(w, r, "/admin/settings?flash_type=danger&flash_msg="+url.QueryEscape("Failed to save enforcement settings: "+err.Error()), http.StatusSeeOther)
return
}
if len(changed) > 0 {
h.audit.Log(userID, audit.ActionEnforcementSettings, fmt.Sprintf("Enforcement settings updated: %s", strings.Join(changed, ", ")), clientIP(r))
}
default:
// Application settings (existing behavior)
batch := make(map[string]string)
@@ -3124,6 +3157,22 @@ func (h *Handler) handleMasterKeyRegenerate(w http.ResponseWriter, r *http.Reque
http.Redirect(w, r, "/admin/settings?flash_type=success&flash_msg="+url.QueryEscape("System master key successfully regenerated."), http.StatusSeeOther)
}
// handleEnforcementRunNow triggers an immediate key enforcement run (owner only)
func (h *Handler) handleEnforcementRunNow(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
http.Redirect(w, r, "/admin/settings", http.StatusSeeOther)
return
}
userID := h.getUserID(r)
logging.Info("Key enforcement manual run triggered by user_id=%d", userID)
h.audit.Log(userID, audit.ActionEnforcementRun, "Manual key enforcement run triggered", clientIP(r))
h.worker.RunNow()
http.Redirect(w, r, "/admin/settings?flash_type=success&flash_msg="+url.QueryEscape("Key enforcement run started. Check the audit log for results."), http.StatusSeeOther)
}
// --- Cron Job Handlers ---
// handleAPICronAssignments returns assignments for a given user as JSON (for AJAX).