Enforce GLEAN_SESSION_KEY requirementUnverified
12d6fac parent: fa08b45 modified
.env.example +1 -0 | @@ -1,5 +1,6 @@ | ||
| 1 | 1 | GLEAN_ADDR=:8080 |
| 2 | 2 | GLEAN_DB=glean.db |
| 3 | +GLEAN_SESSION_KEY=change-me-to-a-random-string | |
| 3 | 4 | GLEAN_JETSTREAM=wss://jetstream.glean.at |
| 4 | 5 | GLEAN_PLC_URL=https://didplc.glean.at |
| 5 | 6 | GLEAN_SYNC_INTERVAL=10m |
| @@ -1,5 +1,6 @@ | |||
| 1 | GLEAN_ADDR=:8080 | 1 | GLEAN_ADDR=:8080 |
| 2 | GLEAN_DB=glean.db | 2 | GLEAN_DB=glean.db |
| 3 | +GLEAN_SESSION_KEY=change-me-to-a-random-string | ||
| 3 | GLEAN_JETSTREAM=wss://jetstream.glean.at | 4 | GLEAN_JETSTREAM=wss://jetstream.glean.at |
| 4 | GLEAN_PLC_URL=https://didplc.glean.at | 5 | GLEAN_PLC_URL=https://didplc.glean.at |
| 5 | GLEAN_SYNC_INTERVAL=10m | 6 | GLEAN_SYNC_INTERVAL=10m |
modified
docs/specs.md +2 -0 | @@ -986,6 +986,8 @@ glean/ | ||
| 986 | 986 | |
| 987 | 987 | ## 10. Auth Flow |
| 988 | 988 | |
| 989 | +Session cookies are HMAC-signed using `GLEAN_SESSION_KEY` (required, must be set to a random string). The server refuses to start without it. | |
| 990 | + | |
| 989 | 991 | DID resolution uses a configurable PLC directory (`GLEAN_PLC_URL`, defaults to `https://didplc.glean.at`). The identity directory is initialized once at startup via `InitIdentity()` with a caching layer (250k entries, 24h TTL). |
| 990 | 992 | |
| 991 | 993 | 1. User visits `/`, clicks "Sign in with Bluesky" (or any AT Proto PDS) |
| @@ -986,6 +986,8 @@ glean/ | |||
| 986 | 986 | ||
| 987 | ## 10. Auth Flow | 987 | ## 10. Auth Flow |
| 988 | 988 | ||
| 989 | +Session cookies are HMAC-signed using `GLEAN_SESSION_KEY` (required, must be set to a random string). The server refuses to start without it. | ||
| 990 | + | ||
| 989 | DID resolution uses a configurable PLC directory (`GLEAN_PLC_URL`, defaults to `https://didplc.glean.at`). The identity directory is initialized once at startup via `InitIdentity()` with a caching layer (250k entries, 24h TTL). | 991 | DID resolution uses a configurable PLC directory (`GLEAN_PLC_URL`, defaults to `https://didplc.glean.at`). The identity directory is initialized once at startup via `InitIdentity()` with a caching layer (250k entries, 24h TTL). |
| 990 | 992 | ||
| 991 | 1. User visits `/`, clicks "Sign in with Bluesky" (or any AT Proto PDS) | 993 | 1. User visits `/`, clicks "Sign in with Bluesky" (or any AT Proto PDS) |
modified
internal/server/server.go +2 -2 | @@ -71,7 +71,7 @@ type Server struct { | ||
| 71 | 71 | sessionKey []byte |
| 72 | 72 | } |
| 73 | 73 | |
| 74 | -func New(dbs *db.Databases, clientID, callbackURL, addr string, scheduler *feed.Scheduler, engine *cluster.Engine, logger *slog.Logger) *Server { | |
| 74 | +func New(dbs *db.Databases, clientID, callbackURL, addr string, scheduler *feed.Scheduler, engine *cluster.Engine, logger *slog.Logger, sessionKey []byte) *Server { | |
| 75 | 75 | oauthStore := db.NewOAuthStore(dbs) |
| 76 | 76 | |
| 77 | 77 | var config oauth.ClientConfig |
| @@ -99,7 +99,7 @@ func New(dbs *db.Databases, clientID, callbackURL, addr string, scheduler *feed. | ||
| 99 | 99 | scraper: scraper.New(logger), |
| 100 | 100 | clientID: clientID, |
| 101 | 101 | callbackURL: callbackURL, |
| 102 | - sessionKey: loadSessionKey(), | |
| 102 | + sessionKey: sessionKey, | |
| 103 | 103 | } |
| 104 | 104 | |
| 105 | 105 | s.setupMiddleware() |
| @@ -71,7 +71,7 @@ type Server struct { | |||
| 71 | sessionKey []byte | 71 | sessionKey []byte |
| 72 | } | 72 | } |
| 73 | 73 | ||
| 74 | -func New(dbs *db.Databases, clientID, callbackURL, addr string, scheduler *feed.Scheduler, engine *cluster.Engine, logger *slog.Logger) *Server { | 74 | +func New(dbs *db.Databases, clientID, callbackURL, addr string, scheduler *feed.Scheduler, engine *cluster.Engine, logger *slog.Logger, sessionKey []byte) *Server { |
| 75 | oauthStore := db.NewOAuthStore(dbs) | 75 | oauthStore := db.NewOAuthStore(dbs) |
| 76 | 76 | ||
| 77 | var config oauth.ClientConfig | 77 | var config oauth.ClientConfig |
| @@ -99,7 +99,7 @@ func New(dbs *db.Databases, clientID, callbackURL, addr string, scheduler *feed. | |||
| 99 | scraper: scraper.New(logger), | 99 | scraper: scraper.New(logger), |
| 100 | clientID: clientID, | 100 | clientID: clientID, |
| 101 | callbackURL: callbackURL, | 101 | callbackURL: callbackURL, |
| 102 | - sessionKey: loadSessionKey(), | 102 | + sessionKey: sessionKey, |
| 103 | } | 103 | } |
| 104 | 104 | ||
| 105 | s.setupMiddleware() | 105 | s.setupMiddleware() |
modified
internal/server/session.go +0 -9 | @@ -7,7 +7,6 @@ import ( | ||
| 7 | 7 | "encoding/base64" |
| 8 | 8 | "encoding/json" |
| 9 | 9 | "net/http" |
| 10 | - "os" | |
| 11 | 10 | |
| 12 | 11 | "pkg.rbrt.fr/glean/internal/atproto" |
| 13 | 12 | "pkg.rbrt.fr/glean/internal/db" |
| @@ -96,14 +95,6 @@ func (s *Server) getSessionData(r *http.Request) *sessionData { | ||
| 96 | 95 | return data |
| 97 | 96 | } |
| 98 | 97 | |
| 99 | -func loadSessionKey() []byte { | |
| 100 | - key := os.Getenv("GLEAN_SESSION_KEY") | |
| 101 | - if key == "" { | |
| 102 | - key = "default-dev-key-change-in-production" | |
| 103 | - } | |
| 104 | - return []byte(key) | |
| 105 | -} | |
| 106 | - | |
| 107 | 98 | func encodeSession(key []byte, data sessionData) (string, error) { |
| 108 | 99 | payload, err := json.Marshal(data) |
| 109 | 100 | if err != nil { |
| @@ -7,7 +7,6 @@ import ( | |||
| 7 | "encoding/base64" | 7 | "encoding/base64" |
| 8 | "encoding/json" | 8 | "encoding/json" |
| 9 | "net/http" | 9 | "net/http" |
| 10 | - "os" | ||
| 11 | 10 | ||
| 12 | "pkg.rbrt.fr/glean/internal/atproto" | 11 | "pkg.rbrt.fr/glean/internal/atproto" |
| 13 | "pkg.rbrt.fr/glean/internal/db" | 12 | "pkg.rbrt.fr/glean/internal/db" |
| @@ -96,14 +95,6 @@ func (s *Server) getSessionData(r *http.Request) *sessionData { | |||
| 96 | return data | 95 | return data |
| 97 | } | 96 | } |
| 98 | 97 | ||
| 99 | -func loadSessionKey() []byte { | ||
| 100 | - key := os.Getenv("GLEAN_SESSION_KEY") | ||
| 101 | - if key == "" { | ||
| 102 | - key = "default-dev-key-change-in-production" | ||
| 103 | - } | ||
| 104 | - return []byte(key) | ||
| 105 | -} | ||
| 106 | - | ||
| 107 | func encodeSession(key []byte, data sessionData) (string, error) { | 98 | func encodeSession(key []byte, data sessionData) (string, error) { |
| 108 | payload, err := json.Marshal(data) | 99 | payload, err := json.Marshal(data) |
| 109 | if err != nil { | 100 | if err != nil { |
modified
main.go +7 -1 | @@ -28,8 +28,14 @@ func main() { | ||
| 28 | 28 | fetchInterval := flag.Duration("fetch-interval", envDuration("GLEAN_FETCH_INTERVAL", 5*time.Minute), "feed fetch tick interval") |
| 29 | 29 | collectionDirURL := flag.String("collection-dir", envOr("GLEAN_COLLECTION_DIR_URL", ""), "collection directory URL for startup backfill") |
| 30 | 30 | backfillConcurrency := flag.Int("backfill-concurrency", envInt("GLEAN_BACKFILL_CONCURRENCY", 5), "max concurrent backfill workers") |
| 31 | + sessionKey := envOr("GLEAN_SESSION_KEY", "") | |
| 31 | 32 | flag.Parse() |
| 32 | 33 | |
| 34 | + if sessionKey == "" { | |
| 35 | + fmt.Fprintln(os.Stderr, "GLEAN_SESSION_KEY is required") | |
| 36 | + os.Exit(1) | |
| 37 | + } | |
| 38 | + | |
| 33 | 39 | atproto.InitIdentity(envOr("GLEAN_PLC_URL", "https://didplc.glean.at")) |
| 34 | 40 | |
| 35 | 41 | logger := slog.New(slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{Level: slog.LevelInfo})) |
| @@ -49,7 +55,7 @@ func main() { | ||
| 49 | 55 | |
| 50 | 56 | engine := cluster.NewEngine(dbs.DB(), logger) |
| 51 | 57 | |
| 52 | - srv := server.New(dbs, clientID, callbackURL, *addr, scheduler, engine, logger) | |
| 58 | + srv := server.New(dbs, clientID, callbackURL, *addr, scheduler, engine, logger, []byte(sessionKey)) | |
| 53 | 59 | |
| 54 | 60 | cron := cluster.NewCron(engine, *clusterInterval, logger) |
| 55 | 61 | |
| @@ -28,8 +28,14 @@ func main() { | |||
| 28 | fetchInterval := flag.Duration("fetch-interval", envDuration("GLEAN_FETCH_INTERVAL", 5*time.Minute), "feed fetch tick interval") | 28 | fetchInterval := flag.Duration("fetch-interval", envDuration("GLEAN_FETCH_INTERVAL", 5*time.Minute), "feed fetch tick interval") |
| 29 | collectionDirURL := flag.String("collection-dir", envOr("GLEAN_COLLECTION_DIR_URL", ""), "collection directory URL for startup backfill") | 29 | collectionDirURL := flag.String("collection-dir", envOr("GLEAN_COLLECTION_DIR_URL", ""), "collection directory URL for startup backfill") |
| 30 | backfillConcurrency := flag.Int("backfill-concurrency", envInt("GLEAN_BACKFILL_CONCURRENCY", 5), "max concurrent backfill workers") | 30 | backfillConcurrency := flag.Int("backfill-concurrency", envInt("GLEAN_BACKFILL_CONCURRENCY", 5), "max concurrent backfill workers") |
| 31 | + sessionKey := envOr("GLEAN_SESSION_KEY", "") | ||
| 31 | flag.Parse() | 32 | flag.Parse() |
| 32 | 33 | ||
| 34 | + if sessionKey == "" { | ||
| 35 | + fmt.Fprintln(os.Stderr, "GLEAN_SESSION_KEY is required") | ||
| 36 | + os.Exit(1) | ||
| 37 | + } | ||
| 38 | + | ||
| 33 | atproto.InitIdentity(envOr("GLEAN_PLC_URL", "https://didplc.glean.at")) | 39 | atproto.InitIdentity(envOr("GLEAN_PLC_URL", "https://didplc.glean.at")) |
| 34 | 40 | ||
| 35 | logger := slog.New(slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{Level: slog.LevelInfo})) | 41 | logger := slog.New(slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{Level: slog.LevelInfo})) |
| @@ -49,7 +55,7 @@ func main() { | |||
| 49 | 55 | ||
| 50 | engine := cluster.NewEngine(dbs.DB(), logger) | 56 | engine := cluster.NewEngine(dbs.DB(), logger) |
| 51 | 57 | ||
| 52 | - srv := server.New(dbs, clientID, callbackURL, *addr, scheduler, engine, logger) | 58 | + srv := server.New(dbs, clientID, callbackURL, *addr, scheduler, engine, logger, []byte(sessionKey)) |
| 53 | 59 | ||
| 54 | cron := cluster.NewCron(engine, *clusterInterval, logger) | 60 | cron := cluster.NewCron(engine, *clusterInterval, logger) |
| 55 | 61 | ||
modified
readme.md +1 -0 | @@ -39,6 +39,7 @@ Then open `http://localhost:8080`. | ||
| 39 | 39 | |
| 40 | 40 | | Variable | Default | What it does | |
| 41 | 41 | | -------------------------- | -------------------------- | --------------------------------------------------------- | |
| 42 | +| `GLEAN_SESSION_KEY` | _(required)_ | Secret key for signing session cookies (any random string) | | |
| 42 | 43 | | `GLEAN_ADDR` | `:8080` | Listen address | |
| 43 | 44 | | `GLEAN_DB` | `glean.db` | SQLite base path (`_users`, `_articles`, `_recs` suffixes) | |
| 44 | 45 | | `GLEAN_JETSTREAM` | `wss://jetstream.glean.at` | Jetstream WebSocket URL | |
| @@ -39,6 +39,7 @@ Then open `http://localhost:8080`. | |||
| 39 | 39 | ||
| 40 | | Variable | Default | What it does | | 40 | | Variable | Default | What it does | |
| 41 | | -------------------------- | -------------------------- | --------------------------------------------------------- | | 41 | | -------------------------- | -------------------------- | --------------------------------------------------------- | |
| 42 | +| `GLEAN_SESSION_KEY` | _(required)_ | Secret key for signing session cookies (any random string) | | ||
| 42 | | `GLEAN_ADDR` | `:8080` | Listen address | | 43 | | `GLEAN_ADDR` | `:8080` | Listen address | |
| 43 | | `GLEAN_DB` | `glean.db` | SQLite base path (`_users`, `_articles`, `_recs` suffixes) | | 44 | | `GLEAN_DB` | `glean.db` | SQLite base path (`_users`, `_articles`, `_recs` suffixes) | |
| 44 | | `GLEAN_JETSTREAM` | `wss://jetstream.glean.at` | Jetstream WebSocket URL | | 45 | | `GLEAN_JETSTREAM` | `wss://jetstream.glean.at` | Jetstream WebSocket URL | |