diff --git a/README.md b/README.md index 145fad3..de9f024 100644 --- a/README.md +++ b/README.md @@ -64,6 +64,16 @@ All endpoints support both trailing-slash and non-trailing-slash variants: If `jwt-public-key` is set in `config.json`, all endpoints require JWT authentication using an Ed25519 key (`Authorization: Bearer `). +> **Security note:** Authentication only verifies the token's Ed25519 +> signature and its expiry — the claims (roles, user) are **not** checked. +> Any validly-signed, unexpired token therefore has full access to every +> endpoint, including the destructive `POST /api/free/` (drops buffered data) +> and the state-dumping `GET /api/debug/`. If the same key is shared with a +> cc-backend that issues lower-privilege user tokens, those tokens also unlock +> cc-metric-store. If `jwt-public-key` is left empty, **no authentication is +> performed on any endpoint** — only run in this mode on a trusted, isolated +> network. + ## Run tests Some benchmarks concurrently access the `MemoryStore`, so enabling the @@ -135,7 +145,7 @@ The config file is a JSON document with four top-level sections. - `addr`: Address and port to listen on (default: `0.0.0.0:8082`) - `https-cert-file` / `https-key-file`: Paths to TLS certificate/key for HTTPS -- `jwt-public-key`: Base64-encoded Ed25519 public key for JWT authentication. If empty, no auth is required. +- `jwt-public-key`: Base64-encoded Ed25519 public key for JWT authentication. Only the signature and expiry are verified; token claims/roles are not enforced, so any valid token has full access (including `/api/free/` and `/api/debug/`). If empty, no auth is required on any endpoint — use only on a trusted network. - `user` / `group`: Drop privileges to this user/group after startup - `backend-url`: Optional URL of a cc-backend instance used as node provider diff --git a/internal/api/authentication.go b/internal/api/authentication.go index 8cf3da9..4535437 100644 --- a/internal/api/authentication.go +++ b/internal/api/authentication.go @@ -15,6 +15,10 @@ import ( "github.com/golang-jwt/jwt/v4" ) +// maxTokenCacheSize bounds the number of validated tokens kept in memory. +// Reaching it triggers eviction of expired entries (see authHandler). +const maxTokenCacheSize = 1024 + func authHandler(next http.Handler, publicKey ed25519.PublicKey) http.Handler { cacheLock := sync.RWMutex{} cache := map[string]*jwt.Token{} @@ -34,6 +38,13 @@ func authHandler(next http.Handler, publicKey ed25519.PublicKey) http.Handler { next.ServeHTTP(rw, r) return } + if ok { + // Cached token has since expired (or become otherwise invalid); + // drop it so the cache does not accumulate stale entries. + cacheLock.Lock() + delete(cache, rawtoken) + cacheLock.Unlock() + } // The actual token is ignored for now. // In case expiration and so on are specified, the Parse function @@ -52,6 +63,21 @@ func authHandler(next http.Handler, publicKey ed25519.PublicKey) http.Handler { } cacheLock.Lock() + // Bound the cache: cc-backend mints short-lived, rotating tokens, so + // without an upper limit the map would grow unbounded over the + // lifetime of the process. When the cap is reached, evict any entries + // that have expired; if none have, clear the cache entirely rather + // than letting it grow without bound. + if len(cache) >= maxTokenCacheSize { + for k, t := range cache { + if t.Claims.Valid() != nil { + delete(cache, k) + } + } + if len(cache) >= maxTokenCacheSize { + clear(cache) + } + } cache[rawtoken] = token cacheLock.Unlock()