diff --git a/cmd/anubis/main.go b/cmd/anubis/main.go index 59dd56f91..c1efe241f 100644 --- a/cmd/anubis/main.go +++ b/cmd/anubis/main.go @@ -83,6 +83,7 @@ var ( versionFlag = flag.Bool("version", false, "print Anubis version") publicUrl = flag.String("public-url", "", "the externally accessible URL for this Anubis instance, used for constructing redirect URLs (e.g., for forwardAuth).") xffStripPrivate = flag.Bool("xff-strip-private", true, "if set, strip private addresses from X-Forwarded-For") + customRealIPHeader = flag.String("custom-real-ip-header", "", "if set, read remote IP from header of this name (in case your environment doesn't set X-Real-IP header)") thothInsecure = flag.Bool("thoth-insecure", false, "if set, connect to Thoth over plain HTTP/2, don't enable this unless support told you to") thothURL = flag.String("thoth-url", "", "if set, URL for Thoth, the IP reputation database for Anubis") @@ -460,6 +461,7 @@ func main() { var h http.Handler h = s + h = internal.CustomRealIPHeader(*customRealIPHeader, h) h = internal.RemoteXRealIP(*useRemoteAddress, *bindNetwork, h) h = internal.XForwardedForToXRealIP(h) h = internal.XForwardedForUpdate(*xffStripPrivate, h) diff --git a/docs/docs/CHANGELOG.md b/docs/docs/CHANGELOG.md index 8b2c9aa63..38aec56dd 100644 --- a/docs/docs/CHANGELOG.md +++ b/docs/docs/CHANGELOG.md @@ -13,6 +13,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 +- Add `-custom-real-ip-header` flag to get the original request IP from a different header than `x-real-ip`. - Add `contentLength` variable to bot expressions. - Add `COOKIE_SAME_SITE_MODE` to force anubis cookies SameSite value, and downgrade automatically from `None` to `Lax` if cookie is insecure. - Fix lock convoy problem in decaymap ([#1103](https://github.com/TecharoHQ/anubis/issues/1103)). diff --git a/docs/docs/admin/caveats-xff.mdx b/docs/docs/admin/caveats-xff.mdx index 655534f3e..54b04f5e8 100644 --- a/docs/docs/admin/caveats-xff.mdx +++ b/docs/docs/admin/caveats-xff.mdx @@ -20,6 +20,8 @@ Upstream: X-Forwarded-For: CF_IP As a workaround, you should configure your web server to parse an alternative source (such as `CF-Connecting-IP`), or pre-process the incoming `X-Forwarded-For` with your web server to ensure it only contains the real client IP address, then pass it to Anubis as `X-Forwarded-For`. +If you do not control the web server upstream of Anubis, the `custom-real-ip-header` command line flag accepts a header value that Anubis will read the real client IP address from. Anubis will set the `X-Real-IP` header to the IP address found in the custom header. + The `X-Real-IP` header will be automatically inferred from `X-Forwarded-For` if not set, setting it explicitly is not necessary as long as `X-Forwarded-For` contains only the real client IP. However setting it explicitly can eliminate spoofed values if your web server doesn't set this. See [Cloudflare](environments/cloudflare.mdx) for an example configuration. diff --git a/docs/docs/admin/installation.mdx b/docs/docs/admin/installation.mdx index b82fe7b93..96f305c74 100644 --- a/docs/docs/admin/installation.mdx +++ b/docs/docs/admin/installation.mdx @@ -76,6 +76,7 @@ Anubis uses these environment variables for configuration: | `COOKIE_DOMAIN` | unset | The domain the Anubis challenge pass cookie should be set to. This should be set to the domain you bought from your registrar (EG: `techaro.lol` if your webapp is running on `anubis.techaro.lol`). See this [stackoverflow explanation of cookies](https://stackoverflow.com/a/1063760) for more information.

Note that unlike `REDIRECT_DOMAINS`, you should never include a port number in this variable. | | `COOKIE_DYNAMIC_DOMAIN` | false | If set to true, automatically set cookie domain fields based on the hostname of the request. EG: if you are making a request to `anubis.techaro.lol`, the Anubis cookie will be valid for any subdomain of `techaro.lol`. | | `COOKIE_EXPIRATION_TIME` | `168h` | The amount of time the authorization cookie is valid for. | +| `CUSTOM_REAL_IP_HEADER` | unset | If set, Anubis will read the client's real IP address from this header, and set it in `X-Real-IP` header. | | `COOKIE_PARTITIONED` | `false` | If set to `true`, enables the [partitioned (CHIPS) flag](https://developers.google.com/privacy-sandbox/cookies/chips), meaning that Anubis inside an iframe has a different set of cookies than the domain hosting the iframe. | | `COOKIE_PREFIX` | `anubis-cookie` | The prefix used for browser cookies created by Anubis. Useful for customization or avoiding conflicts with other applications. | | `COOKIE_SECURE` | `true` | If set to `true`, enables the [Secure flag](https://developer.mozilla.org/en-US/docs/Web/HTTP/Guides/Cookies#block_access_to_your_cookies), meaning that the cookies will only be transmitted over HTTPS. If Anubis is used in an unsecure context (plain HTTP), this will be need to be set to false | diff --git a/internal/headers.go b/internal/headers.go index 8b478665c..21601d246 100644 --- a/internal/headers.go +++ b/internal/headers.go @@ -38,6 +38,22 @@ func UnchangingCache(next http.Handler) http.Handler { }) } +// CustomXRealIPHeader sets the X-Real-IP header to the value of a +// different header. +// Used in environments where the upstream proxy sets the request's +// origin IP in a custom header. +func CustomRealIPHeader(customRealIPHeaderValue string, next http.Handler) http.Handler { + if customRealIPHeaderValue == "" { + slog.Debug("skipping middleware, customRealIPHeaderValue is empty") + return next + } + + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + r.Header.Set("X-Real-IP", r.Header.Get(customRealIPHeaderValue)) + next.ServeHTTP(w, r) + }) +} + // RemoteXRealIP sets the X-Real-Ip header to the request's real IP if // the setting is enabled by the user. func RemoteXRealIP(useRemoteAddress bool, bindNetwork string, next http.Handler) http.Handler {