Merge pull request #2362 from twistlock/populate_htpasswd

Create and populate htpasswd file if missing
This commit is contained in:
Olivier Gambier 2018-08-31 00:25:37 -07:00 committed by GitHub
commit 90705d2fb8
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
5 changed files with 96 additions and 5 deletions

View file

@ -11,6 +11,10 @@ http:
addr: :5000 addr: :5000
headers: headers:
X-Content-Type-Options: [nosniff] X-Content-Type-Options: [nosniff]
auth:
htpasswd:
realm: basic-realm
path: /etc/registry
health: health:
storagedriver: storagedriver:
enabled: true enabled: true

View file

@ -570,6 +570,7 @@ The `auth` option is **optional**. Possible auth providers include:
- [`silly`](#silly) - [`silly`](#silly)
- [`token`](#token) - [`token`](#token)
- [`htpasswd`](#htpasswd) - [`htpasswd`](#htpasswd)
- [`none`]
You can configure only one authentication provider. 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 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. 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 > **Warning**: Only use the `htpasswd` authentication scheme with TLS
> configured, since basic authentication sends passwords as part of the HTTP > configured, since basic authentication sends passwords as part of the HTTP
> header. > header.

View file

@ -7,9 +7,13 @@ package htpasswd
import ( import (
"context" "context"
"crypto/rand"
"encoding/base64"
"fmt" "fmt"
"golang.org/x/crypto/bcrypt"
"net/http" "net/http"
"os" "os"
"path/filepath"
"sync" "sync"
"time" "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`) return nil, fmt.Errorf(`"realm" must be set for htpasswd access controller`)
} }
path, present := options["path"] pathOpt, present := options["path"]
if _, ok := path.(string); !present || !ok { path, ok := pathOpt.(string)
if !present || !ok {
return nil, fmt.Errorf(`"path" must be set for htpasswd access controller`) return nil, fmt.Errorf(`"path" must be set for htpasswd access controller`)
} }
if err := createHtpasswdFile(path); err != nil {
return &accessController{realm: realm.(string), path: path.(string)}, 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) { 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) 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() { func init() {
auth.Register("htpasswd", auth.InitFunc(newAccessController)) auth.Register("htpasswd", auth.InitFunc(newAccessController))
} }

View file

@ -1,9 +1,11 @@
package htpasswd package htpasswd
import ( import (
"bytes"
"io/ioutil" "io/ioutil"
"net/http" "net/http"
"net/http/httptest" "net/http/httptest"
"os"
"testing" "testing"
"github.com/docker/distribution/context" "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))
}
}

View file

@ -307,7 +307,7 @@ func NewApp(ctx context.Context, config *configuration.Configuration) *App {
authType := config.Auth.Type() authType := config.Auth.Type()
if authType != "" { if authType != "" && !strings.EqualFold(authType, "none") {
accessController, err := auth.GetAccessController(config.Auth.Type(), config.Auth.Parameters()) accessController, err := auth.GetAccessController(config.Auth.Type(), config.Auth.Parameters())
if err != nil { if err != nil {
panic(fmt.Sprintf("unable to configure authorization (%s): %v", authType, err)) panic(fmt.Sprintf("unable to configure authorization (%s): %v", authType, err))