Commit 23e979a2af

23e979a2af8b16dd09ab76d704401ffea61d743d

parent: 28f6758983

Verified · cmc

cmc <hello@cleberg.net> · 2026-09-28 07:19 UTC

sshd: test the auth limiter's lockout by registration mode

Ref #260

Layout: unified · split

internal/sshd/sshd_test.go +115
@@ -296,3 +296,118 @@ func TestUnregisteredKeyMessageNamesFingerprintAndHost(t *testing.T) {
296 } 296 }
297 } 297 }
298} 298}
299
300// authMeta is the connection metadata authenticate reads: only the
301// remote address.
302type authMeta struct {
303 ssh.ConnMetadata
304 addr net.Addr
305}
306
307func (m authMeta) RemoteAddr() net.Addr { return m.addr }
308
309func authKey(t *testing.T) ssh.PublicKey {
310 t.Helper()
311 pub, _, err := ed25519.GenerateKey(rand.Reader)
312 if err != nil {
313 t.Fatal(err)
314 }
315 k, err := ssh.NewPublicKey(pub)
316 if err != nil {
317 t.Fatal(err)
318 }
319 return k
320}
321
322// authServer is a Server holding what authenticate uses: a store with a
323// runner account's key, the registration mode, and a limiter of three
324// failures a minute.
325func authServer(t *testing.T, mode string) (*Server, ssh.PublicKey) {
326 t.Helper()
327 st, err := store.Open(filepath.Join(t.TempDir(), "gitbay.db"))
328 if err != nil {
329 t.Fatal(err)
330 }
331 t.Cleanup(func() { st.Close() })
332 if err := st.MigrateUp(); err != nil {
333 t.Fatal(err)
334 }
335 uid, err := st.CreateUser("ci", false)
336 if err != nil {
337 t.Fatal(err)
338 }
339 runner := authKey(t)
340 if err := st.AddSSHKey(uid, ssh.FingerprintSHA256(runner), runner.Type(), runner.Marshal(), "runner", ""); err != nil {
341 t.Fatal(err)
342 }
343 cfg := config.Default()
344 cfg.Registration.Mode = mode
345 return &Server{cfg: cfg, st: st, authLimiter: newRateLimiter(3, time.Minute)}, runner
346}
347
348// With registration closed an unknown key counts against its address.
349// Below the limit a known key's success clears the count. At the limit
350// authenticate refuses before it looks at the key, so the runner's own
351// key from that address is refused too and its success never runs to
352// clear anything, until the window passes. Another address is not
353// affected. This is why a build must not share the runner's source
354// address (#260).
355func TestAuthLockoutHoldsAgainstTheRunnersKey(t *testing.T) {
356 s, runner := authServer(t, "closed")
357 stranger := authKey(t)
358 failTimes := func(n int) {
359 t.Helper()
360 for i := 0; i < n; i++ {
361 if _, err := s.authenticate(fromLoopback, stranger); err == nil {
362 t.Fatal("unknown key admitted with registration closed")
363 }
364 }
365 }
366
367 failTimes(2)
368 if _, err := s.authenticate(fromLoopback, runner); err != nil {
369 t.Fatalf("runner below the limit: %v", err)
370 }
371 failTimes(2)
372 if _, err := s.authenticate(fromLoopback, runner); err != nil {
373 t.Fatalf("runner after its success cleared the count: %v", err)
374 }
375
376 failTimes(3)
377 for i := 0; i < 2; i++ {
378 if _, err := s.authenticate(fromLoopback, runner); err == nil || !strings.Contains(err.Error(), "too many") {
379 t.Fatalf("attempt %d from a locked-out address: %v, want refused", i+1, err)
380 }
381 }
382 if _, err := s.authenticate(fromPublic, runner); err != nil {
383 t.Fatalf("another address was locked out too: %v", err)
384 }
385
386 s.authLimiter.seen["127.0.0.1"].start = time.Now().Add(-2 * time.Minute)
387 if _, err := s.authenticate(fromLoopback, runner); err != nil {
388 t.Fatalf("runner after the window passed: %v", err)
389 }
390}
391
392// With registration open or by invite, an unknown key is admitted to run
393// register and never counts, so no number of unknown-key attempts locks
394// the runner's address out. gitbay.org runs open registration (#260).
395func TestAuthUnknownKeyCountsOnlyWhenClosed(t *testing.T) {
396 for _, mode := range []string{"open", "invite"} {
397 s, runner := authServer(t, mode)
398 for i := 0; i < 10; i++ {
399 p, err := s.authenticate(fromLoopback, authKey(t))
400 if err != nil || p.Extensions["anon-key"] == "" {
401 t.Fatalf("%s: unknown key %d: %v %+v", mode, i+1, err, p)
402 }
403 }
404 if _, err := s.authenticate(fromLoopback, runner); err != nil {
405 t.Fatalf("%s: runner refused after unknown keys: %v", mode, err)
406 }
407 }
408}
409
410var (
411 fromLoopback = authMeta{addr: &net.TCPAddr{IP: net.IPv4(127, 0, 0, 1), Port: 40000}}
412 fromPublic = authMeta{addr: &net.TCPAddr{IP: net.IPv4(203, 0, 113, 7), Port: 40000}}
413)