Skip to content

Commit 95cddf6

Browse files
committed
Reverify unchanged GitRepository on key rotation
The optimized same-revision checkout returns a partial commit and, when the content config and verification mode are unchanged, returns before verifySignature. Since the verification policy was not part of the decision, rotating spec.verify.secretRef or its key material kept an existing SourceVerified=True condition for the new generation, allowing a revoked signing key to remain trusted for the advertised artifact. Record a fingerprint of the verification public keys in the status on success, and require a full checkout and re-verification when the fingerprint of the keys in the referenced Secret changes. The fingerprint depends only on the key material, not on the Secret name or the data key names. Signed-off-by: Matheus Pimenta <matheuscscp@gmail.com> Assisted-by: opencode/deepseek-v4.1-flash
1 parent 143c11a commit 95cddf6

7 files changed

Lines changed: 324 additions & 14 deletions

File tree

‎api/v1/gitrepository_types.go‎

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -288,6 +288,13 @@ type GitRepositoryStatus struct {
288288
// +optional
289289
ObservedSparseCheckout []string `json:"observedSparseCheckout,omitempty"`
290290

291+
// SourceVerificationFingerprint is the fingerprint of the public keys used
292+
// to verify the signature of the Git object(s) for the current Artifact.
293+
// It is used to detect changes to the verification policy, such as a key
294+
// rotation, that require the current revision to be verified again.
295+
// +optional
296+
SourceVerificationFingerprint string `json:"sourceVerificationFingerprint,omitempty"`
297+
291298
// SourceVerificationMode is the last used verification mode indicating
292299
// which Git object(s) have been verified.
293300
// +optional

‎api/v1beta1/zz_generated.deepcopy.go‎

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

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

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -477,6 +477,13 @@ spec:
477477
items:
478478
type: string
479479
type: array
480+
sourceVerificationFingerprint:
481+
description: |-
482+
SourceVerificationFingerprint is the fingerprint of the public keys used
483+
to verify the signature of the Git object(s) for the current Artifact.
484+
It is used to detect changes to the verification policy, such as a key
485+
rotation, that require the current revision to be verified again.
486+
type: string
480487
sourceVerificationMode:
481488
description: |-
482489
SourceVerificationMode is the last used verification mode indicating

‎docs/api/v1/source.md‎

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2413,6 +2413,21 @@ produce the current Artifact.</p>
24132413
</tr>
24142414
<tr>
24152415
<td>
2416+
<code>sourceVerificationFingerprint</code><br>
2417+
<em>
2418+
string
2419+
</em>
2420+
</td>
2421+
<td>
2422+
<em>(Optional)</em>
2423+
<p>SourceVerificationFingerprint is the fingerprint of the public keys used
2424+
to verify the signature of the Git object(s) for the current Artifact.
2425+
It is used to detect changes to the verification policy, such as a key
2426+
rotation, that require the current revision to be verified again.</p>
2427+
</td>
2428+
</tr>
2429+
<tr>
2430+
<td>
24162431
<code>sourceVerificationMode</code><br>
24172432
<em>
24182433
<a href="#source.toolkit.fluxcd.io/v1.GitVerificationMode">

‎docs/spec/v1/gitrepositories.md‎

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1342,6 +1342,16 @@ mode in spec](#verification). The verification status is applicable only to the
13421342
latest Git repository revision used to successfully build and store an
13431343
artifact.
13441344

1345+
### Source Verification Fingerprint
1346+
1347+
The source-controller reports a fingerprint of the public keys it used to verify
1348+
the Git object(s) in the GitRepository's
1349+
`.status.sourceVerificationFingerprint`. The fingerprint is derived from the key
1350+
material in the referenced Secret and does not depend on the Secret name or the
1351+
Secret data key names. It is used by the controller to detect a change in the
1352+
verification policy, such as a key rotation, that requires the current revision
1353+
to be verified again even when its revision did not change.
1354+
13451355
### Observed Generation
13461356

13471357
The source-controller reports an [observed generation][typical-status-properties]

‎internal/controller/gitrepository_controller.go‎

Lines changed: 66 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import (
2323
"net/url"
2424
"os"
2525
"path/filepath"
26+
"sort"
2627
"strings"
2728
"time"
2829

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

1119+
// verificationKeys returns the PGP key rings and SSH authorized keys contained
1120+
// in the given Secret. Data entries with an SSH public key suffix are treated as
1121+
// authorized keys, entries with a PGP public key suffix (or no known suffix) as
1122+
// PGP key rings.
1123+
func verificationKeys(secret *corev1.Secret) (keyRings, authorizedKeys []string) {
1124+
for k, v := range secret.Data {
1125+
switch {
1126+
case strings.HasSuffix(k, publicKeySSHSuffix):
1127+
authorizedKeys = append(authorizedKeys, string(v))
1128+
case strings.HasSuffix(k, publicKeyPGPSuffix):
1129+
keyRings = append(keyRings, string(v))
1130+
default:
1131+
// Provide fallback to support previous undocumented behavior
1132+
keyRings = append(keyRings, string(v))
1133+
}
1134+
}
1135+
return keyRings, authorizedKeys
1136+
}
1137+
1138+
// verificationFingerprint returns a stable fingerprint of the public keys in
1139+
// the given Secret, independent of the Secret name or the data key names. It is
1140+
// used to detect a change in the verification policy, e.g. a key rotation, that
1141+
// requires the current revision to be verified again.
1142+
func verificationFingerprint(secret *corev1.Secret) string {
1143+
keyRings, authorizedKeys := verificationKeys(secret)
1144+
sort.Strings(keyRings)
1145+
sort.Strings(authorizedKeys)
1146+
1147+
var b strings.Builder
1148+
for _, k := range keyRings {
1149+
b.WriteString("pgp:")
1150+
b.WriteString(k)
1151+
b.WriteByte(0)
1152+
}
1153+
for _, k := range authorizedKeys {
1154+
b.WriteString("ssh:")
1155+
b.WriteString(k)
1156+
b.WriteByte(0)
1157+
}
1158+
return digest.Canonical.FromString(b.String()).String()
1159+
}
1160+
1161+
// verificationPolicyChanged returns true if the public keys trusted for
1162+
// verification differ from the ones used for the last successful verification,
1163+
// or if the current policy can not be determined. A changed policy requires the
1164+
// current revision to be verified again, even if it did not change.
1165+
func (r *GitRepositoryReconciler) verificationPolicyChanged(ctx context.Context, obj *sourcev1.GitRepository) bool {
1166+
if obj.Spec.Verification == nil || obj.Spec.Verification.Mode == "" {
1167+
return false
1168+
}
1169+
secret, err := r.getSecret(ctx, obj.Spec.Verification.SecretRef.Name, obj.GetNamespace())
1170+
if err != nil {
1171+
// Return true so the full reconciliation surfaces the error.
1172+
return true
1173+
}
1174+
return verificationFingerprint(secret) != obj.Status.SourceVerificationFingerprint
1175+
}
1176+
11151177
// verifySignature verifies the signature of the given Git commit and/or its referencing tag
11161178
// depending on the verification mode specified on the object.
11171179
// If the signature can not be verified or the verification fails, it records
@@ -1124,6 +1186,7 @@ func (r *GitRepositoryReconciler) verifySignature(ctx context.Context, obj *sour
11241186
// observations if there is none
11251187
if obj.Spec.Verification == nil || obj.Spec.Verification.Mode == "" {
11261188
obj.Status.SourceVerificationMode = nil
1189+
obj.Status.SourceVerificationFingerprint = ""
11271190
conditions.Delete(obj, sourcev1.SourceVerifiedCondition)
11281191
return sreconcile.ResultSuccess, nil
11291192
}
@@ -1143,18 +1206,7 @@ func (r *GitRepositoryReconciler) verifySignature(ctx context.Context, obj *sour
11431206
return sreconcile.ResultEmpty, e
11441207
}
11451208

1146-
var keyRings []string
1147-
var authorizedKeys []string
1148-
for k, v := range secret.Data {
1149-
if strings.HasSuffix(k, publicKeySSHSuffix) {
1150-
authorizedKeys = append(authorizedKeys, string(v))
1151-
} else if strings.HasSuffix(k, publicKeyPGPSuffix) {
1152-
keyRings = append(keyRings, string(v))
1153-
} else {
1154-
// Provide fallback to support previous undocumented behavior
1155-
keyRings = append(keyRings, string(v))
1156-
}
1157-
}
1209+
keyRings, authorizedKeys := verificationKeys(secret)
11581210

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

0 commit comments

Comments
 (0)