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:
parent
2052f29be6
commit
61e5803b56
7 changed files with 137 additions and 5 deletions
|
@ -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.
|
||||||
|
|
|
@ -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:
|
||||||
|
|
|
@ -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"]
|
||||||
|
|
|
@ -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")
|
||||||
}
|
}
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
@ -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"},
|
||||||
|
|
Loading…
Reference in a new issue