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 <noah.treuhaft@docker.com>
This commit is contained in:
Noah Treuhaft 2016-07-08 15:44:52 -07:00
parent 2052f29be6
commit 61e5803b56
7 changed files with 137 additions and 5 deletions

View file

@ -172,6 +172,24 @@ type Configuration struct {
TrustKey string `yaml:"signingkeyfile,omitempty"` TrustKey string `yaml:"signingkeyfile,omitempty"`
} `yaml:"schema1,omitempty"` } `yaml:"schema1,omitempty"`
} `yaml:"compatibility,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. // 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: compatibility:
schema1: schema1:
signingkeyfile: /etc/registry/key.json 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 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 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 signingkeyfile: /etc/registry/key.json
Configure handling of older and deprecated features. Each subsection 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 ### Schema1
@ -1796,6 +1804,39 @@ defines a such a feature with configurable behavior.
</tr> </tr>
</table> </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 ## Example: Development configuration
The following is a simple example you can use for local development: The following is a simple example you can use for local development:

View file

@ -9,7 +9,9 @@ import (
"net/http" "net/http"
"net/url" "net/url"
"os" "os"
"regexp"
"runtime" "runtime"
"strings"
"time" "time"
log "github.com/Sirupsen/logrus" log "github.com/Sirupsen/logrus"
@ -211,6 +213,39 @@ func NewApp(ctx context.Context, config *configuration.Configuration) *App {
options = append(options, storage.EnableRedirect) 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 // configure storage caches
if cc, ok := config.Storage["cache"]; ok { if cc, ok := config.Storage["cache"]; ok {
v, ok := cc["blobdescriptor"] v, ok := cc["blobdescriptor"]

View file

@ -21,13 +21,14 @@ type image struct {
layers map[digest.Digest]io.ReadSeeker 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() ctx := context.Background()
k, err := libtrust.GenerateECP256PrivateKey() k, err := libtrust.GenerateECP256PrivateKey()
if err != nil { if err != nil {
t.Fatal(err) 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 { if err != nil {
t.Fatalf("Failed to construct namespace") t.Fatalf("Failed to construct namespace")
} }

View file

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

View file

@ -102,10 +102,12 @@ func (ms *schema2ManifestHandler) verifyManifest(ctx context.Context, mnfst sche
if len(fsLayer.URLs) == 0 { if len(fsLayer.URLs) == 0 {
err = errMissingURL err = errMissingURL
} }
allow := ms.repository.manifestURLs.allow
deny := ms.repository.manifestURLs.deny
for _, u := range fsLayer.URLs { for _, u := range fsLayer.URLs {
var pu *url.URL var pu *url.URL
pu, err = url.Parse(u) 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 err = errInvalidURL
break break
} }

View file

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