From 395fa2c2835e1926b2b4e6f2bcfbc691028e5479 Mon Sep 17 00:00:00 2001 From: Jonathon Anderson Date: Fri, 6 Mar 2026 16:24:06 -0700 Subject: [PATCH] Support requiring TLS for API access Signed-off-by: Jonathon Anderson --- internal/app/wwclient/root.go | 4 ++-- internal/app/wwctl/configure/tls/main.go | 2 +- internal/pkg/config/api.go | 5 +++++ internal/pkg/config/buildconfig.go.in | 6 +++--- internal/pkg/config/root_test.go | 5 ----- internal/pkg/configure/tls.go | 2 +- internal/pkg/warewulfd/helpers.go | 2 +- internal/pkg/warewulfd/overlay.go | 2 +- internal/pkg/warewulfd/server/server.go | 16 +++++++++++++++- overlays/wwinit/rootfs/warewulf/config.ww | 2 +- 10 files changed, 30 insertions(+), 16 deletions(-) diff --git a/internal/app/wwclient/root.go b/internal/app/wwclient/root.go index e25d7144..97bbb18c 100644 --- a/internal/app/wwclient/root.go +++ b/internal/app/wwclient/root.go @@ -117,7 +117,7 @@ func CobraRunE(cmd *cobra.Command, args []string) (err error) { } tlsConfig := &tls.Config{MinVersion: tls.VersionTLS13} - if conf.Warewulf.EnableTLS() { + if conf.Warewulf.TLSEnabled() { caCert, err := os.ReadFile("/warewulf/tls/warewulf.crt") if err != nil { wwlog.Error("failed to read ca cert: %s", err) @@ -260,7 +260,7 @@ func CobraRunE(cmd *cobra.Command, args []string) (err error) { port := conf.Warewulf.Port scheme := "http" - if conf.Warewulf.EnableTLS() { + if conf.Warewulf.TLSEnabled() { port = conf.Warewulf.TlsPort scheme = "https" } diff --git a/internal/app/wwctl/configure/tls/main.go b/internal/app/wwctl/configure/tls/main.go index af99bc37..d422515b 100644 --- a/internal/app/wwctl/configure/tls/main.go +++ b/internal/app/wwctl/configure/tls/main.go @@ -71,7 +71,7 @@ func CobraRunE(cmd *cobra.Command, args []string) error { return nil } - if !conf.Warewulf.EnableTLS() { + if !conf.Warewulf.TLSEnabled() { fmt.Fprintf(cmd.OutOrStdout(), "TLS is not enabled in warewulf.conf\n") return nil } diff --git a/internal/pkg/config/api.go b/internal/pkg/config/api.go index 766c86db..74c058b5 100644 --- a/internal/pkg/config/api.go +++ b/internal/pkg/config/api.go @@ -29,6 +29,7 @@ func (n IPNet) IPNet() net.IPNet { type APIConf struct { EnabledP *bool `yaml:"enabled,omitempty" default:"false"` + TLSEnabledP *bool `yaml:"tls,omitempty"` AllowedNets []IPNet `yaml:"allowed subnets,omitempty" default:"[\"127.0.0.0/8\", \"::1/128\"]"` } @@ -49,3 +50,7 @@ func (conf *APIConf) Unmarshal(unmarshal func(interface{}) error) error { func (conf APIConf) Enabled() bool { return util.BoolP(conf.EnabledP) } + +func (conf APIConf) TLSEnabled() bool { + return util.BoolP(conf.TLSEnabledP) +} diff --git a/internal/pkg/config/buildconfig.go.in b/internal/pkg/config/buildconfig.go.in index 18909050..4150ef2a 100644 --- a/internal/pkg/config/buildconfig.go.in +++ b/internal/pkg/config/buildconfig.go.in @@ -44,7 +44,7 @@ type WarewulfConf struct { Port int `yaml:"port,omitempty" default:"9873"` TlsPort int `yaml:"tls port,omitempty" default:"9874"` SecureP *bool `yaml:"secure,omitempty" default:"true"` - EnableTLSP *bool `yaml:"tls,omitempty" default:"false"` + TLSEnabledP *bool `yaml:"tls,omitempty"` UpdateInterval int `yaml:"update interval,omitempty" default:"60"` AutobuildOverlaysP *bool `yaml:"autobuild overlays,omitempty" default:"true"` EnableHostOverlayP *bool `yaml:"host overlay,omitempty" default:"true"` @@ -56,8 +56,8 @@ func (conf WarewulfConf) Secure() bool { return util.BoolP(conf.SecureP) } -func (conf WarewulfConf) EnableTLS() bool { - return util.BoolP(conf.EnableTLSP) +func (conf WarewulfConf) TLSEnabled() bool { + return util.BoolP(conf.TLSEnabledP) } func (conf WarewulfConf) AutobuildOverlays() bool { diff --git a/internal/pkg/config/root_test.go b/internal/pkg/config/root_test.go index 26de357c..0c21b945 100644 --- a/internal/pkg/config/root_test.go +++ b/internal/pkg/config/root_test.go @@ -17,7 +17,6 @@ func TestParse(t *testing.T) { result: ` warewulf: autobuild overlays: true - tls: false grubboot: false host overlay: true port: 9873 @@ -65,7 +64,6 @@ network: 192.168.0.0 netmask: 255.255.255.0 warewulf: autobuild overlays: true - tls: false grubboot: false host overlay: true port: 9873 @@ -115,7 +113,6 @@ network: 192.168.0.0 netmask: 255.255.0.0 warewulf: autobuild overlays: true - tls: false grubboot: false host overlay: true port: 9873 @@ -162,7 +159,6 @@ ipaddr6: "2001:db8::1" prefixlen6: "64" warewulf: autobuild overlays: true - tls: false grubboot: false host overlay: true port: 9873 @@ -238,7 +234,6 @@ netmask: 255.255.255.0 network: 192.168.200.0 warewulf: autobuild overlays: true - tls: false grubboot: false host overlay: true port: 9873 diff --git a/internal/pkg/configure/tls.go b/internal/pkg/configure/tls.go index c6695a66..fb55cc14 100644 --- a/internal/pkg/configure/tls.go +++ b/internal/pkg/configure/tls.go @@ -23,7 +23,7 @@ import ( // Returns true if new keys were generated. func TLS(force bool) (bool, error) { conf := warewulfconf.Get() - if !conf.Warewulf.EnableTLS() { + if !conf.Warewulf.TLSEnabled() { return false, nil } diff --git a/internal/pkg/warewulfd/helpers.go b/internal/pkg/warewulfd/helpers.go index 29f2062b..15fdaf08 100644 --- a/internal/pkg/warewulfd/helpers.go +++ b/internal/pkg/warewulfd/helpers.go @@ -58,7 +58,7 @@ func buildTemplateVars(conf *warewulfconf.WarewulfYaml, rinfo parsedRequest, rem Ipaddr: conf.Ipaddr, Ipaddr6: ipaddr6, Port: strconv.Itoa(conf.Warewulf.Port), - TLS: conf.Warewulf.EnableTLS(), + TLS: conf.Warewulf.TLSEnabled(), Authority: authority, Hostname: remoteNode.Id(), Hwaddr: rinfo.hwaddr, diff --git a/internal/pkg/warewulfd/overlay.go b/internal/pkg/warewulfd/overlay.go index 74998518..a21c8d64 100644 --- a/internal/pkg/warewulfd/overlay.go +++ b/internal/pkg/warewulfd/overlay.go @@ -95,7 +95,7 @@ func HandleSystemOverlay(w http.ResponseWriter, req *http.Request) { // If TLS is enabled, returns 403 Forbidden for plain-HTTP requests. // If an explicit ?overlay= list is present, delegates to HandleOverlayList. func HandleRuntimeOverlay(w http.ResponseWriter, req *http.Request) { - if config.Get().Warewulf.EnableTLS() && req.TLS == nil { + if config.Get().Warewulf.TLSEnabled() && req.TLS == nil { wwlog.Denied("runtime overlay requested over insecure connection") w.WriteHeader(http.StatusForbidden) return diff --git a/internal/pkg/warewulfd/server/server.go b/internal/pkg/warewulfd/server/server.go index 7e37c835..e436efdf 100644 --- a/internal/pkg/warewulfd/server/server.go +++ b/internal/pkg/warewulfd/server/server.go @@ -37,6 +37,17 @@ func (h *slashFix) ServeHTTP(w http.ResponseWriter, r *http.Request) { h.mux.ServeHTTP(w, r) } +func requireTLS(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.TLS == nil { + wwlog.Denied("API request over insecure connection") + w.WriteHeader(http.StatusForbidden) + return + } + next.ServeHTTP(w, r) + }) +} + func configureRootHandler(apiHandler http.Handler) *slashFix { var wwHandler http.ServeMux wwHandler.HandleFunc("/provision/", warewulfd.HandleProvision) @@ -89,13 +100,16 @@ func RunServer() error { var apiHandler http.Handler if conf.API != nil && conf.API.Enabled() { apiHandler = api.Handler(auth, conf.API.AllowedIPNets()) + if conf.API.TLSEnabled() { + apiHandler = requireTLS(apiHandler) + } } httpHandler := configureRootHandler(apiHandler) errChan := make(chan error, 2) - if conf.Warewulf.EnableTLS() { + if conf.Warewulf.TLSEnabled() { key := path.Join(conf.Paths.Sysconfdir, "warewulf", "tls", "warewulf.key") crt := path.Join(conf.Paths.Sysconfdir, "warewulf", "tls", "warewulf.crt") diff --git a/overlays/wwinit/rootfs/warewulf/config.ww b/overlays/wwinit/rootfs/warewulf/config.ww index 3b19adfb..4863d9ee 100644 --- a/overlays/wwinit/rootfs/warewulf/config.ww +++ b/overlays/wwinit/rootfs/warewulf/config.ww @@ -11,6 +11,6 @@ WWIPMI_WRITE="{{$.Ipmi.Write.Bool}}" {{- if $.Ipmi.Tags.vlan }} WWIPMI_VLAN="{{$.Ipmi.Tags.vlan}}" {{- end }} -WWTLS={{$.Warewulf.EnableTLS}} +WWTLS={{$.Warewulf.TLSEnabled}} WWTLSPORT={{$.Warewulf.TlsPort}} WWIPADDR={{$.Ipaddr}}