Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Create and populate htpasswd file if missing #2362

Merged
merged 1 commit into from
Aug 31, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions cmd/registry/config-example.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
4 changes: 4 additions & 0 deletions docs/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down Expand Up @@ -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.
Expand Down
51 changes: 47 additions & 4 deletions registry/auth/htpasswd/access.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"

Expand All @@ -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) {
Expand Down Expand Up @@ -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{}{
Copy link
Collaborator

Choose a reason for hiding this comment

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

Also, this message should explain the "escape hatch", as this will confusing to those who have already ran the registry. I'll discuss the "escape hatch" more on distribution/distribution-library-image#58.

Copy link
Author

Choose a reason for hiding this comment

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

@stevvooe but this config only apply if you start the registry with htpasswd enabled.
If you use REGISTRY_AUTH="" (or silly with realm) it is not enabled.

Is it OK to write
Disable default basic authentication by overriding the 'REGISTRY_AUTH' environment variable?

Copy link
Collaborator

Choose a reason for hiding this comment

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

@liron-l If you are using the registry behind a proxy, which is an extremely common deployment, this will break them and those users will panic. What do they do when this happens?

Copy link
Author

Choose a reason for hiding this comment

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

@stevvooe I think the scenario you describe is relevant to the hub image change (which is indeed a breaking change).
However, here we just automatically populate the username/password when they are missing in basic auth scenario (like Jenkins does on default setup).

"user": "docker",
"password": pass,
}).Warnf("htpasswd is missing, provisioning with default user")
return nil
}

func init() {
auth.Register("htpasswd", auth.InitFunc(newAccessController))
}
40 changes: 40 additions & 0 deletions registry/auth/htpasswd/access_test.go
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
package htpasswd

import (
"bytes"
"io/ioutil"
"net/http"
"net/http/httptest"
"os"
"testing"

"github.com/docker/distribution/context"
Expand Down Expand Up @@ -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))
}
}
2 changes: 1 addition & 1 deletion registry/handlers/app.go
Original file line number Diff line number Diff line change
Expand Up @@ -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))
Expand Down