Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions api/v1/gitrepository_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -288,6 +288,13 @@ type GitRepositoryStatus struct {
// +optional
ObservedSparseCheckout []string `json:"observedSparseCheckout,omitempty"`

// SourceVerificationFingerprint is the fingerprint of the public keys used
// to verify the signature of the Git object(s) for the current Artifact.
// It is used to detect changes to the verification policy, such as a key
// rotation, that require the current revision to be verified again.
// +optional
SourceVerificationFingerprint string `json:"sourceVerificationFingerprint,omitempty"`

// SourceVerificationMode is the last used verification mode indicating
// which Git object(s) have been verified.
// +optional
Expand Down
2 changes: 1 addition & 1 deletion api/v1beta1/zz_generated.deepcopy.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
Expand Up @@ -477,6 +477,13 @@ spec:
items:
type: string
type: array
sourceVerificationFingerprint:
description: |-
SourceVerificationFingerprint is the fingerprint of the public keys used
to verify the signature of the Git object(s) for the current Artifact.
It is used to detect changes to the verification policy, such as a key
rotation, that require the current revision to be verified again.
type: string
sourceVerificationMode:
description: |-
SourceVerificationMode is the last used verification mode indicating
Expand Down
15 changes: 15 additions & 0 deletions docs/api/v1/source.md
Original file line number Diff line number Diff line change
Expand Up @@ -2413,6 +2413,21 @@ produce the current Artifact.</p>
</tr>
<tr>
<td>
<code>sourceVerificationFingerprint</code><br>
<em>
string
</em>
</td>
<td>
<em>(Optional)</em>
<p>SourceVerificationFingerprint is the fingerprint of the public keys used
to verify the signature of the Git object(s) for the current Artifact.
It is used to detect changes to the verification policy, such as a key
rotation, that require the current revision to be verified again.</p>
</td>
</tr>
<tr>
<td>
<code>sourceVerificationMode</code><br>
<em>
<a href="#source.toolkit.fluxcd.io/v1.GitVerificationMode">
Expand Down
10 changes: 10 additions & 0 deletions docs/spec/v1/gitrepositories.md
Original file line number Diff line number Diff line change
Expand Up @@ -1342,6 +1342,16 @@ mode in spec](#verification). The verification status is applicable only to the
latest Git repository revision used to successfully build and store an
artifact.

### Source Verification Fingerprint

The source-controller reports a fingerprint of the public keys it used to verify
the Git object(s) in the GitRepository's
`.status.sourceVerificationFingerprint`. The fingerprint is derived from the key
material in the referenced Secret and does not depend on the Secret name or the
Secret data key names. It is used by the controller to detect a change in the
verification policy, such as a key rotation, that requires the current revision
to be verified again even when its revision did not change.

### Observed Generation

The source-controller reports an [observed generation][typical-status-properties]
Expand Down
79 changes: 66 additions & 13 deletions internal/controller/gitrepository_controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import (
"net/url"
"os"
"path/filepath"
"sort"
"strings"
"time"

Expand All @@ -33,6 +34,7 @@ import (
"github.com/fluxcd/pkg/runtime/logger"
"github.com/fluxcd/pkg/runtime/secrets"
"github.com/go-git/go-git/v5/plumbing/transport"
"github.com/opencontainers/go-digest"
ssh "golang.org/x/crypto/ssh"
corev1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/runtime"
Expand Down Expand Up @@ -592,7 +594,9 @@ func (r *GitRepositoryReconciler) reconcileSource(ctx context.Context, sp *patch
// reconciliation can be skipped if other configurations have not changed.
if !git.IsConcreteCommit(*commit) {
// Check if the content config contributing to the artifact has changed.
if !gitContentConfigChanged(obj, includes) {
// A change to the verification policy (e.g. a key rotation) also
// requires a new verification of the unchanged revision.
if !gitContentConfigChanged(obj, includes) && !r.verificationPolicyChanged(ctx, obj) {
ge := serror.NewGeneric(
fmt.Errorf("no changes since last reconciliation: observed revision '%s'",
commitReference(obj, commit)), sourcev1.GitOperationSucceedReason,
Expand Down Expand Up @@ -1112,6 +1116,64 @@ func (r *GitRepositoryReconciler) fetchIncludes(ctx context.Context, obj *source
return &artifacts, nil
}

// verificationKeys returns the PGP key rings and SSH authorized keys contained
// in the given Secret. Data entries with an SSH public key suffix are treated as
// authorized keys, entries with a PGP public key suffix (or no known suffix) as
// PGP key rings.
func verificationKeys(secret *corev1.Secret) (keyRings, authorizedKeys []string) {
for k, v := range secret.Data {
switch {
case strings.HasSuffix(k, publicKeySSHSuffix):
authorizedKeys = append(authorizedKeys, string(v))
case strings.HasSuffix(k, publicKeyPGPSuffix):
keyRings = append(keyRings, string(v))
default:
// Provide fallback to support previous undocumented behavior
keyRings = append(keyRings, string(v))
}
}
return keyRings, authorizedKeys
}

// verificationFingerprint returns a stable fingerprint of the public keys in
// the given Secret, independent of the Secret name or the data key names. It is
// used to detect a change in the verification policy, e.g. a key rotation, that
// requires the current revision to be verified again.
func verificationFingerprint(secret *corev1.Secret) string {
keyRings, authorizedKeys := verificationKeys(secret)
sort.Strings(keyRings)
sort.Strings(authorizedKeys)

var b strings.Builder
for _, k := range keyRings {
b.WriteString("pgp:")
b.WriteString(k)
b.WriteByte(0)
}
for _, k := range authorizedKeys {
b.WriteString("ssh:")
b.WriteString(k)
b.WriteByte(0)
}
return digest.Canonical.FromString(b.String()).String()
}

// verificationPolicyChanged returns true if the public keys trusted for
// verification differ from the ones used for the last successful verification,
// or if the current policy can not be determined. A changed policy requires the
// current revision to be verified again, even if it did not change.
func (r *GitRepositoryReconciler) verificationPolicyChanged(ctx context.Context, obj *sourcev1.GitRepository) bool {
if obj.Spec.Verification == nil || obj.Spec.Verification.Mode == "" {
return false
}
secret, err := r.getSecret(ctx, obj.Spec.Verification.SecretRef.Name, obj.GetNamespace())
if err != nil {
// Return true so the full reconciliation surfaces the error.
return true
}
return verificationFingerprint(secret) != obj.Status.SourceVerificationFingerprint
}

// verifySignature verifies the signature of the given Git commit and/or its referencing tag
// depending on the verification mode specified on the object.
// If the signature can not be verified or the verification fails, it records
Expand All @@ -1124,6 +1186,7 @@ func (r *GitRepositoryReconciler) verifySignature(ctx context.Context, obj *sour
// observations if there is none
if obj.Spec.Verification == nil || obj.Spec.Verification.Mode == "" {
obj.Status.SourceVerificationMode = nil
obj.Status.SourceVerificationFingerprint = ""
conditions.Delete(obj, sourcev1.SourceVerifiedCondition)
return sreconcile.ResultSuccess, nil
}
Expand All @@ -1143,18 +1206,7 @@ func (r *GitRepositoryReconciler) verifySignature(ctx context.Context, obj *sour
return sreconcile.ResultEmpty, e
}

var keyRings []string
var authorizedKeys []string
for k, v := range secret.Data {
if strings.HasSuffix(k, publicKeySSHSuffix) {
authorizedKeys = append(authorizedKeys, string(v))
} else if strings.HasSuffix(k, publicKeyPGPSuffix) {
keyRings = append(keyRings, string(v))
} else {
// Provide fallback to support previous undocumented behavior
keyRings = append(keyRings, string(v))
}
}
keyRings, authorizedKeys := verificationKeys(secret)

var message strings.Builder
if obj.Spec.Verification.VerifyTag() {
Expand Down Expand Up @@ -1217,6 +1269,7 @@ func (r *GitRepositoryReconciler) verifySignature(ctx context.Context, obj *sour
reason := meta.SucceededReason
mode := obj.Spec.Verification.GetMode()
obj.Status.SourceVerificationMode = &mode
obj.Status.SourceVerificationFingerprint = verificationFingerprint(secret)
conditions.MarkTrue(obj, sourcev1.SourceVerifiedCondition, reason, "%s", message.String())
r.eventLogf(ctx, obj, eventv1.EventTypeTrace, reason, "%s", message.String())
return sreconcile.ResultSuccess, nil
Expand Down
Loading
Loading