Skip to content

Commit ed48ccd

Browse files
author
Michal Minář
committed
Prefer secure connection during image pruning
The default stays the same. When a CA bundle or a registry url is specified, require secure connection with certificate verification. Allow the user to force insecure connection using --force-insecure if he has to. Signed-off-by: Michal Minář <[email protected]>
1 parent 67275e1 commit ed48ccd

File tree

4 files changed

+164
-25
lines changed

4 files changed

+164
-25
lines changed

pkg/cmd/admin/prune/images.go

+62-17
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ import (
2828
imageapi "github.com/openshift/origin/pkg/image/api"
2929
"github.com/openshift/origin/pkg/image/prune"
3030
oserrors "github.com/openshift/origin/pkg/util/errors"
31+
"github.com/openshift/origin/pkg/util/netutils"
3132
)
3233

3334
// PruneImagesRecommendedName is the recommended command name
@@ -46,7 +47,17 @@ var (
4647
--confirm flag is needed for changes to be effective.
4748
4849
Only a user with a cluster role %s or higher who is logged-in will be able to actually
49-
delete the images.`)
50+
delete the images.
51+
52+
If the registry is secured with a certificate signed by a self-signed root certificate
53+
authority other than the one present in current user's config, you may need to specify it
54+
using --certificate-authority flag.
55+
56+
Insecure connection is allowed in following cases unless certificate-authority is specified:
57+
1. --force-insecure is given
58+
2. user's config allows for insecure connection (the user logged in to the cluster with
59+
--insecure-skip-tls-verify or allowed for insecure connection)
60+
3. registry url is not given or it's a private/link-local address`)
5061

5162
imagesExample = templates.Examples(`
5263
# See, what the prune command would delete if only images more than an hour old and obsoleted
@@ -80,11 +91,13 @@ type PruneImagesOptions struct {
8091
CABundle string
8192
RegistryUrlOverride string
8293
Namespace string
94+
ForceInsecure bool
8395

8496
OSClient client.Interface
8597
KClient kclientset.Interface
8698
RegistryClient *http.Client
8799
Out io.Writer
100+
Insecure bool
88101
}
89102

90103
// NewCmdPruneImages implements the OpenShift cli prune images command.
@@ -117,8 +130,9 @@ func NewCmdPruneImages(f *clientcmd.Factory, parentName, name string, out io.Wri
117130
cmd.Flags().DurationVar(opts.KeepYoungerThan, "keep-younger-than", *opts.KeepYoungerThan, "Specify the minimum age of an image for it to be considered a candidate for pruning.")
118131
cmd.Flags().IntVar(opts.KeepTagRevisions, "keep-tag-revisions", *opts.KeepTagRevisions, "Specify the number of image revisions for a tag in an image stream that will be preserved.")
119132
cmd.Flags().BoolVar(opts.PruneOverSizeLimit, "prune-over-size-limit", *opts.PruneOverSizeLimit, "Specify if images which are exceeding LimitRanges (see 'openshift.io/Image'), specified in the same namespace, should be considered for pruning. This flag cannot be combined with --keep-younger-than nor --keep-tag-revisions.")
120-
cmd.Flags().StringVar(&opts.CABundle, "certificate-authority", opts.CABundle, "The path to a certificate authority bundle to use when communicating with the managed Docker registries. Defaults to the certificate authority data from the current user's config file.")
133+
cmd.Flags().StringVar(&opts.CABundle, "certificate-authority", opts.CABundle, "The path to a certificate authority bundle to use when communicating with the managed Docker registries. Defaults to the certificate authority data from the current user's config file. It cannot be used together with --force-insecure.")
121134
cmd.Flags().StringVar(&opts.RegistryUrlOverride, "registry-url", opts.RegistryUrlOverride, "The address to use when contacting the registry, instead of using the default value. This is useful if you can't resolve or reach the registry (e.g.; the default is a cluster-internal URL) but you do have an alternative route that works.")
135+
cmd.Flags().BoolVar(&opts.ForceInsecure, "force-insecure", opts.ForceInsecure, "If true, allow an insecure connection to the docker registry that is hosted via HTTP or has an invalid HTTPS certificate. Whenever possible, use --certificate-authority instead of this dangerous option.")
122136

123137
return cmd
124138
}
@@ -151,7 +165,16 @@ func (o *PruneImagesOptions) Complete(f *clientcmd.Factory, cmd *cobra.Command,
151165
}
152166
o.Out = out
153167

154-
osClient, kClient, registryClient, err := getClients(f, o.CABundle)
168+
clientConfig, err := f.ClientConfig()
169+
if err != nil {
170+
return err
171+
}
172+
173+
o.Insecure = o.ForceInsecure
174+
if !o.Insecure && len(o.CABundle) == 0 {
175+
o.Insecure = clientConfig.TLSClientConfig.Insecure || len(o.RegistryUrlOverride) == 0 || netutils.IsPrivateAddress(o.RegistryUrlOverride)
176+
}
177+
osClient, kClient, registryClient, err := getClients(f, o.CABundle, o.Insecure)
155178
if err != nil {
156179
return err
157180
}
@@ -176,6 +199,9 @@ func (o PruneImagesOptions) Validate() error {
176199
if _, err := url.Parse(o.RegistryUrlOverride); err != nil {
177200
return fmt.Errorf("invalid --registry-url flag: %v", err)
178201
}
202+
if o.ForceInsecure && len(o.CABundle) > 0 {
203+
return fmt.Errorf("--certificate-authority cannot be specified with --force-insecure")
204+
}
179205
return nil
180206
}
181207

@@ -251,6 +277,7 @@ func (o PruneImagesOptions) Run() error {
251277
DryRun: o.Confirm == false,
252278
RegistryClient: o.RegistryClient,
253279
RegistryURL: o.RegistryUrlOverride,
280+
Insecure: o.Insecure,
254281
}
255282
if o.Namespace != metav1.NamespaceAll {
256283
options.Namespace = o.Namespace
@@ -435,8 +462,10 @@ func (p *describingManifestDeleter) DeleteManifest(registryClient *http.Client,
435462
return err
436463
}
437464

438-
// getClients returns a Kube client, OpenShift client, and registry client.
439-
func getClients(f *clientcmd.Factory, caBundle string) (*client.Client, kclientset.Interface, *http.Client, error) {
465+
// getClients returns a Kube client, OpenShift client, and registry client. Note that
466+
// registryCABundle and registryInsecure=true are mutually exclusive. If registryInsecure=true is
467+
// specified, the ca bundle is ignored.
468+
func getClients(f *clientcmd.Factory, registryCABundle string, registryInsecure bool) (*client.Client, kclientset.Interface, *http.Client, error) {
440469
clientConfig, err := f.ClientConfig()
441470
if err != nil {
442471
return nil, nil, nil, err
@@ -461,8 +490,18 @@ func getClients(f *clientcmd.Factory, caBundle string) (*client.Client, kclients
461490
return nil, nil, nil, err
462491
}
463492

493+
cadata := []byte{}
494+
registryCABundleIncluded := false
495+
if len(registryCABundle) > 0 {
496+
cadata, err = ioutil.ReadFile(registryCABundle)
497+
if err != nil {
498+
return nil, nil, nil, fmt.Errorf("failed to read registry ca bundle: %v", err)
499+
}
500+
}
501+
464502
// copy the config
465503
registryClientConfig := *clientConfig
504+
registryClientConfig.TLSClientConfig.Insecure = registryInsecure
466505

467506
// zero out everything we don't want to use
468507
registryClientConfig.BearerToken = ""
@@ -471,8 +510,20 @@ func getClients(f *clientcmd.Factory, caBundle string) (*client.Client, kclients
471510
registryClientConfig.KeyFile = ""
472511
registryClientConfig.KeyData = []byte{}
473512

474-
// we have to set a username to something for the Docker login
475-
// but it's not actually used
513+
if registryInsecure {
514+
// it's not allowed to specify insecure flag together with CAs
515+
registryClientConfig.CAFile = ""
516+
registryClientConfig.CAData = []byte{}
517+
518+
} else if len(cadata) > 0 && len(registryClientConfig.CAData) == 0 {
519+
// If given, we want to append cabundle to the resulting tlsConfig.RootCAs. However, if we
520+
// leave CAData unset, tlsConfig may not be created. We could append the caBundle to the
521+
// CAData here directly if we were ok doing a binary magic, which is not the case.
522+
registryClientConfig.CAData = cadata
523+
registryCABundleIncluded = true
524+
}
525+
526+
// we have to set a username to something for the Docker login but it's not actually used
476527
registryClientConfig.Username = "unused"
477528

478529
// set the "password" to be the token
@@ -483,19 +534,13 @@ func getClients(f *clientcmd.Factory, caBundle string) (*client.Client, kclients
483534
return nil, nil, nil, err
484535
}
485536

486-
// if the user specified a CA on the command line, add it to the
487-
// client config's CA roots
488-
if tlsConfig != nil && len(caBundle) > 0 {
489-
data, err := ioutil.ReadFile(caBundle)
490-
if err != nil {
491-
return nil, nil, nil, err
492-
}
493-
537+
// Add the CA bundle to the client config's CA roots if provided and we haven't done that already.
538+
// FIXME: handle registryCABundle on one place
539+
if tlsConfig != nil && len(cadata) > 0 && !registryCABundleIncluded && !registryInsecure {
494540
if tlsConfig.RootCAs == nil {
495541
tlsConfig.RootCAs = x509.NewCertPool()
496542
}
497-
498-
tlsConfig.RootCAs.AppendCertsFromPEM(data)
543+
tlsConfig.RootCAs.AppendCertsFromPEM(cadata)
499544
}
500545

501546
transport := knet.SetTransportDefaults(&http.Transport{

pkg/image/prune/prune.go

+21-8
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ import (
2929
deploygraph "github.com/openshift/origin/pkg/deploy/graph/nodes"
3030
imageapi "github.com/openshift/origin/pkg/image/api"
3131
imagegraph "github.com/openshift/origin/pkg/image/graph/nodes"
32+
"github.com/openshift/origin/pkg/util/netutils"
3233
)
3334

3435
// TODO these edges should probably have an `Add***Edges` method in images/graph and be moved there
@@ -140,6 +141,8 @@ type PrunerOptions struct {
140141
RegistryClient *http.Client
141142
// RegistryURL is the URL for the registry.
142143
RegistryURL string
144+
// Allow a fallback to insecure transport when contacting the registry.
145+
Insecure bool
143146
}
144147

145148
// Pruner knows how to prune istags, images, layers and image configs.
@@ -170,7 +173,8 @@ type registryPinger interface {
170173

171174
// defaultRegistryPinger implements registryPinger.
172175
type defaultRegistryPinger struct {
173-
client *http.Client
176+
client *http.Client
177+
insecure bool
174178
}
175179

176180
func (drp *defaultRegistryPinger) ping(registry string) error {
@@ -183,23 +187,29 @@ func (drp *defaultRegistryPinger) ping(registry string) error {
183187
defer healthResponse.Body.Close()
184188

185189
if healthResponse.StatusCode != http.StatusOK {
186-
return fmt.Errorf("unexpected status code %d", healthResponse.StatusCode)
190+
return fmt.Errorf("unexpected status: %s", healthResponse.Status)
187191
}
188192

189193
return nil
190194
}
191195

192-
var err error
193-
for _, proto := range []string{"https", "http"} {
196+
var errs []error
197+
protos := make([]string, 0, 2)
198+
protos = append(protos, "https")
199+
if drp.insecure || netutils.IsPrivateAddress(registry) {
200+
protos = append(protos, "http")
201+
}
202+
for _, proto := range protos {
194203
glog.V(4).Infof("Trying %s for %s", proto, registry)
195-
err = healthCheck(proto, registry)
204+
err := healthCheck(proto, registry)
196205
if err == nil {
197-
break
206+
return nil
198207
}
208+
errs = append(errs, err)
199209
glog.V(4).Infof("Error with %s for %s: %v", proto, registry, err)
200210
}
201211

202-
return err
212+
return kerrors.NewAggregate(errs)
203213
}
204214

205215
// dryRunRegistryPinger implements registryPinger.
@@ -287,7 +297,10 @@ func NewPruner(options PrunerOptions) Pruner {
287297
if options.DryRun {
288298
rp = &dryRunRegistryPinger{}
289299
} else {
290-
rp = &defaultRegistryPinger{options.RegistryClient}
300+
rp = &defaultRegistryPinger{
301+
client: options.RegistryClient,
302+
insecure: options.Insecure,
303+
}
291304
}
292305

293306
return &pruner{

pkg/util/netutils/common.go

+34
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,9 @@ import (
1010
kerrors "k8s.io/apimachinery/pkg/util/errors"
1111
)
1212

13+
var localHosts []string = []string{"127.0.0.1", "::1", "localhost"}
14+
var localSubnets []string = []string{"10.0.0.0/8", "172.16.0.0/12", "192.168.0.0/16", "fc00::/7", "fe80::/10"}
15+
1316
func IPToUint32(ip net.IP) uint32 {
1417
return binary.BigEndian.Uint32(ip.To4())
1518
}
@@ -114,3 +117,34 @@ func ParseCIDRMask(cidr string) (*net.IPNet, error) {
114117
}
115118
return net, nil
116119
}
120+
121+
// IsPrivateAddress returns true if given address in format "<host>[:<port>]" is a localhost or an ip from
122+
// private network range (e.g. 172.30.0.1, 192.168.0.1).
123+
func IsPrivateAddress(addr string) bool {
124+
host, _, err := net.SplitHostPort(addr)
125+
if err != nil {
126+
// assume indexName is of the form `host` without the port and go on.
127+
host = addr
128+
}
129+
for _, localHost := range localHosts {
130+
if host == localHost {
131+
return true
132+
}
133+
}
134+
135+
ip := net.ParseIP(host)
136+
if ip == nil {
137+
return false
138+
}
139+
140+
for _, subnet := range localSubnets {
141+
ipnet, err := ParseCIDRMask(subnet)
142+
if err != nil {
143+
continue // should not happen
144+
}
145+
if ipnet.Contains(ip) {
146+
return true
147+
}
148+
}
149+
return false
150+
}

pkg/util/netutils/common_test.go

+47
Original file line numberDiff line numberDiff line change
@@ -88,3 +88,50 @@ func TestParseCIDRMask(t *testing.T) {
8888
}
8989
}
9090
}
91+
92+
func TestIsPrivateAddress(t *testing.T) {
93+
for _, tc := range []struct {
94+
address string
95+
isLocal bool
96+
}{
97+
{"localhost", true},
98+
{"example.com", false},
99+
{"registry.localhost", false},
100+
101+
{"9.255.255.255", false},
102+
{"10.0.0.1", true},
103+
{"10.1.255.255", true},
104+
{"10.255.255.255", true},
105+
{"11.0.0.1", false},
106+
107+
{"127.0.0.1", true},
108+
109+
{"172.15.255.253", false},
110+
{"172.16.0.1", true},
111+
{"172.30.0.1", true},
112+
{"172.31.255.255", true},
113+
{"172.32.0.1", false},
114+
115+
{"192.167.122.1", false},
116+
{"192.168.0.1", true},
117+
{"192.168.122.1", true},
118+
{"192.168.255.255", true},
119+
{"192.169.1.1", false},
120+
121+
{"::1", true},
122+
123+
{"fe00::1", false},
124+
{"fd12:3456:789a:1::1", true},
125+
{"fe82:3456:789a:1::1", true},
126+
{"ff00::1", false},
127+
} {
128+
res := IsPrivateAddress(tc.address)
129+
if tc.isLocal && !res {
130+
t.Errorf("address %q considered not local", tc.address)
131+
continue
132+
}
133+
if !tc.isLocal && res {
134+
t.Errorf("address %q considered local", tc.address)
135+
}
136+
}
137+
}

0 commit comments

Comments
 (0)