diff --git a/charts/datree-admission-webhook/templates/clusterrole.yaml b/charts/datree-admission-webhook/templates/clusterrole.yaml index b8818799..a35c9b3e 100644 --- a/charts/datree-admission-webhook/templates/clusterrole.yaml +++ b/charts/datree-admission-webhook/templates/clusterrole.yaml @@ -8,6 +8,13 @@ metadata: annotations: {{ toYaml . | nindent 4 }} {{- end }} rules: + - apiGroups: + - "admissionregistration.k8s.io" + resources: + - "validatingwebhookconfigurations" + verbs: + - "get" + - "update" - apiGroups: - "user.openshift.io" resources: diff --git a/charts/datree-admission-webhook/templates/deployment.yaml b/charts/datree-admission-webhook/templates/deployment.yaml index a0460d7b..e45afaa7 100644 --- a/charts/datree-admission-webhook/templates/deployment.yaml +++ b/charts/datree-admission-webhook/templates/deployment.yaml @@ -1,5 +1,7 @@ {{- $emptyTokenMessage := "\n\t❌ The installation failed since the token that was provided is invalid.\n\t💡 To fix this issue:\n\t\t1. Get your token at: https://app.datree.io/settings/token-management\n\t\t2. Reinstall Datree and set your token using helm:\n\n\t\thelm install -n datree datree-webhook datree-webhook/datree-admission-webhook --create-namespace --set datree.token=\n" -}} {{- $uuidv4RegexPattern := "^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$" -}} +{{- $webhookCertsVolumeName := "webhook-certs-volume" -}} +{{- $webhookCertsVolumeClaimName := "webhook-certs-volume-claim" -}} apiVersion: apps/v1 kind: Deployment metadata: @@ -106,16 +108,15 @@ spec: - containerPort: 5555 name: debug volumeMounts: - - name: webhook-tls-certs - mountPath: /run/secrets/tls - readOnly: true + - mountPath: /etc/webhook-certs + name: {{ $webhookCertsVolumeName }} - name: webhook-config mountPath: /config readOnly: true volumes: - - name: webhook-tls-certs - secret: - secretName: webhook-server-tls + - name: {{ $webhookCertsVolumeName }} + persistentVolumeClaim: + claimName: {{ $webhookCertsVolumeClaimName }} - name: webhook-config projected: sources: @@ -125,3 +126,15 @@ spec: - configMap: name: webhook-scanning-filters optional: true +--- +apiVersion: v1 +kind: PersistentVolumeClaim +metadata: + name: {{ $webhookCertsVolumeClaimName }} + namespace: {{ template "datree.namespace" . }} +spec: + accessModes: + - ReadWriteMany + resources: + requests: + storage: 1Gi diff --git a/charts/datree-admission-webhook/templates/wait-server-ready-post-install.yaml b/charts/datree-admission-webhook/templates/wait-server-ready-post-install.yaml deleted file mode 100644 index 02cd97ed..00000000 --- a/charts/datree-admission-webhook/templates/wait-server-ready-post-install.yaml +++ /dev/null @@ -1,41 +0,0 @@ -apiVersion: batch/v1 -kind: Job -metadata: - name: datree-wait-server-ready-hook-post-install - namespace: {{template "datree.namespace" .}} - labels: {{ include "datree.labels" . | nindent 4 }} - annotations: - argocd.argoproj.io/hook: PostSync - argocd.argoproj.io/hook-delete-policy: BeforeHookCreation - "helm.sh/hook": post-install, post-upgrade - "helm.sh/hook-weight": "-5" - "helm.sh/hook-delete-policy": before-hook-creation, hook-succeeded, hook-failed - {{- with .Values.customAnnotations }} - {{ toYaml . }} - {{- end }} -spec: - ttlSecondsAfterFinished: {{.Values.hooks.ttlSecondsAfterFinished | default "1" }} - template: - metadata: - name: datree-wait-server-ready-hook-post-install - labels: {{ include "datree.labels" . | nindent 8 }} - {{- with .Values.customAnnotations }} - annotations: {{ toYaml . | nindent 8 }} - {{- end }} - spec: - serviceAccountName: datree-wait-server-ready-hook-post-install - restartPolicy: Never - {{- if .Values.imageCredentials.enabled }} - imagePullSecrets: - - name: datree-repo-credentials - {{- end }} - containers: - - name: kubectl-client - image: "{{ .Values.hooks.image.repository }}:{{ .Values.hooks.image.tag }}" - imagePullPolicy: {{.Values.hooks.image.pullPolicy}} - securityContext: {{- toYaml .Values.securityContext | nindent 12 }} - command: - - sh - - "-c" - - >- - kubectl wait --for=condition=ready pod -l app=datree-webhook-server --timeout="{{ default "180s" .Values.hooks.timeoutTime }}" diff --git a/charts/datree-admission-webhook/templates/webhook-with-cert-secrets.yaml b/charts/datree-admission-webhook/templates/webhook-with-cert-secrets.yaml index bfe539b0..2fbbc0fb 100644 --- a/charts/datree-admission-webhook/templates/webhook-with-cert-secrets.yaml +++ b/charts/datree-admission-webhook/templates/webhook-with-cert-secrets.yaml @@ -1,23 +1,4 @@ -{{- $ca := genCA "/CN=Admission Controller Webhook Demo CA" 1827 -}} {{- $svcHost := printf "datree-webhook-server.%s.svc" ( include "datree.namespace" . ) -}} -{{- $altNames := list ( $svcHost ) -}} -{{- $cert := genSignedCert (printf "/CN=%s" $svcHost) nil $altNames 1827 $ca -}} -apiVersion: v1 -kind: Secret -metadata: - name: webhook-server-tls - labels: {{ include "datree.labels" . | nindent 4 }} - namespace: {{ template "datree.namespace" . }} - annotations: - self-signed-cert: "true" - {{- with .Values.customAnnotations }} - {{ toYaml . }} - {{- end }} -type: kubernetes.io/tls -data: - tls.key: {{ $cert.Key | b64enc }} - tls.crt: {{ $cert.Cert | b64enc }} ---- apiVersion: admissionregistration.k8s.io/v1 kind: ValidatingWebhookConfiguration metadata: @@ -45,11 +26,13 @@ webhooks: name: datree-webhook-server namespace: {{ template "datree.namespace" . }} path: "/validate" - caBundle: {{ $ca.Cert | b64enc }} + caBundle: {{ "PLACEHOLDER" | b64enc }} namespaceSelector: matchExpressions: - key: admission.datree/validate operator: DoesNotExist + - key: admission.datree/validate + operator: Exists # the label can't exist AND not exist simultaneously, therefore the webhook will never be invoked until we delete this expression rules: - operations: ["CREATE", "UPDATE"] apiGroups: ["*"] diff --git a/go.mod b/go.mod index b1c2bc40..43ddcd24 100644 --- a/go.mod +++ b/go.mod @@ -7,8 +7,10 @@ require ( github.com/ghodss/yaml v1.0.0 github.com/google/go-cmp v0.5.9 github.com/lithammer/shortuuid v3.0.0+incompatible + github.com/openshift/api v0.0.0-20230705144233-e28cd4dd28a8 github.com/openshift/client-go v0.0.0-20230705133330-7f808ad59404 github.com/patrickmn/go-cache v2.1.0+incompatible + github.com/sirupsen/logrus v1.9.0 github.com/stretchr/testify v1.8.1 go.uber.org/zap v1.10.0 k8s.io/api v0.27.2 @@ -33,7 +35,6 @@ require ( github.com/jinzhu/copier v0.3.5 // indirect github.com/mikefarah/yq/v4 v4.27.3 // indirect github.com/open-policy-agent/opa v0.49.2 // indirect - github.com/openshift/api v0.0.0-20230705144233-e28cd4dd28a8 // indirect github.com/owenrumney/go-sarif/v2 v2.1.2 // indirect github.com/rcrowley/go-metrics v0.0.0-20200313005456-10cdbea86bc0 // indirect github.com/stretchr/objx v0.5.0 // indirect diff --git a/go.sum b/go.sum index fc66718a..f2df72ea 100644 --- a/go.sum +++ b/go.sum @@ -226,6 +226,7 @@ github.com/santhosh-tekuri/jsonschema/v5 v5.0.0/go.mod h1:FKdcjfQW6rpZSnxxUvEA5H github.com/shirou/gopsutil/v3 v3.22.5 h1:atX36I/IXgFiB81687vSiBI5zrMsxcIBkP9cQMJQoJA= github.com/shirou/gopsutil/v3 v3.22.5/go.mod h1:so9G9VzeHt/hsd0YwqprnjHnfARAUktauykSbr+y2gA= github.com/sirupsen/logrus v1.9.0 h1:trlNQbNUG3OdDrDil03MCb1H2o9nJ1x4/5LYw7byDE0= +github.com/sirupsen/logrus v1.9.0/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= github.com/stoewer/go-strcase v1.2.0/go.mod h1:IBiWB2sKIp3wVVQ3Y035++gc+knqhUQag1KpM8ahLw8= @@ -327,6 +328,7 @@ golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220128215802-99c3d69c2c27/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220503163025-988cb79eb6c6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.8.0 h1:EBmGv8NaZBZTWvrbjNoL6HVt+IVy3QDQpJs7VRIw3tU= golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= diff --git a/internal/fixtures/values.dev.yaml b/internal/fixtures/values.dev.yaml index 6633043f..da14a7ae 100644 --- a/internal/fixtures/values.dev.yaml +++ b/internal/fixtures/values.dev.yaml @@ -6,7 +6,7 @@ # The name of the namespace all resources will be created in, if not specified in the release. namespace: "" # The number of Datree webhook-server replicas to deploy for the webhook. -replicaCount: 1 +replicaCount: 2 # Additional labels to add to all resources. customLabels: { } # Additional annotations to add to all resources. diff --git a/internal/startup/startup.go b/internal/startup/startup.go index 7fcf14bc..3249345f 100644 --- a/internal/startup/startup.go +++ b/internal/startup/startup.go @@ -3,6 +3,8 @@ package startup import ( "errors" "fmt" + cert_manager "github.com/datreeio/admission-webhook-datree/pkg/cert-manager" + "github.com/datreeio/admission-webhook-datree/pkg/k8sClient2" "github.com/datreeio/admission-webhook-datree/pkg/openshiftService" "net/http" @@ -87,9 +89,18 @@ func Start() { if err != nil { fmt.Printf("Failed init skip list: %s \n", err.Error()) } - certPath, keyPath, err := server.ValidateCertificate() + + err = cert_manager.GenerateCertificatesIfTheyAreMissing() + if err != nil { + fmt.Printf("Failed to generate certificates: %s \n", err.Error()) + } + k8sClient2Instance, err := k8sClient2.NewK8sClient() + if err != nil { + fmt.Printf("Failed to create k8s client: %s \n", err.Error()) + } + err = k8sClient2Instance.ActivateValidatingWebhookConfiguration() if err != nil { - panic(err) + fmt.Printf("Failed to activate validating webhook configuration: %s \n", err.Error()) } validationController := controllers.NewValidationController(basicCliClient, state, errorReporter, k8sMetadataUtilInstance, &internalLogger, openshiftServiceInstance) @@ -105,7 +116,7 @@ func Start() { internalLogger.LogInfo(fmt.Sprintf("server starting in webhook-version: %s", config.WebhookVersion)) // start server - if err := http.ListenAndServeTLS(":"+port, certPath, keyPath, nil); err != nil { + if err := http.ListenAndServeTLS(":"+port, cert_manager.TlsCertPath, cert_manager.TlsKeyPath, nil); err != nil { err = http.ListenAndServe(":"+port, nil) if err != nil { fmt.Println("Failed to start http server", err.Error()) diff --git a/pkg/cert-manager/cert-manager.go b/pkg/cert-manager/cert-manager.go new file mode 100644 index 00000000..c3f4d6ad --- /dev/null +++ b/pkg/cert-manager/cert-manager.go @@ -0,0 +1,155 @@ +package cert_manager + +import ( + "bytes" + cryptorand "crypto/rand" + "crypto/rsa" + "crypto/x509" + "crypto/x509/pkix" + "encoding/pem" + "errors" + "fmt" + log "github.com/sirupsen/logrus" + "math/big" + "os" + "time" +) + +const certsFolder = "/etc/webhook-certs" +const TlsCertPath = certsFolder + "/tls.crt" +const TlsKeyPath = certsFolder + "/tls.key" +const CaCertPath = certsFolder + "/ca.crt" + +type AllCertificates struct { + Cert []byte + Key []byte + CaCert []byte + CaKey []byte +} + +func GenerateCertificatesIfTheyAreMissing() error { + if !doCertificatesExist() { + generateCertificates() + } + return nil +} + +func doCertificatesExist() bool { + doesFileExist := func(filePath string) bool { + if _, err := os.Stat(filePath); errors.Is(err, os.ErrNotExist) { + return false + } + return true + } + + return doesFileExist(TlsCertPath) && doesFileExist(TlsKeyPath) && doesFileExist(CaCertPath) +} + +func generateCertificates() { + var caPEM, serverCertPEM, serverPrivKeyPEM *bytes.Buffer + // CA config + ca := &x509.Certificate{ + SerialNumber: big.NewInt(2020), + Subject: pkix.Name{ + Organization: []string{"/CN=Datree Admission Controller Webhook CA"}, + }, + NotBefore: time.Now(), + NotAfter: time.Now().AddDate(5, 0, 0), + IsCA: true, + ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth, x509.ExtKeyUsageServerAuth}, + KeyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageCertSign, + BasicConstraintsValid: true, + } + + // CA private key + caPrivKey, err := rsa.GenerateKey(cryptorand.Reader, 4096) + if err != nil { + fmt.Println(err) + } + + // Self signed CA certificate + caBytes, err := x509.CreateCertificate(cryptorand.Reader, ca, ca, &caPrivKey.PublicKey, caPrivKey) + if err != nil { + fmt.Println(err) + } + + // PEM encode CA cert + caPEM = new(bytes.Buffer) + _ = pem.Encode(caPEM, &pem.Block{ + Type: "CERTIFICATE", + Bytes: caBytes, + }) + + webhookDNS := "datree-webhook-server.datree.svc" // TODO use namespace from config + + // server cert config + cert := &x509.Certificate{ + DNSNames: []string{webhookDNS}, + SerialNumber: big.NewInt(1658), + Subject: pkix.Name{ + CommonName: fmt.Sprintf("/CN=%v", webhookDNS), + Organization: []string{"/CN=Datree Admission Controller Webhook CA"}, + }, + NotBefore: time.Now(), + NotAfter: time.Now().AddDate(5, 0, 0), + SubjectKeyId: []byte{1, 2, 3, 4, 6}, + ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth, x509.ExtKeyUsageServerAuth}, + KeyUsage: x509.KeyUsageDigitalSignature, + } + + // server private key + serverPrivKey, err := rsa.GenerateKey(cryptorand.Reader, 4096) + if err != nil { + fmt.Println(err) + } + + // sign the server cert + serverCertBytes, err := x509.CreateCertificate(cryptorand.Reader, cert, ca, &serverPrivKey.PublicKey, caPrivKey) + if err != nil { + fmt.Println(err) + } + + // PEM encode the server cert and key + serverCertPEM = new(bytes.Buffer) + _ = pem.Encode(serverCertPEM, &pem.Block{ + Type: "CERTIFICATE", + Bytes: serverCertBytes, + }) + + serverPrivKeyPEM = new(bytes.Buffer) + _ = pem.Encode(serverPrivKeyPEM, &pem.Block{ + Type: "RSA PRIVATE KEY", + Bytes: x509.MarshalPKCS1PrivateKey(serverPrivKey), + }) + + err = writeFile(CaCertPath, caPEM) + if err != nil { + log.Panic(err) + } + + err = writeFile(TlsCertPath, serverCertPEM) + if err != nil { + log.Panic(err) + } + + err = writeFile(TlsKeyPath, serverPrivKeyPEM) + if err != nil { + log.Panic(err) + } + +} + +// writeFile writes data in the file at the given path +func writeFile(filepath string, sCert *bytes.Buffer) error { + f, err := os.Create(filepath) + if err != nil { + return err + } + defer f.Close() + + _, err = f.Write(sCert.Bytes()) + if err != nil { + return err + } + return nil +} diff --git a/pkg/k8sClient2/k8sClient2.go b/pkg/k8sClient2/k8sClient2.go new file mode 100644 index 00000000..f0616750 --- /dev/null +++ b/pkg/k8sClient2/k8sClient2.go @@ -0,0 +1,59 @@ +package k8sClient2 + +import ( + "context" + cert_manager "github.com/datreeio/admission-webhook-datree/pkg/cert-manager" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/client-go/kubernetes" + "k8s.io/client-go/rest" + "os" +) + +type K8sClient struct { + clientset *kubernetes.Clientset +} + +func NewK8sClient() (*K8sClient, error) { + // creates the in-cluster config + config, err := rest.InClusterConfig() + if err != nil { + return nil, err + } + // creates the clientset + clientsetInstance, err := kubernetes.NewForConfig(config) + if err != nil { + return nil, err + } + + return &K8sClient{ + clientset: clientsetInstance, + }, nil +} + +func (kc *K8sClient) ActivateValidatingWebhookConfiguration() error { + certificateContent, readFileError := os.ReadFile(cert_manager.CaCertPath) + if readFileError != nil { + return readFileError + } + + existingValidatingWebhookConfiguration, err := kc.clientset.AdmissionregistrationV1().ValidatingWebhookConfigurations().Get(context.TODO(), "datree-webhook", metav1.GetOptions{}) + if err != nil { + return err + } + + // update the CABundle from PLACEHOLDER to the actual certificate from persistent volume + existingValidatingWebhookConfiguration.Webhooks[0].ClientConfig.CABundle = certificateContent + + // remove the match expression at index 1, which is responsible for disabling the webhook + matchExpressions := existingValidatingWebhookConfiguration.Webhooks[0].NamespaceSelector.MatchExpressions + if len(matchExpressions) > 1 { + existingValidatingWebhookConfiguration.Webhooks[0].NamespaceSelector.MatchExpressions = append(matchExpressions[:1], matchExpressions[2:]...) + } + + _, err = kc.clientset.AdmissionregistrationV1().ValidatingWebhookConfigurations().Update(context.TODO(), existingValidatingWebhookConfiguration, metav1.UpdateOptions{}) + if err != nil { + return err + } + + return nil +} diff --git a/pkg/server/server.go b/pkg/server/server.go index 8baa1ef6..de9fa561 100644 --- a/pkg/server/server.go +++ b/pkg/server/server.go @@ -2,12 +2,10 @@ package server import ( "errors" - "fmt" servicestate "github.com/datreeio/admission-webhook-datree/pkg/serviceState" "os" "path/filepath" - "github.com/datreeio/admission-webhook-datree/pkg/deploymentConfig" "gopkg.in/yaml.v2" ) @@ -71,24 +69,3 @@ func readConfigScanningFilters() (skipList []string, err error) { } return skipLists, nil } - -func ValidateCertificate() (certPath string, keyPath string, err error) { - tlsDir := `/run/secrets/tls` - tlsCertFile := `tls.crt` - tlsKeyFile := `tls.key` - - certPath = filepath.Join(tlsDir, tlsCertFile) - keyPath = filepath.Join(tlsDir, tlsKeyFile) - - if deploymentConfig.ShouldValidateCertificate { - if _, err := os.Stat(certPath); errors.Is(err, os.ErrNotExist) { - return "", "", fmt.Errorf("cert file doesn't exist") - } - - if _, err := os.Stat(keyPath); errors.Is(err, os.ErrNotExist) { - return "", "", fmt.Errorf("key file doesn't exist") - } - } - - return certPath, keyPath, nil -}