summaryrefslogtreecommitdiffstats
path: root/internal/config
diff options
context:
space:
mode:
authorbndw <ben@bdw.to>2026-02-14 10:11:16 -0800
committerbndw <ben@bdw.to>2026-02-14 10:11:16 -0800
commit606e0a3329a3534a00889eee19c25e7d432f7d2d (patch)
tree526b1419eaa6b9b91126adbfa5990ec47f5d3a07 /internal/config
parenta90009e6b887a8a7ca67f49566af2caffb807776 (diff)
refactor: restructure auth config for better UX
Changed from flat structure to hierarchical read/write config: Before: auth: enabled: bool required: bool allowed_npubs_read: [] allowed_npubs_write: [] After: auth: read: enabled: bool allowed_npubs: [] write: enabled: bool allowed_npubs: [] Three states per operation: - enabled=false: no auth, allow all - enabled=true, allowed_npubs=[]: auth required, any valid signature - enabled=true, allowed_npubs=[...]: auth required, whitelist only Much clearer semantics and easier to reason about.
Diffstat (limited to 'internal/config')
-rw-r--r--internal/config/README.md70
-rw-r--r--internal/config/config.go57
-rw-r--r--internal/config/config_test.go102
3 files changed, 139 insertions, 90 deletions
diff --git a/internal/config/README.md b/internal/config/README.md
index 7deb38f..3dcf215 100644
--- a/internal/config/README.md
+++ b/internal/config/README.md
@@ -80,41 +80,51 @@ database:
80 80
81# Authentication configuration 81# Authentication configuration
82auth: 82auth:
83 # Enable authentication 83 # Read authentication (Subscribe, QueryEvents, CountEvents)
84 enabled: false 84 read:
85 85 enabled: false # false = no auth, allow all
86 # Require authentication for all requests 86 allowed_npubs: [] # npub format only (e.g., npub1...)
87 # If false, authentication is optional (pubkey available if provided) 87 # If enabled=false: no auth, allow all reads
88 required: false 88 # If enabled=true && allowed_npubs=[]: auth required, any valid signature accepted
89 # If enabled=true && allowed_npubs=[...]: auth required, only whitelisted npubs
90
91 # Write authentication (PublishEvent, PublishBatch)
92 write:
93 enabled: false
94 allowed_npubs: []
89 95
90 # Timestamp window in seconds for replay protection 96 # Timestamp window in seconds for replay protection
91 timestamp_window: 60 97 timestamp_window: 60
92 98
93 # Allowed npubs for read operations (optional, whitelist)
94 # If empty, all valid signatures are accepted for reads
95 # Use npub format only (e.g., npub1...)
96 allowed_npubs_read: []
97
98 # Allowed npubs for write operations (optional, whitelist)
99 # If empty, all valid signatures are accepted for writes
100 # Use npub format only (e.g., npub1...)
101 allowed_npubs_write: []
102
103 # Example use cases:
104 # - Public relay: allowed_npubs_write (only some can publish), empty read (everyone can read)
105 # - Private relay: both lists populated (restricted read and write)
106 # - Open relay: both lists empty (everyone can read and write)
107 #
108 # Example:
109 # allowed_npubs_read:
110 # - npub1a2b3c4d5e6f...
111 # allowed_npubs_write:
112 # - npub1a2b3c4d5e6f...
113
114 # Skip authentication for these methods 99 # Skip authentication for these methods
115 skip_methods: 100 skip_methods:
116 - "/grpc.health.v1.Health/Check" 101 - "/grpc.health.v1.Health/Check"
117 102
103# Common patterns:
104# Public relay (anyone can read, only whitelisted can write):
105# read:
106# enabled: false
107# write:
108# enabled: true
109# allowed_npubs:
110# - npub1...
111#
112# Private relay (whitelisted read and write):
113# read:
114# enabled: true
115# allowed_npubs:
116# - npub1...
117# write:
118# enabled: true
119# allowed_npubs:
120# - npub1...
121#
122# Open relay (everyone can read and write):
123# read:
124# enabled: false
125# write:
126# enabled: false
127
118# Rate limiting configuration 128# Rate limiting configuration
119rate_limit: 129rate_limit:
120 # Enable rate limiting 130 # Enable rate limiting
@@ -235,8 +245,10 @@ Complex types:
235 245
236```bash 246```bash
237# Lists (comma-separated, npub format) 247# Lists (comma-separated, npub format)
238export MUXSTR_AUTH_ALLOWED_NPUBS_READ="npub1...,npub1..." 248export MUXSTR_AUTH_READ_ENABLED=true
239export MUXSTR_AUTH_ALLOWED_NPUBS_WRITE="npub1..." 249export MUXSTR_AUTH_READ_ALLOWED_NPUBS="npub1...,npub1..."
250export MUXSTR_AUTH_WRITE_ENABLED=true
251export MUXSTR_AUTH_WRITE_ALLOWED_NPUBS="npub1..."
240 252
241# Durations 253# Durations
242export MUXSTR_SERVER_READ_TIMEOUT="30s" 254export MUXSTR_SERVER_READ_TIMEOUT="30s"
diff --git a/internal/config/config.go b/internal/config/config.go
index 3e52272..294510d 100644
--- a/internal/config/config.go
+++ b/internal/config/config.go
@@ -39,12 +39,19 @@ type DatabaseConfig struct {
39 39
40// AuthConfig holds authentication configuration. 40// AuthConfig holds authentication configuration.
41type AuthConfig struct { 41type AuthConfig struct {
42 Enabled bool `yaml:"enabled"` 42 Read AuthOperationConfig `yaml:"read"`
43 Required bool `yaml:"required"` 43 Write AuthOperationConfig `yaml:"write"`
44 TimestampWindow int64 `yaml:"timestamp_window"` 44 TimestampWindow int64 `yaml:"timestamp_window"`
45 AllowedNpubsRead []string `yaml:"allowed_npubs_read"` // npub format only (bech32) - normalized to hex internally 45 SkipMethods []string `yaml:"skip_methods"`
46 AllowedNpubsWrite []string `yaml:"allowed_npubs_write"` // npub format only (bech32) - normalized to hex internally 46}
47 SkipMethods []string `yaml:"skip_methods"` 47
48// AuthOperationConfig configures auth for a specific operation type (read or write).
49type AuthOperationConfig struct {
50 Enabled bool `yaml:"enabled"` // false = no auth required, true = auth required
51 AllowedNpubs []string `yaml:"allowed_npubs"` // npub format only - normalized to hex internally
52 // If enabled=false: no auth, allow all
53 // If enabled=true && allowed_npubs=[]: auth required, any valid signature accepted
54 // If enabled=true && allowed_npubs=[...]: auth required, only whitelisted npubs
48} 55}
49 56
50// RateLimitConfig holds rate limiting configuration. 57// RateLimitConfig holds rate limiting configuration.
@@ -111,8 +118,14 @@ func Default() *Config {
111 Path: "relay.db", 118 Path: "relay.db",
112 }, 119 },
113 Auth: AuthConfig{ 120 Auth: AuthConfig{
114 Enabled: false, 121 Read: AuthOperationConfig{
115 Required: false, 122 Enabled: false,
123 AllowedNpubs: nil,
124 },
125 Write: AuthOperationConfig{
126 Enabled: false,
127 AllowedNpubs: nil,
128 },
116 TimestampWindow: 60, 129 TimestampWindow: 60,
117 }, 130 },
118 RateLimit: RateLimitConfig{ 131 RateLimit: RateLimitConfig{
@@ -184,15 +197,15 @@ func normalizeNpubs(cfg *Config) error {
184 var err error 197 var err error
185 198
186 // Normalize read allowlist 199 // Normalize read allowlist
187 cfg.Auth.AllowedNpubsRead, err = normalizeNpubList(cfg.Auth.AllowedNpubsRead) 200 cfg.Auth.Read.AllowedNpubs, err = normalizeNpubList(cfg.Auth.Read.AllowedNpubs)
188 if err != nil { 201 if err != nil {
189 return fmt.Errorf("allowed_npubs_read: %w", err) 202 return fmt.Errorf("auth.read.allowed_npubs: %w", err)
190 } 203 }
191 204
192 // Normalize write allowlist 205 // Normalize write allowlist
193 cfg.Auth.AllowedNpubsWrite, err = normalizeNpubList(cfg.Auth.AllowedNpubsWrite) 206 cfg.Auth.Write.AllowedNpubs, err = normalizeNpubList(cfg.Auth.Write.AllowedNpubs)
194 if err != nil { 207 if err != nil {
195 return fmt.Errorf("allowed_npubs_write: %w", err) 208 return fmt.Errorf("auth.write.allowed_npubs: %w", err)
196 } 209 }
197 210
198 return nil 211 return nil
@@ -299,11 +312,17 @@ func applyEnvOverrides(cfg *Config) {
299 } 312 }
300 313
301 // Auth 314 // Auth
302 if val := os.Getenv("MUXSTR_AUTH_ENABLED"); val != "" { 315 if val := os.Getenv("MUXSTR_AUTH_READ_ENABLED"); val != "" {
303 cfg.Auth.Enabled = parseBool(val) 316 cfg.Auth.Read.Enabled = parseBool(val)
317 }
318 if val := os.Getenv("MUXSTR_AUTH_READ_ALLOWED_NPUBS"); val != "" {
319 cfg.Auth.Read.AllowedNpubs = strings.Split(val, ",")
304 } 320 }
305 if val := os.Getenv("MUXSTR_AUTH_REQUIRED"); val != "" { 321 if val := os.Getenv("MUXSTR_AUTH_WRITE_ENABLED"); val != "" {
306 cfg.Auth.Required = parseBool(val) 322 cfg.Auth.Write.Enabled = parseBool(val)
323 }
324 if val := os.Getenv("MUXSTR_AUTH_WRITE_ALLOWED_NPUBS"); val != "" {
325 cfg.Auth.Write.AllowedNpubs = strings.Split(val, ",")
307 } 326 }
308 if val := os.Getenv("MUXSTR_AUTH_TIMESTAMP_WINDOW"); val != "" { 327 if val := os.Getenv("MUXSTR_AUTH_TIMESTAMP_WINDOW"); val != "" {
309 var n int64 328 var n int64
@@ -311,12 +330,6 @@ func applyEnvOverrides(cfg *Config) {
311 cfg.Auth.TimestampWindow = n 330 cfg.Auth.TimestampWindow = n
312 } 331 }
313 } 332 }
314 if val := os.Getenv("MUXSTR_AUTH_ALLOWED_NPUBS_READ"); val != "" {
315 cfg.Auth.AllowedNpubsRead = strings.Split(val, ",")
316 }
317 if val := os.Getenv("MUXSTR_AUTH_ALLOWED_NPUBS_WRITE"); val != "" {
318 cfg.Auth.AllowedNpubsWrite = strings.Split(val, ",")
319 }
320 333
321 // Rate limit 334 // Rate limit
322 if val := os.Getenv("MUXSTR_RATE_LIMIT_ENABLED"); val != "" { 335 if val := os.Getenv("MUXSTR_RATE_LIMIT_ENABLED"); val != "" {
diff --git a/internal/config/config_test.go b/internal/config/config_test.go
index c0d4555..65a742a 100644
--- a/internal/config/config_test.go
+++ b/internal/config/config_test.go
@@ -40,8 +40,10 @@ database:
40 path: "test.db" 40 path: "test.db"
41 41
42auth: 42auth:
43 enabled: true 43 read:
44 required: true 44 enabled: true
45 write:
46 enabled: true
45 timestamp_window: 120 47 timestamp_window: 120
46 48
47rate_limit: 49rate_limit:
@@ -75,12 +77,12 @@ metrics:
75 t.Errorf("expected db path test.db, got %s", cfg.Database.Path) 77 t.Errorf("expected db path test.db, got %s", cfg.Database.Path)
76 } 78 }
77 79
78 if !cfg.Auth.Enabled { 80 if !cfg.Auth.Read.Enabled {
79 t.Error("expected auth enabled") 81 t.Error("expected auth read enabled")
80 } 82 }
81 83
82 if !cfg.Auth.Required { 84 if !cfg.Auth.Write.Enabled {
83 t.Error("expected auth required") 85 t.Error("expected auth write enabled")
84 } 86 }
85 87
86 if cfg.Auth.TimestampWindow != 120 { 88 if cfg.Auth.TimestampWindow != 120 {
@@ -99,11 +101,13 @@ metrics:
99func TestEnvOverrides(t *testing.T) { 101func TestEnvOverrides(t *testing.T) {
100 // Set environment variables 102 // Set environment variables
101 os.Setenv("MUXSTR_SERVER_GRPC_ADDR", ":7777") 103 os.Setenv("MUXSTR_SERVER_GRPC_ADDR", ":7777")
102 os.Setenv("MUXSTR_AUTH_ENABLED", "true") 104 os.Setenv("MUXSTR_AUTH_READ_ENABLED", "true")
105 os.Setenv("MUXSTR_AUTH_WRITE_ENABLED", "true")
103 os.Setenv("MUXSTR_RATE_LIMIT_DEFAULT_RPS", "200") 106 os.Setenv("MUXSTR_RATE_LIMIT_DEFAULT_RPS", "200")
104 defer func() { 107 defer func() {
105 os.Unsetenv("MUXSTR_SERVER_GRPC_ADDR") 108 os.Unsetenv("MUXSTR_SERVER_GRPC_ADDR")
106 os.Unsetenv("MUXSTR_AUTH_ENABLED") 109 os.Unsetenv("MUXSTR_AUTH_READ_ENABLED")
110 os.Unsetenv("MUXSTR_AUTH_WRITE_ENABLED")
107 os.Unsetenv("MUXSTR_RATE_LIMIT_DEFAULT_RPS") 111 os.Unsetenv("MUXSTR_RATE_LIMIT_DEFAULT_RPS")
108 }() 112 }()
109 113
@@ -118,8 +122,12 @@ func TestEnvOverrides(t *testing.T) {
118 t.Errorf("expected env override :7777, got %s", cfg.Server.GrpcAddr) 122 t.Errorf("expected env override :7777, got %s", cfg.Server.GrpcAddr)
119 } 123 }
120 124
121 if !cfg.Auth.Enabled { 125 if !cfg.Auth.Read.Enabled {
122 t.Error("expected auth enabled from env") 126 t.Error("expected auth read enabled from env")
127 }
128
129 if !cfg.Auth.Write.Enabled {
130 t.Error("expected auth write enabled from env")
123 } 131 }
124 132
125 if cfg.RateLimit.DefaultRPS != 200 { 133 if cfg.RateLimit.DefaultRPS != 200 {
@@ -206,7 +214,8 @@ func TestSaveAndLoad(t *testing.T) {
206 // Create config 214 // Create config
207 cfg := Default() 215 cfg := Default()
208 cfg.Server.GrpcAddr = ":9999" 216 cfg.Server.GrpcAddr = ":9999"
209 cfg.Auth.Enabled = true 217 cfg.Auth.Read.Enabled = true
218 cfg.Auth.Write.Enabled = true
210 cfg.RateLimit.DefaultRPS = 100 219 cfg.RateLimit.DefaultRPS = 100
211 220
212 // Save to temp file 221 // Save to temp file
@@ -232,8 +241,12 @@ func TestSaveAndLoad(t *testing.T) {
232 t.Errorf("expected grpc_addr :9999, got %s", loaded.Server.GrpcAddr) 241 t.Errorf("expected grpc_addr :9999, got %s", loaded.Server.GrpcAddr)
233 } 242 }
234 243
235 if !loaded.Auth.Enabled { 244 if !loaded.Auth.Read.Enabled {
236 t.Error("expected auth enabled") 245 t.Error("expected auth read enabled")
246 }
247
248 if !loaded.Auth.Write.Enabled {
249 t.Error("expected auth write enabled")
237 } 250 }
238 251
239 if loaded.RateLimit.DefaultRPS != 100 { 252 if loaded.RateLimit.DefaultRPS != 100 {
@@ -259,12 +272,15 @@ database:
259 path: "test.db" 272 path: "test.db"
260 273
261auth: 274auth:
262 enabled: true 275 read:
263 allowed_npubs_read: 276 enabled: true
264 - npub180cvv07tjdrrgpa0j7j7tmnyl2yr6yr7l8j4s3evf6u64th6gkwsyjh6w6 277 allowed_npubs:
265 - npub1l2vyh47mk2p0qlsku7hg0vn29faehy9hy34ygaclpn66ukqp3afqutajft 278 - npub180cvv07tjdrrgpa0j7j7tmnyl2yr6yr7l8j4s3evf6u64th6gkwsyjh6w6
266 allowed_npubs_write: 279 - npub1l2vyh47mk2p0qlsku7hg0vn29faehy9hy34ygaclpn66ukqp3afqutajft
267 - npub180cvv07tjdrrgpa0j7j7tmnyl2yr6yr7l8j4s3evf6u64th6gkwsyjh6w6 280 write:
281 enabled: true
282 allowed_npubs:
283 - npub180cvv07tjdrrgpa0j7j7tmnyl2yr6yr7l8j4s3evf6u64th6gkwsyjh6w6
268` 284`
269 285
270 if _, err := tmpfile.Write([]byte(configData)); err != nil { 286 if _, err := tmpfile.Write([]byte(configData)); err != nil {
@@ -278,17 +294,17 @@ auth:
278 } 294 }
279 295
280 // Verify read npubs were normalized to hex 296 // Verify read npubs were normalized to hex
281 if len(cfg.Auth.AllowedNpubsRead) != 2 { 297 if len(cfg.Auth.Read.AllowedNpubs) != 2 {
282 t.Errorf("expected 2 allowed npubs for read, got %d", len(cfg.Auth.AllowedNpubsRead)) 298 t.Errorf("expected 2 allowed npubs for read, got %d", len(cfg.Auth.Read.AllowedNpubs))
283 } 299 }
284 300
285 // Verify write npubs were normalized to hex 301 // Verify write npubs were normalized to hex
286 if len(cfg.Auth.AllowedNpubsWrite) != 1 { 302 if len(cfg.Auth.Write.AllowedNpubs) != 1 {
287 t.Errorf("expected 1 allowed npub for write, got %d", len(cfg.Auth.AllowedNpubsWrite)) 303 t.Errorf("expected 1 allowed npub for write, got %d", len(cfg.Auth.Write.AllowedNpubs))
288 } 304 }
289 305
290 // Check that they're hex format (64 chars, not npub1...) 306 // Check that they're hex format (64 chars, not npub1...)
291 for i, pubkey := range cfg.Auth.AllowedNpubsRead { 307 for i, pubkey := range cfg.Auth.Read.AllowedNpubs {
292 if len(pubkey) != 64 { 308 if len(pubkey) != 64 {
293 t.Errorf("read npub %d: expected 64 hex chars, got %d", i, len(pubkey)) 309 t.Errorf("read npub %d: expected 64 hex chars, got %d", i, len(pubkey))
294 } 310 }
@@ -297,7 +313,7 @@ auth:
297 } 313 }
298 } 314 }
299 315
300 for i, pubkey := range cfg.Auth.AllowedNpubsWrite { 316 for i, pubkey := range cfg.Auth.Write.AllowedNpubs {
301 if len(pubkey) != 64 { 317 if len(pubkey) != 64 {
302 t.Errorf("write npub %d: expected 64 hex chars, got %d", i, len(pubkey)) 318 t.Errorf("write npub %d: expected 64 hex chars, got %d", i, len(pubkey))
303 } 319 }
@@ -310,14 +326,14 @@ auth:
310 expectedHex1 := "3bf0c63fcb93463407af97a5e5ee64fa883d107ef9e558472c4eb9aaaefa459d" 326 expectedHex1 := "3bf0c63fcb93463407af97a5e5ee64fa883d107ef9e558472c4eb9aaaefa459d"
311 expectedHex2 := "fa984bd7dbb282f07e16e7ae87b26a2a7b9b90b7246a44771f0cf5ae58018f52" 327 expectedHex2 := "fa984bd7dbb282f07e16e7ae87b26a2a7b9b90b7246a44771f0cf5ae58018f52"
312 328
313 if cfg.Auth.AllowedNpubsRead[0] != expectedHex1 { 329 if cfg.Auth.Read.AllowedNpubs[0] != expectedHex1 {
314 t.Errorf("read npub 0: expected %s, got %s", expectedHex1, cfg.Auth.AllowedNpubsRead[0]) 330 t.Errorf("read npub 0: expected %s, got %s", expectedHex1, cfg.Auth.Read.AllowedNpubs[0])
315 } 331 }
316 if cfg.Auth.AllowedNpubsRead[1] != expectedHex2 { 332 if cfg.Auth.Read.AllowedNpubs[1] != expectedHex2 {
317 t.Errorf("read npub 1: expected %s, got %s", expectedHex2, cfg.Auth.AllowedNpubsRead[1]) 333 t.Errorf("read npub 1: expected %s, got %s", expectedHex2, cfg.Auth.Read.AllowedNpubs[1])
318 } 334 }
319 if cfg.Auth.AllowedNpubsWrite[0] != expectedHex1 { 335 if cfg.Auth.Write.AllowedNpubs[0] != expectedHex1 {
320 t.Errorf("write npub 0: expected %s, got %s", expectedHex1, cfg.Auth.AllowedNpubsWrite[0]) 336 t.Errorf("write npub 0: expected %s, got %s", expectedHex1, cfg.Auth.Write.AllowedNpubs[0])
321 } 337 }
322} 338}
323 339
@@ -337,8 +353,10 @@ server:
337database: 353database:
338 path: "test.db" 354 path: "test.db"
339auth: 355auth:
340 allowed_npubs_read: 356 read:
341 - 3bf0c63fcb93463407af97a5e5ee64fa883d107ef9e558472c4eb9aaaefa459d 357 enabled: true
358 allowed_npubs:
359 - 3bf0c63fcb93463407af97a5e5ee64fa883d107ef9e558472c4eb9aaaefa459d
342`, 360`,
343 expectError: true, 361 expectError: true,
344 errorMsg: "must start with 'npub1'", 362 errorMsg: "must start with 'npub1'",
@@ -352,8 +370,10 @@ server:
352database: 370database:
353 path: "test.db" 371 path: "test.db"
354auth: 372auth:
355 allowed_npubs_write: 373 write:
356 - 3bf0c63fcb93463407af97a5e5ee64fa883d107ef9e558472c4eb9aaaefa459d 374 enabled: true
375 allowed_npubs:
376 - 3bf0c63fcb93463407af97a5e5ee64fa883d107ef9e558472c4eb9aaaefa459d
357`, 377`,
358 expectError: true, 378 expectError: true,
359 errorMsg: "must start with 'npub1'", 379 errorMsg: "must start with 'npub1'",
@@ -367,10 +387,14 @@ server:
367database: 387database:
368 path: "test.db" 388 path: "test.db"
369auth: 389auth:
370 allowed_npubs_read: 390 read:
371 - npub180cvv07tjdrrgpa0j7j7tmnyl2yr6yr7l8j4s3evf6u64th6gkwsyjh6w6 391 enabled: true
372 allowed_npubs_write: 392 allowed_npubs:
373 - npub1l2vyh47mk2p0qlsku7hg0vn29faehy9hy34ygaclpn66ukqp3afqutajft 393 - npub180cvv07tjdrrgpa0j7j7tmnyl2yr6yr7l8j4s3evf6u64th6gkwsyjh6w6
394 write:
395 enabled: true
396 allowed_npubs:
397 - npub1l2vyh47mk2p0qlsku7hg0vn29faehy9hy34ygaclpn66ukqp3afqutajft
374`, 398`,
375 expectError: false, 399 expectError: false,
376 }, 400 },