diff --git a/configuration/configuration.go b/configuration/configuration.go index 7277c0360..68b02a410 100644 --- a/configuration/configuration.go +++ b/configuration/configuration.go @@ -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. diff --git a/docs/configuration.md b/docs/configuration.md index 1ef680f56..cd7703acc 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -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. +## 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: diff --git a/registry/handlers/app.go b/registry/handlers/app.go index 33f496701..cdd88bf12 100644 --- a/registry/handlers/app.go +++ b/registry/handlers/app.go @@ -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"] diff --git a/registry/storage/garbagecollect_test.go b/registry/storage/garbagecollect_test.go index 86fc175a5..88492d812 100644 --- a/registry/storage/garbagecollect_test.go +++ b/registry/storage/garbagecollect_test.go @@ -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") } diff --git a/registry/storage/registry.go b/registry/storage/registry.go index bc3ee9c9a..a480f70cb 100644 --- a/registry/storage/registry.go +++ b/registry/storage/registry.go @@ -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 { diff --git a/registry/storage/schema2manifesthandler.go b/registry/storage/schema2manifesthandler.go index d8b70deee..e72cd672e 100644 --- a/registry/storage/schema2manifesthandler.go +++ b/registry/storage/schema2manifesthandler.go @@ -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 } diff --git a/registry/storage/schema2manifesthandler_test.go b/registry/storage/schema2manifesthandler_test.go index 766b45354..73a7e336a 100644 --- a/registry/storage/schema2manifesthandler_test.go +++ b/registry/storage/schema2manifesthandler_test.go @@ -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"},