From a654c939de84fdb909b923a2e641e2f7e5a28855 Mon Sep 17 00:00:00 2001 From: Alexander Yastrebov Date: Fri, 21 Mar 2025 22:19:04 +0100 Subject: [PATCH] cmd/anubis: derive signing key from configurable secret Add new flag that provides value for Key Derivation Function. This enables multiple instances and restarts without invalidating existing cookies. Fixes #32 --- cmd/anubis/main.go | 34 +++++++++++++++++++++++++------- docs/docs/CHANGELOG.md | 1 + docs/docs/admin/installation.mdx | 1 + go.mod | 1 + go.sum | 2 ++ 5 files changed, 32 insertions(+), 7 deletions(-) diff --git a/cmd/anubis/main.go b/cmd/anubis/main.go index e27e02fb9..e19e4258a 100644 --- a/cmd/anubis/main.go +++ b/cmd/anubis/main.go @@ -39,6 +39,7 @@ import ( "github.com/prometheus/client_golang/prometheus" "github.com/prometheus/client_golang/prometheus/promauto" "github.com/prometheus/client_golang/prometheus/promhttp" + "golang.org/x/crypto/hkdf" ) var ( @@ -54,6 +55,7 @@ var ( target = flag.String("target", "http://localhost:3923", "target to reverse proxy to") healthcheck = flag.Bool("healthcheck", false, "run a health check against Anubis") debugXRealIPDefault = flag.String("debug-x-real-ip-default", "", "If set, replace empty X-Real-Ip headers with this value, useful only for debugging Anubis and running it locally") + secret = flag.String("secret", "", "generate random signing key if empty or use value to derive the key") //go:embed static botPolicies.json static embed.FS @@ -158,7 +160,12 @@ func main() { return } - s, err := New(*target, *policyFname) + private, err := newPrivateKey(*secret) + if err != nil { + log.Fatal(err) + } + + s, err := New(*target, *policyFname, private) if err != nil { log.Fatal(err) } @@ -288,15 +295,15 @@ func (s *Server) challengeFor(r *http.Request, difficulty int) string { return sha256sum(data) } -func New(target, policyFname string) (*Server, error) { +func New(target, policyFname string, private ed25519.PrivateKey) (*Server, error) { u, err := url.Parse(target) if err != nil { return nil, fmt.Errorf("failed to parse target URL: %w", err) } - pub, priv, err := ed25519.GenerateKey(rand.Reader) - if err != nil { - return nil, fmt.Errorf("failed to generate ed25519 key: %w", err) + public, ok := private.Public().(ed25519.PublicKey) + if !ok { + return nil, fmt.Errorf("invalid public key type: %T", private.Public()) } transport := http.DefaultTransport.(*http.Transport).Clone() @@ -342,8 +349,8 @@ func New(target, policyFname string) (*Server, error) { return &Server{ rp: rp, - priv: priv, - pub: pub, + priv: private, + pub: public, policy: policy, dnsblCache: NewDecayMap[string, dnsbl.DroneBLResponse](), }, nil @@ -709,3 +716,16 @@ func serveMainJSWithBestEncoding(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "text/javascript") http.ServeFileFS(w, r, static, "static/js/main.mjs") } + +func newPrivateKey(secret string) (ed25519.PrivateKey, error) { + rr := rand.Reader + if secret != "" { + rr = hkdf.New(sha256.New, []byte(secret), nil, []byte("ed25519 signing key")) + } + + _, private, err := ed25519.GenerateKey(rr) + if err != nil { + return nil, fmt.Errorf("failed to generate ed25519 private key: %w", err) + } + return private, nil +} diff --git a/docs/docs/CHANGELOG.md b/docs/docs/CHANGELOG.md index f0b9dc447..ff71ef64a 100644 --- a/docs/docs/CHANGELOG.md +++ b/docs/docs/CHANGELOG.md @@ -13,6 +13,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Fixed and clarified installation instructions - Introduced integration tests using Playwright +- Derive signing key from configurable secret [#70](https://github.com/TecharoHQ/anubis/issues/70) ## v1.14.2 diff --git a/docs/docs/admin/installation.mdx b/docs/docs/admin/installation.mdx index f716a8449..4f6436b15 100644 --- a/docs/docs/admin/installation.mdx +++ b/docs/docs/admin/installation.mdx @@ -32,6 +32,7 @@ Anubis uses these environment variables for configuration: | `POLICY_FNAME` | unset | The file containing [bot policy configuration](./policies.md). See the bot policy documentation for more details. If unset, the default bot policy configuration is used. | | `SERVE_ROBOTS_TXT` | `false` | If set `true`, Anubis will serve a default `robots.txt` file that disallows all known AI scrapers by name and then additionally disallows every scraper. This is useful if facts and circumstances make it difficult to change the underlying service to serve such a `robots.txt` file. | | `TARGET` | `http://localhost:3923` | The URL of the service that Anubis should forward valid requests to. Supports Unix domain sockets, set this to a URI like so: `unix:///path/to/socket.sock`. | +| `SECRET` | "" (empty) | Generate random signing key if empty or use value to derive the key. Use `head -c 32 /dev/urandom | base64` to create a suitable value. | ## Docker compose diff --git a/go.mod b/go.mod index fb0a56f3e..fbbd883b9 100644 --- a/go.mod +++ b/go.mod @@ -10,6 +10,7 @@ require ( github.com/prometheus/client_golang v1.21.1 github.com/sebest/xff v0.0.0-20210106013422-671bd2870b3a github.com/yl2chen/cidranger v1.0.2 + golang.org/x/crypto v0.36.0 ) require ( diff --git a/go.sum b/go.sum index 6cecd3d0d..5491b689b 100644 --- a/go.sum +++ b/go.sum @@ -86,6 +86,8 @@ golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliY golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU= golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8= golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk= +golang.org/x/crypto v0.36.0 h1:AnAEvhDddvBdpY+uR+MyHmuZzzNqXSe/GvuDeob5L34= +golang.org/x/crypto v0.36.0/go.mod h1:Y4J0ReaxCR1IMaabaSMugxJES1EpwhBHhv2bDHklZvc= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=