Skip to content

Commit

Permalink
Additional registry whitelisting
Browse files Browse the repository at this point in the history
Adds validation to imagestreams and imagestreamtags that forbids new
image pull specs not present on the whitelist.

Resolves bug 1505315

Signed-off-by: Michal Minář <[email protected]>
  • Loading branch information
Michal Minář committed Dec 14, 2017
1 parent d3cdfec commit 842eba4
Show file tree
Hide file tree
Showing 16 changed files with 957 additions and 128 deletions.
31 changes: 31 additions & 0 deletions pkg/image/admission/fake/fake.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package fake

import (
imgadmission "github.com/openshift/origin/pkg/image/admission"
imageapi "github.com/openshift/origin/pkg/image/apis/image"
)

type FakeImageStreamLimitVerifier struct {
ImageStreamEvaluator func(ns string, is *imageapi.ImageStream) error
Err error
}

func (f *FakeImageStreamLimitVerifier) VerifyLimits(ns string, is *imageapi.ImageStream) error {
if f.ImageStreamEvaluator != nil {
return f.ImageStreamEvaluator(ns, is)
}
return f.Err
}

type FakeRegistryWhitelister struct{}

func (rw *FakeRegistryWhitelister) AdmitHostname(host string, insecure bool) error { return nil }
func (rw *FakeRegistryWhitelister) AdmitDockerImageReference(ref *imageapi.DockerImageReference, insecure bool) error {
return nil
}
func (rw *FakeRegistryWhitelister) WhitelistRegistry(hostPortGlob string, insecure bool) error {
return nil
}
func (rw *FakeRegistryWhitelister) Copy() imgadmission.RegistryWhitelister {
return &FakeRegistryWhitelister{}
}
192 changes: 192 additions & 0 deletions pkg/image/admission/registry_whitelist.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,192 @@
package admission

import (
"fmt"
"net"
"reflect"
"strings"

"github.com/golang/glog"

kerrutil "k8s.io/apimachinery/pkg/util/errors"
"k8s.io/apimachinery/pkg/util/sets"

serverapi "github.com/openshift/origin/pkg/cmd/server/api"
imageapi "github.com/openshift/origin/pkg/image/apis/image"
stringsutil "github.com/openshift/origin/pkg/util/strings"
)

// RegistryWhitelister decides whether given image pull specs are allowed by system's image policy.
type RegistryWhitelister interface {
// AdmitHostname returns error if the given host is allowed by the whitelist.
AdmitHostname(host string, insecure bool) error
// AdmitDockerImageReference returns error if the given reference is allowed neither by the whitelist nor
// by collected whitelisted pull specs.
AdmitDockerImageReference(ref *imageapi.DockerImageReference, insecure bool) error
// WhitelistRegistry extends internal whitelist for additional registry domain name. Accepted values are:
// <host>, <host>:<port>
// where each component can contain wildcards like '*' or '??' to mach wide range of registries. If the
// port is omitted, the default will be appended based on the given insecure flag.
WhitelistRegistry(hostPortGlob string, insecure bool) error
// WhitelistPullSpecs allows to whitelist particular pull specs. References must match exactly one of the
// given pull specs for it to be whitelisted.
// TODO: accept insecure flag
WhitelistPullSpecs(pullSpecs ...string)
// Copy returns a deep copy of the whitelister. This is useful for temporarily whitelisting additional
// registires/pullSpecs before a specific validation.
Copy() RegistryWhitelister
}

type allowedHostPortGlobs struct {
host string
port string
}

type registryWhitelister struct {
whitelist []allowedHostPortGlobs
pullSpecs sets.String
registryHostRetriever imageapi.RegistryHostnameRetriever
}

var _ RegistryWhitelister = &registryWhitelister{}

func NewRegistryWhitelister(
whitelist serverapi.AllowedRegistries,
registryHostRetriever imageapi.RegistryHostnameRetriever,
) (RegistryWhitelister, error) {
errs := []error{}
rw := registryWhitelister{
whitelist: make([]allowedHostPortGlobs, 0, len(whitelist)),
pullSpecs: sets.NewString(),
registryHostRetriever: registryHostRetriever,
}
for _, registry := range whitelist {
err := rw.WhitelistRegistry(registry.DomainName, registry.Insecure)
if err != nil {
errs = append(errs, err)
}
}

if len(errs) > 0 {
return nil, kerrutil.NewAggregate(errs)
}
return &rw, nil
}

// WhitelistAllRegistries returns a whitelister that will allow any given registry host name.
// TODO: make a new implementation of RegistryWhitelister instead that will not bother with pull specs
func WhitelistAllRegistries() RegistryWhitelister {
return &registryWhitelister{
whitelist: []allowedHostPortGlobs{{host: "*", port: "*"}},
pullSpecs: sets.NewString(),
}
}

func (rw *registryWhitelister) AdmitHostname(hostname string, insecure bool) error {
return rw.AdmitDockerImageReference(&imageapi.DockerImageReference{Registry: hostname}, insecure)
}

func (rw *registryWhitelister) AdmitDockerImageReference(ref *imageapi.DockerImageReference, insecure bool) error {
const showMax = 4
const fullWhitelistLogLevel = 5

if rw.pullSpecs.Len() > 0 {
if rw.pullSpecs.Has(ref.Exact()) || rw.pullSpecs.Has(ref.DockerClientDefaults().Exact()) || rw.pullSpecs.Has(ref.DaemonMinimal().Exact()) {
return nil
}
}

if rw.registryHostRetriever != nil {
if localRegistry, ok := rw.registryHostRetriever.InternalRegistryHostname(); ok {
rw.WhitelistRegistry(localRegistry, false)
}
}

host, port := ref.RegistryHostPort(insecure)

matchHost := func(h string) bool {
for _, hp := range rw.whitelist {
if stringsutil.IsWildcardMatch(h, hp.host) && stringsutil.IsWildcardMatch(port, hp.port) {
return true
}
}
return false
}

switch host {
case imageapi.DockerDefaultV1Registry, imageapi.DockerDefaultV2Registry:
// try to match plain docker.io first to satisfy `docker.io:*` wildcard
if matchHost(imageapi.DockerDefaultRegistry) {
return nil
}
fallthrough
default:
if matchHost(host) {
return nil
}
}

hostname := ref.Registry
if len(ref.Registry) == 0 {
hostname = net.JoinHostPort(host, port)
}
whitelist := []string{}
full := []string{}
for i := 0; i < len(rw.whitelist); i++ {
if i < showMax-1 || len(rw.whitelist)-showMax == 0 {
whitelist = append(whitelist, fmt.Sprintf("%q", net.JoinHostPort(rw.whitelist[i].host, rw.whitelist[i].port)))
} else if i < showMax {
whitelist = append(whitelist, fmt.Sprintf("and %d more ...", len(rw.whitelist)-showMax+1))
} else if !glog.V(fullWhitelistLogLevel) {
break
}
full = append(full, fmt.Sprintf("%q", net.JoinHostPort(rw.whitelist[i].host, rw.whitelist[i].port)))
}
glog.V(fullWhitelistLogLevel).Infof("registry %q not allowed by whitelist { %s }", hostname, strings.Join(full, ", "))
return fmt.Errorf("registry %q not allowed by whitelist { %s }", hostname, strings.Join(whitelist, ", "))
}

func (rw *registryWhitelister) WhitelistRegistry(hostPortGlob string, insecure bool) error {
hp := allowedHostPortGlobs{}

parts := strings.SplitN(hostPortGlob, ":", 3)
switch len(parts) {
case 1:
hp.host = parts[0]
if insecure {
hp.port = "80"
} else {
hp.port = "443"
}
case 2:
hp.host, hp.port = parts[0], parts[1]
default:
return fmt.Errorf("failed to parse allowed registry %q: too many colons", hostPortGlob)
}

for _, item := range rw.whitelist {
if reflect.DeepEqual(item, hp) {
return nil
}
}
rw.whitelist = append(rw.whitelist, hp)

return nil
}

func (rw *registryWhitelister) WhitelistPullSpecs(pullSpecs ...string) {
rw.pullSpecs.Insert(pullSpecs...)
}

func (rw *registryWhitelister) Copy() RegistryWhitelister {
newRW := registryWhitelister{
whitelist: make([]allowedHostPortGlobs, len(rw.whitelist)),
pullSpecs: sets.NewString(rw.pullSpecs.List()...),
registryHostRetriever: rw.registryHostRetriever,
}

for i, item := range rw.whitelist {
newRW.whitelist[i] = item
}
return &newRW
}
Loading

0 comments on commit 842eba4

Please sign in to comment.