This commit is contained in:
2026-09-01 06:31:41 +02:00
parent ad54651558
commit 695ac8a713
14 changed files with 621 additions and 13 deletions
+370 -2
View File
@@ -13,6 +13,7 @@ import (
"net/url"
"strconv"
"strings"
"time"
"git.send.nrw/sendnrw/dockwatch/internal/audit"
"git.send.nrw/sendnrw/dockwatch/internal/auth"
@@ -20,6 +21,7 @@ import (
"git.send.nrw/sendnrw/dockwatch/internal/composeedit"
"git.send.nrw/sendnrw/dockwatch/internal/config"
"git.send.nrw/sendnrw/dockwatch/internal/gitops"
"git.send.nrw/sendnrw/dockwatch/internal/hostsecurity"
"git.send.nrw/sendnrw/dockwatch/internal/monitor"
"git.send.nrw/sendnrw/dockwatch/internal/nodes"
"git.send.nrw/sendnrw/dockwatch/internal/notify"
@@ -37,10 +39,11 @@ type Server struct {
audit *audit.Service
notify *notify.Service
git *gitops.Service
security *hostsecurity.Service
}
func New(c config.Config, a *auth.Service, ss *stacks.Service, n *nodes.Manager, m *monitor.Service, au *audit.Service, nt *notify.Service, gs *gitops.Service) http.Handler {
s := &Server{cfg: c, auth: a, stacks: ss, nodes: n, monitors: m, audit: au, notify: nt, git: gs}
func New(c config.Config, a *auth.Service, ss *stacks.Service, n *nodes.Manager, m *monitor.Service, au *audit.Service, nt *notify.Service, gs *gitops.Service, hs *hostsecurity.Service) http.Handler {
s := &Server{cfg: c, auth: a, stacks: ss, nodes: n, monitors: m, audit: au, notify: nt, git: gs, security: hs}
mux := http.NewServeMux()
mux.HandleFunc("GET /healthz", func(w http.ResponseWriter, r *http.Request) {
jsonOut(w, 200, map[string]any{"ok": true, "mode": c.Mode, "build": buildinfo.Current()})
@@ -129,6 +132,18 @@ func New(c config.Config, a *auth.Service, ss *stacks.Service, n *nodes.Manager,
api.Handle("PUT /api/nodes/{id}", auth.RequireRole("admin", http.HandlerFunc(s.updateNode)))
api.Handle("DELETE /api/nodes/{id}", auth.RequireRole("admin", http.HandlerFunc(s.deleteNode)))
api.HandleFunc("GET /api/nodes/{id}/health", s.nodeHealth)
api.Handle("GET /api/security/status", auth.RequireRole("admin", http.HandlerFunc(s.securityStatus)))
api.Handle("GET /api/security/firewall", auth.RequireRole("admin", http.HandlerFunc(s.securityFirewall)))
api.Handle("POST /api/security/firewall/preview", auth.RequireRole("admin", http.HandlerFunc(s.securityFirewallPreview)))
api.Handle("POST /api/security/firewall/apply", auth.RequireRole("admin", http.HandlerFunc(s.securityFirewallApply)))
api.Handle("POST /api/security/firewall/commit", auth.RequireRole("admin", http.HandlerFunc(s.securityFirewallCommit)))
api.Handle("POST /api/security/firewall/rollback", auth.RequireRole("admin", http.HandlerFunc(s.securityFirewallRollback)))
api.Handle("GET /api/security/fail2ban", auth.RequireRole("admin", http.HandlerFunc(s.securityFail2Ban)))
api.Handle("PUT /api/security/fail2ban", auth.RequireRole("admin", http.HandlerFunc(s.securityApplyFail2Ban)))
api.Handle("GET /api/security/auditd", auth.RequireRole("admin", http.HandlerFunc(s.securityAuditd)))
api.Handle("PUT /api/security/auditd", auth.RequireRole("admin", http.HandlerFunc(s.securityApplyAuditd)))
api.Handle("POST /api/security/components/{component}/install", auth.RequireRole("admin", http.HandlerFunc(s.securityInstall)))
api.Handle("POST /api/security/components/{component}/actions/{action}", auth.RequireRole("admin", http.HandlerFunc(s.securityComponentAction)))
mux.Handle("/api/", a.Middleware(mutationOriginGuard(s.auditMiddleware(api))))
assets, _ := fs.Sub(web.FS, ".")
f := http.FileServer(http.FS(assets))
@@ -149,6 +164,18 @@ func (s *Server) agent(m *http.ServeMux) {
a.HandleFunc("POST /agent/v1/docker/containers/{id}/bind-permissions/preview", s.localBindPermissionPreview)
a.HandleFunc("POST /agent/v1/host/bind-permissions/repair", s.localRepairBindPermissions)
a.HandleFunc("POST /agent/v1/host/users", s.localCreateHostUser)
a.HandleFunc("GET /agent/v1/security/status", s.localSecurityStatus)
a.HandleFunc("GET /agent/v1/security/firewall", s.localSecurityFirewall)
a.HandleFunc("POST /agent/v1/security/firewall/preview", s.localSecurityFirewallPreview)
a.HandleFunc("POST /agent/v1/security/firewall/apply", s.localSecurityFirewallApply)
a.HandleFunc("POST /agent/v1/security/firewall/commit", s.localSecurityFirewallCommit)
a.HandleFunc("POST /agent/v1/security/firewall/rollback", s.localSecurityFirewallRollback)
a.HandleFunc("GET /agent/v1/security/fail2ban", s.localSecurityFail2Ban)
a.HandleFunc("PUT /agent/v1/security/fail2ban", s.localSecurityApplyFail2Ban)
a.HandleFunc("GET /agent/v1/security/auditd", s.localSecurityAuditd)
a.HandleFunc("PUT /agent/v1/security/auditd", s.localSecurityApplyAuditd)
a.HandleFunc("POST /agent/v1/security/components/{component}/install", s.localSecurityInstall)
a.HandleFunc("POST /agent/v1/security/components/{component}/actions/{action}", s.localSecurityComponentAction)
a.HandleFunc("GET /agent/v1/stacks", s.localList)
a.HandleFunc("GET /agent/v1/stacks/{name}", s.localGet)
a.HandleFunc("PUT /agent/v1/stacks/{name}", s.localSave)
@@ -558,6 +585,19 @@ func (s *Server) relay(w http.ResponseWriter, r *http.Request, id int64, method,
w.WriteHeader(status)
_, _ = w.Write(b)
}
func (s *Server) relayWithTimeout(w http.ResponseWriter, r *http.Request, id int64, method, path string, body any, timeout time.Duration) {
b, status, e := s.nodes.DoWithTimeout(r.Context(), id, method, path, body, timeout)
if e != nil {
if status == 0 {
status = 502
}
http.Error(w, e.Error(), status)
return
}
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(status)
_, _ = w.Write(b)
}
func (s *Server) dockerInventory(w http.ResponseWriter, r *http.Request) {
kind := r.PathValue("kind")
if id := nodeID(r); id > 0 {
@@ -1384,3 +1424,331 @@ func (s *Server) proxyTerminal(w http.ResponseWriter, r *http.Request, id int64)
case <-done:
}
}
func (s *Server) securityStatus(w http.ResponseWriter, r *http.Request) {
if id := nodeID(r); id > 0 {
s.relay(w, r, id, http.MethodGet, "/agent/v1/security/status", nil)
return
}
s.localSecurityStatus(w, r)
}
func (s *Server) localSecurityStatus(w http.ResponseWriter, r *http.Request) {
if s.security == nil {
http.Error(w, "host security service unavailable", http.StatusServiceUnavailable)
return
}
jsonOut(w, http.StatusOK, s.security.Status(r.Context()))
}
func (s *Server) securityFirewall(w http.ResponseWriter, r *http.Request) {
if id := nodeID(r); id > 0 {
s.relay(w, r, id, http.MethodGet, "/agent/v1/security/firewall", nil)
return
}
s.localSecurityFirewall(w, r)
}
func (s *Server) localSecurityFirewall(w http.ResponseWriter, r *http.Request) {
if s.security == nil {
http.Error(w, "host security service unavailable", http.StatusServiceUnavailable)
return
}
jsonOut(w, http.StatusOK, s.security.FirewallPolicy())
}
func (s *Server) securityFirewallPreview(w http.ResponseWriter, r *http.Request) {
var in hostsecurity.FirewallPolicy
if e := read(r, &in); e != nil {
http.Error(w, e.Error(), 400)
return
}
if id := nodeID(r); id > 0 {
s.relay(w, r, id, http.MethodPost, "/agent/v1/security/firewall/preview", in)
return
}
s.securityFirewallPreviewLocal(w, r, in)
}
func (s *Server) localSecurityFirewallPreview(w http.ResponseWriter, r *http.Request) {
var in hostsecurity.FirewallPolicy
if e := read(r, &in); e != nil {
http.Error(w, e.Error(), 400)
return
}
s.securityFirewallPreviewLocal(w, r, in)
}
func (s *Server) securityFirewallPreviewLocal(w http.ResponseWriter, r *http.Request, in hostsecurity.FirewallPolicy) {
if s.security == nil {
http.Error(w, "host security service unavailable", 503)
return
}
v, e := s.security.PreviewFirewall(r.Context(), in)
if e != nil {
http.Error(w, e.Error(), 400)
return
}
jsonOut(w, 200, v)
}
func (s *Server) securityFirewallApply(w http.ResponseWriter, r *http.Request) {
var in struct {
Policy hostsecurity.FirewallPolicy `json:"policy"`
RollbackSeconds int `json:"rollback_seconds"`
}
if e := read(r, &in); e != nil {
http.Error(w, e.Error(), 400)
return
}
if id := nodeID(r); id > 0 {
s.relay(w, r, id, http.MethodPost, "/agent/v1/security/firewall/apply", in)
return
}
s.securityFirewallApplyLocal(w, r, in.Policy, in.RollbackSeconds)
}
func (s *Server) localSecurityFirewallApply(w http.ResponseWriter, r *http.Request) {
var in struct {
Policy hostsecurity.FirewallPolicy `json:"policy"`
RollbackSeconds int `json:"rollback_seconds"`
}
if e := read(r, &in); e != nil {
http.Error(w, e.Error(), 400)
return
}
s.securityFirewallApplyLocal(w, r, in.Policy, in.RollbackSeconds)
}
func (s *Server) securityFirewallApplyLocal(w http.ResponseWriter, r *http.Request, p hostsecurity.FirewallPolicy, seconds int) {
if s.security == nil {
http.Error(w, "host security service unavailable", 503)
return
}
v, e := s.security.ApplyFirewall(r.Context(), p, seconds)
if e != nil {
http.Error(w, e.Error(), 400)
return
}
jsonOut(w, 200, v)
}
func (s *Server) securityFirewallCommit(w http.ResponseWriter, r *http.Request) {
var in struct {
ChangeID string `json:"change_id"`
}
if e := read(r, &in); e != nil {
http.Error(w, e.Error(), 400)
return
}
if id := nodeID(r); id > 0 {
s.relay(w, r, id, http.MethodPost, "/agent/v1/security/firewall/commit", in)
return
}
s.securityFirewallCommitLocal(w, r, in.ChangeID)
}
func (s *Server) localSecurityFirewallCommit(w http.ResponseWriter, r *http.Request) {
var in struct {
ChangeID string `json:"change_id"`
}
if e := read(r, &in); e != nil {
http.Error(w, e.Error(), 400)
return
}
s.securityFirewallCommitLocal(w, r, in.ChangeID)
}
func (s *Server) securityFirewallCommitLocal(w http.ResponseWriter, r *http.Request, id string) {
if s.security == nil {
http.Error(w, "host security service unavailable", 503)
return
}
if e := s.security.CommitFirewall(id); e != nil {
http.Error(w, e.Error(), 400)
return
}
jsonOut(w, 200, map[string]any{"ok": true, "message": "Firewall change committed."})
}
func (s *Server) securityFirewallRollback(w http.ResponseWriter, r *http.Request) {
var in struct {
ChangeID string `json:"change_id"`
}
if e := read(r, &in); e != nil {
http.Error(w, e.Error(), 400)
return
}
if id := nodeID(r); id > 0 {
s.relay(w, r, id, http.MethodPost, "/agent/v1/security/firewall/rollback", in)
return
}
s.securityFirewallRollbackLocal(w, r, in.ChangeID)
}
func (s *Server) localSecurityFirewallRollback(w http.ResponseWriter, r *http.Request) {
var in struct {
ChangeID string `json:"change_id"`
}
if e := read(r, &in); e != nil {
http.Error(w, e.Error(), 400)
return
}
s.securityFirewallRollbackLocal(w, r, in.ChangeID)
}
func (s *Server) securityFirewallRollbackLocal(w http.ResponseWriter, r *http.Request, id string) {
if s.security == nil {
http.Error(w, "host security service unavailable", 503)
return
}
if e := s.security.RollbackFirewall(r.Context(), id); e != nil {
http.Error(w, e.Error(), 400)
return
}
jsonOut(w, 200, map[string]any{"ok": true, "message": "Firewall change rolled back."})
}
func (s *Server) securityFail2Ban(w http.ResponseWriter, r *http.Request) {
if id := nodeID(r); id > 0 {
s.relay(w, r, id, http.MethodGet, "/agent/v1/security/fail2ban", nil)
return
}
s.localSecurityFail2Ban(w, r)
}
func (s *Server) localSecurityFail2Ban(w http.ResponseWriter, r *http.Request) {
if s.security == nil {
http.Error(w, "host security service unavailable", 503)
return
}
jsonOut(w, 200, s.security.Fail2BanPolicy())
}
func (s *Server) securityApplyFail2Ban(w http.ResponseWriter, r *http.Request) {
var in hostsecurity.Fail2BanPolicy
if e := read(r, &in); e != nil {
http.Error(w, e.Error(), 400)
return
}
if id := nodeID(r); id > 0 {
s.relay(w, r, id, http.MethodPut, "/agent/v1/security/fail2ban", in)
return
}
s.securityApplyFail2BanLocal(w, r, in)
}
func (s *Server) localSecurityApplyFail2Ban(w http.ResponseWriter, r *http.Request) {
var in hostsecurity.Fail2BanPolicy
if e := read(r, &in); e != nil {
http.Error(w, e.Error(), 400)
return
}
s.securityApplyFail2BanLocal(w, r, in)
}
func (s *Server) securityApplyFail2BanLocal(w http.ResponseWriter, r *http.Request, in hostsecurity.Fail2BanPolicy) {
if s.security == nil {
http.Error(w, "host security service unavailable", 503)
return
}
v, e := s.security.ApplyFail2Ban(r.Context(), in)
if e != nil {
http.Error(w, e.Error(), 400)
return
}
jsonOut(w, 200, v)
}
func (s *Server) securityAuditd(w http.ResponseWriter, r *http.Request) {
if id := nodeID(r); id > 0 {
s.relay(w, r, id, http.MethodGet, "/agent/v1/security/auditd", nil)
return
}
s.localSecurityAuditd(w, r)
}
func (s *Server) localSecurityAuditd(w http.ResponseWriter, r *http.Request) {
if s.security == nil {
http.Error(w, "host security service unavailable", 503)
return
}
jsonOut(w, 200, s.security.AuditdPolicy())
}
func (s *Server) securityApplyAuditd(w http.ResponseWriter, r *http.Request) {
var in hostsecurity.AuditdPolicy
if e := read(r, &in); e != nil {
http.Error(w, e.Error(), 400)
return
}
if id := nodeID(r); id > 0 {
s.relay(w, r, id, http.MethodPut, "/agent/v1/security/auditd", in)
return
}
s.securityApplyAuditdLocal(w, r, in)
}
func (s *Server) localSecurityApplyAuditd(w http.ResponseWriter, r *http.Request) {
var in hostsecurity.AuditdPolicy
if e := read(r, &in); e != nil {
http.Error(w, e.Error(), 400)
return
}
s.securityApplyAuditdLocal(w, r, in)
}
func (s *Server) securityApplyAuditdLocal(w http.ResponseWriter, r *http.Request, in hostsecurity.AuditdPolicy) {
if s.security == nil {
http.Error(w, "host security service unavailable", 503)
return
}
v, e := s.security.ApplyAuditd(r.Context(), in)
if e != nil {
http.Error(w, e.Error(), 400)
return
}
jsonOut(w, 200, v)
}
func (s *Server) securityInstall(w http.ResponseWriter, r *http.Request) {
var in hostsecurity.InstallInput
if e := read(r, &in); e != nil {
http.Error(w, e.Error(), 400)
return
}
component := r.PathValue("component")
if id := nodeID(r); id > 0 {
s.relayWithTimeout(w, r, id, http.MethodPost, "/agent/v1/security/components/"+url.PathEscape(component)+"/install", in, 10*time.Minute)
return
}
s.securityInstallLocal(w, r, component, in)
}
func (s *Server) localSecurityInstall(w http.ResponseWriter, r *http.Request) {
var in hostsecurity.InstallInput
if e := read(r, &in); e != nil {
http.Error(w, e.Error(), 400)
return
}
s.securityInstallLocal(w, r, r.PathValue("component"), in)
}
func (s *Server) securityInstallLocal(w http.ResponseWriter, r *http.Request, component string, in hostsecurity.InstallInput) {
if s.security == nil {
http.Error(w, "host security service unavailable", 503)
return
}
v, e := s.security.Install(r.Context(), component, in)
if e != nil {
http.Error(w, e.Error(), 400)
return
}
jsonOut(w, 200, v)
}
func (s *Server) securityComponentAction(w http.ResponseWriter, r *http.Request) {
component, action := r.PathValue("component"), r.PathValue("action")
if id := nodeID(r); id > 0 {
s.relay(w, r, id, http.MethodPost, "/agent/v1/security/components/"+url.PathEscape(component)+"/actions/"+url.PathEscape(action), map[string]any{})
return
}
s.securityComponentActionLocal(w, r, component, action)
}
func (s *Server) localSecurityComponentAction(w http.ResponseWriter, r *http.Request) {
s.securityComponentActionLocal(w, r, r.PathValue("component"), r.PathValue("action"))
}
func (s *Server) securityComponentActionLocal(w http.ResponseWriter, r *http.Request, component, action string) {
if s.security == nil {
http.Error(w, "host security service unavailable", 503)
return
}
v, e := s.security.ComponentAction(r.Context(), component, action)
if e != nil {
http.Error(w, e.Error(), 400)
return
}
jsonOut(w, 200, v)
}