Skip to content

feat: support hashed_password users (mysql_native_password)#29

Open
takaidohigasi wants to merge 2 commits into
mainfrom
feat/hashed-password-users
Open

feat: support hashed_password users (mysql_native_password)#29
takaidohigasi wants to merge 2 commits into
mainfrom
feat/hashed-password-users

Conversation

@takaidohigasi

Copy link
Copy Markdown
Owner

Summary

Adds a hashed_password alternative to password for proxy.users entries, so personal accounts whose plaintext the interceptor never sees can authenticate against the inbound handshake. The plaintext is no longer required for proxy-side auth.

proxy:
  users:
    - username: "app_user"
      password: "app_pw"
    # NEW — alternative to password:
    - username: "personal_alice"
      hashed_password: "*4DF1D66463C18D44E3B001A8FB1BBFBEA13E27FC"

The hashed form is MySQL's standard mysql_native_password stored value (* + 40 hex chars = * + SHA1(SHA1(plaintext)) hex-encoded), i.e. exactly what mysql.user.authentication_string carries for that plugin.

How

  • go.mod adds a replace directive pointing at github.com/takaidohigasi/go-mysql@0d8f39fd, which is the head of go-mysql-org/go-mysql#1129. The fork only touches server/ files (Credential, InMemoryAuthenticationHandler.AddUserWithHashedPassword, HashedPassword value type with shape validation); the client path the interceptor uses for backend connections is unaffected. Drop the replace once upstream merges + tags a release.
  • internal/config/config.go — new UserConfig.HashedPassword field. Validator enforces "exactly one of password / hashed_password" per entry and shape-checks the hashed form at load time so typos surface immediately instead of as login errors hours later.
  • internal/proxy/server.go — register hashed users via the fork's AddUserWithHashedPassword; mysql.DecodePasswordHex decodes the *XXXX... form to the underlying 20 bytes. Hashed users intentionally do not appear in userPasswords, so the existing missing-entry lookup in handleConnection surfaces them with a sharpened error message at backend-connect time.

Scope deliberately limited

Path Status
Inbound handshake (client → proxy) ✅ Supported via this PR.
Outbound backend connect (proxy → backend) ❌ Still requires plaintext. Hashed-only users authenticate successfully but the session terminates at backend-connect with a clear error rather than running queries.

End-to-end query forwarding for hashed-only users would need either (a) go-mysql client-side hash-based auth (so we can reuse the hash for the outbound connect), or (b) a per-user backend_password override field. Both are tracked as follow-ups; this PR unblocks audit-only use cases — personal users no longer break config validation, and their auth attempts now appear in audit logs instead of silently being rejected.

Test plan

  • TestLoad_HashedPassword_Accepts — shape-correct value round-trips through Load.
  • TestLoad_HashedPassword_RejectsBoth — both fields set fails fast.
  • TestLoad_HashedPassword_RejectsMalformed — 4 typo categories (missing leading *, too short, too long, non-hex).
  • TestUsersWiring "hashed-password users skip userPasswords" — registers on the auth handler but stays absent from userPasswords (load-bearing for the clear-error path).
  • gofmt -l . clean
  • go vet ./... clean
  • go build ./...
  • go test -race -count=1 ./... (full suite incl. integration)

Follow-up to remove the replace directive

Once go-mysql-org/go-mysql#1129 merges and a tagged release ships, switch go.mod back to the upstream module + new tag and remove the replace line. The new exported API (HashedPassword, AddUserWithHashedPassword, Credential.HashedPasswords) is the same in upstream so no source changes here are needed.

🤖 Generated with Claude Code

takaidohigasi and others added 2 commits May 12, 2026 11:05
Adds an alternative authentication mode for users whose plaintext
password the proxy never sees — typically personal accounts mirrored
from another proxy's config that stores only the MySQL
`*XXXXXXXX...` (mysql_native_password) hash.

API:

  proxy:
    users:
      - username: "personal_alice"
        hashed_password: "*4DF1D66463C18D44E3B001A8FB1BBFBEA13E27FC"

Wiring:

* go.mod: replace github.com/go-mysql-org/go-mysql with
  github.com/takaidohigasi/go-mysql at the commit hosting PR #1129
  (server: support pre-computed hashed passwords in Credential). The
  fork only touches server/ files (Credential, InMemoryAuthenticationHandler.AddUserWithHashedPassword,
  HashedPassword value type with shape validation); the client path
  the interceptor uses for outbound backend connections is
  unaffected. Drop the replace once upstream merges + tags.

* internal/config: new UserConfig.HashedPassword field. Validator
  requires exactly one of Password / HashedPassword per entry, and
  shape-checks HashedPassword (must be "*" + 40 hex chars) at load
  time so typos fail fast instead of surfacing as login errors hours
  later.

* internal/proxy/server.go: register hashed users via the fork's
  AddUserWithHashedPassword. mysql.DecodePasswordHex strips the "*"
  and decodes the standard MySQL stored form. Hashed users are
  intentionally OMITTED from userPasswords so the existing
  handleConnection lookup surfaces them as "missing entry" at
  backend-connect time, with a sharpened error message pointing
  operators at the limitation.

Scope deliberately limited:

* Inbound handshake only. The proxy's outbound backend connection
  still requires plaintext. A hashed-only user authenticates
  successfully but the session terminates at backend-connect with
  a clear error rather than running queries. Granting hashed-only
  users true query access would need either (a) go-mysql client-
  side hash auth (would let us reuse the hash for the outbound
  connect), or (b) a per-user `backend_password` override field.
  Both are tracked as follow-ups; this PR unblocks audit/auth-only
  use cases (personal users mirrored into config that no longer
  fail config validation, and whose auth attempts now appear in
  audit logs instead of silently being rejected).

Tests:

* TestLoad_HashedPassword_Accepts: shape-correct value round-trips
  through Load.
* TestLoad_HashedPassword_RejectsBoth: ambiguity rejection.
* TestLoad_HashedPassword_RejectsMalformed: 4 typo categories
  (missing leading "*", too short, too long, non-hex).
* TestUsersWiring "hashed-password users skip userPasswords":
  hashed users register on the auth handler but stay absent from
  userPasswords, preserving the load-bearing miss-on-lookup that
  drives the clear-error path.

Verified locally:
  gofmt -l .                            (clean)
  go vet ./...                          (clean)
  go build ./...                        (clean)
  go test -race -count=1 ./...          (ok across all packages
                                         including integration)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant