From 3eeaa6d9a33f9294ade64c8ff26144fba86a379e Mon Sep 17 00:00:00 2001 From: grm Date: Fri, 18 Sep 2026 13:47:49 +0300 Subject: Security: Add HTTPS and TRUST_PROXY settings MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The app only ever speaks plain HTTP, so it could not know the site was served over TLS: generated links were http:// (the /webadmin bounce from a blog host sent the login page over http), the session cookie was never Secure and nothing sent HSTS. HTTPS=true fixes all three; ClearSessionCookie now uses the same attributes as the set, since a browser only replaces a cookie whose Secure flag matches. TRUST_PROXY=true makes the client address the last X-Forwarded-For entry — the one our proxy appended — so throttling and the log see real addresses instead of the proxy's; earlier entries are whatever the client sent and are ignored. Production startup warns when HTTPS is off. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01Sd8UPWrvyYCLj97JexNw3A --- internal/config/config.go | 20 ++++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) (limited to 'internal/config/config.go') diff --git a/internal/config/config.go b/internal/config/config.go index a239aa2..c231860 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -19,6 +19,12 @@ type Config struct { SuperadminPassword string MaxUploadBytes int64 // default per-file limit; the superadmin can override it per blog Dev bool // reload templates/static from disk + // HTTPS says the proxy in front terminates TLS: generated links are + // https://, the session cookie is Secure and HSTS is sent. + HTTPS bool + // TrustProxy says X-Forwarded-For was written by our own proxy, so its + // last entry is the client address (for throttling and the log). + TrustProxy bool } func Load() (*Config, error) { @@ -31,6 +37,8 @@ func Load() (*Config, error) { SuperadminUsername: env("SUPERADMIN_USERNAME", "admin"), SuperadminPassword: env("SUPERADMIN_PASSWORD", ""), Dev: envBool("DEV", false), + HTTPS: envBool("HTTPS", false), + TrustProxy: envBool("TRUST_PROXY", false), } mb, err := strconv.Atoi(env("MAX_UPLOAD_MB", "10")) if err != nil || mb <= 0 { @@ -62,12 +70,20 @@ func (c *Config) BlogURL(sub string) string { if sub == RootSubdomain { return c.RootURL() } - return "http://" + sub + "." + c.HostWithPort() + return c.Scheme() + "://" + sub + "." + c.HostWithPort() } // RootURL returns the public URL of the management site. func (c *Config) RootURL() string { - return "http://" + c.HostWithPort() + return c.Scheme() + "://" + c.HostWithPort() +} + +// Scheme is the one the public reaches the site by. +func (c *Config) Scheme() string { + if c.HTTPS { + return "https" + } + return "http" } func (c *Config) HostWithPort() string { -- cgit v1.2.3