Commit 711e3cff authored by Evan Cordell's avatar Evan Cordell

Add new admission controller: image policy webhook

parent c5e3b79f
......@@ -169,6 +169,7 @@ plugin/pkg/admission/admit
plugin/pkg/admission/alwayspullimages
plugin/pkg/admission/deny
plugin/pkg/admission/exec
plugin/pkg/admission/imagepolicy
plugin/pkg/admission/namespace/autoprovision
plugin/pkg/admission/namespace/exists
plugin/pkg/admission/securitycontext/scdeny
......@@ -202,4 +203,4 @@ pkg/util/maps
pkg/volume/quobyte
test/integration/discoverysummarizer
test/integration/examples
test/integration/federation
test/integration/federation
\ No newline at end of file
/*
Copyright 2016 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 imagepolicy contains an admission controller that configures a webhook to which policy
// decisions are delegated.
package imagepolicy
import (
"encoding/json"
"errors"
"fmt"
"io"
"strings"
"time"
"github.com/golang/glog"
"k8s.io/kubernetes/pkg/api"
apierrors "k8s.io/kubernetes/pkg/api/errors"
"k8s.io/kubernetes/pkg/api/unversioned"
"k8s.io/kubernetes/pkg/apis/imagepolicy/v1alpha1"
clientset "k8s.io/kubernetes/pkg/client/clientset_generated/internalclientset"
"k8s.io/kubernetes/pkg/util/yaml"
"k8s.io/kubernetes/pkg/client/restclient"
"k8s.io/kubernetes/pkg/util/cache"
"k8s.io/kubernetes/plugin/pkg/webhook"
"k8s.io/kubernetes/pkg/admission"
)
var (
groupVersions = []unversioned.GroupVersion{v1alpha1.SchemeGroupVersion}
)
func init() {
admission.RegisterPlugin("ImagePolicyWebhook", func(client clientset.Interface, config io.Reader) (admission.Interface, error) {
newImagePolicyWebhook, err := NewImagePolicyWebhook(client, config)
if err != nil {
return nil, err
}
return newImagePolicyWebhook, nil
})
}
// imagePolicyWebhook is an implementation of admission.Interface.
type imagePolicyWebhook struct {
*admission.Handler
webhook *webhook.GenericWebhook
responseCache *cache.LRUExpireCache
allowTTL time.Duration
denyTTL time.Duration
retryBackoff time.Duration
defaultAllow bool
}
func (a *imagePolicyWebhook) statusTTL(status v1alpha1.ImageReviewStatus) time.Duration {
if status.Allowed {
return a.allowTTL
}
return a.denyTTL
}
// Filter out annotations that don't match *.image-policy.k8s.io/*
func (a *imagePolicyWebhook) filterAnnotations(allAnnotations map[string]string) map[string]string {
annotations := make(map[string]string)
for k, v := range allAnnotations {
if strings.Contains(k, ".image-policy.k8s.io/") {
annotations[k] = v
}
}
return annotations
}
// Function to call on webhook failure; behavior determined by defaultAllow flag
func (a *imagePolicyWebhook) webhookError(attributes admission.Attributes, err error) error {
if err != nil {
glog.V(2).Infof("error contacting webhook backend: %s")
if a.defaultAllow {
glog.V(2).Infof("resource allowed in spite of webhook backend failure")
return nil
}
glog.V(2).Infof("resource not allowed due to webhook backend failure ")
return admission.NewForbidden(attributes, err)
}
return nil
}
func (a *imagePolicyWebhook) Admit(attributes admission.Attributes) (err error) {
// Ignore all calls to subresources or resources other than pods.
allowedResources := map[unversioned.GroupResource]bool{
api.Resource("pods"): true,
}
if len(attributes.GetSubresource()) != 0 || !allowedResources[attributes.GetResource().GroupResource()] {
return nil
}
pod, ok := attributes.GetObject().(*api.Pod)
if !ok {
return apierrors.NewBadRequest("Resource was marked with kind Pod but was unable to be converted")
}
// Build list of ImageReviewContainerSpec
var imageReviewContainerSpecs []v1alpha1.ImageReviewContainerSpec
containers := make([]api.Container, 0, len(pod.Spec.Containers)+len(pod.Spec.InitContainers))
containers = append(containers, pod.Spec.Containers...)
containers = append(containers, pod.Spec.InitContainers...)
for _, c := range containers {
imageReviewContainerSpecs = append(imageReviewContainerSpecs, v1alpha1.ImageReviewContainerSpec{
Image: c.Image,
})
}
imageReview := v1alpha1.ImageReview{
Spec: v1alpha1.ImageReviewSpec{
Containers: imageReviewContainerSpecs,
Annotations: a.filterAnnotations(pod.Annotations),
Namespace: attributes.GetNamespace(),
},
}
if err := a.admitPod(attributes, &imageReview); err != nil {
return admission.NewForbidden(attributes, err)
}
return nil
}
func (a *imagePolicyWebhook) admitPod(attributes admission.Attributes, review *v1alpha1.ImageReview) error {
cacheKey, err := json.Marshal(review.Spec)
if err != nil {
return err
}
if entry, ok := a.responseCache.Get(string(cacheKey)); ok {
review.Status = entry.(v1alpha1.ImageReviewStatus)
} else {
result := a.webhook.WithExponentialBackoff(func() restclient.Result {
return a.webhook.RestClient.Post().Body(review).Do()
})
if err := result.Error(); err != nil {
return a.webhookError(attributes, err)
}
var statusCode int
if result.StatusCode(&statusCode); statusCode < 200 || statusCode >= 300 {
return a.webhookError(attributes, fmt.Errorf("Error contacting webhook: %d", statusCode))
}
if err := result.Into(review); err != nil {
return a.webhookError(attributes, err)
}
a.responseCache.Add(string(cacheKey), review.Status, a.statusTTL(review.Status))
}
if !review.Status.Allowed {
if len(review.Status.Reason) > 0 {
return fmt.Errorf("image policy webook backend denied one or more images: %s", review.Status.Reason)
}
return errors.New("one or more images rejected by webhook backend")
}
return nil
}
// NewImagePolicyWebhook a new imagePolicyWebhook from the provided config file.
// The config file is specified by --admission-controller-config-file and has the
// following format for a webhook:
//
// {
// "imagePolicy": {
// "kubeConfigFile": "path/to/kubeconfig/for/backend",
// "allowTTL": 30, # time in s to cache approval
// "denyTTL": 30, # time in s to cache denial
// "retryBackoff": 500, # time in ms to wait between retries
// "defaultAllow": true # determines behavior if the webhook backend fails
// }
// }
//
// The config file may be json or yaml.
//
// The kubeconfig property refers to another file in the kubeconfig format which
// specifies how to connect to the webhook backend.
//
// The kubeconfig's cluster field is used to refer to the remote service, user refers to the returned authorizer.
//
// # clusters refers to the remote service.
// clusters:
// - name: name-of-remote-imagepolicy-service
// cluster:
// certificate-authority: /path/to/ca.pem # CA for verifying the remote service.
// server: https://images.example.com/policy # URL of remote service to query. Must use 'https'.
//
// # users refers to the API server's webhook configuration.
// users:
// - name: name-of-api-server
// user:
// client-certificate: /path/to/cert.pem # cert for the webhook plugin to use
// client-key: /path/to/key.pem # key matching the cert
//
// For additional HTTP configuration, refer to the kubeconfig documentation
// http://kubernetes.io/v1.1/docs/user-guide/kubeconfig-file.html.
func NewImagePolicyWebhook(client clientset.Interface, configFile io.Reader) (admission.Interface, error) {
var config AdmissionConfig
d := yaml.NewYAMLOrJSONDecoder(configFile, 4096)
err := d.Decode(&config)
if err != nil {
return nil, err
}
whConfig := config.ImagePolicyWebhook
if err := normalizeWebhookConfig(&whConfig); err != nil {
return nil, err
}
gw, err := webhook.NewGenericWebhook(whConfig.KubeConfigFile, groupVersions, whConfig.RetryBackoff)
if err != nil {
return nil, err
}
return &imagePolicyWebhook{
Handler: admission.NewHandler(admission.Create, admission.Update),
webhook: gw,
responseCache: cache.NewLRUExpireCache(1024),
allowTTL: whConfig.AllowTTL,
denyTTL: whConfig.DenyTTL,
defaultAllow: whConfig.DefaultAllow,
}, nil
}
/*
Copyright 2016 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 imagepolicy contains an admission controller that configures a webhook to which policy
// decisions are delegated.
package imagepolicy
import (
"fmt"
"time"
"github.com/golang/glog"
)
const (
defaultRetryBackoff = time.Duration(500) * time.Millisecond
minRetryBackoff = time.Duration(1)
maxRetryBackoff = time.Duration(5) * time.Minute
defaultAllowTTL = time.Duration(5) * time.Minute
defaultDenyTTL = time.Duration(30) * time.Second
minAllowTTL = time.Duration(1) * time.Second
maxAllowTTL = time.Duration(30) * time.Minute
minDenyTTL = time.Duration(1) * time.Second
maxDenyTTL = time.Duration(30) * time.Minute
useDefault = time.Duration(0) //sentinel for using default TTL
disableTTL = time.Duration(-1) //sentinel for disabling a TTL
)
// imagePolicyWebhookConfig holds config data for imagePolicyWebhook
type imagePolicyWebhookConfig struct {
KubeConfigFile string `json:"kubeConfigFile"`
AllowTTL time.Duration `json:"allowTTL"`
DenyTTL time.Duration `json:"denyTTL"`
RetryBackoff time.Duration `json:"retryBackoff"`
DefaultAllow bool `json:"defaultAllow"`
}
// AdmissionConfig holds config data for admission controllers
type AdmissionConfig struct {
ImagePolicyWebhook imagePolicyWebhookConfig `json:"imagePolicy"`
}
func normalizeWebhookConfig(config *imagePolicyWebhookConfig) (err error) {
config.RetryBackoff, err = normalizeConfigDuration("backoff", time.Millisecond, config.RetryBackoff, minRetryBackoff, maxRetryBackoff, defaultRetryBackoff)
if err != nil {
return err
}
config.AllowTTL, err = normalizeConfigDuration("allow cache", time.Second, config.AllowTTL, minAllowTTL, maxAllowTTL, defaultAllowTTL)
if err != nil {
return err
}
config.DenyTTL, err = normalizeConfigDuration("deny cache", time.Second, config.DenyTTL, minDenyTTL, maxDenyTTL, defaultDenyTTL)
if err != nil {
return err
}
return nil
}
func normalizeConfigDuration(name string, scale, value, min, max, defaultValue time.Duration) (time.Duration, error) {
// disable with -1 sentinel
if value == disableTTL {
glog.V(2).Infof("image policy webhook %s disabled", name)
return time.Duration(0), nil
}
// use defualt with 0 sentinel
if value == useDefault {
glog.V(2).Infof("image policy webhook %s using default value", name)
return defaultValue, nil
}
// convert to s; unmarshalling gives ns
value *= scale
// check value is within range
if value <= min || value > max {
return value, fmt.Errorf("valid value is between %v and %v, got %v", min, max, value)
}
return value, nil
}
/*
Copyright 2016 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 imagepolicy
import (
"reflect"
"testing"
"time"
)
func TestConfigNormalization(t *testing.T) {
tests := []struct {
test string
config imagePolicyWebhookConfig
normalizedConfig imagePolicyWebhookConfig
wantErr bool
}{
{
test: "config within normal ranges",
config: imagePolicyWebhookConfig{
AllowTTL: ((minAllowTTL + maxAllowTTL) / 2) / time.Second,
DenyTTL: ((minDenyTTL + maxDenyTTL) / 2) / time.Second,
RetryBackoff: ((minRetryBackoff + maxRetryBackoff) / 2) / time.Millisecond,
},
normalizedConfig: imagePolicyWebhookConfig{
AllowTTL: ((minAllowTTL + maxAllowTTL) / 2) / time.Second * time.Second,
DenyTTL: ((minDenyTTL + maxDenyTTL) / 2) / time.Second * time.Second,
RetryBackoff: (minRetryBackoff + maxRetryBackoff) / 2,
},
wantErr: false,
},
{
test: "config below normal ranges, error",
config: imagePolicyWebhookConfig{
AllowTTL: minAllowTTL - time.Duration(1),
DenyTTL: minDenyTTL - time.Duration(1),
RetryBackoff: minRetryBackoff - time.Duration(1),
},
wantErr: true,
},
{
test: "config above normal ranges, error",
config: imagePolicyWebhookConfig{
AllowTTL: time.Duration(1) + maxAllowTTL,
DenyTTL: time.Duration(1) + maxDenyTTL,
RetryBackoff: time.Duration(1) + maxRetryBackoff,
},
wantErr: true,
},
{
test: "config wants default values",
config: imagePolicyWebhookConfig{
AllowTTL: useDefault,
DenyTTL: useDefault,
RetryBackoff: useDefault,
},
normalizedConfig: imagePolicyWebhookConfig{
AllowTTL: defaultAllowTTL,
DenyTTL: defaultDenyTTL,
RetryBackoff: defaultRetryBackoff,
},
wantErr: false,
},
{
test: "config wants disabled values",
config: imagePolicyWebhookConfig{
AllowTTL: disableTTL,
DenyTTL: disableTTL,
RetryBackoff: disableTTL,
},
normalizedConfig: imagePolicyWebhookConfig{
AllowTTL: time.Duration(0),
DenyTTL: time.Duration(0),
RetryBackoff: time.Duration(0),
},
wantErr: false,
},
}
for _, tt := range tests {
err := normalizeWebhookConfig(&tt.config)
if err == nil && tt.wantErr == true {
t.Errorf("%s: expected error from normalization and didn't have one", tt.test)
}
if err != nil && tt.wantErr == false {
t.Errorf("%s: unexpected error from normalization: %v", tt.test, err)
}
if err == nil && !reflect.DeepEqual(tt.config, tt.normalizedConfig) {
t.Errorf("%s: expected config to be normalized. got: %v expected: %v", tt.test, tt.config, tt.normalizedConfig)
}
}
}
/*
Copyright 2016 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 imagepolicy checks a webhook for image admission
package imagepolicy // import "k8s.io/kubernetes/plugin/pkg/admission/imagepolicy"
#!/bin/bash
# Copyright 2016 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 webhook authz plugin tests.
#
# It is not expected to be run often (there is no go generate rule), and mainly
# exists for documentation purposes.
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 = 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
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=webhook_imagepolicy_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=webhook_imagepolicy_ca"
# Create a server certiticate
openssl genrsa -out serverKey.pem 2048
openssl req -new -key serverKey.pem -out server.csr -subj "/CN=webhook_imagepolicy_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=webhook_imagepolicy_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 2016 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 imagepolicy webhook tests." >> $outfile
echo "" >> $outfile
echo "package imagepolicy" >> $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
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment