From 68bebf7db4be258cea4ad0397c22e21bf1e6016e Mon Sep 17 00:00:00 2001 From: Martin Weidenauer Date: Thu, 19 Jun 2025 01:42:00 +0200 Subject: [PATCH 1/7] Add JWTRestrictionHeader funktionality --- cmd/anubis/main.go | 8 +++++--- lib/anubis.go | 40 ++++++++++++++++++++++++++++++++++------ lib/config.go | 1 + 3 files changed, 40 insertions(+), 9 deletions(-) diff --git a/cmd/anubis/main.go b/cmd/anubis/main.go index bc21473e..0195d548 100644 --- a/cmd/anubis/main.go +++ b/cmd/anubis/main.go @@ -73,9 +73,10 @@ var ( versionFlag = flag.Bool("version", false, "print Anubis version") xffStripPrivate = flag.Bool("xff-strip-private", true, "if set, strip private addresses from X-Forwarded-For") - 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") - thothToken = flag.String("thoth-token", "", "if set, API token for Thoth, the IP reputation database for Anubis") + 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") + thothToken = flag.String("thoth-token", "", "if set, API token for Thoth, the IP reputation database for Anubis") + jwtRestrictionHeader = flag.String("jwt-restriction-header", "", "If set, the JWT is only valid if the current value of this header matched the value when the JWT was created") ) func keyFromHex(value string) (ed25519.PrivateKey, error) { @@ -347,6 +348,7 @@ func main() { Target: *target, WebmasterEmail: *webmasterEmail, OGCacheConsidersHost: *ogCacheConsiderHost, + JWTRestrictionHeader: *jwtRestrictionHeader, }) if err != nil { log.Fatalf("can't construct libanubis.Server: %v", err) diff --git a/lib/anubis.go b/lib/anubis.go index c8945fba..ee580ed3 100644 --- a/lib/anubis.go +++ b/lib/anubis.go @@ -188,6 +188,13 @@ func (s *Server) maybeReverseProxy(w http.ResponseWriter, r *http.Request, httpS return } + if s.opts.JWTRestrictionHeader != "" && claims["restriction"] != internal.SHA256sum(r.Header.Get(s.opts.JWTRestrictionHeader)) { + lg.Debug("JWT restriction header is invalid") + s.ClearCookie(w, s.cookieName, cookiePath) + s.RenderIndex(w, r, rule, httpStatusOnly) + return + } + r.Header.Add("X-Anubis-Status", "PASS") s.ServeHTTPNext(w, r) } @@ -383,12 +390,33 @@ func (s *Server) PassChallenge(w http.ResponseWriter, r *http.Request) { } // generate JWT cookie - tokenString, err := s.signJWT(jwt.MapClaims{ - "challenge": challengeStr, - "method": rule.Challenge.Algorithm, - "policyRule": rule.Hash(), - "action": string(cr.Rule), - }) + var tokenString string + + // check if JWTRestrictionHeader is set and header is in request + if s.opts.JWTRestrictionHeader != "" { + if r.Header.Get(s.opts.JWTRestrictionHeader) == "" { + lg.Error("JWTRestrictionHeader is set in config but not found in request, please check your reverse proxy config.") + s.ClearCookie(w, s.cookieName, cookiePath) + s.respondWithError(w, r, "failed to sign JWT") + return + } else { + tokenString, err = s.signJWT(jwt.MapClaims{ + "challenge": challengeStr, + "method": rule.Challenge.Algorithm, + "policyRule": rule.Hash(), + "action": string(cr.Rule), + "restriction": internal.SHA256sum(r.Header.Get(s.opts.JWTRestrictionHeader)), + }) + } + } else { + tokenString, err = s.signJWT(jwt.MapClaims{ + "challenge": challengeStr, + "method": rule.Challenge.Algorithm, + "policyRule": rule.Hash(), + "action": string(cr.Rule), + }) + } + if err != nil { lg.Error("failed to sign JWT", "err", err) s.ClearCookie(w, s.cookieName, cookiePath) diff --git a/lib/config.go b/lib/config.go index a893a437..1de3452d 100644 --- a/lib/config.go +++ b/lib/config.go @@ -42,6 +42,7 @@ type Options struct { OGPassthrough bool CookiePartitioned bool ServeRobotsTXT bool + JWTRestrictionHeader string } func LoadPoliciesOrDefault(ctx context.Context, fname string, defaultDifficulty int) (*policy.ParsedConfig, error) { From 22f32dc04904fdc8976f9f627cafaba35bf54fa1 Mon Sep 17 00:00:00 2001 From: Martin Weidenauer Date: Thu, 19 Jun 2025 01:42:24 +0200 Subject: [PATCH 2/7] Add JWTRestrictionHeader to docs --- docs/docs/CHANGELOG.md | 1 + docs/docs/admin/installation.mdx | 1 + 2 files changed, 2 insertions(+) diff --git a/docs/docs/CHANGELOG.md b/docs/docs/CHANGELOG.md index d714c940..fa4ead09 100644 --- a/docs/docs/CHANGELOG.md +++ b/docs/docs/CHANGELOG.md @@ -27,6 +27,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Add `robots2policy` CLI utility to convert robots.txt files to Anubis challenge policies using CEL expressions ([#409](https://github.com/TecharoHQ/anubis/issues/409)) - Implement GeoIP and ASN based checks via [Thoth](https://anubis.techaro.lol/docs/admin/thoth) ([#206](https://github.com/TecharoHQ/anubis/issues/206)) - Replace internal SHA256 hashing with xxhash for 4-6x performance improvement in policy evaluation and cache operations +- Add optional restrictions based on the value of a header ## v1.19.1: Jenomis cen Lexentale - Echo 1 diff --git a/docs/docs/admin/installation.mdx b/docs/docs/admin/installation.mdx index 60f49fcd..32c6d7a5 100644 --- a/docs/docs/admin/installation.mdx +++ b/docs/docs/admin/installation.mdx @@ -87,6 +87,7 @@ If you don't know or understand what these settings mean, ignore them. These are | `TARGET_SNI` | unset | If set, overrides the TLS handshake hostname in requests forwarded to `TARGET`. | | `TARGET_HOST` | unset | If set, overrides the Host header in requests forwarded to `TARGET`. | | `TARGET_INSECURE_SKIP_VERIFY` | `false` | If `true`, skip TLS certificate validation for targets that listen over `https`. If your backend does not listen over `https`, ignore this setting. | +| `JWT_RESTRICTION_HEADER | unset | If set, the JWT is only valid if the current value of this header matched the value when the JWT was created. Example: `X-Real-IP` | From 624295df02ca6ced8692b354af9a8798326f3043 Mon Sep 17 00:00:00 2001 From: Martin Weidenauer Date: Thu, 19 Jun 2025 22:14:22 +0200 Subject: [PATCH 3/7] Move JWT_RESTRICTION_HEADER from advanced section to normal one --- docs/docs/admin/installation.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/docs/admin/installation.mdx b/docs/docs/admin/installation.mdx index 32c6d7a5..cbccdded 100644 --- a/docs/docs/admin/installation.mdx +++ b/docs/docs/admin/installation.mdx @@ -72,6 +72,7 @@ Anubis uses these environment variables for configuration: | `USE_REMOTE_ADDRESS` | unset | If set to `true`, Anubis will take the client's IP from the network socket. For production deployments, it is expected that a reverse proxy is used in front of Anubis, which pass the IP using headers, instead. | | `WEBMASTER_EMAIL` | unset | If set, shows a contact email address when rendering error pages. This email address will be how users can get in contact with administrators. | | `XFF_STRIP_PRIVATE` | `true` | If set, strip private addresses from `X-Forwarded-For` headers. To unset this, you must set `XFF_STRIP_PRIVATE=false` or `--xff-strip-private=false`. | +| `JWT_RESTRICTION_HEADER` | unset | If set, the JWT is only valid if the current value of this header matches the value when the JWT was created. You can use it e.g. to restrict a JWT to the source IP of the user using `X-Real-IP`. |
Advanced configuration settings @@ -87,7 +88,6 @@ If you don't know or understand what these settings mean, ignore them. These are | `TARGET_SNI` | unset | If set, overrides the TLS handshake hostname in requests forwarded to `TARGET`. | | `TARGET_HOST` | unset | If set, overrides the Host header in requests forwarded to `TARGET`. | | `TARGET_INSECURE_SKIP_VERIFY` | `false` | If `true`, skip TLS certificate validation for targets that listen over `https`. If your backend does not listen over `https`, ignore this setting. | -| `JWT_RESTRICTION_HEADER | unset | If set, the JWT is only valid if the current value of this header matched the value when the JWT was created. Example: `X-Real-IP` |
From 0e06c1aa7f4804b0a2997f69cb167d74cb36e171 Mon Sep 17 00:00:00 2001 From: Martin Weidenauer Date: Thu, 19 Jun 2025 22:32:42 +0200 Subject: [PATCH 4/7] Add rull request URL to Changelog --- docs/docs/CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/docs/CHANGELOG.md b/docs/docs/CHANGELOG.md index fa4ead09..cd3008f0 100644 --- a/docs/docs/CHANGELOG.md +++ b/docs/docs/CHANGELOG.md @@ -27,7 +27,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Add `robots2policy` CLI utility to convert robots.txt files to Anubis challenge policies using CEL expressions ([#409](https://github.com/TecharoHQ/anubis/issues/409)) - Implement GeoIP and ASN based checks via [Thoth](https://anubis.techaro.lol/docs/admin/thoth) ([#206](https://github.com/TecharoHQ/anubis/issues/206)) - Replace internal SHA256 hashing with xxhash for 4-6x performance improvement in policy evaluation and cache operations -- Add optional restrictions based on the value of a header +- Add optional restrictions for JWT based on the value of a header ([#697](https://github.com/TecharoHQ/anubis/pull/697)) ## v1.19.1: Jenomis cen Lexentale - Echo 1 From 34537f4aefb79d5e8aa460fea980e8c83c6b5587 Mon Sep 17 00:00:00 2001 From: Martin Weidenauer Date: Fri, 20 Jun 2025 00:33:40 +0200 Subject: [PATCH 5/7] Set default value of JWT_RESTRICTION_HEADER to X-Real-IP --- cmd/anubis/main.go | 2 +- docs/docs/admin/installation.mdx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/cmd/anubis/main.go b/cmd/anubis/main.go index 349d6fea..3416917c 100644 --- a/cmd/anubis/main.go +++ b/cmd/anubis/main.go @@ -76,7 +76,7 @@ var ( 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") thothToken = flag.String("thoth-token", "", "if set, API token for Thoth, the IP reputation database for Anubis") - jwtRestrictionHeader = flag.String("jwt-restriction-header", "", "If set, the JWT is only valid if the current value of this header matched the value when the JWT was created") + jwtRestrictionHeader = flag.String("jwt-restriction-header", "X-Real-IP", "If set, the JWT is only valid if the current value of this header matched the value when the JWT was created") ) func keyFromHex(value string) (ed25519.PrivateKey, error) { diff --git a/docs/docs/admin/installation.mdx b/docs/docs/admin/installation.mdx index a5f5e8a1..6a66d55e 100644 --- a/docs/docs/admin/installation.mdx +++ b/docs/docs/admin/installation.mdx @@ -83,7 +83,7 @@ Anubis uses these environment variables for configuration: | `USE_REMOTE_ADDRESS` | unset | If set to `true`, Anubis will take the client's IP from the network socket. For production deployments, it is expected that a reverse proxy is used in front of Anubis, which pass the IP using headers, instead. | | `WEBMASTER_EMAIL` | unset | If set, shows a contact email address when rendering error pages. This email address will be how users can get in contact with administrators. | | `XFF_STRIP_PRIVATE` | `true` | If set, strip private addresses from `X-Forwarded-For` headers. To unset this, you must set `XFF_STRIP_PRIVATE=false` or `--xff-strip-private=false`. | -| `JWT_RESTRICTION_HEADER` | unset | If set, the JWT is only valid if the current value of this header matches the value when the JWT was created. You can use it e.g. to restrict a JWT to the source IP of the user using `X-Real-IP`. | +| `JWT_RESTRICTION_HEADER` | `X-Real-IP` | If set, the JWT is only valid if the current value of this header matches the value when the JWT was created. You can use it e.g. to restrict a JWT to the source IP of the user using `X-Real-IP`. |
Advanced configuration settings From 90caf60cb236642f55850fee344a5b9c4ec5374b Mon Sep 17 00:00:00 2001 From: Martin Weidenauer Date: Fri, 20 Jun 2025 00:56:18 +0200 Subject: [PATCH 6/7] Remove OpenGraph from Options to match main branch and make it compilable after resolved conflicts --- cmd/anubis/main.go | 2 -- 1 file changed, 2 deletions(-) diff --git a/cmd/anubis/main.go b/cmd/anubis/main.go index 3416917c..463de924 100644 --- a/cmd/anubis/main.go +++ b/cmd/anubis/main.go @@ -351,8 +351,6 @@ func main() { CookieDomain: *cookieDomain, CookieExpiration: *cookieExpiration, CookiePartitioned: *cookiePartitioned, - OGPassthrough: *ogPassthrough, - OGTimeToLive: *ogTimeToLive, RedirectDomains: redirectDomainsList, Target: *target, WebmasterEmail: *webmasterEmail, From c672c8f1d4ffa08222be8a0360514175e05382d3 Mon Sep 17 00:00:00 2001 From: Martin Weidenauer Date: Fri, 20 Jun 2025 00:57:47 +0200 Subject: [PATCH 7/7] Fix indents in lib/config.go --- lib/config.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/config.go b/lib/config.go index 395a2527..99e05d86 100644 --- a/lib/config.go +++ b/lib/config.go @@ -38,7 +38,7 @@ type Options struct { PrivateKey ed25519.PrivateKey CookieExpiration time.Duration StripBasePrefix bool - OpenGraph config.OpenGraph + OpenGraph config.OpenGraph CookiePartitioned bool ServeRobotsTXT bool JWTRestrictionHeader string