Merge pull request #1829 from nwt/foreign-layer-host-whitelist
Add a foreign layer URL host whitelist
This commit is contained in:
commit
2b72dd3927
7 changed files with 137 additions and 5 deletions
|
@ -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 a new issue