Merge pull request #23 from fluxcd/signature-verification

gitrepository: Implement PGP signature verification
This commit is contained in:
Stefan Prodan 2020-04-14 18:47:24 +03:00 committed by GitHub
commit 1e3bc471d5
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 107 additions and 6 deletions

View File

@ -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"
) )

View File

@ -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"`

View File

@ -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

View File

@ -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

View File

@ -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
} }