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>
This commit is contained in:
Martin 2025-06-26 12:06:44 +02:00 committed by GitHub
parent 1562f88c35
commit 59f5b07281
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 68 additions and 36 deletions

View File

@ -67,6 +67,7 @@ distros
dnf dnf
dnsbl dnsbl
dnserr dnserr
domainhere
dracula dracula
dronebl dronebl
droneblresponse droneblresponse
@ -145,6 +146,7 @@ JWTs
kagi kagi
kagibot kagibot
keikaku keikaku
Keyfunc
keypair keypair
KHTML KHTML
kinda kinda
@ -313,4 +315,5 @@ yourdomain
yoursite yoursite
Zenos Zenos
zizmor zizmor
Zonbocom
zos zos

View File

@ -48,6 +48,7 @@ var (
cookieDomain = flag.String("cookie-domain", "", "if set, the top-level domain that the Anubis cookie will be valid for") 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") 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") 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") 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") 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") metricsBind = flag.String("metrics-bind", ":9090", "network address to bind metrics to")
@ -290,11 +291,15 @@ func main() {
"this may result in unexpected behavior") "this may result in unexpected behavior")
} }
var priv ed25519.PrivateKey var ed25519Priv ed25519.PrivateKey
if *ed25519PrivateKeyHex != "" && *ed25519PrivateKeyHexFile != "" { 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") log.Fatal("do not specify both ED25519_PRIVATE_KEY_HEX and ED25519_PRIVATE_KEY_HEX_FILE")
} else if *ed25519PrivateKeyHex != "" { } else if *ed25519PrivateKeyHex != "" {
priv, err = keyFromHex(*ed25519PrivateKeyHex) ed25519Priv, err = keyFromHex(*ed25519PrivateKeyHex)
if err != nil { if err != nil {
log.Fatalf("failed to parse and validate ED25519_PRIVATE_KEY_HEX: %v", err) 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) 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 { if err != nil {
log.Fatalf("failed to parse and validate content of ED25519_PRIVATE_KEY_HEX_FILE: %v", err) log.Fatalf("failed to parse and validate content of ED25519_PRIVATE_KEY_HEX_FILE: %v", err)
} }
} else { } else {
_, priv, err = ed25519.GenerateKey(rand.Reader) _, ed25519Priv, err = ed25519.GenerateKey(rand.Reader)
if err != nil { if err != nil {
log.Fatalf("failed to generate ed25519 key: %v", err) log.Fatalf("failed to generate ed25519 key: %v", err)
} }
@ -346,7 +351,8 @@ func main() {
Next: rp, Next: rp,
Policy: policy, Policy: policy,
ServeRobotsTXT: *robotsTxt, ServeRobotsTXT: *robotsTxt,
PrivateKey: priv, ED25519PrivateKey: ed25519Priv,
HS512Secret: []byte(*hs512Secret),
CookieDomain: *cookieDomain, CookieDomain: *cookieDomain,
CookieExpiration: *cookieExpiration, CookieExpiration: *cookieExpiration,
CookiePartitioned: *cookiePartitioned, CookiePartitioned: *cookiePartitioned,

View File

@ -45,6 +45,7 @@ And some cleanups/refactors were added:
- Make progress bar styling more compatible (UXP, etc) - 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 - 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 - 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. 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.

View File

@ -93,11 +93,12 @@ If you don't know or understand what these settings mean, ignore them. These are
::: :::
| Environment Variable | Default value | Explanation | | Environment Variable | Default value | Explanation |
| :---------------------------- | :------------ | :-------------------------------------------------------------------------------------------------------------------------------------------------- | | :---------------------------- | :------------ | :---------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `TARGET_SNI` | unset | If set, overrides the TLS handshake hostname in requests forwarded to `TARGET`. | | `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_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. | | `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. |
</details> </details>

View File

@ -63,19 +63,37 @@ var (
) )
type Server struct { type Server struct {
next http.Handler next http.Handler
mux *http.ServeMux mux *http.ServeMux
policy *policy.ParsedConfig policy *policy.ParsedConfig
DNSBLCache *decaymap.Impl[string, dnsbl.DroneBLResponse] DNSBLCache *decaymap.Impl[string, dnsbl.DroneBLResponse]
OGTags *ogtags.OGTagCache OGTags *ogtags.OGTagCache
cookieName string cookieName string
priv ed25519.PrivateKey ed25519Priv ed25519.PrivateKey
pub ed25519.PublicKey hs512Secret []byte
opts Options 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 { 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( challengeData := fmt.Sprintf(
"X-Real-IP=%s,User-Agent=%s,WeekTime=%s,Fingerprint=%x,Difficulty=%d", "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 return
} }
token, err := jwt.ParseWithClaims(ckie.Value, jwt.MapClaims{}, func(token *jwt.Token) (interface{}, error) { token, err := jwt.ParseWithClaims(ckie.Value, jwt.MapClaims{}, s.getTokenKeyfunc(), jwt.WithExpirationRequired(), jwt.WithStrictDecoding())
return s.pub, nil
}, jwt.WithExpirationRequired(), jwt.WithStrictDecoding())
if err != nil || !token.Valid { if err != nil || !token.Valid {
lg.Debug("invalid token", "path", r.URL.Path, "err", err) lg.Debug("invalid token", "path", r.URL.Path, "err", err)

View File

@ -36,7 +36,8 @@ type Options struct {
BasePrefix string BasePrefix string
WebmasterEmail string WebmasterEmail string
RedirectDomains []string RedirectDomains []string
PrivateKey ed25519.PrivateKey ED25519PrivateKey ed25519.PrivateKey
HS512Secret []byte
CookieExpiration time.Duration CookieExpiration time.Duration
StripBasePrefix bool StripBasePrefix bool
OpenGraph config.OpenGraph OpenGraph config.OpenGraph
@ -88,13 +89,13 @@ func LoadPoliciesOrDefault(ctx context.Context, fname string, defaultDifficulty
} }
func New(opts Options) (*Server, error) { 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") slog.Debug("opts.PrivateKey not set, generating a new one")
_, priv, err := ed25519.GenerateKey(rand.Reader) _, priv, err := ed25519.GenerateKey(rand.Reader)
if err != nil { if err != nil {
return nil, fmt.Errorf("lib: can't generate private key: %v", err) return nil, fmt.Errorf("lib: can't generate private key: %v", err)
} }
opts.PrivateKey = priv opts.ED25519PrivateKey = priv
} }
anubis.BasePrefix = opts.BasePrefix anubis.BasePrefix = opts.BasePrefix
@ -106,14 +107,14 @@ func New(opts Options) (*Server, error) {
} }
result := &Server{ result := &Server{
next: opts.Next, next: opts.Next,
priv: opts.PrivateKey, ed25519Priv: opts.ED25519PrivateKey,
pub: opts.PrivateKey.Public().(ed25519.PublicKey), hs512Secret: opts.HS512Secret,
policy: opts.Policy, policy: opts.Policy,
opts: opts, opts: opts,
DNSBLCache: decaymap.New[string, dnsbl.DroneBLResponse](), DNSBLCache: decaymap.New[string, dnsbl.DroneBLResponse](),
OGTags: ogtags.NewOGTagCache(opts.Target, opts.Policy.OpenGraph), OGTags: ogtags.NewOGTagCache(opts.Target, opts.Policy.OpenGraph),
cookieName: cookieName, cookieName: cookieName,
} }
mux := http.NewServeMux() mux := http.NewServeMux()

View File

@ -201,5 +201,9 @@ func (s *Server) signJWT(claims jwt.MapClaims) (string, error) {
claims["nbf"] = time.Now().Add(-1 * time.Minute).Unix() claims["nbf"] = time.Now().Add(-1 * time.Minute).Unix()
claims["exp"] = time.Now().Add(s.opts.CookieExpiration).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)
}
} }