Merge pull request #1829 from nwt/foreign-layer-host-whitelist
Add a foreign layer URL host whitelist
This commit is contained in:
commit
2b72dd3927
@ -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 {
|
||||||
|
@ -97,10 +97,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 New Issue
Block a user