Merge pull request #2362 from twistlock/populate_htpasswd
Create and populate htpasswd file if missingmaster
						commit
						90705d2fb8
					
				|  | @ -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 | ||||
|  |  | |||
|  | @ -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. | ||||
| 
 | ||||
|  | @ -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. | ||||
|  |  | |||
|  | @ -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)) | ||||
| } | ||||
|  |  | |||
|  | @ -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)) | ||||
| 	} | ||||
| } | ||||
|  |  | |||
|  | @ -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)) | ||||
|  |  | |||
		Loading…
	
		Reference in New Issue