Merge pull request #1829 from nwt/foreign-layer-host-whitelist
Add a foreign layer URL host whitelistmaster
						commit
						2b72dd3927
					
				|  | @ -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.
 | ||||
|  |  | |||
|  | @ -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: | ||||
|  |  | |||
|  | @ -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"] | ||||
|  |  | |||
|  | @ -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") | ||||
| 	} | ||||
|  |  | |||
|  | @ -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 { | ||||
|  |  | |||
|  | @ -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 | ||||
| 					} | ||||
|  |  | |||
|  | @ -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"}, | ||||
|  |  | |||
		Loading…
	
		Reference in New Issue