Browse Source

Add tests to Istio Webhook Controller (#16418)

* Add tests to Istio Webhook Controller

* Add the command to generate the example CA cert

* Fix the error when go test count is 50

Fix the error when "go test ./security/pkg/k8s/chiron/... -count 50".

* Simplify the code

* Refactor code

* Remove managing webhookconfiguration
lei-tang 3 weeks ago
parent
commit
88763fbecc

+ 12
- 137
security/cmd/chiron/main.go View File

@@ -15,15 +15,10 @@
15 15
 package main
16 16
 
17 17
 import (
18
-	"io/ioutil"
19 18
 	"os"
20
-	"strconv"
21 19
 	"strings"
22 20
 	"time"
23 21
 
24
-	"k8s.io/api/admissionregistration/v1beta1"
25
-
26
-	"github.com/ghodss/yaml"
27 22
 	"github.com/spf13/cobra"
28 23
 	"github.com/spf13/cobra/doc"
29 24
 
@@ -40,8 +35,12 @@ import (
40 35
 )
41 36
 
42 37
 const (
43
-	mutatingWebhookConfigurationKind   = "mutatingwebhookconfiguration"
44
-	validatingWebhookConfigurationKind = "validatingwebhookconfiguration"
38
+	// DefaultCertGracePeriodRatio is the default length of certificate rotation grace period,
39
+	// configured as the ratio of the certificate TTL.
40
+	DefaultCertGracePeriodRatio = 0.5
41
+
42
+	// DefaultMinCertGracePeriod is the default minimum grace period for workload cert rotation.
43
+	DefaultMinCertGracePeriod = 10 * time.Minute
45 44
 )
46 45
 
47 46
 var (
@@ -72,27 +71,12 @@ type cliOptions struct {
72 71
 	// The file path of k8s CA certificate
73 72
 	k8sCaCertFile string
74 73
 
75
-	// The file paths of the webhookconfigurations.
76
-	// In prototype, only one mutatingwebhookconfiguration file and one validatingwebhookconfiguration are supported.
77
-	webhookConfigFiles string
78
-	// The names of the MutatingWebhookConfiguration to manage
79
-	// In prototype, only one is supported.
80
-	mutatingWebhookConfigNames string
81 74
 	// The names of the services of mutating webhooks to manage
82 75
 	// In prototype, only one is supported.
83 76
 	mutatingWebhookServiceNames string
84
-	// The ports of the services of mutating webhooks to manage
85
-	// In prototype, only one is supported.
86
-	mutatingWebhookServicePorts string
87
-	// The names of the valitatingWebhookConfiguration to manage
88
-	// In prototype, only one is supported.
89
-	validatingWebhookConfigNames string
90 77
 	// The names of the services of validating webhooks to manage
91 78
 	// In prototype, only one is supported.
92 79
 	validatingWebhookServiceNames string
93
-	// The ports of the services of validating webhooks to manage
94
-	// In prototype, only one is supported.
95
-	validatingWebhookServicePorts string
96 80
 
97 81
 	// The minimum grace period for cert rotation.
98 82
 	certMinGracePeriod time.Duration
@@ -108,9 +92,6 @@ type cliOptions struct {
108 92
 
109 93
 	// Whether enable the webhook controller
110 94
 	enableController bool
111
-
112
-	// Whether delete webhookconfigurations upon exit
113
-	deleteWebhookConfigurationsOnExit bool
114 95
 }
115 96
 
116 97
 func init() {
@@ -118,8 +99,6 @@ func init() {
118 99
 
119 100
 	flags.BoolVar(&opts.enableController, "enable-controller", false, "Specifies whether enabling "+
120 101
 		"Istio Webhook Controller.")
121
-	flags.BoolVar(&opts.deleteWebhookConfigurationsOnExit, "delete-webhook-configurations-on-exit", false, "Specifies whether deleting "+
122
-		"webhookconfigurations when Istio Webhook Controller exits.")
123 102
 	flags.StringVar(&opts.certificateNamespace, "certificate-namespace", "istio-system",
124 103
 		"The namespace of the webhook certificates.")
125 104
 
@@ -135,43 +114,25 @@ func init() {
135 114
 
136 115
 	// Certificate issuance configuration.
137 116
 	flags.Float32Var(&opts.certGracePeriodRatio, "cert-grace-period-ratio",
138
-		cmd.DefaultWorkloadCertGracePeriodRatio, "The certificate rotation grace period, as a ratio of the "+
117
+		DefaultCertGracePeriodRatio, "The certificate rotation grace period, as a ratio of the "+
139 118
 			"certificate TTL.")
140 119
 	flags.DurationVar(&opts.certMinGracePeriod, "cert-min-grace-period",
141
-		cmd.DefaultWorkloadMinCertGracePeriod, "The minimum certificate rotation grace period.")
142
-
143
-	// TODO (lei-tang): configuration files may also be loaded from CRDs.
144
-	flags.StringVar(&opts.webhookConfigFiles, "webhook-config-files",
145
-		"/etc/galley-webhook-config/validatingwebhookconfiguration.yaml,/etc/sidecarinjector-webhook-config/mutatingwebhookconfiguration.yaml",
146
-		"The file paths of the webhookconfigurations, separated by comma. "+
147
-			"Only one mutatingwebhookconfiguration file and one validatingwebhookconfiguration are supported.")
148
-	flags.StringVar(&opts.mutatingWebhookConfigNames, "mutating-webhook-config-names", "istio-sidecar-injector",
149
-		"The names of the mutatingwebhookconfiguration resources in Kubernetes, separated by comma. Currently, Chiron will only manage the first one.")
120
+		DefaultMinCertGracePeriod, "The minimum certificate rotation grace period.")
121
+
150 122
 	flags.StringVar(&opts.mutatingWebhookServiceNames, "mutating-webhook-service-names", "istio-sidecar-injector",
151 123
 		"The names of the services of mutating webhooks, separated by comma. Currently, Chiron will only manage the first one.")
152
-	flags.StringVar(&opts.mutatingWebhookServicePorts, "mutating-webhook-service-ports", "443",
153
-		"The ports of the services of mutating webhooks, separated by comma. Currently, Chiron will only manage the first one.")
154
-	flags.StringVar(&opts.validatingWebhookConfigNames, "validating-webhook-config-names", "istio-galley",
155
-		"The names of the validatingwebhookconfiguration resources in Kubernetes, separated by comma. Currently, Chiron will only manage the first one.")
156 124
 	flags.StringVar(&opts.validatingWebhookServiceNames, "validating-webhook-service-names", "istio-galley",
157 125
 		"The names of the services of validating webhooks, separated by comma. Currently, Chiron will only manage the first one.")
158
-	flags.StringVar(&opts.validatingWebhookServicePorts, "validating-webhook-service-ports", "443",
159
-		"The ports of the services of validating webhooks, separated by comma. Currently, Chiron will only manage the first one.")
160 126
 
161 127
 	// Hide the command line options for the prototype
162 128
 	_ = flags.MarkHidden("enable-controller")
163
-	_ = flags.MarkHidden("delete-webhook-configurations-on-exit")
164 129
 	_ = flags.MarkHidden("certificate-namespace")
165 130
 	_ = flags.MarkHidden("kube-config")
166 131
 	_ = flags.MarkHidden("cert-grace-period-ratio")
167 132
 	_ = flags.MarkHidden("cert-min-grace-period")
168 133
 	_ = flags.MarkHidden("webhook-config-files")
169
-	_ = flags.MarkHidden("mutating-webhook-config-names")
170 134
 	_ = flags.MarkHidden("mutating-webhook-service-names")
171
-	_ = flags.MarkHidden("mutating-webhook-service-ports")
172
-	_ = flags.MarkHidden("validating-webhook-config-names")
173 135
 	_ = flags.MarkHidden("validating-webhook-service-names")
174
-	_ = flags.MarkHidden("validating-webhook-service-ports")
175 136
 
176 137
 	rootCmd.AddCommand(version.CobraCommand())
177 138
 	rootCmd.AddCommand(collateral.CobraCommand(rootCmd, &doc.GenManHeader{
@@ -206,68 +167,15 @@ func runWebhookController() {
206 167
 		os.Exit(1)
207 168
 	}
208 169
 
209
-	webhookConfigFiles := strings.Split(opts.webhookConfigFiles, ",")
210
-	mutatingWebhookConfigNames := strings.Split(opts.mutatingWebhookConfigNames, ",")
211 170
 	mutatingWebhookServiceNames := strings.Split(opts.mutatingWebhookServiceNames, ",")
212
-	mutatingWebhookServicePorts := strings.Split(opts.mutatingWebhookServicePorts, ",")
213
-	validatingWebhookConfigNames := strings.Split(opts.validatingWebhookConfigNames, ",")
214 171
 	validatingWebhookServiceNames := strings.Split(opts.validatingWebhookServiceNames, ",")
215
-	validatingWebhookServicePorts := strings.Split(opts.validatingWebhookServicePorts, ",")
216
-
217
-	var mutatingWebhookConfigFiles, validatingWebhookConfigFiles []string
218
-	for _, fileName := range webhookConfigFiles {
219
-		if b, err := isMutatingWebhookConfiguration(fileName); err == nil && b {
220
-			mutatingWebhookConfigFiles = append(mutatingWebhookConfigFiles, fileName)
221
-		} else if b, err := isValidatingWebhookConfiguration(fileName); err == nil && b {
222
-			validatingWebhookConfigFiles = append(validatingWebhookConfigFiles, fileName)
223
-		}
224
-	}
225
-	if len(mutatingWebhookConfigFiles) == 0 || len(validatingWebhookConfigFiles) == 0 {
226
-		log.Error("no validatingwebhookconfiguration files or no mutatingwebhookconfiguration files")
227
-		os.Exit(1)
228
-	}
229
-	if len(mutatingWebhookConfigNames) == 0 || len(validatingWebhookConfigNames) == 0 {
230
-		log.Error("no validatingwebhookconfiguration names or no mutatingwebhookconfiguration names")
231
-		os.Exit(1)
232
-	}
233
-	if len(mutatingWebhookConfigNames) != len(mutatingWebhookServiceNames) ||
234
-		len(mutatingWebhookConfigNames) != len(mutatingWebhookServicePorts) {
235
-		log.Error("the mutating webhook config names must be 1-to-1 mapped to the service names and service ports")
236
-		os.Exit(1)
237
-	}
238
-	if len(validatingWebhookConfigNames) != len(validatingWebhookServiceNames) ||
239
-		len(validatingWebhookConfigNames) != len(validatingWebhookServicePorts) {
240
-		log.Error("the validating webhook config names must be 1-to-1 mapped to the service names and service ports")
241
-		os.Exit(1)
242
-	}
243
-
244
-	// convert the port number to int
245
-	var mutatingSvcPorts = []int{}
246
-	var validatingSvcPorts = []int{}
247
-	for _, p := range mutatingWebhookServicePorts {
248
-		port, err := strconv.Atoi(p)
249
-		if err != nil {
250
-			log.Errorf("the mutating webhook port number is invalid: %v", err)
251
-			os.Exit(1)
252
-		}
253
-		mutatingSvcPorts = append(mutatingSvcPorts, port)
254
-	}
255
-	for _, p := range validatingWebhookServicePorts {
256
-		port, err := strconv.Atoi(p)
257
-		if err != nil {
258
-			log.Errorf("the validating webhook port number is invalid: %v", err)
259
-			os.Exit(1)
260
-		}
261
-		validatingSvcPorts = append(validatingSvcPorts, port)
262
-	}
263 172
 
264 173
 	stopCh := make(chan struct{})
265 174
 
266
-	wc, err := chiron.NewWebhookController(opts.deleteWebhookConfigurationsOnExit, opts.certGracePeriodRatio, opts.certMinGracePeriod,
175
+	wc, err := chiron.NewWebhookController(opts.certGracePeriodRatio, opts.certMinGracePeriod,
267 176
 		k8sClient.CoreV1(), k8sClient.AdmissionregistrationV1beta1(), k8sClient.CertificatesV1beta1(),
268
-		opts.k8sCaCertFile, opts.certificateNamespace, mutatingWebhookConfigFiles, mutatingWebhookConfigNames,
269
-		mutatingWebhookServiceNames, mutatingSvcPorts, validatingWebhookConfigFiles, validatingWebhookConfigNames,
270
-		validatingWebhookServiceNames, validatingSvcPorts)
177
+		opts.k8sCaCertFile, opts.certificateNamespace, mutatingWebhookServiceNames,
178
+		validatingWebhookServiceNames)
271 179
 
272 180
 	if err != nil {
273 181
 		log.Errorf("failed to create webhook controller: %v", err)
@@ -276,39 +184,6 @@ func runWebhookController() {
276 184
 
277 185
 	// Run the controller to manage the lifecycles of webhook certificates and webhook configurations
278 186
 	wc.Run(stopCh)
279
-	defer wc.K8sCaCertWatcher.Close()
280
-	defer wc.MutatingWebhookFileWatcher.Close()
281
-	defer wc.ValidatingWebhookFileWatcher.Close()
282 187
 
283 188
 	istiocmd.WaitSignal(stopCh)
284 189
 }
285
-
286
-func isMutatingWebhookConfiguration(fileName string) (bool, error) {
287
-	webhookConfigData, err := ioutil.ReadFile(fileName)
288
-	if err != nil {
289
-		return false, err
290
-	}
291
-	var webhookConfig v1beta1.MutatingWebhookConfiguration
292
-	if err := yaml.Unmarshal(webhookConfigData, &webhookConfig); err != nil {
293
-		return false, err
294
-	}
295
-	if !strings.EqualFold(webhookConfig.Kind, mutatingWebhookConfigurationKind) {
296
-		return false, nil
297
-	}
298
-	return true, nil
299
-}
300
-
301
-func isValidatingWebhookConfiguration(fileName string) (bool, error) {
302
-	webhookConfigData, err := ioutil.ReadFile(fileName)
303
-	if err != nil {
304
-		return false, err
305
-	}
306
-	var webhookConfig v1beta1.ValidatingWebhookConfiguration
307
-	if err := yaml.Unmarshal(webhookConfigData, &webhookConfig); err != nil {
308
-		return false, err
309
-	}
310
-	if !strings.EqualFold(webhookConfig.Kind, validatingWebhookConfigurationKind) {
311
-		return false, nil
312
-	}
313
-	return true, nil
314
-}

+ 0
- 101
security/cmd/chiron/main_test.go View File

@@ -1,101 +0,0 @@
1
-// Copyright 2019 Istio Authors
2
-//
3
-// Licensed under the Apache License, Version 2.0 (the "License");
4
-// you may not use this file except in compliance with the License.
5
-// You may obtain a copy of the License at
6
-//
7
-//     http://www.apache.org/licenses/LICENSE-2.0
8
-//
9
-// Unless required by applicable law or agreed to in writing, software
10
-// distributed under the License is distributed on an "AS IS" BASIS,
11
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
-// See the License for the specific language governing permissions and
13
-// limitations under the License.
14
-
15
-package main
16
-
17
-import (
18
-	"testing"
19
-)
20
-
21
-func TestIsMutatingWebhookConfiguration(t *testing.T) {
22
-	testCases := map[string]struct {
23
-		path       string
24
-		shouldFail bool
25
-		expectRet  bool
26
-	}{
27
-		"file not exist": {
28
-			path:       "./invalid-path/invalid-file",
29
-			shouldFail: true,
30
-		},
31
-		"valid mutating webhook config": {
32
-			path:       "./test-data/example-mutating-webhook-config.yaml",
33
-			shouldFail: false,
34
-			expectRet:  true,
35
-		},
36
-		"invalid mutating webhook config": {
37
-			path:       "./test-data/example-validating-webhook-config.yaml",
38
-			shouldFail: false,
39
-			expectRet:  false,
40
-		},
41
-	}
42
-
43
-	for _, tc := range testCases {
44
-		ret, err := isMutatingWebhookConfiguration(tc.path)
45
-		if tc.shouldFail {
46
-			if err == nil {
47
-				t.Errorf("should have failed at isMutatingWebhookConfiguration()")
48
-			} else {
49
-				// Should fail, skip the current case.
50
-				continue
51
-			}
52
-		} else if err != nil {
53
-			t.Errorf("failed at isMutatingWebhookConfiguration(): %v", err)
54
-		}
55
-
56
-		if tc.expectRet != ret {
57
-			t.Error("the return value is unexpected")
58
-		}
59
-	}
60
-}
61
-
62
-func TestIsValidatingWebhookConfiguration(t *testing.T) {
63
-	testCases := map[string]struct {
64
-		path       string
65
-		shouldFail bool
66
-		expectRet  bool
67
-	}{
68
-		"file not exist": {
69
-			path:       "./invalid-path/invalid-file",
70
-			shouldFail: true,
71
-		},
72
-		"valid validating webhook config": {
73
-			path:       "./test-data/example-validating-webhook-config.yaml",
74
-			shouldFail: false,
75
-			expectRet:  true,
76
-		},
77
-		"invalid validating webhook config": {
78
-			path:       "./test-data/example-mutating-webhook-config.yaml",
79
-			shouldFail: false,
80
-			expectRet:  false,
81
-		},
82
-	}
83
-
84
-	for _, tc := range testCases {
85
-		ret, err := isValidatingWebhookConfiguration(tc.path)
86
-		if tc.shouldFail {
87
-			if err == nil {
88
-				t.Errorf("should have failed at isValidatingWebhookConfiguration()")
89
-			} else {
90
-				// Should fail, skip the current case.
91
-				continue
92
-			}
93
-		} else if err != nil {
94
-			t.Errorf("failed at isValidatingWebhookConfiguration(): %v", err)
95
-		}
96
-
97
-		if tc.expectRet != ret {
98
-			t.Error("the return value is unexpected")
99
-		}
100
-	}
101
-}

+ 0
- 28
security/cmd/chiron/test-data/example-mutating-webhook-config.yaml View File

@@ -1,28 +0,0 @@
1
-apiVersion: admissionregistration.k8s.io/v1beta1
2
-kind: MutatingWebhookConfiguration
3
-metadata:
4
-  labels:
5
-    app: protomutateWebhook
6
-  name: protomutate
7
-webhooks:
8
-- clientConfig:
9
-    caBundle: ""
10
-    service:
11
-      name: protomutate
12
-      namespace: istio-system
13
-      path: /inject
14
-  failurePolicy: Fail
15
-  name: protomutate.istio.io
16
-  namespaceSelector:
17
-    matchLabels:
18
-      protomutate-injection: enabled
19
-  rules:
20
-  - apiGroups:
21
-    - ""
22
-    apiVersions:
23
-    - v1
24
-    operations:
25
-    - CREATE
26
-    resources:
27
-    - pods
28
-  sideEffects: Unknown

+ 0
- 28
security/cmd/chiron/test-data/example-validating-webhook-config.yaml View File

@@ -1,28 +0,0 @@
1
-apiVersion: admissionregistration.k8s.io/v1beta1
2
-kind: ValidatingWebhookConfiguration
3
-metadata:
4
-  labels:
5
-    app: protovalidateWebhook
6
-  name: protovalidate
7
-webhooks:
8
-- clientConfig:
9
-    caBundle: ""
10
-    service:
11
-      name: protovalidate
12
-      namespace: istio-system
13
-      path: /validate
14
-  failurePolicy: Fail
15
-  name: protovalidate.istio.io
16
-  namespaceSelector:
17
-    matchLabels:
18
-      protovalidate-validation: enabled
19
-  rules:
20
-  - apiGroups:
21
-    - ""
22
-    apiVersions:
23
-    - v1
24
-    operations:
25
-    - CREATE
26
-    resources:
27
-    - pods
28
-  sideEffects: Unknown

+ 234
- 190
security/pkg/k8s/chiron/controller.go View File

@@ -15,14 +15,15 @@
15 15
 package chiron
16 16
 
17 17
 import (
18
+	"bytes"
19
+	"crypto/x509"
20
+	"encoding/pem"
18 21
 	"fmt"
19
-	"path/filepath"
20 22
 	"sync"
21 23
 	"time"
22 24
 
23
-	"github.com/howeyc/fsnotify"
24
-	"k8s.io/api/admissionregistration/v1beta1"
25 25
 	v1 "k8s.io/api/core/v1"
26
+	"k8s.io/apimachinery/pkg/api/errors"
26 27
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
27 28
 	"k8s.io/apimachinery/pkg/fields"
28 29
 	"k8s.io/apimachinery/pkg/runtime"
@@ -34,6 +35,8 @@ import (
34 35
 	"k8s.io/client-go/tools/cache"
35 36
 
36 37
 	"istio.io/istio/security/pkg/listwatch"
38
+	"istio.io/istio/security/pkg/pki/ca"
39
+	"istio.io/istio/security/pkg/pki/util"
37 40
 	"istio.io/pkg/log"
38 41
 )
39 42
 
@@ -50,10 +53,7 @@ const (
50 53
 	prefixWebhookSecretName = "istio.webhook"
51 54
 
52 55
 	// The Istio webhook secret annotation type
53
-	IstioSecretType = "istio.io/webhook-key-and-cert"
54
-
55
-	// The ID/name for the private key file.
56
-	PrivateKeyID = "key.pem"
56
+	IstioWebhookSecretType = "istio.io/webhook-key-and-cert"
57 57
 
58 58
 	// For debugging, set the resync period to be a shorter period.
59 59
 	secretResyncPeriod = 10 * time.Second
@@ -61,26 +61,25 @@ const (
61 61
 
62 62
 	recommendedMinGracePeriodRatio = 0.2
63 63
 	recommendedMaxGracePeriodRatio = 0.8
64
+
65
+	// The size of a private key for a leaf certificate.
66
+	keySize = 2048
67
+
68
+	// The number of retries when requesting to create secret.
69
+	secretCreationRetry = 3
70
+
71
+	// The interval for reading a certificate
72
+	certReadInterval = 500 * time.Millisecond
73
+	// The number of tries for reading a certificate
74
+	maxNumCertRead = 20
64 75
 )
65 76
 
66 77
 // WebhookController manages the service accounts' secrets that contains Istio keys and certificates.
67 78
 type WebhookController struct {
68
-	// The file paths of MutatingWebhookConfiguration
69
-	mutatingWebhookConfigFiles []string
70
-	// The names of MutatingWebhookConfiguration
71
-	mutatingWebhookConfigNames []string
72 79
 	// The names of the services of mutating webhooks
73 80
 	mutatingWebhookServiceNames []string
74
-	// The ports of the services of mutating webhooks
75
-	mutatingWebhookServicePorts []int
76
-	// The file paths of ValidatingWebhookConfiguration
77
-	validatingWebhookConfigFiles []string
78
-	// The names of ValidatingWebhookConfiguration
79
-	validatingWebhookConfigNames []string
80 81
 	// The names of the services of validating webhooks
81 82
 	validatingWebhookServiceNames []string
82
-	// The ports of the services of validating webhooks
83
-	validatingWebhookServicePorts []int
84 83
 
85 84
 	// Current CA certificate
86 85
 	CACert     []byte
@@ -95,29 +94,17 @@ type WebhookController struct {
95 94
 	// The namespace of the webhook certificates
96 95
 	namespace      string
97 96
 	minGracePeriod time.Duration
98
-	// The configuration of mutating webhook
99
-	mutatingWebhookConfig *v1beta1.MutatingWebhookConfiguration
100
-	// The configuration of validating webhook
101
-	validatingWebhookConfig *v1beta1.ValidatingWebhookConfiguration
102
-	// Watcher for the k8s CA cert file
103
-	K8sCaCertWatcher *fsnotify.Watcher
104
-	// Watcher for the mutatingwebhook config file
105
-	MutatingWebhookFileWatcher *fsnotify.Watcher
106
-	// Watcher for the validatingwebhook config file
107
-	ValidatingWebhookFileWatcher *fsnotify.Watcher
108
-	certMutex                    sync.RWMutex
97
+	certMutex      sync.RWMutex
109 98
 	// Length of the grace period for the certificate rotation.
110
-	gracePeriodRatio                  float32
111
-	deleteWebhookConfigurationsOnExit bool
99
+	gracePeriodRatio float32
112 100
 }
113 101
 
114 102
 // NewWebhookController returns a pointer to a newly constructed WebhookController instance.
115
-func NewWebhookController(deleteWebhookConfigurationsOnExit bool, gracePeriodRatio float32, minGracePeriod time.Duration,
103
+func NewWebhookController(gracePeriodRatio float32, minGracePeriod time.Duration,
116 104
 	core corev1.CoreV1Interface, admission admissionv1.AdmissionregistrationV1beta1Interface,
117 105
 	certClient certclient.CertificatesV1beta1Interface, k8sCaCertFile, nameSpace string,
118
-	mutatingWebhookConfigFiles, mutatingWebhookConfigNames, mutatingWebhookServiceNames []string,
119
-	mutatingWebhookServicePorts []int, validatingWebhookConfigFiles, validatingWebhookConfigNames,
120
-	validatingWebhookServiceNames []string, validatingWebhookServicePorts []int) (*WebhookController, error) {
106
+	mutatingWebhookServiceNames []string,
107
+	validatingWebhookServiceNames []string) (*WebhookController, error) {
121 108
 	if gracePeriodRatio < 0 || gracePeriodRatio > 1 {
122 109
 		return nil, fmt.Errorf("grace period ratio %f should be within [0, 1]", gracePeriodRatio)
123 110
 	}
@@ -127,25 +114,18 @@ func NewWebhookController(deleteWebhookConfigurationsOnExit bool, gracePeriodRat
127 114
 	}
128 115
 
129 116
 	c := &WebhookController{
130
-		deleteWebhookConfigurationsOnExit: deleteWebhookConfigurationsOnExit,
131
-		gracePeriodRatio:                  gracePeriodRatio,
132
-		minGracePeriod:                    minGracePeriod,
133
-		k8sCaCertFile:                     k8sCaCertFile,
134
-		core:                              core,
135
-		admission:                         admission,
136
-		certClient:                        certClient,
137
-		namespace:                         nameSpace,
138
-		mutatingWebhookConfigFiles:        mutatingWebhookConfigFiles,
139
-		mutatingWebhookConfigNames:        mutatingWebhookConfigNames,
140
-		mutatingWebhookServiceNames:       mutatingWebhookServiceNames,
141
-		mutatingWebhookServicePorts:       mutatingWebhookServicePorts,
142
-		validatingWebhookConfigFiles:      validatingWebhookConfigFiles,
143
-		validatingWebhookConfigNames:      validatingWebhookConfigNames,
144
-		validatingWebhookServiceNames:     validatingWebhookServiceNames,
145
-		validatingWebhookServicePorts:     validatingWebhookServicePorts,
117
+		gracePeriodRatio:              gracePeriodRatio,
118
+		minGracePeriod:                minGracePeriod,
119
+		k8sCaCertFile:                 k8sCaCertFile,
120
+		core:                          core,
121
+		admission:                     admission,
122
+		certClient:                    certClient,
123
+		namespace:                     nameSpace,
124
+		mutatingWebhookServiceNames:   mutatingWebhookServiceNames,
125
+		validatingWebhookServiceNames: validatingWebhookServiceNames,
146 126
 	}
147 127
 
148
-	// read CA cert at the beginning of launching the controller and when the CA cert changes.
128
+	// read CA cert at the beginning of launching the controller.
149 129
 	_, err := reloadCACert(c)
150 130
 	if err != nil {
151 131
 		return nil, err
@@ -153,7 +133,7 @@ func NewWebhookController(deleteWebhookConfigurationsOnExit bool, gracePeriodRat
153 133
 
154 134
 	namespaces := []string{nameSpace}
155 135
 
156
-	istioSecretSelector := fields.SelectorFromSet(map[string]string{"type": IstioSecretType}).String()
136
+	istioSecretSelector := fields.SelectorFromSet(map[string]string{"type": IstioWebhookSecretType}).String()
157 137
 	scrtLW := listwatch.MultiNamespaceListerWatcher(namespaces, func(namespace string) cache.ListerWatcher {
158 138
 		return &cache.ListWatch{
159 139
 			ListFunc: func(options metav1.ListOptions) (runtime.Object, error) {
@@ -173,26 +153,6 @@ func NewWebhookController(deleteWebhookConfigurationsOnExit bool, gracePeriodRat
173 153
 			UpdateFunc: c.scrtUpdated,
174 154
 		})
175 155
 
176
-	watchers := []**fsnotify.Watcher{&c.K8sCaCertWatcher, &c.MutatingWebhookFileWatcher, &c.ValidatingWebhookFileWatcher}
177
-	// Create a watcher such that when the file changes, the event is detected.
178
-	// Each watcher corresponds to a file.
179
-	// Watch the parent directory of the target files so we can catch
180
-	// symlink updates of k8s ConfigMaps volumes.
181
-	// The files watched include the CA certificate file and the webhookconfiguration files,
182
-	// which are ConfigMap file mounts.
183
-	// In the prototype, only the first webhookconfiguration is watched.
184
-	files := []string{k8sCaCertFile, mutatingWebhookConfigFiles[0], validatingWebhookConfigFiles[0]}
185
-	for i := range watchers {
186
-		*watchers[i], err = fsnotify.NewWatcher()
187
-		if err != nil {
188
-			return nil, err
189
-		}
190
-		watchDir, _ := filepath.Split(files[i])
191
-		if err := (*watchers[i]).Watch(watchDir); err != nil {
192
-			return nil, fmt.Errorf("could not watch %v: %v", files[i], err)
193
-		}
194
-	}
195
-
196 156
 	return c, nil
197 157
 }
198 158
 
@@ -200,165 +160,249 @@ func NewWebhookController(deleteWebhookConfigurationsOnExit bool, gracePeriodRat
200 160
 func (wc *WebhookController) Run(stopCh chan struct{}) {
201 161
 	// Create secrets containing certificates for webhooks
202 162
 	for _, svcName := range wc.mutatingWebhookServiceNames {
203
-		wc.upsertSecret(wc.getWebhookSecretNameFromSvcname(svcName), wc.namespace)
163
+		err := wc.upsertSecret(wc.getWebhookSecretNameFromSvcName(svcName), wc.namespace)
164
+		if err != nil {
165
+			log.Errorf("error when upserting svc (%v) in ns (%v): %v", svcName, wc.namespace, err)
166
+		}
204 167
 	}
205 168
 	for _, svcName := range wc.validatingWebhookServiceNames {
206
-		wc.upsertSecret(wc.getWebhookSecretNameFromSvcname(svcName), wc.namespace)
207
-	}
208
-
209
-	// Currently, Chiron only patches one mutating webhook and one validating webhook.
210
-	var mutatingWebhookChangedCh chan struct{}
211
-	// Delete the existing webhookconfiguration, if any.
212
-	err := wc.deleteMutatingWebhookConfig(wc.mutatingWebhookConfigNames[0])
213
-	if err != nil {
214
-		log.Infof("deleting mutating webhook config %v returns: %v",
215
-			wc.mutatingWebhookConfigNames[0], err)
216
-	}
217
-	hostMutate := fmt.Sprintf("%s.%s", wc.mutatingWebhookServiceNames[0], wc.namespace)
218
-	go wc.checkAndCreateMutatingWebhook(hostMutate, wc.mutatingWebhookServicePorts[0], stopCh)
219
-	// Only the first mutatingWebhookConfigNames is supported
220
-	mutatingWebhookChangedCh = wc.monitorMutatingWebhookConfig(wc.mutatingWebhookConfigNames[0], stopCh)
221
-
222
-	var validatingWebhookChangedCh chan struct{}
223
-	// Delete the existing webhookconfiguration, if any.
224
-	err = wc.deleteValidatingWebhookConfig(wc.validatingWebhookConfigNames[0])
225
-	if err != nil {
226
-		log.Infof("deleting validatingwebhook config %v returns: %v",
227
-			wc.validatingWebhookConfigNames[0], err)
169
+		err := wc.upsertSecret(wc.getWebhookSecretNameFromSvcName(svcName), wc.namespace)
170
+		if err != nil {
171
+			log.Errorf("error when upserting svc (%v) in ns (%v): %v", svcName, wc.namespace, err)
172
+		}
228 173
 	}
229 174
 
230
-	hostValidate := fmt.Sprintf("%s.%s", wc.validatingWebhookServiceNames[0], wc.namespace)
231
-	go wc.checkAndCreateValidatingWebhook(hostValidate, wc.validatingWebhookServicePorts[0], stopCh)
232
-	// Only the first validatingWebhookConfigNames is supported
233
-	validatingWebhookChangedCh = wc.monitorValidatingWebhookConfig(wc.validatingWebhookConfigNames[0], stopCh)
234
-
235 175
 	// Manage the secrets of webhooks
236 176
 	go wc.scrtController.Run(stopCh)
237 177
 
238 178
 	// upsertSecret to update and insert secret
239
-	// it throws error if the secret cache is not synchronized, but the secret exists in the system
179
+	// it throws error if the secret cache is not synchronized, but the secret exists in the system.
180
+	// Hence waiting for the cache is synced.
240 181
 	cache.WaitForCacheSync(stopCh, wc.scrtController.HasSynced)
241
-
242
-	// Watch for the CA certificate and webhookconfiguration updates
243
-	go wc.watchConfigChanges(mutatingWebhookChangedCh, validatingWebhookChangedCh, stopCh)
244 182
 }
245 183
 
246
-func (wc *WebhookController) upsertSecret(secretName, secretNamespace string) {
247
-	// TODO (lei-tang): add the implementation of this function.
184
+func (wc *WebhookController) upsertSecret(secretName, secretNamespace string) error {
185
+	secret := &v1.Secret{
186
+		Data: map[string][]byte{},
187
+		ObjectMeta: metav1.ObjectMeta{
188
+			Annotations: nil,
189
+			Name:        secretName,
190
+			Namespace:   secretNamespace,
191
+		},
192
+		Type: IstioWebhookSecretType,
193
+	}
194
+
195
+	existingSecret, err := wc.core.Secrets(secretNamespace).Get(secretName, metav1.GetOptions{})
196
+	if err == nil && existingSecret != nil {
197
+		log.Debugf("upsertSecret(): the secret (%v) in namespace (%v) exists, return",
198
+			secretName, secretNamespace)
199
+		// Do nothing for existing secrets. Rotating expiring certs are handled by the `scrtUpdated` method.
200
+		return nil
201
+	}
202
+
203
+	svcName, found := wc.getServiceName(secretName)
204
+	if !found {
205
+		log.Errorf("failed to find the service name for the secret (%v) to insert", secretName)
206
+		return fmt.Errorf("failed to find the service name for the secret (%v) to insert", secretName)
207
+	}
208
+
209
+	// Now we know the secret does not exist yet. So we create a new one.
210
+	chain, key, caCert, err := genKeyCertK8sCA(wc, secretName, secretNamespace, svcName)
211
+	if err != nil {
212
+		log.Errorf("failed to generate key and certificate for secret %v in namespace %v (error %v)",
213
+			secretName, secretNamespace, err)
214
+		return err
215
+	}
216
+	secret.Data = map[string][]byte{
217
+		ca.CertChainID:  chain,
218
+		ca.PrivateKeyID: key,
219
+		ca.RootCertID:   caCert,
220
+	}
221
+
222
+	// We retry several times when create secret to mitigate transient network failures.
223
+	for i := 0; i < secretCreationRetry; i++ {
224
+		_, err = wc.core.Secrets(secretNamespace).Create(secret)
225
+		if err == nil || errors.IsAlreadyExists(err) {
226
+			if errors.IsAlreadyExists(err) {
227
+				log.Infof("Istio secret \"%s\" in namespace \"%s\" already exists", secretName, secretNamespace)
228
+			}
229
+			break
230
+		} else {
231
+			log.Warnf("failed to create secret in attempt %v/%v, (error: %s)", i+1, secretCreationRetry, err)
232
+		}
233
+		time.Sleep(time.Second)
234
+	}
235
+
236
+	if err != nil && !errors.IsAlreadyExists(err) {
237
+		log.Errorf("failed to create secret \"%s\" in namespace \"%s\" (error: %s), retries %v times",
238
+			secretName, secretNamespace, err, secretCreationRetry)
239
+		return err
240
+	}
241
+
242
+	log.Infof("Istio secret \"%s\" in namespace \"%s\" has been created", secretName, secretNamespace)
243
+	return nil
248 244
 }
249 245
 
250 246
 func (wc *WebhookController) scrtDeleted(obj interface{}) {
251
-	// TODO (lei-tang): add the implementation of this function.
247
+	log.Debugf("enter WebhookController.scrtDeleted()")
248
+	scrt, ok := obj.(*v1.Secret)
249
+	if !ok {
250
+		log.Warnf("Failed to convert to secret object: %v", obj)
251
+		return
252
+	}
253
+
254
+	scrtName := scrt.Name
255
+	if wc.isWebhookSecret(scrtName, scrt.GetNamespace()) {
256
+		log.Infof("Re-create deleted Istio secret %s in namespace %s", scrtName, scrt.GetNamespace())
257
+		err := wc.upsertSecret(scrtName, scrt.GetNamespace())
258
+		if err != nil {
259
+			log.Errorf("Re-create deleted Istio secret %s in namespace %s failed: %v",
260
+				scrtName, scrt.GetNamespace(), err)
261
+		}
262
+	}
252 263
 }
253 264
 
254 265
 // scrtUpdated() is the callback function for update event. It handles
255 266
 // the certificate rotations.
256 267
 func (wc *WebhookController) scrtUpdated(oldObj, newObj interface{}) {
257
-	// TODO (lei-tang): add the implementation of this function.
258
-}
268
+	log.Debugf("enter WebhookController.scrtUpdated()")
269
+	scrt, ok := newObj.(*v1.Secret)
270
+	if !ok {
271
+		log.Warnf("failed to convert to secret object: %v", newObj)
272
+		return
273
+	}
274
+	namespace := scrt.GetNamespace()
275
+	name := scrt.GetName()
276
+	// Only handle webhook secret update events
277
+	if !wc.isWebhookSecret(name, namespace) {
278
+		return
279
+	}
259 280
 
260
-func (wc *WebhookController) watchConfigChanges(mutatingWebhookChangedCh, validatingWebhookChangedCh,
261
-	stopCh chan struct{}) {
262
-	// TODO (lei-tang): add the implementation of this function.
263
-}
281
+	certBytes := scrt.Data[ca.CertChainID]
282
+	cert, err := util.ParsePemEncodedCertificate(certBytes)
283
+	if err != nil {
284
+		log.Warnf("failed to parse certificates in secret %s/%s (error: %v), refreshing the secret.",
285
+			namespace, name, err)
286
+		if err = wc.refreshSecret(scrt); err != nil {
287
+			log.Errora(err)
288
+		}
264 289
 
265
-// Delete the mutatingwebhookconfiguration.
266
-func (wc *WebhookController) deleteMutatingWebhookConfig(configName string) error {
267
-	// TODO (lei-tang): add the implementation of this function.
268
-	return nil
269
-}
290
+		return
291
+	}
270 292
 
271
-// Delete the validatingwebhookconfiguration.
272
-func (wc *WebhookController) deleteValidatingWebhookConfig(configName string) error {
273
-	// TODO (lei-tang): add the implementation of this function.
274
-	return nil
275
-}
293
+	certLifeTimeLeft := time.Until(cert.NotAfter)
294
+	certLifeTime := cert.NotAfter.Sub(cert.NotBefore)
295
+	// Because time.Duration only takes int type, multiply gracePeriodRatio by 1000 and then divide it.
296
+	gracePeriod := time.Duration(wc.gracePeriodRatio*1000) * certLifeTime / 1000
297
+	if gracePeriod < wc.minGracePeriod {
298
+		log.Warnf("gracePeriod (%v * %f) = %v is less than minGracePeriod %v. Apply minGracePeriod.",
299
+			certLifeTime, wc.gracePeriodRatio, gracePeriod, wc.minGracePeriod)
300
+		gracePeriod = wc.minGracePeriod
301
+	}
276 302
 
277
-func (wc *WebhookController) checkAndCreateMutatingWebhook(host string, port int, stopCh chan struct{}) {
278
-	// Check the webhook service status. Only configure webhook if the webhook service is available.
279
-	for {
280
-		if isTCPReachable(host, port) {
281
-			log.Info("the webhook service is reachable")
282
-			break
283
-		}
284
-		select {
285
-		case <-stopCh:
286
-			log.Debugf("webhook controlller is stopped")
287
-			return
288
-		default:
289
-			log.Debugf("the webhook service at (%v, %v) is unreachable, check again later ...", host, port)
290
-			time.Sleep(2 * time.Second)
291
-		}
303
+	// Refresh the secret if 1) the certificate contained in the secret is about
304
+	// to expire, or 2) the root certificate in the secret is different than the
305
+	// one held by the CA (this may happen when the CA is restarted and
306
+	// a new self-signed CA cert is generated).
307
+	// The secret will be periodically inspected, so an update to the CA certificate
308
+	// will eventually lead to the update of workload certificates.
309
+	caCert, err := wc.getCACert()
310
+	if err != nil {
311
+		log.Errorf("failed to get CA certificate: %v", err)
312
+		return
292 313
 	}
314
+	if certLifeTimeLeft < gracePeriod || !bytes.Equal(caCert, scrt.Data[ca.RootCertID]) {
315
+		log.Infof("refreshing secret %s/%s, either the leaf certificate is about to expire "+
316
+			"or the root certificate is outdated", namespace, name)
293 317
 
294
-	// Try to create the initial webhook configuration.
295
-	err := wc.rebuildMutatingWebhookConfig()
296
-	if err == nil {
297
-		createErr := createOrUpdateMutatingWebhookConfig(wc)
298
-		if createErr != nil {
299
-			log.Errorf("error when creating or updating muatingwebhookconfiguration: %v", createErr)
300
-			return
318
+		if err = wc.refreshSecret(scrt); err != nil {
319
+			log.Errorf("failed to update secret %s/%s (error: %s)", namespace, name, err)
301 320
 		}
302
-	} else {
303
-		log.Errorf("error when rebuilding mutatingwebhookconfiguration: %v", err)
304 321
 	}
305 322
 }
306 323
 
307
-func (wc *WebhookController) checkAndCreateValidatingWebhook(host string, port int, stopCh chan struct{}) {
308
-	// Check the webhook service status. Only configure webhook if the webhook service is available.
309
-	for {
310
-		if isTCPReachable(host, port) {
311
-			log.Info("the webhook service is reachable")
312
-			break
313
-		}
314
-		select {
315
-		case <-stopCh:
316
-			log.Debugf("webhook controlller is stopped")
317
-			return
318
-		default:
319
-			log.Debugf("the webhook service is unreachable, check again later ...")
320
-			time.Sleep(2 * time.Second)
321
-		}
324
+// refreshSecret is an inner func to refresh cert secrets when necessary
325
+func (wc *WebhookController) refreshSecret(scrt *v1.Secret) error {
326
+	namespace := scrt.GetNamespace()
327
+	scrtName := scrt.Name
328
+
329
+	svcName, found := wc.getServiceName(scrtName)
330
+	if !found {
331
+		return fmt.Errorf("failed to find the service name for the secret (%v) to refresh", scrtName)
322 332
 	}
323 333
 
324
-	// Try to create the initial webhook configuration.
325
-	err := wc.rebuildValidatingWebhookConfig()
326
-	if err == nil {
327
-		createErr := createOrUpdateValidatingWebhookConfig(wc)
328
-		if createErr != nil {
329
-			log.Errorf("error when creating or updating validatingwebhookconfiguration: %v", createErr)
330
-			return
331
-		}
332
-	} else {
333
-		log.Errorf("error when rebuilding validatingwebhookconfiguration: %v", err)
334
+	chain, key, caCert, err := genKeyCertK8sCA(wc, scrtName, namespace, svcName)
335
+	if err != nil {
336
+		return err
334 337
 	}
338
+
339
+	scrt.Data[ca.CertChainID] = chain
340
+	scrt.Data[ca.PrivateKeyID] = key
341
+	scrt.Data[ca.RootCertID] = caCert
342
+
343
+	_, err = wc.core.Secrets(namespace).Update(scrt)
344
+	return err
335 345
 }
336 346
 
337
-// Run an informer that watches the changes of mutatingwebhookconfiguration.
338
-func (wc *WebhookController) monitorMutatingWebhookConfig(webhookConfigName string, stopC <-chan struct{}) chan struct{} {
339
-	// TODO (lei-tang): add the implementation of this function.
347
+// Clean up the CSR
348
+func (wc *WebhookController) cleanUpCertGen(csrName string) error {
349
+	// Delete CSR
350
+	log.Debugf("delete CSR: %v", csrName)
351
+	err := wc.certClient.CertificateSigningRequests().Delete(csrName, nil)
352
+	if err != nil {
353
+		log.Errorf("failed to delete CSR (%v): %v", csrName, err)
354
+		return err
355
+	}
340 356
 	return nil
341 357
 }
342 358
 
343
-// Run an informer that watches the changes of validatingwebhookconfiguration.
344
-func (wc *WebhookController) monitorValidatingWebhookConfig(webhookConfigName string, stopC <-chan struct{}) chan struct{} {
345
-	// TODO (lei-tang): add the implementation of this function.
346
-	return nil
359
+// Return whether the input secret name is a Webhook secret
360
+func (wc *WebhookController) isWebhookSecret(name, namespace string) bool {
361
+	for _, n := range wc.mutatingWebhookServiceNames {
362
+		if name == wc.getWebhookSecretNameFromSvcName(n) && namespace == wc.namespace {
363
+			return true
364
+		}
365
+	}
366
+	for _, n := range wc.validatingWebhookServiceNames {
367
+		if name == wc.getWebhookSecretNameFromSvcName(n) && namespace == wc.namespace {
368
+			return true
369
+		}
370
+	}
371
+	return false
347 372
 }
348 373
 
349
-// Rebuild the mutatingwebhookconfiguration and save it for subsequent calls to createOrUpdateWebhookConfig.
350
-func (wc *WebhookController) rebuildMutatingWebhookConfig() error {
351
-	// TODO (lei-tang): add the implementation of this function.
352
-	return nil
374
+// Get the CA cert. K8sCaCertWatcher handles the update of CA cert.
375
+func (wc *WebhookController) getCACert() ([]byte, error) {
376
+	wc.certMutex.Lock()
377
+	cp := append([]byte(nil), wc.CACert...)
378
+	wc.certMutex.Unlock()
379
+
380
+	block, _ := pem.Decode(cp)
381
+	if block == nil {
382
+		return nil, fmt.Errorf("invalid PEM encoded CA certificate")
383
+	}
384
+	if _, err := x509.ParseCertificate(block.Bytes); err != nil {
385
+		return nil, fmt.Errorf("invalid ca certificate (%v), parsing error: %v", string(cp), err)
386
+	}
387
+	return cp, nil
353 388
 }
354 389
 
355
-// Rebuild the validatingwebhookconfiguration and save it for subsequent calls to createOrUpdateWebhookConfig.
356
-func (wc *WebhookController) rebuildValidatingWebhookConfig() error {
357
-	// TODO (lei-tang): add the implementation of this function.
358
-	return nil
390
+// Get the service name for the secret. Return the service name and whether it is found.
391
+func (wc *WebhookController) getServiceName(secretName string) (string, bool) {
392
+	for _, name := range wc.mutatingWebhookServiceNames {
393
+		if wc.getWebhookSecretNameFromSvcName(name) == secretName {
394
+			return name, true
395
+		}
396
+	}
397
+	for _, name := range wc.validatingWebhookServiceNames {
398
+		if wc.getWebhookSecretNameFromSvcName(name) == secretName {
399
+			return name, true
400
+		}
401
+	}
402
+	return "", false
359 403
 }
360 404
 
361 405
 // Return the webhook secret name based on the service name
362
-func (wc *WebhookController) getWebhookSecretNameFromSvcname(svcName string) string {
406
+func (wc *WebhookController) getWebhookSecretNameFromSvcName(svcName string) string {
363 407
 	return fmt.Sprintf("%s.%s", prefixWebhookSecretName, svcName)
364 408
 }

+ 746
- 45
security/pkg/k8s/chiron/controller_test.go View File

@@ -15,76 +15,123 @@
15 15
 package chiron
16 16
 
17 17
 import (
18
+	"bytes"
19
+	"reflect"
18 20
 	"testing"
19 21
 	"time"
20 22
 
23
+	"istio.io/istio/security/pkg/pki/ca"
24
+
25
+	v1 "k8s.io/api/core/v1"
26
+
27
+	"istio.io/istio/security/pkg/pki/util"
28
+
29
+	cert "k8s.io/api/certificates/v1beta1"
30
+
31
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
32
+
21 33
 	"k8s.io/client-go/kubernetes/fake"
22 34
 )
23 35
 
24
-func TestNewWebhookController(t *testing.T) {
25
-	client := fake.NewSimpleClientset()
26
-	mutatingWebhookConfigFiles := []string{"./test-data/empty-webhook-config.yaml"}
27
-	validatingWebhookConfigFiles := []string{"./test-data/empty-webhook-config.yaml"}
36
+const (
37
+	exampleExpiredCert = `-----BEGIN CERTIFICATE-----
38
+MIIDXjCCAkagAwIBAgIQGbJDoVfdXBsPos+p8RGqZDANBgkqhkiG9w0BAQsFADBF
39
+MQswCQYDVQQGEwJBVTETMBEGA1UECAwKU29tZS1TdGF0ZTEhMB8GA1UECgwYSW50
40
+ZXJuZXQgV2lkZ2l0cyBQdHkgTHRkMB4XDTE5MDgxNjIxNDQzNVoXDTE5MDgxNjIx
41
+NDQzNlowEzERMA8GA1UEChMISnVqdSBvcmcwggEiMA0GCSqGSIb3DQEBAQUAA4IB
42
+DwAwggEKAoIBAQDYwh5Txh3pJVIjCt5JciYM4GMt3DsjF7E4JdVUUu682YpFevT8
43
+rWJVaZjZajVQaT4IIYw+kxqf0hdVLJG11OHI3OeZ1IJe5yuV+STXks/+vEDMMLuD
44
+vqWZl7oFIuXR6merPAPLAmxo0U5E9kp6ftfHJMK3uj1eNp/BZE/xH8QYe86kAckd
45
+QYPsz0gW1YMdpxRG1OFmbih8CdbRUjCgHHPOxbAJOIDM4xtj8M1rFgVnyH+8NucW
46
+DddKy63GASUphakC73hMnoEQksbVg6rdYlnYrdPcLgmcLeO+vdI5EjbXMaXy7GMk
47
+JvTJI5KRAq+jPHOZmWHd2zAGUpLRPr8EFQp3AgMBAAGjfDB6MA4GA1UdDwEB/wQE
48
+AwIFoDAMBgNVHRMBAf8EAjAAMB8GA1UdIwQYMBaAFKgUbNBCMgiPmanahMYt1KRO
49
+dFX9MDkGA1UdEQEB/wQvMC2GK3NwaWZmZTovL2NsdXN0ZXIubG9jYWwvbnMvZGVm
50
+YXVsdC9zYS9jbGllbnQwDQYJKoZIhvcNAQELBQADggEBAFEf0ZJnNz7h4MqGz720
51
+0FShDX02WPmNSfl89gY/Q4/djTF5yfqgaDAZWf4PsDwpdMpT0eRrshpzvLrRjcd4
52
+Ev6bXGncIwnNFtXQ4dseup+kmaYZF24zpRjyoH9owwu1T5Wb1cSrpFFby5xWuoTC
53
+bsKlR5CZF+dHUwc1iMaj/4kuVjvt4imM0coeaOUzOcMCruO54IqsFcJg2YA80MI+
54
+6UiM8hj8ERDls5iBNThWKKE0yva4HFh1gj5f427NP7CSikUFXs61gytlFHEjHyco
55
+lK8KK65mLIDLshz2+6lPHFXv9tEpouEUws5lhR5O9Q+9LmfBLPE7rD2aUic8EApg
56
+3TE=
57
+-----END CERTIFICATE-----`
58
+	// The example certificate here can be generated through
59
+	// the following command:
60
+	// kubectl exec -it POD-NAME -n NAMESPACE -- cat /var/run/secrets/kubernetes.io/serviceaccount/ca.crt
61
+	// Its validity is 5 years.
62
+	exampleCACert1 = `-----BEGIN CERTIFICATE-----
63
+MIIDCzCCAfOgAwIBAgIQbfOzhcKTldFipQ1X2WXpHDANBgkqhkiG9w0BAQsFADAv
64
+MS0wKwYDVQQDEyRhNzU5YzcyZC1lNjcyLTQwMzYtYWMzYy1kYzAxMDBmMTVkNWUw
65
+HhcNMTkwNTE2MjIxMTI2WhcNMjQwNTE0MjMxMTI2WjAvMS0wKwYDVQQDEyRhNzU5
66
+YzcyZC1lNjcyLTQwMzYtYWMzYy1kYzAxMDBmMTVkNWUwggEiMA0GCSqGSIb3DQEB
67
+AQUAA4IBDwAwggEKAoIBAQC6sSAN80Ci0DYFpNDumGYoejMQai42g6nSKYS+ekvs
68
+E7uT+eepO74wj8o6nFMNDu58+XgIsvPbWnn+3WtUjJfyiQXxmmTg8om4uY1C7R1H
69
+gMsrL26pUaXZ/lTE8ZV5CnQJ9XilagY4iZKeptuZkxrWgkFBD7tr652EA3hmj+3h
70
+4sTCQ+pBJKG8BJZDNRrCoiABYBMcFLJsaKuGZkJ6KtxhQEO9QxJVaDoSvlCRGa8R
71
+fcVyYQyXOZ+0VHZJQgaLtqGpiQmlFttpCwDiLfMkk3UAd79ovkhN1MCq+O5N7YVt
72
+eVQWaTUqUV2tKUFvVq21Zdl4dRaq+CF5U8uOqLY/4Kg9AgMBAAGjIzAhMA4GA1Ud
73
+DwEB/wQEAwICBDAPBgNVHRMBAf8EBTADAQH/MA0GCSqGSIb3DQEBCwUAA4IBAQCg
74
+oF71Ey2b1QY22C6BXcANF1+wPzxJovFeKYAnUqwh3rF7pIYCS/adZXOKlgDBsbcS
75
+MxAGnCRi1s+A7hMYj3sQAbBXttc31557lRoJrx58IeN5DyshT53t7q4VwCzuCXFT
76
+3zRHVRHQnO6LHgZx1FuKfwtkhfSXDyYU2fQYw2Hcb9krYU/alViVZdE0rENXCClq
77
+xO7AQk5MJcGg6cfE5wWAKU1ATjpK4CN+RTn8v8ODLoI2SW3pfsnXxm93O+pp9HN4
78
++O+1PQtNUWhCfh+g6BN2mYo2OEZ8qGSxDlMZej4YOdVkW8PHmFZTK0w9iJKqM5o1
79
+V6g5gZlqSoRhICK09tpc
80
+-----END CERTIFICATE-----`
81
+	exampleCACert2 = `-----BEGIN CERTIFICATE-----
82
+MIIDDDCCAfSgAwIBAgIRALsN8ND73NbYSKTZZa4jf2EwDQYJKoZIhvcNAQELBQAw
83
+LzEtMCsGA1UEAxMkZTNlM2RlZWQtYzIyNi00OWM2LThmOTktNDU3NmRmMzQ0YWQ1
84
+MB4XDTE5MDYwMTE1NTU0M1oXDTI0MDUzMDE2NTU0M1owLzEtMCsGA1UEAxMkZTNl
85
+M2RlZWQtYzIyNi00OWM2LThmOTktNDU3NmRmMzQ0YWQ1MIIBIjANBgkqhkiG9w0B
86
+AQEFAAOCAQ8AMIIBCgKCAQEA43oeK/hS92ANjmg50LCl3tM7eYAlBB/XgCl+bfp3
87
+KwEf+uW5yEvzSVHd2VPFI/kJJeLFrsyCRaU4FwxWcEr2Ld07DPL34oyZRRXQF0w6
88
+4ZNSVmevBNdZLqHcoIUtR1iFJbkctE93HpGw5Kg1NXRLDu47wQtzcC3GDOEk1amu
89
+mL916R2OcYEeOcyRDnlbLcsTYRvK5WBQsux4E0iu2Eo9GIajKmbxVLxA9fsmqG4i
90
+/HoVkLmCg+ZRPR/66AFLPFV1J3RWp0K4HKGzBeCyd2RC+o0g8tJX3EVSuQpqzS8p
91
+i2t71cYu/Sf5gt3wXsNHyzE6bF1o+acyzWvJlBym/HsbAQIDAQABoyMwITAOBgNV
92
+HQ8BAf8EBAMCAgQwDwYDVR0TAQH/BAUwAwEB/zANBgkqhkiG9w0BAQsFAAOCAQEA
93
+kJXGkFEybCp7RxUSILuIqMtKcYcQU9ulKmLSn51VrpcRHP4SH7UJ0aXMjAdRLsop
94
+em7YgbvToGNingqcmSJlunR3jXDecSXJLUO1xcfw6N+B2BXRgUv8wV42btr2EV6q
95
+4HKou+MnKdrQkMUx218AT8TNPBb/Yx01m8YUS7mGUTApAhBneGEcKJ8xOznIuR5v
96
+CihWQA9AmUvfixpXNpJc4vqiYErwIXrYpuwc79SRtLuO70vV7FCctz+4JPpR7mp9
97
+dHMZfGO1KXMbYT9P5bm+itlWSyrnn0qK/Cn5RHBoFyY91VcQJTgABS/z5O0pZ662
98
+sNzF00Jhi0gU7th75QT3MA==
99
+-----END CERTIFICATE-----`
100
+)
28 101
 
102
+func TestNewWebhookController(t *testing.T) {
29 103
 	testCases := map[string]struct {
30
-		deleteWebhookConfigOnExit     bool
31 104
 		gracePeriodRatio              float32
32 105
 		minGracePeriod                time.Duration
33 106
 		k8sCaCertFile                 string
34 107
 		namespace                     string
35
-		mutatingWebhookConfigFiles    []string
36
-		mutatingWebhookConfigNames    []string
37 108
 		mutatingWebhookServiceNames   []string
38
-		mutatingWebhookServicePorts   []int
39
-		validatingWebhookConfigFiles  []string
40
-		validatingWebhookConfigNames  []string
41 109
 		validatingWebhookServiceNames []string
42
-		validatingWebhookServicePorts []int
43 110
 		shouldFail                    bool
44 111
 	}{
45 112
 		"invalid grade period ratio": {
46
-			gracePeriodRatio:             1.5,
47
-			k8sCaCertFile:                "./test-data/example-invalid-ca-cert.pem",
48
-			mutatingWebhookConfigFiles:   mutatingWebhookConfigFiles,
49
-			validatingWebhookConfigFiles: validatingWebhookConfigFiles,
50
-			shouldFail:                   true,
113
+			gracePeriodRatio: 1.5,
114
+			k8sCaCertFile:    "./test-data/example-invalid-ca-cert.pem",
115
+			shouldFail:       true,
51 116
 		},
52 117
 		"invalid CA cert path": {
53
-			gracePeriodRatio:             0.6,
54
-			k8sCaCertFile:                "./invalid-path/invalid-file",
55
-			mutatingWebhookConfigFiles:   mutatingWebhookConfigFiles,
56
-			validatingWebhookConfigFiles: validatingWebhookConfigFiles,
57
-			shouldFail:                   true,
118
+			gracePeriodRatio: 0.6,
119
+			k8sCaCertFile:    "./invalid-path/invalid-file",
120
+			shouldFail:       true,
58 121
 		},
59 122
 		"valid CA cert path": {
60
-			gracePeriodRatio:             0.6,
61
-			k8sCaCertFile:                "./test-data/example-ca-cert.pem",
62
-			mutatingWebhookConfigFiles:   mutatingWebhookConfigFiles,
63
-			validatingWebhookConfigFiles: validatingWebhookConfigFiles,
64
-			shouldFail:                   false,
65
-		},
66
-		"invalid mutating webhook config file": {
67
-			gracePeriodRatio:             0.6,
68
-			k8sCaCertFile:                "./test-data/example-ca-cert.pem",
69
-			mutatingWebhookConfigFiles:   []string{"./invalid-path/invalid-file"},
70
-			validatingWebhookConfigFiles: validatingWebhookConfigFiles,
71
-			shouldFail:                   true,
72
-		},
73
-		"invalid validatating webhook config file": {
74
-			gracePeriodRatio:             0.6,
75
-			k8sCaCertFile:                "./test-data/example-ca-cert.pem",
76
-			mutatingWebhookConfigFiles:   mutatingWebhookConfigFiles,
77
-			validatingWebhookConfigFiles: []string{"./invalid-path/invalid-file"},
78
-			shouldFail:                   true,
123
+			gracePeriodRatio: 0.6,
124
+			k8sCaCertFile:    "./test-data/example-ca-cert.pem",
125
+			shouldFail:       false,
79 126
 		},
80 127
 	}
81 128
 
82 129
 	for _, tc := range testCases {
83
-		_, err := NewWebhookController(tc.deleteWebhookConfigOnExit, tc.gracePeriodRatio, tc.minGracePeriod,
130
+		client := fake.NewSimpleClientset()
131
+		_, err := NewWebhookController(tc.gracePeriodRatio, tc.minGracePeriod,
84 132
 			client.CoreV1(), client.AdmissionregistrationV1beta1(), client.CertificatesV1beta1(),
85
-			tc.k8sCaCertFile, tc.namespace, tc.mutatingWebhookConfigFiles, tc.mutatingWebhookConfigNames,
86
-			tc.mutatingWebhookServiceNames, tc.mutatingWebhookServicePorts, tc.validatingWebhookConfigFiles,
87
-			tc.validatingWebhookConfigNames, tc.validatingWebhookServiceNames, tc.validatingWebhookServicePorts)
133
+			tc.k8sCaCertFile, tc.namespace, tc.mutatingWebhookServiceNames,
134
+			tc.validatingWebhookServiceNames)
88 135
 		if tc.shouldFail {
89 136
 			if err == nil {
90 137
 				t.Errorf("should have failed at NewWebhookController()")
@@ -93,7 +140,661 @@ func TestNewWebhookController(t *testing.T) {
93 140
 				continue
94 141
 			}
95 142
 		} else if err != nil {
96
-			t.Errorf("failed at NewWebhookController(): %v", err)
143
+			t.Errorf("should not fail at NewWebhookController(), err: %v", err)
144
+		}
145
+	}
146
+}
147
+
148
+func TestUpsertSecret(t *testing.T) {
149
+	mutatingWebhookServiceNames := []string{"foo"}
150
+
151
+	testCases := map[string]struct {
152
+		gracePeriodRatio              float32
153
+		minGracePeriod                time.Duration
154
+		k8sCaCertFile                 string
155
+		namespace                     string
156
+		mutatingWebhookSerivceNames   []string
157
+		validatingWebhookServiceNames []string
158
+		scrtName                      string
159
+		expectFaill                   bool
160
+	}{
161
+		"upsert a valid secret name should succeed": {
162
+			gracePeriodRatio:            0.6,
163
+			k8sCaCertFile:               "./test-data/example-ca-cert.pem",
164
+			namespace:                   "foo.ns",
165
+			mutatingWebhookSerivceNames: mutatingWebhookServiceNames,
166
+			scrtName:                    "istio.webhook.foo",
167
+			expectFaill:                 false,
168
+		},
169
+		"upsert an invalid secret name should fail": {
170
+			gracePeriodRatio:            0.6,
171
+			k8sCaCertFile:               "./test-data/example-ca-cert.pem",
172
+			namespace:                   "bar.ns",
173
+			mutatingWebhookSerivceNames: mutatingWebhookServiceNames,
174
+			scrtName:                    "istio.webhook.bar",
175
+			expectFaill:                 true,
176
+		},
177
+	}
178
+
179
+	for _, tc := range testCases {
180
+		client := fake.NewSimpleClientset()
181
+		csr := &cert.CertificateSigningRequest{
182
+			ObjectMeta: metav1.ObjectMeta{
183
+				Name: "domain-cluster.local-ns--secret-mock-secret",
184
+			},
185
+			Status: cert.CertificateSigningRequestStatus{
186
+				Certificate: []byte(exampleIssuedCert),
187
+			},
188
+		}
189
+		client.PrependReactor("get", "certificatesigningrequests", defaultReactionFunc(csr))
190
+
191
+		wc, err := NewWebhookController(tc.gracePeriodRatio, tc.minGracePeriod,
192
+			client.CoreV1(), client.AdmissionregistrationV1beta1(), client.CertificatesV1beta1(),
193
+			tc.k8sCaCertFile, tc.namespace, tc.mutatingWebhookSerivceNames,
194
+			tc.validatingWebhookServiceNames)
195
+		if err != nil {
196
+			t.Errorf("failed at creating webhook controller: %v", err)
197
+			continue
198
+		}
199
+
200
+		err = wc.upsertSecret(tc.scrtName, tc.namespace)
201
+		if tc.expectFaill {
202
+			if err == nil {
203
+				t.Errorf("should have failed at upsertSecret")
204
+			}
205
+			continue
206
+		} else if err != nil {
207
+			t.Errorf("should not failed at upsertSecret, err: %v", err)
208
+		}
209
+	}
210
+}
211
+
212
+func TestScrtDeleted(t *testing.T) {
213
+	mutatingWebhookServiceNames := []string{"foo"}
214
+
215
+	testCases := map[string]struct {
216
+		gracePeriodRatio              float32
217
+		minGracePeriod                time.Duration
218
+		k8sCaCertFile                 string
219
+		namespace                     string
220
+		mutatingWebhookSerivceNames   []string
221
+		validatingWebhookServiceNames []string
222
+		scrtName                      string
223
+	}{
224
+		"recover a deleted secret should succeed": {
225
+			gracePeriodRatio:            0.6,
226
+			k8sCaCertFile:               "./test-data/example-ca-cert.pem",
227
+			namespace:                   "foo.ns",
228
+			mutatingWebhookSerivceNames: mutatingWebhookServiceNames,
229
+			scrtName:                    "istio.webhook.foo",
230
+		},
231
+	}
232
+
233
+	csr := &cert.CertificateSigningRequest{
234
+		ObjectMeta: metav1.ObjectMeta{
235
+			Name: "domain-cluster.local-ns--secret-mock-secret",
236
+		},
237
+		Status: cert.CertificateSigningRequestStatus{
238
+			Certificate: []byte(exampleIssuedCert),
239
+		},
240
+	}
241
+
242
+	for _, tc := range testCases {
243
+		client := fake.NewSimpleClientset()
244
+		client.PrependReactor("get", "certificatesigningrequests", defaultReactionFunc(csr))
245
+
246
+		wc, err := NewWebhookController(tc.gracePeriodRatio, tc.minGracePeriod,
247
+			client.CoreV1(), client.AdmissionregistrationV1beta1(), client.CertificatesV1beta1(),
248
+			tc.k8sCaCertFile, tc.namespace,
249
+			tc.mutatingWebhookSerivceNames,
250
+			tc.validatingWebhookServiceNames)
251
+		if err != nil {
252
+			t.Errorf("failed at creating webhook controller: %v", err)
253
+			continue
254
+		}
255
+
256
+		_, err = client.CoreV1().Secrets(tc.namespace).Create(&v1.Secret{
257
+			ObjectMeta: metav1.ObjectMeta{
258
+				Name: tc.scrtName,
259
+				Labels: map[string]string{
260
+					"secret": "for-testing",
261
+				},
262
+			},
263
+		})
264
+		if err != nil {
265
+			t.Fatalf("failed creating test secret (%v): %v", tc.scrtName, err)
266
+		}
267
+		scrt, err := client.CoreV1().Secrets(tc.namespace).Get(tc.scrtName, metav1.GetOptions{})
268
+		if err != nil || scrt == nil {
269
+			t.Fatalf("failed to get test secret (%v): err (%v), secret (%v)", tc.scrtName, err, scrt)
270
+		}
271
+		err = client.CoreV1().Secrets(tc.namespace).Delete(tc.scrtName, &metav1.DeleteOptions{})
272
+		if err != nil {
273
+			t.Fatalf("failed deleting test secret (%v): %v", tc.scrtName, err)
274
+		}
275
+		_, err = client.CoreV1().Secrets(tc.namespace).Get(tc.scrtName, metav1.GetOptions{})
276
+		if err == nil {
277
+			t.Fatal("the deleted secret should not exist")
278
+		}
279
+
280
+		// The secret deleted should be recovered.
281
+		wc.scrtDeleted(scrt)
282
+		scrt, err = client.CoreV1().Secrets(tc.namespace).Get(tc.scrtName, metav1.GetOptions{})
283
+		if err != nil || scrt == nil {
284
+			t.Fatalf("after scrtDeleted(), failed to get test secret (%v): err (%v), secret (%v)",
285
+				tc.scrtName, err, scrt)
286
+		}
287
+	}
288
+}
289
+
290
+func TestScrtUpdated(t *testing.T) {
291
+	mutatingWebhookServiceNames := []string{"foo"}
292
+
293
+	testCases := map[string]struct {
294
+		gracePeriodRatio              float32
295
+		minGracePeriod                time.Duration
296
+		k8sCaCertFile                 string
297
+		namespace                     string
298
+		mutatingWebhookSerivceNames   []string
299
+		validatingWebhookServiceNames []string
300
+		scrtName                      string
301
+		changeCACert                  bool
302
+		invalidNewSecret              bool
303
+		replaceWithExpiredCert        bool
304
+		expectUpdate                  bool
305
+		newScrtName                   string
306
+	}{
307
+		"invalid new secret should not affect existing secret": {
308
+			gracePeriodRatio:            0.6,
309
+			k8sCaCertFile:               "./test-data/example-ca-cert.pem",
310
+			namespace:                   "foo.ns",
311
+			mutatingWebhookSerivceNames: mutatingWebhookServiceNames,
312
+			scrtName:                    "istio.webhook.foo",
313
+			invalidNewSecret:            true,
314
+			expectUpdate:                false,
315
+		},
316
+		"non-webhook secret should not be updated": {
317
+			gracePeriodRatio:            0.6,
318
+			k8sCaCertFile:               "./test-data/example-ca-cert.pem",
319
+			namespace:                   "foo.ns",
320
+			mutatingWebhookSerivceNames: mutatingWebhookServiceNames,
321
+			scrtName:                    "istio.webhook.foo",
322
+			newScrtName:                 "bar",
323
+			invalidNewSecret:            false,
324
+			expectUpdate:                false,
325
+		},
326
+		"expired certificate should be updated": {
327
+			gracePeriodRatio:            0.6,
328
+			k8sCaCertFile:               "./test-data/example-ca-cert.pem",
329
+			namespace:                   "foo.ns",
330
+			mutatingWebhookSerivceNames: mutatingWebhookServiceNames,
331
+			scrtName:                    "istio.webhook.foo",
332
+			replaceWithExpiredCert:      true,
333
+			expectUpdate:                true,
334
+		},
335
+		"changing CA certificate should lead to updating secret": {
336
+			gracePeriodRatio:            0.6,
337
+			k8sCaCertFile:               "./test-data/example-ca-cert.pem",
338
+			namespace:                   "foo.ns",
339
+			mutatingWebhookSerivceNames: mutatingWebhookServiceNames,
340
+			scrtName:                    "istio.webhook.foo",
341
+			changeCACert:                true,
342
+			replaceWithExpiredCert:      false,
343
+			expectUpdate:                true,
344
+		},
345
+	}
346
+
347
+	csr := &cert.CertificateSigningRequest{
348
+		ObjectMeta: metav1.ObjectMeta{
349
+			Name: "domain-cluster.local-ns--secret-mock-secret",
350
+		},
351
+		Status: cert.CertificateSigningRequestStatus{
352
+			Certificate: []byte(exampleIssuedCert),
353
+		},
354
+	}
355
+
356
+	for _, tc := range testCases {
357
+		client := fake.NewSimpleClientset()
358
+		client.PrependReactor("get", "certificatesigningrequests", defaultReactionFunc(csr))
359
+
360
+		wc, err := NewWebhookController(tc.gracePeriodRatio, tc.minGracePeriod,
361
+			client.CoreV1(), client.AdmissionregistrationV1beta1(), client.CertificatesV1beta1(),
362
+			tc.k8sCaCertFile, tc.namespace,
363
+			tc.mutatingWebhookSerivceNames,
364
+			tc.validatingWebhookServiceNames)
365
+		if err != nil {
366
+			t.Errorf("failed at creating webhook controller: %v", err)
367
+			continue
368
+		}
369
+
370
+		err = wc.upsertSecret(tc.scrtName, tc.namespace)
371
+		if err != nil {
372
+			t.Errorf("should not failed at upsertSecret, err: %v", err)
373
+		}
374
+		scrt, err := client.CoreV1().Secrets(tc.namespace).Get(tc.scrtName, metav1.GetOptions{})
375
+		if err != nil || scrt == nil {
376
+			t.Fatalf("failed to get test secret (%v): err (%v), secret (%v)", tc.scrtName, err, scrt)
377
+		}
378
+
379
+		if tc.newScrtName != "" {
380
+			scrt.Name = tc.newScrtName
381
+		}
382
+		if tc.replaceWithExpiredCert {
383
+			scrt.Data[ca.CertChainID] = []byte(exampleExpiredCert)
384
+		}
385
+		if tc.changeCACert {
386
+			scrt.Data[ca.RootCertID] = []byte(exampleCACert2)
387
+		}
388
+
389
+		var newScrt interface{}
390
+		if tc.invalidNewSecret {
391
+			// point to an invalid secret object
392
+			newScrt = &v1.ConfigMap{}
393
+		} else {
394
+			newScrt = &v1.Secret{}
395
+			scrt.DeepCopyInto(newScrt.(*v1.Secret))
396
+		}
397
+		wc.scrtUpdated(scrt, newScrt)
398
+
399
+		// scrt2 is the secret after updating, which will be compared against original scrt
400
+		scrt2, err := client.CoreV1().Secrets(tc.namespace).Get(tc.scrtName, metav1.GetOptions{})
401
+		if err != nil || scrt2 == nil {
402
+			t.Fatalf("failed to get test secret (%v): err (%v), secret (%v)", tc.scrtName, err, scrt2)
403
+		}
404
+		if tc.newScrtName != "" {
405
+			scrt2.Name = tc.newScrtName
406
+		}
407
+		if tc.expectUpdate {
408
+			if reflect.DeepEqual(scrt, scrt2) {
409
+				t.Errorf("change is expected while there is no change")
410
+			}
411
+		} else {
412
+			if !reflect.DeepEqual(scrt, scrt2) {
413
+				t.Errorf("change is not expected while there is change")
414
+			}
415
+		}
416
+	}
417
+}
418
+
419
+func TestRefreshSecret(t *testing.T) {
420
+	mutatingWebhookServiceNames := []string{"foo"}
421
+
422
+	testCases := map[string]struct {
423
+		gracePeriodRatio              float32
424
+		minGracePeriod                time.Duration
425
+		k8sCaCertFile                 string
426
+		namespace                     string
427
+		mutatingWebhookSerivceNames   []string
428
+		validatingWebhookServiceNames []string
429
+		scrtName                      string
430
+		changeCACert                  bool
431
+		expectUpdate                  bool
432
+	}{
433
+		"refresh a secret with different CA cert should succeed": {
434
+			gracePeriodRatio:            0.6,
435
+			k8sCaCertFile:               "./test-data/example-ca-cert.pem",
436
+			namespace:                   "foo.ns",
437
+			mutatingWebhookSerivceNames: mutatingWebhookServiceNames,
438
+			scrtName:                    "istio.webhook.foo",
439
+			changeCACert:                true,
440
+			expectUpdate:                true,
441
+		},
442
+	}
443
+
444
+	csr := &cert.CertificateSigningRequest{
445
+		ObjectMeta: metav1.ObjectMeta{
446
+			Name: "domain-cluster.local-ns--secret-mock-secret",
447
+		},
448
+		Status: cert.CertificateSigningRequestStatus{
449
+			Certificate: []byte(exampleIssuedCert),
450
+		},
451
+	}
452
+
453
+	for _, tc := range testCases {
454
+		client := fake.NewSimpleClientset()
455
+		client.PrependReactor("get", "certificatesigningrequests", defaultReactionFunc(csr))
456
+
457
+		wc, err := NewWebhookController(tc.gracePeriodRatio, tc.minGracePeriod,
458
+			client.CoreV1(), client.AdmissionregistrationV1beta1(), client.CertificatesV1beta1(),
459
+			tc.k8sCaCertFile, tc.namespace,
460
+			tc.mutatingWebhookSerivceNames, tc.validatingWebhookServiceNames)
461
+
462
+		if err != nil {
463
+			t.Errorf("failed at creating webhook controller: %v", err)
464
+			continue
465
+		}
466
+
467
+		err = wc.upsertSecret(tc.scrtName, tc.namespace)
468
+		if err != nil {
469
+			t.Errorf("should not failed at upsertSecret, err: %v", err)
470
+		}
471
+		scrt, err := client.CoreV1().Secrets(tc.namespace).Get(tc.scrtName, metav1.GetOptions{})
472
+		if err != nil || scrt == nil {
473
+			t.Fatalf("failed to get test secret (%v): err (%v), secret (%v)", tc.scrtName, err, scrt)
474
+		}
475
+
476
+		if tc.changeCACert {
477
+			scrt.Data[ca.RootCertID] = []byte(exampleCACert2)
478
+		}
479
+
480
+		newScrt := &v1.Secret{}
481
+		scrt.DeepCopyInto(newScrt)
482
+		err = wc.refreshSecret(newScrt)
483
+		if err != nil {
484
+			t.Fatalf("failed to refresh secret (%v), err: %v", newScrt, err)
485
+		}
486
+
487
+		// scrt2 is the secret after refreshing, which will be compared against original scrt
488
+		scrt2, err := client.CoreV1().Secrets(tc.namespace).Get(tc.scrtName, metav1.GetOptions{})
489
+		if err != nil || scrt2 == nil {
490
+			t.Fatalf("failed to get test secret (%v): err (%v), secret (%v)", tc.scrtName, err, scrt2)
491
+		}
492
+		if tc.expectUpdate {
493
+			if reflect.DeepEqual(scrt, scrt2) {
494
+				t.Errorf("change is expected while there is no change")
495
+			}
496
+		} else {
497
+			if !reflect.DeepEqual(scrt, scrt2) {
498
+				t.Errorf("change is not expected while there is change")
499
+			}
500
+		}
501
+	}
502
+}
503
+
504
+func TestCleanUpCertGen(t *testing.T) {
505
+	mutatingWebhookServiceNames := []string{"foo"}
506
+
507
+	testCases := map[string]struct {
508
+		gracePeriodRatio              float32
509
+		minGracePeriod                time.Duration
510
+		k8sCaCertFile                 string
511
+		namespace                     string
512
+		mutatingWebhookSerivceNames   []string
513
+		validatingWebhookServiceNames []string
514
+	}{
515
+		"clean up a CSR should succeed": {
516
+			gracePeriodRatio:            0.6,
517
+			k8sCaCertFile:               "./test-data/example-ca-cert.pem",
518
+			namespace:                   "foo.ns",
519
+			mutatingWebhookSerivceNames: mutatingWebhookServiceNames,
520
+		},
521
+	}
522
+
523
+	csrName := "test-csr"
524
+
525
+	for _, tc := range testCases {
526
+		client := fake.NewSimpleClientset()
527
+		wc, err := NewWebhookController(tc.gracePeriodRatio, tc.minGracePeriod,
528
+			client.CoreV1(), client.AdmissionregistrationV1beta1(), client.CertificatesV1beta1(),
529
+			tc.k8sCaCertFile, tc.namespace, tc.mutatingWebhookSerivceNames,
530
+			tc.validatingWebhookServiceNames)
531
+		if err != nil {
532
+			t.Fatalf("failed at creating webhook controller: %v", err)
533
+		}
534
+
535
+		options := util.CertOptions{
536
+			Host:       "test-host",
537
+			RSAKeySize: keySize,
538
+			IsDualUse:  false,
539
+			PKCS8Key:   false,
540
+		}
541
+		csrPEM, _, err := util.GenCSR(options)
542
+		if err != nil {
543
+			t.Fatalf("CSR generation error (%v)", err)
544
+		}
545
+
546
+		k8sCSR := &cert.CertificateSigningRequest{
547
+			TypeMeta: metav1.TypeMeta{
548
+				APIVersion: "certificates.k8s.io/v1beta1",
549
+				Kind:       "CertificateSigningRequest",
550
+			},
551
+			ObjectMeta: metav1.ObjectMeta{
552
+				Name: csrName,
553
+			},
554
+			Spec: cert.CertificateSigningRequestSpec{
555
+				Request: csrPEM,
556
+				Groups:  []string{"system:authenticated"},
557
+				Usages: []cert.KeyUsage{
558
+					cert.UsageDigitalSignature,
559
+					cert.UsageKeyEncipherment,
560
+					cert.UsageServerAuth,
561
+					cert.UsageClientAuth,
562
+				},
563
+			},
564
+		}
565
+		_, err = wc.certClient.CertificateSigningRequests().Create(k8sCSR)
566
+		if err != nil {
567
+			t.Fatalf("error when creating CSR: %v", err)
568
+		}
569
+
570
+		csr, err := wc.certClient.CertificateSigningRequests().Get(csrName, metav1.GetOptions{})
571
+		if err != nil || csr == nil {
572
+			t.Fatalf("failed to get CSR: name (%v), err (%v), CSR (%v)", csrName, err, csr)
573
+		}
574
+
575
+		// The CSR should be deleted.
576
+		err = wc.cleanUpCertGen(csrName)
577
+		if err != nil {
578
+			t.Errorf("cleanUpCertGen returns an error: %v", err)
579
+		}
580
+		_, err = wc.certClient.CertificateSigningRequests().Get(csrName, metav1.GetOptions{})
581
+		if err == nil {
582
+			t.Fatalf("should failed at getting CSR: name (%v)", csrName)
583
+		}
584
+	}
585
+}
586
+
587
+func TestIsWebhookSecret(t *testing.T) {
588
+	mutatingWebhookServiceNames := []string{"foo", "bar"}
589
+
590
+	testCases := map[string]struct {
591
+		gracePeriodRatio              float32
592
+		minGracePeriod                time.Duration
593
+		k8sCaCertFile                 string
594
+		namespace                     string
595
+		mutatingWebhookServiceNames   []string
596
+		validatingWebhookServiceNames []string
597
+		scrtName                      string
598
+		scrtNameSpace                 string
599
+		expectedRet                   bool
600
+	}{
601
+		"a valid webhook secret in valid namespace": {
602
+			gracePeriodRatio:            0.6,
603
+			k8sCaCertFile:               "./test-data/example-ca-cert.pem",
604
+			namespace:                   "ns.foo",
605
+			mutatingWebhookServiceNames: mutatingWebhookServiceNames,
606
+			scrtName:                    "istio.webhook.foo",
607
+			scrtNameSpace:               "ns.foo",
608
+			expectedRet:                 true,
609
+		},
610
+		"an invalid webhook secret in valid namespace": {
611
+			gracePeriodRatio:            0.6,
612
+			k8sCaCertFile:               "./test-data/example-ca-cert.pem",
613
+			namespace:                   "ns.foo",
614
+			mutatingWebhookServiceNames: mutatingWebhookServiceNames,
615
+			scrtName:                    "istio.webhook.invalid",
616
+			scrtNameSpace:               "ns.foo",
617
+			expectedRet:                 false,
618
+		},
619
+		"a valid webhook secret in invalid namespace": {
620
+			gracePeriodRatio:            0.6,
621
+			k8sCaCertFile:               "./test-data/example-ca-cert.pem",
622
+			namespace:                   "ns.foo",
623
+			mutatingWebhookServiceNames: mutatingWebhookServiceNames,
624
+			scrtName:                    "istio.webhook.foo",
625
+			scrtNameSpace:               "ns.invalid",
626
+			expectedRet:                 false,
627
+		},
628
+	}
629
+
630
+	for _, tc := range testCases {
631
+		client := fake.NewSimpleClientset()
632
+		wc, err := NewWebhookController(tc.gracePeriodRatio, tc.minGracePeriod,
633
+			client.CoreV1(), client.AdmissionregistrationV1beta1(), client.CertificatesV1beta1(),
634
+			tc.k8sCaCertFile, tc.namespace,
635
+			tc.mutatingWebhookServiceNames, tc.validatingWebhookServiceNames)
636
+		if err != nil {
637
+			t.Fatalf("failed to create a webhook controller: %v", err)
638
+		}
639
+
640
+		ret := wc.isWebhookSecret(tc.scrtName, tc.scrtNameSpace)
641
+		if tc.expectedRet != ret {
642
+			t.Fatalf("expected result (%v) differs from the actual result (%v)", tc.expectedRet, ret)
643
+		}
644
+		// Sleep for watchers to release the file handles
645
+		time.Sleep(10 * time.Millisecond)
646
+	}
647
+}
648
+
649
+func TestGetCACert(t *testing.T) {
650
+	mutatingWebhookServiceNames := []string{"foo"}
651
+
652
+	testCases := map[string]struct {
653
+		gracePeriodRatio              float32
654
+		minGracePeriod                time.Duration
655
+		k8sCaCertFile                 string
656
+		namespace                     string
657
+		mutatingWebhookSerivceNames   []string
658
+		validatingWebhookServiceNames []string
659
+		expectFail                    bool
660
+	}{
661
+		"getCACert should succeed for a valid certificate": {
662
+			gracePeriodRatio:            0.6,
663
+			k8sCaCertFile:               "./test-data/example-ca-cert.pem",
664
+			namespace:                   "foo.ns",
665
+			mutatingWebhookSerivceNames: mutatingWebhookServiceNames,
666
+			expectFail:                  false,
667
+		},
668
+	}
669
+
670
+	for _, tc := range testCases {
671
+		client := fake.NewSimpleClientset()
672
+		// If the CA cert. is invalid, NewWebhookController will fail.
673
+		wc, err := NewWebhookController(tc.gracePeriodRatio, tc.minGracePeriod,
674
+			client.CoreV1(), client.AdmissionregistrationV1beta1(), client.CertificatesV1beta1(),
675
+			tc.k8sCaCertFile, tc.namespace, tc.mutatingWebhookSerivceNames,
676
+			tc.validatingWebhookServiceNames)
677
+		if err != nil {
678
+			t.Fatalf("failed at creating webhook controller: %v", err)
679
+		}
680
+
681
+		cert, err := wc.getCACert()
682
+		if !tc.expectFail {
683
+			if err != nil {
684
+				t.Errorf("failed to get CA cert: %v", err)
685
+			} else if !bytes.Equal(cert, []byte(exampleCACert1)) {
686
+				t.Errorf("the CA certificate read does not match the actual certificate")
687
+			}
688
+		} else if err == nil {
689
+			t.Error("expect failure on getting CA cert but succeeded")
690
+		}
691
+	}
692
+}
693
+
694
+func TestGetServiceName(t *testing.T) {
695
+	mutatingWebhookServiceNames := []string{"foo", "bar"}
696
+	validatingWebhookServiceNames := []string{"baz"}
697
+
698
+	testCases := map[string]struct {
699
+		gracePeriodRatio              float32
700
+		minGracePeriod                time.Duration
701
+		k8sCaCertFile                 string
702
+		namespace                     string
703
+		mutatingWebhookServiceNames   []string
704
+		validatingWebhookServiceNames []string
705
+		scrtName                      string
706
+		expectFound                   bool
707
+		expectedSvcName               string
708
+	}{
709
+		"a mutating webhook service corresponding to a secret exists": {
710
+			gracePeriodRatio:            0.6,
711
+			k8sCaCertFile:               "./test-data/example-ca-cert.pem",
712
+			mutatingWebhookServiceNames: mutatingWebhookServiceNames,
713
+			scrtName:                    "istio.webhook.foo",
714
+			expectFound:                 true,
715
+			expectedSvcName:             "foo",
716
+		},
717
+		"a mutating service corresponding to a secret does not exists": {
718
+			gracePeriodRatio:            0.6,
719
+			k8sCaCertFile:               "./test-data/example-ca-cert.pem",
720
+			mutatingWebhookServiceNames: mutatingWebhookServiceNames,
721
+			scrtName:                    "istio.webhook.baz",
722
+			expectFound:                 false,
723
+			expectedSvcName:             "foo",
724
+		},
725
+		"a validating webhook service corresponding to a secret exists": {
726
+			gracePeriodRatio:              0.6,
727
+			k8sCaCertFile:                 "./test-data/example-ca-cert.pem",
728
+			mutatingWebhookServiceNames:   mutatingWebhookServiceNames,
729
+			validatingWebhookServiceNames: validatingWebhookServiceNames,
730
+			scrtName:                      "istio.webhook.baz",
731
+			expectFound:                   true,
732
+			expectedSvcName:               "baz",
733
+		},
734
+		"a validating webhook service corresponding to a secret does not exists": {
735
+			gracePeriodRatio:              0.6,
736
+			k8sCaCertFile:                 "./test-data/example-ca-cert.pem",
737
+			mutatingWebhookServiceNames:   mutatingWebhookServiceNames,
738
+			validatingWebhookServiceNames: validatingWebhookServiceNames,
739
+			scrtName:                      "istio.webhook.barr",
740
+			expectFound:                   false,
741
+			expectedSvcName:               "bar",
742
+		},
743
+	}
744
+
745
+	for _, tc := range testCases {
746
+		client := fake.NewSimpleClientset()
747
+		wc, err := NewWebhookController(tc.gracePeriodRatio, tc.minGracePeriod,
748
+			client.CoreV1(), client.AdmissionregistrationV1beta1(), client.CertificatesV1beta1(),
749
+			tc.k8sCaCertFile, tc.namespace, tc.mutatingWebhookServiceNames,
750
+			tc.validatingWebhookServiceNames)
751
+		if err != nil {
752
+			t.Errorf("failed to create a webhook controller: %v", err)
753
+		}
754
+
755
+		ret, found := wc.getServiceName(tc.scrtName)
756
+		if tc.expectFound != found {
757
+			t.Errorf("expected found (%v) differs from the actual found (%v)", tc.expectFound, found)
758
+			continue
759
+		}
760
+		if found && tc.expectedSvcName != ret {
761
+			t.Errorf("the service name (%v) returned is not as expcted (%v)", ret, tc.expectedSvcName)
762
+		}
763
+	}
764
+}
765
+
766
+func TestGetWebhookSecretNameFromSvcName(t *testing.T) {
767
+	testCases := map[string]struct {
768
+		gracePeriodRatio              float32
769
+		minGracePeriod                time.Duration
770
+		k8sCaCertFile                 string
771
+		namespace                     string
772
+		mutatingWebhookServiceNames   []string
773
+		validatingWebhookServiceNames []string
774
+		svcName                       string
775
+		expectedScrtName              string
776
+	}{
777
+		"expected secret name matches the return": {
778
+			gracePeriodRatio: 0.6,
779
+			k8sCaCertFile:    "./test-data/example-ca-cert.pem",
780
+			svcName:          "foo",
781
+			expectedScrtName: "istio.webhook.foo",
782
+		},
783
+	}
784
+
785
+	for _, tc := range testCases {
786
+		client := fake.NewSimpleClientset()
787
+		wc, err := NewWebhookController(tc.gracePeriodRatio, tc.minGracePeriod,
788
+			client.CoreV1(), client.AdmissionregistrationV1beta1(), client.CertificatesV1beta1(),
789
+			tc.k8sCaCertFile, tc.namespace, tc.mutatingWebhookServiceNames,
790
+			tc.validatingWebhookServiceNames)
791
+		if err != nil {
792
+			t.Errorf("failed to create a webhook controller: %v", err)
793
+		}
794
+
795
+		ret := wc.getWebhookSecretNameFromSvcName(tc.svcName)
796
+		if tc.expectedScrtName != ret {
797
+			t.Errorf("the secret name (%v) returned is not as expcted (%v)", ret, tc.expectedScrtName)
97 798
 		}
98 799
 	}
99 800
 }

+ 0
- 0
security/pkg/k8s/chiron/test-data/empty-webhook-config.yaml View File


+ 19
- 0
security/pkg/k8s/chiron/test-data/example-ca-cert2.pem View File

@@ -0,0 +1,19 @@
1
+-----BEGIN CERTIFICATE-----
2
+MIIDDDCCAfSgAwIBAgIRALsN8ND73NbYSKTZZa4jf2EwDQYJKoZIhvcNAQELBQAw
3
+LzEtMCsGA1UEAxMkZTNlM2RlZWQtYzIyNi00OWM2LThmOTktNDU3NmRmMzQ0YWQ1
4
+MB4XDTE5MDYwMTE1NTU0M1oXDTI0MDUzMDE2NTU0M1owLzEtMCsGA1UEAxMkZTNl
5
+M2RlZWQtYzIyNi00OWM2LThmOTktNDU3NmRmMzQ0YWQ1MIIBIjANBgkqhkiG9w0B
6
+AQEFAAOCAQ8AMIIBCgKCAQEA43oeK/hS92ANjmg50LCl3tM7eYAlBB/XgCl+bfp3
7
+KwEf+uW5yEvzSVHd2VPFI/kJJeLFrsyCRaU4FwxWcEr2Ld07DPL34oyZRRXQF0w6
8
+4ZNSVmevBNdZLqHcoIUtR1iFJbkctE93HpGw5Kg1NXRLDu47wQtzcC3GDOEk1amu
9
+mL916R2OcYEeOcyRDnlbLcsTYRvK5WBQsux4E0iu2Eo9GIajKmbxVLxA9fsmqG4i
10
+/HoVkLmCg+ZRPR/66AFLPFV1J3RWp0K4HKGzBeCyd2RC+o0g8tJX3EVSuQpqzS8p
11
+i2t71cYu/Sf5gt3wXsNHyzE6bF1o+acyzWvJlBym/HsbAQIDAQABoyMwITAOBgNV
12
+HQ8BAf8EBAMCAgQwDwYDVR0TAQH/BAUwAwEB/zANBgkqhkiG9w0BAQsFAAOCAQEA
13
+kJXGkFEybCp7RxUSILuIqMtKcYcQU9ulKmLSn51VrpcRHP4SH7UJ0aXMjAdRLsop
14
+em7YgbvToGNingqcmSJlunR3jXDecSXJLUO1xcfw6N+B2BXRgUv8wV42btr2EV6q
15
+4HKou+MnKdrQkMUx218AT8TNPBb/Yx01m8YUS7mGUTApAhBneGEcKJ8xOznIuR5v
16
+CihWQA9AmUvfixpXNpJc4vqiYErwIXrYpuwc79SRtLuO70vV7FCctz+4JPpR7mp9
17
+dHMZfGO1KXMbYT9P5bm+itlWSyrnn0qK/Cn5RHBoFyY91VcQJTgABS/z5O0pZ662
18
+sNzF00Jhi0gU7th75QT3MA==
19
+-----END CERTIFICATE-----

+ 0
- 37
security/pkg/k8s/chiron/test-data/example-mutating-webhook-config.yaml View File

@@ -1,37 +0,0 @@
1
-apiVersion: v1
2
-kind: ConfigMap
3
-metadata:
4
-  name: istio-protomutate-webhook-configuration
5
-  namespace: istio-system
6
-  labels:
7
-    app: protomutateWebhook
8
-data:
9
-  mutatingwebhookconfiguration.yaml: |-
10
-    apiVersion: admissionregistration.k8s.io/v1beta1
11
-    kind: MutatingWebhookConfiguration
12
-    metadata:
13
-      labels:
14
-        app: protomutateWebhook
15
-      name: protomutate
16
-    webhooks:
17
-    - clientConfig:
18
-        caBundle: ""
19
-        service:
20
-          name: protomutate
21
-          namespace: istio-system
22
-          path: /inject
23
-      failurePolicy: Fail
24
-      name: protomutate.istio.io
25
-      namespaceSelector:
26
-        matchLabels:
27
-          protomutate-injection: enabled
28
-      rules:
29
-      - apiGroups:
30
-        - ""
31
-        apiVersions:
32
-        - v1
33
-        operations:
34
-        - CREATE
35
-        resources:
36
-        - pods
37
-      sideEffects: Unknown

+ 0
- 37
security/pkg/k8s/chiron/test-data/example-validating-webhook-config.yaml View File

@@ -1,37 +0,0 @@
1
-apiVersion: v1
2
-kind: ConfigMap
3
-metadata:
4
-  name: istio-protovalidate-webhook-configuration
5
-  namespace: istio-system
6
-  labels:
7
-    app: protovalidateWebhook
8
-data:
9
-  validatingwebhookconfiguration.yaml: |-
10
-    apiVersion: admissionregistration.k8s.io/v1beta1
11
-    kind: ValidatingWebhookConfiguration
12
-    metadata:
13
-      labels:
14
-        app: protovalidateWebhook
15
-      name: protovalidate
16
-    webhooks:
17
-    - clientConfig:
18
-        caBundle: ""
19
-        service:
20
-          name: protovalidate
21
-          namespace: istio-system
22
-          path: /validate
23
-      failurePolicy: Fail
24
-      name: protovalidate.istio.io
25
-      namespaceSelector:
26
-        matchLabels:
27
-          protovalidate-validation: enabled
28
-      rules:
29
-      - apiGroups:
30
-        - ""
31
-        apiVersions:
32
-        - v1
33
-        operations:
34
-        - CREATE
35
-        resources:
36
-        - pods
37
-      sideEffects: Unknown

+ 223
- 105
security/pkg/k8s/chiron/utils.go View File

@@ -21,19 +21,92 @@ import (
21 21
 	"fmt"
22 22
 	"io/ioutil"
23 23
 	"net"
24
-	"reflect"
25 24
 	"time"
26 25
 
27
-	"k8s.io/api/admissionregistration/v1beta1"
28
-
29
-	"github.com/ghodss/yaml"
26
+	"istio.io/istio/pkg/spiffe"
30 27
 
28
+	"istio.io/istio/security/pkg/pki/util"
31 29
 	"istio.io/pkg/log"
32 30
 
31
+	cert "k8s.io/api/certificates/v1beta1"
33 32
 	kerrors "k8s.io/apimachinery/pkg/api/errors"
34 33
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
35 34
 )
36 35
 
36
+// Generate a certificate and key from k8s CA
37
+// Working flow:
38
+// 1. Generate a CSR
39
+// 2. Submit a CSR
40
+// 3. Approve a CSR
41
+// 4. Read the signed certificate
42
+// 5. Clean up the artifacts (e.g., delete CSR)
43
+func genKeyCertK8sCA(wc *WebhookController, secretName string, secretNamespace, svcName string) ([]byte, []byte, []byte, error) {
44
+	// 1. Generate a CSR
45
+	// Construct the dns id from service name and name space.
46
+	// Example: istio-pilot.istio-system.svc, istio-pilot.istio-system
47
+	id := fmt.Sprintf("%s.%s.svc,%s.%s", svcName, secretNamespace, svcName, secretNamespace)
48
+	options := util.CertOptions{
49
+		Host:       id,
50
+		RSAKeySize: keySize,
51
+		IsDualUse:  false,
52
+		PKCS8Key:   false,
53
+	}
54
+	csrPEM, keyPEM, err := util.GenCSR(options)
55
+	if err != nil {
56
+		log.Errorf("CSR generation error (%v)", err)
57
+		return nil, nil, nil, err
58
+	}
59
+
60
+	// 2. Submit the CSR
61
+	csrName := fmt.Sprintf("domain-%s-ns-%s-secret-%s", spiffe.GetTrustDomain(), secretNamespace, secretName)
62
+	numRetries := 3
63
+	r, err := submitCSR(wc, csrName, csrPEM, numRetries)
64
+	if err != nil {
65
+		return nil, nil, nil, err
66
+	}
67
+	if r == nil {
68
+		return nil, nil, nil, fmt.Errorf("the CSR returned is nil")
69
+	}
70
+
71
+	// 3. Approve a CSR
72
+	log.Debugf("approve CSR (%v) ...", csrName)
73
+	csrMsg := fmt.Sprintf("CSR (%s) for the webhook certificate (%s) is approved", csrName, id)
74
+	r.Status.Conditions = append(r.Status.Conditions, cert.CertificateSigningRequestCondition{
75
+		Type:    cert.CertificateApproved,
76
+		Reason:  csrMsg,
77
+		Message: csrMsg,
78
+	})
79
+	reqApproval, err := wc.certClient.CertificateSigningRequests().UpdateApproval(r)
80
+	if err != nil {
81
+		log.Debugf("failed to approve CSR (%v): %v", csrName, err)
82
+		errCsr := wc.cleanUpCertGen(csrName)
83
+		if errCsr != nil {
84
+			log.Errorf("failed to clean up CSR (%v): %v", csrName, err)
85
+		}
86
+		return nil, nil, nil, err
87
+	}
88
+	log.Debugf("CSR (%v) is approved: %v", csrName, reqApproval)
89
+
90
+	// 4. Read the signed certificate
91
+	certChain, caCert, err := readSignedCertificate(wc, csrName, certReadInterval, maxNumCertRead)
92
+	if err != nil {
93
+		log.Debugf("failed to read signed cert. (%v): %v", csrName, err)
94
+		errCsr := wc.cleanUpCertGen(csrName)
95
+		if errCsr != nil {
96
+			log.Errorf("failed to clean up CSR (%v): %v", csrName, err)
97
+		}
98
+		return nil, nil, nil, err
99
+	}
100
+
101
+	// 5. Clean up the artifacts (e.g., delete CSR)
102
+	err = wc.cleanUpCertGen(csrName)
103
+	if err != nil {
104
+		log.Errorf("failed to clean up CSR (%v): %v", csrName, err)
105
+	}
106
+	// If there is a failure of cleaning up CSR, the error is returned.
107
+	return certChain, keyPEM, caCert, err
108
+}
109
+
37 110
 // Read CA certificate and check whether it is a valid certificate.
38 111
 func readCACert(caCertPath string) ([]byte, error) {
39 112
 	caCert, err := ioutil.ReadFile(caCertPath)
@@ -68,125 +141,170 @@ func isTCPReachable(host string, port int) bool {
68 141
 	return true
69 142
 }
70 143
 
71
-// Rebuild the desired mutatingwebhookconfiguration from the specified CA
72
-// and webhook config files.
73
-func rebuildMutatingWebhookConfigHelper(
74
-	caCert []byte, webhookConfigFile, webhookConfigName string,
75
-) (*v1beta1.MutatingWebhookConfiguration, error) {
76
-	// load and validate configuration
77
-	webhookConfigData, err := ioutil.ReadFile(webhookConfigFile)
144
+// Reload CA cert from file and return whether CA cert is changed
145
+func reloadCACert(wc *WebhookController) (bool, error) {
146
+	certChanged := false
147
+	wc.certMutex.Lock()
148
+	defer wc.certMutex.Unlock()
149
+	caCert, err := readCACert(wc.k8sCaCertFile)
78 150
 	if err != nil {
79
-		return nil, err
80
-	}
81
-	var webhookConfig v1beta1.MutatingWebhookConfiguration
82
-	if err := yaml.Unmarshal(webhookConfigData, &webhookConfig); err != nil {
83
-		return nil, fmt.Errorf("could not decode mutatingwebhookconfiguration from %v: %v",
84
-			webhookConfigFile, err)
151
+		return certChanged, err
85 152
 	}
86
-
87
-	// the webhook name is fixed at startup time
88
-	webhookConfig.Name = webhookConfigName
89
-
90
-	// patch the ca-cert into the user provided configuration
91
-	for i := range webhookConfig.Webhooks {
92
-		webhookConfig.Webhooks[i].ClientConfig.CABundle = caCert
153
+	if !bytes.Equal(caCert, wc.CACert) {
154
+		wc.CACert = append([]byte(nil), caCert...)
155
+		certChanged = true
93 156
 	}
94
-
95
-	return &webhookConfig, nil
157
+	return certChanged, nil
96 158
 }
97 159
 
98
-// Create or update the mutatingwebhookconfiguration based on the config from rebuildMutatingWebhookConfig().
99
-func createOrUpdateMutatingWebhookConfig(wc *WebhookController) error {
100
-	if wc == nil {
101
-		return fmt.Errorf("webhook controller is nil")
160
+func submitCSR(wc *WebhookController, csrName string, csrPEM []byte, numRetries int) (*cert.CertificateSigningRequest, error) {
161
+	k8sCSR := &cert.CertificateSigningRequest{
162
+		TypeMeta: metav1.TypeMeta{
163
+			APIVersion: "certificates.k8s.io/v1beta1",
164
+			Kind:       "CertificateSigningRequest",
165
+		},
166
+		ObjectMeta: metav1.ObjectMeta{
167
+			Name: csrName,
168
+		},
169
+		Spec: cert.CertificateSigningRequestSpec{
170
+			Request: csrPEM,
171
+			Groups:  []string{"system:authenticated"},
172
+			Usages: []cert.KeyUsage{
173
+				cert.UsageDigitalSignature,
174
+				cert.UsageKeyEncipherment,
175
+				cert.UsageServerAuth,
176
+				cert.UsageClientAuth,
177
+			},
178
+		},
102 179
 	}
103
-	if wc.mutatingWebhookConfig == nil {
104
-		return fmt.Errorf("mutatingwebhookconfiguration is nil")
180
+	var reqRet *cert.CertificateSigningRequest
181
+	var errRet error
182
+	for i := 0; i < numRetries; i++ {
183
+		log.Debugf("trial %v to create CSR (%v)", i, csrName)
184
+		reqRet, errRet = wc.certClient.CertificateSigningRequests().Create(k8sCSR)
185
+		if errRet == nil && reqRet != nil {
186
+			break
187
+		}
188
+		// If an err other than the CSR exists is returned, re-try
189
+		if !kerrors.IsAlreadyExists(errRet) {
190
+			log.Debugf("failed to create CSR (%v): %v", csrName, errRet)
191
+			continue
192
+		}
193
+		// If CSR exists, delete the existing CSR and create again
194
+		log.Debugf("delete an existing CSR: %v", csrName)
195
+		errRet = wc.certClient.CertificateSigningRequests().Delete(csrName, nil)
196
+		if errRet != nil {
197
+			log.Errorf("failed to delete CSR (%v): %v", csrName, errRet)
198
+			continue
199
+		}
200
+		log.Debugf("create CSR (%v) after the existing one was deleted", csrName)
201
+		reqRet, errRet = wc.certClient.CertificateSigningRequests().Create(k8sCSR)
202
+		if errRet == nil && reqRet != nil {
203
+			break
204
+		}
105 205
 	}
106
-	client := wc.admission.MutatingWebhookConfigurations()
107
-	webhookConfig := wc.mutatingWebhookConfig
206
+	return reqRet, errRet
207
+}
108 208
 
109
-	current, err := client.Get(webhookConfig.Name, metav1.GetOptions{})
110
-	if err != nil {
111
-		// If the webhookconfiguration does not exist yet, create the config.
112
-		if kerrors.IsNotFound(err) {
113
-			log.Debugf("get webhookConfig %v: NotFound", webhookConfig.Name)
114
-			// Create the webhookconfiguration
115
-			_, createErr := client.Create(webhookConfig)
116
-			return createErr
117
-		}
118
-		log.Errorf("get webhookConfig %v err: %v", webhookConfig.Name, err)
119
-		// There is an error when getting the webhookconfiguration and the error is
120
-		// not that the webhookconfiguration not found. In this case, still try the update.
121
-	}
122
-	// Update the configuration only if the webhooks in the current is different from those configured.
123
-	// Only copy the relevant fields that we want reconciled and ignore everything else, e.g. labels, selectors.
124
-	updated := current.DeepCopyObject().(*v1beta1.MutatingWebhookConfiguration)
125
-	updated.Webhooks = webhookConfig.Webhooks
126
-
127
-	if !reflect.DeepEqual(updated, current) {
128
-		// Update mutatingwebhookconfiguration to based on current and the webhook configured.
129
-		_, err := client.Update(updated)
209
+// Read the signed certificate
210
+func readSignedCertificate(wc *WebhookController, csrName string,
211
+	readInterval time.Duration, maxNumRead int) ([]byte, []byte, error) {
212
+	var reqSigned *cert.CertificateSigningRequest
213
+	for i := 0; i < maxNumRead; i++ {
214
+		// It takes some time for certificate to be ready, so wait first.
215
+		time.Sleep(readInterval)
216
+		r, err := wc.certClient.CertificateSigningRequests().Get(csrName, metav1.GetOptions{})
130 217
 		if err != nil {
131
-			log.Errorf("update webhookconfiguration returns err: %v", err)
218
+			log.Errorf("failed to get the CSR (%v): %v", csrName, err)
219
+			errCsr := wc.cleanUpCertGen(csrName)
220
+			if errCsr != nil {
221
+				log.Errorf("failed to clean up CSR (%v): %v", csrName, err)
222
+			}
223
+			return nil, nil, err
224
+		}
225
+		if r.Status.Certificate != nil {
226
+			// Certificate is ready
227
+			reqSigned = r
228
+			break
132 229
 		}
133
-		return err
134 230
 	}
135
-	return nil
136
-}
137
-
138
-// Create or update the validatingwebhookconfiguration based on the config from rebuildValidatingWebhookConfig().
139
-func createOrUpdateValidatingWebhookConfig(wc *WebhookController) error {
140
-	if wc == nil {
141
-		return fmt.Errorf("webhook controller is nil")
231
+	if reqSigned == nil {
232
+		log.Errorf("failed to read the certificate for CSR (%v), nil CSR", csrName)
233
+		errCsr := wc.cleanUpCertGen(csrName)
234
+		if errCsr != nil {
235
+			log.Errorf("failed to clean up CSR (%v): %v", csrName, errCsr)
236
+		}
237
+		return nil, nil, fmt.Errorf("failed to read the certificate for CSR (%v), nil CSR", csrName)
142 238
 	}
143
-	if wc.validatingWebhookConfig == nil {
144
-		return fmt.Errorf("validatingwebhookconfiguration is nil")
239
+	if reqSigned.Status.Certificate == nil {
240
+		log.Errorf("failed to read the certificate for CSR (%v), nil cert", csrName)
241
+		// Output the first CertificateDenied condition, if any, in the status
242
+		for _, c := range reqSigned.Status.Conditions {
243
+			if c.Type == cert.CertificateDenied {
244
+				log.Errorf("CertificateDenied, name: %v, uid: %v, cond-type: %v, cond: %s",
245
+					reqSigned.Name, reqSigned.UID, c.Type, c.String())
246
+				break
247
+			}
248
+		}
249
+		errCsr := wc.cleanUpCertGen(csrName)
250
+		if errCsr != nil {
251
+			log.Errorf("failed to clean up CSR (%v): %v", csrName, errCsr)
252
+		}
253
+		return nil, nil, fmt.Errorf("failed to read the certificate for CSR (%v), nil cert", csrName)
145 254
 	}
146
-	client := wc.admission.ValidatingWebhookConfigurations()
147
-	webhookConfig := wc.validatingWebhookConfig
148 255
 
149
-	current, err := client.Get(webhookConfig.Name, metav1.GetOptions{})
256
+	log.Debugf("the length of the certificate is %v", len(reqSigned.Status.Certificate))
257
+	log.Debugf("the certificate for CSR (%v) is: %v", csrName, string(reqSigned.Status.Certificate))
258
+
259
+	certPEM := reqSigned.Status.Certificate
260
+	caCert, err := wc.getCACert()
150 261
 	if err != nil {
151
-		// If the webhookconfiguration does not exist yet, create the config.
152
-		if kerrors.IsNotFound(err) {
153
-			log.Debugf("get webhookConfig %v: NotFound", webhookConfig.Name)
154
-			// Create the webhookconfiguration
155
-			_, createErr := client.Create(webhookConfig)
156
-			return createErr
157
-		}
158
-		log.Errorf("get webhookConfig %v err: %v", webhookConfig.Name, err)
159
-		// There is an error when getting the webhookconfiguration and the error is
160
-		// not that the webhookconfiguration not found. In this case, still try the update.
161
-	}
162
-	// Update the configuration only if the webhooks in the current is different from those configured.
163
-	// Only copy the relevant fields that we want reconciled and ignore everything else, e.g. labels, selectors.
164
-	updated := current.DeepCopyObject().(*v1beta1.ValidatingWebhookConfiguration)
165
-	updated.Webhooks = webhookConfig.Webhooks
166
-
167
-	if !reflect.DeepEqual(updated, current) {
168
-		// Update webhookconfiguration to based on current and the webhook configured.
169
-		_, err := client.Update(updated)
170
-		if err != nil {
171
-			log.Errorf("update webhookconfiguration returns err: %v", err)
262
+		log.Errorf("error when getting CA cert (%v)", err)
263
+		errCsr := wc.cleanUpCertGen(csrName)
264
+		if errCsr != nil {
265
+			log.Errorf("failed to clean up CSR (%v): %v", csrName, err)
172 266
 		}
173
-		return err
267
+		return nil, nil, err
174 268
 	}
175
-	return nil
176
-}
177
-
178
-// Reload CA cert from file and return whether CA cert is changed
179
-func reloadCACert(wc *WebhookController) (bool, error) {
180
-	certChanged := false
181
-	wc.certMutex.Lock()
182
-	defer wc.certMutex.Unlock()
183
-	caCert, err := readCACert(wc.k8sCaCertFile)
269
+	// Verify the certificate chain before returning the certificate
270
+	roots := x509.NewCertPool()
271
+	if roots == nil {
272
+		errCsr := wc.cleanUpCertGen(csrName)
273
+		if errCsr != nil {
274
+			log.Errorf("failed to clean up CSR (%v): %v", csrName, err)
275
+		}
276
+		return nil, nil, fmt.Errorf("failed to create cert pool")
277
+	}
278
+	if ok := roots.AppendCertsFromPEM(caCert); !ok {
279
+		errCsr := wc.cleanUpCertGen(csrName)
280
+		if errCsr != nil {
281
+			log.Errorf("failed to clean up CSR (%v): %v", csrName, err)
282
+		}
283
+		return nil, nil, fmt.Errorf("failed to append CA certificate")
284
+	}
285
+	certParsed, err := util.ParsePemEncodedCertificate(certPEM)
184 286
 	if err != nil {
185
-		return certChanged, err
287
+		log.Errorf("failed to parse the certificate: %v", err)
288
+		errCsr := wc.cleanUpCertGen(csrName)
289
+		if errCsr != nil {
290
+			log.Errorf("failed to clean up CSR (%v): %v", csrName, err)
291
+		}
292
+		return nil, nil, fmt.Errorf("failed to parse the certificate: %v", err)
186 293
 	}
187
-	if !bytes.Equal(caCert, wc.CACert) {
188
-		wc.CACert = append([]byte(nil), caCert...)
189
-		certChanged = true
294
+	_, err = certParsed.Verify(x509.VerifyOptions{
295
+		Roots: roots,
296
+	})
297
+	if err != nil {
298
+		log.Errorf("failed to verify the certificate chain: %v", err)
299
+		errCsr := wc.cleanUpCertGen(csrName)
300
+		if errCsr != nil {
301
+			log.Errorf("failed to clean up CSR (%v): %v", csrName, err)
302
+		}
303
+		return nil, nil, fmt.Errorf("failed to verify the certificate chain: %v", err)
190 304
 	}
191
-	return certChanged, nil
305
+	certChain := []byte{}
306
+	certChain = append(certChain, certPEM...)
307
+	certChain = append(certChain, caCert...)
308
+
309
+	return certChain, caCert, nil
192 310
 }

+ 279
- 43
security/pkg/k8s/chiron/utils_test.go View File

@@ -24,7 +24,15 @@ import (
24 24
 	"testing"
25 25
 	"time"
26 26
 
27
+	"istio.io/istio/pkg/spiffe"
28
+
29
+	cert "k8s.io/api/certificates/v1beta1"
30
+
31
+	"k8s.io/apimachinery/pkg/runtime"
32
+
33
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
27 34
 	"k8s.io/client-go/kubernetes/fake"
35
+	kt "k8s.io/client-go/testing"
28 36
 )
29 37
 
30 38
 const (
@@ -47,16 +55,95 @@ xO7AQk5MJcGg6cfE5wWAKU1ATjpK4CN+RTn8v8ODLoI2SW3pfsnXxm93O+pp9HN4
47 55
 +O+1PQtNUWhCfh+g6BN2mYo2OEZ8qGSxDlMZej4YOdVkW8PHmFZTK0w9iJKqM5o1
48 56
 V6g5gZlqSoRhICK09tpc
49 57
 -----END CERTIFICATE-----`
50
-)
51 58
 
52
-var (
53
-	fakeCACert = []byte("fake-CA-cert")
59
+	exampleIssuedCert = `-----BEGIN CERTIFICATE-----
60
+MIIDGDCCAgCgAwIBAgIRAKvYcPLFqnJcwtshCGfNzTswDQYJKoZIhvcNAQELBQAw
61
+LzEtMCsGA1UEAxMkYTc1OWM3MmQtZTY3Mi00MDM2LWFjM2MtZGMwMTAwZjE1ZDVl
62
+MB4XDTE5MDgwNjE5NTU0NVoXDTI0MDgwNDE5NTU0NVowCzEJMAcGA1UEChMAMIIB
63
+IjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAyLIFJU5yJ5VXhbmizir+7Glm
64
+1tVEYXKGiqYbMRbfsFm7V6Z4l00D9/eHvfTXaFpqhv6HBm31MArjYB3OaaV6krvT
65
+whBUEPSkGBFe/eMPSFWBW27a0nw0cK2s/5yuFhTRtcUrZ9+ojJg4IS3oSm2UZ6UJ
66
+DuNI3qwB6OlPQOcWX8uEp4eAaolD1lIbLRQYvxYrBqnyCZBLE+MJgA1/VB3dAECB
67
+TxPtAqcwLFcvsM5ABys8yK8FrqRn5Bx54NiztgG+yU30W33xjdqzmEmuIIk4JjPU
68
+ZQRsug7XClDvQKM6lbYcYS1td2zT08hdgURFXJ9VR64ALFp00/bvglpryu8FmQID
69
+AQABo1MwUTAMBgNVHRMBAf8EAjAAMEEGA1UdEQQ6MDiCHHByb3RvbXV0YXRlLmlz
70
+dGlvLXN5c3RlbS5zdmOCGHByb3RvbXV0YXRlLmlzdGlvLXN5c3RlbTANBgkqhkiG
71
+9w0BAQsFAAOCAQEAhcVEZSuNMqMUJrWVb3b+6pmw9o1f7j6a51KWxOiIl6YuTYFS
72
+WaR0lHSW8wLesjsjm1awWO/F3QRuYWbalANy7434GMAGF53u/uc+Z8aE3EItER9o
73
+SpAJos6OfJqyok7JXDdOYRDD5/hBerj68R9llWzNJd27/1jZ0NF2sIE1W4QFddy/
74
++8YA4+IqwkWB5/LbeRznl3EjFZDpCEJk0gg5XwAR5eIEy4QU8GueTwrDkssFdBGq
75
+0naco7/Es7CWQscYdKHAgYgk0UAyu8sGV235Uw3hlOrbZ/kqvyUmsSujgT8irmDV
76
+e+5z6MTAO6ktvHdQlSuH6ARn47bJrZOlkttAhg==
77
+-----END CERTIFICATE-----
78
+`
54 79
 )
55 80
 
56 81
 type mockTLSServer struct {
57 82
 	httpServer *httptest.Server
58 83
 }
59 84
 
85
+func defaultReactionFunc(obj runtime.Object) kt.ReactionFunc {
86
+	return func(act kt.Action) (bool, runtime.Object, error) {
87
+		return true, obj, nil
88
+	}
89
+}
90
+
91
+func TestGenKeyCertK8sCA(t *testing.T) {
92
+	testCases := map[string]struct {
93
+		gracePeriodRatio              float32
94
+		minGracePeriod                time.Duration
95
+		k8sCaCertFile                 string
96
+		namespace                     string
97
+		mutatingWebhookSerivceNames   []string
98
+		validatingWebhookServiceNames []string
99
+
100
+		secretName      string
101
+		secretNameSpace string
102
+		svcName         string
103
+
104
+		expectFaill bool
105
+	}{
106
+		"gen cert should succeed": {
107
+			gracePeriodRatio: 0.6,
108
+			k8sCaCertFile:    "./test-data/example-ca-cert.pem",
109
+			secretName:       "mock-secret",
110
+			secretNameSpace:  "mock-secret-namespace",
111
+			svcName:          "mock-service-name",
112
+			expectFaill:      false,
113
+		},
114
+	}
115
+
116
+	for _, tc := range testCases {
117
+		client := fake.NewSimpleClientset()
118
+		csr := &cert.CertificateSigningRequest{
119
+			ObjectMeta: metav1.ObjectMeta{
120
+				Name: "domain-cluster.local-ns--secret-mock-secret",
121
+			},
122
+			Status: cert.CertificateSigningRequestStatus{
123
+				Certificate: []byte(exampleIssuedCert),
124
+			},
125
+		}
126
+		client.PrependReactor("get", "certificatesigningrequests", defaultReactionFunc(csr))
127
+
128
+		wc, err := NewWebhookController(tc.gracePeriodRatio, tc.minGracePeriod,
129
+			client.CoreV1(), client.AdmissionregistrationV1beta1(), client.CertificatesV1beta1(),
130
+			tc.k8sCaCertFile, tc.namespace, tc.mutatingWebhookSerivceNames, tc.validatingWebhookServiceNames)
131
+		if err != nil {
132
+			t.Errorf("failed at creating webhook controller: %v", err)
133
+			continue
134
+		}
135
+
136
+		_, _, _, err = genKeyCertK8sCA(wc, tc.secretName, tc.namespace, tc.svcName)
137
+		if tc.expectFaill {
138
+			if err == nil {
139
+				t.Errorf("should have failed at updateMutatingWebhookConfig")
140
+			}
141
+		} else if err != nil {
142
+			t.Errorf("failed at updateMutatingWebhookConfig: %v", err)
143
+		}
144
+	}
145
+}
146
+
60 147
 func TestReadCACert(t *testing.T) {
61 148
 	testCases := map[string]struct {
62 149
 		certPath     string
@@ -125,64 +212,31 @@ func TestIsTCPReachable(t *testing.T) {
125 212
 	}
126 213
 }
127 214
 
128
-func TestRebuildMutatingWebhookConfigHelper(t *testing.T) {
129
-	configFile := "./test-data/example-mutating-webhook-config.yaml"
130
-	configName := "proto-mutate"
131
-	webhookConfig, err := rebuildMutatingWebhookConfigHelper(fakeCACert, configFile, configName)
132
-	if err != nil {
133
-		t.Fatalf("err rebuilding mutating webhook config: %v", err)
134
-	}
135
-	if webhookConfig.Name != configName {
136
-		t.Fatalf("webhookConfig.Name (%v) is different from %v", webhookConfig.Name, configName)
137
-	}
138
-	for i := range webhookConfig.Webhooks {
139
-		if !bytes.Equal(webhookConfig.Webhooks[i].ClientConfig.CABundle, fakeCACert) {
140
-			t.Fatalf("webhookConfig CA bundle(%v) is different from %v",
141
-				webhookConfig.Webhooks[i].ClientConfig.CABundle, fakeCACert)
142
-		}
143
-	}
144
-}
145
-
146 215
 func TestReloadCACert(t *testing.T) {
147
-	client := fake.NewSimpleClientset()
148
-	mutatingWebhookConfigFiles := []string{"./test-data/empty-webhook-config.yaml"}
149
-	validatingWebhookConfigFiles := []string{"./test-data/empty-webhook-config.yaml"}
150
-
151 216
 	testCases := map[string]struct {
152
-		deleteWebhookConfigOnExit     bool
153 217
 		gracePeriodRatio              float32
154 218
 		minGracePeriod                time.Duration
155 219
 		k8sCaCertFile                 string
156 220
 		namespace                     string
157
-		mutatingWebhookConfigFiles    []string
158
-		mutatingWebhookConfigNames    []string
159 221
 		mutatingWebhookSerivceNames   []string
160
-		mutatingWebhookSerivcePorts   []int
161
-		validatingWebhookConfigFiles  []string
162
-		validatingWebhookConfigNames  []string
163 222
 		validatingWebhookServiceNames []string
164
-		validatingWebhookServicePorts []int
165 223
 
166 224
 		expectFaill   bool
167 225
 		expectChanged bool
168 226
 	}{
169 227
 		"reload from valid CA cert path": {
170
-			deleteWebhookConfigOnExit:    false,
171
-			gracePeriodRatio:             0.6,
172
-			k8sCaCertFile:                "./test-data/example-ca-cert.pem",
173
-			mutatingWebhookConfigFiles:   mutatingWebhookConfigFiles,
174
-			validatingWebhookConfigFiles: validatingWebhookConfigFiles,
175
-			expectFaill:                  false,
176
-			expectChanged:                false,
228
+			gracePeriodRatio: 0.6,
229
+			k8sCaCertFile:    "./test-data/example-ca-cert.pem",
230
+			expectFaill:      false,
231
+			expectChanged:    false,
177 232
 		},
178 233
 	}
179 234
 
180 235
 	for _, tc := range testCases {
181
-		wc, err := NewWebhookController(tc.deleteWebhookConfigOnExit, tc.gracePeriodRatio, tc.minGracePeriod,
236
+		client := fake.NewSimpleClientset()
237
+		wc, err := NewWebhookController(tc.gracePeriodRatio, tc.minGracePeriod,
182 238
 			client.CoreV1(), client.AdmissionregistrationV1beta1(), client.CertificatesV1beta1(),
183
-			tc.k8sCaCertFile, tc.namespace, tc.mutatingWebhookConfigFiles, tc.mutatingWebhookConfigNames,
184
-			tc.mutatingWebhookSerivceNames, tc.mutatingWebhookSerivcePorts, tc.validatingWebhookConfigFiles,
185
-			tc.validatingWebhookConfigNames, tc.validatingWebhookServiceNames, tc.validatingWebhookServicePorts)
239
+			tc.k8sCaCertFile, tc.namespace, tc.mutatingWebhookSerivceNames, tc.validatingWebhookServiceNames)
186 240
 		if err != nil {
187 241
 			t.Errorf("failed at creating webhook controller: %v", err)
188 242
 			continue
@@ -209,6 +263,188 @@ func TestReloadCACert(t *testing.T) {
209 263
 	}
210 264
 }
211 265
 
266
+func TestSubmitCSR(t *testing.T) {
267
+	testCases := map[string]struct {
268
+		gracePeriodRatio              float32
269
+		minGracePeriod                time.Duration
270
+		k8sCaCertFile                 string
271
+		namespace                     string
272
+		mutatingWebhookSerivceNames   []string
273
+		validatingWebhookServiceNames []string
274
+
275
+		secretName      string
276
+		secretNameSpace string
277
+		svcName         string
278
+
279
+		createDuplicate bool
280
+		expectFaill     bool
281
+	}{
282
+		"submit a CSR without duplicate should succeed": {
283
+			gracePeriodRatio: 0.6,
284
+			k8sCaCertFile:    "./test-data/example-ca-cert.pem",
285
+			secretName:       "mock-secret",
286
+			secretNameSpace:  "mock-secret-namespace",
287
+			svcName:          "mock-service-name",
288
+			createDuplicate:  false,
289
+			expectFaill:      false,
290
+		},
291
+		"submit a CSR with duplicate should succeed": {
292
+			gracePeriodRatio: 0.6,
293
+			k8sCaCertFile:    "./test-data/example-ca-cert.pem",
294
+			secretName:       "mock-secret",
295
+			secretNameSpace:  "mock-secret-namespace",
296
+			svcName:          "mock-service-name",
297
+			createDuplicate:  false,
298
+			expectFaill:      false,
299
+		},
300
+	}
301
+
302
+	for _, tc := range testCases {
303
+		client := fake.NewSimpleClientset()
304
+		csr := &cert.CertificateSigningRequest{
305
+			ObjectMeta: metav1.ObjectMeta{
306
+				Name: "domain-cluster.local-ns--secret-mock-secret",
307
+			},
308
+			Status: cert.CertificateSigningRequestStatus{
309
+				Certificate: []byte(exampleIssuedCert),
310
+			},
311
+		}
312
+		client.PrependReactor("get", "certificatesigningrequests", defaultReactionFunc(csr))
313
+
314
+		wc, err := NewWebhookController(tc.gracePeriodRatio, tc.minGracePeriod,
315
+			client.CoreV1(), client.AdmissionregistrationV1beta1(), client.CertificatesV1beta1(),
316
+			tc.k8sCaCertFile, tc.namespace, tc.mutatingWebhookSerivceNames, tc.validatingWebhookServiceNames)
317
+		if err != nil {
318
+			t.Errorf("failed at creating webhook controller: %v", err)
319
+			continue
320
+		}
321
+
322
+		csrName := fmt.Sprintf("domain-%s-ns-%s-secret-%s", spiffe.GetTrustDomain(), tc.secretNameSpace, tc.secretName)
323
+		numRetries := 3
324
+		csrPEM := "fake-csr"
325
+
326
+		if tc.createDuplicate {
327
+			// Create a duplicate CSR to whether submitCSR() still functions correctly
328
+			k8sCSR := &cert.CertificateSigningRequest{
329
+				TypeMeta: metav1.TypeMeta{
330
+					APIVersion: "certificates.k8s.io/v1beta1",
331
+					Kind:       "CertificateSigningRequest",
332
+				},
333
+				ObjectMeta: metav1.ObjectMeta{
334
+					Name: csrName,
335
+				},
336
+				Spec: cert.CertificateSigningRequestSpec{
337
+					Request: []byte(csrPEM),
338
+					Groups:  []string{"system:authenticated"},
339
+					Usages: []cert.KeyUsage{
340
+						cert.UsageDigitalSignature,
341
+						cert.UsageKeyEncipherment,
342
+						cert.UsageServerAuth,
343
+						cert.UsageClientAuth,
344
+					},
345
+				},
346
+			}
347
+			reqRet, errRet := wc.certClient.CertificateSigningRequests().Create(k8sCSR)
348
+			if errRet == nil && reqRet != nil {
349
+				t.Errorf("failed to create a CSR, return is nil or error (%v)", errRet)
350
+				continue
351
+			}
352
+		}
353
+
354
+		r, err := submitCSR(wc, csrName, []byte(csrPEM), numRetries)
355
+		if tc.expectFaill {
356
+			if err == nil {
357
+				t.Errorf("should have failed at updateMutatingWebhookConfig")
358
+			}
359
+		} else if err != nil || r == nil {
360
+			t.Errorf("failed at updateMutatingWebhookConfig: %v", err)
361
+		}
362
+	}
363
+}
364
+
365
+func TestReadSignedCertificate(t *testing.T) {
366
+	testCases := map[string]struct {
367
+		gracePeriodRatio              float32
368
+		minGracePeriod                time.Duration
369
+		k8sCaCertFile                 string
370
+		namespace                     string
371
+		mutatingWebhookSerivceNames   []string
372
+		validatingWebhookServiceNames []string