Skip to content

Commit

Permalink
Create and populate htpasswd file if missing
Browse files Browse the repository at this point in the history
If htpasswd authentication option is configured but the htpasswd file is
missing, populate it with a default user and automatically generated
password.
The password will be printed to stdout.

Signed-off-by: Liron Levin <liron@twistlock.com>
  • Loading branch information
Liron Levin committed Aug 13, 2017
1 parent 06fa77a commit adae5b3
Show file tree
Hide file tree
Showing 4 changed files with 87 additions and 4 deletions.
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: /auth/htpasswd
health:
storagedriver:
enabled: true
Expand Down
3 changes: 3 additions & 0 deletions docs/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -598,6 +598,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
44 changes: 40 additions & 4 deletions registry/auth/htpasswd/access.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,19 @@ package htpasswd

import (
"context"
"crypto/rand"
"encoding/base64"
"fmt"
"golang.org/x/crypto/bcrypt"
"net/http"
"os"
"path/filepath"
"sync"
"time"

dcontext "github.com/docker/distribution/context"
"github.com/docker/distribution/registry/auth"
"github.com/sirupsen/logrus"
)

type accessController struct {
Expand All @@ -33,12 +38,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 +119,34 @@ 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 _, err := os.Open(path); os.IsNotExist(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.URLEncoding.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
}
logrus.Warnf("htpasswd is missing. provisioned with default user:docker password: %s", pass)
}
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))
}
}

0 comments on commit adae5b3

Please sign in to comment.