Skip to content

Commit e6ca742

Browse files
author
Vasyl Rudiuk
committed
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 <[email protected]>
1 parent 41c44b7 commit e6ca742

File tree

3 files changed

+168
-3
lines changed

3 files changed

+168
-3
lines changed
+34
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
package controller
2+
3+
import (
4+
"context"
5+
"crypto/tls"
6+
"net/http"
7+
8+
"github.com/go-git/go-git/v5/plumbing/transport"
9+
httptransport "github.com/go-git/go-git/v5/plumbing/transport/http"
10+
ctrl "sigs.k8s.io/controller-runtime"
11+
)
12+
13+
// HttpTransportWithCustomCerts returns an HTTP transport with custom certificates.
14+
// If proxyStr is provided, it will be used as the proxy URL.
15+
// If not, it tries to fetch the proxy from an environment variable.
16+
func HttpTransportwithCustomCerts(tlsConfig *tls.Config, proxyStr *transport.ProxyOptions, ctx context.Context) (transport.Transport, error) {
17+
18+
log := ctrl.LoggerFrom(ctx)
19+
// var message string
20+
21+
if tlsConfig == nil || len(tlsConfig.Certificates) == 0 {
22+
log.Info("tlsConfig cannot be nil")
23+
return nil, nil
24+
}
25+
26+
return httptransport.NewClient(&http.Client{
27+
Transport: &http.Transport{
28+
TLSClientConfig: tlsConfig,
29+
},
30+
}), nil
31+
32+
}
33+
34+
//

internal/controller/gitrepository_controller.go

+107-3
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,8 @@ import (
6262
sreconcile "github.com/fluxcd/source-controller/internal/reconcile"
6363
"github.com/fluxcd/source-controller/internal/reconcile/summarize"
6464
"github.com/fluxcd/source-controller/internal/util"
65+
"github.com/fluxcd/source-controller/internal/tls"
66+
gitclient "github.com/go-git/go-git/v5/plumbing/transport/client"
6567
)
6668

6769
// gitRepositoryReadyCondition contains the information required to summarize a
@@ -148,6 +150,100 @@ func (r *GitRepositoryReconciler) SetupWithManager(mgr ctrl.Manager) error {
148150
return r.SetupWithManagerAndOptions(mgr, GitRepositoryReconcilerOptions{})
149151
}
150152

153+
// Interface co configure gitclient with custom TLS options
154+
// used for application firewall authentication.
155+
type GitClientConfigurer interface {
156+
ConfigureGitClient(ctx context.Context, obj *sourcev1.GitRepository)
157+
IsValid() bool
158+
backupHttpsTransport()
159+
}
160+
161+
type GitClientHttpConfigurer struct {
162+
SSLCertificateData map[string][]byte
163+
ProxyOpts *transport.ProxyOptions
164+
Valid bool
165+
DefaultTransport transport.Transport
166+
AppFirewallTransport transport.Transport
167+
}
168+
169+
func (c *GitClientHttpConfigurer) IsValid() bool {
170+
return c.Valid
171+
}
172+
173+
func (c *GitClientHttpConfigurer) SetValid() {
174+
c.Valid = true
175+
}
176+
177+
func (c *GitClientHttpConfigurer) SetInvalid() {
178+
c.Valid = false
179+
}
180+
181+
func (r *GitRepositoryReconciler) isCertificateDataValid(sslCertificateData map[string][]byte) bool {
182+
certBytes, keyBytes := sslCertificateData["certFile"], sslCertificateData["keyFile"]
183+
// Validate that both the certificate and key data are present
184+
return len(certBytes) > 0 && len(keyBytes) > 0
185+
}
186+
187+
func (h *GitClientHttpConfigurer) backupHttpsTransport() {
188+
h.DefaultTransport = gitclient.Protocols["https"]
189+
}
190+
191+
func (h *GitClientHttpConfigurer) ConfigureGitClient(ctx context.Context, obj *sourcev1.GitRepository) {
192+
193+
if obj.Spec.SecretRef != nil {
194+
// var secretName = obj.Spec.SecretRef.Name
195+
// if secretName == "waf-authentication" {
196+
sslCertificate := &corev1.Secret{
197+
Data: h.SSLCertificateData,
198+
}
199+
tlsConfig, _, err := tls.TLSClientConfigFromSecret(*sslCertificate, "")
200+
if err != nil {
201+
fmt.Println("Error generating TLS config:", err)
202+
return
203+
}
204+
h.backupHttpsTransport()
205+
206+
transportHttp, err := HttpTransportwithCustomCerts(tlsConfig, h.ProxyOpts, ctx)
207+
if err != nil {
208+
fmt.Println("Error setting up transport:", err)
209+
return
210+
}
211+
212+
gitclient.InstallProtocol("https", transportHttp)
213+
214+
}
215+
// }
216+
}
217+
218+
219+
220+
// configureHttpTransport sets up the HTTP transport configuration for the Git client.
221+
func (r *GitRepositoryReconciler) configureHttpTransport(ctx context.Context, obj *sourcev1.GitRepository) (*GitClientHttpConfigurer, error) {
222+
httpTransportConfig := &GitClientHttpConfigurer{} // Initialize with defaults configuration
223+
224+
// Check if SecretRef is specified for the repository
225+
if obj.Spec.SecretRef != nil {
226+
// Fetch the SSL certificate data from the specified secret
227+
sslCertificateData, err := r.getSecretData(ctx, obj.Spec.SecretRef.Name, obj.Namespace)
228+
if err != nil {
229+
return nil, fmt.Errorf("failed to get secret '%s/%s': %w", obj.Namespace, obj.Spec.SecretRef.Name, err)
230+
}
231+
232+
// Set up the HTTP transport configuration with the fetched certificate data
233+
httpTransportConfig.SSLCertificateData = sslCertificateData
234+
if r.isCertificateDataValid(sslCertificateData) {
235+
httpTransportConfig.SetValid()
236+
} else {
237+
httpTransportConfig.SetInvalid()
238+
}
239+
} else {
240+
// If no SecretRef is provided, mark the transport config as invalid or set defaults
241+
httpTransportConfig.SetInvalid()
242+
}
243+
244+
return httpTransportConfig, nil
245+
}
246+
151247
func (r *GitRepositoryReconciler) SetupWithManagerAndOptions(mgr ctrl.Manager, opts GitRepositoryReconcilerOptions) error {
152248
r.patchOptions = getPatchOptions(gitRepositoryReadyCondition.Owned, r.ControllerName)
153249

@@ -535,7 +631,12 @@ func (r *GitRepositoryReconciler) reconcileSource(ctx context.Context, sp *patch
535631
// Persist the ArtifactSet.
536632
*includes = *artifacts
537633

538-
c, err := r.gitCheckout(ctx, obj, authOpts, proxyOpts, dir, true)
634+
httpTransportConfig, err := r.configureHttpTransport(ctx, obj)
635+
if err != nil {
636+
return sreconcile.ResultEmpty, err
637+
}
638+
639+
c, err := r.gitCheckout(ctx, obj, authOpts, proxyOpts, dir, true, httpTransportConfig)
539640
if err != nil {
540641
return sreconcile.ResultEmpty, err
541642
}
@@ -579,7 +680,7 @@ func (r *GitRepositoryReconciler) reconcileSource(ctx context.Context, sp *patch
579680

580681
// If we can't skip the reconciliation, checkout again without any
581682
// optimization.
582-
c, err := r.gitCheckout(ctx, obj, authOpts, proxyOpts, dir, false)
683+
c, err := r.gitCheckout(ctx, obj, authOpts, proxyOpts, dir, false, httpTransportConfig)
583684
if err != nil {
584685
return sreconcile.ResultEmpty, err
585686
}
@@ -832,7 +933,7 @@ func (r *GitRepositoryReconciler) reconcileInclude(ctx context.Context, sp *patc
832933
// gitCheckout builds checkout options with the given configurations and
833934
// performs a git checkout.
834935
func (r *GitRepositoryReconciler) gitCheckout(ctx context.Context, obj *sourcev1.GitRepository,
835-
authOpts *git.AuthOptions, proxyOpts *transport.ProxyOptions, dir string, optimized bool) (*git.Commit, error) {
936+
authOpts *git.AuthOptions, proxyOpts *transport.ProxyOptions, dir string, optimized bool, clientConf GitClientConfigurer) (*git.Commit, error) {
836937
// Configure checkout strategy.
837938
cloneOpts := repository.CloneConfig{
838939
RecurseSubmodules: obj.Spec.RecurseSubmodules,
@@ -866,6 +967,9 @@ func (r *GitRepositoryReconciler) gitCheckout(ctx context.Context, obj *sourcev1
866967
clientOpts = append(clientOpts, gogit.WithProxy(*proxyOpts))
867968
}
868969

970+
if clientConf.IsValid() {
971+
clientConf.ConfigureGitClient(ctx, obj)
972+
}
869973
gitReader, err := gogit.NewClient(dir, authOpts, clientOpts...)
870974
if err != nil {
871975
e := serror.NewGeneric(

internal/controller/gitrepository_controller_test.go

+27
Original file line numberDiff line numberDiff line change
@@ -571,6 +571,33 @@ func TestGitRepositoryReconciler_reconcileSource_authStrategy(t *testing.T) {
571571
*conditions.UnknownCondition(meta.ReadyCondition, meta.ProgressingReason, "building artifact: new upstream revision 'master@sha1:<commit>'"),
572572
},
573573
},
574+
{
575+
name: "HTTPS with TLS certs authentication with WAF Reconciling=True",
576+
protocol: "https",
577+
server: options{
578+
publicKey: tlsPublicKey,
579+
privateKey: tlsPrivateKey,
580+
ca: tlsCA,
581+
},
582+
secret: &corev1.Secret{
583+
ObjectMeta: metav1.ObjectMeta{
584+
Name: "waf-authentication",
585+
},
586+
Data: map[string][]byte{
587+
"certFile": clientPublicKey,
588+
"keyFile": clientPrivateKey,
589+
"caFile": tlsCA,
590+
},
591+
},
592+
beforeFunc: func(obj *sourcev1.GitRepository) {
593+
obj.Spec.SecretRef = &meta.LocalObjectReference{Name: "waf-authentication"}
594+
},
595+
want: sreconcile.ResultSuccess,
596+
assertConditions: []metav1.Condition{
597+
*conditions.TrueCondition(meta.ReconcilingCondition, meta.ProgressingReason, "building artifact: new upstream revision 'master@sha1:<commit>'"),
598+
*conditions.UnknownCondition(meta.ReadyCondition, meta.ProgressingReason, "building artifact: new upstream revision 'master@sha1:<commit>'"),
599+
},
600+
},
574601
}
575602

576603
for _, tt := range tests {

0 commit comments

Comments
 (0)