Compare commits

..

1 Commits

Author SHA1 Message Date
Xe Iaso f19a5f7eb8 docs(k8s): document that Kubernetes support needs a non-default storage backend
Closes: #1602
Signed-off-by: Xe Iaso <me@xeiaso.net>
2026-06-01 10:29:23 -04:00
13 changed files with 40 additions and 67 deletions
-3
View File
@@ -44,6 +44,3 @@ xou
AWOO AWOO
firewalls firewalls
bindhosts bindhosts
handrolled
xai
gitlab
+1
View File
@@ -10,3 +10,4 @@ builds:
ldflags: ldflags:
- -s -w - -s -w
- -extldflags "-static" - -extldflags "-static"
- -X github.com/TecharoHQ/anubis.Version={{.Env.VERSION}}
+3 -18
View File
@@ -1,27 +1,12 @@
// Package anubis contains the version number of Anubis. // Package anubis contains the version number of Anubis.
package anubis package anubis
import ( import "time"
"runtime/debug"
"time"
)
func init() {
bi, ok := debug.ReadBuildInfo()
if !ok {
return
}
// XXX(Xe): many things in this repo assume that the development version
// of anubis is `devel` and ReadBuildInfo returns `(devel)`. Shim the gap.
if bi.Main.Version != "(devel)" {
Version = bi.Main.Version
}
}
// Version is the current version of Anubis. // Version is the current version of Anubis.
// //
// This is set from the Go module runtime version. // This variable is set at build time using the -X linker flag. If not set,
// it defaults to "devel".
var Version = "devel" var Version = "devel"
// CookieName is the name of the cookie that Anubis uses in order to validate // CookieName is the name of the cookie that Anubis uses in order to validate
-4
View File
@@ -36,7 +36,6 @@ import (
"github.com/TecharoHQ/anubis/lib/thoth" "github.com/TecharoHQ/anubis/lib/thoth"
"github.com/TecharoHQ/anubis/web" "github.com/TecharoHQ/anubis/web"
"github.com/facebookgo/flagenv" "github.com/facebookgo/flagenv"
"github.com/google/uuid"
_ "github.com/joho/godotenv/autoload" _ "github.com/joho/godotenv/autoload"
healthv1 "google.golang.org/grpc/health/grpc_health_v1" healthv1 "google.golang.org/grpc/health/grpc_health_v1"
) )
@@ -194,9 +193,6 @@ func main() {
flagenv.Parse() flagenv.Parse()
flag.Parse() flag.Parse()
// Must be set before any concurrent UUID call.
uuid.EnableRandPool()
if *versionFlag { if *versionFlag {
fmt.Println("Anubis", anubis.Version) fmt.Println("Anubis", anubis.Version)
return return
-3
View File
@@ -41,9 +41,6 @@ bots:
# Challenge Firefox AI previews # Challenge Firefox AI previews
- import: (data)/clients/x-firefox-ai.yaml - import: (data)/clients/x-firefox-ai.yaml
# x.ai has a scraper that is killing gitlab instances
- import: (data)/crawlers/xai.yaml
# Allow common "keeping the internet working" routes (well-known, favicon, robots.txt) # Allow common "keeping the internet working" routes (well-known, favicon, robots.txt)
- import: (data)/common/keep-internet-working.yaml - import: (data)/common/keep-internet-working.yaml
-8
View File
@@ -1,8 +0,0 @@
- name: xai-crawler-and-asn
action: DENY
user_agent_regex: code-review-sourcing.*\+xai-research
remote_addresses:
- 69.12.56.0/21
- name: xai-crawler-user-agent
action: DENY
user_agent_regex: code-review-sourcing.*\+xai-research
-3
View File
@@ -25,9 +25,6 @@
# Challenge Firefox AI previews # Challenge Firefox AI previews
- import: (data)/clients/x-firefox-ai.yaml - import: (data)/clients/x-firefox-ai.yaml
# x.ai has a scraper that is killing gitlab instances
- import: (data)/crawlers/xai.yaml
# Allow common "keeping the internet working" routes (well-known, favicon, robots.txt) # Allow common "keeping the internet working" routes (well-known, favicon, robots.txt)
- import: (data)/common/keep-internet-working.yaml - import: (data)/common/keep-internet-working.yaml
-4
View File
@@ -23,14 +23,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Improve error messages and fix broken REDIRECT_DOMAINS link in docs ([#1193](https://github.com/TecharoHQ/anubis/issues/1193)) - Improve error messages and fix broken REDIRECT_DOMAINS link in docs ([#1193](https://github.com/TecharoHQ/anubis/issues/1193))
- Add Bulgarian locale ([#1394](https://github.com/TecharoHQ/anubis/pull/1394)) - Add Bulgarian locale ([#1394](https://github.com/TecharoHQ/anubis/pull/1394))
- Fixed case-sensitivity mismatch in geoipchecker.go - Fixed case-sensitivity mismatch in geoipchecker.go
- Use [Go's native version stamping](https://michael.stapelberg.ch/posts/2026-04-05-stamp-it-all-programs-must-report-their-version/) instead of a handrolled variant.
- Fix CEL internal errors when iterating `headers`/`query` map wrappers by implementing map iterators for `HTTPHeaders` and `URLValues` ([#1465](https://github.com/TecharoHQ/anubis/pull/1465)). - Fix CEL internal errors when iterating `headers`/`query` map wrappers by implementing map iterators for `HTTPHeaders` and `URLValues` ([#1465](https://github.com/TecharoHQ/anubis/pull/1465)).
- Enable [metrics serving via TLS](./admin/policies.mdx#tls), including [mutual TLS (mTLS)](./admin/policies.mdx#mtls). - Enable [metrics serving via TLS](./admin/policies.mdx#tls), including [mutual TLS (mTLS)](./admin/policies.mdx#mtls).
- Enable [HTTP basic auth](./admin/policies.mdx#http-basic-authentication) for the metrics server. - Enable [HTTP basic auth](./admin/policies.mdx#http-basic-authentication) for the metrics server.
- Fix a bug in the dataset poisoning maze that could allow denial of service [#1580](https://github.com/TecharoHQ/anubis/issues/1580). - Fix a bug in the dataset poisoning maze that could allow denial of service [#1580](https://github.com/TecharoHQ/anubis/issues/1580).
- Add config option to add ASN to logs/metrics. - Add config option to add ASN to logs/metrics.
- Log weight when issuing challenge. - Log weight when issuing challenge.
- Block x.ai's crawler for code review training.
- Gate pprof endpoints behind `metrics.debug` in the policy file. - Gate pprof endpoints behind `metrics.debug` in the policy file.
- Limit naive honeypot r9k delay to one second. - Limit naive honeypot r9k delay to one second.
- Fix an obscure case where adding query values to a subrequest match could cause an invalid rule match when using path based matching for protected resources. - Fix an obscure case where adding query values to a subrequest match could cause an invalid rule match when using path based matching for protected resources.
@@ -41,9 +39,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Fix a race in the bbolt store where the asynchronous cleanup scheduled by an expired read could delete a value that had just been refreshed; the delete now only fires when the key still carries the same expired generation it observed. - Fix a race in the bbolt store where the asynchronous cleanup scheduled by an expired read could delete a value that had just been refreshed; the delete now only fires when the key still carries the same expired generation it observed.
- Marginally increase the performances of requests processing - Marginally increase the performances of requests processing
- Marginally improve the performances of PoW validation - Marginally improve the performances of PoW validation
- Marginally improve the performances of challenges generation/display
- Significantly improve the performances of the gzip middleware - Significantly improve the performances of the gzip middleware
- Significantly improve the performances of the PoW validation
## v1.25.0: Necron ## v1.25.0: Necron
@@ -131,11 +131,27 @@ Then point your Ingress to the Anubis port:
name: anubis name: anubis
``` ```
## Storage
By default, Anubis stores all of its data in memory. This memory is not shared between pods. If you have multiple instances of Anubis without the data being [stored outside of memory](../policies.mdx#storage-backends) and a [shared cookie key](../installation.mdx#key-generation), you will run into [unexpected behaviour](https://github.com/TecharoHQ/anubis/issues/1602) when user traffic traverses between pods.
Based on the deployment of your Kubernetes cluster, here are the preferable storage backends to pick from:
| Backend | Pro | Con |
| :------- | :-------------------------------------------------------------- | :------------------------------------------------------------------------------------------- |
| `bbolt` | Only requires a ReadWriteOnce PVC. | Does not support more than one Anubis pod. |
| `memory` | Requires no configuration. | Process memory is not shared between pods. |
| `s3api` | Great if your cluster includes Rook/Ceph to use RADOS directly. | Potentially higher latency unless you use a store like [Tigris](https://www.tigrisdata.com). |
| `valkey` | Trivial to configure in your cluster. | If your Redis/Valkey server is down, Anubis is going to have issues. |
Pick your poison accordingly. Many production deployments use the `s3api` and `valkey` backends without issue. Single node deployments can get away with either `memory` or `bbolt` depending on the facts and circumstances of the deployment.
## Envoy Gateway ## Envoy Gateway
If you are using envoy-gateway, the `X-Real-Ip` header is not set by default, but Anubis does require it. You can resolve this by adding the header, either on the specific `HTTPRoute` where Anubis is listening, or on the `ClientTrafficPolicy` to apply it to any number of Gateways: If you are using envoy-gateway, the `X-Real-Ip` header is not set by default, but Anubis does require it. You can resolve this by adding the header, either on the specific `HTTPRoute` where Anubis is listening, or on the `ClientTrafficPolicy` to apply it to any number of Gateways:
HTTPRoute: HTTPRoute:
```yaml ```yaml
apiVersion: gateway.networking.k8s.io/v1 apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute kind: HTTPRoute
@@ -160,6 +176,7 @@ spec:
``` ```
Applying to any number of Gateways: Applying to any number of Gateways:
```yaml ```yaml
apiVersion: gateway.envoyproxy.io/v1alpha1 apiVersion: gateway.envoyproxy.io/v1alpha1
kind: ClientTrafficPolicy kind: ClientTrafficPolicy
+7
View File
@@ -219,8 +219,11 @@ Anubis offers the following storage backends:
- [`memory`](#memory) -- A simple in-memory hashmap - [`memory`](#memory) -- A simple in-memory hashmap
- [`bbolt`](#bbolt) -- An on-disk key/value store backed by [bbolt](https://github.com/etcd-io/bbolt), an embedded key/value database for Go programs - [`bbolt`](#bbolt) -- An on-disk key/value store backed by [bbolt](https://github.com/etcd-io/bbolt), an embedded key/value database for Go programs
- [`s3api`](#s3api) -- Amazon S3 based storage or another compatible object store
- [`valkey`](#valkey) -- A remote in-memory key/value database backed by [Valkey](https://valkey.io/) (or another database compatible with the [RESP](https://redis.io/docs/latest/develop/reference/protocol-spec/) protocol) - [`valkey`](#valkey) -- A remote in-memory key/value database backed by [Valkey](https://valkey.io/) (or another database compatible with the [RESP](https://redis.io/docs/latest/develop/reference/protocol-spec/) protocol)
:::warning
If no storage backend is set in the policy file, Anubis will use the [`memory`](#memory) backend by default. This is equivalent to the following in the policy file: If no storage backend is set in the policy file, Anubis will use the [`memory`](#memory) backend by default. This is equivalent to the following in the policy file:
```yaml ```yaml
@@ -229,6 +232,10 @@ store:
parameters: {} parameters: {}
``` ```
This means that all session data that is required for the challenge mechanism to work is stored **IN PROCESS MEMORY** that is **NOT** shared between instances of Anubis. If you set up Anubis with multiple instances using the `memory` storage backend, your users will sometimes get "Administrator has misconfigured Anubis" error messages when it cannot look up the aforementioned session data.
:::
### `memory` ### `memory`
The memory backend is an in-memory cache. This backend works best if you don't use multiple instances of Anubis or don't have mutable storage in the environment you're running Anubis in. The memory backend is an in-memory cache. This backend works best if you don't use multiple instances of Anubis or don't have mutable storage in the environment you're running Anubis in.
+5 -7
View File
@@ -4,7 +4,6 @@ import (
"context" "context"
"crypto/ed25519" "crypto/ed25519"
"crypto/rand" "crypto/rand"
"encoding/hex"
"encoding/json" "encoding/json"
"errors" "errors"
"fmt" "fmt"
@@ -163,7 +162,6 @@ func (s *Server) issueChallenge(ctx context.Context, r *http.Request, lg *slog.L
if err != nil { if err != nil {
return nil, err return nil, err
} }
idStr := id.String()
var randomData = make([]byte, 64) var randomData = make([]byte, 64)
if _, err := rand.Read(randomData); err != nil { if _, err := rand.Read(randomData); err != nil {
@@ -171,9 +169,9 @@ func (s *Server) issueChallenge(ctx context.Context, r *http.Request, lg *slog.L
} }
chall := challenge.Challenge{ chall := challenge.Challenge{
ID: idStr, ID: id.String(),
Method: rule.Challenge.Algorithm, Method: rule.Challenge.Algorithm,
RandomData: hex.EncodeToString(randomData), RandomData: fmt.Sprintf("%x", randomData),
IssuedAt: time.Now(), IssuedAt: time.Now(),
Difficulty: rule.Challenge.Difficulty, Difficulty: rule.Challenge.Difficulty,
PolicyRuleHash: rule.Hash(), PolicyRuleHash: rule.Hash(),
@@ -184,11 +182,11 @@ func (s *Server) issueChallenge(ctx context.Context, r *http.Request, lg *slog.L
} }
j := store.JSON[challenge.Challenge]{Underlying: s.store} j := store.JSON[challenge.Challenge]{Underlying: s.store}
if err := j.Set(ctx, "challenge:"+idStr, chall, 30*time.Minute); err != nil { if err := j.Set(ctx, "challenge:"+id.String(), chall, 30*time.Minute); err != nil {
return nil, err return nil, err
} }
lg.Info("new challenge issued", "challenge", idStr, "weight", cr.Weight) lg.Info("new challenge issued", "challenge", id.String(), "weight", cr.Weight)
return &chall, err return &chall, err
} }
@@ -242,7 +240,7 @@ func (s *Server) maybeReverseProxyOrPage(w http.ResponseWriter, r *http.Request)
func (s *Server) maybeReverseProxy(w http.ResponseWriter, r *http.Request, httpStatusOnly bool) { func (s *Server) maybeReverseProxy(w http.ResponseWriter, r *http.Request, httpStatusOnly bool) {
lg, r := s.getRequestLogger(r) lg, r := s.getRequestLogger(r)
if val, _ := s.store.Get(r.Context(), "ogtags:allow:"+r.Host+r.URL.String()); val != nil { if val, _ := s.store.Get(r.Context(), fmt.Sprintf("ogtags:allow:%s%s", r.Host, r.URL.String())); val != nil {
lg.Debug("serving opengraph tag asset") lg.Debug("serving opengraph tag asset")
s.ServeHTTPNext(w, r) s.ServeHTTPNext(w, r)
return return
+5 -15
View File
@@ -1,15 +1,14 @@
package proofofwork package proofofwork
import ( import (
"crypto/sha256"
"crypto/subtle" "crypto/subtle"
"encoding/hex"
"fmt" "fmt"
"log/slog" "log/slog"
"net/http" "net/http"
"strconv" "strconv"
"strings" "strings"
"github.com/TecharoHQ/anubis/internal"
chall "github.com/TecharoHQ/anubis/lib/challenge" chall "github.com/TecharoHQ/anubis/lib/challenge"
"github.com/TecharoHQ/anubis/lib/localization" "github.com/TecharoHQ/anubis/lib/localization"
"github.com/a-h/templ" "github.com/a-h/templ"
@@ -67,20 +66,11 @@ func (i *Impl) Validate(r *http.Request, lg *slog.Logger, in *chall.ValidateInpu
return chall.NewError("validate", "invalid response", fmt.Errorf("%w response", chall.ErrMissingField)) return chall.NewError("validate", "invalid response", fmt.Errorf("%w response", chall.ErrMissingField))
} }
// Stream the challenge and nonce into a single sha256 hasher to avoid calcString := challenge + nonceStr
// the intermediate "challenge + nonceStr" concatenation. Hex-encode calculated := internal.SHA256sum(calcString)
// the digest into a stack buffer so the comparison runs without
// allocating a heap string.
h := sha256.New()
h.Write([]byte(challenge))
h.Write([]byte(nonceStr))
var sumBuf [sha256.Size]byte
sum := h.Sum(sumBuf[:0])
var hexBuf [sha256.Size * 2]byte
hex.Encode(hexBuf[:], sum)
if subtle.ConstantTimeCompare([]byte(response), hexBuf[:]) != 1 { if subtle.ConstantTimeCompare([]byte(response), []byte(calculated)) != 1 {
return chall.NewError("validate", "invalid response", fmt.Errorf("%w: wanted response %s but got %s", chall.ErrFailed, string(hexBuf[:]), response)) return chall.NewError("validate", "invalid response", fmt.Errorf("%w: wanted response %s but got %s", chall.ErrFailed, calculated, response))
} }
// compare the leading zeroes // compare the leading zeroes
+2 -2
View File
@@ -17,8 +17,8 @@ $`npm run assets`;
}, },
build: ({ bin, etc, systemd, doc }) => { build: ({ bin, etc, systemd, doc }) => {
$`go build -o ${bin}/anubis -ldflags '-s -w -extldflags "-static" ./cmd/anubis`; $`go build -o ${bin}/anubis -ldflags '-s -w -extldflags "-static" -X "github.com/TecharoHQ/anubis.Version=${git.tag()}"' ./cmd/anubis`;
$`go build -o ${bin}/anubis-robots2policy -ldflags '-s -w -extldflags "-static"' ./cmd/robots2policy`; $`go build -o ${bin}/anubis-robots2policy -ldflags '-s -w -extldflags "-static" -X "github.com/TecharoHQ/anubis.Version=${git.tag()}"' ./cmd/robots2policy`;
file.install("./run/anubis@.service", `${systemd}/anubis@.service`); file.install("./run/anubis@.service", `${systemd}/anubis@.service`);
file.install("./run/default.env", `${etc}/default.env`); file.install("./run/default.env", `${etc}/default.env`);