mirror of
https://github.com/k3s-io/kubernetes.git
synced 2025-07-30 06:54:01 +00:00
add generic webhook admission controller
As part of https://github.com/kubernetes/community/pull/132, thsi commit adds a generic webhook admission controller. This plugin allows for a completely declarative approach for filtering/matching admission requests and for matching admission requests, calls out to an external webhook for handling admission requests.
This commit is contained in:
parent
5375bc0cc8
commit
b26c19bc61
@ -47,6 +47,7 @@ import (
|
||||
"k8s.io/kubernetes/plugin/pkg/admission/securitycontext/scdeny"
|
||||
"k8s.io/kubernetes/plugin/pkg/admission/serviceaccount"
|
||||
storagedefault "k8s.io/kubernetes/plugin/pkg/admission/storageclass/default"
|
||||
"k8s.io/kubernetes/plugin/pkg/admission/webhook"
|
||||
)
|
||||
|
||||
// registerAllAdmissionPlugins registers all admission plugins
|
||||
@ -73,4 +74,5 @@ func registerAllAdmissionPlugins(plugins *admission.Plugins) {
|
||||
scdeny.Register(plugins)
|
||||
serviceaccount.Register(plugins)
|
||||
storagedefault.Register(plugins)
|
||||
webhook.Register(plugins)
|
||||
}
|
||||
|
164
plugin/pkg/admission/webhook/admission.go
Normal file
164
plugin/pkg/admission/webhook/admission.go
Normal file
@ -0,0 +1,164 @@
|
||||
/*
|
||||
Copyright 2017 The Kubernetes Authors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
// Package webhook checks a webhook for configured operation admission
|
||||
package webhook
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
|
||||
"github.com/golang/glog"
|
||||
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
"k8s.io/apimachinery/pkg/util/yaml"
|
||||
"k8s.io/apiserver/pkg/admission"
|
||||
"k8s.io/apiserver/pkg/util/webhook"
|
||||
|
||||
"k8s.io/kubernetes/pkg/api"
|
||||
admissionv1alpha1 "k8s.io/kubernetes/pkg/apis/admission/v1alpha1"
|
||||
|
||||
// install the clientgo admissiony API for use with api registry
|
||||
_ "k8s.io/kubernetes/pkg/apis/admission/install"
|
||||
)
|
||||
|
||||
var (
|
||||
groupVersions = []schema.GroupVersion{
|
||||
admissionv1alpha1.SchemeGroupVersion,
|
||||
}
|
||||
)
|
||||
|
||||
// Register registers a plugin
|
||||
func Register(plugins *admission.Plugins) {
|
||||
plugins.Register("GenericAdmissionWebhook", func(configFile io.Reader) (admission.Interface, error) {
|
||||
var gwhConfig struct {
|
||||
WebhookConfig GenericAdmissionWebhookConfig `json:"webhook"`
|
||||
}
|
||||
|
||||
d := yaml.NewYAMLOrJSONDecoder(configFile, 4096)
|
||||
err := d.Decode(&gwhConfig)
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
plugin, err := NewGenericAdmissionWebhook(&gwhConfig.WebhookConfig)
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return plugin, nil
|
||||
})
|
||||
}
|
||||
|
||||
// NewGenericAdmissionWebhook returns a generic admission webhook plugin.
|
||||
func NewGenericAdmissionWebhook(config *GenericAdmissionWebhookConfig) (admission.Interface, error) {
|
||||
err := normalizeConfig(config)
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
gw, err := webhook.NewGenericWebhook(api.Registry, api.Codecs, config.KubeConfigFile, groupVersions, config.RetryBackoff)
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &GenericAdmissionWebhook{
|
||||
Handler: admission.NewHandler(admission.Connect, admission.Create, admission.Delete, admission.Update),
|
||||
webhook: gw,
|
||||
rules: config.Rules,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// GenericAdmissionWebhook is an implementation of admission.Interface.
|
||||
type GenericAdmissionWebhook struct {
|
||||
*admission.Handler
|
||||
webhook *webhook.GenericWebhook
|
||||
rules []Rule
|
||||
}
|
||||
|
||||
// Admit makes an admission decision based on the request attributes.
|
||||
func (a *GenericAdmissionWebhook) Admit(attr admission.Attributes) (err error) {
|
||||
var matched *Rule
|
||||
|
||||
// Process all declared rules to attempt to find a match
|
||||
for i, rule := range a.rules {
|
||||
if Matches(rule, attr) {
|
||||
glog.V(2).Infof("rule at index %d matched request", i)
|
||||
matched = &a.rules[i]
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if matched == nil {
|
||||
glog.V(2).Infof("rule explicitly allowed the request: no rule matched the admission request")
|
||||
return nil
|
||||
}
|
||||
|
||||
// The matched rule skips processing this request
|
||||
if matched.Type == Skip {
|
||||
glog.V(2).Infof("rule explicitly allowed the request")
|
||||
return nil
|
||||
}
|
||||
|
||||
// Make the webhook request
|
||||
request := admissionv1alpha1.NewAdmissionReview(attr)
|
||||
response := a.webhook.RestClient.Post().Body(&request).Do()
|
||||
|
||||
// Handle webhook response
|
||||
if err := response.Error(); err != nil {
|
||||
return a.handleError(attr, matched.FailAction, err)
|
||||
}
|
||||
|
||||
var statusCode int
|
||||
if response.StatusCode(&statusCode); statusCode < 200 || statusCode >= 300 {
|
||||
return a.handleError(attr, matched.FailAction, fmt.Errorf("error contacting webhook: %d", statusCode))
|
||||
}
|
||||
|
||||
if err := response.Into(&request); err != nil {
|
||||
return a.handleError(attr, matched.FailAction, err)
|
||||
}
|
||||
|
||||
if !request.Status.Allowed {
|
||||
if request.Status.Result != nil && len(request.Status.Result.Reason) > 0 {
|
||||
return a.handleError(attr, Deny, fmt.Errorf("webhook backend denied the request: %s", request.Status.Result.Reason))
|
||||
}
|
||||
return a.handleError(attr, Deny, errors.New("webhook backend denied the request"))
|
||||
}
|
||||
|
||||
// The webhook admission controller DOES NOT allow mutation of the admission request so nothing else is required
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (a *GenericAdmissionWebhook) handleError(attr admission.Attributes, allowIfErr FailAction, err error) error {
|
||||
if err != nil {
|
||||
glog.V(2).Infof("error contacting webhook backend: %s", err)
|
||||
if allowIfErr != Allow {
|
||||
glog.V(2).Infof("resource not allowed due to webhook backend failure: %s", err)
|
||||
return admission.NewForbidden(attr, err)
|
||||
}
|
||||
glog.V(2).Infof("resource allowed in spite of webhook backend failure")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// TODO: Allow configuring the serialization strategy
|
369
plugin/pkg/admission/webhook/admission_test.go
Normal file
369
plugin/pkg/admission/webhook/admission_test.go
Normal file
@ -0,0 +1,369 @@
|
||||
/*
|
||||
Copyright 2017 The Kubernetes Authors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package webhook
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/tls"
|
||||
"crypto/x509"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"testing"
|
||||
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apiserver/pkg/admission"
|
||||
"k8s.io/apiserver/pkg/authentication/user"
|
||||
"k8s.io/client-go/pkg/api"
|
||||
"k8s.io/client-go/tools/clientcmd/api/v1"
|
||||
"k8s.io/kubernetes/pkg/apis/admission/v1alpha1"
|
||||
|
||||
_ "k8s.io/kubernetes/pkg/apis/admission/install"
|
||||
)
|
||||
|
||||
const (
|
||||
errAdmitPrefix = `pods "my-pod" is forbidden: `
|
||||
)
|
||||
|
||||
var (
|
||||
requestCount int
|
||||
testRoot string
|
||||
testServer *httptest.Server
|
||||
)
|
||||
|
||||
type genericAdmissionWebhookTest struct {
|
||||
test string
|
||||
config *GenericAdmissionWebhookConfig
|
||||
value interface{}
|
||||
}
|
||||
|
||||
func TestMain(m *testing.M) {
|
||||
tmpRoot, err := ioutil.TempDir("", "")
|
||||
|
||||
if err != nil {
|
||||
killTests(err)
|
||||
}
|
||||
|
||||
testRoot = tmpRoot
|
||||
|
||||
// Cleanup
|
||||
defer os.RemoveAll(testRoot)
|
||||
|
||||
// Create the test webhook server
|
||||
cert, err := tls.X509KeyPair(clientCert, clientKey)
|
||||
|
||||
if err != nil {
|
||||
killTests(err)
|
||||
}
|
||||
|
||||
rootCAs := x509.NewCertPool()
|
||||
|
||||
rootCAs.AppendCertsFromPEM(caCert)
|
||||
|
||||
testServer = httptest.NewUnstartedServer(http.HandlerFunc(webhookHandler))
|
||||
|
||||
testServer.TLS = &tls.Config{
|
||||
Certificates: []tls.Certificate{cert},
|
||||
ClientCAs: rootCAs,
|
||||
ClientAuth: tls.RequireAndVerifyClientCert,
|
||||
}
|
||||
|
||||
// Create the test webhook server
|
||||
testServer.StartTLS()
|
||||
|
||||
// Cleanup
|
||||
defer testServer.Close()
|
||||
|
||||
// Create an invalid and valid Kubernetes configuration file
|
||||
var kubeConfig bytes.Buffer
|
||||
|
||||
err = json.NewEncoder(&kubeConfig).Encode(v1.Config{
|
||||
Clusters: []v1.NamedCluster{
|
||||
{
|
||||
Cluster: v1.Cluster{
|
||||
Server: testServer.URL,
|
||||
CertificateAuthorityData: caCert,
|
||||
},
|
||||
},
|
||||
},
|
||||
AuthInfos: []v1.NamedAuthInfo{
|
||||
{
|
||||
AuthInfo: v1.AuthInfo{
|
||||
ClientCertificateData: clientCert,
|
||||
ClientKeyData: clientKey,
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
killTests(err)
|
||||
}
|
||||
|
||||
// The files needed on disk for the webhook tests
|
||||
files := map[string][]byte{
|
||||
"ca.pem": caCert,
|
||||
"client.pem": clientCert,
|
||||
"client-key.pem": clientKey,
|
||||
"kube-config": kubeConfig.Bytes(),
|
||||
}
|
||||
|
||||
// Write the certificate files to disk or fail
|
||||
for fileName, fileData := range files {
|
||||
if err := ioutil.WriteFile(filepath.Join(testRoot, fileName), fileData, 0400); err != nil {
|
||||
killTests(err)
|
||||
}
|
||||
}
|
||||
|
||||
// Run the tests
|
||||
m.Run()
|
||||
}
|
||||
|
||||
// TestNewGenericAdmissionWebhook tests that NewGenericAdmissionWebhook works as expected
|
||||
func TestNewGenericAdmissionWebhook(t *testing.T) {
|
||||
tests := []genericAdmissionWebhookTest{
|
||||
genericAdmissionWebhookTest{
|
||||
test: "Empty webhook config",
|
||||
config: &GenericAdmissionWebhookConfig{},
|
||||
value: fmt.Errorf("kubeConfigFile is required"),
|
||||
},
|
||||
genericAdmissionWebhookTest{
|
||||
test: "Broken webhook config",
|
||||
config: &GenericAdmissionWebhookConfig{
|
||||
KubeConfigFile: filepath.Join(testRoot, "kube-config.missing"),
|
||||
Rules: []Rule{
|
||||
Rule{
|
||||
Type: Skip,
|
||||
},
|
||||
},
|
||||
},
|
||||
value: fmt.Errorf("stat .*kube-config.missing.*"),
|
||||
},
|
||||
genericAdmissionWebhookTest{
|
||||
test: "Valid webhook config",
|
||||
config: &GenericAdmissionWebhookConfig{
|
||||
KubeConfigFile: filepath.Join(testRoot, "kube-config"),
|
||||
Rules: []Rule{
|
||||
Rule{
|
||||
Type: Skip,
|
||||
},
|
||||
},
|
||||
},
|
||||
value: nil,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
_, err := NewGenericAdmissionWebhook(tt.config)
|
||||
|
||||
checkForError(t, tt.test, tt.value, err)
|
||||
}
|
||||
}
|
||||
|
||||
// TestAdmit tests that GenericAdmissionWebhook#Admit works as expected
|
||||
func TestAdmit(t *testing.T) {
|
||||
configWithFailAction := makeGoodConfig()
|
||||
kind := api.Kind("Pod").WithVersion("v1")
|
||||
name := "my-pod"
|
||||
namespace := "webhook-test"
|
||||
object := api.Pod{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Labels: map[string]string{
|
||||
"pod.name": name,
|
||||
},
|
||||
Name: name,
|
||||
Namespace: namespace,
|
||||
},
|
||||
TypeMeta: metav1.TypeMeta{
|
||||
APIVersion: "v1",
|
||||
Kind: "Pod",
|
||||
},
|
||||
}
|
||||
oldObject := api.Pod{
|
||||
ObjectMeta: metav1.ObjectMeta{Name: name, Namespace: namespace},
|
||||
}
|
||||
operation := admission.Update
|
||||
resource := api.Resource("pods").WithVersion("v1")
|
||||
subResource := ""
|
||||
userInfo := user.DefaultInfo{
|
||||
Name: "webhook-test",
|
||||
UID: "webhook-test",
|
||||
}
|
||||
|
||||
configWithFailAction.Rules[0].FailAction = Allow
|
||||
|
||||
tests := []genericAdmissionWebhookTest{
|
||||
genericAdmissionWebhookTest{
|
||||
test: "No matching rule",
|
||||
config: &GenericAdmissionWebhookConfig{
|
||||
KubeConfigFile: filepath.Join(testRoot, "kube-config"),
|
||||
Rules: []Rule{
|
||||
Rule{
|
||||
Operations: []admission.Operation{
|
||||
admission.Create,
|
||||
},
|
||||
Type: Send,
|
||||
},
|
||||
},
|
||||
},
|
||||
value: nil,
|
||||
},
|
||||
genericAdmissionWebhookTest{
|
||||
test: "Matching rule skips",
|
||||
config: &GenericAdmissionWebhookConfig{
|
||||
KubeConfigFile: filepath.Join(testRoot, "kube-config"),
|
||||
Rules: []Rule{
|
||||
Rule{
|
||||
Operations: []admission.Operation{
|
||||
admission.Update,
|
||||
},
|
||||
Type: Skip,
|
||||
},
|
||||
},
|
||||
},
|
||||
value: nil,
|
||||
},
|
||||
genericAdmissionWebhookTest{
|
||||
test: "Matching rule sends (webhook internal server error)",
|
||||
config: makeGoodConfig(),
|
||||
value: fmt.Errorf(`%san error on the server ("webhook internal server error") has prevented the request from succeeding`, errAdmitPrefix),
|
||||
},
|
||||
genericAdmissionWebhookTest{
|
||||
test: "Matching rule sends (webhook unsuccessful response)",
|
||||
config: makeGoodConfig(),
|
||||
value: fmt.Errorf("%serror contacting webhook: 101", errAdmitPrefix),
|
||||
},
|
||||
genericAdmissionWebhookTest{
|
||||
test: "Matching rule sends (webhook unmarshallable response)",
|
||||
config: makeGoodConfig(),
|
||||
value: fmt.Errorf("%scouldn't get version/kind; json parse error: json: cannot unmarshal string into Go value of type struct.*", errAdmitPrefix),
|
||||
},
|
||||
genericAdmissionWebhookTest{
|
||||
test: "Matching rule sends (webhook error but allowed via fail action)",
|
||||
config: configWithFailAction,
|
||||
value: nil,
|
||||
},
|
||||
genericAdmissionWebhookTest{
|
||||
test: "Matching rule sends (webhook request denied without reason)",
|
||||
config: makeGoodConfig(),
|
||||
value: fmt.Errorf("%swebhook backend denied the request", errAdmitPrefix),
|
||||
},
|
||||
genericAdmissionWebhookTest{
|
||||
test: "Matching rule sends (webhook request denied with reason)",
|
||||
config: makeGoodConfig(),
|
||||
value: fmt.Errorf("%swebhook backend denied the request: you shall not pass", errAdmitPrefix),
|
||||
},
|
||||
genericAdmissionWebhookTest{
|
||||
test: "Matching rule sends (webhook request allowed)",
|
||||
config: makeGoodConfig(),
|
||||
value: nil,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
wh, err := NewGenericAdmissionWebhook(tt.config)
|
||||
|
||||
if err != nil {
|
||||
t.Errorf("%s: unexpected error: %v", tt.test, err)
|
||||
} else {
|
||||
err = wh.Admit(admission.NewAttributesRecord(&object, &oldObject, kind, namespace, name, resource, subResource, operation, &userInfo))
|
||||
|
||||
checkForError(t, tt.test, tt.value, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func checkForError(t *testing.T, test string, expected, actual interface{}) {
|
||||
aErr, _ := actual.(error)
|
||||
eErr, _ := expected.(error)
|
||||
|
||||
if eErr != nil {
|
||||
if aErr == nil {
|
||||
t.Errorf("%s: expected an error", test)
|
||||
} else if eErr.Error() != aErr.Error() && !regexp.MustCompile(eErr.Error()).MatchString(aErr.Error()) {
|
||||
t.Errorf("%s: unexpected error message to match:\n Expected: %s\n Actual: %s", test, eErr.Error(), aErr.Error())
|
||||
}
|
||||
} else {
|
||||
if aErr != nil {
|
||||
t.Errorf("%s: unexpected error: %v", test, aErr)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func killTests(err error) {
|
||||
panic(fmt.Sprintf("Unable to bootstrap tests: %v", err))
|
||||
}
|
||||
|
||||
func makeGoodConfig() *GenericAdmissionWebhookConfig {
|
||||
return &GenericAdmissionWebhookConfig{
|
||||
KubeConfigFile: filepath.Join(testRoot, "kube-config"),
|
||||
Rules: []Rule{
|
||||
Rule{
|
||||
Operations: []admission.Operation{
|
||||
admission.Update,
|
||||
},
|
||||
Type: Send,
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func webhookHandler(w http.ResponseWriter, r *http.Request) {
|
||||
requestCount++
|
||||
|
||||
switch requestCount {
|
||||
case 1, 4:
|
||||
http.Error(w, "webhook internal server error", http.StatusInternalServerError)
|
||||
return
|
||||
case 2:
|
||||
w.WriteHeader(http.StatusSwitchingProtocols)
|
||||
w.Write([]byte("webhook invalid request"))
|
||||
return
|
||||
case 3:
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.Write([]byte("webhook invalid response"))
|
||||
case 5:
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(&v1alpha1.AdmissionReview{
|
||||
Status: v1alpha1.AdmissionReviewStatus{
|
||||
Allowed: false,
|
||||
},
|
||||
})
|
||||
case 6:
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(&v1alpha1.AdmissionReview{
|
||||
Status: v1alpha1.AdmissionReviewStatus{
|
||||
Allowed: false,
|
||||
Result: &metav1.Status{
|
||||
Reason: "you shall not pass",
|
||||
},
|
||||
},
|
||||
})
|
||||
case 7:
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(&v1alpha1.AdmissionReview{
|
||||
Status: v1alpha1.AdmissionReviewStatus{
|
||||
Allowed: true,
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
218
plugin/pkg/admission/webhook/certs_test.go
Normal file
218
plugin/pkg/admission/webhook/certs_test.go
Normal file
@ -0,0 +1,218 @@
|
||||
/*
|
||||
Copyright 2017 The Kubernetes Authors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
// This file was generated using openssl by the gencerts.sh script
|
||||
// and holds raw certificates for the webhook tests.
|
||||
|
||||
package webhook
|
||||
|
||||
var caKey = []byte(`-----BEGIN RSA PRIVATE KEY-----
|
||||
MIIEowIBAAKCAQEA5qdHpJR3sAg1afEWLlnxdbDV1LtAvCd9WWE/4g71+BBwvwLL
|
||||
C/piPRi+m/7AYHJ9zoVwvwAUYBJM2ppQV2Gwsq4XA5dLvr8fXt4+YA/sLTAo0PAm
|
||||
/QQmZziHU9v7OJ04ypjTTuA86D/EkkpEP7lRkN/NFpV3PhU0F5TTrkrzSpePBIR1
|
||||
aPSzh/6Um6TtFk9oiqat05leWcMzonizrgeQU9EW6bSYY5w+gq1X2eZb1s9eR98H
|
||||
0xcZfoh3qHmJ1Iq9Cc2Nr+MUohm2m45ozT+1L+g46ZMJxyPB6xLr7uOhkoJsK30q
|
||||
67AZKPo58tDTmEKSXfBotIvFI5N9P3sAWxcTiQIDAQABAoIBAAaJHOWT82RAh0ru
|
||||
MuOzVr0v+o8hky8Bq3KZ59Z++AdEZ/1xldFMEfaLOfNvn4HcHKZ6b3xqAynJuvXC
|
||||
w54GPZyChFJsug+4mKn2gCv2p4mMQMvS0jf/IxtvpZ4BsLek9NQAypQElJU8IVTH
|
||||
1/E6Tg5d2RDXwV43+Zbld64Ln6MwZGwv8UFPEHylDMjwkex5u3tzVBD5NaegI0MD
|
||||
AHAf3fiCsANmAeGWjTvUXQsOes6wjaHw6kbih5QrXM6iThHfU/YHXYmgfdfSSIFC
|
||||
4puLaehp3/U8HcI97xN238B0khnkOVHzUmRJpmf17SWFSkOAZyUMiFfTSFSOedvu
|
||||
lFO7v0UCgYEA/FczxADDZXq4SSTvrU61XzolgI//OKblsqe4RW8JGtk8pwjZdYOQ
|
||||
v4UAYEcv5rUWA3wWohcjgNWzI9EzdhOCYpC9YFqbHJalmwhGgOjRCvDy581pcXz7
|
||||
xsfkm2loWm3g+PcrCIset2tGQw/5gqSkBW42E/U0ba25wCS6RlYWCFsCgYEA6f+Q
|
||||
vENXPmBUBW8TsyjWj7MZKVzKuV2yyKT37Mf7RmrpSNpft0PLJDqGqRMfhe1J7Adm
|
||||
Np1fv/18RngjGjV3fSnjbvQ51748gabwzGZKWKiWJPsRDqjEriiQcFInhaqVJs7F
|
||||
D0TaWalBHWyjPHCQQx7rWqA8tr3Wpga0AhNUuOsCgYEAnbrQU7L6cEM+UBIzcswh
|
||||
GO4apPrdWIcSSxMFXvlh4pNpkys36nmbj+tN6eB1c6s7oF//McBu48gwWrIYjbTy
|
||||
KjQ4+7KHBF6yE28fytI8YK9t1jESuOqb4ovuPKqtnODT4Ct3jbaQM6xtVdv1ZZEO
|
||||
KYrTaLQ72lbeJdmPSgnjacMCgYASip6kXE2ocqeVuqR7+MtvnYhr359sqsEE5xWC
|
||||
HKKLhOMxU6Rr+CI7n6uV8B76VMAbxMZTo4q3wtU7HD/jzsLGFzCfVRjUQI241EqW
|
||||
V7Cib9Fd4ssKN1NGXY58Z/YbwFWLOq0gtZr7qc6wDzCsFFtKBkQt7S6CaG5+v186
|
||||
HuACuwKBgEd0JaREFj6AG6boytQAx+Npj+wGG7K22O8v9YslJjaS5t2i8XrvIr0F
|
||||
5ltR8Ijegp9a2pCgjshaEzUqMHhxX3EGvUxVM4R430EaQ933WRPmiLlVLyhthYt0
|
||||
9oPxMoN783J7UP/IkBc6AGi3a4uTn/h6Vi884wOLop4bmsK37uIt
|
||||
-----END RSA PRIVATE KEY-----`)
|
||||
|
||||
var caCert = []byte(`-----BEGIN CERTIFICATE-----
|
||||
MIIDhDCCAmygAwIBAgIJAIf67NAEFfGmMA0GCSqGSIb3DQEBBQUAMDQxMjAwBgNV
|
||||
BAMUKWdlbmVyaWNfd2ViaG9va19hZG1pc3Npb25fcGx1Z2luX3Rlc3RzX2NhMCAX
|
||||
DTE3MDUwNDIyMzMyOFoYDzIyOTEwMjE3MjIzMzI4WjA0MTIwMAYDVQQDFClnZW5l
|
||||
cmljX3dlYmhvb2tfYWRtaXNzaW9uX3BsdWdpbl90ZXN0c19jYTCCASIwDQYJKoZI
|
||||
hvcNAQEBBQADggEPADCCAQoCggEBAOanR6SUd7AINWnxFi5Z8XWw1dS7QLwnfVlh
|
||||
P+IO9fgQcL8Cywv6Yj0Yvpv+wGByfc6FcL8AFGASTNqaUFdhsLKuFwOXS76/H17e
|
||||
PmAP7C0wKNDwJv0EJmc4h1Pb+zidOMqY007gPOg/xJJKRD+5UZDfzRaVdz4VNBeU
|
||||
065K80qXjwSEdWj0s4f+lJuk7RZPaIqmrdOZXlnDM6J4s64HkFPRFum0mGOcPoKt
|
||||
V9nmW9bPXkffB9MXGX6Id6h5idSKvQnNja/jFKIZtpuOaM0/tS/oOOmTCccjwesS
|
||||
6+7joZKCbCt9KuuwGSj6OfLQ05hCkl3waLSLxSOTfT97AFsXE4kCAwEAAaOBljCB
|
||||
kzAdBgNVHQ4EFgQU55hOE1Dsydy16+6wgOxwsPKZ8JEwZAYDVR0jBF0wW4AU55hO
|
||||
E1Dsydy16+6wgOxwsPKZ8JGhOKQ2MDQxMjAwBgNVBAMUKWdlbmVyaWNfd2ViaG9v
|
||||
a19hZG1pc3Npb25fcGx1Z2luX3Rlc3RzX2NhggkAh/rs0AQV8aYwDAYDVR0TBAUw
|
||||
AwEB/zANBgkqhkiG9w0BAQUFAAOCAQEAtEa7AMnFdc/GmVrtY0aOmA3h1WJ/rhhd
|
||||
SnVOp7LmA+1jnA6FMMvVCOFflYLfBwLhIPjPj4arQadWHyd5Quok6GIqgzL4RkK7
|
||||
67hPMc8inNH8w+9K0kgsCls06Jy08NIt7QTtIckh8skvxQsfJ6An/ROiCNI5QiYj
|
||||
oQOK3Tp8jGf/2wcsJLeO9y09ZPcOUbLkDe2YlnT+OqNMx9VXrPSvRwq2qtYphgYW
|
||||
YWHPMEBnB/9XrVkEtnKeWPjtjarVd+rNnfVpCW0ImnZRFtKQQeI4rtf4Gr83Gdju
|
||||
Z0gPepfIFptDMl5wKWyw4o2XVSZ69Ur8tQQynoNyX/FTIx6tQ//hzg==
|
||||
-----END CERTIFICATE-----`)
|
||||
|
||||
var badCAKey = []byte(`-----BEGIN RSA PRIVATE KEY-----
|
||||
MIIEpAIBAAKCAQEA2Mbqfl5nW+rYCSw/+G00ymtBElvTwzEI88sg3m5PXU0XMv5y
|
||||
Tyjt+S3mhbzLY460ty6fXlEiveNNP0bzbo0k+tvUj7HbOG4q4Cs0JWiLvibchRXQ
|
||||
LimqtndsVsT2xj56m7oTETWMYhjd8KFz1s6w7yK9dChe2pBCeeGjIbQs0def8Bf0
|
||||
5h7BbsmuXQb44nnpUdWgFI/Frqcz6jO6jySYV5U3ajqCjcWOBQmXRtMi1we48Hdj
|
||||
2t4jW/evbRIo/tX88kluXFSVBmTitKA3nn23AzA+/qa6kTDwzQ8OntjC09JT7ykS
|
||||
764WLRiBOo+wC3a6M3eZNMkW615+DNP7wAjYEwIDAQABAoIBAC9lZnXUvDKLqUpw
|
||||
I1h0wBsV0jdqXmWJ/hQXsIsRgUa8CTt8CJAoOcfGcmWBPtL4q6h1iCC+CqOL5CLW
|
||||
p3jfYVt73wC/+VdgNv2mVJNtRUiBBKwQdeDx+UJF4CkkjXQQywvrZinYFGaKW1Q2
|
||||
aLZpoKPYa6XPAdY1vmMZo2pGE5qZbGbIXKcl3kl5N2X0qyJKvTR6RA1Q8JiTmATh
|
||||
H1U3S9K/MIGU+9OwP8zCDVKFdDI+xgJvDo05W8bt6XLfmE4bLRYHRfe0iE0uBNSC
|
||||
zx5OGBNqkiHq/vMk5mLCYM0w/uzUkhB5uXqe5gSqa7DioLrHztqAe+sbtBvfL+Yd
|
||||
hP3asWECgYEA9NQXa2B6AVXVf2GII67H8diPFrILKLEw4CUZjFmVbvrKIRB2ypqA
|
||||
IsohpYl97s0nstetZt/75uEvkv3RPu5ad5LJaVAPHE3ESXKdAz6u4fSmi33qqPi6
|
||||
PvhHLYDDXvMgC7j2yCYyANVKNg2T+EJvpWMISlXM4h7CVm21CSDVZjECgYEA4qsl
|
||||
zDA3sHoClfC4nAOVrRghYlHU2bT6HPLxjLtBkcUTfj6nO6nOLGC7EFVkqYw5mUWq
|
||||
uSNntk1D1+MYVnZBeKqw6y21FFsclmzzXTtAJg0vuAg1jxnMHqiDbNqpUUO8ZWLG
|
||||
iz2tdAWiBAKwZv0Psv44Dy++4v/BMd8FBb/iHYMCgYEAttKmRmW91b9t9Xg0fEjp
|
||||
QBzyBQWhNZrTn520rUy8PSqDxBsSSgsDgncklwPMCYYjjfZmo3rBFdC0gPSOy4qb
|
||||
/cycIMtK7VzZJeuzehfV6h+SOnolwFY0Zg9qv3z257FwDbDqf92d22dqymBrTaj2
|
||||
zC7eovvdSkGj53x3AsEE+hECgYAd7JJU3pi7h6AHw3vbvO1pqKHfpQYAp8/NOpWB
|
||||
CsehQu9L32GcktJRMYQAqAVeDNEd1wCu6Gmsu46VVbnE0F/cWkx4/9PEGDMx+Lg4
|
||||
OrZBT8RY+1x2w+UatwyCtmtb+yFIET4866uWgZfeB6zaK9aCvuUPvDHrLfCHcPXs
|
||||
yGRFmQKBgQDYvcjkUqKvnZwwpV6p6BHi6PuHWyjL+GLTk/IzsxOJykHzX4SV46Sd
|
||||
9IxyE+OWZhkISBMSMe8wQC/S+QYs8hDbEE3WH1DXp33joyNgt2aWI0Splu7S1f6P
|
||||
Pe08fYlLgz1xrbJuJAOgvkbskAUcmfmRwFeGaZSkkCtkzVxgN7LS3Q==
|
||||
-----END RSA PRIVATE KEY-----`)
|
||||
|
||||
var badCACert = []byte(`-----BEGIN CERTIFICATE-----
|
||||
MIIDhDCCAmygAwIBAgIJAIabsJi6ILN7MA0GCSqGSIb3DQEBBQUAMDQxMjAwBgNV
|
||||
BAMUKWdlbmVyaWNfd2ViaG9va19hZG1pc3Npb25fcGx1Z2luX3Rlc3RzX2NhMCAX
|
||||
DTE3MDUwNDIyMzMyOFoYDzIyOTEwMjE3MjIzMzI4WjA0MTIwMAYDVQQDFClnZW5l
|
||||
cmljX3dlYmhvb2tfYWRtaXNzaW9uX3BsdWdpbl90ZXN0c19jYTCCASIwDQYJKoZI
|
||||
hvcNAQEBBQADggEPADCCAQoCggEBANjG6n5eZ1vq2AksP/htNMprQRJb08MxCPPL
|
||||
IN5uT11NFzL+ck8o7fkt5oW8y2OOtLcun15RIr3jTT9G826NJPrb1I+x2zhuKuAr
|
||||
NCVoi74m3IUV0C4pqrZ3bFbE9sY+epu6ExE1jGIY3fChc9bOsO8ivXQoXtqQQnnh
|
||||
oyG0LNHXn/AX9OYewW7Jrl0G+OJ56VHVoBSPxa6nM+ozuo8kmFeVN2o6go3FjgUJ
|
||||
l0bTItcHuPB3Y9reI1v3r20SKP7V/PJJblxUlQZk4rSgN559twMwPv6mupEw8M0P
|
||||
Dp7YwtPSU+8pEu+uFi0YgTqPsAt2ujN3mTTJFutefgzT+8AI2BMCAwEAAaOBljCB
|
||||
kzAdBgNVHQ4EFgQU9SUu1vDxGeyGEgy7pzZjLm+WSH0wZAYDVR0jBF0wW4AU9SUu
|
||||
1vDxGeyGEgy7pzZjLm+WSH2hOKQ2MDQxMjAwBgNVBAMUKWdlbmVyaWNfd2ViaG9v
|
||||
a19hZG1pc3Npb25fcGx1Z2luX3Rlc3RzX2NhggkAhpuwmLogs3swDAYDVR0TBAUw
|
||||
AwEB/zANBgkqhkiG9w0BAQUFAAOCAQEAViib1ZcwLUi5YE279NRypsGLa8pfT/A8
|
||||
10u41L4xw+32mD2HojMmAlAF4jnC62exaXFsAYEsze4TF+0zqwDkHyGqViw/hKAv
|
||||
SrGgPUX3C7wLyiMa3pjZfcQQy+80SKiJLeClxxjkdhO0mGNo1LdJThYU5IADHVtF
|
||||
u2oOKLTjWBVzkMRkTXp5RReeEoUPvFgJhPKIVLggdXdJT8oQjgIVlx6IuzjU0AeM
|
||||
tJ5AIWYrsqv9FlpfUXWjdiy8uF3iLWTOpd6pICnjzfj02wQouTEkxQ2iFinl7Das
|
||||
iK+7d34q6Ww1/1nu4EBBDYB1VlWdhDLJVT4F+mF8wZFBu863Ba+U5Q==
|
||||
-----END CERTIFICATE-----`)
|
||||
|
||||
var serverKey = []byte(`-----BEGIN RSA PRIVATE KEY-----
|
||||
MIIEowIBAAKCAQEAquioChbTG+iyTg3+SwGww/7yN84jtj55v8Xld8TgkyavSZm7
|
||||
Q6WURevvXtJHMug/NAz6qoCFy/zOiozUrMB36GFOA8MtwaOddCMdHMruX7q2+CiF
|
||||
amhFg58uPfVr9qit20JiyBhaPH97WmQzwsfRQt4E1mCbEHZYK6r1RlF3i00nDCxc
|
||||
741wuYp4Uc5oYM0dYoVCv11DYdf62v1grLFO02a5qjAULS74KKNaJ4YDOmqnTtWt
|
||||
I75ZkEJSC2TT90o8eFIRuImY17venqIbp30A+XtvMhITg55648Zdci+CGHDcqJA1
|
||||
C+y0rxXZkcuFFFrK0tiN7K/cPK1LDtS08gp61QIDAQABAoIBAFhGVxTu+Rc/N2lt
|
||||
fNzNALobIoyEYpms50GQO5eDDuOyZXNEfh7QlScQV9DIF5JJtutxkL8kJvdXmm6h
|
||||
ku+vcb+LErqKw0Vy9s6XnF/UyQ6U6BCBDXgKZ202eLHz41HBihrnzRHA0krRJato
|
||||
efuvLXy2JBV+TFlSZvQXFxy801wVIxlI7Jh94YR7CT3HYmD5qjAtiTVkSS+wmx++
|
||||
cfh0rrMulkXDEUlRPm/fXqt0do+neH9eNYee4h0mbZ84f3ecz/ql3HfkfoK9ZBq+
|
||||
M85VWnEvRetRF0AQlYK3IUPQH2XHIEZkUab8s+EZoIsgVBY9xvl6VTEQWFtbzRWC
|
||||
7ozg2PkCgYEA3X8Pv6a5wzWBwX17RkTXcPqS44uzJ/K0adv1QSrVvXrZJQ0FEGqr
|
||||
gR74aQXiaVl06X/N50y/soQIHWpvqGjoEbkA+jS5y4GkGxAviFFAuRnW9DyRePxH
|
||||
nQiYFzgxBj55iDsdvdqJ2e0vM1EWNcaVmghNI25D+cBc4Qh1Zz4qFSsCgYEAxYg9
|
||||
vYGepcTMJ0/dk4SVMaITi/8nAnZHnptILpSjgPNp8udsovQPkymwo+EDnjQqVPO4
|
||||
OIIICEopk6Zup0Rq2iW3zRGbRJtp+uJ7mfFS+nT6sAe0tPWbGejwrudDDcx0fkx4
|
||||
C+1//rJ95H0c9L4nd51azCJD0k2yKtIFyj91r/8CgYBwMkWa8exU+oyQo2xHSuXK
|
||||
n9K6GnCUwrcqjDWuXfFI+qp1vyOajj3zuOlh4Y4viRXUlV2KVXEhDwpBREHtD77G
|
||||
A22AUCbw8+lZoBhDt8zONk2RCAE0RK5N2CWaVWdX31uWa0OEgOelESUAnIlgkggD
|
||||
r0LLuLYME6m4f51gv7d3YwKBgCFp8He0C3AjIB2uRt8DWHFy5zeRS7oA5BCSV915
|
||||
S0cu5ccvGpNeEZxlOvodwAzs6hRAvfLhHBa65NmTF7i3vBN2uea4iblLSNwln57k
|
||||
0ZKIYzePtiO+QCRb4QrVF+SnpzUOHmh2HmapLt6Nw24rFGYJeih5y1sxxWe060HR
|
||||
BkllAoGBAMkT1a3BhhEwoyyKiwC05+gzlKfAWz7t6J//6/yx+82lXDk4/J455qcw
|
||||
ny2y6P6r964EUoqMrAU0bdTs3sKDtOLdNMIt5RfoDBsdQDt2ktbv0pvii3E8SQFi
|
||||
JuJWSenrfFaI+AgwE9jDo1Hy6dhF6/hnV3+QoznwEPRAO6wmPyVA
|
||||
-----END RSA PRIVATE KEY-----`)
|
||||
|
||||
var serverCert = []byte(`-----BEGIN CERTIFICATE-----
|
||||
MIIDOzCCAiOgAwIBAgIJAN0PSMLOjTVAMA0GCSqGSIb3DQEBBQUAMDQxMjAwBgNV
|
||||
BAMUKWdlbmVyaWNfd2ViaG9va19hZG1pc3Npb25fcGx1Z2luX3Rlc3RzX2NhMCAX
|
||||
DTE3MDUwNDIyMzMyOFoYDzIyOTEwMjE3MjIzMzI4WjA4MTYwNAYDVQQDDC1nZW5l
|
||||
cmljX3dlYmhvb2tfYWRtaXNzaW9uX3BsdWdpbl90ZXN0c19zZXJ2ZXIwggEiMA0G
|
||||
CSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCq6KgKFtMb6LJODf5LAbDD/vI3ziO2
|
||||
Pnm/xeV3xOCTJq9JmbtDpZRF6+9e0kcy6D80DPqqgIXL/M6KjNSswHfoYU4Dwy3B
|
||||
o510Ix0cyu5furb4KIVqaEWDny499Wv2qK3bQmLIGFo8f3taZDPCx9FC3gTWYJsQ
|
||||
dlgrqvVGUXeLTScMLFzvjXC5inhRzmhgzR1ihUK/XUNh1/ra/WCssU7TZrmqMBQt
|
||||
Lvgoo1onhgM6aqdO1a0jvlmQQlILZNP3Sjx4UhG4iZjXu96eohunfQD5e28yEhOD
|
||||
nnrjxl1yL4IYcNyokDUL7LSvFdmRy4UUWsrS2I3sr9w8rUsO1LTyCnrVAgMBAAGj
|
||||
SjBIMAkGA1UdEwQCMAAwCwYDVR0PBAQDAgXgMB0GA1UdJQQWMBQGCCsGAQUFBwMC
|
||||
BggrBgEFBQcDATAPBgNVHREECDAGhwR/AAABMA0GCSqGSIb3DQEBBQUAA4IBAQCA
|
||||
/IFclqY/dsadGY0e9U3xq0XliIjZIHaI6OLXmO8xBdLDVf2+d6Bt3dl9gQMtGQI3
|
||||
zj9vqJrM+znXR30yAERFefZItk8hTzAotk15HYExVJkIn5JQBaXRbeO2DUZFgAnu
|
||||
6OU6KnuVC6i+7xDlbMl8wtRPmeZ6FS1wW4wnxLWZtKYAuLVDs0ISy6qbznGhCkWc
|
||||
b0uPbxnMmZHQLVL+yF1LYWpX9+Xa9QnWXOSY7KHtcuYXZB/XV4Pt6aDncg76bFdl
|
||||
MG3bocbJ9MsoS/LdlAiYzLNmKlFa243QPOo/zN170NhEZaF1lM80YBaLIE4rRtga
|
||||
nrkNOnPHx1evvuleH0Yv
|
||||
-----END CERTIFICATE-----`)
|
||||
|
||||
var clientKey = []byte(`-----BEGIN RSA PRIVATE KEY-----
|
||||
MIIEogIBAAKCAQEA1Cf8Tch5IieN9zAI0b5FFJoMzPsN3RAYD5GAk3Xo9XNbKnvJ
|
||||
38o2TKdxFu1/Q8hqG3668vWuL6TYDYVcYWtC6rwNpU/moAdJZyyEtmZIam1Va1VW
|
||||
HDCpylsk4b8jbUxxm6OzF1XxZpBUJg9o/1OvL1XA0rmcDiddXN5VdL2adJZsS54t
|
||||
gI57hcH0jkQocHTEv5gIl2tcTDStd4GeWxBmCotYdCQWcNk+96QMRxIai88aPPYw
|
||||
QxpOQeaABrYrjPO1fJA0jimM9bhkjGXK6cnrRQrg3jkeF3vuPw9BFbE4VeENLY+y
|
||||
v2OWBPYOD8ov0Tn2inge4QjE2DZyK95l46wQbwIDAQABAoIBAGZiAY1cALEt24H9
|
||||
yVPG+bluelz1jwQuvx3MPvtqvIivKcC/ynVYNYoaiCXjaTZB4orwRrH3RB8z8xvb
|
||||
TvCofbugEwnDHG3/9jl3L3iCtdG+f6lznkGublH8WDklL6iQaocMoeHSFNRFNIbF
|
||||
iwskzHcQcCSBdEEUWCb4GM9krMQz7pBR9BloyV40ZAGilMXI9F9FZ0YBWWee09gi
|
||||
jid6sEzYQveZ5RQgEEDrE/i+jzXkU8sBKsSm1GuKH62+YrtelBqP83DwVIKthMOJ
|
||||
79tw6i98v5JHV+1ikqC1Na/c7OxBBF3xrgwCN47ok0cHaIXh7SX+EIN6jcwKTmRH
|
||||
VZQBz2ECgYEA6tTpJEKNAzEY3KOO578zY2hWhPUNqzUTm8yLxZedhDt2rwyWjd4J
|
||||
HhS7CiNqFpMT/kxFyOupA8SFJPZDhJBXttnyzO9Sb61fLWgSP739K9OQfK1FGTA6
|
||||
khjc3vHtBeGrWm9+1jSxQtrwly7Rs+EmdvseDCN7yie/mgrBLS7p1l8CgYEA50fN
|
||||
6BnbeAgCTK8GDBWSJPaYUlo/lebUDCn5QIp1LK93vPQWjJR8xRBwL1TbiVKGd054
|
||||
dRZVuJYMJx+2mbrt5ca9UArisZp5OZgR4xz29n9u69P5XiuG8Fq/JBJXp1GXONVx
|
||||
JNOsUHOW/b3w2tNUWZcMQAH601BHOtO+EtaEX/ECgYBHygz4A8xeFG1YTjwKxt3r
|
||||
3uLMRKoIE/LJp093eXEzEoam3v9LoXxCEO5ZHBh7jD0JecG/uaNyvmpBsXNUnFfk
|
||||
U16xndwiveqh0/X4PJmgA05hfwrnt2HAdg9XrLfcG3Ap9nnc/EDQgmQYo7yB9Cux
|
||||
JfW6mkJmu54Mdos1x+i+mwKBgHmewcGe71E0bPkkRLrQERUM89bCjJNoWfO3ktIE
|
||||
vU9tSjr75GuyndYHKedJ6VRSKFHO2vs/bn5tsSBVxfEbYoSlOOJBhyo8AClwNV/H
|
||||
2HqRUqQCySxjGUeFgOQYHS3ocuw5GZFzGjcIQctXObPo0391NcTnBZ5fpcVimZ5Q
|
||||
XjYRAoGAN2O3HQjPyThoOUOn5MJEIx0L5bMvGNDzJO+oFngAntX/zv0zu/zsD9tc
|
||||
kk4EbMLluiw2/XJvYjCStieaYxbSWioKwlThy39C+iUut+IbpP2eI46SOkhvPczt
|
||||
4u1/sslqjs4ZSntR4Z9UOk3vY3oxRKbiXX2/vl9cqzB/cGYu01c=
|
||||
-----END RSA PRIVATE KEY-----`)
|
||||
|
||||
var clientCert = []byte(`-----BEGIN CERTIFICATE-----
|
||||
MIIDOzCCAiOgAwIBAgIJAN0PSMLOjTVBMA0GCSqGSIb3DQEBBQUAMDQxMjAwBgNV
|
||||
BAMUKWdlbmVyaWNfd2ViaG9va19hZG1pc3Npb25fcGx1Z2luX3Rlc3RzX2NhMCAX
|
||||
DTE3MDUwNDIyMzMyOFoYDzIyOTEwMjE3MjIzMzI4WjA4MTYwNAYDVQQDDC1nZW5l
|
||||
cmljX3dlYmhvb2tfYWRtaXNzaW9uX3BsdWdpbl90ZXN0c19jbGllbnQwggEiMA0G
|
||||
CSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDUJ/xNyHkiJ433MAjRvkUUmgzM+w3d
|
||||
EBgPkYCTdej1c1sqe8nfyjZMp3EW7X9DyGobfrry9a4vpNgNhVxha0LqvA2lT+ag
|
||||
B0lnLIS2ZkhqbVVrVVYcMKnKWyThvyNtTHGbo7MXVfFmkFQmD2j/U68vVcDSuZwO
|
||||
J11c3lV0vZp0lmxLni2AjnuFwfSORChwdMS/mAiXa1xMNK13gZ5bEGYKi1h0JBZw
|
||||
2T73pAxHEhqLzxo89jBDGk5B5oAGtiuM87V8kDSOKYz1uGSMZcrpyetFCuDeOR4X
|
||||
e+4/D0EVsThV4Q0tj7K/Y5YE9g4Pyi/ROfaKeB7hCMTYNnIr3mXjrBBvAgMBAAGj
|
||||
SjBIMAkGA1UdEwQCMAAwCwYDVR0PBAQDAgXgMB0GA1UdJQQWMBQGCCsGAQUFBwMC
|
||||
BggrBgEFBQcDATAPBgNVHREECDAGhwR/AAABMA0GCSqGSIb3DQEBBQUAA4IBAQC/
|
||||
TBXk511JBKLosKVqrjluo8bzbgnREUrPcKclatiAOiIFKbMBy4nE4BlGZZW34t1u
|
||||
sStB1dDHBHIuEkZxs93xwjqXN03yNNfve+FkRcb+guaZJEIBRlNocNxhd+lVDo8J
|
||||
axRTdoOxyEOHGCjg+gyb0i9f/rqEqLnDwnYLZbH9Qbh/yv6OgISUTYOCzH35H0/6
|
||||
unY5JaBhRvmJHI0Z3KtmvMShbUyzoYD+oNLaS31fvoYIekcHsnOjZGBukaIx1bE1
|
||||
4SFjCUSPGDdzJdaYxQb0UXNI7oXKr6e6YeOrglIrVbboa0X3jtqGF1U7rop8ts3v
|
||||
24SeXsvxqJht40itVvGK
|
||||
-----END CERTIFICATE-----`)
|
120
plugin/pkg/admission/webhook/config.go
Normal file
120
plugin/pkg/admission/webhook/config.go
Normal file
@ -0,0 +1,120 @@
|
||||
/*
|
||||
Copyright 2017 The Kubernetes Authors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
// Package webhook checks a webhook for configured operation admission
|
||||
package webhook
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"k8s.io/apiserver/pkg/admission"
|
||||
)
|
||||
|
||||
const (
|
||||
defaultFailAction = Deny
|
||||
defaultRetryBackoff = time.Duration(500) * time.Millisecond
|
||||
errInvalidFailAction = "webhook rule (%d) has an invalid fail action: %s should be either %v or %v"
|
||||
errInvalidRuleOperation = "webhook rule (%d) has an invalid operation (%d): %s"
|
||||
errInvalidRuleType = "webhook rule (%d) has an invalid type: %s should be either %v or %v"
|
||||
errMissingKubeConfigFile = "kubeConfigFile is required"
|
||||
errOneRuleRequired = "webhook requires at least one rule defined"
|
||||
errRetryBackoffOutOfRange = "webhook retry backoff is invalid: %v should be between %v and %v"
|
||||
minRetryBackoff = time.Duration(1) * time.Millisecond
|
||||
maxRetryBackoff = time.Duration(5) * time.Minute
|
||||
)
|
||||
|
||||
func normalizeConfig(config *GenericAdmissionWebhookConfig) error {
|
||||
allowFailAction := string(Allow)
|
||||
denyFailAction := string(Deny)
|
||||
connectOperation := string(admission.Connect)
|
||||
createOperation := string(admission.Create)
|
||||
deleteOperation := string(admission.Delete)
|
||||
updateOperation := string(admission.Update)
|
||||
sendRuleType := string(Send)
|
||||
skipRuleType := string(Skip)
|
||||
|
||||
// Validate the kubeConfigFile property is present
|
||||
if config.KubeConfigFile == "" {
|
||||
return fmt.Errorf(errMissingKubeConfigFile)
|
||||
}
|
||||
|
||||
// Normalize and validate the retry backoff
|
||||
if config.RetryBackoff == 0 {
|
||||
config.RetryBackoff = defaultRetryBackoff
|
||||
} else {
|
||||
// Unmarshalling gives nanoseconds so convert to milliseconds
|
||||
config.RetryBackoff *= time.Millisecond
|
||||
}
|
||||
|
||||
if config.RetryBackoff < minRetryBackoff || config.RetryBackoff > maxRetryBackoff {
|
||||
return fmt.Errorf(errRetryBackoffOutOfRange, config.RetryBackoff, minRetryBackoff, maxRetryBackoff)
|
||||
}
|
||||
|
||||
// Validate that there is at least one rule
|
||||
if len(config.Rules) == 0 {
|
||||
return fmt.Errorf(errOneRuleRequired)
|
||||
}
|
||||
|
||||
for i, rule := range config.Rules {
|
||||
// Normalize and validate the fail action
|
||||
failAction := strings.ToUpper(string(rule.FailAction))
|
||||
|
||||
switch failAction {
|
||||
case "":
|
||||
config.Rules[i].FailAction = defaultFailAction
|
||||
case allowFailAction:
|
||||
config.Rules[i].FailAction = Allow
|
||||
case denyFailAction:
|
||||
config.Rules[i].FailAction = Deny
|
||||
default:
|
||||
return fmt.Errorf(errInvalidFailAction, i, rule.FailAction, Allow, Deny)
|
||||
}
|
||||
|
||||
// Normalize and validate the rule operation(s)
|
||||
for j, operation := range rule.Operations {
|
||||
operation := strings.ToUpper(string(operation))
|
||||
|
||||
switch operation {
|
||||
case connectOperation:
|
||||
config.Rules[i].Operations[j] = admission.Connect
|
||||
case createOperation:
|
||||
config.Rules[i].Operations[j] = admission.Create
|
||||
case deleteOperation:
|
||||
config.Rules[i].Operations[j] = admission.Delete
|
||||
case updateOperation:
|
||||
config.Rules[i].Operations[j] = admission.Update
|
||||
default:
|
||||
return fmt.Errorf(errInvalidRuleOperation, i, j, rule.Operations[j])
|
||||
}
|
||||
}
|
||||
|
||||
// Normalize and validate the rule type
|
||||
ruleType := strings.ToUpper(string(rule.Type))
|
||||
|
||||
switch ruleType {
|
||||
case sendRuleType:
|
||||
config.Rules[i].Type = Send
|
||||
case skipRuleType:
|
||||
config.Rules[i].Type = Skip
|
||||
default:
|
||||
return fmt.Errorf(errInvalidRuleType, i, rule.Type, Send, Skip)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
220
plugin/pkg/admission/webhook/config_test.go
Normal file
220
plugin/pkg/admission/webhook/config_test.go
Normal file
@ -0,0 +1,220 @@
|
||||
/*
|
||||
Copyright 2017 The Kubernetes Authors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package webhook
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"reflect"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"k8s.io/apiserver/pkg/admission"
|
||||
)
|
||||
|
||||
func TestConfigNormalization(t *testing.T) {
|
||||
defaultRules := []Rule{
|
||||
Rule{
|
||||
Type: Skip,
|
||||
},
|
||||
}
|
||||
highRetryBackoff := (maxRetryBackoff / time.Millisecond) + (time.Duration(1) * time.Millisecond)
|
||||
kubeConfigFile := "/tmp/kube/config"
|
||||
lowRetryBackoff := time.Duration(-1)
|
||||
normalizedValidRules := []Rule{
|
||||
Rule{
|
||||
APIGroups: []string{""},
|
||||
FailAction: Allow,
|
||||
Namespaces: []string{"my-ns"},
|
||||
Operations: []admission.Operation{
|
||||
admission.Connect,
|
||||
admission.Create,
|
||||
admission.Delete,
|
||||
admission.Update,
|
||||
},
|
||||
Resources: []string{"pods"},
|
||||
ResourceNames: []string{"my-name"},
|
||||
Type: Send,
|
||||
},
|
||||
Rule{
|
||||
FailAction: Deny,
|
||||
Type: Skip,
|
||||
},
|
||||
}
|
||||
rawValidRules := []Rule{
|
||||
Rule{
|
||||
APIGroups: []string{""},
|
||||
FailAction: FailAction("AlLoW"),
|
||||
Namespaces: []string{"my-ns"},
|
||||
Operations: []admission.Operation{
|
||||
admission.Operation("connect"),
|
||||
admission.Operation("CREATE"),
|
||||
admission.Operation("DeLeTe"),
|
||||
admission.Operation("UPdaTE"),
|
||||
},
|
||||
Resources: []string{"pods"},
|
||||
ResourceNames: []string{"my-name"},
|
||||
Type: RuleType("SenD"),
|
||||
},
|
||||
Rule{
|
||||
FailAction: Deny,
|
||||
Type: Skip,
|
||||
},
|
||||
}
|
||||
unknownFailAction := FailAction("Unknown")
|
||||
unknownOperation := admission.Operation("Unknown")
|
||||
unknownRuleType := RuleType("Allow")
|
||||
tests := []struct {
|
||||
test string
|
||||
rawConfig GenericAdmissionWebhookConfig
|
||||
normalizedConfig GenericAdmissionWebhookConfig
|
||||
err error
|
||||
}{
|
||||
{
|
||||
test: "kubeConfigFile was not provided (error)",
|
||||
rawConfig: GenericAdmissionWebhookConfig{},
|
||||
err: fmt.Errorf(errMissingKubeConfigFile),
|
||||
},
|
||||
{
|
||||
test: "retryBackoff was not provided (use default)",
|
||||
rawConfig: GenericAdmissionWebhookConfig{
|
||||
KubeConfigFile: kubeConfigFile,
|
||||
Rules: defaultRules,
|
||||
},
|
||||
normalizedConfig: GenericAdmissionWebhookConfig{
|
||||
KubeConfigFile: kubeConfigFile,
|
||||
RetryBackoff: defaultRetryBackoff,
|
||||
Rules: defaultRules,
|
||||
},
|
||||
},
|
||||
{
|
||||
test: "retryBackoff was below minimum value (error)",
|
||||
rawConfig: GenericAdmissionWebhookConfig{
|
||||
KubeConfigFile: kubeConfigFile,
|
||||
RetryBackoff: lowRetryBackoff,
|
||||
Rules: defaultRules,
|
||||
},
|
||||
err: fmt.Errorf(errRetryBackoffOutOfRange, lowRetryBackoff*time.Millisecond, minRetryBackoff, maxRetryBackoff),
|
||||
},
|
||||
{
|
||||
test: "retryBackoff was above maximum value (error)",
|
||||
rawConfig: GenericAdmissionWebhookConfig{
|
||||
KubeConfigFile: kubeConfigFile,
|
||||
RetryBackoff: highRetryBackoff,
|
||||
Rules: defaultRules,
|
||||
},
|
||||
err: fmt.Errorf(errRetryBackoffOutOfRange, highRetryBackoff*time.Millisecond, minRetryBackoff, maxRetryBackoff),
|
||||
},
|
||||
{
|
||||
test: "rules should have at least one rule (error)",
|
||||
rawConfig: GenericAdmissionWebhookConfig{
|
||||
KubeConfigFile: kubeConfigFile,
|
||||
},
|
||||
err: fmt.Errorf(errOneRuleRequired),
|
||||
},
|
||||
{
|
||||
test: "fail action was not provided (use default)",
|
||||
rawConfig: GenericAdmissionWebhookConfig{
|
||||
KubeConfigFile: kubeConfigFile,
|
||||
Rules: []Rule{
|
||||
Rule{
|
||||
Type: Skip,
|
||||
},
|
||||
},
|
||||
},
|
||||
normalizedConfig: GenericAdmissionWebhookConfig{
|
||||
KubeConfigFile: kubeConfigFile,
|
||||
RetryBackoff: defaultRetryBackoff,
|
||||
Rules: []Rule{
|
||||
Rule{
|
||||
FailAction: defaultFailAction,
|
||||
Type: Skip,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
test: "rule has invalid fail action (error)",
|
||||
rawConfig: GenericAdmissionWebhookConfig{
|
||||
KubeConfigFile: kubeConfigFile,
|
||||
Rules: []Rule{
|
||||
Rule{
|
||||
FailAction: unknownFailAction,
|
||||
},
|
||||
},
|
||||
},
|
||||
err: fmt.Errorf(errInvalidFailAction, 0, unknownFailAction, Allow, Deny),
|
||||
},
|
||||
{
|
||||
test: "rule has invalid operation (error)",
|
||||
rawConfig: GenericAdmissionWebhookConfig{
|
||||
KubeConfigFile: kubeConfigFile,
|
||||
Rules: []Rule{
|
||||
Rule{
|
||||
Operations: []admission.Operation{unknownOperation},
|
||||
},
|
||||
},
|
||||
},
|
||||
err: fmt.Errorf(errInvalidRuleOperation, 0, 0, unknownOperation),
|
||||
},
|
||||
{
|
||||
test: "rule has invalid type (error)",
|
||||
rawConfig: GenericAdmissionWebhookConfig{
|
||||
KubeConfigFile: kubeConfigFile,
|
||||
Rules: []Rule{
|
||||
Rule{
|
||||
Type: unknownRuleType,
|
||||
},
|
||||
},
|
||||
},
|
||||
err: fmt.Errorf(errInvalidRuleType, 0, unknownRuleType, Send, Skip),
|
||||
},
|
||||
{
|
||||
test: "valid configuration",
|
||||
rawConfig: GenericAdmissionWebhookConfig{
|
||||
KubeConfigFile: kubeConfigFile,
|
||||
Rules: rawValidRules,
|
||||
},
|
||||
normalizedConfig: GenericAdmissionWebhookConfig{
|
||||
KubeConfigFile: kubeConfigFile,
|
||||
RetryBackoff: defaultRetryBackoff,
|
||||
Rules: normalizedValidRules,
|
||||
},
|
||||
err: nil,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
err := normalizeConfig(&tt.rawConfig)
|
||||
if err == nil {
|
||||
if tt.err != nil {
|
||||
// Ensure that expected errors are produced
|
||||
t.Errorf("%s: expected error but did not produce one", tt.test)
|
||||
} else if !reflect.DeepEqual(tt.rawConfig, tt.normalizedConfig) {
|
||||
// Ensure that valid configurations are structured properly
|
||||
t.Errorf("%s: normalized config mismtach. got: %v expected: %v", tt.test, tt.rawConfig, tt.normalizedConfig)
|
||||
}
|
||||
} else {
|
||||
if tt.err == nil {
|
||||
// Ensure that unexpected errors are not produced
|
||||
t.Errorf("%s: unexpected error: %v", tt.test, err)
|
||||
} else if err != nil && tt.err != nil && err.Error() != tt.err.Error() {
|
||||
// Ensure that expected errors are formated properly
|
||||
t.Errorf("%s: error message mismatch. got: '%v' expected: '%v'", tt.test, err, tt.err)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
18
plugin/pkg/admission/webhook/doc.go
Normal file
18
plugin/pkg/admission/webhook/doc.go
Normal file
@ -0,0 +1,18 @@
|
||||
/*
|
||||
Copyright 2017 The Kubernetes Authors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
// Package webhook checks a webhook for configured operation admission
|
||||
package webhook // import "k8s.io/kubernetes/plugin/pkg/admission/webhook"
|
107
plugin/pkg/admission/webhook/gencerts.sh
Executable file
107
plugin/pkg/admission/webhook/gencerts.sh
Executable file
@ -0,0 +1,107 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Copyright 2017 The Kubernetes Authors.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
set -e
|
||||
|
||||
# gencerts.sh generates the certificates for the generic webhook admission plugin tests.
|
||||
#
|
||||
# It is not expected to be run often (there is no go generate rule), and mainly
|
||||
# exists for documentation purposes.
|
||||
|
||||
CN_BASE="generic_webhook_admission_plugin_tests"
|
||||
|
||||
cat > server.conf << EOF
|
||||
[req]
|
||||
req_extensions = v3_req
|
||||
distinguished_name = req_distinguished_name
|
||||
[req_distinguished_name]
|
||||
[ v3_req ]
|
||||
basicConstraints = CA:FALSE
|
||||
keyUsage = nonRepudiation, digitalSignature, keyEncipherment
|
||||
extendedKeyUsage = clientAuth, serverAuth
|
||||
subjectAltName = @alt_names
|
||||
[alt_names]
|
||||
IP.1 = 127.0.0.1
|
||||
EOF
|
||||
|
||||
cat > client.conf << EOF
|
||||
[req]
|
||||
req_extensions = v3_req
|
||||
distinguished_name = req_distinguished_name
|
||||
[req_distinguished_name]
|
||||
[ v3_req ]
|
||||
basicConstraints = CA:FALSE
|
||||
keyUsage = nonRepudiation, digitalSignature, keyEncipherment
|
||||
extendedKeyUsage = clientAuth, serverAuth
|
||||
subjectAltName = @alt_names
|
||||
[alt_names]
|
||||
IP.1 = 127.0.0.1
|
||||
EOF
|
||||
|
||||
# Create a certificate authority
|
||||
openssl genrsa -out caKey.pem 2048
|
||||
openssl req -x509 -new -nodes -key caKey.pem -days 100000 -out caCert.pem -subj "/CN=${CN_BASE}_ca"
|
||||
|
||||
# Create a second certificate authority
|
||||
openssl genrsa -out badCAKey.pem 2048
|
||||
openssl req -x509 -new -nodes -key badCAKey.pem -days 100000 -out badCACert.pem -subj "/CN=${CN_BASE}_ca"
|
||||
|
||||
# Create a server certiticate
|
||||
openssl genrsa -out serverKey.pem 2048
|
||||
openssl req -new -key serverKey.pem -out server.csr -subj "/CN=${CN_BASE}_server" -config server.conf
|
||||
openssl x509 -req -in server.csr -CA caCert.pem -CAkey caKey.pem -CAcreateserial -out serverCert.pem -days 100000 -extensions v3_req -extfile server.conf
|
||||
|
||||
# Create a client certiticate
|
||||
openssl genrsa -out clientKey.pem 2048
|
||||
openssl req -new -key clientKey.pem -out client.csr -subj "/CN=${CN_BASE}_client" -config client.conf
|
||||
openssl x509 -req -in client.csr -CA caCert.pem -CAkey caKey.pem -CAcreateserial -out clientCert.pem -days 100000 -extensions v3_req -extfile client.conf
|
||||
|
||||
outfile=certs_test.go
|
||||
|
||||
cat > $outfile << EOF
|
||||
/*
|
||||
Copyright 2017 The Kubernetes Authors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
EOF
|
||||
|
||||
echo "// This file was generated using openssl by the gencerts.sh script" >> $outfile
|
||||
echo "// and holds raw certificates for the webhook tests." >> $outfile
|
||||
echo "" >> $outfile
|
||||
echo "package webhook" >> $outfile
|
||||
for file in caKey caCert badCAKey badCACert serverKey serverCert clientKey clientCert; do
|
||||
data=$(cat ${file}.pem)
|
||||
echo "" >> $outfile
|
||||
echo "var $file = []byte(\`$data\`)" >> $outfile
|
||||
done
|
||||
|
||||
# Clean up after we're done.
|
||||
rm *.pem
|
||||
rm *.csr
|
||||
rm *.srl
|
||||
rm *.conf
|
98
plugin/pkg/admission/webhook/rules.go
Normal file
98
plugin/pkg/admission/webhook/rules.go
Normal file
@ -0,0 +1,98 @@
|
||||
/*
|
||||
Copyright 2017 The Kubernetes Authors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
// Package webhook checks a webhook for configured operation admission
|
||||
package webhook
|
||||
|
||||
import "k8s.io/apiserver/pkg/admission"
|
||||
|
||||
func indexOf(items []string, requested string) int {
|
||||
for i, item := range items {
|
||||
if item == requested {
|
||||
return i
|
||||
}
|
||||
}
|
||||
|
||||
return -1
|
||||
}
|
||||
|
||||
// APIGroupMatches returns if the admission.Attributes matches the rule's API groups
|
||||
func APIGroupMatches(rule Rule, attr admission.Attributes) bool {
|
||||
if len(rule.APIGroups) == 0 {
|
||||
return true
|
||||
}
|
||||
|
||||
return indexOf(rule.APIGroups, attr.GetResource().Group) > -1
|
||||
}
|
||||
|
||||
// Matches returns if the admission.Attributes matches the rule
|
||||
func Matches(rule Rule, attr admission.Attributes) bool {
|
||||
return APIGroupMatches(rule, attr) &&
|
||||
NamespaceMatches(rule, attr) &&
|
||||
OperationMatches(rule, attr) &&
|
||||
ResourceMatches(rule, attr) &&
|
||||
ResourceNamesMatches(rule, attr)
|
||||
}
|
||||
|
||||
// OperationMatches returns if the admission.Attributes matches the rule's operation
|
||||
func OperationMatches(rule Rule, attr admission.Attributes) bool {
|
||||
if len(rule.Operations) == 0 {
|
||||
return true
|
||||
}
|
||||
|
||||
aOp := attr.GetOperation()
|
||||
|
||||
for _, rOp := range rule.Operations {
|
||||
if aOp == rOp {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// NamespaceMatches returns if the admission.Attributes matches the rule's namespaces
|
||||
func NamespaceMatches(rule Rule, attr admission.Attributes) bool {
|
||||
if len(rule.Namespaces) == 0 {
|
||||
return true
|
||||
}
|
||||
|
||||
return indexOf(rule.Namespaces, attr.GetNamespace()) > -1
|
||||
}
|
||||
|
||||
// ResourceMatches returns if the admission.Attributes matches the rule's resource (and optional subresource)
|
||||
func ResourceMatches(rule Rule, attr admission.Attributes) bool {
|
||||
if len(rule.Resources) == 0 {
|
||||
return true
|
||||
}
|
||||
|
||||
resource := attr.GetResource().Resource
|
||||
|
||||
if len(attr.GetSubresource()) > 0 {
|
||||
resource = attr.GetResource().Resource + "/" + attr.GetSubresource()
|
||||
}
|
||||
|
||||
return indexOf(rule.Resources, resource) > -1
|
||||
}
|
||||
|
||||
// ResourceNamesMatches returns if the admission.Attributes matches the rule's resource names
|
||||
func ResourceNamesMatches(rule Rule, attr admission.Attributes) bool {
|
||||
if len(rule.ResourceNames) == 0 {
|
||||
return true
|
||||
}
|
||||
|
||||
return indexOf(rule.ResourceNames, attr.GetName()) > -1
|
||||
}
|
309
plugin/pkg/admission/webhook/rules_test.go
Normal file
309
plugin/pkg/admission/webhook/rules_test.go
Normal file
@ -0,0 +1,309 @@
|
||||
/*
|
||||
Copyright 2017 The Kubernetes Authors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package webhook
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"k8s.io/apiserver/pkg/admission"
|
||||
"k8s.io/client-go/pkg/api"
|
||||
)
|
||||
|
||||
type ruleTest struct {
|
||||
test string
|
||||
rule Rule
|
||||
attrAPIGroup string
|
||||
attrNamespace string
|
||||
attrOperation admission.Operation
|
||||
attrResource string
|
||||
attrResourceName string
|
||||
attrSubResource string
|
||||
shouldMatch bool
|
||||
}
|
||||
|
||||
func TestAPIGroupMatches(t *testing.T) {
|
||||
tests := []ruleTest{
|
||||
ruleTest{
|
||||
test: "apiGroups empty match",
|
||||
rule: Rule{},
|
||||
shouldMatch: true,
|
||||
},
|
||||
ruleTest{
|
||||
test: "apiGroup match",
|
||||
rule: Rule{
|
||||
APIGroups: []string{"my-group"},
|
||||
},
|
||||
attrAPIGroup: "my-group",
|
||||
shouldMatch: true,
|
||||
},
|
||||
ruleTest{
|
||||
test: "apiGroup mismatch",
|
||||
rule: Rule{
|
||||
APIGroups: []string{"my-group"},
|
||||
},
|
||||
attrAPIGroup: "your-group",
|
||||
shouldMatch: false,
|
||||
},
|
||||
}
|
||||
|
||||
runTests(t, "apiGroups", tests)
|
||||
}
|
||||
|
||||
func TestMatches(t *testing.T) {
|
||||
tests := []ruleTest{
|
||||
ruleTest{
|
||||
test: "empty rule matches",
|
||||
rule: Rule{},
|
||||
shouldMatch: true,
|
||||
},
|
||||
ruleTest{
|
||||
test: "all properties match",
|
||||
rule: Rule{
|
||||
APIGroups: []string{"my-group"},
|
||||
Namespaces: []string{"my-ns"},
|
||||
Operations: []admission.Operation{admission.Create},
|
||||
ResourceNames: []string{"my-name"},
|
||||
Resources: []string{"pods/status"},
|
||||
},
|
||||
shouldMatch: true,
|
||||
attrAPIGroup: "my-group",
|
||||
attrNamespace: "my-ns",
|
||||
attrOperation: admission.Create,
|
||||
attrResource: "pods",
|
||||
attrResourceName: "my-name",
|
||||
attrSubResource: "status",
|
||||
},
|
||||
ruleTest{
|
||||
test: "no properties match",
|
||||
rule: Rule{
|
||||
APIGroups: []string{"my-group"},
|
||||
Namespaces: []string{"my-ns"},
|
||||
Operations: []admission.Operation{admission.Create},
|
||||
ResourceNames: []string{"my-name"},
|
||||
Resources: []string{"pods/status"},
|
||||
},
|
||||
shouldMatch: false,
|
||||
attrAPIGroup: "your-group",
|
||||
attrNamespace: "your-ns",
|
||||
attrOperation: admission.Delete,
|
||||
attrResource: "secrets",
|
||||
attrResourceName: "your-name",
|
||||
},
|
||||
}
|
||||
|
||||
runTests(t, "", tests)
|
||||
}
|
||||
|
||||
func TestNamespaceMatches(t *testing.T) {
|
||||
tests := []ruleTest{
|
||||
ruleTest{
|
||||
test: "namespaces empty match",
|
||||
rule: Rule{},
|
||||
shouldMatch: true,
|
||||
},
|
||||
ruleTest{
|
||||
test: "namespace match",
|
||||
rule: Rule{
|
||||
Namespaces: []string{"my-ns"},
|
||||
},
|
||||
attrNamespace: "my-ns",
|
||||
shouldMatch: true,
|
||||
},
|
||||
ruleTest{
|
||||
test: "namespace mismatch",
|
||||
rule: Rule{
|
||||
Namespaces: []string{"my-ns"},
|
||||
},
|
||||
attrNamespace: "your-ns",
|
||||
shouldMatch: false,
|
||||
},
|
||||
}
|
||||
|
||||
runTests(t, "namespaces", tests)
|
||||
}
|
||||
|
||||
func TestOperationMatches(t *testing.T) {
|
||||
tests := []ruleTest{
|
||||
ruleTest{
|
||||
test: "operations empty match",
|
||||
rule: Rule{},
|
||||
shouldMatch: true,
|
||||
},
|
||||
ruleTest{
|
||||
test: "operation match",
|
||||
rule: Rule{
|
||||
Operations: []admission.Operation{admission.Create},
|
||||
},
|
||||
attrOperation: admission.Create,
|
||||
shouldMatch: true,
|
||||
},
|
||||
ruleTest{
|
||||
test: "operation mismatch",
|
||||
rule: Rule{
|
||||
Operations: []admission.Operation{admission.Create},
|
||||
},
|
||||
attrOperation: admission.Delete,
|
||||
shouldMatch: false,
|
||||
},
|
||||
}
|
||||
|
||||
runTests(t, "operations", tests)
|
||||
}
|
||||
|
||||
func TestResourceMatches(t *testing.T) {
|
||||
tests := []ruleTest{
|
||||
ruleTest{
|
||||
test: "resources empty match",
|
||||
rule: Rule{},
|
||||
shouldMatch: true,
|
||||
},
|
||||
ruleTest{
|
||||
test: "resource match",
|
||||
rule: Rule{
|
||||
Resources: []string{"pods"},
|
||||
},
|
||||
attrResource: "pods",
|
||||
shouldMatch: true,
|
||||
},
|
||||
ruleTest{
|
||||
test: "resource mismatch",
|
||||
rule: Rule{
|
||||
Resources: []string{"pods"},
|
||||
},
|
||||
attrResource: "secrets",
|
||||
shouldMatch: false,
|
||||
},
|
||||
ruleTest{
|
||||
test: "resource with subresource match",
|
||||
rule: Rule{
|
||||
Resources: []string{"pods/status"},
|
||||
},
|
||||
attrResource: "pods",
|
||||
attrSubResource: "status",
|
||||
shouldMatch: true,
|
||||
},
|
||||
ruleTest{
|
||||
test: "resource with subresource mismatch",
|
||||
rule: Rule{
|
||||
Resources: []string{"pods"},
|
||||
},
|
||||
attrResource: "pods",
|
||||
attrSubResource: "status",
|
||||
shouldMatch: false,
|
||||
},
|
||||
}
|
||||
|
||||
runTests(t, "resources", tests)
|
||||
}
|
||||
|
||||
func TestResourceNameMatches(t *testing.T) {
|
||||
tests := []ruleTest{
|
||||
ruleTest{
|
||||
test: "resourceNames empty match",
|
||||
rule: Rule{},
|
||||
shouldMatch: true,
|
||||
},
|
||||
ruleTest{
|
||||
test: "resourceName match",
|
||||
rule: Rule{
|
||||
ResourceNames: []string{"my-name"},
|
||||
},
|
||||
attrResourceName: "my-name",
|
||||
shouldMatch: true,
|
||||
},
|
||||
ruleTest{
|
||||
test: "resourceName mismatch",
|
||||
rule: Rule{
|
||||
ResourceNames: []string{"my-name"},
|
||||
},
|
||||
attrResourceName: "your-name",
|
||||
shouldMatch: false,
|
||||
},
|
||||
}
|
||||
|
||||
runTests(t, "resourceNames", tests)
|
||||
}
|
||||
|
||||
func runTests(t *testing.T, prop string, tests []ruleTest) {
|
||||
for _, tt := range tests {
|
||||
if tt.attrResource == "" {
|
||||
tt.attrResource = "pods"
|
||||
}
|
||||
|
||||
res := api.Resource(tt.attrResource).WithVersion("version")
|
||||
|
||||
if tt.attrAPIGroup != "" {
|
||||
res.Group = tt.attrAPIGroup
|
||||
}
|
||||
|
||||
attr := admission.NewAttributesRecord(nil, nil, api.Kind("Pod").WithVersion("version"), tt.attrNamespace, tt.attrResourceName, res, tt.attrSubResource, tt.attrOperation, nil)
|
||||
var attrVal string
|
||||
var ruleVal []string
|
||||
var matches bool
|
||||
|
||||
switch prop {
|
||||
case "":
|
||||
matches = Matches(tt.rule, attr)
|
||||
case "apiGroups":
|
||||
attrVal = tt.attrAPIGroup
|
||||
matches = APIGroupMatches(tt.rule, attr)
|
||||
ruleVal = tt.rule.APIGroups
|
||||
case "namespaces":
|
||||
attrVal = tt.attrNamespace
|
||||
matches = NamespaceMatches(tt.rule, attr)
|
||||
ruleVal = tt.rule.Namespaces
|
||||
case "operations":
|
||||
attrVal = string(tt.attrOperation)
|
||||
matches = OperationMatches(tt.rule, attr)
|
||||
ruleVal = make([]string, len(tt.rule.Operations))
|
||||
|
||||
for _, rOp := range tt.rule.Operations {
|
||||
ruleVal = append(ruleVal, string(rOp))
|
||||
}
|
||||
case "resources":
|
||||
attrVal = tt.attrResource
|
||||
matches = ResourceMatches(tt.rule, attr)
|
||||
ruleVal = tt.rule.Resources
|
||||
case "resourceNames":
|
||||
attrVal = tt.attrResourceName
|
||||
matches = ResourceNamesMatches(tt.rule, attr)
|
||||
ruleVal = tt.rule.ResourceNames
|
||||
default:
|
||||
t.Errorf("Unexpected test property: %s", prop)
|
||||
}
|
||||
|
||||
if matches && !tt.shouldMatch {
|
||||
if prop == "" {
|
||||
testError(t, tt.test, "Expected match")
|
||||
} else {
|
||||
testError(t, tt.test, fmt.Sprintf("Expected %s rule property not to match %s against one of the following: %s", prop, attrVal, strings.Join(ruleVal, ", ")))
|
||||
}
|
||||
} else if !matches && tt.shouldMatch {
|
||||
if prop == "" {
|
||||
testError(t, tt.test, "Unexpected match")
|
||||
} else {
|
||||
testError(t, tt.test, fmt.Sprintf("Expected %s rule property to match %s against one of the following: %s", prop, attrVal, strings.Join(ruleVal, ", ")))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func testError(t *testing.T, name, msg string) {
|
||||
t.Errorf("test failed (%s): %s", name, msg)
|
||||
}
|
69
plugin/pkg/admission/webhook/types.go
Normal file
69
plugin/pkg/admission/webhook/types.go
Normal file
@ -0,0 +1,69 @@
|
||||
/*
|
||||
Copyright 2017 The Kubernetes Authors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
// Package webhook checks a webhook for configured operation admission
|
||||
package webhook
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"k8s.io/apiserver/pkg/admission"
|
||||
)
|
||||
|
||||
// FailAction is the action to take whenever a webhook fails and there are no more retries available
|
||||
type FailAction string
|
||||
|
||||
const (
|
||||
// Allow is the fail action taken whenever the webhook call fails and there are no more retries
|
||||
Allow FailAction = "ALLOW"
|
||||
// Deny is the fail action taken whenever the webhook call fails and there are no more retries
|
||||
Deny FailAction = "DENY"
|
||||
)
|
||||
|
||||
// GenericAdmissionWebhookConfig holds configuration for an admission webhook
|
||||
type GenericAdmissionWebhookConfig struct {
|
||||
KubeConfigFile string `json:"kubeConfigFile"`
|
||||
RetryBackoff time.Duration `json:"retryBackoff"`
|
||||
Rules []Rule `json:"rules"`
|
||||
}
|
||||
|
||||
// Rule is the type defining an admission rule in the admission controller configuration file
|
||||
type Rule struct {
|
||||
// APIGroups is a list of API groups that contain the resource this rule applies to.
|
||||
APIGroups []string `json:"apiGroups"`
|
||||
// FailAction is the action to take whenever the webhook fails and there are no more retries (Default: DENY)
|
||||
FailAction FailAction `json:"failAction"`
|
||||
// Namespaces is a list of namespaces this rule applies to.
|
||||
Namespaces []string `json:"namespaces"`
|
||||
// Operations is a list of admission operations this rule applies to.
|
||||
Operations []admission.Operation `json:"operations"`
|
||||
// Resources is a list of resources this rule applies to.
|
||||
Resources []string `json:"resources"`
|
||||
// ResourceNames is a list of resource names this rule applies to.
|
||||
ResourceNames []string `json:"resourceNames"`
|
||||
// Type is the admission rule type
|
||||
Type RuleType `json:"type"`
|
||||
}
|
||||
|
||||
// RuleType is the type of admission rule
|
||||
type RuleType string
|
||||
|
||||
const (
|
||||
// Send is the rule type for when a matching admission.Attributes should be sent to the webhook
|
||||
Send RuleType = "SEND"
|
||||
// Skip is the rule type for when a matching admission.Attributes should not be sent to the webhook
|
||||
Skip RuleType = "SKIP"
|
||||
)
|
Loading…
Reference in New Issue
Block a user