Skip to content

Commit

Permalink
Merge pull request #2362 from twistlock/populate_htpasswd
Browse files Browse the repository at this point in the history
Create and populate htpasswd file if missing
  • Loading branch information
dmp42 committed Aug 31, 2018
2 parents b12bd40 + c785740 commit 90705d2
Show file tree
Hide file tree
Showing 5 changed files with 96 additions and 5 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: /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 @@ -570,6 +570,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 @@ -615,6 +616,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{}{
"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 @@ -307,7 +307,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

0 comments on commit 90705d2

Please sign in to comment.