From e6ca742016f773a62030a17b733dcc18ab399743 Mon Sep 17 00:00:00 2001 From: Vasyl Rudiuk Date: Thu, 30 Nov 2023 21:24:52 +0000 Subject: [PATCH 1/4] Add Custom Git Client for WAF-Secured Repos Introduce a custom Git client to handle repositories behind Web Application Firewalls (WAF). This client facilitates TLS certificate-based authentication, enabling secure Git operations in corporate environments. Signed-off-by: Vasyl Rudiuk --- internal/controller/auth_certificate.go | 34 ++++++ .../controller/gitrepository_controller.go | 110 +++++++++++++++++- .../gitrepository_controller_test.go | 27 +++++ 3 files changed, 168 insertions(+), 3 deletions(-) create mode 100644 internal/controller/auth_certificate.go diff --git a/internal/controller/auth_certificate.go b/internal/controller/auth_certificate.go new file mode 100644 index 000000000..fee4a30e0 --- /dev/null +++ b/internal/controller/auth_certificate.go @@ -0,0 +1,34 @@ +package controller + +import ( + "context" + "crypto/tls" + "net/http" + + "github.com/go-git/go-git/v5/plumbing/transport" + httptransport "github.com/go-git/go-git/v5/plumbing/transport/http" + ctrl "sigs.k8s.io/controller-runtime" +) + +// HttpTransportWithCustomCerts returns an HTTP transport with custom certificates. +// If proxyStr is provided, it will be used as the proxy URL. +// If not, it tries to fetch the proxy from an environment variable. +func HttpTransportwithCustomCerts(tlsConfig *tls.Config, proxyStr *transport.ProxyOptions, ctx context.Context) (transport.Transport, error) { + + log := ctrl.LoggerFrom(ctx) + // var message string + + if tlsConfig == nil || len(tlsConfig.Certificates) == 0 { + log.Info("tlsConfig cannot be nil") + return nil, nil + } + + return httptransport.NewClient(&http.Client{ + Transport: &http.Transport{ + TLSClientConfig: tlsConfig, + }, + }), nil + +} + +// \ No newline at end of file diff --git a/internal/controller/gitrepository_controller.go b/internal/controller/gitrepository_controller.go index 2440904a2..7e33e00b0 100644 --- a/internal/controller/gitrepository_controller.go +++ b/internal/controller/gitrepository_controller.go @@ -62,6 +62,8 @@ import ( sreconcile "github.com/fluxcd/source-controller/internal/reconcile" "github.com/fluxcd/source-controller/internal/reconcile/summarize" "github.com/fluxcd/source-controller/internal/util" + "github.com/fluxcd/source-controller/internal/tls" + gitclient "github.com/go-git/go-git/v5/plumbing/transport/client" ) // gitRepositoryReadyCondition contains the information required to summarize a @@ -148,6 +150,100 @@ func (r *GitRepositoryReconciler) SetupWithManager(mgr ctrl.Manager) error { return r.SetupWithManagerAndOptions(mgr, GitRepositoryReconcilerOptions{}) } +// Interface co configure gitclient with custom TLS options +// used for application firewall authentication. +type GitClientConfigurer interface { + ConfigureGitClient(ctx context.Context, obj *sourcev1.GitRepository) + IsValid() bool + backupHttpsTransport() +} + +type GitClientHttpConfigurer struct { + SSLCertificateData map[string][]byte + ProxyOpts *transport.ProxyOptions + Valid bool + DefaultTransport transport.Transport + AppFirewallTransport transport.Transport +} + +func (c *GitClientHttpConfigurer) IsValid() bool { + return c.Valid +} + +func (c *GitClientHttpConfigurer) SetValid() { + c.Valid = true +} + +func (c *GitClientHttpConfigurer) SetInvalid() { + c.Valid = false +} + +func (r *GitRepositoryReconciler) isCertificateDataValid(sslCertificateData map[string][]byte) bool { + certBytes, keyBytes := sslCertificateData["certFile"], sslCertificateData["keyFile"] + // Validate that both the certificate and key data are present + return len(certBytes) > 0 && len(keyBytes) > 0 +} + +func (h *GitClientHttpConfigurer) backupHttpsTransport() { + h.DefaultTransport = gitclient.Protocols["https"] +} + +func (h *GitClientHttpConfigurer) ConfigureGitClient(ctx context.Context, obj *sourcev1.GitRepository) { + + if obj.Spec.SecretRef != nil { + // var secretName = obj.Spec.SecretRef.Name + // if secretName == "waf-authentication" { + sslCertificate := &corev1.Secret{ + Data: h.SSLCertificateData, + } + tlsConfig, _, err := tls.TLSClientConfigFromSecret(*sslCertificate, "") + if err != nil { + fmt.Println("Error generating TLS config:", err) + return + } + h.backupHttpsTransport() + + transportHttp, err := HttpTransportwithCustomCerts(tlsConfig, h.ProxyOpts, ctx) + if err != nil { + fmt.Println("Error setting up transport:", err) + return + } + + gitclient.InstallProtocol("https", transportHttp) + + } + // } +} + + + +// configureHttpTransport sets up the HTTP transport configuration for the Git client. +func (r *GitRepositoryReconciler) configureHttpTransport(ctx context.Context, obj *sourcev1.GitRepository) (*GitClientHttpConfigurer, error) { + httpTransportConfig := &GitClientHttpConfigurer{} // Initialize with defaults configuration + + // Check if SecretRef is specified for the repository + if obj.Spec.SecretRef != nil { + // Fetch the SSL certificate data from the specified secret + sslCertificateData, err := r.getSecretData(ctx, obj.Spec.SecretRef.Name, obj.Namespace) + if err != nil { + return nil, fmt.Errorf("failed to get secret '%s/%s': %w", obj.Namespace, obj.Spec.SecretRef.Name, err) + } + + // Set up the HTTP transport configuration with the fetched certificate data + httpTransportConfig.SSLCertificateData = sslCertificateData + if r.isCertificateDataValid(sslCertificateData) { + httpTransportConfig.SetValid() + } else { + httpTransportConfig.SetInvalid() + } + } else { + // If no SecretRef is provided, mark the transport config as invalid or set defaults + httpTransportConfig.SetInvalid() + } + + return httpTransportConfig, nil +} + func (r *GitRepositoryReconciler) SetupWithManagerAndOptions(mgr ctrl.Manager, opts GitRepositoryReconcilerOptions) error { r.patchOptions = getPatchOptions(gitRepositoryReadyCondition.Owned, r.ControllerName) @@ -535,7 +631,12 @@ func (r *GitRepositoryReconciler) reconcileSource(ctx context.Context, sp *patch // Persist the ArtifactSet. *includes = *artifacts - c, err := r.gitCheckout(ctx, obj, authOpts, proxyOpts, dir, true) + httpTransportConfig, err := r.configureHttpTransport(ctx, obj) + if err != nil { + return sreconcile.ResultEmpty, err + } + + c, err := r.gitCheckout(ctx, obj, authOpts, proxyOpts, dir, true, httpTransportConfig) if err != nil { return sreconcile.ResultEmpty, err } @@ -579,7 +680,7 @@ func (r *GitRepositoryReconciler) reconcileSource(ctx context.Context, sp *patch // If we can't skip the reconciliation, checkout again without any // optimization. - c, err := r.gitCheckout(ctx, obj, authOpts, proxyOpts, dir, false) + c, err := r.gitCheckout(ctx, obj, authOpts, proxyOpts, dir, false, httpTransportConfig) if err != nil { return sreconcile.ResultEmpty, err } @@ -832,7 +933,7 @@ func (r *GitRepositoryReconciler) reconcileInclude(ctx context.Context, sp *patc // gitCheckout builds checkout options with the given configurations and // performs a git checkout. func (r *GitRepositoryReconciler) gitCheckout(ctx context.Context, obj *sourcev1.GitRepository, - authOpts *git.AuthOptions, proxyOpts *transport.ProxyOptions, dir string, optimized bool) (*git.Commit, error) { + authOpts *git.AuthOptions, proxyOpts *transport.ProxyOptions, dir string, optimized bool, clientConf GitClientConfigurer) (*git.Commit, error) { // Configure checkout strategy. cloneOpts := repository.CloneConfig{ RecurseSubmodules: obj.Spec.RecurseSubmodules, @@ -866,6 +967,9 @@ func (r *GitRepositoryReconciler) gitCheckout(ctx context.Context, obj *sourcev1 clientOpts = append(clientOpts, gogit.WithProxy(*proxyOpts)) } + if clientConf.IsValid() { + clientConf.ConfigureGitClient(ctx, obj) + } gitReader, err := gogit.NewClient(dir, authOpts, clientOpts...) if err != nil { e := serror.NewGeneric( diff --git a/internal/controller/gitrepository_controller_test.go b/internal/controller/gitrepository_controller_test.go index 800c65577..676aa38e4 100644 --- a/internal/controller/gitrepository_controller_test.go +++ b/internal/controller/gitrepository_controller_test.go @@ -571,6 +571,33 @@ func TestGitRepositoryReconciler_reconcileSource_authStrategy(t *testing.T) { *conditions.UnknownCondition(meta.ReadyCondition, meta.ProgressingReason, "building artifact: new upstream revision 'master@sha1:'"), }, }, + { + name: "HTTPS with TLS certs authentication with WAF Reconciling=True", + protocol: "https", + server: options{ + publicKey: tlsPublicKey, + privateKey: tlsPrivateKey, + ca: tlsCA, + }, + secret: &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: "waf-authentication", + }, + Data: map[string][]byte{ + "certFile": clientPublicKey, + "keyFile": clientPrivateKey, + "caFile": tlsCA, + }, + }, + beforeFunc: func(obj *sourcev1.GitRepository) { + obj.Spec.SecretRef = &meta.LocalObjectReference{Name: "waf-authentication"} + }, + want: sreconcile.ResultSuccess, + assertConditions: []metav1.Condition{ + *conditions.TrueCondition(meta.ReconcilingCondition, meta.ProgressingReason, "building artifact: new upstream revision 'master@sha1:'"), + *conditions.UnknownCondition(meta.ReadyCondition, meta.ProgressingReason, "building artifact: new upstream revision 'master@sha1:'"), + }, + }, } for _, tt := range tests { From e31680c91712264e8612f9a518c92dda8713fabc Mon Sep 17 00:00:00 2001 From: Vasyl Rudiuk Date: Thu, 30 Nov 2023 21:42:25 +0000 Subject: [PATCH 2/4] updated readme Signed-off-by: Vasyl Rudiuk --- DEVELOPMENT.md | 31 ++++++++++++++++--- internal/controller/auth_certificate.go | 4 +-- .../controller/gitrepository_controller.go | 4 +-- 3 files changed, 29 insertions(+), 10 deletions(-) diff --git a/DEVELOPMENT.md b/DEVELOPMENT.md index 072e7232b..c2738f1bd 100644 --- a/DEVELOPMENT.md +++ b/DEVELOPMENT.md @@ -121,16 +121,39 @@ make deploy Create a `.vscode/launch.json` file: ```json { - "version": "0.2.0", +"version": "0.2.0", "configurations": [ { - "name": "Launch Package", + "name": "Debug Test Function", + "type": "go", + "request": "launch", + "mode": "test", + "program": "${workspaceFolder}/internal/controller/", + "env": { + "HTTPS_PROXY": "", + "HTTP_PROXY": "", + "KUBEBUILDER_ASSETS": "${workspaceFolder}/build/testbin/k8s/1.28.0-linux-amd64/", + "GIT_CONFIG_GLOBAL":"/dev/null", + "GIT_CONFIG_NOSYSTEM":"true", + + }, + "args": [ + "-test.run", "^.*", + "-test.v" + ] + }, + { + "name": "Debug", "type": "go", "request": "launch", "mode": "auto", - "program": "${workspaceFolder}/main.go" + "program": "${workspaceFolder}/main.go", + "args": [ + "--storage-adv-addr=:0", + "--storage-path=/tmp/" + ] } - ] + ], } ``` diff --git a/internal/controller/auth_certificate.go b/internal/controller/auth_certificate.go index fee4a30e0..dd813903c 100644 --- a/internal/controller/auth_certificate.go +++ b/internal/controller/auth_certificate.go @@ -29,6 +29,4 @@ func HttpTransportwithCustomCerts(tlsConfig *tls.Config, proxyStr *transport.Pro }, }), nil -} - -// \ No newline at end of file +} \ No newline at end of file diff --git a/internal/controller/gitrepository_controller.go b/internal/controller/gitrepository_controller.go index 7e33e00b0..cbccf11c0 100644 --- a/internal/controller/gitrepository_controller.go +++ b/internal/controller/gitrepository_controller.go @@ -61,8 +61,8 @@ import ( "github.com/fluxcd/source-controller/internal/features" sreconcile "github.com/fluxcd/source-controller/internal/reconcile" "github.com/fluxcd/source-controller/internal/reconcile/summarize" - "github.com/fluxcd/source-controller/internal/util" "github.com/fluxcd/source-controller/internal/tls" + "github.com/fluxcd/source-controller/internal/util" gitclient "github.com/go-git/go-git/v5/plumbing/transport/client" ) @@ -215,8 +215,6 @@ func (h *GitClientHttpConfigurer) ConfigureGitClient(ctx context.Context, obj *s // } } - - // configureHttpTransport sets up the HTTP transport configuration for the Git client. func (r *GitRepositoryReconciler) configureHttpTransport(ctx context.Context, obj *sourcev1.GitRepository) (*GitClientHttpConfigurer, error) { httpTransportConfig := &GitClientHttpConfigurer{} // Initialize with defaults configuration From fcb866261b1744f823efa324815f531fcae4bc62 Mon Sep 17 00:00:00 2001 From: Vasyl Rudiuk Date: Thu, 30 Nov 2023 21:44:18 +0000 Subject: [PATCH 3/4] added new line Signed-off-by: Vasyl Rudiuk --- internal/controller/auth_certificate.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/controller/auth_certificate.go b/internal/controller/auth_certificate.go index dd813903c..56b2faa29 100644 --- a/internal/controller/auth_certificate.go +++ b/internal/controller/auth_certificate.go @@ -29,4 +29,4 @@ func HttpTransportwithCustomCerts(tlsConfig *tls.Config, proxyStr *transport.Pro }, }), nil -} \ No newline at end of file +} From 8eed3c8942a893b1b2644f1830f25a19d89895b8 Mon Sep 17 00:00:00 2001 From: Vasyl Rudiuk Date: Mon, 4 Dec 2023 16:21:57 +0000 Subject: [PATCH 4/4] added back proxyOpts In some cases we might need to handle proxy setting for the custom gitClient. Signed-off-by: Vasyl Rudiuk --- internal/controller/auth_certificate.go | 13 +++++++++++++ internal/controller/gitrepository_controller.go | 6 +----- 2 files changed, 14 insertions(+), 5 deletions(-) diff --git a/internal/controller/auth_certificate.go b/internal/controller/auth_certificate.go index 56b2faa29..795466fc7 100644 --- a/internal/controller/auth_certificate.go +++ b/internal/controller/auth_certificate.go @@ -4,6 +4,7 @@ import ( "context" "crypto/tls" "net/http" + "net/url" "github.com/go-git/go-git/v5/plumbing/transport" httptransport "github.com/go-git/go-git/v5/plumbing/transport/http" @@ -18,6 +19,17 @@ func HttpTransportwithCustomCerts(tlsConfig *tls.Config, proxyStr *transport.Pro log := ctrl.LoggerFrom(ctx) // var message string + var ( + proxyUrl *url.URL + err error + ) + if proxyStr != nil { + proxyUrl, err = url.Parse(proxyStr.URL) + if err != nil { + log.Info("failed to parse proxy url: %w", err) + } + } + if tlsConfig == nil || len(tlsConfig.Certificates) == 0 { log.Info("tlsConfig cannot be nil") return nil, nil @@ -25,6 +37,7 @@ func HttpTransportwithCustomCerts(tlsConfig *tls.Config, proxyStr *transport.Pro return httptransport.NewClient(&http.Client{ Transport: &http.Transport{ + Proxy: http.ProxyURL(proxyUrl), TLSClientConfig: tlsConfig, }, }), nil diff --git a/internal/controller/gitrepository_controller.go b/internal/controller/gitrepository_controller.go index cbccf11c0..bc11529fd 100644 --- a/internal/controller/gitrepository_controller.go +++ b/internal/controller/gitrepository_controller.go @@ -155,7 +155,6 @@ func (r *GitRepositoryReconciler) SetupWithManager(mgr ctrl.Manager) error { type GitClientConfigurer interface { ConfigureGitClient(ctx context.Context, obj *sourcev1.GitRepository) IsValid() bool - backupHttpsTransport() } type GitClientHttpConfigurer struct { @@ -184,9 +183,6 @@ func (r *GitRepositoryReconciler) isCertificateDataValid(sslCertificateData map[ return len(certBytes) > 0 && len(keyBytes) > 0 } -func (h *GitClientHttpConfigurer) backupHttpsTransport() { - h.DefaultTransport = gitclient.Protocols["https"] -} func (h *GitClientHttpConfigurer) ConfigureGitClient(ctx context.Context, obj *sourcev1.GitRepository) { @@ -201,7 +197,7 @@ func (h *GitClientHttpConfigurer) ConfigureGitClient(ctx context.Context, obj *s fmt.Println("Error generating TLS config:", err) return } - h.backupHttpsTransport() + transportHttp, err := HttpTransportwithCustomCerts(tlsConfig, h.ProxyOpts, ctx) if err != nil {