Weighs the soul of incoming HTTP requests to stop AI crawlers
11
fork

Configure Feed

Select the types of activity you want to include in your feed.

feat: Add option to use HS512 secret for JWT instead of ED25519 (#680)

* Add functionality for HS512 JWT tokens

* Add HS512_SECRET to installation docs

* Update CHANGELOG.md regarding HS512

* Move HS512_SECRET to advenced section in docs

* Move token Keyfunc logic to Server function

* Add Keyfunc to spelling

* chore: spelling

Signed-off-by: Xe Iaso <me@xeiaso.net>

---------

Signed-off-by: Xe Iaso <me@xeiaso.net>
Co-authored-by: Martin Weidenauer <mweidenauer@nanx0as46153.anx.local>
Co-authored-by: Xe Iaso <me@xeiaso.net>

authored by

Martin
Martin Weidenauer
Xe Iaso
and committed by
GitHub
59f5b072 1562f88c

+68 -36
+3
.github/actions/spelling/expect.txt
··· 67 67 dnf 68 68 dnsbl 69 69 dnserr 70 + domainhere 70 71 dracula 71 72 dronebl 72 73 droneblresponse ··· 145 146 kagi 146 147 kagibot 147 148 keikaku 149 + Keyfunc 148 150 keypair 149 151 KHTML 150 152 kinda ··· 313 315 yoursite 314 316 Zenos 315 317 zizmor 318 + Zonbocom 316 319 zos
+12 -6
cmd/anubis/main.go
··· 48 48 cookieDomain = flag.String("cookie-domain", "", "if set, the top-level domain that the Anubis cookie will be valid for") 49 49 cookieExpiration = flag.Duration("cookie-expiration-time", anubis.CookieDefaultExpirationTime, "The amount of time the authorization cookie is valid for") 50 50 cookiePartitioned = flag.Bool("cookie-partitioned", false, "if true, sets the partitioned flag on Anubis cookies, enabling CHIPS support") 51 + hs512Secret = flag.String("hs512-secret", "", "secret used to sign JWTs, uses ed25519 if not set") 51 52 ed25519PrivateKeyHex = flag.String("ed25519-private-key-hex", "", "private key used to sign JWTs, if not set a random one will be assigned") 52 53 ed25519PrivateKeyHexFile = flag.String("ed25519-private-key-hex-file", "", "file name containing value for ed25519-private-key-hex") 53 54 metricsBind = flag.String("metrics-bind", ":9090", "network address to bind metrics to") ··· 290 291 "this may result in unexpected behavior") 291 292 } 292 293 293 - var priv ed25519.PrivateKey 294 - if *ed25519PrivateKeyHex != "" && *ed25519PrivateKeyHexFile != "" { 294 + var ed25519Priv ed25519.PrivateKey 295 + if *hs512Secret != "" && (*ed25519PrivateKeyHex != "" || *ed25519PrivateKeyHexFile != "") { 296 + log.Fatal("do not specify both HS512 and ED25519 secrets") 297 + } else if *hs512Secret != "" { 298 + ed25519Priv = ed25519.PrivateKey(*hs512Secret) 299 + } else if *ed25519PrivateKeyHex != "" && *ed25519PrivateKeyHexFile != "" { 295 300 log.Fatal("do not specify both ED25519_PRIVATE_KEY_HEX and ED25519_PRIVATE_KEY_HEX_FILE") 296 301 } else if *ed25519PrivateKeyHex != "" { 297 - priv, err = keyFromHex(*ed25519PrivateKeyHex) 302 + ed25519Priv, err = keyFromHex(*ed25519PrivateKeyHex) 298 303 if err != nil { 299 304 log.Fatalf("failed to parse and validate ED25519_PRIVATE_KEY_HEX: %v", err) 300 305 } ··· 304 309 log.Fatalf("failed to read ED25519_PRIVATE_KEY_HEX_FILE %s: %v", *ed25519PrivateKeyHexFile, err) 305 310 } 306 311 307 - priv, err = keyFromHex(string(bytes.TrimSpace(hexFile))) 312 + ed25519Priv, err = keyFromHex(string(bytes.TrimSpace(hexFile))) 308 313 if err != nil { 309 314 log.Fatalf("failed to parse and validate content of ED25519_PRIVATE_KEY_HEX_FILE: %v", err) 310 315 } 311 316 } else { 312 - _, priv, err = ed25519.GenerateKey(rand.Reader) 317 + _, ed25519Priv, err = ed25519.GenerateKey(rand.Reader) 313 318 if err != nil { 314 319 log.Fatalf("failed to generate ed25519 key: %v", err) 315 320 } ··· 346 351 Next: rp, 347 352 Policy: policy, 348 353 ServeRobotsTXT: *robotsTxt, 349 - PrivateKey: priv, 354 + ED25519PrivateKey: ed25519Priv, 355 + HS512Secret: []byte(*hs512Secret), 350 356 CookieDomain: *cookieDomain, 351 357 CookieExpiration: *cookieExpiration, 352 358 CookiePartitioned: *cookiePartitioned,
+1
docs/docs/CHANGELOG.md
··· 45 45 - Make progress bar styling more compatible (UXP, etc) 46 46 - Add `--strip-base-prefix` flag/envvar to strip the base prefix from request paths when forwarding to target servers 47 47 - Fix an off-by-one in the default threshold config 48 + - Add functionality for HS512 JWT algorithm 48 49 49 50 Request weight is one of the biggest ticket features in Anubis. This enables Anubis to be much closer to a Web Application Firewall and when combined with custom thresholds allows administrators to have Anubis take advanced reactions. For more information about request weight, see [the request weight section](./admin/policies.mdx#request-weight) of the policy file documentation. 50 51
+6 -5
docs/docs/admin/installation.mdx
··· 93 93 94 94 ::: 95 95 96 - | Environment Variable | Default value | Explanation | 97 - | :---------------------------- | :------------ | :-------------------------------------------------------------------------------------------------------------------------------------------------- | 98 - | `TARGET_SNI` | unset | If set, overrides the TLS handshake hostname in requests forwarded to `TARGET`. | 99 - | `TARGET_HOST` | unset | If set, overrides the Host header in requests forwarded to `TARGET`. | 100 - | `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. | 96 + | Environment Variable | Default value | Explanation | 97 + | :---------------------------- | :------------ | :---------------------------------------------------------------------------------------------------------------------------------------------------------------------- | 98 + | `TARGET_SNI` | unset | If set, overrides the TLS handshake hostname in requests forwarded to `TARGET`. | 99 + | `TARGET_HOST` | unset | If set, overrides the Host header in requests forwarded to `TARGET`. | 100 + | `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. | 101 + | `HS512_SECRET` | unset | Secret string for JWT HS512 algorithm. If this is not set, Anubis will use ED25519 as defined via the variables above. The longer the better; 128 chars should suffice. | 101 102 102 103 </details> 103 104
+29 -13
lib/anubis.go
··· 63 63 ) 64 64 65 65 type Server struct { 66 - next http.Handler 67 - mux *http.ServeMux 68 - policy *policy.ParsedConfig 69 - DNSBLCache *decaymap.Impl[string, dnsbl.DroneBLResponse] 70 - OGTags *ogtags.OGTagCache 71 - cookieName string 72 - priv ed25519.PrivateKey 73 - pub ed25519.PublicKey 74 - opts Options 66 + next http.Handler 67 + mux *http.ServeMux 68 + policy *policy.ParsedConfig 69 + DNSBLCache *decaymap.Impl[string, dnsbl.DroneBLResponse] 70 + OGTags *ogtags.OGTagCache 71 + cookieName string 72 + ed25519Priv ed25519.PrivateKey 73 + hs512Secret []byte 74 + opts Options 75 + } 76 + 77 + func (s *Server) getTokenKeyfunc() jwt.Keyfunc { 78 + // return ED25519 key if HS512 is not set 79 + if len(s.hs512Secret) == 0 { 80 + return func(token *jwt.Token) (interface{}, error) { 81 + return s.ed25519Priv.Public().(ed25519.PublicKey), nil 82 + } 83 + } else { 84 + return func(token *jwt.Token) (interface{}, error) { 85 + return s.hs512Secret, nil 86 + } 87 + } 75 88 } 76 89 77 90 func (s *Server) challengeFor(r *http.Request, difficulty int) string { 78 - fp := sha256.Sum256(s.pub[:]) 91 + var fp [32]byte 92 + if len(s.hs512Secret) == 0 { 93 + fp = sha256.Sum256(s.ed25519Priv.Public().(ed25519.PublicKey)[:]) 94 + } else { 95 + fp = sha256.Sum256(s.hs512Secret) 96 + } 79 97 80 98 challengeData := fmt.Sprintf( 81 99 "X-Real-IP=%s,User-Agent=%s,WeekTime=%s,Fingerprint=%x,Difficulty=%d", ··· 149 167 return 150 168 } 151 169 152 - token, err := jwt.ParseWithClaims(ckie.Value, jwt.MapClaims{}, func(token *jwt.Token) (interface{}, error) { 153 - return s.pub, nil 154 - }, jwt.WithExpirationRequired(), jwt.WithStrictDecoding()) 170 + token, err := jwt.ParseWithClaims(ckie.Value, jwt.MapClaims{}, s.getTokenKeyfunc(), jwt.WithExpirationRequired(), jwt.WithStrictDecoding()) 155 171 156 172 if err != nil || !token.Valid { 157 173 lg.Debug("invalid token", "path", r.URL.Path, "err", err)
+12 -11
lib/config.go
··· 36 36 BasePrefix string 37 37 WebmasterEmail string 38 38 RedirectDomains []string 39 - PrivateKey ed25519.PrivateKey 39 + ED25519PrivateKey ed25519.PrivateKey 40 + HS512Secret []byte 40 41 CookieExpiration time.Duration 41 42 StripBasePrefix bool 42 43 OpenGraph config.OpenGraph ··· 88 89 } 89 90 90 91 func New(opts Options) (*Server, error) { 91 - if opts.PrivateKey == nil { 92 + if opts.ED25519PrivateKey == nil && opts.HS512Secret == nil { 92 93 slog.Debug("opts.PrivateKey not set, generating a new one") 93 94 _, priv, err := ed25519.GenerateKey(rand.Reader) 94 95 if err != nil { 95 96 return nil, fmt.Errorf("lib: can't generate private key: %v", err) 96 97 } 97 - opts.PrivateKey = priv 98 + opts.ED25519PrivateKey = priv 98 99 } 99 100 100 101 anubis.BasePrefix = opts.BasePrefix ··· 106 107 } 107 108 108 109 result := &Server{ 109 - next: opts.Next, 110 - priv: opts.PrivateKey, 111 - pub: opts.PrivateKey.Public().(ed25519.PublicKey), 112 - policy: opts.Policy, 113 - opts: opts, 114 - DNSBLCache: decaymap.New[string, dnsbl.DroneBLResponse](), 115 - OGTags: ogtags.NewOGTagCache(opts.Target, opts.Policy.OpenGraph), 116 - cookieName: cookieName, 110 + next: opts.Next, 111 + ed25519Priv: opts.ED25519PrivateKey, 112 + hs512Secret: opts.HS512Secret, 113 + policy: opts.Policy, 114 + opts: opts, 115 + DNSBLCache: decaymap.New[string, dnsbl.DroneBLResponse](), 116 + OGTags: ogtags.NewOGTagCache(opts.Target, opts.Policy.OpenGraph), 117 + cookieName: cookieName, 117 118 } 118 119 119 120 mux := http.NewServeMux()
+5 -1
lib/http.go
··· 201 201 claims["nbf"] = time.Now().Add(-1 * time.Minute).Unix() 202 202 claims["exp"] = time.Now().Add(s.opts.CookieExpiration).Unix() 203 203 204 - return jwt.NewWithClaims(jwt.SigningMethodEdDSA, claims).SignedString(s.priv) 204 + if len(s.hs512Secret) == 0 { 205 + return jwt.NewWithClaims(jwt.SigningMethodEdDSA, claims).SignedString(s.ed25519Priv) 206 + } else { 207 + return jwt.NewWithClaims(jwt.SigningMethodHS512, claims).SignedString(s.hs512Secret) 208 + } 205 209 }