Merge pull request #1829 from nwt/foreign-layer-host-whitelist

Add a foreign layer URL host whitelist
This commit is contained in:
Richard Scothern 2016-07-21 16:02:20 -07:00 committed by GitHub
commit 2b72dd3927
7 changed files with 137 additions and 5 deletions

View file

@ -172,6 +172,24 @@ type Configuration struct {
TrustKey string `yaml:"signingkeyfile,omitempty"`
} `yaml:"schema1,omitempty"`
} `yaml:"compatibility,omitempty"`
// Validation configures validation options for the registry.
Validation struct {
// Enabled enables the other options in this section.
Enabled bool `yaml:"enabled,omitempty"`
// Manifests configures manifest validation.
Manifests struct {
// URLs configures validation for URLs in pushed manifests.
URLs struct {
// Allow specifies regular expressions (https://godoc.org/regexp/syntax)
// that URLs in pushed manifests must match.
Allow []string `yaml:"allow,omitempty"`
// Deny specifies regular expressions (https://godoc.org/regexp/syntax)
// that URLs in pushed manifests must not match.
Deny []string `yaml:"deny,omitempty"`
} `yaml:"urls,omitempty"`
} `yaml:"manifests,omitempty"`
} `yaml:"validation,omitempty"`
}
// LogHook is composed of hook Level and Type.

View file

@ -246,6 +246,14 @@ information about each option that appears later in this page.
compatibility:
schema1:
signingkeyfile: /etc/registry/key.json
validation:
enabled: true
manifests:
urls:
allow:
- ^https?://([^/]+\.)*example\.com/
deny:
- ^https?://www\.example\.com/
In some instances a configuration option is **optional** but it contains child
options marked as **required**. This indicates that you can omit the parent with
@ -1771,7 +1779,7 @@ To enable pulling private repositories (e.g. `batman/robin`) a username and pass
signingkeyfile: /etc/registry/key.json
Configure handling of older and deprecated features. Each subsection
defines a such a feature with configurable behavior.
defines such a feature with configurable behavior.
### Schema1
@ -1796,6 +1804,39 @@ defines a such a feature with configurable behavior.
</tr>
</table>
## Validation
validation:
enabled: true
manifests:
urls:
allow:
- ^https?://([^/]+\.)*example\.com/
deny:
- ^https?://www\.example\.com/
### Enabled
Use the `enabled` flag to enable the other options in the `validation`
section. They are disabled by default.
### Manifests
Use the `manifest` subsection to configure manifest validation.
#### URLs
The `allow` and `deny` options are both lists of
[regular expressions](https://godoc.org/regexp/syntax) that restrict the URLs in
pushed manifests.
If `allow` is unset, pushing a manifest containing URLs will fail.
If `allow` is set, pushing a manifest will succeed only if all URLs within match
one of the `allow` regular expressions and one of the following holds:
1. `deny` is unset.
2. `deny` is set but no URLs within the manifest match any of the `deny` regular expressions.
## Example: Development configuration
The following is a simple example you can use for local development:

View file

@ -9,7 +9,9 @@ import (
"net/http"
"net/url"
"os"
"regexp"
"runtime"
"strings"
"time"
log "github.com/Sirupsen/logrus"
@ -211,6 +213,39 @@ func NewApp(ctx context.Context, config *configuration.Configuration) *App {
options = append(options, storage.EnableRedirect)
}
// configure validation
if config.Validation.Enabled {
if len(config.Validation.Manifests.URLs.Allow) == 0 && len(config.Validation.Manifests.URLs.Deny) == 0 {
// If Allow and Deny are empty, allow nothing.
options = append(options, storage.ManifestURLsAllowRegexp(regexp.MustCompile("^$")))
} else {
if len(config.Validation.Manifests.URLs.Allow) > 0 {
for i, s := range config.Validation.Manifests.URLs.Allow {
// Validate via compilation.
if _, err := regexp.Compile(s); err != nil {
panic(fmt.Sprintf("validation.manifests.urls.allow: %s", err))
}
// Wrap with non-capturing group.
config.Validation.Manifests.URLs.Allow[i] = fmt.Sprintf("(?:%s)", s)
}
re := regexp.MustCompile(strings.Join(config.Validation.Manifests.URLs.Allow, "|"))
options = append(options, storage.ManifestURLsAllowRegexp(re))
}
if len(config.Validation.Manifests.URLs.Deny) > 0 {
for i, s := range config.Validation.Manifests.URLs.Deny {
// Validate via compilation.
if _, err := regexp.Compile(s); err != nil {
panic(fmt.Sprintf("validation.manifests.urls.deny: %s", err))
}
// Wrap with non-capturing group.
config.Validation.Manifests.URLs.Deny[i] = fmt.Sprintf("(?:%s)", s)
}
re := regexp.MustCompile(strings.Join(config.Validation.Manifests.URLs.Deny, "|"))
options = append(options, storage.ManifestURLsDenyRegexp(re))
}
}
}
// configure storage caches
if cc, ok := config.Storage["cache"]; ok {
v, ok := cc["blobdescriptor"]

View file

@ -21,13 +21,14 @@ type image struct {
layers map[digest.Digest]io.ReadSeeker
}
func createRegistry(t *testing.T, driver driver.StorageDriver) distribution.Namespace {
func createRegistry(t *testing.T, driver driver.StorageDriver, options ...RegistryOption) distribution.Namespace {
ctx := context.Background()
k, err := libtrust.GenerateECP256PrivateKey()
if err != nil {
t.Fatal(err)
}
registry, err := NewRegistry(ctx, driver, EnableDelete, Schema1SigningKey(k))
options = append([]RegistryOption{EnableDelete, Schema1SigningKey(k)}, options...)
registry, err := NewRegistry(ctx, driver, options...)
if err != nil {
t.Fatalf("Failed to construct namespace")
}

View file

@ -1,6 +1,8 @@
package storage
import (
"regexp"
"github.com/docker/distribution"
"github.com/docker/distribution/context"
"github.com/docker/distribution/reference"
@ -20,6 +22,10 @@ type registry struct {
resumableDigestEnabled bool
schema1SigningKey libtrust.PrivateKey
blobDescriptorServiceFactory distribution.BlobDescriptorServiceFactory
manifestURLs struct {
allow *regexp.Regexp
deny *regexp.Regexp
}
}
// RegistryOption is the type used for functional options for NewRegistry.
@ -46,6 +52,22 @@ func DisableDigestResumption(registry *registry) error {
return nil
}
// ManifestURLsAllowRegexp is a functional option for NewRegistry.
func ManifestURLsAllowRegexp(r *regexp.Regexp) RegistryOption {
return func(registry *registry) error {
registry.manifestURLs.allow = r
return nil
}
}
// ManifestURLsDenyRegexp is a functional option for NewRegistry.
func ManifestURLsDenyRegexp(r *regexp.Regexp) RegistryOption {
return func(registry *registry) error {
registry.manifestURLs.deny = r
return nil
}
}
// Schema1SigningKey returns a functional option for NewRegistry. It sets the
// key for signing all schema1 manifests.
func Schema1SigningKey(key libtrust.PrivateKey) RegistryOption {

View file

@ -97,10 +97,12 @@ func (ms *schema2ManifestHandler) verifyManifest(ctx context.Context, mnfst sche
if len(fsLayer.URLs) == 0 {
err = errMissingURL
}
allow := ms.repository.manifestURLs.allow
deny := ms.repository.manifestURLs.deny
for _, u := range fsLayer.URLs {
var pu *url.URL
pu, err = url.Parse(u)
if err != nil || (pu.Scheme != "http" && pu.Scheme != "https") || pu.Fragment != "" {
if err != nil || (pu.Scheme != "http" && pu.Scheme != "https") || pu.Fragment != "" || (allow != nil && !allow.MatchString(u)) || (deny != nil && deny.MatchString(u)) {
err = errInvalidURL
break
}

View file

@ -1,6 +1,7 @@
package storage
import (
"regexp"
"testing"
"github.com/docker/distribution"
@ -13,7 +14,9 @@ import (
func TestVerifyManifestForeignLayer(t *testing.T) {
ctx := context.Background()
inmemoryDriver := inmemory.New()
registry := createRegistry(t, inmemoryDriver)
registry := createRegistry(t, inmemoryDriver,
ManifestURLsAllowRegexp(regexp.MustCompile("^https?://foo")),
ManifestURLsDenyRegexp(regexp.MustCompile("^https?://foo/nope")))
repo := makeRepository(t, registry, "test")
manifestService := makeManifestService(t, repo)
@ -83,6 +86,16 @@ func TestVerifyManifestForeignLayer(t *testing.T) {
[]string{"", "https://foo/bar"},
errInvalidURL,
},
{
foreignLayer,
[]string{"http://nope/bar"},
errInvalidURL,
},
{
foreignLayer,
[]string{"http://foo/nope"},
errInvalidURL,
},
{
foreignLayer,
[]string{"http://foo/bar"},