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 Nov 1, 2017
1 parent 7484e51 commit c785740
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
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
Expand Up @@ -553,6 +553,7 @@ The `auth` option is **optional**. Possible auth providers include:
- [`silly`](#silly)
- [`token`](#token)

This comment has been minimized.

Copy link
@Qdigital

Qdigital Jun 9, 2020

Qdigital

- [`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
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
@@ -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
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

0 comments on commit c785740

Please sign in to comment.