Skip to content

Add support for mTLS to GitHub App transport #947

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Aug 14, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -19,13 +19,13 @@ require (
github.com/fluxcd/image-reflector-controller/api v0.35.2
github.com/fluxcd/pkg/apis/acl v0.8.0
github.com/fluxcd/pkg/apis/event v0.18.0
github.com/fluxcd/pkg/apis/meta v1.17.0
github.com/fluxcd/pkg/apis/meta v1.18.0
github.com/fluxcd/pkg/auth v0.21.0
github.com/fluxcd/pkg/cache v0.10.0
github.com/fluxcd/pkg/git v0.34.0
github.com/fluxcd/pkg/git/gogit v0.37.0
github.com/fluxcd/pkg/git v0.35.0
github.com/fluxcd/pkg/git/gogit v0.38.0
github.com/fluxcd/pkg/gittestserver v0.18.0
github.com/fluxcd/pkg/runtime v0.69.0
github.com/fluxcd/pkg/runtime v0.79.0
github.com/fluxcd/pkg/ssh v0.20.0
github.com/fluxcd/source-controller/api v1.6.1
github.com/go-git/go-billy/v5 v5.6.2
Expand Down
16 changes: 8 additions & 8 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -132,20 +132,20 @@ github.com/fluxcd/pkg/apis/acl v0.8.0 h1:mZNl4mOQQf5/cdMCYgKcrZTZRndCtMtkI0BDfNO
github.com/fluxcd/pkg/apis/acl v0.8.0/go.mod h1:uv7pXXR/gydiX4MUwlQa7vS8JONEDztynnjTvY3JxKQ=
github.com/fluxcd/pkg/apis/event v0.18.0 h1:PNbWk9gvX8gMIi6VsJapnuDO+giLEeY+6olLVXvXFkk=
github.com/fluxcd/pkg/apis/event v0.18.0/go.mod h1:7S/DGboLolfbZ6stO6dcDhG1SfkPWQ9foCULvbiYpiA=
github.com/fluxcd/pkg/apis/meta v1.17.0 h1:KVMDyJQj1NYCsppsFUkbJGMnKxsqJVpnKBFolHf/q8E=
github.com/fluxcd/pkg/apis/meta v1.17.0/go.mod h1:97l3hTwBpJbXBY+wetNbqrUsvES8B1jGioKcBUxmqd8=
github.com/fluxcd/pkg/apis/meta v1.18.0 h1:ACHrMIjlcioE9GKS7NGk62KX4NshqNewr8sBwMcXABs=
github.com/fluxcd/pkg/apis/meta v1.18.0/go.mod h1:97l3hTwBpJbXBY+wetNbqrUsvES8B1jGioKcBUxmqd8=
github.com/fluxcd/pkg/auth v0.21.0 h1:ckAQqP12wuptXEkMY18SQKWEY09m9e6yI0mEMsDV15M=
github.com/fluxcd/pkg/auth v0.21.0/go.mod h1:MXmpsXT97c874HCw5hnfqFUP7TsG8/Ss1vFrk8JccfM=
github.com/fluxcd/pkg/cache v0.10.0 h1:M+OGDM4da1cnz7q+sZSBtkBJHpiJsLnKVmR9OdMWxEY=
github.com/fluxcd/pkg/cache v0.10.0/go.mod h1:pPXRzQUDQagsCniuOolqVhnAkbNgYOg8d2cTliPs7ME=
github.com/fluxcd/pkg/git v0.34.0 h1:qTViWkfpEDnjzySyKRKliqUeGj/DznqlkmPhaDNIsFY=
github.com/fluxcd/pkg/git v0.34.0/go.mod h1:F9Asm3MlLW4uZx3FF92+bqho+oktdMdnTn/QmXe56NE=
github.com/fluxcd/pkg/git/gogit v0.37.0 h1:JINylFYpwrxS3MCu5Ei+g6XPgxbs5lv9PppIYYr07KY=
github.com/fluxcd/pkg/git/gogit v0.37.0/go.mod h1:X7YzW5mb4srA05h4SpL2OEGEHq02tbXQF5DPJen9hlc=
github.com/fluxcd/pkg/git v0.35.0 h1:mAauhsdfxNW4yQdXviVlvcN/uCGGG0+6p5D1+HFZI9w=
github.com/fluxcd/pkg/git v0.35.0/go.mod h1:F9Asm3MlLW4uZx3FF92+bqho+oktdMdnTn/QmXe56NE=
github.com/fluxcd/pkg/git/gogit v0.38.0 h1:222KmjpKf9pxqi8rAtm1omDcpGTY4JkahLrAwZ3AcwU=
github.com/fluxcd/pkg/git/gogit v0.38.0/go.mod h1:kHStdfd/AtkH5ED0UEWP2tmMGnfxg1GG92D29M+lRJ0=
github.com/fluxcd/pkg/gittestserver v0.18.0 h1:jkuLmzWFfq+v1ziI0LspZrUzc5WzCO98BaWb8OVRPtk=
github.com/fluxcd/pkg/gittestserver v0.18.0/go.mod h1:2wDLqUkPuixk/8pGQdef9ewaGJXf7Z+xHDVq8PIFG4E=
github.com/fluxcd/pkg/runtime v0.69.0 h1:5gPY95NSFI34GlQTj0+NHjOFpirSwviCUb9bM09b5nA=
github.com/fluxcd/pkg/runtime v0.69.0/go.mod h1:ug+pat+I4wfOBuCy2E/pLmBNd3kOOo4cP2jxnxefPwY=
github.com/fluxcd/pkg/runtime v0.79.0 h1:9tv79EiQDx/QJH9mYDd9kZ9WybCVWBUGoiBHij+eKkc=
github.com/fluxcd/pkg/runtime v0.79.0/go.mod h1:iGhdaEq+lMJQTJNAFEPOU4gUJ7kt3yeDcJPZy7O9IUw=
github.com/fluxcd/pkg/ssh v0.20.0 h1:Ak0laIYIc/L8lEfqls/LDWRW8wYPESGaravQsCRGLb8=
github.com/fluxcd/pkg/ssh v0.20.0/go.mod h1:sRfAAkxx1GwCGjYirKPnTKdNkNrJRo9kqzWLVFXKv7E=
github.com/fluxcd/pkg/version v0.9.0 h1:pQBHMt9TbnnTUzj3EoMhRi5JUkNBqrTBSAaoLG1ovUA=
Expand Down
105 changes: 50 additions & 55 deletions internal/source/git.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import (
"time"

"github.com/ProtonMail/go-crypto/openpgp"
"github.com/fluxcd/pkg/runtime/secrets"
"github.com/go-git/go-git/v5/plumbing/transport"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
Expand Down Expand Up @@ -62,13 +63,14 @@ type gitSrcCfg struct {
}

func buildGitConfig(ctx context.Context, c client.Client, originKey, srcKey types.NamespacedName, gitSpec *imagev1.GitSpec, opts SourceOptions) (*gitSrcCfg, error) {
var err error
cfg := &gitSrcCfg{
srcKey: srcKey,
}

// Get the repo.
repo := &sourcev1.GitRepository{}
if err := c.Get(ctx, srcKey, repo); err != nil {
if err = c.Get(ctx, srcKey, repo); err != nil {
if client.IgnoreNotFound(err) == nil {
return nil, fmt.Errorf("referenced git repository does not exist: %w", err)
}
Expand All @@ -94,14 +96,26 @@ func buildGitConfig(ctx context.Context, c client.Client, originKey, srcKey type

// Configure push first as the client options below depend on the push
// configuration.
if err := configurePush(cfg, gitSpec, cfg.checkoutRef); err != nil {
if err = configurePush(cfg, gitSpec, cfg.checkoutRef); err != nil {
return nil, err
}

proxyOpts, proxyURL, err := getProxyOpts(ctx, c, repo)
if err != nil {
return nil, err
var proxyURL *url.URL
var proxyOpts *transport.ProxyOptions
// Check if a proxy secret reference is provided in the GitRepository spec.
if repo.Spec.ProxySecretRef != nil {
secretRef := types.NamespacedName{
Name: repo.Spec.ProxySecretRef.Name,
Namespace: repo.GetNamespace(),
}
// Get the proxy URL from runtime/secret
proxyURL, err = secrets.ProxyURLFromSecretRef(ctx, c, secretRef)
if err != nil {
return nil, err
}
proxyOpts = &transport.ProxyOptions{URL: proxyURL.String()}
}

cfg.authOpts, err = getAuthOpts(ctx, c, repo, opts, proxyURL)
if err != nil {
return nil, err
Expand Down Expand Up @@ -165,13 +179,15 @@ func configurePush(cfg *gitSrcCfg, gitSpec *imagev1.GitSpec, checkoutRef *source

func getAuthOpts(ctx context.Context, c client.Client, repo *sourcev1.GitRepository,
srcOpts SourceOptions, proxyURL *url.URL) (*git.AuthOptions, error) {
var secret *corev1.Secret
var data map[string][]byte
var err error
if repo.Spec.SecretRef != nil {
data, err = getSecretData(ctx, c, repo.Spec.SecretRef.Name, repo.GetNamespace())
secret, err = getSecret(ctx, c, repo.Spec.SecretRef.Name, repo.GetNamespace())
if err != nil {
return nil, fmt.Errorf("failed to get auth secret '%s/%s': %w", repo.GetNamespace(), repo.Spec.SecretRef.Name, err)
}
data = secret.Data
}

u, err := url.Parse(repo.Spec.URL)
Expand Down Expand Up @@ -211,24 +227,34 @@ func getAuthOpts(ctx context.Context, c client.Client, repo *sourcev1.GitReposit
if repo.Spec.SecretRef == nil {
return nil, fmt.Errorf("secretRef with github app data must be specified when provider is set to github: %w", ErrInvalidSourceConfiguration)
}
targetURL := fmt.Sprintf("%s://%s", u.Scheme, u.Host)
authMethods, err := secrets.AuthMethodsFromSecret(ctx, secret, secrets.WithTargetURL(targetURL), secrets.WithTLSSystemCertPool())
if err != nil {
return nil, err
}
if !authMethods.HasGitHubAppData() {
return nil, fmt.Errorf("secretRef with github app data must be specified when provider is set to github: %w", ErrInvalidSourceConfiguration)
}

getCreds = func() (*authutils.GitCredentials, error) {
var opts []github.OptFunc
var appOpts []github.OptFunc

if len(data) > 0 {
opts = append(opts, github.WithAppData(data))
}
appOpts = append(appOpts, github.WithAppData(authMethods.GitHubAppData))

if proxyURL != nil {
opts = append(opts, github.WithProxyURL(proxyURL))
appOpts = append(appOpts, github.WithProxyURL(proxyURL))
}

if srcOpts.tokenCache != nil {
opts = append(opts, github.WithCache(srcOpts.tokenCache, imagev1.ImageUpdateAutomationKind,
appOpts = append(appOpts, github.WithCache(srcOpts.tokenCache, imagev1.ImageUpdateAutomationKind,
srcOpts.objName, srcOpts.objNamespace, cache.OperationReconcile))
}

username, password, err := github.GetCredentials(ctx, opts...)
if authMethods.HasTLS() {
appOpts = append(appOpts, github.WithTLSConfig(authMethods.TLS))
}

username, password, err := github.GetCredentials(ctx, appOpts...)
if err != nil {
return nil, err
}
Expand All @@ -255,45 +281,6 @@ func getAuthOpts(ctx context.Context, c client.Client, repo *sourcev1.GitReposit
return opts, nil
}

func getProxyOpts(ctx context.Context, c client.Client, repo *sourcev1.GitRepository) (*transport.ProxyOptions, *url.URL, error) {
if repo.Spec.ProxySecretRef == nil {
return nil, nil, nil
}
name := repo.Spec.ProxySecretRef.Name
namespace := repo.GetNamespace()
proxyData, err := getSecretData(ctx, c, name, namespace)
if err != nil {
return nil, nil, fmt.Errorf("failed to get proxy secret '%s/%s': %w", namespace, name, err)
}
b, ok := proxyData["address"]
if !ok {
return nil, nil, fmt.Errorf("invalid proxy secret '%s/%s': key 'address' is missing", namespace, name)
}

address := string(b)
username := string(proxyData["username"])
password := string(proxyData["password"])

proxyOpts := &transport.ProxyOptions{
URL: address,
Username: username,
Password: password,
}

proxyURL, err := url.Parse(string(address))
if err != nil {
return nil, nil, fmt.Errorf("invalid address in proxy secret '%s/%s': %w", namespace, name, err)
}
switch {
case username != "" && password == "":
proxyURL.User = url.User(username)
case username != "" && password != "":
proxyURL.User = url.UserPassword(username, password)
}

return proxyOpts, proxyURL, nil
}

func getSigningEntity(ctx context.Context, c client.Client, namespace string, gitSpec *imagev1.GitSpec) (*openpgp.Entity, error) {
secretName := gitSpec.Commit.SigningKey.SecretRef.Name
secretData, err := getSecretData(ctx, c, secretName, namespace)
Expand Down Expand Up @@ -330,13 +317,21 @@ func getSigningEntity(ctx context.Context, c client.Client, namespace string, gi
}

func getSecretData(ctx context.Context, c client.Client, name, namespace string) (map[string][]byte, error) {
secret, err := getSecret(ctx, c, name, namespace)
if err != nil {
return nil, err
}
return secret.Data, nil
}

func getSecret(ctx context.Context, c client.Client, name, namespace string) (*corev1.Secret, error) {
key := types.NamespacedName{
Namespace: namespace,
Name: name,
}
var secret corev1.Secret
if err := c.Get(ctx, key, &secret); err != nil {
secret := &corev1.Secret{}
if err := c.Get(ctx, key, secret); err != nil {
return nil, err
}
return secret.Data, nil
return secret, nil
}
115 changes: 23 additions & 92 deletions internal/source/git_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,11 +19,9 @@ package source
import (
"context"
"fmt"
"net/url"
"testing"
"time"

"github.com/go-git/go-git/v5/plumbing/transport"
. "github.com/onsi/gomega"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
Expand All @@ -32,12 +30,13 @@ import (
"sigs.k8s.io/controller-runtime/pkg/client"
fakeclient "sigs.k8s.io/controller-runtime/pkg/client/fake"

imagev1 "github.com/fluxcd/image-automation-controller/api/v1beta2"
"github.com/fluxcd/image-automation-controller/internal/testutil"
"github.com/fluxcd/pkg/apis/meta"
"github.com/fluxcd/pkg/git"
"github.com/fluxcd/pkg/git/github"
sourcev1 "github.com/fluxcd/source-controller/api/v1"

imagev1 "github.com/fluxcd/image-automation-controller/api/v1beta2"
"github.com/fluxcd/image-automation-controller/internal/testutil"
)

func Test_getAuthOpts(t *testing.T) {
Expand Down Expand Up @@ -196,6 +195,26 @@ func Test_getAuthOpts_providerAuth(t *testing.T) {
},
wantErr: "Key must be a PEM encoded PKCS1 or PKCS8 key",
},
{
name: "github provider with basic auth in secret",
url: "https://example.com/org/repo",
secret: &corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: "basic-auth-secret",
},
Data: map[string][]byte{
"username": []byte("abc"),
"password": []byte(""),
},
},
beforeFunc: func(obj *sourcev1.GitRepository) {
obj.Spec.Provider = sourcev1.GitProviderGitHub
obj.Spec.SecretRef = &meta.LocalObjectReference{
Name: "basic-auth-secret",
}
},
wantErr: "secretRef with github app data must be specified when provider is set to github",
},
{
name: "generic provider with github app data in secret",
url: "https://example.com/org/repo",
Expand Down Expand Up @@ -266,94 +285,6 @@ func Test_getAuthOpts_providerAuth(t *testing.T) {
}
}

func Test_getProxyOpts(t *testing.T) {
namespace := "default"
invalidProxy := &corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: "invalid-proxy",
Namespace: namespace,
},
Data: map[string][]byte{
"url": []byte("https://example.com"),
},
}
validProxy := &corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: "valid-proxy",
Namespace: namespace,
},
Data: map[string][]byte{
"address": []byte("https://example.com"),
"username": []byte("user"),
"password": []byte("pass"),
},
}

tests := []struct {
name string
secretName string
want *transport.ProxyOptions
wantProxyURL *url.URL
wantErr bool
}{
{
name: "non-existing secret",
secretName: "non-existing",
want: nil,
wantProxyURL: nil,
wantErr: true,
},
{
name: "invalid proxy secret",
secretName: "invalid-proxy",
want: nil,
wantProxyURL: nil,
wantErr: true,
},
{
name: "valid proxy secret",
secretName: "valid-proxy",
want: &transport.ProxyOptions{
URL: "https://example.com",
Username: "user",
Password: "pass",
},
wantProxyURL: &url.URL{
Scheme: "https",
Host: "example.com",
User: url.UserPassword("user", "pass"),
},
wantErr: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
g := NewWithT(t)

clientBuilder := fakeclient.NewClientBuilder().
WithScheme(scheme.Scheme).
WithObjects(invalidProxy, validProxy)
c := clientBuilder.Build()

gitRepo := &sourcev1.GitRepository{}
gitRepo.Namespace = namespace
if tt.secretName != "" {
gitRepo.Spec = sourcev1.GitRepositorySpec{
ProxySecretRef: &meta.LocalObjectReference{Name: tt.secretName},
}
}

got, gotProxyURL, err := getProxyOpts(context.TODO(), c, gitRepo)
if (err != nil) != tt.wantErr {
g.Fail(fmt.Sprintf("unexpected error: %v", err))
return
}
g.Expect(got).To(Equal(tt.want))
g.Expect(gotProxyURL).To(Equal(tt.wantProxyURL))
})
}
}

func Test_getSigningEntity(t *testing.T) {
g := NewWithT(t)

Expand Down