From 7a33e169458e1a79af88c8e389690ee3bc70013a Mon Sep 17 00:00:00 2001 From: binwiederhier Date: Sat, 31 May 2025 23:07:40 -0400 Subject: [PATCH] Cleanup, examples --- cmd/serve.go | 10 ++++++---- docs/config.md | 51 +++++++++++++++++++++++++++++++++++++++++------- server/config.go | 2 +- server/server.go | 4 ++-- server/util.go | 8 ++++---- 5 files changed, 57 insertions(+), 18 deletions(-) diff --git a/cmd/serve.go b/cmd/serve.go index 3745ebce..576e72f0 100644 --- a/cmd/serve.go +++ b/cmd/serve.go @@ -89,8 +89,8 @@ var flagsServe = append( altsrc.NewStringFlag(&cli.StringFlag{Name: "visitor-email-limit-replenish", Aliases: []string{"visitor_email_limit_replenish"}, EnvVars: []string{"NTFY_VISITOR_EMAIL_LIMIT_REPLENISH"}, Value: util.FormatDuration(server.DefaultVisitorEmailLimitReplenish), Usage: "interval at which burst limit is replenished (one per x)"}), altsrc.NewBoolFlag(&cli.BoolFlag{Name: "visitor-subscriber-rate-limiting", Aliases: []string{"visitor_subscriber_rate_limiting"}, EnvVars: []string{"NTFY_VISITOR_SUBSCRIBER_RATE_LIMITING"}, Value: false, Usage: "enables subscriber-based rate limiting"}), altsrc.NewBoolFlag(&cli.BoolFlag{Name: "behind-proxy", Aliases: []string{"behind_proxy", "P"}, EnvVars: []string{"NTFY_BEHIND_PROXY"}, Value: false, Usage: "if set, use forwarded header (e.g. X-Forwarded-For, X-Client-IP) to determine visitor IP address (for rate limiting)"}), - altsrc.NewStringFlag(&cli.StringFlag{Name: "proxy-forwarded-header", Aliases: []string{"proxy_forwarded_header"}, EnvVars: []string{"NTFY_PROXY_FORWARDED_HEADER"}, Value: "X-Forwarded-For", Usage: "if set, use specified header to determine visitor IP address instead of XFF (for rate limiting)"}), - altsrc.NewStringFlag(&cli.StringFlag{Name: "proxy-trusted-addrs", Aliases: []string{"proxy_trusted_addrs"}, EnvVars: []string{"NTFY_PROXY_TRUSTED_ADDRS"}, Value: "", Usage: "comma-separated list of trusted IP addresses to remove from forwarded header"}), + altsrc.NewStringFlag(&cli.StringFlag{Name: "proxy-forwarded-header", Aliases: []string{"proxy_forwarded_header"}, EnvVars: []string{"NTFY_PROXY_FORWARDED_HEADER"}, Value: "X-Forwarded-For", Usage: "use specified header to determine visitor IP address (for rate limiting)"}), + altsrc.NewStringFlag(&cli.StringFlag{Name: "proxy-trusted-addresses", Aliases: []string{"proxy_trusted_addresses"}, EnvVars: []string{"NTFY_PROXY_TRUSTED_ADDRESSES"}, Value: "", Usage: "comma-separated list of trusted IP addresses to remove from forwarded header"}), altsrc.NewStringFlag(&cli.StringFlag{Name: "stripe-secret-key", Aliases: []string{"stripe_secret_key"}, EnvVars: []string{"NTFY_STRIPE_SECRET_KEY"}, Value: "", Usage: "key used for the Stripe API communication, this enables payments"}), altsrc.NewStringFlag(&cli.StringFlag{Name: "stripe-webhook-key", Aliases: []string{"stripe_webhook_key"}, EnvVars: []string{"NTFY_STRIPE_WEBHOOK_KEY"}, Value: "", Usage: "key required to validate the authenticity of incoming webhooks from Stripe"}), altsrc.NewStringFlag(&cli.StringFlag{Name: "billing-contact", Aliases: []string{"billing_contact"}, EnvVars: []string{"NTFY_BILLING_CONTACT"}, Value: "", Usage: "e-mail or website to display in upgrade dialog (only if payments are enabled)"}), @@ -193,7 +193,7 @@ func execServe(c *cli.Context) error { visitorEmailLimitReplenishStr := c.String("visitor-email-limit-replenish") behindProxy := c.Bool("behind-proxy") proxyForwardedHeader := c.String("proxy-forwarded-header") - proxyTrustedAddrs := util.SplitNoEmpty(c.String("proxy-trusted-addrs"), ",") + proxyTrustedAddresses := util.SplitNoEmpty(c.String("proxy-trusted-addresses"), ",") stripeSecretKey := c.String("stripe-secret-key") stripeWebhookKey := c.String("stripe-webhook-key") billingContact := c.String("billing-contact") @@ -322,6 +322,8 @@ func execServe(c *cli.Context) error { } } else if webPushExpiryWarningDuration > 0 && webPushExpiryWarningDuration > webPushExpiryDuration { return errors.New("web push expiry warning duration cannot be higher than web push expiry duration") + } else if behindProxy && proxyForwardedHeader == "" { + return errors.New("if behind-proxy is set, proxy-forwarded-header must also be set") } // Backwards compatibility @@ -421,7 +423,7 @@ func execServe(c *cli.Context) error { conf.VisitorSubscriberRateLimiting = visitorSubscriberRateLimiting conf.BehindProxy = behindProxy conf.ProxyForwardedHeader = proxyForwardedHeader - conf.ProxyTrustedAddrs = proxyTrustedAddrs + conf.ProxyTrustedAddresses = proxyTrustedAddresses conf.StripeSecretKey = stripeSecretKey conf.StripeWebhookKey = stripeWebhookKey conf.BillingContact = billingContact diff --git a/docs/config.md b/docs/config.md index 3c441fc4..3b89f247 100644 --- a/docs/config.md +++ b/docs/config.md @@ -554,15 +554,50 @@ using Let's Encrypt using certbot, or simply because you'd like to share the por Whatever your reasons may be, there are a few things to consider. If you are running ntfy behind a proxy, you should set the `behind-proxy` flag. This will instruct the -[rate limiting](#rate-limiting) logic to use the `X-Forwarded-For` header as the primary identifier for a visitor, -as opposed to the remote IP address. If the `behind-proxy` flag is not set, all visitors will -be counted as one, because from the perspective of the ntfy server, they all share the proxy's IP address. If your proxy or CDN provider uses a custom header to securely pass the source IP/Client IP to your application, you can specify that header instead of using the XFF. Using the custom header (unique per provide/cdn/proxy), will disable the use of the XFF header. +[rate limiting](#rate-limiting) logic to use the header configured in `proxy-forwarded-header` (default is `X-Forwarded-For`) +as the primary identifier for a visitor, as opposed to the remote IP address. -=== "/etc/ntfy/server.yml" +If the `behind-proxy` flag is not set, all visitors will be counted as one, because from the perspective of the +ntfy server, they all share the proxy's IP address. + +Relevant flags to consider: + +* `behind-proxy`: if set, ntfy will use the `proxy-forwarded-header` to identify visitors (default: `false`) +* `proxy-forwarded-header`: the header to use to identify visitors (default: `X-Forwarded-For`) +* `proxy-trusted-addresses`: a comma-separated list of IP addresses that are removed from the forwarded header + to determine the real IP address (default: empty) + +=== "/etc/ntfy/server.yml (behind a proxy)" ``` yaml - # Tell ntfy to use "X-Forwarded-For" to identify visitors + # Tell ntfy to use "X-Forwarded-For" header to identify visitors for rate limiting + # + # Example: If "X-Forwarded-For: 9.9.9.9, 1.2.3.4" is set, + # the visitor IP will be 1.2.3.4 (right-most address). + # behind-proxy: true - proxy-client-ip-header: "X-Client-IP" + ``` + +=== "/etc/ntfy/server.yml (with custom header)" + ``` yaml + # Tell ntfy to use "X-Client-IP" header to identify visitors for rate limiting + # + # Example: If "X-Client-IP: 9.9.9.9" is set, + # the visitor IP will be 9.9.9.9. + # + behind-proxy: true + proxy-forwarded-header: "X-Client-IP" + ``` + +=== "/etc/ntfy/server.yml (multiple proxies)" + ``` yaml + # Tell ntfy to use "X-Forwarded-For" header to identify visitors for rate limiting, + # and to strip the IP addresses of the proxies 1.2.3.4 and 1.2.3.5 + # + # Example: If "X-Forwarded-For: 9.9.9.9, 1.2.3.4" is set, + # the visitor IP will be 9.9.9.9 (right-most unknown address). + # + behind-proxy: true + proxy-trusted-addresses: "1.2.3.4, 1.2.3.5" ``` ### TLS/SSL @@ -1391,7 +1426,9 @@ variable before running the `ntfy` command (e.g. `export NTFY_LISTEN_HTTP=:80`). | `cache-batch-timeout` | `NTFY_CACHE_BATCH_TIMEOUT` | *duration* | 0s | Timeout for batched async writes to the message cache (if zero, writes are synchronous) | | `auth-file` | `NTFY_AUTH_FILE` | *filename* | - | Auth database file used for access control. If set, enables authentication and access control. See [access control](#access-control). | | `auth-default-access` | `NTFY_AUTH_DEFAULT_ACCESS` | `read-write`, `read-only`, `write-only`, `deny-all` | `read-write` | Default permissions if no matching entries in the auth database are found. Default is `read-write`. | -| `behind-proxy` | `NTFY_BEHIND_PROXY` | *bool* | false | If set, the X-Forwarded-For header is used to determine the visitor IP address instead of the remote address of the connection. | +| `behind-proxy` | `NTFY_BEHIND_PROXY` | *bool* | false | If set, use forwarded header (e.g. X-Forwarded-For, X-Client-IP) to determine visitor IP address (for rate limiting) | +| `proxy-forwarded-header` | `NTFY_PROXY_FORWARDED_HEADER` | *string* | `X-Forwarded-For` | Use specified header to determine visitor IP address (for rate limiting) | +| `proxy-trusted-addresses` | `NTFY_PROXY_TRUSTED_ADDRESSES` | *comma-separated list of IPs* | - | Comma-separated list of trusted IP addresses to remove from forwarded header | | `attachment-cache-dir` | `NTFY_ATTACHMENT_CACHE_DIR` | *directory* | - | Cache directory for attached files. To enable attachments, this has to be set. | | `attachment-total-size-limit` | `NTFY_ATTACHMENT_TOTAL_SIZE_LIMIT` | *size* | 5G | Limit of the on-disk attachment cache directory. If the limits is exceeded, new attachments will be rejected. | | `attachment-file-size-limit` | `NTFY_ATTACHMENT_FILE_SIZE_LIMIT` | *size* | 15M | Per-file attachment size limit (e.g. 300k, 2M, 100M). Larger attachment will be rejected. | diff --git a/server/config.go b/server/config.go index c7cfee1f..75e6d488 100644 --- a/server/config.go +++ b/server/config.go @@ -145,7 +145,7 @@ type Config struct { VisitorSubscriberRateLimiting bool // Enable subscriber-based rate limiting for UnifiedPush topics BehindProxy bool // If true, the server will trust the proxy client IP header to determine the client IP address ProxyForwardedHeader string // The header field to read the real/client IP address from, if BehindProxy is true, defaults to "X-Forwarded-For" - ProxyTrustedAddrs []string // List of trusted proxy addresses that will be stripped from the Forwarded header if BehindProxy is true + ProxyTrustedAddresses []string // List of trusted proxy addresses that will be stripped from the Forwarded header if BehindProxy is true StripeSecretKey string StripeWebhookKey string StripePriceCacheDuration time.Duration diff --git a/server/server.go b/server/server.go index e73976b1..e1126757 100644 --- a/server/server.go +++ b/server/server.go @@ -1937,7 +1937,7 @@ func (s *Server) authorizeTopic(next handleFunc, perm user.Permission) handleFun // that subsequent logging calls still have a visitor context. func (s *Server) maybeAuthenticate(r *http.Request) (*visitor, error) { // Read the "Authorization" header value and exit out early if it's not set - ip := extractIPAddress(r, s.config.BehindProxy, s.config.ProxyForwardedHeader, s.config.ProxyTrustedAddrs) + ip := extractIPAddress(r, s.config.BehindProxy, s.config.ProxyForwardedHeader, s.config.ProxyTrustedAddresses) vip := s.visitor(ip, nil) if s.userManager == nil { return vip, nil @@ -2012,7 +2012,7 @@ func (s *Server) authenticateBearerAuth(r *http.Request, token string) (*user.Us if err != nil { return nil, err } - ip := extractIPAddress(r, s.config.BehindProxy, s.config.ProxyForwardedHeader, s.config.ProxyTrustedAddrs) + ip := extractIPAddress(r, s.config.BehindProxy, s.config.ProxyForwardedHeader, s.config.ProxyTrustedAddresses) go s.userManager.EnqueueTokenUpdate(token, &user.TokenUpdate{ LastAccess: time.Now(), LastOrigin: ip, diff --git a/server/util.go b/server/util.go index 936e74f8..34194681 100644 --- a/server/util.go +++ b/server/util.go @@ -74,9 +74,9 @@ func readQueryParam(r *http.Request, names ...string) string { return "" } -func extractIPAddress(r *http.Request, behindProxy bool, proxyForwardedHeader string, proxyTrustedAddrs []string) netip.Addr { +func extractIPAddress(r *http.Request, behindProxy bool, proxyForwardedHeader string, proxyTrustedAddresses []string) netip.Addr { if behindProxy && proxyForwardedHeader != "" { - if addr, err := extractIPAddressFromHeader(r, proxyForwardedHeader, proxyTrustedAddrs); err == nil { + if addr, err := extractIPAddressFromHeader(r, proxyForwardedHeader, proxyTrustedAddresses); err == nil { return addr } // Fall back to the remote address if the header is not found or invalid @@ -94,14 +94,14 @@ func extractIPAddress(r *http.Request, behindProxy bool, proxyForwardedHeader st // X-Forwarded-For can contain multiple addresses (see #328). If we are behind a proxy, // only the right-most address can be trusted (as this is the one added by our proxy server). // See https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-Forwarded-For for details. -func extractIPAddressFromHeader(r *http.Request, forwardedHeader string, trustedAddrs []string) (netip.Addr, error) { +func extractIPAddressFromHeader(r *http.Request, forwardedHeader string, trustedAddresses []string) (netip.Addr, error) { value := strings.TrimSpace(r.Header.Get(forwardedHeader)) if value == "" { return netip.IPv4Unspecified(), fmt.Errorf("no %s header found", forwardedHeader) } addrs := util.Map(util.SplitNoEmpty(value, ","), strings.TrimSpace) clientAddrs := util.Filter(addrs, func(addr string) bool { - return !slices.Contains(trustedAddrs, addr) + return !slices.Contains(trustedAddresses, addr) }) if len(clientAddrs) == 0 { return netip.IPv4Unspecified(), fmt.Errorf("no client IP address found in %s header: %s", forwardedHeader, value)