refactor(config): infer auth type from config instead of explicit type field
- commit
- 5276d7160e247748b8fba82d421328e30911a2e9
- author
- bnema · · 2026-01-18 03:16 UTC
- parent
- fb60add
- signature
- unsigned
1 file · +55 -37
M internal/app/run.go +55 -37
| @@ -7,6 +7,7 @@ import ( | |||
| 7 | 7 | "encoding/hex" | |
| 8 | 8 | "encoding/json" | |
| 9 | 9 | "fmt" | |
| 10 | + | "net" | |
| 10 | 11 | "net/http" | |
| 11 | 12 | "os" | |
| 12 | 13 | "os/signal" | |
| @@ -31,10 +32,9 @@ import ( | |||
| 31 | 32 | "gordon/internal/adapters/out/secrets" | |
| 32 | 33 | "gordon/internal/adapters/out/tokenstore" | |
| 33 | 34 | ||
| 34 | - | "golang.org/x/time/rate" | |
| 35 | - | ||
| 36 | 35 | // Adapters - Input | |
| 37 | 36 | "gordon/internal/adapters/in/http/admin" | |
| 37 | + | authhandler "gordon/internal/adapters/in/http/auth" | |
| 38 | 38 | "gordon/internal/adapters/in/http/middleware" | |
| 39 | 39 | "gordon/internal/adapters/in/http/registry" | |
| 40 | 40 | ||
| @@ -127,7 +127,7 @@ type services struct { | |||
| 127 | 127 | registrySvc *registrySvc.Service | |
| 128 | 128 | proxySvc *proxy.Service | |
| 129 | 129 | authSvc *auth.Service | |
| 130 | - | tokenHandler *registry.TokenHandler | |
| 130 | + | authHandler *authhandler.Handler | |
| 131 | 131 | adminHandler *admin.Handler | |
| 132 | 132 | internalRegUser string | |
| 133 | 133 | internalRegPass string | |
| @@ -301,11 +301,11 @@ func createServices(ctx context.Context, v *viper.Viper, cfg Config, log zerowra | |||
| 301 | 301 | ||
| 302 | 302 | // Create token handler for registry token endpoint | |
| 303 | 303 | if svc.authSvc != nil { | |
| 304 | - | internalAuth := registry.InternalAuth{ | |
| 304 | + | internalAuth := authhandler.InternalAuth{ | |
| 305 | 305 | Username: svc.internalRegUser, | |
| 306 | 306 | Password: svc.internalRegPass, | |
| 307 | 307 | } | |
| 308 | - | svc.tokenHandler = registry.NewTokenHandler(svc.authSvc, internalAuth, log) | |
| 308 | + | svc.authHandler = authhandler.NewHandler(svc.authSvc, internalAuth, log) | |
| 309 | 309 | } | |
| 310 | 310 | ||
| 311 | 311 | // Determine env directory for admin API | |
| @@ -568,11 +568,11 @@ func createAuthService(ctx context.Context, cfg Config, log zerowrap.Logger) (ou | |||
| 568 | 568 | return nil, nil, nil | |
| 569 | 569 | } | |
| 570 | 570 | ||
| 571 | - | authType := resolveAuthType(cfg.Auth.Type) | |
| 571 | + | authType := resolveAuthType(cfg) | |
| 572 | 572 | backend := resolveSecretsBackend(cfg.Auth.SecretsBackend) | |
| 573 | 573 | dataDir := resolveDataDir(cfg.Server.DataDir) | |
| 574 | 574 | ||
| 575 | - | store, err := createTokenStore(authType, backend, dataDir, log) | |
| 575 | + | store, err := createTokenStore(backend, dataDir, log) | |
| 576 | 576 | if err != nil { | |
| 577 | 577 | return nil, nil, err | |
| 578 | 578 | } | |
| @@ -592,11 +592,26 @@ func createAuthService(ctx context.Context, cfg Config, log zerowrap.Logger) (ou | |||
| 592 | 592 | return store, authSvc, nil | |
| 593 | 593 | } | |
| 594 | 594 | ||
| 595 | - | func resolveAuthType(authType string) domain.AuthType { | |
| 596 | - | if authType == "token" { | |
| 595 | + | // resolveAuthType determines the auth type from config. | |
| 596 | + | // If password_hash is configured, password auth is available (plus tokens). | |
| 597 | + | // If only token_secret is configured, token-only mode. | |
| 598 | + | // The explicit "type" field is deprecated but still respected for backwards compat. | |
| 599 | + | func resolveAuthType(cfg Config) domain.AuthType { | |
| 600 | + | // Explicit type takes precedence (backwards compatibility) | |
| 601 | + | if cfg.Auth.Type == "token" { | |
| 597 | 602 | return domain.AuthTypeToken | |
| 598 | 603 | } | |
| 599 | - | return domain.AuthTypePassword | |
| 604 | + | if cfg.Auth.Type == "password" { | |
| 605 | + | return domain.AuthTypePassword | |
| 606 | + | } | |
| 607 | + | ||
| 608 | + | // Infer from config: password auth if password_hash is configured | |
| 609 | + | if cfg.Auth.PasswordHash != "" || cfg.Auth.Password != "" { | |
| 610 | + | return domain.AuthTypePassword | |
| 611 | + | } | |
| 612 | + | ||
| 613 | + | // Default to token-only | |
| 614 | + | return domain.AuthTypeToken | |
| 600 | 615 | } | |
| 601 | 616 | ||
| 602 | 617 | func resolveSecretsBackend(backend string) domain.SecretsBackend { | |
| @@ -619,11 +634,8 @@ func resolveDataDir(dataDir string) string { | |||
| 619 | 634 | return dataDir | |
| 620 | 635 | } | |
| 621 | 636 | ||
| 622 | - | func createTokenStore(authType domain.AuthType, backend domain.SecretsBackend, dataDir string, log zerowrap.Logger) (out.TokenStore, error) { | |
| 623 | - | if authType != domain.AuthTypeToken { | |
| 624 | - | return nil, nil | |
| 625 | - | } | |
| 626 | - | ||
| 637 | + | func createTokenStore(backend domain.SecretsBackend, dataDir string, log zerowrap.Logger) (out.TokenStore, error) { | |
| 638 | + | // Token store is always created since tokens work in both auth modes | |
| 627 | 639 | store, err := tokenstore.NewStore(backend, dataDir, log) | |
| 628 | 640 | if err != nil { | |
| 629 | 641 | return nil, log.WrapErr(err, "failed to create token store") | |
| @@ -638,8 +650,16 @@ func buildAuthConfig(ctx context.Context, cfg Config, authType domain.AuthType, | |||
| 638 | 650 | Username: cfg.Auth.Username, | |
| 639 | 651 | } | |
| 640 | 652 | ||
| 641 | - | switch authType { | |
| 642 | - | case domain.AuthTypePassword: | |
| 653 | + | // Token config is always required (tokens work in all auth modes) | |
| 654 | + | secret, expiry, err := loadTokenConfig(ctx, cfg, backend, dataDir, log) | |
| 655 | + | if err != nil { | |
| 656 | + | return auth.Config{}, err | |
| 657 | + | } | |
| 658 | + | authConfig.TokenSecret = secret | |
| 659 | + | authConfig.TokenExpiry = expiry | |
| 660 | + | ||
| 661 | + | // Password config only needed for password auth mode | |
| 662 | + | if authType == domain.AuthTypePassword { | |
| 643 | 663 | hash, err := loadPasswordHash(ctx, cfg, backend, dataDir, log) | |
| 644 | 664 | if err != nil { | |
| 645 | 665 | return auth.Config{}, err | |
| @@ -648,13 +668,6 @@ func buildAuthConfig(ctx context.Context, cfg Config, authType domain.AuthType, | |||
| 648 | 668 | return auth.Config{}, errAuthNotConfigured() | |
| 649 | 669 | } | |
| 650 | 670 | authConfig.PasswordHash = hash | |
| 651 | - | case domain.AuthTypeToken: | |
| 652 | - | secret, expiry, err := loadTokenConfig(ctx, cfg, backend, dataDir, log) | |
| 653 | - | if err != nil { | |
| 654 | - | return auth.Config{}, err | |
| 655 | - | } | |
| 656 | - | authConfig.TokenSecret = secret | |
| 657 | - | authConfig.TokenExpiry = expiry | |
| 658 | 671 | } | |
| 659 | 672 | ||
| 660 | 673 | return authConfig, nil | |
| @@ -714,7 +727,7 @@ func loadTokenSecret(ctx context.Context, cfg Config, backend domain.SecretsBack | |||
| 714 | 727 | ||
| 715 | 728 | // Fall back to config-specified path via secrets backend | |
| 716 | 729 | if cfg.Auth.TokenSecret == "" { | |
| 717 | - | return nil, fmt.Errorf("token_secret is required for token authentication; set %s environment variable or configure auth.token_secret", TokenSecretEnvVar) | |
| 730 | + | return nil, fmt.Errorf("token_secret is required for JWT token generation; set %s environment variable or configure auth.token_secret", TokenSecretEnvVar) | |
| 718 | 731 | } | |
| 719 | 732 | ||
| 720 | 733 | secret, err := loadSecret(ctx, backend, cfg.Auth.TokenSecret, dataDir, log) | |
| @@ -902,19 +915,21 @@ func createHTTPHandlers(svc *services, cfg Config, log zerowrap.Logger) (http.Ha | |||
| 902 | 915 | ||
| 903 | 916 | registryWithMiddleware := middleware.Chain(registryMiddlewares...)(registryHandler) | |
| 904 | 917 | ||
| 905 | - | // Create a mux that routes /v2/token to the token handler (no auth required) | |
| 906 | - | // and all other /v2/* routes to the registry handler (with auth) | |
| 918 | + | // Create a mux that routes: | |
| 919 | + | // - /auth/* to auth handler (no auth required, for password login and token exchange) | |
| 920 | + | // - /v2/* to registry handler (with auth) | |
| 921 | + | // - /admin/* to admin handler (with auth) | |
| 907 | 922 | registryMux := http.NewServeMux() | |
| 908 | - | if svc.tokenHandler != nil { | |
| 909 | - | // Token endpoint is NOT protected by auth - it's where clients get tokens | |
| 910 | - | // but it still needs rate limiting to prevent brute force attacks | |
| 911 | - | tokenWithMiddleware := middleware.Chain( | |
| 923 | + | if svc.authHandler != nil { | |
| 924 | + | // Auth endpoints are NOT protected by auth - they're where clients authenticate | |
| 925 | + | // but still need rate limiting to prevent brute force attacks | |
| 926 | + | authWithMiddleware := middleware.Chain( | |
| 912 | 927 | middleware.PanicRecovery(log), | |
| 913 | 928 | middleware.RequestLogger(log), | |
| 914 | 929 | middleware.SecurityHeaders, | |
| 915 | 930 | rateLimitMiddleware, | |
| 916 | - | )(svc.tokenHandler) | |
| 917 | - | registryMux.Handle("/v2/token", tokenWithMiddleware) | |
| 931 | + | )(svc.authHandler) | |
| 932 | + | registryMux.Handle("/auth/", authWithMiddleware) | |
| 918 | 933 | } | |
| 919 | 934 | registryMux.Handle("/v2/", registryWithMiddleware) | |
| 920 | 935 | ||
| @@ -927,12 +942,15 @@ func createHTTPHandlers(svc *services, cfg Config, log zerowrap.Logger) (http.Ha | |||
| 927 | 942 | } | |
| 928 | 943 | // Add admin auth middleware if auth is enabled | |
| 929 | 944 | if svc.authSvc != nil { | |
| 930 | - | // Rate limiter for admin API uses per-IP rate from config | |
| 931 | - | var adminLimiter *rate.Limiter | |
| 945 | + | // Create rate limiters for admin API - uses same config as registry | |
| 946 | + | var globalLimiter, ipLimiter out.RateLimiter | |
| 947 | + | var trustedNets []*net.IPNet | |
| 932 | 948 | if cfg.API.RateLimit.Enabled { | |
| 933 | - | adminLimiter = rate.NewLimiter(rate.Limit(cfg.API.RateLimit.PerIPRPS), cfg.API.RateLimit.Burst) | |
| 949 | + | globalLimiter = ratelimit.NewMemoryStore(cfg.API.RateLimit.GlobalRPS, cfg.API.RateLimit.Burst, log) | |
| 950 | + | ipLimiter = ratelimit.NewMemoryStore(cfg.API.RateLimit.PerIPRPS, cfg.API.RateLimit.Burst, log) | |
| 951 | + | trustedNets = middleware.ParseTrustedProxies(cfg.API.RateLimit.TrustedProxies) | |
| 934 | 952 | } | |
| 935 | - | adminMiddlewares = append(adminMiddlewares, admin.AuthMiddleware(svc.authSvc, adminLimiter, log)) | |
| 953 | + | adminMiddlewares = append(adminMiddlewares, admin.AuthMiddleware(svc.authSvc, globalLimiter, ipLimiter, trustedNets, log)) | |
| 936 | 954 | } | |
| 937 | 955 | adminWithMiddleware := middleware.Chain(adminMiddlewares...)(svc.adminHandler) | |
| 938 | 956 | registryMux.Handle("/admin/", adminWithMiddleware) | |
| @@ -7,6 +7,7 @@ import ( | |||||
| 7 | "encoding/hex" | 7 | "encoding/hex" | ||
| 8 | "encoding/json" | 8 | "encoding/json" | ||
| 9 | "fmt" | 9 | "fmt" | ||
| 10 | + | "net" | |||
| 10 | "net/http" | 11 | "net/http" | ||
| 11 | "os" | 12 | "os" | ||
| 12 | "os/signal" | 13 | "os/signal" | ||
| @@ -31,10 +32,9 @@ import ( | |||||
| 31 | "gordon/internal/adapters/out/secrets" | 32 | "gordon/internal/adapters/out/secrets" | ||
| 32 | "gordon/internal/adapters/out/tokenstore" | 33 | "gordon/internal/adapters/out/tokenstore" | ||
| 33 | 34 | ||||
| 34 | - | "golang.org/x/time/rate" | |||
| 35 | - | ||||
| 36 | // Adapters - Input | 35 | // Adapters - Input | ||
| 37 | "gordon/internal/adapters/in/http/admin" | 36 | "gordon/internal/adapters/in/http/admin" | ||
| 37 | + | authhandler "gordon/internal/adapters/in/http/auth" | |||
| 38 | "gordon/internal/adapters/in/http/middleware" | 38 | "gordon/internal/adapters/in/http/middleware" | ||
| 39 | "gordon/internal/adapters/in/http/registry" | 39 | "gordon/internal/adapters/in/http/registry" | ||
| 40 | 40 | ||||
| @@ -127,7 +127,7 @@ type services struct { | |||||
| 127 | registrySvc *registrySvc.Service | 127 | registrySvc *registrySvc.Service | ||
| 128 | proxySvc *proxy.Service | 128 | proxySvc *proxy.Service | ||
| 129 | authSvc *auth.Service | 129 | authSvc *auth.Service | ||
| 130 | - | tokenHandler *registry.TokenHandler | 130 | + | authHandler *authhandler.Handler |
| 131 | adminHandler *admin.Handler | 131 | adminHandler *admin.Handler | ||
| 132 | internalRegUser string | 132 | internalRegUser string | ||
| 133 | internalRegPass string | 133 | internalRegPass string | ||
| @@ -301,11 +301,11 @@ func createServices(ctx context.Context, v *viper.Viper, cfg Config, log zerowra | |||||
| 301 | 301 | ||||
| 302 | // Create token handler for registry token endpoint | 302 | // Create token handler for registry token endpoint | ||
| 303 | if svc.authSvc != nil { | 303 | if svc.authSvc != nil { | ||
| 304 | - | internalAuth := registry.InternalAuth{ | 304 | + | internalAuth := authhandler.InternalAuth{ |
| 305 | Username: svc.internalRegUser, | 305 | Username: svc.internalRegUser, | ||
| 306 | Password: svc.internalRegPass, | 306 | Password: svc.internalRegPass, | ||
| 307 | } | 307 | } | ||
| 308 | - | svc.tokenHandler = registry.NewTokenHandler(svc.authSvc, internalAuth, log) | 308 | + | svc.authHandler = authhandler.NewHandler(svc.authSvc, internalAuth, log) |
| 309 | } | 309 | } | ||
| 310 | 310 | ||||
| 311 | // Determine env directory for admin API | 311 | // Determine env directory for admin API | ||
| @@ -568,11 +568,11 @@ func createAuthService(ctx context.Context, cfg Config, log zerowrap.Logger) (ou | |||||
| 568 | return nil, nil, nil | 568 | return nil, nil, nil | ||
| 569 | } | 569 | } | ||
| 570 | 570 | ||||
| 571 | - | authType := resolveAuthType(cfg.Auth.Type) | 571 | + | authType := resolveAuthType(cfg) |
| 572 | backend := resolveSecretsBackend(cfg.Auth.SecretsBackend) | 572 | backend := resolveSecretsBackend(cfg.Auth.SecretsBackend) | ||
| 573 | dataDir := resolveDataDir(cfg.Server.DataDir) | 573 | dataDir := resolveDataDir(cfg.Server.DataDir) | ||
| 574 | 574 | ||||
| 575 | - | store, err := createTokenStore(authType, backend, dataDir, log) | 575 | + | store, err := createTokenStore(backend, dataDir, log) |
| 576 | if err != nil { | 576 | if err != nil { | ||
| 577 | return nil, nil, err | 577 | return nil, nil, err | ||
| 578 | } | 578 | } | ||
| @@ -592,11 +592,26 @@ func createAuthService(ctx context.Context, cfg Config, log zerowrap.Logger) (ou | |||||
| 592 | return store, authSvc, nil | 592 | return store, authSvc, nil | ||
| 593 | } | 593 | } | ||
| 594 | 594 | ||||
| 595 | - | func resolveAuthType(authType string) domain.AuthType { | 595 | + | // resolveAuthType determines the auth type from config. |
| 596 | - | if authType == "token" { | 596 | + | // If password_hash is configured, password auth is available (plus tokens). |
| 597 | + | // If only token_secret is configured, token-only mode. | |||
| 598 | + | // The explicit "type" field is deprecated but still respected for backwards compat. | |||
| 599 | + | func resolveAuthType(cfg Config) domain.AuthType { | |||
| 600 | + | // Explicit type takes precedence (backwards compatibility) | |||
| 601 | + | if cfg.Auth.Type == "token" { | |||
| 597 | return domain.AuthTypeToken | 602 | return domain.AuthTypeToken | ||
| 598 | } | 603 | } | ||
| 599 | - | return domain.AuthTypePassword | 604 | + | if cfg.Auth.Type == "password" { |
| 605 | + | return domain.AuthTypePassword | |||
| 606 | + | } | |||
| 607 | + | ||||
| 608 | + | // Infer from config: password auth if password_hash is configured | |||
| 609 | + | if cfg.Auth.PasswordHash != "" || cfg.Auth.Password != "" { | |||
| 610 | + | return domain.AuthTypePassword | |||
| 611 | + | } | |||
| 612 | + | ||||
| 613 | + | // Default to token-only | |||
| 614 | + | return domain.AuthTypeToken | |||
| 600 | } | 615 | } | ||
| 601 | 616 | ||||
| 602 | func resolveSecretsBackend(backend string) domain.SecretsBackend { | 617 | func resolveSecretsBackend(backend string) domain.SecretsBackend { | ||
| @@ -619,11 +634,8 @@ func resolveDataDir(dataDir string) string { | |||||
| 619 | return dataDir | 634 | return dataDir | ||
| 620 | } | 635 | } | ||
| 621 | 636 | ||||
| 622 | - | func createTokenStore(authType domain.AuthType, backend domain.SecretsBackend, dataDir string, log zerowrap.Logger) (out.TokenStore, error) { | 637 | + | func createTokenStore(backend domain.SecretsBackend, dataDir string, log zerowrap.Logger) (out.TokenStore, error) { |
| 623 | - | if authType != domain.AuthTypeToken { | 638 | + | // Token store is always created since tokens work in both auth modes |
| 624 | - | return nil, nil | |||
| 625 | - | } | |||
| 626 | - | ||||
| 627 | store, err := tokenstore.NewStore(backend, dataDir, log) | 639 | store, err := tokenstore.NewStore(backend, dataDir, log) | ||
| 628 | if err != nil { | 640 | if err != nil { | ||
| 629 | return nil, log.WrapErr(err, "failed to create token store") | 641 | return nil, log.WrapErr(err, "failed to create token store") | ||
| @@ -638,8 +650,16 @@ func buildAuthConfig(ctx context.Context, cfg Config, authType domain.AuthType, | |||||
| 638 | Username: cfg.Auth.Username, | 650 | Username: cfg.Auth.Username, | ||
| 639 | } | 651 | } | ||
| 640 | 652 | ||||
| 641 | - | switch authType { | 653 | + | // Token config is always required (tokens work in all auth modes) |
| 642 | - | case domain.AuthTypePassword: | 654 | + | secret, expiry, err := loadTokenConfig(ctx, cfg, backend, dataDir, log) |
| 655 | + | if err != nil { | |||
| 656 | + | return auth.Config{}, err | |||
| 657 | + | } | |||
| 658 | + | authConfig.TokenSecret = secret | |||
| 659 | + | authConfig.TokenExpiry = expiry | |||
| 660 | + | ||||
| 661 | + | // Password config only needed for password auth mode | |||
| 662 | + | if authType == domain.AuthTypePassword { | |||
| 643 | hash, err := loadPasswordHash(ctx, cfg, backend, dataDir, log) | 663 | hash, err := loadPasswordHash(ctx, cfg, backend, dataDir, log) | ||
| 644 | if err != nil { | 664 | if err != nil { | ||
| 645 | return auth.Config{}, err | 665 | return auth.Config{}, err | ||
| @@ -648,13 +668,6 @@ func buildAuthConfig(ctx context.Context, cfg Config, authType domain.AuthType, | |||||
| 648 | return auth.Config{}, errAuthNotConfigured() | 668 | return auth.Config{}, errAuthNotConfigured() | ||
| 649 | } | 669 | } | ||
| 650 | authConfig.PasswordHash = hash | 670 | authConfig.PasswordHash = hash | ||
| 651 | - | case domain.AuthTypeToken: | |||
| 652 | - | secret, expiry, err := loadTokenConfig(ctx, cfg, backend, dataDir, log) | |||
| 653 | - | if err != nil { | |||
| 654 | - | return auth.Config{}, err | |||
| 655 | - | } | |||
| 656 | - | authConfig.TokenSecret = secret | |||
| 657 | - | authConfig.TokenExpiry = expiry | |||
| 658 | } | 671 | } | ||
| 659 | 672 | ||||
| 660 | return authConfig, nil | 673 | return authConfig, nil | ||
| @@ -714,7 +727,7 @@ func loadTokenSecret(ctx context.Context, cfg Config, backend domain.SecretsBack | |||||
| 714 | 727 | ||||
| 715 | // Fall back to config-specified path via secrets backend | 728 | // Fall back to config-specified path via secrets backend | ||
| 716 | if cfg.Auth.TokenSecret == "" { | 729 | if cfg.Auth.TokenSecret == "" { | ||
| 717 | - | return nil, fmt.Errorf("token_secret is required for token authentication; set %s environment variable or configure auth.token_secret", TokenSecretEnvVar) | 730 | + | return nil, fmt.Errorf("token_secret is required for JWT token generation; set %s environment variable or configure auth.token_secret", TokenSecretEnvVar) |
| 718 | } | 731 | } | ||
| 719 | 732 | ||||
| 720 | secret, err := loadSecret(ctx, backend, cfg.Auth.TokenSecret, dataDir, log) | 733 | secret, err := loadSecret(ctx, backend, cfg.Auth.TokenSecret, dataDir, log) | ||
| @@ -902,19 +915,21 @@ func createHTTPHandlers(svc *services, cfg Config, log zerowrap.Logger) (http.Ha | |||||
| 902 | 915 | ||||
| 903 | registryWithMiddleware := middleware.Chain(registryMiddlewares...)(registryHandler) | 916 | registryWithMiddleware := middleware.Chain(registryMiddlewares...)(registryHandler) | ||
| 904 | 917 | ||||
| 905 | - | // Create a mux that routes /v2/token to the token handler (no auth required) | 918 | + | // Create a mux that routes: |
| 906 | - | // and all other /v2/* routes to the registry handler (with auth) | 919 | + | // - /auth/* to auth handler (no auth required, for password login and token exchange) |
| 920 | + | // - /v2/* to registry handler (with auth) | |||
| 921 | + | // - /admin/* to admin handler (with auth) | |||
| 907 | registryMux := http.NewServeMux() | 922 | registryMux := http.NewServeMux() | ||
| 908 | - | if svc.tokenHandler != nil { | 923 | + | if svc.authHandler != nil { |
| 909 | - | // Token endpoint is NOT protected by auth - it's where clients get tokens | 924 | + | // Auth endpoints are NOT protected by auth - they're where clients authenticate |
| 910 | - | // but it still needs rate limiting to prevent brute force attacks | 925 | + | // but still need rate limiting to prevent brute force attacks |
| 911 | - | tokenWithMiddleware := middleware.Chain( | 926 | + | authWithMiddleware := middleware.Chain( |
| 912 | middleware.PanicRecovery(log), | 927 | middleware.PanicRecovery(log), | ||
| 913 | middleware.RequestLogger(log), | 928 | middleware.RequestLogger(log), | ||
| 914 | middleware.SecurityHeaders, | 929 | middleware.SecurityHeaders, | ||
| 915 | rateLimitMiddleware, | 930 | rateLimitMiddleware, | ||
| 916 | - | )(svc.tokenHandler) | 931 | + | )(svc.authHandler) |
| 917 | - | registryMux.Handle("/v2/token", tokenWithMiddleware) | 932 | + | registryMux.Handle("/auth/", authWithMiddleware) |
| 918 | } | 933 | } | ||
| 919 | registryMux.Handle("/v2/", registryWithMiddleware) | 934 | registryMux.Handle("/v2/", registryWithMiddleware) | ||
| 920 | 935 | ||||
| @@ -927,12 +942,15 @@ func createHTTPHandlers(svc *services, cfg Config, log zerowrap.Logger) (http.Ha | |||||
| 927 | } | 942 | } | ||
| 928 | // Add admin auth middleware if auth is enabled | 943 | // Add admin auth middleware if auth is enabled | ||
| 929 | if svc.authSvc != nil { | 944 | if svc.authSvc != nil { | ||
| 930 | - | // Rate limiter for admin API uses per-IP rate from config | 945 | + | // Create rate limiters for admin API - uses same config as registry |
| 931 | - | var adminLimiter *rate.Limiter | 946 | + | var globalLimiter, ipLimiter out.RateLimiter |
| 947 | + | var trustedNets []*net.IPNet | |||
| 932 | if cfg.API.RateLimit.Enabled { | 948 | if cfg.API.RateLimit.Enabled { | ||
| 933 | - | adminLimiter = rate.NewLimiter(rate.Limit(cfg.API.RateLimit.PerIPRPS), cfg.API.RateLimit.Burst) | 949 | + | globalLimiter = ratelimit.NewMemoryStore(cfg.API.RateLimit.GlobalRPS, cfg.API.RateLimit.Burst, log) |
| 950 | + | ipLimiter = ratelimit.NewMemoryStore(cfg.API.RateLimit.PerIPRPS, cfg.API.RateLimit.Burst, log) | |||
| 951 | + | trustedNets = middleware.ParseTrustedProxies(cfg.API.RateLimit.TrustedProxies) | |||
| 934 | } | 952 | } | ||
| 935 | - | adminMiddlewares = append(adminMiddlewares, admin.AuthMiddleware(svc.authSvc, adminLimiter, log)) | 953 | + | adminMiddlewares = append(adminMiddlewares, admin.AuthMiddleware(svc.authSvc, globalLimiter, ipLimiter, trustedNets, log)) |
| 936 | } | 954 | } | ||
| 937 | adminWithMiddleware := middleware.Chain(adminMiddlewares...)(svc.adminHandler) | 955 | adminWithMiddleware := middleware.Chain(adminMiddlewares...)(svc.adminHandler) | ||
| 938 | registryMux.Handle("/admin/", adminWithMiddleware) | 956 | registryMux.Handle("/admin/", adminWithMiddleware) | ||