software steht so weit.
jetzt build + docker build und testen
This commit is contained in:
1 parent
42f0c0accb
commit
ca6cdcca63
90 files changed
+10298
-480
No files matched your search
@@ -3,9 +3,12 @@ package server
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"math"
|
||||
"net"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"sultan/pkg/auth"
|
||||
"sultan/pkg/policy"
|
||||
@@ -28,17 +31,40 @@ type EnrollResponse struct {
|
||||
MasterPublicKey string `json:"master_public_key"` // zum Prüfen von Lizenzen/Leases
|
||||
}
|
||||
|
||||
// MaintenanceRequest ist der Body von POST /api/v1/maintenance: Der Knoten
|
||||
// meldet Wartung an oder ab (z. B. aus einem Patch-/Reboot-Skript).
|
||||
type MaintenanceRequest struct {
|
||||
Enabled bool `json:"enabled"`
|
||||
Duration string `json:"duration,omitempty"` // Go-Dauer; "" = 4h, "0" = unbefristet
|
||||
Reason string `json:"reason,omitempty"` // intern
|
||||
PublicReason string `json:"public_reason,omitempty"`
|
||||
}
|
||||
|
||||
// RotateKeyRequest ist der Body von POST /api/v1/rotate-key. Der Request ist
|
||||
// mit dem bisherigen Schlüssel signiert; Proof (auth.RotationProof) beweist
|
||||
// den Besitz des neuen.
|
||||
type RotateKeyRequest struct {
|
||||
PublicKey string `json:"public_key"`
|
||||
Proof string `json:"proof"`
|
||||
}
|
||||
|
||||
// DefaultMaintenance ist die Wartungsdauer ohne Angabe.
|
||||
const DefaultMaintenance = 4 * time.Hour
|
||||
|
||||
func (s *Server) agentRoutes() http.Handler {
|
||||
mux := http.NewServeMux()
|
||||
mux.HandleFunc("POST /api/v1/enroll", s.handleEnroll)
|
||||
mux.HandleFunc("GET /api/v1/stream", s.handleStream)
|
||||
mux.HandleFunc("POST /api/v1/state", s.handleState)
|
||||
mux.HandleFunc("POST /api/v1/maintenance", s.handleMaintenance)
|
||||
mux.HandleFunc("POST /api/v1/rotate-key", s.handleRotateKey)
|
||||
return mux
|
||||
}
|
||||
|
||||
func (s *Server) providerRoutes() http.Handler {
|
||||
mux := http.NewServeMux()
|
||||
mux.Handle("GET /v1/traefik-config", s.registry.Handler())
|
||||
mux.HandleFunc("GET /metrics/checkmk", s.handleCheckmk)
|
||||
mux.HandleFunc("GET /healthz", func(w http.ResponseWriter, _ *http.Request) {
|
||||
w.Write([]byte("ok\n"))
|
||||
})
|
||||
@@ -46,6 +72,13 @@ func (s *Server) providerRoutes() http.Handler {
|
||||
}
|
||||
|
||||
func (s *Server) handleEnroll(w http.ResponseWriter, r *http.Request) {
|
||||
// Vor der Signaturprüfung: Fluten kosten so kaum etwas.
|
||||
if ok, retry := s.enrollRL.allow(remoteIP(r)); !ok {
|
||||
w.Header().Set("Retry-After", strconv.Itoa(int(math.Ceil(retry.Seconds()))))
|
||||
s.log.Warn("Enrollment gebremst (zu viele Versuche)", "ip", remoteIP(r))
|
||||
http.Error(w, "zu viele Versuche", http.StatusTooManyRequests)
|
||||
return
|
||||
}
|
||||
pub, err := s.verifier.VerifyUnregistered(r)
|
||||
if err != nil {
|
||||
authError(w, err)
|
||||
@@ -144,6 +177,91 @@ func (s *Server) handleState(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(http.StatusNoContent)
|
||||
}
|
||||
|
||||
func (s *Server) handleMaintenance(w http.ResponseWriter, r *http.Request) {
|
||||
id, err := s.verifier.Verify(r)
|
||||
if err != nil {
|
||||
authError(w, err)
|
||||
return
|
||||
}
|
||||
var req MaintenanceRequest
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
http.Error(w, "ungültiger Body", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
if !req.Enabled {
|
||||
err = s.store.ClearNodeMaintenance(r.Context(), id.Node.ID)
|
||||
} else {
|
||||
d := DefaultMaintenance
|
||||
if req.Duration != "" {
|
||||
if d, err = time.ParseDuration(req.Duration); err != nil || d < 0 {
|
||||
http.Error(w, "duration ungültig (z. B. 30m, 4h; 0 = unbefristet)", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
}
|
||||
m := store.Maintenance{Reason: req.Reason, PublicReason: req.PublicReason}
|
||||
if d > 0 {
|
||||
m.Until = time.Now().Add(d)
|
||||
}
|
||||
err = s.store.SetNodeMaintenance(r.Context(), id.Node.ID, m)
|
||||
}
|
||||
switch {
|
||||
case errors.Is(err, store.ErrInvalid):
|
||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
case err != nil:
|
||||
s.log.Error("Wartung setzen", "node", id.Node.ID, "err", err)
|
||||
http.Error(w, "interner Fehler", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
if req.Enabled {
|
||||
s.log.Info("Wartung angemeldet", "node", id.Node.ID, "duration", req.Duration, "reason", req.Reason,
|
||||
"public_reason", req.PublicReason)
|
||||
} else {
|
||||
s.log.Info("Wartung beendet", "node", id.Node.ID)
|
||||
}
|
||||
// Sofort wirksam, nicht erst beim nächsten Refresh.
|
||||
if err := s.registry.Refresh(r.Context()); err != nil {
|
||||
s.log.Error("Neubewertung nach Wartungsänderung", "err", err)
|
||||
}
|
||||
w.WriteHeader(http.StatusNoContent)
|
||||
}
|
||||
|
||||
// handleRotateKey tauscht den Schlüssel eines Knotens. Idempotent: Ist der
|
||||
// neue Schlüssel schon aktiv (Wiederholung nach einem Abbruch, signiert mit
|
||||
// dem neuen Schlüssel), antwortet er ebenfalls mit 204.
|
||||
func (s *Server) handleRotateKey(w http.ResponseWriter, r *http.Request) {
|
||||
id, err := s.verifier.Verify(r)
|
||||
if err != nil {
|
||||
authError(w, err)
|
||||
return
|
||||
}
|
||||
var req RotateKeyRequest
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
http.Error(w, "ungültiger Body", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
pub, err := auth.DecodePublicKey(req.PublicKey)
|
||||
if err != nil || !auth.VerifyRotationProof(id.Node.ID, pub, req.Proof) {
|
||||
http.Error(w, "Besitznachweis für den neuen Schlüssel ungültig", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
encoded := auth.EncodePublicKey(pub)
|
||||
if encoded != id.Node.PublicKey {
|
||||
err := s.store.UpdateNodePublicKey(r.Context(), id.Node.ID, encoded)
|
||||
switch {
|
||||
case errors.Is(err, store.ErrExists):
|
||||
http.Error(w, "Schlüssel bereits vergeben", http.StatusConflict)
|
||||
return
|
||||
case err != nil:
|
||||
s.log.Error("Schlüssel tauschen", "node", id.Node.ID, "err", err)
|
||||
http.Error(w, "interner Fehler", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
s.log.Info("Knotenschlüssel gewechselt", "node", id.Node.ID)
|
||||
}
|
||||
w.WriteHeader(http.StatusNoContent)
|
||||
}
|
||||
|
||||
func authError(w http.ResponseWriter, err error) {
|
||||
code := auth.StatusCode(err)
|
||||
msg := http.StatusText(code)
|
||||
|
||||
Reference in new issue
Block a user