1
0
Fork 0
mirror of https://github.com/kyverno/kyverno.git synced 2025-03-31 03:45:17 +00:00
This commit is contained in:
Denis Belyshev 2019-03-21 19:19:24 +02:00
commit a9843b2f55
14 changed files with 756 additions and 87 deletions

17
config/config.go Normal file
View file

@ -0,0 +1,17 @@
package config
const (
// These constants MUST be equal to the corresponding names in service definition in definitions/install.yaml
WebhookServiceNamespace = "kube-system"
WebhookServiceName = "kube-policy-svc"
WebhookConfigName = "nirmata-kube-policy-webhook-cfg"
MutationWebhookName = "webhook.nirmata.kube-policy"
)
var (
WebhookServicePath = "/mutate"
WebhookConfigLabels = map[string]string {
"app": "kube-policy",
}
)

17
constants/constants.go Normal file
View file

@ -0,0 +1,17 @@
package constants
const (
// These constants MUST be equal to the corresponding names in service definition in definitions/install.yaml
WebhookServiceNamespace = "kube-system"
WebhookServiceName = "kube-policy-svc"
WebhookConfigName = "nirmata-kube-policy-webhook-cfg"
MutationWebhookName = "webhook.nirmata.kube-policy"
)
var (
WebhookServicePath = "/mutate"
WebhookConfigLabels = map[string]string {
"app": "kube-policy",
}
)

View file

@ -145,3 +145,60 @@ spec:
type: object
additionalProperties:
type: string
---
apiVersion: v1
kind: Service
metadata:
namespace: kube-system
name: kube-policy-svc
labels:
app: kube-policy
spec:
ports:
- port: 443
targetPort: 443
selector:
app: kube-policy
---
apiVersion: v1
kind: ServiceAccount
metadata:
name: kube-policy-service-account
namespace: kube-system
---
kind: ClusterRoleBinding
apiVersion: rbac.authorization.k8s.io/v1beta1
metadata:
name: kube-policy-admin
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: ClusterRole
name: cluster-admin
subjects:
- kind: ServiceAccount
name: kube-policy-service-account
namespace: kube-system
---
apiVersion: extensions/v1beta1
kind: Deployment
metadata:
namespace: kube-system
name: kube-policy-deployment
labels:
app: kube-policy
spec:
replicas: 1
template:
metadata:
labels:
app: kube-policy
spec:
serviceAccountName: kube-policy-service-account
containers:
- name: kube-policy
image: nirmata/kube-policy:latest
imagePullPolicy: IfNotPresent
ports:
- containerPort: 443
securityContext:
privileged: true

76
init.go Normal file
View file

@ -0,0 +1,76 @@
package main
import (
"io/ioutil"
"log"
"net/url"
"github.com/nirmata/kube-policy/config"
"github.com/nirmata/kube-policy/kubeclient"
"github.com/nirmata/kube-policy/utils"
rest "k8s.io/client-go/rest"
clientcmd "k8s.io/client-go/tools/clientcmd"
)
func createClientConfig(kubeconfig string) (*rest.Config, error) {
if kubeconfig == "" {
log.Printf("Using in-cluster configuration")
return rest.InClusterConfig()
} else {
log.Printf("Using configuration from '%s'", kubeconfig)
return clientcmd.BuildConfigFromFlags("", kubeconfig)
}
}
func readTlsPairFromFiles(certFile, keyFile string) *utils.TlsPemPair {
if certFile == "" || keyFile == "" {
return nil
}
certContent, err := ioutil.ReadFile(certFile)
if err != nil {
log.Printf("Unable to read file with TLS certificate: path - %s, error - %v", certFile, err)
return nil
}
keyContent, err := ioutil.ReadFile(keyFile)
if err != nil {
log.Printf("Unable to read file with TLS private key: path - %s, error - %v", keyFile, err)
return nil
}
return &utils.TlsPemPair{
Certificate: certContent,
PrivateKey: keyContent,
}
}
// Loads or creates PEM private key and TLS certificate for webhook server
// Returns struct with key/certificate pair
func initTlsPemsPair(configuration *rest.Config, client *kubeclient.KubeClient) (*utils.TlsPemPair, error) {
apiServerUrl, err := url.Parse(configuration.Host)
if err != nil {
return nil, err
}
certProps := utils.TlsCertificateProps{
Service: config.WebhookServiceName,
Namespace: config.WebhookServiceNamespace,
ApiServerHost: apiServerUrl.Hostname(),
}
tlsPair := client.ReadTlsPair(certProps)
if utils.IsTlsPairShouldBeUpdated(tlsPair) {
log.Printf("Generating new key/certificate pair for TLS")
tlsPair, err = client.GenerateTlsPemPair(certProps)
if err != nil {
return nil, err
}
err = client.WriteTlsPair(certProps, tlsPair)
if err != nil {
log.Printf("Unable to save TLS pair to the cluster: %v", err)
}
}
return tlsPair, nil
}

181
kubeclient/certificates.go Normal file
View file

@ -0,0 +1,181 @@
package kubeclient
import (
"errors"
"fmt"
"time"
"github.com/nirmata/kube-policy/utils"
certificates "k8s.io/api/certificates/v1beta1"
v1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)
// Issues TLS certificate for webhook server using given PEM private key
// Returns signed and approved TLS certificate in PEM format
func (kc *KubeClient) GenerateTlsPemPair(props utils.TlsCertificateProps) (*utils.TlsPemPair, error) {
privateKey, err := utils.TlsGeneratePrivateKey()
if err != nil {
return nil, err
}
certRequest, err := utils.TlsCertificateGenerateRequest(privateKey, props)
if err != nil {
return nil, errors.New(fmt.Sprintf("Unable to create certificate request: %v", err))
}
certRequest, err = kc.submitAndApproveCertificateRequest(certRequest)
if err != nil {
return nil, errors.New(fmt.Sprintf("Unable to submit and approve certificate request: %v", err))
}
tlsCert, err := kc.fetchCertificateFromRequest(certRequest, 10)
if err != nil {
return nil, errors.New(fmt.Sprintf("Unable to fetch certificate from request: %v", err))
}
return &utils.TlsPemPair{
Certificate: tlsCert,
PrivateKey: utils.TlsPrivateKeyToPem(privateKey),
}, nil
}
// Submits and approves certificate request, returns request which need to be fetched
func (kc *KubeClient) submitAndApproveCertificateRequest(req *certificates.CertificateSigningRequest) (*certificates.CertificateSigningRequest, error) {
certClient := kc.client.CertificatesV1beta1().CertificateSigningRequests()
csrList, err := certClient.List(metav1.ListOptions{})
if err != nil {
return nil, errors.New(fmt.Sprintf("Unable to list existing certificate requests: %v", err))
}
for _, csr := range csrList.Items {
if csr.ObjectMeta.Name == req.ObjectMeta.Name {
err := certClient.Delete(csr.ObjectMeta.Name, defaultDeleteOptions())
if err != nil {
return nil, errors.New(fmt.Sprintf("Unable to delete existing certificate request: %v", err))
}
kc.logger.Printf("Old certificate request is deleted")
break
}
}
res, err := certClient.Create(req)
if err != nil {
return nil, err
}
kc.logger.Printf("Certificate request %s is created", req.ObjectMeta.Name)
res.Status.Conditions = append(res.Status.Conditions, certificates.CertificateSigningRequestCondition{
Type: certificates.CertificateApproved,
Reason: "NKP-Approve",
Message: "This CSR was approved by Nirmata kube-policy controller",
})
res, err = certClient.UpdateApproval(res)
if err != nil {
return nil, errors.New(fmt.Sprintf("Unable to approve certificate request: %v", err))
}
kc.logger.Printf("Certificate request %s is approved", res.ObjectMeta.Name)
return res, nil
}
const certificateFetchWaitInterval time.Duration = 200 * time.Millisecond
// Fetches certificate from given request. Tries to obtain certificate for maxWaitSeconds
func (kc *KubeClient) fetchCertificateFromRequest(req *certificates.CertificateSigningRequest, maxWaitSeconds uint8) ([]byte, error) {
// TODO: react of SIGINT and SIGTERM
timeStart := time.Now()
certClient := kc.client.CertificatesV1beta1().CertificateSigningRequests()
for time.Now().Sub(timeStart) < time.Duration(maxWaitSeconds)*time.Second {
r, err := certClient.Get(req.ObjectMeta.Name, defaultGetOptions())
if err != nil {
return nil, err
}
if r.Status.Certificate != nil {
return r.Status.Certificate, nil
}
for _, condition := range r.Status.Conditions {
if condition.Type == certificates.CertificateDenied {
return nil, errors.New(condition.String())
}
}
}
return nil, errors.New(fmt.Sprintf("Cerificate fetch timeout is reached: %d seconds", maxWaitSeconds))
}
const privateKeyField string = "privateKey"
const certificateField string = "certificate"
// Reads the pair of TLS certificate and key from the specified secret.
func (kc *KubeClient) ReadTlsPair(props utils.TlsCertificateProps) *utils.TlsPemPair {
name := generateSecretName(props)
secret, err := kc.client.CoreV1().Secrets(props.Namespace).Get(name, defaultGetOptions())
if err != nil {
kc.logger.Printf("Unable to get secret %s/%s: %s", props.Namespace, name, err)
return nil
}
pemPair := utils.TlsPemPair{
Certificate: secret.Data[certificateField],
PrivateKey: secret.Data[privateKeyField],
}
if len(pemPair.Certificate) == 0 {
kc.logger.Printf("TLS Certificate not found in secret %s/%s", props.Namespace, name)
return nil
}
if len(pemPair.PrivateKey) == 0 {
kc.logger.Printf("TLS PrivateKey not found in secret %s/%s", props.Namespace, name)
return nil
}
return &pemPair
}
// Writes the pair of TLS certificate and key to the specified secret.
// Updates existing secret or creates new one.
func (kc *KubeClient) WriteTlsPair(props utils.TlsCertificateProps, pemPair *utils.TlsPemPair) error {
name := generateSecretName(props)
secret, err := kc.client.CoreV1().Secrets(props.Namespace).Get(name, defaultGetOptions())
if err == nil { // Update existing secret
if secret.Data == nil {
secret.Data = make(map[string][]byte)
}
secret.Data[certificateField] = pemPair.Certificate
secret.Data[privateKeyField] = pemPair.PrivateKey
secret, err = kc.client.CoreV1().Secrets(props.Namespace).Update(secret)
if err == nil {
kc.logger.Printf("Secret %s is updated", name)
}
} else { // Create new secret
secret = &v1.Secret{
TypeMeta: metav1.TypeMeta{
Kind: "Secret",
APIVersion: "v1",
},
ObjectMeta: metav1.ObjectMeta{
Name: name,
Namespace: props.Namespace,
},
Data: map[string][]byte{
certificateField: pemPair.Certificate,
privateKeyField: pemPair.PrivateKey,
},
}
secret, err = kc.client.CoreV1().Secrets(props.Namespace).Create(secret)
if err == nil {
kc.logger.Printf("Secret %s is created", name)
}
}
return err
}
func generateSecretName(props utils.TlsCertificateProps) string {
return utils.GenerateInClusterServiceName(props) + ".kube-policy-tls-pair"
}

View file

@ -22,7 +22,7 @@ type KubeClient struct {
// Checks parameters and creates new instance of KubeClient
func NewKubeClient(config *rest.Config, logger *log.Logger) (*KubeClient, error) {
if logger == nil {
logger = log.New(os.Stdout, "Policy Controller: ", log.LstdFlags|log.Lshortfile)
logger = log.New(os.Stdout, "Kubernetes client: ", log.LstdFlags|log.Lshortfile)
}
client, err := kubernetes.NewForConfig(config)
@ -113,6 +113,13 @@ func defaultGetOptions() metav1.GetOptions {
}
}
func defaultDeleteOptions() *metav1.DeleteOptions {
var deletePeriod int64 = 0
return &metav1.DeleteOptions{
GracePeriodSeconds: &deletePeriod,
}
}
const namespaceCreationMaxWaitTime time.Duration = 30 * time.Second
const namespaceCreationWaitInterval time.Duration = 100 * time.Millisecond

54
main.go
View file

@ -2,51 +2,36 @@ package main
import (
"flag"
"fmt"
"log"
"github.com/nirmata/kube-policy/controller"
"github.com/nirmata/kube-policy/kubeclient"
"github.com/nirmata/kube-policy/webhooks"
"github.com/nirmata/kube-policy/server"
rest "k8s.io/client-go/rest"
clientcmd "k8s.io/client-go/tools/clientcmd"
signals "k8s.io/sample-controller/pkg/signals"
)
var (
masterURL string
kubeconfig string
cert string
key string
)
func createClientConfig(masterURL, kubeconfig string) (*rest.Config, error) {
// TODO: make possible to create config within a cluster with proper rights
config, err := clientcmd.BuildConfigFromFlags(masterURL, kubeconfig)
if err != nil {
return nil, err
}
return config, nil
}
func main() {
flag.Parse()
if cert == "" || key == "" {
log.Fatal("TLS certificate or/and key is not set")
}
clientConfig, err := createClientConfig(masterURL, kubeconfig)
clientConfig, err := createClientConfig(kubeconfig)
if err != nil {
log.Fatalf("Error building kubeconfig: %v\n", err)
return
}
err = webhooks.RegisterMutationWebhook(clientConfig)
if err != nil {
log.Fatalf("Error registering mutation webhook server: %v\n", err)
}
controller, err := controller.NewPolicyController(clientConfig, nil)
if err != nil {
log.Fatalf("Error creating PolicyController! Error: %s\n", err)
return
log.Fatalf("Error creating PolicyController: %s\n", err)
}
kubeclient, err := kubeclient.NewKubeClient(clientConfig, nil)
@ -54,13 +39,21 @@ func main() {
log.Fatalf("Error creating kubeclient: %v\n", err)
}
tlsPair := readTlsPairFromFiles(cert, key)
if tlsPair != nil {
log.Print("Using given TLS key/certificate pair")
} else {
tlsPair, err = initTlsPemsPair(clientConfig, kubeclient)
if err != nil {
log.Fatalf("Failed to initialize TLS key/certificate pair: %v\n", err)
}
}
serverConfig := server.WebhookServerConfig{
CertFile: cert,
KeyFile: key,
TlsPemPair: tlsPair,
Controller: controller,
Kubeclient: kubeclient,
}
server, err := server.NewWebhookServer(serverConfig, nil)
if err != nil {
log.Fatalf("Unable to create webhook server: %v\n", err)
@ -71,19 +64,18 @@ func main() {
controller.Run(stopCh)
if err != nil {
log.Fatalf("Error running PolicyController! Error: %s\n", err)
return
log.Fatalf("Error running PolicyController: %s\n", err)
}
fmt.Println("Policy Controller has started")
log.Println("Policy Controller has started")
<-stopCh
server.Stop()
fmt.Println("Policy Controller has stopped")
log.Println("Policy Controller has stopped")
}
func init() {
flag.StringVar(&kubeconfig, "kubeconfig", "", "Path to a kubeconfig. Only required if out-of-cluster.")
flag.StringVar(&masterURL, "master", "", "The address of the Kubernetes API server. Overrides any value in kubeconfig. Only required if out-of-cluster.")
flag.StringVar(&cert, "cert", "", "TLS certificate used in connection with cluster.")
flag.StringVar(&key, "key", "", "Key, used in TLS connection.")
flag.Parse()
}

View file

@ -12,9 +12,12 @@ import (
"os"
"time"
controller "github.com/nirmata/kube-policy/controller"
kubeclient "github.com/nirmata/kube-policy/kubeclient"
webhooks "github.com/nirmata/kube-policy/webhooks"
"github.com/nirmata/kube-policy/config"
"github.com/nirmata/kube-policy/controller"
"github.com/nirmata/kube-policy/kubeclient"
"github.com/nirmata/kube-policy/utils"
"github.com/nirmata/kube-policy/webhooks"
v1beta1 "k8s.io/api/admission/v1beta1"
)
@ -30,31 +33,30 @@ type WebhookServer struct {
// Configuration struct for WebhookServer used in NewWebhookServer
// Controller and Kubeclient should be initialized and valid
type WebhookServerConfig struct {
CertFile string
KeyFile string
TlsPemPair *utils.TlsPemPair
Controller *controller.PolicyController
Kubeclient *kubeclient.KubeClient
}
// NewWebhookServer creates new instance of WebhookServer accordingly to given configuration
// Policy Controller and Kubernetes Client should be initialized in configuration
func NewWebhookServer(config WebhookServerConfig, logger *log.Logger) (*WebhookServer, error) {
func NewWebhookServer(configuration WebhookServerConfig, logger *log.Logger) (*WebhookServer, error) {
if logger == nil {
logger = log.New(os.Stdout, "HTTPS Server: ", log.LstdFlags|log.Lshortfile)
}
if config.Controller == nil || config.Kubeclient == nil {
return nil, errors.New("WebHook server requires initialized Policy Controller and Kubernetes Client")
if configuration.TlsPemPair == nil || configuration.Controller == nil || configuration.Kubeclient == nil {
return nil, errors.New("WebhookServerConfig is not initialized properly")
}
var tlsConfig tls.Config
pair, err := tls.LoadX509KeyPair(config.CertFile, config.KeyFile)
pair, err := tls.X509KeyPair(configuration.TlsPemPair.Certificate, configuration.TlsPemPair.PrivateKey)
if err != nil {
return nil, err
}
tlsConfig.Certificates = []tls.Certificate{pair}
mw, err := webhooks.NewMutationWebhook(config.Kubeclient, config.Controller, logger)
mw, err := webhooks.NewMutationWebhook(configuration.Kubeclient, configuration.Controller, logger)
if err != nil {
return nil, err
}
@ -65,7 +67,7 @@ func NewWebhookServer(config WebhookServerConfig, logger *log.Logger) (*WebhookS
}
mux := http.NewServeMux()
mux.HandleFunc("/mutate", ws.serve)
mux.HandleFunc(config.WebhookServicePath, ws.serve)
ws.server = http.Server{
Addr: ":443", // Listen on port for HTTPS requests
@ -81,7 +83,7 @@ func NewWebhookServer(config WebhookServerConfig, logger *log.Logger) (*WebhookS
// Main server endpoint for all requests
func (ws *WebhookServer) serve(w http.ResponseWriter, r *http.Request) {
if r.URL.Path == "/mutate" {
if r.URL.Path == config.WebhookServicePath {
admissionReview := ws.parseAdmissionReview(r, w)
if admissionReview == nil {
return

141
utils/certificates_utils.go Normal file
View file

@ -0,0 +1,141 @@
package utils
import (
"crypto/rand"
"crypto/rsa"
"crypto/x509"
"crypto/x509/pkix"
"encoding/pem"
"errors"
"net"
"time"
certificates "k8s.io/api/certificates/v1beta1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)
// Properties of TLS certificate which should be issued for webhook server
type TlsCertificateProps struct {
Service string
Namespace string
ApiServerHost string
}
// The pair of TLS certificate corresponding private key, both in PEM format
type TlsPemPair struct {
Certificate []byte
PrivateKey []byte
}
// Generates RSA private key
func TlsGeneratePrivateKey() (*rsa.PrivateKey, error) {
return rsa.GenerateKey(rand.Reader, 2048)
}
// Creates PEM block from private key object
func TlsPrivateKeyToPem(rsaKey *rsa.PrivateKey) []byte {
privateKey := &pem.Block{
Type: "PRIVATE KEY",
Bytes: x509.MarshalPKCS1PrivateKey(rsaKey),
}
return pem.EncodeToMemory(privateKey)
}
// Creates PEM block from raw certificate request
func TlsCertificateRequestToPem(csrRaw []byte) []byte {
csrBlock := &pem.Block{
Type: "CERTIFICATE REQUEST",
Bytes: csrRaw,
}
return pem.EncodeToMemory(csrBlock)
}
// Generates raw certificate signing request
func TlsCertificateGenerateRequest(privateKey *rsa.PrivateKey, props TlsCertificateProps) (*certificates.CertificateSigningRequest, error) {
dnsNames := make([]string, 3)
dnsNames[0] = props.Service
dnsNames[1] = props.Service + "." + props.Namespace
// The full service name is the CommonName for the certificate
commonName := GenerateInClusterServiceName(props)
dnsNames[2] = commonName
var ips []net.IP
apiServerIp := net.ParseIP(props.ApiServerHost)
if apiServerIp != nil {
ips = append(ips, apiServerIp)
} else {
dnsNames = append(dnsNames, props.ApiServerHost)
}
csrTemplate := x509.CertificateRequest{
Subject: pkix.Name{
CommonName: props.Service, //commonName,
},
SignatureAlgorithm: x509.SHA256WithRSA,
DNSNames: dnsNames,
IPAddresses: ips,
}
csrBytes, err := x509.CreateCertificateRequest(rand.Reader, &csrTemplate, privateKey)
if err != nil {
return nil, err
}
return &certificates.CertificateSigningRequest{
TypeMeta: metav1.TypeMeta{
APIVersion: "certificates.k8s.io/v1beta1",
Kind: "CertificateSigningRequest",
},
ObjectMeta: metav1.ObjectMeta{
Name: props.Service + "." + props.Namespace + ".cert-request",
},
Spec: certificates.CertificateSigningRequestSpec{
Request: TlsCertificateRequestToPem(csrBytes),
Groups: []string{"system:masters", "system:authenticated"},
Usages: []certificates.KeyUsage{
certificates.UsageDigitalSignature,
certificates.UsageKeyEncipherment,
certificates.UsageServerAuth,
certificates.UsageClientAuth,
},
},
}, nil
}
// The generated service name should be the common name for TLS certificate
func GenerateInClusterServiceName(props TlsCertificateProps) string {
return props.Service + "." + props.Namespace + ".svc"
}
//Gets NotAfter property from raw certificate
func TlsCertificateGetExpirationDate(certData []byte) (*time.Time, error) {
block, _ := pem.Decode(certData)
if block == nil {
return nil, errors.New("Failed to decode PEM")
}
cert, err := x509.ParseCertificate(block.Bytes)
if err != nil {
return nil, errors.New("Failed to parse certificate: %v" + err.Error())
}
return &cert.NotAfter, nil
}
// The certificate is valid for a year, but we update it earlier to avoid using
// an expired certificate in a controller that has been running for a long time
const timeReserveBeforeCertificateExpiration time.Duration = time.Hour * 24 * 30 * 6 // About half a year
func IsTlsPairShouldBeUpdated(tlsPair *TlsPemPair) bool {
if tlsPair == nil {
return true
}
expirationDate, err := TlsCertificateGetExpirationDate(tlsPair.Certificate)
if err != nil {
return true
}
return expirationDate.Sub(time.Now()) < timeReserveBeforeCertificateExpiration
}

View file

@ -41,42 +41,32 @@ func (mw *MutationWebhook) Mutate(request *v1beta1.AdmissionRequest) *v1beta1.Ad
var allPatches []PatchBytes
for _, policy := range policies {
patchingSets := getPolicyPatchingSets(policy)
mw.logger.Printf("Applying policy %s with %d rules", policy.ObjectMeta.Name, len(policy.Spec.Rules))
for ruleIdx, rule := range policy.Spec.Rules {
err := rule.Validate()
if err != nil {
mw.logger.Printf("Invalid rule detected: #%d in policy %s", ruleIdx, policy.ObjectMeta.Name)
continue
}
policyPatches, err := mw.applyPolicyRules(request, policy)
if err != nil {
mw.controller.LogPolicyError(policy.Name, err.Error())
mw.logger.Printf("Applying policy %s, rule #%d", policy.ObjectMeta.Name, ruleIdx)
rulePatches, err := mw.applyRule(request, rule, patchingSets)
if err != nil {
return mw.denyResourceCreation(policy.Name, fmt.Sprintf("Unable to apply rule #%d: %s", ruleIdx, err))
}
errStr := fmt.Sprintf("Unable to apply policy %s: %v", policy.Name, err)
mw.logger.Printf("Denying the request because of error: %s", errStr)
return mw.denyResourceCreation(errStr)
}
rulePatchesProcessed, err := ProcessPatches(rulePatches, request.Object.Raw, patchingSets)
if err != nil {
return mw.denyResourceCreation(policy.Name, fmt.Sprintf("Unable to apply rule #%d: %s", ruleIdx, err))
}
if len(policyPatches) > 0 {
meta := parseMetadataFromObject(request.Object.Raw)
namespace := parseNamespaceFromMetadata(meta)
name := parseNameFromMetadata(meta)
mw.controller.LogPolicyInfo(policy.Name, fmt.Sprintf("Applied to %s %s/%s", request.Kind.Kind, namespace, name))
if rulePatches != nil {
allPatches = append(allPatches, rulePatchesProcessed...)
mw.logger.Printf("Prepared %d patches", len(rulePatchesProcessed))
} else {
mw.logger.Print("No patches prepared")
}
allPatches = append(allPatches, policyPatches...)
}
}
patchType := v1beta1.PatchTypeJSONPatch
return &v1beta1.AdmissionResponse{
Allowed: true,
Patch: JoinPatches(allPatches),
PatchType: func() *v1beta1.PatchType {
pt := v1beta1.PatchTypeJSONPatch
return &pt
}(),
Allowed: true,
Patch: JoinPatches(allPatches),
PatchType: &patchType,
}
}
@ -89,20 +79,43 @@ func getPolicyPatchingSets(policy types.Policy) PatchingSets {
return PatchingSetsDefault
}
// Applies all rule to the created object and returns list of JSON patches.
// Applies all policy rules to the created object and returns list of processed JSON patches.
// May return nil patches if it is not necessary to create patches for requested object.
// Returns error ONLY in case when creation of resource should be denied.
func (mw *MutationWebhook) applyRule(request *v1beta1.AdmissionRequest, rule types.PolicyRule, errorBehavior PatchingSets) ([]types.PolicyPatch, error) {
if !IsRuleApplicableToRequest(rule.Resource, request) {
return nil, nil
func (mw *MutationWebhook) applyPolicyRules(request *v1beta1.AdmissionRequest, policy types.Policy) ([]PatchBytes, error) {
patchingSets := getPolicyPatchingSets(policy)
var policyPatches []PatchBytes
for ruleIdx, rule := range policy.Spec.Rules {
err := rule.Validate()
if err != nil {
mw.logger.Printf("Invalid rule detected: #%d in policy %s", ruleIdx, policy.ObjectMeta.Name)
continue
}
if !IsRuleApplicableToRequest(rule.Resource, request) {
return nil, nil
}
err = mw.applyRuleGenerators(request, rule)
if err != nil && patchingSets == PatchingSetsStopOnError {
return nil, errors.New(fmt.Sprintf("Failed to apply generators from rule #%d: %s", ruleIdx, err))
}
rulePatchesProcessed, err := ProcessPatches(rule.Patches, request.Object.Raw, patchingSets)
if err != nil {
return nil, errors.New(fmt.Sprintf("Failed to process patches from rule #%d: %s", ruleIdx, err))
}
if rulePatchesProcessed != nil {
policyPatches = append(policyPatches, rulePatchesProcessed...)
mw.logger.Printf("Rule %d: prepared %d patches", ruleIdx, len(rulePatchesProcessed))
} else {
mw.logger.Print("Rule %d: no patches prepared")
}
}
err := mw.applyRuleGenerators(request, rule)
if err != nil && errorBehavior == PatchingSetsStopOnError {
return nil, err
} else {
return rule.Patches, nil
}
return policyPatches, nil
}
// Applies "configMapGenerator" and "secretGenerator" described in PolicyRule
@ -121,7 +134,7 @@ func (mw *MutationWebhook) applyRuleGenerators(request *v1beta1.AdmissionRequest
return nil
}
// Creates resourceKind (ConfigMap or Secret) with parameters specified in generator in cluster specified in request
// Creates resourceKind (ConfigMap or Secret) with parameters specified in generator in cluster specified in request.
func (mw *MutationWebhook) applyConfigGenerator(generator *types.PolicyConfigGenerator, namespace string, configKind string) error {
if generator == nil {
return nil
@ -149,10 +162,7 @@ func (mw *MutationWebhook) applyConfigGenerator(generator *types.PolicyConfigGen
}
// Forms AdmissionResponse with denial of resource creation and error message
func (mw *MutationWebhook) denyResourceCreation(policyName, errStr string) *v1beta1.AdmissionResponse {
mw.logger.Printf("Denying the request because of error: %s", errStr)
mw.controller.LogPolicyError(policyName, errStr)
func (mw *MutationWebhook) denyResourceCreation(errStr string) *v1beta1.AdmissionResponse {
return &v1beta1.AdmissionResponse{
Result: &metav1.Status{
Message: errStr,

82
webhooks/registration.go Normal file
View file

@ -0,0 +1,82 @@
package webhooks
import (
"io/ioutil"
"github.com/nirmata/kube-policy/config"
adm "k8s.io/api/admissionregistration/v1beta1"
meta "k8s.io/apimachinery/pkg/apis/meta/v1"
admreg "k8s.io/client-go/kubernetes/typed/admissionregistration/v1beta1"
rest "k8s.io/client-go/rest"
)
func RegisterMutationWebhook(config *rest.Config) error {
registrationClient, err := admreg.NewForConfig(config)
if err != nil {
return err
}
_, err = registrationClient.MutatingWebhookConfigurations().Create(constructWebhookConfig(config))
if err != nil {
return err
}
return nil
}
func constructWebhookConfig(configuration *rest.Config) *adm.MutatingWebhookConfiguration {
return &adm.MutatingWebhookConfiguration{
ObjectMeta: meta.ObjectMeta{
Name: config.WebhookConfigName,
Labels: config.WebhookConfigLabels,
},
Webhooks: []adm.Webhook{
adm.Webhook{
Name: config.MutationWebhookName,
ClientConfig: adm.WebhookClientConfig{
Service: &adm.ServiceReference{
Namespace: config.WebhookServiceNamespace,
Name: config.WebhookServiceName,
Path: &config.WebhookServicePath,
},
CABundle: ExtractCA(configuration),
},
Rules: []adm.RuleWithOperations{
adm.RuleWithOperations{
Operations: []adm.OperationType{
adm.Create,
},
Rule: adm.Rule{
APIGroups: []string{
"*",
},
APIVersions: []string{
"*",
},
Resources: []string{
"*/*",
},
},
},
},
},
},
}
}
func ExtractCA(config *rest.Config) (result []byte) {
fileName := config.TLSClientConfig.CAFile
if fileName != "" {
result, err := ioutil.ReadFile(fileName)
if err != nil {
return nil
}
return result
} else {
return config.TLSClientConfig.CAData
}
}

View file

@ -0,0 +1,66 @@
package webhooks_test
import (
"gotest.tools/assert"
"io/ioutil"
"testing"
"bytes"
"github.com/nirmata/kube-policy/webhooks"
rest "k8s.io/client-go/rest"
)
func TestExtractCA_EmptyBundle(t *testing.T) {
CAFile := "resources/CAFile"
config := &rest.Config {
TLSClientConfig: rest.TLSClientConfig {
CAData: nil,
CAFile: CAFile,
},
}
expected, err := ioutil.ReadFile(CAFile)
assert.Assert(t, err == nil)
actual := webhooks.ExtractCA(config)
assert.Assert(t, bytes.Equal(expected, actual))
}
func TestExtractCA_EmptyCAFile(t *testing.T) {
CABundle := []byte(`LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSUN5RENDQWJDZ0F3SUJBZ0lCQURBTkJna3Foa2lHOXcwQkFRc0ZBREFWTVJNd0VRWURWUVFERXdwcmRXSmwKY201bGRHVnpNQjRYRFRFNU1ETXhPVEUwTURjd05Gb1hEVEk1TURNeE5qRTBNRGN3TkZvd0ZURVRNQkVHQTFVRQpBeE1LYTNWaVpYSnVaWFJsY3pDQ0FTSXdEUVlKS29aSWh2Y05BUUVCQlFBRGdnRVBBRENDQVFvQ2dnRUJBTStQClVLVmExcm9tQndOZzdqNnBBSGo5TDQ4RVJpdEplRzRXM1pUYmNMNWNKbnVTQmFsc1h1TWpQTGZmbUV1VEZIdVAKenRqUlBEUHcreEg1d3VTWFF2U0tIaXF2VE1pUm9DSlJFa09sQXpIa1dQM0VrdnUzNzRqZDVGV3Q3NEhnRk91cApIZ1ZwdUxPblczK2NDVE5iQ3VkeDFMVldRbGgwQzJKbm1Lam5uS1YrTkxzNFJVaVk1dk91ekpuNHl6QldLRjM2CmJLZ3ZDOVpMWlFSM3dZcnJNZWllYzBnWVY2VlJtaGgxSjRDV3V1UWd0ckM2d2NJanFWZFdEUlJyNHFMdEtDcDIKQVNIZmNieitwcEdHblJ5Z2FzcWNJdnpiNUVwV3NIRGtHRStUUW5WQ0JmTmsxN0NEOTZBQ1pmRWVybzEvWE16MgpRbzZvcUE0dnF5ZkdWWVU5RVZFQ0F3RUFBYU1qTUNFd0RnWURWUjBQQVFIL0JBUURBZ0trTUE4R0ExVWRFd0VCCi93UUZNQU1CQWY4d0RRWUpLb1pJaHZjTkFRRUxCUUFEZ2dFQkFNWFVpUVJpdUc4cGdzcHMrZTdGZWdCdEJOZEcKZlFUdHVLRWFUZ0U0RjQwamJ3UmdrN25DTHlsSHgvRG04aVRRQmsyWjR4WnNuY0huRys4SkwrckRLdlJBSE5iVQpsYnpReXA1V3FwdjdPcThwZ01wU0o5bTdVY3BGZmRVZkorNW43aXFnTGdMb3lhNmtRVTR2Rk0yTE1rWjI5NVpxCmVId0hnREo5Z3IwWGNyOWM1L2tRdkxFc2Z2WU5QZVhuamNyWXlDb2JNcVduSElxeVd3cHM1VTJOaGgraXhSZEIKbzRRL3RJS04xOU93WGZBaVc5SENhNzZMb3ZXaUhPU2UxVnFzK1h1N1A5ckx4eW1vQm91aFcxVmZ0bUo5Qy9vTAp3cFVuNnlXRCttY0tkZ3J5QTFjTWJ4Q281bUd6YTNLaFk1QTd5eDQ1cThkSEIzTWU4d0FCam1wWEs0ST0KLS0tLS1FTkQgQ0VSVElGSUNBVEUtLS0tLQo=`)
config := &rest.Config {
TLSClientConfig: rest.TLSClientConfig {
CAData: CABundle,
CAFile: "",
},
}
actual := webhooks.ExtractCA(config)
assert.Assert(t, bytes.Equal(CABundle, actual))
}
func TestExtractCA_EmptyConfig(t *testing.T) {
config := &rest.Config {
TLSClientConfig: rest.TLSClientConfig {
CAData: nil,
CAFile: "",
},
}
actual := webhooks.ExtractCA(config)
assert.Assert(t, actual == nil)
}
func TestExtractCA_InvalidFile(t *testing.T) {
config := &rest.Config {
TLSClientConfig: rest.TLSClientConfig {
CAData: nil,
CAFile: "somenonexistingfile",
},
}
actual := webhooks.ExtractCA(config)
assert.Assert(t, actual == nil)
}

14
webhooks/resources/CAFile Normal file
View file

@ -0,0 +1,14 @@
-----BEGIN CERTIFICATE-----
V2VsY29tZSB0byBUaGUgUnVzdCBQcm9ncmFtbWluZyBMYW5ndWFnZSwgY
W4gaW50cm9kdWN0b3J5IGJvb2sgYWJvdXQgUnVzdC4gVGhlIFJ1c3QgcH
JvZ3JhbW1pbmcgbGFuZ3VhZ2UgaGVscHMgeW91IHdyaXRlIGZhc3Rlciw
gbW9yZSByZWxpYWJsZSBzb2Z0d2FyZS4gSGlnaC1sZXZlbCBlcmdvbm9t
aWNzIGFuZCBsb3ctbGV2ZWwgY29udHJvbCBhcmUgb2Z0ZW4gYXQgb2Rkc
yBpbiBwcm9ncmFtbWluZyBsYW5ndWFnZSBkZXNpZ247IFJ1c3QgY2hhbG
xlbmdlcyB0aGF0IGNvbmZsaWN0LiBUaHJvdWdoIGJhbGFuY2luZyBwb3d
lcmZ1bCB0ZWNobmljYWwgY2FwYWNpdHkgYW5kIGEgZ3JlYXQgZGV2ZWxv
cGVyIGV4cGVyaWVuY2UsIFJ1c3QgZ2l2ZXMgeW91IHRoZSBvcHRpb24gd
G8gY29udHJvbCBsb3ctbGV2ZWwgZGV0YWlscyAoc3VjaCBhcyBtZW1vcn
kgdXNhZ2UpIHdpdGhvdXQgYWxsIHRoZSBoYXNzbGUgdHJhZGl0aW9uYWx
seSBhc3NvY2lhdGVkIHdpdGggc3VjaCBjb250cm9sLgyzmqp31l8rqr1==
-----END CERTIFICATE-----

View file

@ -31,3 +31,10 @@ func parseNameFromMetadata(meta map[string]interface{}) string {
}
return ""
}
func parseNamespaceFromMetadata(meta map[string]interface{}) string {
if namespace, ok := meta["namespace"].(string); ok {
return namespace
}
return ""
}