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 {