Skip to content

Commit 8c027ce

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 8c027ce

File tree

4 files changed

+165
-25
lines changed

4 files changed

+165
-25
lines changed

pkg/cmd/admin/prune/images.go

+63-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,18 @@ 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:
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 AND
61+
--certificate-authority is not specified`)
5062

5163
imagesExample = templates.Examples(`
5264
# See, what the prune command would delete if only images more than an hour old and obsoleted
@@ -80,11 +92,13 @@ type PruneImagesOptions struct {
8092
CABundle string
8193
RegistryUrlOverride string
8294
Namespace string
95+
ForceInsecure bool
8396

8497
OSClient client.Interface
8598
KClient kclientset.Interface
8699
RegistryClient *http.Client
87100
Out io.Writer
101+
Insecure bool
88102
}
89103

90104
// NewCmdPruneImages implements the OpenShift cli prune images command.
@@ -117,8 +131,9 @@ func NewCmdPruneImages(f *clientcmd.Factory, parentName, name string, out io.Wri
117131
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.")
118132
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.")
119133
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.")
134+
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.")
121135
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.")
136+
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.")
122137

123138
return cmd
124139
}
@@ -151,7 +166,16 @@ func (o *PruneImagesOptions) Complete(f *clientcmd.Factory, cmd *cobra.Command,
151166
}
152167
o.Out = out
153168

154-
osClient, kClient, registryClient, err := getClients(f, o.CABundle)
169+
clientConfig, err := f.ClientConfig()
170+
if err != nil {
171+
return err
172+
}
173+
174+
o.Insecure = o.ForceInsecure
175+
if !o.Insecure && len(o.CABundle) == 0 && clientConfig.TLSClientConfig.Insecure {
176+
o.Insecure = len(o.RegistryUrlOverride) == 0 || netutils.IsPrivateAddress(o.RegistryUrlOverride)
177+
}
178+
osClient, kClient, registryClient, err := getClients(f, o.CABundle, o.Insecure)
155179
if err != nil {
156180
return err
157181
}
@@ -176,6 +200,9 @@ func (o PruneImagesOptions) Validate() error {
176200
if _, err := url.Parse(o.RegistryUrlOverride); err != nil {
177201
return fmt.Errorf("invalid --registry-url flag: %v", err)
178202
}
203+
if o.ForceInsecure && len(o.CABundle) > 0 {
204+
return fmt.Errorf("--certificate-authority cannot be specified with --force-insecure")
205+
}
179206
return nil
180207
}
181208

@@ -251,6 +278,7 @@ func (o PruneImagesOptions) Run() error {
251278
DryRun: o.Confirm == false,
252279
RegistryClient: o.RegistryClient,
253280
RegistryURL: o.RegistryUrlOverride,
281+
Insecure: o.Insecure,
254282
}
255283
if o.Namespace != metav1.NamespaceAll {
256284
options.Namespace = o.Namespace
@@ -435,8 +463,10 @@ func (p *describingManifestDeleter) DeleteManifest(registryClient *http.Client,
435463
return err
436464
}
437465

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) {
466+
// getClients returns a Kube client, OpenShift client, and registry client. Note that
467+
// registryCABundle and registryInsecure=true are mutually exclusive. If registryInsecure=true is
468+
// specified, the ca bundle is ignored.
469+
func getClients(f *clientcmd.Factory, registryCABundle string, registryInsecure bool) (*client.Client, kclientset.Interface, *http.Client, error) {
440470
clientConfig, err := f.ClientConfig()
441471
if err != nil {
442472
return nil, nil, nil, err
@@ -461,8 +491,18 @@ func getClients(f *clientcmd.Factory, caBundle string) (*client.Client, kclients
461491
return nil, nil, nil, err
462492
}
463493

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

467507
// zero out everything we don't want to use
468508
registryClientConfig.BearerToken = ""
@@ -471,8 +511,20 @@ func getClients(f *clientcmd.Factory, caBundle string) (*client.Client, kclients
471511
registryClientConfig.KeyFile = ""
472512
registryClientConfig.KeyData = []byte{}
473513

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

478530
// set the "password" to be the token
@@ -483,19 +535,13 @@ func getClients(f *clientcmd.Factory, caBundle string) (*client.Client, kclients
483535
return nil, nil, nil, err
484536
}
485537

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-
538+
// Add the CA bundle to the client config's CA roots if provided and we haven't done that already.
539+
// FIXME: handle registryCABundle on one place
540+
if tlsConfig != nil && len(cadata) > 0 && !registryCABundleIncluded && !registryInsecure {
494541
if tlsConfig.RootCAs == nil {
495542
tlsConfig.RootCAs = x509.NewCertPool()
496543
}
497-
498-
tlsConfig.RootCAs.AppendCertsFromPEM(data)
544+
tlsConfig.RootCAs.AppendCertsFromPEM(cadata)
499545
}
500546

501547
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)