Skip to content

Commit aaee433

Browse files
author
Philip Laine
authored
Merge pull request #283 from fluxcd/feature/self-signed-certs
Add custom CA validation for Git over HTTPS
2 parents a55a714 + c063484 commit aaee433

File tree

8 files changed

+169
-22
lines changed

8 files changed

+169
-22
lines changed

controllers/gitrepository_controller_test.go

Lines changed: 78 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,9 @@ package controllers
1818

1919
import (
2020
"context"
21+
"crypto/tls"
2122
"fmt"
23+
"net/http"
2224
"net/url"
2325
"os"
2426
"path"
@@ -30,6 +32,8 @@ import (
3032
"github.com/go-git/go-git/v5/config"
3133
"github.com/go-git/go-git/v5/plumbing"
3234
"github.com/go-git/go-git/v5/plumbing/object"
35+
"github.com/go-git/go-git/v5/plumbing/transport/client"
36+
httptransport "github.com/go-git/go-git/v5/plumbing/transport/http"
3337
"github.com/go-git/go-git/v5/storage/memory"
3438
. "github.com/onsi/ginkgo"
3539
. "github.com/onsi/ginkgo/extensions/table"
@@ -40,6 +44,7 @@ import (
4044

4145
"github.com/fluxcd/pkg/gittestserver"
4246

47+
"github.com/fluxcd/pkg/apis/meta"
4348
sourcev1 "github.com/fluxcd/source-controller/api/v1beta1"
4449
)
4550

@@ -65,6 +70,18 @@ var _ = Describe("GitRepositoryReconciler", func() {
6570
err = k8sClient.Create(context.Background(), namespace)
6671
Expect(err).NotTo(HaveOccurred(), "failed to create test namespace")
6772

73+
cert := corev1.Secret{
74+
ObjectMeta: metav1.ObjectMeta{
75+
Name: "cert",
76+
Namespace: namespace.Name,
77+
},
78+
Data: map[string][]byte{
79+
"caFile": exampleCA,
80+
},
81+
}
82+
err = k8sClient.Create(context.Background(), &cert)
83+
Expect(err).NotTo(HaveOccurred())
84+
6885
gitServer, err = gittestserver.NewTempGitServer()
6986
Expect(err).NotTo(HaveOccurred())
7087
gitServer.AutoCreate()
@@ -87,6 +104,7 @@ var _ = Describe("GitRepositoryReconciler", func() {
87104
expectMessage string
88105
expectRevision string
89106

107+
secretRef *meta.LocalObjectReference
90108
gitImplementation string
91109
}
92110

@@ -274,6 +292,55 @@ var _ = Describe("GitRepositoryReconciler", func() {
274292
Expect(err).NotTo(HaveOccurred())
275293
u.Path = path.Join(u.Path, fmt.Sprintf("repository-%s.git", randStringRunes(5)))
276294

295+
var transport = httptransport.NewClient(&http.Client{
296+
Transport: &http.Transport{
297+
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
298+
},
299+
})
300+
client.InstallProtocol("https", transport)
301+
302+
fs := memfs.New()
303+
gitrepo, err := git.Init(memory.NewStorage(), fs)
304+
Expect(err).NotTo(HaveOccurred())
305+
306+
wt, err := gitrepo.Worktree()
307+
Expect(err).NotTo(HaveOccurred())
308+
309+
ff, _ := fs.Create("fixture")
310+
_ = ff.Close()
311+
_, err = wt.Add(fs.Join("fixture"))
312+
Expect(err).NotTo(HaveOccurred())
313+
314+
commit, err := wt.Commit("Sample", &git.CommitOptions{Author: &object.Signature{
315+
Name: "John Doe",
316+
317+
When: time.Now(),
318+
}})
319+
Expect(err).NotTo(HaveOccurred())
320+
321+
gitrepo.Worktree()
322+
323+
for _, ref := range t.createRefs {
324+
hRef := plumbing.NewHashReference(plumbing.ReferenceName(ref), commit)
325+
err = gitrepo.Storer.SetReference(hRef)
326+
Expect(err).NotTo(HaveOccurred())
327+
}
328+
329+
remote, err := gitrepo.CreateRemote(&config.RemoteConfig{
330+
Name: "origin",
331+
URLs: []string{u.String()},
332+
})
333+
Expect(err).NotTo(HaveOccurred())
334+
335+
err = remote.Push(&git.PushOptions{
336+
RefSpecs: []config.RefSpec{"refs/heads/*:refs/heads/*", "refs/tags/*:refs/tags/*"},
337+
})
338+
Expect(err).NotTo(HaveOccurred())
339+
340+
t.reference.Commit = strings.Replace(t.reference.Commit, "<commit>", commit.String(), 1)
341+
342+
client.InstallProtocol("https", httptransport.DefaultClient)
343+
277344
key := types.NamespacedName{
278345
Name: fmt.Sprintf("git-ref-test-%s", randStringRunes(5)),
279346
Namespace: namespace.Name,
@@ -288,6 +355,7 @@ var _ = Describe("GitRepositoryReconciler", func() {
288355
Interval: metav1.Duration{Duration: indexInterval},
289356
Reference: t.reference,
290357
GitImplementation: t.gitImplementation,
358+
SecretRef: t.secretRef,
291359
},
292360
}
293361
Expect(k8sClient.Create(context.Background(), created)).Should(Succeed())
@@ -316,13 +384,22 @@ var _ = Describe("GitRepositoryReconciler", func() {
316384
expectStatus: metav1.ConditionFalse,
317385
expectMessage: "x509: certificate signed by unknown authority",
318386
}),
319-
Entry("self signed v2", refTestCase{
387+
Entry("self signed v2 without CA", refTestCase{
320388
reference: &sourcev1.GitRepositoryRef{Branch: "main"},
321389
waitForReason: sourcev1.GitOperationFailedReason,
322390
expectStatus: metav1.ConditionFalse,
323391
expectMessage: "error: user rejected certificate",
324392
gitImplementation: sourcev1.LibGit2Implementation,
325393
}),
394+
Entry("self signed v2 with CA", refTestCase{
395+
reference: &sourcev1.GitRepositoryRef{Branch: "some-branch"},
396+
createRefs: []string{"refs/heads/some-branch"},
397+
waitForReason: sourcev1.GitOperationSucceedReason,
398+
expectStatus: metav1.ConditionTrue,
399+
expectRevision: "some-branch",
400+
secretRef: &meta.LocalObjectReference{Name: "cert"},
401+
gitImplementation: sourcev1.LibGit2Implementation,
402+
}),
326403
)
327404
})
328405
})

docs/spec/v1beta1/gitrepositories.md

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -405,6 +405,39 @@ kubectl create secret generic pgp-public-keys \
405405
--from-file=author2.asc
406406
```
407407

408+
## Self-signed certificates
409+
410+
Cloning over HTTPS from a Git repository with a self-signed certificate:
411+
412+
```yaml
413+
apiVersion: source.toolkit.fluxcd.io/v1beta1
414+
kind: GitRepository
415+
metadata:
416+
name: podinfo
417+
namespace: default
418+
spec:
419+
interval: 1m
420+
url: https://customdomain.com/stefanprodan/podinfo
421+
secretRef:
422+
name: https-credentials
423+
gitImplementation: libgit2
424+
---
425+
apiVersion: v1
426+
kind: Secret
427+
metadata:
428+
name: https-credentials
429+
namespace: default
430+
type: Opaque
431+
data:
432+
username: <BASE64>
433+
password: <BASE64>
434+
caFile: <BASE64>
435+
```
436+
437+
Note that the Git implementation has to be `libgit2` as `go-git` does not support custom CA verification.
438+
It is also possible to specify a `caFile` for public repositories, in that case the username and password
439+
can be omitted.
440+
408441
## Status examples
409442

410443
Successful sync:

go.mod

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,12 +20,12 @@ require (
2020
github.com/go-git/go-billy/v5 v5.0.0
2121
github.com/go-git/go-git/v5 v5.2.0
2222
github.com/go-logr/logr v0.3.0
23-
github.com/libgit2/git2go/v31 v31.3.0
23+
github.com/libgit2/git2go/v31 v31.4.7
2424
github.com/minio/minio-go/v7 v7.0.5
2525
github.com/onsi/ginkgo v1.14.1
2626
github.com/onsi/gomega v1.10.2
2727
github.com/spf13/pflag v1.0.5
28-
golang.org/x/crypto v0.0.0-20201002170205-7f63de1d35b0
28+
golang.org/x/crypto v0.0.0-20201203163018-be400aefbc4c
2929
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e
3030
helm.sh/helm/v3 v3.5.0
3131
k8s.io/api v0.20.2

go.sum

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -427,6 +427,8 @@ github.com/google/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hf
427427
github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
428428
github.com/google/pprof v0.0.0-20200229191704-1ebb73c60ed3/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
429429
github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
430+
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 h1:El6M4kTTCOh6aBiKaUGG7oYTSPP8MxqL4YI3kZKwcP4=
431+
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ=
430432
github.com/google/uuid v1.0.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
431433
github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
432434
github.com/google/uuid v1.1.2 h1:EVhdT+1Kseyi1/pUmXKaFxYsDNy9RQYkMWRH68J/W7Y=
@@ -553,8 +555,8 @@ github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
553555
github.com/lib/pq v1.8.0/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
554556
github.com/lib/pq v1.9.0 h1:L8nSXQQzAYByakOFMTwpjRoHsMJklur4Gi59b6VivR8=
555557
github.com/lib/pq v1.9.0/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
556-
github.com/libgit2/git2go/v31 v31.3.0 h1:d8ciyYVKir+gKwra3KuNxTyVvbgGKn4admdt1PNNAOg=
557-
github.com/libgit2/git2go/v31 v31.3.0/go.mod h1:mnc0hPGPs0nDi9INrurTpioeRzje9DvSXqON/+JEhwY=
558+
github.com/libgit2/git2go/v31 v31.4.7 h1:P85qB5at5un4qPqUcvOZbAom7P0G4KAG/OLVyD29kQ0=
559+
github.com/libgit2/git2go/v31 v31.4.7/go.mod h1:c/rkJcBcUFx6wHaT++UwNpKvIsmPNqCeQ/vzO4DrEec=
558560
github.com/liggitt/tabwriter v0.0.0-20181228230101-89fcab3d43de h1:9TO3cAIGXtEhnIaL+V+BEER86oLrvS+kWobKpbJuye0=
559561
github.com/liggitt/tabwriter v0.0.0-20181228230101-89fcab3d43de/go.mod h1:zAbeS9B/r2mtpb6U+EI2rYA5OAXxsYw6wTamcNW+zcE=
560562
github.com/lightstep/lightstep-tracer-common/golang/gogo v0.0.0-20190605223551-bc2310a04743/go.mod h1:qklhhLq1aX+mtWk9cPHPzaBjWImj5ULL6C7HFJtXQMM=
@@ -903,6 +905,8 @@ golang.org/x/crypto v0.0.0-20200709230013-948cd5f35899/go.mod h1:LzIPMQfyMNhhGPh
903905
golang.org/x/crypto v0.0.0-20200728195943-123391ffb6de/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
904906
golang.org/x/crypto v0.0.0-20201002170205-7f63de1d35b0 h1:hb9wdF1z5waM+dSIICn1l0DkLVDT3hqhhQsDNUmHPRE=
905907
golang.org/x/crypto v0.0.0-20201002170205-7f63de1d35b0/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
908+
golang.org/x/crypto v0.0.0-20201203163018-be400aefbc4c h1:9HhBz5L/UjnK9XLtiZhYAdue5BVKep3PMmS2LuPDt8k=
909+
golang.org/x/crypto v0.0.0-20201203163018-be400aefbc4c/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I=
906910
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
907911
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
908912
golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
@@ -1032,6 +1036,10 @@ golang.org/x/sys v0.0.0-20200625212154-ddb9806d33ae/go.mod h1:h1NjWce9XRLGQEsW7w
10321036
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
10331037
golang.org/x/sys v0.0.0-20201112073958-5cba982894dd h1:5CtCZbICpIOFdgO940moixOPjc0178IU44m4EjOO5IY=
10341038
golang.org/x/sys v0.0.0-20201112073958-5cba982894dd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
1039+
golang.org/x/sys v0.0.0-20201204225414-ed752295db88 h1:KmZPnMocC93w341XZp26yTJg8Za7lhb2KhkYmixoeso=
1040+
golang.org/x/sys v0.0.0-20201204225414-ed752295db88/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
1041+
golang.org/x/term v0.0.0-20201117132131-f5c789dd3221 h1:/ZHdbVpdR/jk3g30/d4yUL0JU9kksj8+F/bnQUVLGDM=
1042+
golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw=
10351043
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
10361044
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
10371045
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=

pkg/git/common/common.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ const (
2828
DefaultOrigin = "origin"
2929
DefaultBranch = "master"
3030
DefaultPublicKeyAuthUser = "git"
31+
CAFile = "caFile"
3132
)
3233

3334
type Commit interface {

pkg/git/v1/transport.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,10 @@ func AuthSecretStrategyForURL(URL string) (common.AuthSecretStrategy, error) {
4747
type BasicAuth struct{}
4848

4949
func (s *BasicAuth) Method(secret corev1.Secret) (*common.Auth, error) {
50+
if _, ok := secret.Data[common.CAFile]; ok {
51+
return nil, fmt.Errorf("found caFile key in secret '%s' but go-git HTTP transport does not support custom certificates", secret.Name)
52+
}
53+
5054
auth := &http.BasicAuth{}
5155
if username, ok := secret.Data["username"]; ok {
5256
auth.Username = string(username)
@@ -65,6 +69,10 @@ type PublicKeyAuth struct {
6569
}
6670

6771
func (s *PublicKeyAuth) Method(secret corev1.Secret) (*common.Auth, error) {
72+
if _, ok := secret.Data[common.CAFile]; ok {
73+
return nil, fmt.Errorf("found caFile key in secret '%s' but go-git SSH transport does not support custom certificates", secret.Name)
74+
}
75+
6876
identity := secret.Data["identity"]
6977
knownHosts := secret.Data["known_hosts"]
7078
if len(identity) == 0 || len(knownHosts) == 0 {

pkg/git/v2/transport.go

Lines changed: 35 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -20,11 +20,13 @@ import (
2020
"bufio"
2121
"bytes"
2222
"crypto/sha1"
23+
"crypto/x509"
2324
"fmt"
24-
"golang.org/x/crypto/ssh"
2525
"net/url"
2626
"strings"
2727

28+
"golang.org/x/crypto/ssh"
29+
2830
"github.com/fluxcd/source-controller/pkg/git/common"
2931
git2go "github.com/libgit2/git2go/v31"
3032
corev1 "k8s.io/api/core/v1"
@@ -49,6 +51,7 @@ func AuthSecretStrategyForURL(URL string) (common.AuthSecretStrategy, error) {
4951
type BasicAuth struct{}
5052

5153
func (s *BasicAuth) Method(secret corev1.Secret) (*common.Auth, error) {
54+
var credCallback git2go.CredentialsCallback
5255
var username string
5356
if d, ok := secret.Data["username"]; ok {
5457
username = string(d)
@@ -57,26 +60,49 @@ func (s *BasicAuth) Method(secret corev1.Secret) (*common.Auth, error) {
5760
if d, ok := secret.Data["password"]; ok {
5861
password = string(d)
5962
}
60-
if username == "" || password == "" {
61-
return nil, fmt.Errorf("invalid '%s' secret data: required fields 'username' and 'password'", secret.Name)
63+
if username != "" && password != "" {
64+
credCallback = func(url string, username_from_url string, allowed_types git2go.CredType) (*git2go.Cred, error) {
65+
cred, err := git2go.NewCredUserpassPlaintext(username, password)
66+
if err != nil {
67+
return nil, err
68+
}
69+
return cred, nil
70+
}
6271
}
6372

64-
credCallback := func(url string, username_from_url string, allowed_types git2go.CredType) (*git2go.Cred, error) {
65-
cred, err := git2go.NewCredUserpassPlaintext(username, password)
66-
if err != nil {
67-
return nil, err
73+
var certCallback git2go.CertificateCheckCallback
74+
if caFile, ok := secret.Data[common.CAFile]; ok {
75+
certCallback = func(cert *git2go.Certificate, valid bool, hostname string) git2go.ErrorCode {
76+
roots := x509.NewCertPool()
77+
ok := roots.AppendCertsFromPEM(caFile)
78+
if !ok {
79+
return git2go.ErrCertificate
80+
}
81+
82+
opts := x509.VerifyOptions{
83+
Roots: roots,
84+
DNSName: hostname,
85+
}
86+
_, err := cert.X509.Verify(opts)
87+
if err != nil {
88+
return git2go.ErrCertificate
89+
}
90+
return git2go.ErrOk
6891
}
69-
return cred, nil
7092
}
7193

72-
return &common.Auth{CredCallback: credCallback, CertCallback: nil}, nil
94+
return &common.Auth{CredCallback: credCallback, CertCallback: certCallback}, nil
7395
}
7496

7597
type PublicKeyAuth struct {
7698
user string
7799
}
78100

79101
func (s *PublicKeyAuth) Method(secret corev1.Secret) (*common.Auth, error) {
102+
if _, ok := secret.Data[common.CAFile]; ok {
103+
return nil, fmt.Errorf("found caFile key in secret '%s' but libgit2 SSH transport does not support custom certificates", secret.Name)
104+
}
105+
80106
identity := secret.Data["identity"]
81107
knownHosts := secret.Data["known_hosts"]
82108
if len(identity) == 0 || len(knownHosts) == 0 {

pkg/git/v2/transport_test.go

Lines changed: 2 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -96,12 +96,9 @@ func TestBasicAuthStrategy_Method(t *testing.T) {
9696
name string
9797
secret corev1.Secret
9898
modify func(secret *corev1.Secret)
99-
want *common.Auth
10099
wantErr bool
101100
}{
102-
{"without username", basicAuthSecretFixture, func(s *corev1.Secret) { delete(s.Data, "username") }, nil, true},
103-
{"without password", basicAuthSecretFixture, func(s *corev1.Secret) { delete(s.Data, "password") }, nil, true},
104-
{"empty", corev1.Secret{}, nil, nil, true},
101+
{"with username and password", basicAuthSecretFixture, nil, false},
105102
}
106103
for _, tt := range tests {
107104
t.Run(tt.name, func(t *testing.T) {
@@ -110,14 +107,11 @@ func TestBasicAuthStrategy_Method(t *testing.T) {
110107
tt.modify(secret)
111108
}
112109
s := &BasicAuth{}
113-
got, err := s.Method(*secret)
110+
_, err := s.Method(*secret)
114111
if (err != nil) != tt.wantErr {
115112
t.Errorf("Method() error = %v, wantErr %v", err, tt.wantErr)
116113
return
117114
}
118-
if !reflect.DeepEqual(got, tt.want) {
119-
t.Errorf("Method() got = %v, want %v", got, tt.want)
120-
}
121115
})
122116
}
123117
}

0 commit comments

Comments
 (0)