Skip to content

Commit 1e3bc47

Browse files
authored
Merge pull request #23 from fluxcd/signature-verification
gitrepository: Implement PGP signature verification
2 parents 00b494e + 40c1851 commit 1e3bc47

File tree

5 files changed

+107
-6
lines changed

5 files changed

+107
-6
lines changed

api/v1alpha1/condition_types.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,4 +65,8 @@ const (
6565
// AuthenticationFailedReason represents the fact that a given secret does not
6666
// have the required fields or the provided credentials do not match.
6767
AuthenticationFailedReason string = "AuthenticationFailed"
68+
69+
// VerificationFailedReason represents the fact that the cryptographic provenance
70+
// verification for the source failed.
71+
VerificationFailedReason string = "VerificationFailed"
6872
)

api/v1alpha1/gitrepository_types.go

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,10 @@ type GitRepositorySpec struct {
4444
// master branch.
4545
// +optional
4646
Reference *GitRepositoryRef `json:"ref,omitempty"`
47+
48+
// Verify OpenPGP signature for the commit that HEAD points to.
49+
// +optional
50+
Verification *GitRepositoryVerification `json:"verify,omitempty"`
4751
}
4852

4953
// GitRepositoryRef defines the git ref used for pull and checkout operations.
@@ -66,7 +70,17 @@ type GitRepositoryRef struct {
6670
Commit string `json:"commit"`
6771
}
6872

69-
// GitRepositoryStatus defines the observed state of the GitRepository.
73+
// GitRepositoryVerification defines the OpenPGP signature verification process.
74+
type GitRepositoryVerification struct {
75+
// Mode describes what git object should be verified, currently ('head').
76+
// +kubebuilder:validation:Enum=head
77+
Mode string `json:"mode"`
78+
79+
// The secret name containing the public keys of all trusted git authors.
80+
SecretRef corev1.LocalObjectReference `json:"secretRef,omitempty"`
81+
}
82+
83+
// GitRepositoryStatus defines the observed state of a Git repository.
7084
type GitRepositoryStatus struct {
7185
// +optional
7286
Conditions []SourceCondition `json:"conditions,omitempty"`

api/v1alpha1/zz_generated.deepcopy.go

Lines changed: 21 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

config/crd/bases/source.fluxcd.io_gitrepositories.yaml

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -86,12 +86,34 @@ spec:
8686
description: The repository URL, can be a HTTP or SSH address.
8787
pattern: ^(http|https|ssh)://
8888
type: string
89+
verify:
90+
description: Verify OpenPGP signature for the commit that HEAD points
91+
to.
92+
properties:
93+
mode:
94+
description: Mode describes what git object should be verified,
95+
currently ('head').
96+
enum:
97+
- head
98+
type: string
99+
secretRef:
100+
description: The secret name containing the public keys of all trusted
101+
git authors.
102+
properties:
103+
name:
104+
description: 'Name of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names
105+
TODO: Add other useful fields. apiVersion, kind, uid?'
106+
type: string
107+
type: object
108+
required:
109+
- mode
110+
type: object
89111
required:
90112
- interval
91113
- url
92114
type: object
93115
status:
94-
description: GitRepositoryStatus defines the observed state of the GitRepository.
116+
description: GitRepositoryStatus defines the observed state of a Git repository.
95117
properties:
96118
artifact:
97119
description: Artifact represents the output of the last successful repository

controllers/gitrepository_controller.go

Lines changed: 44 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ import (
3636
"sigs.k8s.io/controller-runtime/pkg/client"
3737

3838
sourcev1 "github.com/fluxcd/source-controller/api/v1alpha1"
39-
internalgit "github.com/fluxcd/source-controller/internal/git"
39+
intgit "github.com/fluxcd/source-controller/internal/git"
4040
)
4141

4242
// GitRepositoryReconciler reconciles a GitRepository object
@@ -76,10 +76,11 @@ func (r *GitRepositoryReconciler) Reconcile(req ctrl.Request) (ctrl.Result, erro
7676
log.Error(err, "artifacts GC failed")
7777
}
7878

79-
// try git clone
79+
// try git sync
8080
syncedRepo, err := r.sync(ctx, *repo.DeepCopy())
8181
if err != nil {
8282
log.Error(err, "Git repository sync failed")
83+
return ctrl.Result{Requeue: true}, err
8384
}
8485

8586
// update status
@@ -128,6 +129,7 @@ func (r *GitRepositoryReconciler) sync(ctx context.Context, repository sourcev1.
128129
}
129130
}
130131

132+
// determine auth method
131133
var auth transport.AuthMethod
132134
if repository.Spec.SecretRef != nil {
133135
name := types.NamespacedName{
@@ -142,7 +144,7 @@ func (r *GitRepositoryReconciler) sync(ctx context.Context, repository sourcev1.
142144
return sourcev1.GitRepositoryNotReady(repository, sourcev1.AuthenticationFailedReason, err.Error()), err
143145
}
144146

145-
method, cleanup, err := internalgit.AuthMethodFromSecret(repository.Spec.URL, secret)
147+
method, cleanup, err := intgit.AuthMethodFromSecret(repository.Spec.URL, secret)
146148
if err != nil {
147149
err = fmt.Errorf("auth error: %w", err)
148150
return sourcev1.GitRepositoryNotReady(repository, sourcev1.AuthenticationFailedReason, err.Error()), err
@@ -259,6 +261,45 @@ func (r *GitRepositoryReconciler) sync(ctx context.Context, repository sourcev1.
259261
return sourcev1.GitRepositoryNotReady(repository, sourcev1.GitOperationFailedReason, err.Error()), err
260262
}
261263

264+
// verify PGP signature
265+
if repository.Spec.Verification != nil {
266+
commit, err := repo.CommitObject(ref.Hash())
267+
if err != nil {
268+
err = fmt.Errorf("git resolve HEAD error: %w", err)
269+
return sourcev1.GitRepositoryNotReady(repository, sourcev1.GitOperationFailedReason, err.Error()), err
270+
}
271+
272+
if commit.PGPSignature == "" {
273+
err = fmt.Errorf("PGP signature not found for commit '%s'", ref.Hash())
274+
return sourcev1.GitRepositoryNotReady(repository, sourcev1.VerificationFailedReason, err.Error()), err
275+
}
276+
277+
name := types.NamespacedName{
278+
Namespace: repository.GetNamespace(),
279+
Name: repository.Spec.Verification.SecretRef.Name,
280+
}
281+
282+
var secret corev1.Secret
283+
err = r.Client.Get(ctx, name, &secret)
284+
if err != nil {
285+
err = fmt.Errorf("PGP public keys secret error: %w", err)
286+
return sourcev1.GitRepositoryNotReady(repository, sourcev1.VerificationFailedReason, err.Error()), err
287+
}
288+
289+
var verified bool
290+
for _, bytes := range secret.Data {
291+
if _, err := commit.Verify(string(bytes)); err == nil {
292+
verified = true
293+
break
294+
}
295+
}
296+
297+
if !verified {
298+
err = fmt.Errorf("PGP signature of '%s' can't be verified", commit.Author)
299+
return sourcev1.GitRepositoryNotReady(repository, sourcev1.VerificationFailedReason, err.Error()), err
300+
}
301+
}
302+
262303
if revision == "" {
263304
revision = fmt.Sprintf("%s/%s", branch, ref.Hash().String())
264305
}
@@ -307,7 +348,6 @@ func (r *GitRepositoryReconciler) shouldResetStatus(repository sourcev1.GitRepos
307348
}
308349
}
309350

310-
// set initial status
311351
if len(repository.Status.Conditions) == 0 || resetStatus {
312352
resetStatus = true
313353
}

0 commit comments

Comments
 (0)