From d71d085f321c79d69f1579607ec686618fc3d52e Mon Sep 17 00:00:00 2001 From: Pulse Monitor Date: Mon, 18 Aug 2025 15:29:37 +0000 Subject: [PATCH] feat: add HTTPS/TLS support via environment variables - Add HTTPS_ENABLED, TLS_CERT_FILE, TLS_KEY_FILE environment variables - Server automatically starts in HTTPS mode when configured - Falls back to HTTP with warning if certs missing - WebSocket origins automatically adjust for HTTPS - Fully backward compatible - defaults to HTTP - Documented in README and CONFIGURATION.md Addresses kenrmayfield's request for HTTPS support --- README.md | 18 +++++++++++++++ cmd/pulse/main.go | 46 +++++++++++++++++++++++++++++---------- docs/CONFIGURATION.md | 9 ++++++-- internal/config/config.go | 19 ++++++++++++++++ 4 files changed, 78 insertions(+), 14 deletions(-) diff --git a/README.md b/README.md index 29ea63b99..058ef44f3 100644 --- a/README.md +++ b/README.md @@ -253,6 +253,24 @@ Configure email notifications in **Settings → Alerts → Email Destinations** 2. Use your email as username and app password as password 3. Server: smtp-mail.outlook.com, Port: 587, Enable STARTTLS +### HTTPS/TLS Configuration +Enable HTTPS by setting these environment variables: +```bash +# Systemd: sudo systemctl edit pulse-backend +Environment="HTTPS_ENABLED=true" +Environment="TLS_CERT_FILE=/etc/pulse/cert.pem" +Environment="TLS_KEY_FILE=/etc/pulse/key.pem" + +# Docker +docker run -d -p 7655:7655 \ + -e HTTPS_ENABLED=true \ + -e TLS_CERT_FILE=/data/cert.pem \ + -e TLS_KEY_FILE=/data/key.pem \ + -v pulse_data:/data \ + -v /path/to/certs:/data/certs:ro \ + rcourtman/pulse:latest +``` + For deployment overrides (ports, etc), use environment variables: ```bash # Systemd: sudo systemctl edit pulse-backend diff --git a/cmd/pulse/main.go b/cmd/pulse/main.go index 3c4617456..f3e1d628a 100644 --- a/cmd/pulse/main.go +++ b/cmd/pulse/main.go @@ -82,12 +82,19 @@ func runServer() { // This will be dynamically set based on the actual request host allowedOrigins := []string{} // Add localhost variants for development - allowedOrigins = append(allowedOrigins, - "http://localhost:"+fmt.Sprintf("%d", cfg.FrontendPort), - "http://127.0.0.1:"+fmt.Sprintf("%d", cfg.FrontendPort), - ) - // If HTTPS is likely being used, add those too - if cfg.FrontendPort == 443 || cfg.FrontendPort == 8443 { + if cfg.HTTPSEnabled { + allowedOrigins = append(allowedOrigins, + "https://localhost:"+fmt.Sprintf("%d", cfg.FrontendPort), + "https://127.0.0.1:"+fmt.Sprintf("%d", cfg.FrontendPort), + ) + } else { + allowedOrigins = append(allowedOrigins, + "http://localhost:"+fmt.Sprintf("%d", cfg.FrontendPort), + "http://127.0.0.1:"+fmt.Sprintf("%d", cfg.FrontendPort), + ) + } + // If HTTPS is likely being used based on port, add those too + if !cfg.HTTPSEnabled && (cfg.FrontendPort == 443 || cfg.FrontendPort == 8443) { allowedOrigins = append(allowedOrigins, "https://localhost:"+fmt.Sprintf("%d", cfg.FrontendPort), "https://127.0.0.1:"+fmt.Sprintf("%d", cfg.FrontendPort), @@ -140,12 +147,27 @@ func runServer() { // Start server go func() { - log.Info(). - Str("host", cfg.BackendHost). - Int("port", cfg.FrontendPort). - Msg("Server listening") - if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed { - log.Fatal().Err(err).Msg("Failed to start server") + if cfg.HTTPSEnabled && cfg.TLSCertFile != "" && cfg.TLSKeyFile != "" { + log.Info(). + Str("host", cfg.BackendHost). + Int("port", cfg.FrontendPort). + Str("protocol", "HTTPS"). + Msg("Server listening") + if err := srv.ListenAndServeTLS(cfg.TLSCertFile, cfg.TLSKeyFile); err != nil && err != http.ErrServerClosed { + log.Fatal().Err(err).Msg("Failed to start HTTPS server") + } + } else { + if cfg.HTTPSEnabled { + log.Warn().Msg("HTTPS_ENABLED is true but TLS_CERT_FILE or TLS_KEY_FILE not configured, falling back to HTTP") + } + log.Info(). + Str("host", cfg.BackendHost). + Int("port", cfg.FrontendPort). + Str("protocol", "HTTP"). + Msg("Server listening") + if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed { + log.Fatal().Err(err).Msg("Failed to start HTTP server") + } } }() diff --git a/docs/CONFIGURATION.md b/docs/CONFIGURATION.md index e505722f0..ec99cd4ce 100644 --- a/docs/CONFIGURATION.md +++ b/docs/CONFIGURATION.md @@ -120,12 +120,17 @@ For backwards compatibility, some settings can be overridden via environment var - `BACKEND_HOST` - IP address to bind to (default: `0.0.0.0`) - Set to `127.0.0.1` or `localhost` to only listen on localhost (for reverse proxy) - `FRONTEND_PORT` - Port to listen on (default: `7655`) + +3. **HTTPS/TLS variables** - For enabling HTTPS + - `HTTPS_ENABLED` - Set to `true` to enable HTTPS (default: `false`) + - `TLS_CERT_FILE` - Path to TLS certificate file (e.g., `/etc/pulse/cert.pem`) + - `TLS_KEY_FILE` - Path to TLS private key file (e.g., `/etc/pulse/key.pem`) -3. **System settings (from system.json)** - Normal priority +4. **System settings (from system.json)** - Normal priority - If system.json exists, it takes precedence - If missing, environment variables are checked -4. **Legacy environment variables** - Lowest priority (deprecated) +5. **Legacy environment variables** - Lowest priority (deprecated) - `POLLING_INTERVAL` - Only used if system.json doesn't exist - `CONNECTION_TIMEOUT` - Can override system.json value - `ALLOWED_ORIGINS` - Can override system.json value diff --git a/internal/config/config.go b/internal/config/config.go index b86af799e..f6d86cc81 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -88,6 +88,11 @@ type Config struct { AuthPass string `envconfig:"PULSE_AUTH_PASS"` AllowedOrigins string `envconfig:"ALLOWED_ORIGINS" default:"*"` IframeEmbeddingAllow string `envconfig:"IFRAME_EMBEDDING_ALLOW" default:"SAMEORIGIN"` + + // HTTPS/TLS settings + HTTPSEnabled bool `envconfig:"HTTPS_ENABLED" default:"false"` + TLSCertFile string `envconfig:"TLS_CERT_FILE" default:""` + TLSKeyFile string `envconfig:"TLS_KEY_FILE" default:""` // Update settings UpdateChannel string `envconfig:"UPDATE_CHANNEL" default:"stable"` @@ -346,6 +351,20 @@ func Load() (*Config, error) { } log.Debug().Bool("is_hashed", IsPasswordHashed(authPass)).Msg("Loaded auth password from env var") } + // HTTPS/TLS configuration from environment + if httpsEnabled := os.Getenv("HTTPS_ENABLED"); httpsEnabled != "" { + cfg.HTTPSEnabled = httpsEnabled == "true" || httpsEnabled == "1" + log.Debug().Bool("enabled", cfg.HTTPSEnabled).Msg("HTTPS enabled status from env var") + } + if tlsCertFile := os.Getenv("TLS_CERT_FILE"); tlsCertFile != "" { + cfg.TLSCertFile = tlsCertFile + log.Debug().Str("cert_file", tlsCertFile).Msg("TLS cert file from env var") + } + if tlsKeyFile := os.Getenv("TLS_KEY_FILE"); tlsKeyFile != "" { + cfg.TLSKeyFile = tlsKeyFile + log.Debug().Str("key_file", tlsKeyFile).Msg("TLS key file from env var") + } + // REMOVED: Update channel, auto-update, connection timeout, and allowed origins env vars // These settings now ONLY come from system.json to prevent confusion // Only keeping essential deployment/infrastructure env vars