diff --git a/.github/actions/spelling/expect.txt b/.github/actions/spelling/expect.txt index 909633e..ed3aeab 100644 --- a/.github/actions/spelling/expect.txt +++ b/.github/actions/spelling/expect.txt @@ -67,6 +67,7 @@ distros dnf dnsbl dnserr +domainhere dracula dronebl droneblresponse @@ -145,6 +146,7 @@ JWTs kagi kagibot keikaku +Keyfunc keypair KHTML kinda @@ -313,4 +315,5 @@ yourdomain yoursite Zenos zizmor +Zonbocom zos diff --git a/cmd/anubis/main.go b/cmd/anubis/main.go index 17e771f..f29f91c 100644 --- a/cmd/anubis/main.go +++ b/cmd/anubis/main.go @@ -48,6 +48,7 @@ var ( cookieDomain = flag.String("cookie-domain", "", "if set, the top-level domain that the Anubis cookie will be valid for") cookieExpiration = flag.Duration("cookie-expiration-time", anubis.CookieDefaultExpirationTime, "The amount of time the authorization cookie is valid for") cookiePartitioned = flag.Bool("cookie-partitioned", false, "if true, sets the partitioned flag on Anubis cookies, enabling CHIPS support") + hs512Secret = flag.String("hs512-secret", "", "secret used to sign JWTs, uses ed25519 if not set") ed25519PrivateKeyHex = flag.String("ed25519-private-key-hex", "", "private key used to sign JWTs, if not set a random one will be assigned") ed25519PrivateKeyHexFile = flag.String("ed25519-private-key-hex-file", "", "file name containing value for ed25519-private-key-hex") metricsBind = flag.String("metrics-bind", ":9090", "network address to bind metrics to") @@ -290,11 +291,15 @@ func main() { "this may result in unexpected behavior") } - var priv ed25519.PrivateKey - if *ed25519PrivateKeyHex != "" && *ed25519PrivateKeyHexFile != "" { + var ed25519Priv ed25519.PrivateKey + if *hs512Secret != "" && (*ed25519PrivateKeyHex != "" || *ed25519PrivateKeyHexFile != "") { + log.Fatal("do not specify both HS512 and ED25519 secrets") + } else if *hs512Secret != "" { + ed25519Priv = ed25519.PrivateKey(*hs512Secret) + } else if *ed25519PrivateKeyHex != "" && *ed25519PrivateKeyHexFile != "" { log.Fatal("do not specify both ED25519_PRIVATE_KEY_HEX and ED25519_PRIVATE_KEY_HEX_FILE") } else if *ed25519PrivateKeyHex != "" { - priv, err = keyFromHex(*ed25519PrivateKeyHex) + ed25519Priv, err = keyFromHex(*ed25519PrivateKeyHex) if err != nil { log.Fatalf("failed to parse and validate ED25519_PRIVATE_KEY_HEX: %v", err) } @@ -304,12 +309,12 @@ func main() { log.Fatalf("failed to read ED25519_PRIVATE_KEY_HEX_FILE %s: %v", *ed25519PrivateKeyHexFile, err) } - priv, err = keyFromHex(string(bytes.TrimSpace(hexFile))) + ed25519Priv, err = keyFromHex(string(bytes.TrimSpace(hexFile))) if err != nil { log.Fatalf("failed to parse and validate content of ED25519_PRIVATE_KEY_HEX_FILE: %v", err) } } else { - _, priv, err = ed25519.GenerateKey(rand.Reader) + _, ed25519Priv, err = ed25519.GenerateKey(rand.Reader) if err != nil { log.Fatalf("failed to generate ed25519 key: %v", err) } @@ -346,7 +351,8 @@ func main() { Next: rp, Policy: policy, ServeRobotsTXT: *robotsTxt, - PrivateKey: priv, + ED25519PrivateKey: ed25519Priv, + HS512Secret: []byte(*hs512Secret), CookieDomain: *cookieDomain, CookieExpiration: *cookieExpiration, CookiePartitioned: *cookiePartitioned, diff --git a/docs/docs/CHANGELOG.md b/docs/docs/CHANGELOG.md index 07c04ad..ee0579c 100644 --- a/docs/docs/CHANGELOG.md +++ b/docs/docs/CHANGELOG.md @@ -45,6 +45,7 @@ And some cleanups/refactors were added: - Make progress bar styling more compatible (UXP, etc) - Add `--strip-base-prefix` flag/envvar to strip the base prefix from request paths when forwarding to target servers - Fix an off-by-one in the default threshold config +- Add functionality for HS512 JWT algorithm 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. diff --git a/docs/docs/admin/installation.mdx b/docs/docs/admin/installation.mdx index 862739a..dd810a3 100644 --- a/docs/docs/admin/installation.mdx +++ b/docs/docs/admin/installation.mdx @@ -93,11 +93,12 @@ If you don't know or understand what these settings mean, ignore them. These are ::: -| Environment Variable | Default value | Explanation | -| :---------------------------- | :------------ | :-------------------------------------------------------------------------------------------------------------------------------------------------- | -| `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. | +| Environment Variable | Default value | Explanation | +| :---------------------------- | :------------ | :---------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `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. | +| `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. | diff --git a/lib/anubis.go b/lib/anubis.go index edc3fea..44393fb 100644 --- a/lib/anubis.go +++ b/lib/anubis.go @@ -63,19 +63,37 @@ var ( ) type Server struct { - next http.Handler - mux *http.ServeMux - policy *policy.ParsedConfig - DNSBLCache *decaymap.Impl[string, dnsbl.DroneBLResponse] - OGTags *ogtags.OGTagCache - cookieName string - priv ed25519.PrivateKey - pub ed25519.PublicKey - opts Options + next http.Handler + mux *http.ServeMux + policy *policy.ParsedConfig + DNSBLCache *decaymap.Impl[string, dnsbl.DroneBLResponse] + OGTags *ogtags.OGTagCache + cookieName string + ed25519Priv ed25519.PrivateKey + hs512Secret []byte + opts Options +} + +func (s *Server) getTokenKeyfunc() jwt.Keyfunc { + // return ED25519 key if HS512 is not set + if len(s.hs512Secret) == 0 { + return func(token *jwt.Token) (interface{}, error) { + return s.ed25519Priv.Public().(ed25519.PublicKey), nil + } + } else { + return func(token *jwt.Token) (interface{}, error) { + return s.hs512Secret, nil + } + } } func (s *Server) challengeFor(r *http.Request, difficulty int) string { - fp := sha256.Sum256(s.pub[:]) + var fp [32]byte + if len(s.hs512Secret) == 0 { + fp = sha256.Sum256(s.ed25519Priv.Public().(ed25519.PublicKey)[:]) + } else { + fp = sha256.Sum256(s.hs512Secret) + } challengeData := fmt.Sprintf( "X-Real-IP=%s,User-Agent=%s,WeekTime=%s,Fingerprint=%x,Difficulty=%d", @@ -149,9 +167,7 @@ func (s *Server) maybeReverseProxy(w http.ResponseWriter, r *http.Request, httpS return } - token, err := jwt.ParseWithClaims(ckie.Value, jwt.MapClaims{}, func(token *jwt.Token) (interface{}, error) { - return s.pub, nil - }, jwt.WithExpirationRequired(), jwt.WithStrictDecoding()) + token, err := jwt.ParseWithClaims(ckie.Value, jwt.MapClaims{}, s.getTokenKeyfunc(), jwt.WithExpirationRequired(), jwt.WithStrictDecoding()) if err != nil || !token.Valid { lg.Debug("invalid token", "path", r.URL.Path, "err", err) diff --git a/lib/config.go b/lib/config.go index cecefbc..b882483 100644 --- a/lib/config.go +++ b/lib/config.go @@ -36,7 +36,8 @@ type Options struct { BasePrefix string WebmasterEmail string RedirectDomains []string - PrivateKey ed25519.PrivateKey + ED25519PrivateKey ed25519.PrivateKey + HS512Secret []byte CookieExpiration time.Duration StripBasePrefix bool OpenGraph config.OpenGraph @@ -88,13 +89,13 @@ func LoadPoliciesOrDefault(ctx context.Context, fname string, defaultDifficulty } func New(opts Options) (*Server, error) { - if opts.PrivateKey == nil { + if opts.ED25519PrivateKey == nil && opts.HS512Secret == nil { slog.Debug("opts.PrivateKey not set, generating a new one") _, priv, err := ed25519.GenerateKey(rand.Reader) if err != nil { return nil, fmt.Errorf("lib: can't generate private key: %v", err) } - opts.PrivateKey = priv + opts.ED25519PrivateKey = priv } anubis.BasePrefix = opts.BasePrefix @@ -106,14 +107,14 @@ func New(opts Options) (*Server, error) { } result := &Server{ - next: opts.Next, - priv: opts.PrivateKey, - pub: opts.PrivateKey.Public().(ed25519.PublicKey), - policy: opts.Policy, - opts: opts, - DNSBLCache: decaymap.New[string, dnsbl.DroneBLResponse](), - OGTags: ogtags.NewOGTagCache(opts.Target, opts.Policy.OpenGraph), - cookieName: cookieName, + next: opts.Next, + ed25519Priv: opts.ED25519PrivateKey, + hs512Secret: opts.HS512Secret, + policy: opts.Policy, + opts: opts, + DNSBLCache: decaymap.New[string, dnsbl.DroneBLResponse](), + OGTags: ogtags.NewOGTagCache(opts.Target, opts.Policy.OpenGraph), + cookieName: cookieName, } mux := http.NewServeMux() diff --git a/lib/http.go b/lib/http.go index 46af196..691b1a6 100644 --- a/lib/http.go +++ b/lib/http.go @@ -201,5 +201,9 @@ func (s *Server) signJWT(claims jwt.MapClaims) (string, error) { claims["nbf"] = time.Now().Add(-1 * time.Minute).Unix() claims["exp"] = time.Now().Add(s.opts.CookieExpiration).Unix() - return jwt.NewWithClaims(jwt.SigningMethodEdDSA, claims).SignedString(s.priv) + if len(s.hs512Secret) == 0 { + return jwt.NewWithClaims(jwt.SigningMethodEdDSA, claims).SignedString(s.ed25519Priv) + } else { + return jwt.NewWithClaims(jwt.SigningMethodHS512, claims).SignedString(s.hs512Secret) + } }