Merge pull request #23 from fluxcd/signature-verification
gitrepository: Implement PGP signature verification
This commit is contained in:
commit
1e3bc471d5
|
@ -65,4 +65,8 @@ const (
|
||||||
// AuthenticationFailedReason represents the fact that a given secret does not
|
// AuthenticationFailedReason represents the fact that a given secret does not
|
||||||
// have the required fields or the provided credentials do not match.
|
// have the required fields or the provided credentials do not match.
|
||||||
AuthenticationFailedReason string = "AuthenticationFailed"
|
AuthenticationFailedReason string = "AuthenticationFailed"
|
||||||
|
|
||||||
|
// VerificationFailedReason represents the fact that the cryptographic provenance
|
||||||
|
// verification for the source failed.
|
||||||
|
VerificationFailedReason string = "VerificationFailed"
|
||||||
)
|
)
|
||||||
|
|
|
@ -44,6 +44,10 @@ type GitRepositorySpec struct {
|
||||||
// master branch.
|
// master branch.
|
||||||
// +optional
|
// +optional
|
||||||
Reference *GitRepositoryRef `json:"ref,omitempty"`
|
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.
|
// GitRepositoryRef defines the git ref used for pull and checkout operations.
|
||||||
|
@ -66,7 +70,17 @@ type GitRepositoryRef struct {
|
||||||
Commit string `json:"commit"`
|
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 {
|
type GitRepositoryStatus struct {
|
||||||
// +optional
|
// +optional
|
||||||
Conditions []SourceCondition `json:"conditions,omitempty"`
|
Conditions []SourceCondition `json:"conditions,omitempty"`
|
||||||
|
|
|
@ -129,6 +129,11 @@ func (in *GitRepositorySpec) DeepCopyInto(out *GitRepositorySpec) {
|
||||||
*out = new(GitRepositoryRef)
|
*out = new(GitRepositoryRef)
|
||||||
**out = **in
|
**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.
|
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new GitRepositorySpec.
|
||||||
|
@ -168,6 +173,22 @@ func (in *GitRepositoryStatus) DeepCopy() *GitRepositoryStatus {
|
||||||
return out
|
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.
|
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||||
func (in *HelmChart) DeepCopyInto(out *HelmChart) {
|
func (in *HelmChart) DeepCopyInto(out *HelmChart) {
|
||||||
*out = *in
|
*out = *in
|
||||||
|
|
|
@ -86,12 +86,34 @@ spec:
|
||||||
description: The repository URL, can be a HTTP or SSH address.
|
description: The repository URL, can be a HTTP or SSH address.
|
||||||
pattern: ^(http|https|ssh)://
|
pattern: ^(http|https|ssh)://
|
||||||
type: string
|
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:
|
required:
|
||||||
- interval
|
- interval
|
||||||
- url
|
- url
|
||||||
type: object
|
type: object
|
||||||
status:
|
status:
|
||||||
description: GitRepositoryStatus defines the observed state of the GitRepository.
|
description: GitRepositoryStatus defines the observed state of a Git repository.
|
||||||
properties:
|
properties:
|
||||||
artifact:
|
artifact:
|
||||||
description: Artifact represents the output of the last successful repository
|
description: Artifact represents the output of the last successful repository
|
||||||
|
|
|
@ -36,7 +36,7 @@ import (
|
||||||
"sigs.k8s.io/controller-runtime/pkg/client"
|
"sigs.k8s.io/controller-runtime/pkg/client"
|
||||||
|
|
||||||
sourcev1 "github.com/fluxcd/source-controller/api/v1alpha1"
|
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
|
// 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")
|
log.Error(err, "artifacts GC failed")
|
||||||
}
|
}
|
||||||
|
|
||||||
// try git clone
|
// try git sync
|
||||||
syncedRepo, err := r.sync(ctx, *repo.DeepCopy())
|
syncedRepo, err := r.sync(ctx, *repo.DeepCopy())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error(err, "Git repository sync failed")
|
log.Error(err, "Git repository sync failed")
|
||||||
|
return ctrl.Result{Requeue: true}, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// update status
|
// update status
|
||||||
|
@ -128,6 +129,7 @@ func (r *GitRepositoryReconciler) sync(ctx context.Context, repository sourcev1.
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// determine auth method
|
||||||
var auth transport.AuthMethod
|
var auth transport.AuthMethod
|
||||||
if repository.Spec.SecretRef != nil {
|
if repository.Spec.SecretRef != nil {
|
||||||
name := types.NamespacedName{
|
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
|
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 {
|
if err != nil {
|
||||||
err = fmt.Errorf("auth error: %w", err)
|
err = fmt.Errorf("auth error: %w", err)
|
||||||
return sourcev1.GitRepositoryNotReady(repository, sourcev1.AuthenticationFailedReason, err.Error()), 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
|
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 == "" {
|
if revision == "" {
|
||||||
revision = fmt.Sprintf("%s/%s", branch, ref.Hash().String())
|
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 {
|
if len(repository.Status.Conditions) == 0 || resetStatus {
|
||||||
resetStatus = true
|
resetStatus = true
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue