diff --git a/cmd/registry/config-example.yml b/cmd/registry/config-example.yml index 3277f9a2..c760cd56 100644 --- a/cmd/registry/config-example.yml +++ b/cmd/registry/config-example.yml @@ -11,6 +11,10 @@ http: addr: :5000 headers: X-Content-Type-Options: [nosniff] +auth: + htpasswd: + realm: basic-realm + path: /etc/registry health: storagedriver: enabled: true diff --git a/docs/configuration.md b/docs/configuration.md index c7f9023f..9bf89d8c 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -553,6 +553,7 @@ The `auth` option is **optional**. Possible auth providers include: - [`silly`](#silly) - [`token`](#token) - [`htpasswd`](#htpasswd) +- [`none`] You can configure only one authentication provider. @@ -598,6 +599,9 @@ The only supported password format is are ignored. The `htpasswd` file is loaded once, at startup. If the file is invalid, the registry will display an error and will not start. +> **Warning**: If the `htpasswd` file is missing, the file will be created and provisioned with a default user and automatically generated password. +> The password will be printed to stdout. + > **Warning**: Only use the `htpasswd` authentication scheme with TLS > configured, since basic authentication sends passwords as part of the HTTP > header. diff --git a/registry/auth/htpasswd/access.go b/registry/auth/htpasswd/access.go index 8f40913b..034c503a 100644 --- a/registry/auth/htpasswd/access.go +++ b/registry/auth/htpasswd/access.go @@ -7,9 +7,13 @@ package htpasswd import ( "context" + "crypto/rand" + "encoding/base64" "fmt" + "golang.org/x/crypto/bcrypt" "net/http" "os" + "path/filepath" "sync" "time" @@ -33,12 +37,15 @@ func newAccessController(options map[string]interface{}) (auth.AccessController, return nil, fmt.Errorf(`"realm" must be set for htpasswd access controller`) } - path, present := options["path"] - if _, ok := path.(string); !present || !ok { + pathOpt, present := options["path"] + path, ok := pathOpt.(string) + if !present || !ok { return nil, fmt.Errorf(`"path" must be set for htpasswd access controller`) } - - return &accessController{realm: realm.(string), path: path.(string)}, nil + if err := createHtpasswdFile(path); err != nil { + return nil, err + } + return &accessController{realm: realm.(string), path: path}, nil } func (ac *accessController) Authorized(ctx context.Context, accessRecords ...auth.Access) (context.Context, error) { @@ -111,6 +118,42 @@ func (ch challenge) Error() string { return fmt.Sprintf("basic authentication challenge for realm %q: %s", ch.realm, ch.err) } +// createHtpasswdFile creates and populates htpasswd file with a new user in case the file is missing +func createHtpasswdFile(path string) error { + if f, err := os.Open(path); err == nil { + f.Close() + return nil + } else if !os.IsNotExist(err) { + return err + } + + if err := os.MkdirAll(filepath.Dir(path), 0700); err != nil { + return err + } + f, err := os.OpenFile(path, os.O_WRONLY|os.O_CREATE, 0600) + if err != nil { + return fmt.Errorf("failed to open htpasswd path %s", err) + } + defer f.Close() + var secretBytes [32]byte + if _, err := rand.Read(secretBytes[:]); err != nil { + return err + } + pass := base64.RawURLEncoding.EncodeToString(secretBytes[:]) + encryptedPass, err := bcrypt.GenerateFromPassword([]byte(pass), bcrypt.DefaultCost) + if err != nil { + return err + } + if _, err := f.Write([]byte(fmt.Sprintf("docker:%s", string(encryptedPass[:])))); err != nil { + return err + } + dcontext.GetLoggerWithFields(context.Background(), map[interface{}]interface{}{ + "user": "docker", + "password": pass, + }).Warnf("htpasswd is missing, provisioning with default user") + return nil +} + func init() { auth.Register("htpasswd", auth.InitFunc(newAccessController)) } diff --git a/registry/auth/htpasswd/access_test.go b/registry/auth/htpasswd/access_test.go index 553f05cf..7a3d411e 100644 --- a/registry/auth/htpasswd/access_test.go +++ b/registry/auth/htpasswd/access_test.go @@ -1,9 +1,11 @@ package htpasswd import ( + "bytes" "io/ioutil" "net/http" "net/http/httptest" + "os" "testing" "github.com/docker/distribution/context" @@ -120,3 +122,41 @@ func TestBasicAccessController(t *testing.T) { } } + +func TestCreateHtpasswdFile(t *testing.T) { + tempFile, err := ioutil.TempFile("", "htpasswd-test") + if err != nil { + t.Fatalf("could not create temporary htpasswd file %v", err) + } + defer tempFile.Close() + options := map[string]interface{}{ + "realm": "/auth/htpasswd", + "path": tempFile.Name(), + } + // Ensure file is not populated + if _, err := newAccessController(options); err != nil { + t.Fatalf("error creating access controller %v", err) + } + content, err := ioutil.ReadAll(tempFile) + if err != nil { + t.Fatalf("failed to read file %v", err) + } + if !bytes.Equal([]byte{}, content) { + t.Fatalf("htpasswd file should not be populated %v", string(content)) + } + if err := os.Remove(tempFile.Name()); err != nil { + t.Fatalf("failed to remove temp file %v", err) + } + + // Ensure htpasswd file is populated + if _, err := newAccessController(options); err != nil { + t.Fatalf("error creating access controller %v", err) + } + content, err = ioutil.ReadFile(tempFile.Name()) + if err != nil { + t.Fatalf("failed to read file %v", err) + } + if !bytes.HasPrefix(content, []byte("docker:$2a$")) { + t.Fatalf("failed to find default user in file %s", string(content)) + } +} diff --git a/registry/handlers/app.go b/registry/handlers/app.go index e38050cb..cfbb8d5f 100644 --- a/registry/handlers/app.go +++ b/registry/handlers/app.go @@ -300,7 +300,7 @@ func NewApp(ctx context.Context, config *configuration.Configuration) *App { authType := config.Auth.Type() - if authType != "" { + if authType != "" && !strings.EqualFold(authType, "none") { accessController, err := auth.GetAccessController(config.Auth.Type(), config.Auth.Parameters()) if err != nil { panic(fmt.Sprintf("unable to configure authorization (%s): %v", authType, err))