Skip to content

Commit

Permalink
feat: uses bcrypt hash instead (#3293)
Browse files Browse the repository at this point in the history
  • Loading branch information
amir20 authored Sep 26, 2024
1 parent 3c15ac2 commit de79f03
Show file tree
Hide file tree
Showing 4 changed files with 37 additions and 19 deletions.
22 changes: 10 additions & 12 deletions docs/guide/authentication.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,16 +18,16 @@ The content of the file looks like:
users:
# "admin" here is username
admin:
name: "Admin"
# Just sha-256 which can be computed with "echo -n password | shasum -a 256"
password: "5e884898da28047151d0e56f8dc6292773603d0d6aabbdd62a11ef721d1542d8"
email: me@email.net
name: Admin
# Generate with docker run amir20/dozzle generate --name Admin --email me@email.net --password secret admin
password: $2a$11$9ho4vY2LdJ/WBopFcsAS0uORC0x2vuFHQgT/yBqZyzclhHsoaIkzK
```
> [!TIP]
> This file can be generated with `docker run amir20/dozzle generate` with v6.6.x. See [below](#generating-users-yml) for more details.
Dozzle uses `email` to generate avatars using [Gravatar](https://gravatar.com/). It is optional. The password is hashed using `bcrypt` which can be generated using `docker run amir20/dozzle generate`.

Dozzle uses `email` to generate avatars using [Gravatar](https://gravatar.com/). It is optional. The password is hashed using `sha256` which can be generated with `echo -n 'secret-password' | shasum -a 256` or `echo -n 'secret-password' | sha256sum` on linux.
> [!WARNING]
> In previous versions of Dozzle, SHA-256 was used to hash passwords. Bcrypt is now more secure and is recommended for future use. Dozzle will revert to SHA-256 if it does not find a bcrypt hash. It is advisable to update the password hash to bcrypt using `docker run amir20/dozzle generate`. For more details, see [this issue](https://github.com/amir20/dozzle/security/advisories/GHSA-w7qr-q9fh-fj35).

You will need to mount this file for Dozzle to find it. Here is an example:

Expand All @@ -52,21 +52,19 @@ services:

```yaml [users.yml]
users:
# "admin" here is username
admin:
name: "Admin"
# Just sha-256 which can be computed with "echo -n password | shasum -a 256"
password: "5e884898da28047151d0e56f8dc6292773603d0d6aabbdd62a11ef721d1542d8"
email: me@email.net
name: Admin
password: $2a$11$9ho4vY2LdJ/WBopFcsAS0uORC0x2vuFHQgT/yBqZyzclhHsoaIkzK
```

:::

Dozzle uses [JWT](https://en.wikipedia.org/wiki/JSON_Web_Token) to generate tokens for authentication. This token is saved in a cookie.

## Generating users.yml <Badge type="tip" text="v6.6.x" />
## Generating users.yml

Starting with version `v6.6.x`, Dozzle has a builtin `generate` command to generate `users.yml`. Here is an example:
Dozzle has a builtin `generate` command to generate `users.yml`. Here is an example:

```sh
docker run amir20/dozzle generate admin --password password --email test@email.net --name "John Doe" > users.yml
Expand Down
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ require (
github.com/samber/lo v1.47.0
github.com/wk8/go-ordered-map/v2 v2.1.8
github.com/yuin/goldmark v1.7.4
golang.org/x/crypto v0.27.0
golang.org/x/sync v0.8.0
google.golang.org/grpc v1.67.0
google.golang.org/protobuf v1.34.2
Expand Down Expand Up @@ -71,7 +72,6 @@ require (
go.opentelemetry.io/otel/metric v1.28.0 // indirect
go.opentelemetry.io/otel/sdk v1.22.0 // indirect
go.opentelemetry.io/otel/trace v1.28.0 // indirect
golang.org/x/crypto v0.27.0 // indirect
golang.org/x/text v0.18.0 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20240814211410-ddb44dafa142 // indirect
gotest.tools/v3 v3.0.3 // indirect
Expand Down
2 changes: 0 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,6 @@ github.com/decred/dcrd/dcrec/secp256k1/v4 v4.3.0 h1:rpfIENRNNilwHwZeG5+P150SMrnN
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.3.0/go.mod h1:v57UDF4pDQJcEfFUCRop3lJL149eHGSe9Jvczhzjo/0=
github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk=
github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E=
github.com/docker/docker v27.3.0+incompatible h1:BNb1QY6o4JdKpqwi9IB+HUYcRRrVN4aGFUTvDmWYK1A=
github.com/docker/docker v27.3.0+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
github.com/docker/docker v27.3.1+incompatible h1:KttF0XoteNTicmUtBO0L2tP+J7FGRFTjaEF4k6WdhfI=
github.com/docker/docker v27.3.1+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
github.com/docker/go-connections v0.5.0 h1:USnMq7hx7gwdVZq1L49hLXaFtUdTADjXGp+uj1Br63c=
Expand Down
30 changes: 26 additions & 4 deletions internal/auth/users.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import (

"github.com/go-chi/jwtauth/v5"
"github.com/rs/zerolog/log"
"golang.org/x/crypto/bcrypt"
"gopkg.in/yaml.v3"
)

Expand Down Expand Up @@ -61,7 +62,11 @@ func GenerateUsers(user User, hashPassword bool) *bytes.Buffer {
buffer := &bytes.Buffer{}

if hashPassword {
user.Password = sha256sum(user.Password)
hash, err := bcrypt.GenerateFromPassword([]byte(user.Password), 11)
if err != nil {
log.Fatal().Err(err).Msg("Failed to hash password")
}
user.Password = string(hash)
}

users := UserDatabase{
Expand Down Expand Up @@ -93,8 +98,8 @@ func decodeUsersFromFile(path string) (UserDatabase, error) {
log.Fatal().Msgf("User %s has an empty password", username)
}

if len(user.Password) != 64 {
log.Fatal().Str("password", user.Password).Msgf("User %s has an invalid password hash", username)
if !(len(user.Password) == 64 || len(user.Password) == 60) {
log.Fatal().Str("password", user.Password).Str("user", username).Msg("Invalid password for user")
}

if user.Name == "" {
Expand Down Expand Up @@ -146,9 +151,10 @@ func (u *UserDatabase) FindByPassword(username, password string) *User {
return nil
}

if user.Password != sha256sum(password) {
if !CompareHashAndPassword(user.Password, password) {
return nil
}

return user
}

Expand All @@ -157,6 +163,22 @@ func sha256sum(s string) string {
return hex.EncodeToString(bytes[:])
}

func CompareHashAndPassword(hash, password string) bool {
if len(hash) == 64 {
log.Warn().Msg("Using sha256sum for password comparison. Consider using a more secure hash algorithm to protected against brute-force attacks. See https://github.com/amir20/dozzle/security/advisories/GHSA-w7qr-q9fh-fj35 for more details.")
return hash == sha256sum(password)
}

if len(hash) == 60 {
err := bcrypt.CompareHashAndPassword([]byte(hash), []byte(password))
return err == nil
}

log.Error().Str("hash", hash).Msg("Invalid hash length. Expecting 64 or 60 characters.")

return false
}

func UserFromContext(ctx context.Context) *User {
if user, ok := ctx.Value(remoteUser).(User); ok {
return &user
Expand Down

0 comments on commit de79f03

Please sign in to comment.