Skip to content

feat(registry): enhance authentication checks in htpasswd implementation#4758

Merged
milosgajdos merged 1 commit intodistribution:mainfrom
mnixry:main
Apr 4, 2026
Merged

feat(registry): enhance authentication checks in htpasswd implementation#4758
milosgajdos merged 1 commit intodistribution:mainfrom
mnixry:main

Conversation

@mnixry
Copy link
Copy Markdown
Contributor

@mnixry mnixry commented Jan 1, 2026

Ref: goharbor/harbor-helm#2293

This PR improves error handling for the htpasswd authenticator. Previously, unsupported algorithms or malformed hashes triggered a generic 'authentication failure,' making it hard to debug configuration issues. The logs will now be more specific.

Copy link
Copy Markdown
Member

@milosgajdos milosgajdos left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@milosgajdos
Copy link
Copy Markdown
Member

@mnixry mind rebasing? we've merged something that fixed Azure tests

Copy link
Copy Markdown
Member

@thaJeztah thaJeztah left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks! I left some suggestions; let me also push my branch in which I made (these changes)

if !userExists {
credentials = dummyHash
}
if err := bcrypt.CompareHashAndPassword(credentials, []byte(password)); err != nil {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The actual error returned from this function is discarded, and only used as "boolean" value and for logging;

	if err := localHTPasswd.authenticateUser(username, password); err != nil {
		dcontext.GetLogger(req.Context()).Errorf("error authenticating user %q: %v", username, err)
		return nil, &challenge{
			realm: ac.realm,
			err:   auth.ErrAuthenticationFailure,
		}
	}

Perhaps we should change the encapsulation, and handle logging here to make sure the returned error would never surface to the user (potentially opening similar attack vectors as mentioned above);

We can pass the context to authenticateUser and make the caller use the actual error returned, which can now safely be used;

	if err := localHTPasswd.authenticateUser(req.Context(), username, password); err != nil {
		return nil, &challenge{
			realm: ac.realm,
			err:   err,
		}
	}

BUT change the localHTPasswd.authenticateUser to handle both logging and to return the "safe to use", "safe to return" error.

// dummyBcryptHash is used for non-existing users to prevent timing attacks.
const dummyBcryptHash = "$2a$05$AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"

var dummyHash = []byte(dummyBcryptHash)

// AuthenticateUser checks a given user:password credential against the
// receiving HTPasswd's file. If the check passes, nil is returned.
func (htpasswd *htpasswd) authenticateUser(ctx context.Context, username string, password string) error {
	credentials, ok := htpasswd.entries[username]
	if !ok {
		// timing attack paranoia, always compare the hash even if the user is not found.
		_ = bcrypt.CompareHashAndPassword(dummyHash, []byte(password))
		return auth.ErrAuthenticationFailure
	}

	if err := bcrypt.CompareHashAndPassword(credentials, []byte(password)); err != nil {
		if !errors.Is(err, bcrypt.ErrMismatchedHashAndPassword) {
			dcontext.GetLogger(ctx).WithError(err).Errorf("error authenticating user %q", username)
		}
		// Always return a generic "ErrAuthenticationFailure", not the underlying error.
		return auth.ErrAuthenticationFailure
	}

	return nil
}

Doing so encapsulates the logic to this function, making it clear within scope what we're doing, and does not require special handling on the calling site.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@thaJeztah Thanks for the suggestion! These changes look very neat to me overall.

I do have one small question, though: it seems this would change the behavior for failed authentication attempts. Previously, any authentication request for an existing user, including cases where authentication failed because of an incorrect password would be logged. With the current change, it looks like failed authentication would only be logged when the htpasswd file is misconfigured.

Is that behavior change intentional, and do we consider it acceptable?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good question; it was mostly intentional, because a user sending an invalid password is not an error in the registry; it's just an invalid request (and returned by the registry as an error to the user). At least it should not be logged as an .Error / .Errorf, and even Warn or Info is probably too much, but we could add a Debug log; maybe these 4xx errors are already logged elsewhere as debug logs (not sure)?

@milosgajdos any thoughts / opinions? (Debug, or at most Info seems suitable if we would add back something)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think I'd smash Info, because it could be useful to operators who the end users usually bug about things like this, but I think Debug works, too tbh

@thaJeztah
Copy link
Copy Markdown
Member

@thaJeztah
Copy link
Copy Markdown
Member

Sorry for the duplicated comments; I think GitHub is kinda broken on reviews 😂

@milosgajdos
Copy link
Copy Markdown
Member

@mnixry you still interested in getting this in?

@mnixry
Copy link
Copy Markdown
Contributor Author

mnixry commented Mar 31, 2026

you still interested in getting this in?

@milosgajdos Thanks for the reminder. I do want to get this merged, but I’ve been a bit distracted by other work lately :(

I’ve now cherry-picked @thaJeztah’s suggested changes, which look very neat and make a lot of sense to me, and I also made a few minor fixes for testes that weren’t covered by the suggestion commit.

@milosgajdos
Copy link
Copy Markdown
Member

PTAL @thaJeztah since you requested the changes

- Added a dummy hash for nonexistent users to prevent timing attacks.
- Updated test cases to include a nonexistent user scenario for better coverage.
- Introduced a global dummy hash variable to streamline authentication for nonexistent users.
- Updated the authentication logic to utilize the new dummy hash for improved consistency.
- Added support for overriding the dummy hash in the access controller for testing purposes.
- Updated the authentication logic to utilize the provided dummy hash during user authentication.
- Updated test cases to use `t.TempDir()` for creating temporary htpasswd files, enhancing test isolation and cleanup.
- Simplified file reading and error handling in the `TestCreateHtpasswdFile` function.

Co-authored-by: Sebastiaan van Stijn <github@gone.nl>
Signed-off-by: HexMix <32300164+mnixry@users.noreply.github.com>
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
Comment on lines +55 to +60
logger := dcontext.GetLogger(ctx).WithError(err).WithField("username", username)
if !errors.Is(err, bcrypt.ErrMismatchedHashAndPassword) {
logger.Error("error authenticating user")
} else {
logger.Info("user failed to authenticate")
}
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Did a quick rebase and squash, and added "Info" logs for regular errors (i.e., user mis-typed password 😂)

Copy link
Copy Markdown
Member

@thaJeztah thaJeztah left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

@milosgajdos PTAL

@milosgajdos milosgajdos merged commit 7482bff into distribution:main Apr 4, 2026
21 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants