mirror of
https://github.com/TecharoHQ/anubis.git
synced 2025-08-04 02:08:59 -04:00
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:
parent
1562f88c35
commit
59f5b07281
3
.github/actions/spelling/expect.txt
vendored
3
.github/actions/spelling/expect.txt
vendored
@ -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
|
||||||
|
@ -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,
|
||||||
|
@ -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.
|
||||||
|
|
||||||
|
@ -94,10 +94,11 @@ 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>
|
||||||
|
|
||||||
|
@ -69,13 +69,31 @@ type Server struct {
|
|||||||
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)
|
||||||
|
@ -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
|
||||||
@ -107,8 +108,8 @@ 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](),
|
||||||
|
@ -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)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user