diff --git a/api/v1alpha1/condition_types.go b/api/v1alpha1/condition_types.go index d62c563a..71e63097 100644 --- a/api/v1alpha1/condition_types.go +++ b/api/v1alpha1/condition_types.go @@ -65,4 +65,8 @@ const ( // AuthenticationFailedReason represents the fact that a given secret does not // have the required fields or the provided credentials do not match. AuthenticationFailedReason string = "AuthenticationFailed" + + // VerificationFailedReason represents the fact that the cryptographic provenance + // verification for the source failed. + VerificationFailedReason string = "VerificationFailed" ) diff --git a/api/v1alpha1/gitrepository_types.go b/api/v1alpha1/gitrepository_types.go index e63b95ea..c2560564 100644 --- a/api/v1alpha1/gitrepository_types.go +++ b/api/v1alpha1/gitrepository_types.go @@ -44,6 +44,10 @@ type GitRepositorySpec struct { // master branch. // +optional Reference *GitRepositoryRef `json:"ref,omitempty"` + + // Verify OpenPGP signature for the commit that HEAD points to. + // +optional + Verification *GitRepositoryVerification `json:"verify,omitempty"` } // GitRepositoryRef defines the git ref used for pull and checkout operations. @@ -66,7 +70,17 @@ type GitRepositoryRef struct { Commit string `json:"commit"` } -// GitRepositoryStatus defines the observed state of the GitRepository. +// GitRepositoryVerification defines the OpenPGP signature verification process. +type GitRepositoryVerification struct { + // Mode describes what git object should be verified, currently ('head'). + // +kubebuilder:validation:Enum=head + Mode string `json:"mode"` + + // The secret name containing the public keys of all trusted git authors. + SecretRef corev1.LocalObjectReference `json:"secretRef,omitempty"` +} + +// GitRepositoryStatus defines the observed state of a Git repository. type GitRepositoryStatus struct { // +optional Conditions []SourceCondition `json:"conditions,omitempty"` diff --git a/api/v1alpha1/zz_generated.deepcopy.go b/api/v1alpha1/zz_generated.deepcopy.go index 57439315..cabc171b 100644 --- a/api/v1alpha1/zz_generated.deepcopy.go +++ b/api/v1alpha1/zz_generated.deepcopy.go @@ -129,6 +129,11 @@ func (in *GitRepositorySpec) DeepCopyInto(out *GitRepositorySpec) { *out = new(GitRepositoryRef) **out = **in } + if in.Verification != nil { + in, out := &in.Verification, &out.Verification + *out = new(GitRepositoryVerification) + **out = **in + } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new GitRepositorySpec. @@ -168,6 +173,22 @@ func (in *GitRepositoryStatus) DeepCopy() *GitRepositoryStatus { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *GitRepositoryVerification) DeepCopyInto(out *GitRepositoryVerification) { + *out = *in + out.SecretRef = in.SecretRef +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new GitRepositoryVerification. +func (in *GitRepositoryVerification) DeepCopy() *GitRepositoryVerification { + if in == nil { + return nil + } + out := new(GitRepositoryVerification) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *HelmChart) DeepCopyInto(out *HelmChart) { *out = *in diff --git a/config/crd/bases/source.fluxcd.io_gitrepositories.yaml b/config/crd/bases/source.fluxcd.io_gitrepositories.yaml index 6cd6e1c4..7fdd6fec 100644 --- a/config/crd/bases/source.fluxcd.io_gitrepositories.yaml +++ b/config/crd/bases/source.fluxcd.io_gitrepositories.yaml @@ -86,12 +86,34 @@ spec: description: The repository URL, can be a HTTP or SSH address. pattern: ^(http|https|ssh):// type: string + verify: + description: Verify OpenPGP signature for the commit that HEAD points + to. + properties: + mode: + description: Mode describes what git object should be verified, + currently ('head'). + enum: + - head + type: string + secretRef: + description: The secret name containing the public keys of all trusted + git authors. + properties: + name: + description: 'Name of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + TODO: Add other useful fields. apiVersion, kind, uid?' + type: string + type: object + required: + - mode + type: object required: - interval - url type: object status: - description: GitRepositoryStatus defines the observed state of the GitRepository. + description: GitRepositoryStatus defines the observed state of a Git repository. properties: artifact: description: Artifact represents the output of the last successful repository diff --git a/controllers/gitrepository_controller.go b/controllers/gitrepository_controller.go index 13fab7d4..b76fb1b1 100644 --- a/controllers/gitrepository_controller.go +++ b/controllers/gitrepository_controller.go @@ -36,7 +36,7 @@ import ( "sigs.k8s.io/controller-runtime/pkg/client" sourcev1 "github.com/fluxcd/source-controller/api/v1alpha1" - internalgit "github.com/fluxcd/source-controller/internal/git" + intgit "github.com/fluxcd/source-controller/internal/git" ) // GitRepositoryReconciler reconciles a GitRepository object @@ -76,10 +76,11 @@ func (r *GitRepositoryReconciler) Reconcile(req ctrl.Request) (ctrl.Result, erro log.Error(err, "artifacts GC failed") } - // try git clone + // try git sync syncedRepo, err := r.sync(ctx, *repo.DeepCopy()) if err != nil { log.Error(err, "Git repository sync failed") + return ctrl.Result{Requeue: true}, err } // update status @@ -128,6 +129,7 @@ func (r *GitRepositoryReconciler) sync(ctx context.Context, repository sourcev1. } } + // determine auth method var auth transport.AuthMethod if repository.Spec.SecretRef != nil { name := types.NamespacedName{ @@ -142,7 +144,7 @@ func (r *GitRepositoryReconciler) sync(ctx context.Context, repository sourcev1. return sourcev1.GitRepositoryNotReady(repository, sourcev1.AuthenticationFailedReason, err.Error()), err } - method, cleanup, err := internalgit.AuthMethodFromSecret(repository.Spec.URL, secret) + method, cleanup, err := intgit.AuthMethodFromSecret(repository.Spec.URL, secret) if err != nil { err = fmt.Errorf("auth error: %w", err) return sourcev1.GitRepositoryNotReady(repository, sourcev1.AuthenticationFailedReason, err.Error()), err @@ -259,6 +261,45 @@ func (r *GitRepositoryReconciler) sync(ctx context.Context, repository sourcev1. return sourcev1.GitRepositoryNotReady(repository, sourcev1.GitOperationFailedReason, err.Error()), err } + // verify PGP signature + if repository.Spec.Verification != nil { + commit, err := repo.CommitObject(ref.Hash()) + if err != nil { + err = fmt.Errorf("git resolve HEAD error: %w", err) + return sourcev1.GitRepositoryNotReady(repository, sourcev1.GitOperationFailedReason, err.Error()), err + } + + if commit.PGPSignature == "" { + err = fmt.Errorf("PGP signature not found for commit '%s'", ref.Hash()) + return sourcev1.GitRepositoryNotReady(repository, sourcev1.VerificationFailedReason, err.Error()), err + } + + name := types.NamespacedName{ + Namespace: repository.GetNamespace(), + Name: repository.Spec.Verification.SecretRef.Name, + } + + var secret corev1.Secret + err = r.Client.Get(ctx, name, &secret) + if err != nil { + err = fmt.Errorf("PGP public keys secret error: %w", err) + return sourcev1.GitRepositoryNotReady(repository, sourcev1.VerificationFailedReason, err.Error()), err + } + + var verified bool + for _, bytes := range secret.Data { + if _, err := commit.Verify(string(bytes)); err == nil { + verified = true + break + } + } + + if !verified { + err = fmt.Errorf("PGP signature of '%s' can't be verified", commit.Author) + return sourcev1.GitRepositoryNotReady(repository, sourcev1.VerificationFailedReason, err.Error()), err + } + } + if revision == "" { revision = fmt.Sprintf("%s/%s", branch, ref.Hash().String()) } @@ -307,7 +348,6 @@ func (r *GitRepositoryReconciler) shouldResetStatus(repository sourcev1.GitRepos } } - // set initial status if len(repository.Status.Conditions) == 0 || resetStatus { resetStatus = true }