Skip to content

bug: local registry authentication is confusing and broken when Gordon is deployed #88

@bnema

Description

@bnema

Summary

Gordon's internal registry has multiple authentication layers that interact in non-obvious ways. The core confusion when Gordon is deployed on a remote server: the gordon-internal credential is loopback-only by design and cannot be used from outside the host — but this is not obvious from the CLI output or error messages. Several other issues (dead code, non-refreshed service token) compound the complexity.

Confirmed Issues

1. gordon-internal is loopback-only and the error is silent

File: internal/adapters/in/http/middleware/auth.go:128-138

if isLocalhostRequest(r) && isInternalRegistryAuth(r, internalAuth) {
    next.ServeHTTP(w, r)  // skip all JWT validation
    return
}

This credential pair works only when the request originates from 127.0.0.1. A client on a remote machine using gordon-internal + the random password gets a generic 401 with no explanation. The error message does not tell the user these credentials are loopback-only.

gordon auth internal explicitly mentions localhost in its output and documentation (internal/adapters/in/cli/auth.go:392, docs/cli/auth.md:222), so the command itself is not misleading. The problem is the 401 response body gives no useful hint to a confused operator.

2. Remote registry access works, but the path is not obvious

Confirmed working path for remote access (documented in docs/config/auth.md:91):

  1. POST /auth/password with the configured username + password → receives a 24h JWT
  2. Use that JWT as the password for docker login registry.example.com

This works via the token-as-password pattern (internal/adapters/in/http/middleware/auth.go:269). The issue is discoverability: there is no CLI command that wraps this flow (e.g., gordon auth docker-login), and a confused operator will reach for gordon auth internal first.

3. Service token is never refreshed (confirmed)

File: internal/app/run.go:1159

// Note: Service tokens are not auto-refreshed. If the token expires during
// container runtime, the container will need to be recreated to get a new token.

The gordon-service JWT token (used for non-loopback image pulls by the container service) has a 30-day TTL (serviceTokenDefaultTTL = 30 * 24 * time.Hour). After 30 days, internal image pulls will silently fail unless Gordon is restarted.

4. RegistryAuth old middleware is dead code in production (confirmed)

File: internal/adapters/in/http/middleware/auth.go:22

An old basic-auth-only middleware RegistryAuth exists in the same file as the current RegistryAuthV2. The old one is not wired in production (run.go:1479 uses RegistryAuthV2 only) but appears in tests. It should be removed or clearly marked as test-only to avoid confusion when reading the code.

5. GetRegistryAuthConfig() reads a deprecated field (confirmed, test-only)

File: internal/usecase/config/service.go:537-541

Reads auth.password (a deprecated plain-text config field) rather than the active bcrypt hash. Currently only called in tests — not a production bug, but a misleading API that should be removed.

6. isLocalhostRequest is duplicated (confirmed)

Identical helper defined in both:

  • internal/adapters/in/http/middleware/auth.go:299
  • internal/adapters/in/http/auth/handler.go:354

Authentication Reference

For clarity, the credential types and where each one works:

Credential Where it works Used by
gordon-internal + random hex Loopback (127.0.0.1) only Docker daemon pulling from localhost
gordon-service JWT (pull scope) Non-loopback, service-to-service Container service pulling images
Configured username + bcrypt password Anywhere, via POST /auth/password Human/CLI login, issues 24h JWT
Long-lived user tokens (JWT) Anywhere CI/CD, docker login, remote CLI
Ephemeral access tokens (≤5 min JWT) Anywhere Docker Registry v2 token protocol

Proposed Improvements

  • Add a gordon registry login (or similar) command that wraps the POST /auth/passworddocker login flow for operators.
  • Improve the 401 response body to indicate when loopback-only credentials are detected from a non-loopback source.
  • Implement service token auto-refresh (see also feat: extend token expiration on each client-server interaction (+24h with 1h debounce) #84).
  • Remove or clearly mark RegistryAuth as test-only.
  • Consolidate isLocalhostRequest to a single shared location.

Affected Files

  • internal/adapters/in/http/middleware/auth.go — dual middleware, duplicated helper, loopback bypass
  • internal/adapters/in/http/auth/handler.go — duplicated helper, internal bypass in token endpoint
  • internal/app/run.go:725-737, 1151-1173, 1479 — credential generation and wiring
  • internal/usecase/config/service.go:537 — deprecated GetRegistryAuthConfig()
  • internal/usecase/container/service.go:1356pullImage() credential branching logic

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions