feat!: require explicit auth credentials when auth.enabled=true
BREAKING: Gordon now requires authentication credentials to be configured when auth is enabled (which is the default). Server will fail to start with a helpful error message explaining setup options. This prevents accidentally running an unauthenticated registry in production.
- commit
- 2b91abcad3da13ea0b201de1a1543258e125d40a
- author
- brice · · 2026-01-17 15:57 UTC
- committer
- brice · · 2026-01-17 16:01 UTC
- parent
- 30433bf
- signature
- unsigned
3 files · +39 -6
M docs/config/auth.md +27 -2
| @@ -2,6 +2,33 @@ | |||
| 2 | 2 | ||
| 3 | 3 | Configure authentication for the Gordon registry and Admin API. | |
| 4 | 4 | ||
| 5 | + | ## Quick Start | |
| 6 | + | ||
| 7 | + | Authentication is **enabled by default** for security. Gordon will not start without authentication configured. | |
| 8 | + | ||
| 9 | + | **Option 1: Password Authentication (simplest)** | |
| 10 | + | ```bash | |
| 11 | + | # 1. Generate password hash | |
| 12 | + | gordon auth password hash | |
| 13 | + | # Enter your password, copy the bcrypt hash | |
| 14 | + | ||
| 15 | + | # 2. Save hash to secrets file | |
| 16 | + | mkdir -p ~/.gordon/secrets | |
| 17 | + | echo '$2a$10$...' > ~/.gordon/secrets/password_hash | |
| 18 | + | ||
| 19 | + | # 3. Add to config.toml | |
| 20 | + | cat >> ~/.gordon/config.toml << 'EOF' | |
| 21 | + | [auth] | |
| 22 | + | password_hash = "password_hash" | |
| 23 | + | EOF | |
| 24 | + | ``` | |
| 25 | + | ||
| 26 | + | **Option 2: Disable Authentication (development only)** | |
| 27 | + | ```toml | |
| 28 | + | [auth] | |
| 29 | + | enabled = false | |
| 30 | + | ``` | |
| 31 | + | ||
| 5 | 32 | ## Configuration | |
| 6 | 33 | ||
| 7 | 34 | ```toml | |
| @@ -13,8 +40,6 @@ token_secret = "gordon/auth/token_secret" | |||
| 13 | 40 | token_expiry = "30d" | |
| 14 | 41 | ``` | |
| 15 | 42 | ||
| 16 | - | > **Note:** Authentication is enabled by default for security. For local development without auth, explicitly set `enabled = false`. | |
| 17 | - | ||
| 18 | 43 | ## Options | |
| 19 | 44 | ||
| 20 | 45 | | Option | Type | Default | Description | | |
| @@ -2,6 +2,33 @@ | |||||
| 2 | 2 | ||||
| 3 | Configure authentication for the Gordon registry and Admin API. | 3 | Configure authentication for the Gordon registry and Admin API. | ||
| 4 | 4 | ||||
| 5 | + | ## Quick Start | |||
| 6 | + | ||||
| 7 | + | Authentication is **enabled by default** for security. Gordon will not start without authentication configured. | |||
| 8 | + | ||||
| 9 | + | **Option 1: Password Authentication (simplest)** | |||
| 10 | + | ```bash | |||
| 11 | + | # 1. Generate password hash | |||
| 12 | + | gordon auth password hash | |||
| 13 | + | # Enter your password, copy the bcrypt hash | |||
| 14 | + | ||||
| 15 | + | # 2. Save hash to secrets file | |||
| 16 | + | mkdir -p ~/.gordon/secrets | |||
| 17 | + | echo '$2a$10$...' > ~/.gordon/secrets/password_hash | |||
| 18 | + | ||||
| 19 | + | # 3. Add to config.toml | |||
| 20 | + | cat >> ~/.gordon/config.toml << 'EOF' | |||
| 21 | + | [auth] | |||
| 22 | + | password_hash = "password_hash" | |||
| 23 | + | EOF | |||
| 24 | + | ``` | |||
| 25 | + | ||||
| 26 | + | **Option 2: Disable Authentication (development only)** | |||
| 27 | + | ```toml | |||
| 28 | + | [auth] | |||
| 29 | + | enabled = false | |||
| 30 | + | ``` | |||
| 31 | + | ||||
| 5 | ## Configuration | 32 | ## Configuration | ||
| 6 | 33 | ||||
| 7 | ```toml | 34 | ```toml | ||
| @@ -13,8 +40,6 @@ token_secret = "gordon/auth/token_secret" | |||||
| 13 | token_expiry = "30d" | 40 | token_expiry = "30d" | ||
| 14 | ``` | 41 | ``` | ||
| 15 | 42 | ||||
| 16 | - | > **Note:** Authentication is enabled by default for security. For local development without auth, explicitly set `enabled = false`. | |||
| 17 | - | ||||
| 18 | ## Options | 43 | ## Options | ||
| 19 | 44 | ||||
| 20 | | Option | Type | Default | Description | | 45 | | Option | Type | Default | Description | | ||
M docs/config/reference.md +4 -4
| @@ -15,15 +15,15 @@ gordon_domain = "" # Required: Gordon domain (registry | |||
| 15 | 15 | data_dir = "~/.gordon" # Data directory (varies by install type) | |
| 16 | 16 | ||
| 17 | 17 | # ============================================================================= | |
| 18 | - | # AUTHENTICATION | |
| 18 | + | # AUTHENTICATION (required - Gordon won't start without credentials configured) | |
| 19 | 19 | # ============================================================================= | |
| 20 | 20 | [auth] | |
| 21 | - | enabled = true # Enable registry authentication | |
| 21 | + | enabled = true # Enable registry authentication (default: true) | |
| 22 | 22 | type = "password" # "password" or "token" | |
| 23 | 23 | secrets_backend = "unsafe" # "pass", "sops", or "unsafe" | |
| 24 | 24 | username = "" # Username for password auth | |
| 25 | - | password_hash = "" # Path in secrets backend to bcrypt hash | |
| 26 | - | token_secret = "" # Path in secrets backend to JWT signing key | |
| 25 | + | password_hash = "" # Path in secrets backend to bcrypt hash (REQUIRED for password type) | |
| 26 | + | token_secret = "" # Path in secrets backend to JWT signing key (REQUIRED for token type) | |
| 27 | 27 | token_expiry = "720h" # Token expiry duration (720h = 30 days) | |
| 28 | 28 | ||
| 29 | 29 | # ============================================================================= | |
| @@ -15,15 +15,15 @@ gordon_domain = "" # Required: Gordon domain (registry | |||||
| 15 | data_dir = "~/.gordon" # Data directory (varies by install type) | 15 | data_dir = "~/.gordon" # Data directory (varies by install type) | ||
| 16 | 16 | ||||
| 17 | # ============================================================================= | 17 | # ============================================================================= | ||
| 18 | - | # AUTHENTICATION | 18 | + | # AUTHENTICATION (required - Gordon won't start without credentials configured) |
| 19 | # ============================================================================= | 19 | # ============================================================================= | ||
| 20 | [auth] | 20 | [auth] | ||
| 21 | - | enabled = true # Enable registry authentication | 21 | + | enabled = true # Enable registry authentication (default: true) |
| 22 | type = "password" # "password" or "token" | 22 | type = "password" # "password" or "token" | ||
| 23 | secrets_backend = "unsafe" # "pass", "sops", or "unsafe" | 23 | secrets_backend = "unsafe" # "pass", "sops", or "unsafe" | ||
| 24 | username = "" # Username for password auth | 24 | username = "" # Username for password auth | ||
| 25 | - | password_hash = "" # Path in secrets backend to bcrypt hash | 25 | + | password_hash = "" # Path in secrets backend to bcrypt hash (REQUIRED for password type) |
| 26 | - | token_secret = "" # Path in secrets backend to JWT signing key | 26 | + | token_secret = "" # Path in secrets backend to JWT signing key (REQUIRED for token type) |
| 27 | token_expiry = "720h" # Token expiry duration (720h = 30 days) | 27 | token_expiry = "720h" # Token expiry duration (720h = 30 days) | ||
| 28 | 28 | ||||
| 29 | # ============================================================================= | 29 | # ============================================================================= | ||
M internal/app/run.go +8 -0
| @@ -594,6 +594,9 @@ func buildAuthConfig(ctx context.Context, cfg Config, authType domain.AuthType, | |||
| 594 | 594 | if err != nil { | |
| 595 | 595 | return auth.Config{}, err | |
| 596 | 596 | } | |
| 597 | + | if hash == "" { | |
| 598 | + | return auth.Config{}, errAuthNotConfigured() | |
| 599 | + | } | |
| 597 | 600 | authConfig.PasswordHash = hash | |
| 598 | 601 | case domain.AuthTypeToken: | |
| 599 | 602 | secret, expiry, err := loadTokenConfig(ctx, cfg, backend, dataDir, log) | |
| @@ -607,6 +610,11 @@ func buildAuthConfig(ctx context.Context, cfg Config, authType domain.AuthType, | |||
| 607 | 610 | return authConfig, nil | |
| 608 | 611 | } | |
| 609 | 612 | ||
| 613 | + | // errAuthNotConfigured returns an error when auth is enabled but credentials are not configured. | |
| 614 | + | func errAuthNotConfigured() error { | |
| 615 | + | return fmt.Errorf("auth is enabled by default, configure auth type and secrets backend. See: https://gordon.bnema.dev/docs/config/auth") | |
| 616 | + | } | |
| 617 | + | ||
| 610 | 618 | func loadPasswordHash(ctx context.Context, cfg Config, backend domain.SecretsBackend, dataDir string, log zerowrap.Logger) (string, error) { | |
| 611 | 619 | if cfg.Auth.PasswordHash != "" { | |
| 612 | 620 | hash, err := loadSecret(ctx, backend, cfg.Auth.PasswordHash, dataDir, log) | |
| @@ -594,6 +594,9 @@ func buildAuthConfig(ctx context.Context, cfg Config, authType domain.AuthType, | |||||
| 594 | if err != nil { | 594 | if err != nil { | ||
| 595 | return auth.Config{}, err | 595 | return auth.Config{}, err | ||
| 596 | } | 596 | } | ||
| 597 | + | if hash == "" { | |||
| 598 | + | return auth.Config{}, errAuthNotConfigured() | |||
| 599 | + | } | |||
| 597 | authConfig.PasswordHash = hash | 600 | authConfig.PasswordHash = hash | ||
| 598 | case domain.AuthTypeToken: | 601 | case domain.AuthTypeToken: | ||
| 599 | secret, expiry, err := loadTokenConfig(ctx, cfg, backend, dataDir, log) | 602 | secret, expiry, err := loadTokenConfig(ctx, cfg, backend, dataDir, log) | ||
| @@ -607,6 +610,11 @@ func buildAuthConfig(ctx context.Context, cfg Config, authType domain.AuthType, | |||||
| 607 | return authConfig, nil | 610 | return authConfig, nil | ||
| 608 | } | 611 | } | ||
| 609 | 612 | ||||
| 613 | + | // errAuthNotConfigured returns an error when auth is enabled but credentials are not configured. | |||
| 614 | + | func errAuthNotConfigured() error { | |||
| 615 | + | return fmt.Errorf("auth is enabled by default, configure auth type and secrets backend. See: https://gordon.bnema.dev/docs/config/auth") | |||
| 616 | + | } | |||
| 617 | + | ||||
| 610 | func loadPasswordHash(ctx context.Context, cfg Config, backend domain.SecretsBackend, dataDir string, log zerowrap.Logger) (string, error) { | 618 | func loadPasswordHash(ctx context.Context, cfg Config, backend domain.SecretsBackend, dataDir string, log zerowrap.Logger) (string, error) { | ||
| 611 | if cfg.Auth.PasswordHash != "" { | 619 | if cfg.Auth.PasswordHash != "" { | ||
| 612 | hash, err := loadSecret(ctx, backend, cfg.Auth.PasswordHash, dataDir, log) | 620 | hash, err := loadSecret(ctx, backend, cfg.Auth.PasswordHash, dataDir, log) | ||