From 61e5803b56a41292779bdd259ea14f166531579e Mon Sep 17 00:00:00 2001 From: Noah Treuhaft Date: Fri, 8 Jul 2016 15:44:52 -0700 Subject: [PATCH] Add control over validation of URLs in pushed manifests Until we have some experience hosting foreign layer manifests, the Hub operators wish to limit foreign layers on Hub. To that end, this change adds registry configuration options to restrict the URLs that may appear in pushed manifests. Signed-off-by: Noah Treuhaft --- configuration/configuration.go | 18 ++++++++ docs/configuration.md | 43 ++++++++++++++++++- registry/handlers/app.go | 35 +++++++++++++++ registry/storage/garbagecollect_test.go | 5 ++- registry/storage/registry.go | 22 ++++++++++ registry/storage/schema2manifesthandler.go | 4 +- .../storage/schema2manifesthandler_test.go | 15 ++++++- 7 files changed, 137 insertions(+), 5 deletions(-) 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 94034b260..a750c5efc 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 fc7cc6ebc..b1d91e3a7 100644 --- a/registry/storage/schema2manifesthandler.go +++ b/registry/storage/schema2manifesthandler.go @@ -102,10 +102,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"},