detached 4d70f40

feat(app): integrate authentication services and secrets backend

Wire authentication into application:
- Add secrets backend configuration
- Enhanced registry auth config (password/token types)
- Create and inject auth service
- Register auth CLI commands
- Add bcrypt and JWT dependencies
commit
4d70f404ecb7c97c1a05580f653325d1fa2b6944
author
brice · · 2026-01-12 18:53 UTC
committer
brice · · 2026-01-12 21:32 UTC
parent
136d617
signature
unsigned
4 files · +177 -6
M go.mod +3 -0
@@ -14,8 +14,11 @@ require (
1414
1515 require (
1616 github.com/cespare/xxhash/v2 v2.3.0 // indirect
17+ github.com/golang-jwt/jwt/v5 v5.3.0 // indirect
1718 github.com/rs/zerolog v1.34.0 // indirect
1819 go.yaml.in/yaml/v3 v3.0.4 // indirect
20+ golang.org/x/crypto v0.47.0 // indirect
21+ golang.org/x/term v0.39.0 // indirect
1922 gopkg.in/natefinch/lumberjack.v2 v2.2.1 // indirect
2023 )
2124
@@ -14,8 +14,11 @@ require (
14 14
15 require (15 require (
16 github.com/cespare/xxhash/v2 v2.3.0 // indirect16 github.com/cespare/xxhash/v2 v2.3.0 // indirect
17+ github.com/golang-jwt/jwt/v5 v5.3.0 // indirect
17 github.com/rs/zerolog v1.34.0 // indirect18 github.com/rs/zerolog v1.34.0 // indirect
18 go.yaml.in/yaml/v3 v3.0.4 // indirect19 go.yaml.in/yaml/v3 v3.0.4 // indirect
20+ golang.org/x/crypto v0.47.0 // indirect
21+ golang.org/x/term v0.39.0 // indirect
19 gopkg.in/natefinch/lumberjack.v2 v2.2.1 // indirect22 gopkg.in/natefinch/lumberjack.v2 v2.2.1 // indirect
20 )23 )
21 24
M go.sum +6 -0 generated file: collapsed
M internal/adapters/in/cli/root.go +1 -0
@@ -33,6 +33,7 @@ configuration rules, making it ideal for single-server deployments.`,
3333 rootCmd.AddCommand(newStartCmd())
3434 rootCmd.AddCommand(newReloadCmd())
3535 rootCmd.AddCommand(newVersionCmd())
36+ rootCmd.AddCommand(newAuthCmd())
3637
3738 return rootCmd
3839 }
@@ -33,6 +33,7 @@ configuration rules, making it ideal for single-server deployments.`,
33 rootCmd.AddCommand(newStartCmd())33 rootCmd.AddCommand(newStartCmd())
34 rootCmd.AddCommand(newReloadCmd())34 rootCmd.AddCommand(newReloadCmd())
35 rootCmd.AddCommand(newVersionCmd())35 rootCmd.AddCommand(newVersionCmd())
36+ rootCmd.AddCommand(newAuthCmd())
36 37
37 return rootCmd38 return rootCmd
38 }39 }
M internal/app/run.go +167 -6
@@ -22,12 +22,20 @@ import (
2222 "gordon/internal/adapters/out/filesystem"
2323 "gordon/internal/adapters/out/logwriter"
2424 "gordon/internal/adapters/out/secrets"
25+ "gordon/internal/adapters/out/tokenstore"
2526
2627 // Adapters - Input
2728 "gordon/internal/adapters/in/http/middleware"
2829 "gordon/internal/adapters/in/http/registry"
2930
31+ // Boundaries
32+ "gordon/internal/boundaries/out"
33+
34+ // Domain
35+ "gordon/internal/domain"
36+
3037 // Use cases
38+ "gordon/internal/usecase/auth"
3139 "gordon/internal/usecase/config"
3240 "gordon/internal/usecase/container"
3341 "gordon/internal/usecase/proxy"
@@ -66,10 +74,18 @@ type Config struct {
6674 Dir string `mapstructure:"dir"`
6775 } `mapstructure:"env"`
6876
77+ Secrets struct {
78+ Backend string `mapstructure:"backend"` // "pass", "sops", or "unsafe"
79+ } `mapstructure:"secrets"`
80+
6981 RegistryAuth struct {
70- Enabled bool `mapstructure:"enabled"`
71- Username string `mapstructure:"username"`
72- Password string `mapstructure:"password"`
82+ Enabled bool `mapstructure:"enabled"`
83+ Type string `mapstructure:"type"` // "password" or "token"
84+ Username string `mapstructure:"username"`
85+ Password string `mapstructure:"password"` // deprecated: use password_hash
86+ PasswordHash string `mapstructure:"password_hash"` // path in secrets backend
87+ TokenSecret string `mapstructure:"token_secret"` // path in secrets backend
88+ TokenExpiry string `mapstructure:"token_expiry"` // e.g., "720h"
7389 } `mapstructure:"registry_auth"`
7490 }
7591
@@ -81,10 +97,13 @@ type services struct {
8197 manifestStorage *filesystem.ManifestStorage
8298 envLoader *envloader.FileLoader
8399 logWriter *logwriter.LogWriter
100+ tokenStore out.TokenStore
84101 configSvc *config.Service
85102 containerSvc *container.Service
86103 registrySvc *registrySvc.Service
87104 proxySvc *proxy.Service
105+ authSvc *auth.Service
106+ tokenHandler *registry.TokenHandler
88107 }
89108
90109 // Run initializes and starts the Gordon application.
@@ -208,6 +227,11 @@ func createServices(ctx context.Context, v *viper.Viper, cfg Config, log zerowra
208227 return nil, err
209228 }
210229
230+ // Create auth service (if enabled)
231+ if svc.tokenStore, svc.authSvc, err = createAuthService(ctx, cfg, log); err != nil {
232+ return nil, err
233+ }
234+
211235 // Create use case services
212236 svc.configSvc = config.NewService(v, svc.eventBus)
213237 if err := svc.configSvc.Load(ctx); err != nil {
@@ -221,6 +245,11 @@ func createServices(ctx context.Context, v *viper.Viper, cfg Config, log zerowra
221245 RegistryPort: cfg.Server.RegistryPort,
222246 })
223247
248+ // Create token handler for registry token endpoint
249+ if svc.authSvc != nil {
250+ svc.tokenHandler = registry.NewTokenHandler(svc.authSvc, log)
251+ }
252+
224253 return svc, nil
225254 }
226255
@@ -329,6 +358,122 @@ func createLogWriter(cfg Config, log zerowrap.Logger) (*logwriter.LogWriter, err
329358 return writer, nil
330359 }
331360
361+// createAuthService creates the authentication service and token store.
362+func createAuthService(ctx context.Context, cfg Config, log zerowrap.Logger) (out.TokenStore, *auth.Service, error) {
363+ if !cfg.RegistryAuth.Enabled {
364+ return nil, nil, nil
365+ }
366+
367+ // Determine auth type
368+ authType := domain.AuthTypePassword
369+ if cfg.RegistryAuth.Type == "token" {
370+ authType = domain.AuthTypeToken
371+ }
372+
373+ // Determine secrets backend
374+ backend := domain.SecretsBackendUnsafe // default
375+ switch cfg.Secrets.Backend {
376+ case "pass":
377+ backend = domain.SecretsBackendPass
378+ case "sops":
379+ backend = domain.SecretsBackendSops
380+ case "unsafe", "":
381+ backend = domain.SecretsBackendUnsafe
382+ }
383+
384+ // Get data directory
385+ dataDir := cfg.Server.DataDir
386+ if dataDir == "" {
387+ dataDir = "/var/lib/gordon"
388+ }
389+
390+ // Create token store (needed for token auth or to store revocations)
391+ var store out.TokenStore
392+ var err error
393+ if authType == domain.AuthTypeToken {
394+ store, err = tokenstore.NewStore(backend, dataDir, log)
395+ if err != nil {
396+ return nil, nil, log.WrapErr(err, "failed to create token store")
397+ }
398+ }
399+
400+ // Build auth config
401+ authConfig := auth.Config{
402+ Enabled: cfg.RegistryAuth.Enabled,
403+ AuthType: authType,
404+ Username: cfg.RegistryAuth.Username,
405+ }
406+
407+ // Load secrets based on auth type
408+ if authType == domain.AuthTypePassword {
409+ // For password auth, we need the bcrypt hash
410+ if cfg.RegistryAuth.PasswordHash != "" {
411+ // Load from secrets backend
412+ hash, err := loadSecret(ctx, backend, cfg.RegistryAuth.PasswordHash, dataDir, log)
413+ if err != nil {
414+ return nil, nil, log.WrapErr(err, "failed to load password hash")
415+ }
416+ authConfig.PasswordHash = hash
417+ } else if cfg.RegistryAuth.Password != "" {
418+ // Backward compatibility: use plain password (will be hashed on validation)
419+ // This is less secure but maintains backward compatibility
420+ log.Warn().Msg("using plain password in config is deprecated, use password_hash with a secrets backend")
421+ authConfig.PasswordHash = cfg.RegistryAuth.Password
422+ }
423+ } else {
424+ // For token auth, we need the signing secret
425+ if cfg.RegistryAuth.TokenSecret != "" {
426+ secret, err := loadSecret(ctx, backend, cfg.RegistryAuth.TokenSecret, dataDir, log)
427+ if err != nil {
428+ return nil, nil, log.WrapErr(err, "failed to load token secret")
429+ }
430+ authConfig.TokenSecret = []byte(secret)
431+ } else {
432+ return nil, nil, fmt.Errorf("token_secret is required for token authentication")
433+ }
434+
435+ // Parse token expiry
436+ if cfg.RegistryAuth.TokenExpiry != "" {
437+ expiry, err := time.ParseDuration(cfg.RegistryAuth.TokenExpiry)
438+ if err != nil {
439+ return nil, nil, fmt.Errorf("invalid token_expiry: %w", err)
440+ }
441+ authConfig.TokenExpiry = expiry
442+ }
443+ }
444+
445+ authSvc := auth.NewService(authConfig, store, log)
446+
447+ log.Info().
448+ Str("type", string(authType)).
449+ Str("backend", string(backend)).
450+ Msg("registry authentication enabled")
451+
452+ return store, authSvc, nil
453+}
454+
455+// loadSecret loads a secret from the configured backend.
456+func loadSecret(ctx context.Context, backend domain.SecretsBackend, path, dataDir string, log zerowrap.Logger) (string, error) {
457+ switch backend {
458+ case domain.SecretsBackendPass:
459+ provider := secrets.NewPassProvider(log)
460+ return provider.GetSecret(ctx, path)
461+ case domain.SecretsBackendSops:
462+ provider := secrets.NewSopsProvider(log)
463+ return provider.GetSecret(ctx, path)
464+ case domain.SecretsBackendUnsafe:
465+ // For unsafe backend, path is relative to dataDir/secrets/
466+ secretFile := filepath.Join(dataDir, "secrets", path)
467+ data, err := os.ReadFile(secretFile)
468+ if err != nil {
469+ return "", fmt.Errorf("failed to read secret file: %w", err)
470+ }
471+ return string(data), nil
472+ default:
473+ return "", fmt.Errorf("unknown secrets backend: %s", backend)
474+ }
475+}
476+
332477 // createContainerService creates the container service with configuration.
333478 func createContainerService(v *viper.Viper, cfg Config, svc *services) *container.Service {
334479 containerConfig := container.Config{
@@ -414,13 +559,26 @@ func createHTTPHandlers(svc *services, cfg Config, log zerowrap.Logger) (http.Ha
414559 middleware.RequestLogger(log),
415560 }
416561
417- if cfg.RegistryAuth.Enabled {
562+ if cfg.RegistryAuth.Enabled && svc.authSvc != nil {
418563 registryMiddlewares = append(registryMiddlewares,
419- middleware.RegistryAuth(cfg.RegistryAuth.Username, cfg.RegistryAuth.Password, log))
564+ middleware.RegistryAuthV2(svc.authSvc, log))
420565 }
421566
422567 registryWithMiddleware := middleware.Chain(registryMiddlewares...)(registryHandler)
423568
569+ // Create a mux that routes /v2/token to the token handler (no auth required)
570+ // and all other /v2/* routes to the registry handler (with auth)
571+ registryMux := http.NewServeMux()
572+ if svc.tokenHandler != nil {
573+ // Token endpoint is NOT protected by auth - it's where clients get tokens
574+ tokenWithLogging := middleware.Chain(
575+ middleware.PanicRecovery(log),
576+ middleware.RequestLogger(log),
577+ )(svc.tokenHandler)
578+ registryMux.Handle("/v2/token", tokenWithLogging)
579+ }
580+ registryMux.Handle("/v2/", registryWithMiddleware)
581+
424582 proxyMiddlewares := []func(http.Handler) http.Handler{
425583 middleware.PanicRecovery(log),
426584 middleware.RequestLogger(log),
@@ -429,7 +587,7 @@ func createHTTPHandlers(svc *services, cfg Config, log zerowrap.Logger) (http.Ha
429587
430588 proxyWithMiddleware := middleware.Chain(proxyMiddlewares...)(svc.proxySvc)
431589
432- return registryWithMiddleware, proxyWithMiddleware
590+ return registryMux, proxyWithMiddleware
433591 }
434592
435593 // runServers starts the HTTP servers and waits for shutdown.
@@ -578,7 +736,10 @@ func loadConfig(v *viper.Viper, configPath string) error {
578736 v.SetDefault("logging.container_logs.max_backups", 3)
579737 v.SetDefault("logging.container_logs.max_age", 28)
580738 v.SetDefault("env.dir", "/var/lib/gordon/env")
739+ v.SetDefault("secrets.backend", "unsafe")
581740 v.SetDefault("registry_auth.enabled", false)
741+ v.SetDefault("registry_auth.type", "password")
742+ v.SetDefault("registry_auth.token_expiry", "720h")
582743
583744 if configPath != "" {
584745 v.SetConfigFile(configPath)
@@ -22,12 +22,20 @@ import (
22 "gordon/internal/adapters/out/filesystem"22 "gordon/internal/adapters/out/filesystem"
23 "gordon/internal/adapters/out/logwriter"23 "gordon/internal/adapters/out/logwriter"
24 "gordon/internal/adapters/out/secrets"24 "gordon/internal/adapters/out/secrets"
25+ "gordon/internal/adapters/out/tokenstore"
25 26
26 // Adapters - Input27 // Adapters - Input
27 "gordon/internal/adapters/in/http/middleware"28 "gordon/internal/adapters/in/http/middleware"
28 "gordon/internal/adapters/in/http/registry"29 "gordon/internal/adapters/in/http/registry"
29 30
31+ // Boundaries
32+ "gordon/internal/boundaries/out"
33+
34+ // Domain
35+ "gordon/internal/domain"
36+
30 // Use cases37 // Use cases
38+ "gordon/internal/usecase/auth"
31 "gordon/internal/usecase/config"39 "gordon/internal/usecase/config"
32 "gordon/internal/usecase/container"40 "gordon/internal/usecase/container"
33 "gordon/internal/usecase/proxy"41 "gordon/internal/usecase/proxy"
@@ -66,10 +74,18 @@ type Config struct {
66 Dir string `mapstructure:"dir"`74 Dir string `mapstructure:"dir"`
67 } `mapstructure:"env"`75 } `mapstructure:"env"`
68 76
77+ Secrets struct {
78+ Backend string `mapstructure:"backend"` // "pass", "sops", or "unsafe"
79+ } `mapstructure:"secrets"`
80+
69 RegistryAuth struct {81 RegistryAuth struct {
70- Enabled bool `mapstructure:"enabled"`82+ Enabled bool `mapstructure:"enabled"`
71- Username string `mapstructure:"username"`83+ Type string `mapstructure:"type"` // "password" or "token"
72- Password string `mapstructure:"password"`84+ Username string `mapstructure:"username"`
85+ Password string `mapstructure:"password"` // deprecated: use password_hash
86+ PasswordHash string `mapstructure:"password_hash"` // path in secrets backend
87+ TokenSecret string `mapstructure:"token_secret"` // path in secrets backend
88+ TokenExpiry string `mapstructure:"token_expiry"` // e.g., "720h"
73 } `mapstructure:"registry_auth"`89 } `mapstructure:"registry_auth"`
74 }90 }
75 91
@@ -81,10 +97,13 @@ type services struct {
81 manifestStorage *filesystem.ManifestStorage97 manifestStorage *filesystem.ManifestStorage
82 envLoader *envloader.FileLoader98 envLoader *envloader.FileLoader
83 logWriter *logwriter.LogWriter99 logWriter *logwriter.LogWriter
100+ tokenStore out.TokenStore
84 configSvc *config.Service101 configSvc *config.Service
85 containerSvc *container.Service102 containerSvc *container.Service
86 registrySvc *registrySvc.Service103 registrySvc *registrySvc.Service
87 proxySvc *proxy.Service104 proxySvc *proxy.Service
105+ authSvc *auth.Service
106+ tokenHandler *registry.TokenHandler
88 }107 }
89 108
90 // Run initializes and starts the Gordon application.109 // Run initializes and starts the Gordon application.
@@ -208,6 +227,11 @@ func createServices(ctx context.Context, v *viper.Viper, cfg Config, log zerowra
208 return nil, err227 return nil, err
209 }228 }
210 229
230+ // Create auth service (if enabled)
231+ if svc.tokenStore, svc.authSvc, err = createAuthService(ctx, cfg, log); err != nil {
232+ return nil, err
233+ }
234+
211 // Create use case services235 // Create use case services
212 svc.configSvc = config.NewService(v, svc.eventBus)236 svc.configSvc = config.NewService(v, svc.eventBus)
213 if err := svc.configSvc.Load(ctx); err != nil {237 if err := svc.configSvc.Load(ctx); err != nil {
@@ -221,6 +245,11 @@ func createServices(ctx context.Context, v *viper.Viper, cfg Config, log zerowra
221 RegistryPort: cfg.Server.RegistryPort,245 RegistryPort: cfg.Server.RegistryPort,
222 })246 })
223 247
248+ // Create token handler for registry token endpoint
249+ if svc.authSvc != nil {
250+ svc.tokenHandler = registry.NewTokenHandler(svc.authSvc, log)
251+ }
252+
224 return svc, nil253 return svc, nil
225 }254 }
226 255
@@ -329,6 +358,122 @@ func createLogWriter(cfg Config, log zerowrap.Logger) (*logwriter.LogWriter, err
329 return writer, nil358 return writer, nil
330 }359 }
331 360
361+// createAuthService creates the authentication service and token store.
362+func createAuthService(ctx context.Context, cfg Config, log zerowrap.Logger) (out.TokenStore, *auth.Service, error) {
363+ if !cfg.RegistryAuth.Enabled {
364+ return nil, nil, nil
365+ }
366+
367+ // Determine auth type
368+ authType := domain.AuthTypePassword
369+ if cfg.RegistryAuth.Type == "token" {
370+ authType = domain.AuthTypeToken
371+ }
372+
373+ // Determine secrets backend
374+ backend := domain.SecretsBackendUnsafe // default
375+ switch cfg.Secrets.Backend {
376+ case "pass":
377+ backend = domain.SecretsBackendPass
378+ case "sops":
379+ backend = domain.SecretsBackendSops
380+ case "unsafe", "":
381+ backend = domain.SecretsBackendUnsafe
382+ }
383+
384+ // Get data directory
385+ dataDir := cfg.Server.DataDir
386+ if dataDir == "" {
387+ dataDir = "/var/lib/gordon"
388+ }
389+
390+ // Create token store (needed for token auth or to store revocations)
391+ var store out.TokenStore
392+ var err error
393+ if authType == domain.AuthTypeToken {
394+ store, err = tokenstore.NewStore(backend, dataDir, log)
395+ if err != nil {
396+ return nil, nil, log.WrapErr(err, "failed to create token store")
397+ }
398+ }
399+
400+ // Build auth config
401+ authConfig := auth.Config{
402+ Enabled: cfg.RegistryAuth.Enabled,
403+ AuthType: authType,
404+ Username: cfg.RegistryAuth.Username,
405+ }
406+
407+ // Load secrets based on auth type
408+ if authType == domain.AuthTypePassword {
409+ // For password auth, we need the bcrypt hash
410+ if cfg.RegistryAuth.PasswordHash != "" {
411+ // Load from secrets backend
412+ hash, err := loadSecret(ctx, backend, cfg.RegistryAuth.PasswordHash, dataDir, log)
413+ if err != nil {
414+ return nil, nil, log.WrapErr(err, "failed to load password hash")
415+ }
416+ authConfig.PasswordHash = hash
417+ } else if cfg.RegistryAuth.Password != "" {
418+ // Backward compatibility: use plain password (will be hashed on validation)
419+ // This is less secure but maintains backward compatibility
420+ log.Warn().Msg("using plain password in config is deprecated, use password_hash with a secrets backend")
421+ authConfig.PasswordHash = cfg.RegistryAuth.Password
422+ }
423+ } else {
424+ // For token auth, we need the signing secret
425+ if cfg.RegistryAuth.TokenSecret != "" {
426+ secret, err := loadSecret(ctx, backend, cfg.RegistryAuth.TokenSecret, dataDir, log)
427+ if err != nil {
428+ return nil, nil, log.WrapErr(err, "failed to load token secret")
429+ }
430+ authConfig.TokenSecret = []byte(secret)
431+ } else {
432+ return nil, nil, fmt.Errorf("token_secret is required for token authentication")
433+ }
434+
435+ // Parse token expiry
436+ if cfg.RegistryAuth.TokenExpiry != "" {
437+ expiry, err := time.ParseDuration(cfg.RegistryAuth.TokenExpiry)
438+ if err != nil {
439+ return nil, nil, fmt.Errorf("invalid token_expiry: %w", err)
440+ }
441+ authConfig.TokenExpiry = expiry
442+ }
443+ }
444+
445+ authSvc := auth.NewService(authConfig, store, log)
446+
447+ log.Info().
448+ Str("type", string(authType)).
449+ Str("backend", string(backend)).
450+ Msg("registry authentication enabled")
451+
452+ return store, authSvc, nil
453+}
454+
455+// loadSecret loads a secret from the configured backend.
456+func loadSecret(ctx context.Context, backend domain.SecretsBackend, path, dataDir string, log zerowrap.Logger) (string, error) {
457+ switch backend {
458+ case domain.SecretsBackendPass:
459+ provider := secrets.NewPassProvider(log)
460+ return provider.GetSecret(ctx, path)
461+ case domain.SecretsBackendSops:
462+ provider := secrets.NewSopsProvider(log)
463+ return provider.GetSecret(ctx, path)
464+ case domain.SecretsBackendUnsafe:
465+ // For unsafe backend, path is relative to dataDir/secrets/
466+ secretFile := filepath.Join(dataDir, "secrets", path)
467+ data, err := os.ReadFile(secretFile)
468+ if err != nil {
469+ return "", fmt.Errorf("failed to read secret file: %w", err)
470+ }
471+ return string(data), nil
472+ default:
473+ return "", fmt.Errorf("unknown secrets backend: %s", backend)
474+ }
475+}
476+
332 // createContainerService creates the container service with configuration.477 // createContainerService creates the container service with configuration.
333 func createContainerService(v *viper.Viper, cfg Config, svc *services) *container.Service {478 func createContainerService(v *viper.Viper, cfg Config, svc *services) *container.Service {
334 containerConfig := container.Config{479 containerConfig := container.Config{
@@ -414,13 +559,26 @@ func createHTTPHandlers(svc *services, cfg Config, log zerowrap.Logger) (http.Ha
414 middleware.RequestLogger(log),559 middleware.RequestLogger(log),
415 }560 }
416 561
417- if cfg.RegistryAuth.Enabled {562+ if cfg.RegistryAuth.Enabled && svc.authSvc != nil {
418 registryMiddlewares = append(registryMiddlewares,563 registryMiddlewares = append(registryMiddlewares,
419- middleware.RegistryAuth(cfg.RegistryAuth.Username, cfg.RegistryAuth.Password, log))564+ middleware.RegistryAuthV2(svc.authSvc, log))
420 }565 }
421 566
422 registryWithMiddleware := middleware.Chain(registryMiddlewares...)(registryHandler)567 registryWithMiddleware := middleware.Chain(registryMiddlewares...)(registryHandler)
423 568
569+ // Create a mux that routes /v2/token to the token handler (no auth required)
570+ // and all other /v2/* routes to the registry handler (with auth)
571+ registryMux := http.NewServeMux()
572+ if svc.tokenHandler != nil {
573+ // Token endpoint is NOT protected by auth - it's where clients get tokens
574+ tokenWithLogging := middleware.Chain(
575+ middleware.PanicRecovery(log),
576+ middleware.RequestLogger(log),
577+ )(svc.tokenHandler)
578+ registryMux.Handle("/v2/token", tokenWithLogging)
579+ }
580+ registryMux.Handle("/v2/", registryWithMiddleware)
581+
424 proxyMiddlewares := []func(http.Handler) http.Handler{582 proxyMiddlewares := []func(http.Handler) http.Handler{
425 middleware.PanicRecovery(log),583 middleware.PanicRecovery(log),
426 middleware.RequestLogger(log),584 middleware.RequestLogger(log),
@@ -429,7 +587,7 @@ func createHTTPHandlers(svc *services, cfg Config, log zerowrap.Logger) (http.Ha
429 587
430 proxyWithMiddleware := middleware.Chain(proxyMiddlewares...)(svc.proxySvc)588 proxyWithMiddleware := middleware.Chain(proxyMiddlewares...)(svc.proxySvc)
431 589
432- return registryWithMiddleware, proxyWithMiddleware590+ return registryMux, proxyWithMiddleware
433 }591 }
434 592
435 // runServers starts the HTTP servers and waits for shutdown.593 // runServers starts the HTTP servers and waits for shutdown.
@@ -578,7 +736,10 @@ func loadConfig(v *viper.Viper, configPath string) error {
578 v.SetDefault("logging.container_logs.max_backups", 3)736 v.SetDefault("logging.container_logs.max_backups", 3)
579 v.SetDefault("logging.container_logs.max_age", 28)737 v.SetDefault("logging.container_logs.max_age", 28)
580 v.SetDefault("env.dir", "/var/lib/gordon/env")738 v.SetDefault("env.dir", "/var/lib/gordon/env")
739+ v.SetDefault("secrets.backend", "unsafe")
581 v.SetDefault("registry_auth.enabled", false)740 v.SetDefault("registry_auth.enabled", false)
741+ v.SetDefault("registry_auth.type", "password")
742+ v.SetDefault("registry_auth.token_expiry", "720h")
582 743
583 if configPath != "" {744 if configPath != "" {
584 v.SetConfigFile(configPath)745 v.SetConfigFile(configPath)

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 8

Tags 106

↑↓ moveenter openesc close