feat: auto-reapply SSL cert to all managed proxies on change

When the SSL certificate is changed in settings, automatically
updates all existing NPM proxy hosts managed by Docker Watcher
in the background. Clears SSL if cert is removed.
This commit is contained in:
2026-03-29 13:11:21 +03:00
parent 9f284932a1
commit f71c314262
+101 -1
View File
@@ -1,12 +1,15 @@
package api package api
import ( import (
"context"
"fmt" "fmt"
"log/slog"
"net/http" "net/http"
"strings" "strings"
"github.com/alexei/docker-watcher/internal/crypto" "github.com/alexei/docker-watcher/internal/crypto"
"github.com/alexei/docker-watcher/internal/npm" "github.com/alexei/docker-watcher/internal/npm"
"github.com/alexei/docker-watcher/internal/store"
"github.com/alexei/docker-watcher/internal/webhook" "github.com/alexei/docker-watcher/internal/webhook"
) )
@@ -93,14 +96,22 @@ func (s *Server) updateSettings(w http.ResponseWriter, r *http.Request) {
if req.PollingInterval != "" { if req.PollingInterval != "" {
updated.PollingInterval = req.PollingInterval updated.PollingInterval = req.PollingInterval
} }
if req.SSLCertificateID != nil { sslChanged := false
if req.SSLCertificateID != nil && *req.SSLCertificateID != updated.SSLCertificateID {
updated.SSLCertificateID = *req.SSLCertificateID updated.SSLCertificateID = *req.SSLCertificateID
sslChanged = true
} }
if err := s.store.UpdateSettings(updated); err != nil { if err := s.store.UpdateSettings(updated); err != nil {
respondError(w, http.StatusInternalServerError, "failed to update settings: "+err.Error()) respondError(w, http.StatusInternalServerError, "failed to update settings: "+err.Error())
return return
} }
// If SSL cert changed, update all existing NPM proxy hosts in the background.
if sslChanged {
go s.reapplySSLToAllProxies(updated)
}
respondJSON(w, http.StatusOK, map[string]string{"status": "updated"}) respondJSON(w, http.StatusOK, map[string]string{"status": "updated"})
} }
@@ -204,3 +215,92 @@ func isWildcardCert(cert npm.Certificate) bool {
return false return false
} }
// reapplySSLToAllProxies updates all existing NPM proxy hosts managed by Docker Watcher
// to use the new SSL certificate. Runs in the background after settings change.
func (s *Server) reapplySSLToAllProxies(settings store.Settings) {
ctx := context.Background()
npmPassword, err := crypto.Decrypt(s.encKey, settings.NpmPassword)
if err != nil {
slog.Error("reapply SSL: decrypt npm password", "error", err)
return
}
npmClient := npm.New(settings.NpmURL)
if err := npmClient.Authenticate(ctx, settings.NpmEmail, npmPassword); err != nil {
slog.Error("reapply SSL: authenticate to NPM", "error", err)
return
}
// Get all proxy hosts from NPM.
hosts, err := npmClient.ListProxyHosts(ctx)
if err != nil {
slog.Error("reapply SSL: list proxy hosts", "error", err)
return
}
// Get all our managed instances to identify which proxy hosts are ours.
projects, err := s.store.GetAllProjects()
if err != nil {
slog.Error("reapply SSL: get projects", "error", err)
return
}
// Build a set of NPM proxy IDs that belong to our instances.
managedProxyIDs := make(map[int]bool)
for _, p := range projects {
stages, err := s.store.GetStagesByProjectID(p.ID)
if err != nil {
continue
}
for _, st := range stages {
instances, err := s.store.GetInstancesByStageID(st.ID)
if err != nil {
continue
}
for _, inst := range instances {
if inst.NpmProxyID > 0 {
managedProxyIDs[inst.NpmProxyID] = true
}
}
}
}
updated := 0
for _, host := range hosts {
if !managedProxyIDs[host.ID] {
continue
}
config := npm.ProxyHostConfig{
DomainNames: host.DomainNames,
ForwardScheme: host.ForwardScheme,
ForwardHost: host.ForwardHost,
ForwardPort: host.ForwardPort,
BlockExploits: true,
AllowWebsocket: true,
HTTP2Support: true,
Meta: npm.Meta{},
Locations: []any{},
}
if settings.SSLCertificateID > 0 {
config.CertificateID = settings.SSLCertificateID
config.SSLForced = true
config.HSTSEnabled = true
} else {
config.CertificateID = 0
config.SSLForced = false
config.HSTSEnabled = false
}
if _, err := npmClient.UpdateProxyHost(ctx, host.ID, config); err != nil {
slog.Warn("reapply SSL: update proxy host failed", "host_id", host.ID, "error", err)
continue
}
updated++
}
slog.Info("reapply SSL: completed", "updated", updated, "total_managed", len(managedProxyIDs))
}