Skip to content
Draft
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
28 changes: 28 additions & 0 deletions api/v1/gitrepository_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -246,6 +246,14 @@ type GitRepositoryVerification struct {
// and SSH public keys must be stored under keys with the .sshpub suffix.
// +required
SecretRef meta.LocalObjectReference `json:"secretRef"`

// ForwardOnly, when true, rejects a revision that is not a descendant of
// the last verified revision recorded in
// .status.sourceVerificationRevision, such as a branch force-pushed to an
// older commit or a tag moved backwards. Not supported with
// .spec.ref.semver.
// +optional
ForwardOnly bool `json:"forwardOnly,omitempty"`
}

// GitRepositoryStatus records the observed state of a Git repository.
Expand Down Expand Up @@ -300,7 +308,15 @@ type GitRepositoryStatus struct {
// +optional
SourceVerificationMode *GitVerificationMode `json:"sourceVerificationMode,omitempty"`

// SourceVerificationRevision is the last revision whose Git object(s)
// were successfully verified. It is the baseline for forward-only
// verification: when .spec.verify.forwardOnly is set, a new revision
// must be a descendant of this revision.
// +optional
SourceVerificationRevision string `json:"sourceVerificationRevision,omitempty"`

meta.ReconcileRequestStatus `json:",inline"`
meta.ForceRequestStatus `json:",inline"`
}

const (
Expand Down Expand Up @@ -335,6 +351,18 @@ func (in *GitRepository) GetArtifact() *meta.Artifact {
return in.Status.Artifact
}

// GetLastHandledReconcileRequest returns the last handled reconcile request
// value recorded in the status.
func (in *GitRepository) GetLastHandledReconcileRequest() string {
return in.Status.GetLastHandledReconcileRequest()
}

// GetLastHandledForceRequestStatus returns a pointer to the last handled
// force request value recorded in the status.
func (in *GitRepository) GetLastHandledForceRequestStatus() *string {
return &in.Status.LastHandledForceAt
}

// GetProvider returns the Git authentication provider.
func (v *GitRepository) GetProvider() string {
if v.Spec.Provider == "" {
Expand Down
1 change: 1 addition & 0 deletions api/v1/zz_generated.deepcopy.go

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

21 changes: 21 additions & 0 deletions config/crd/bases/source.toolkit.fluxcd.io_gitrepositories.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -214,6 +214,14 @@ spec:
Verification specifies the configuration to verify the Git commit
signature(s).
properties:
forwardOnly:
description: |-
ForwardOnly, when true, rejects a revision that is not a descendant of
the last verified revision recorded in
.status.sourceVerificationRevision, such as a branch force-pushed to an
older commit or a tag moved backwards. Not supported with
.spec.ref.semver.
type: boolean
mode:
default: HEAD
description: |-
Expand Down Expand Up @@ -414,6 +422,12 @@ spec:
- url
type: object
type: array
lastHandledForceAt:
description: |-
LastHandledForceAt holds the value of the most recent
force request value, so a change of the annotation value
can be detected.
type: string
lastHandledReconcileAt:
description: |-
LastHandledReconcileAt holds the value of the most recent
Expand Down Expand Up @@ -489,6 +503,13 @@ spec:
SourceVerificationMode is the last used verification mode indicating
which Git object(s) have been verified.
type: string
sourceVerificationRevision:
description: |-
SourceVerificationRevision is the last revision whose Git object(s)
were successfully verified. It is the baseline for forward-only
verification: when .spec.verify.forwardOnly is set, a new revision
must be a descendant of this revision.
type: string
type: object
type: object
served: true
Expand Down
46 changes: 46 additions & 0 deletions docs/api/v1/source.md
Original file line number Diff line number Diff line change
Expand Up @@ -2464,6 +2464,21 @@ which Git object(s) have been verified.</p>
</tr>
<tr>
<td>
<code>sourceVerificationRevision</code><br>
<em>
string
</em>
</td>
<td>
<em>(Optional)</em>
<p>SourceVerificationRevision is the last revision whose Git object(s)
were successfully verified. It is the baseline for forward-only
verification: when .spec.verify.forwardOnly is set, a new revision
must be a descendant of this revision.</p>
</td>
</tr>
<tr>
<td>
<code>ReconcileRequestStatus</code><br>
<em>
<a href="https://pkg.go.dev/github.com/fluxcd/pkg/apis/meta#ReconcileRequestStatus">
Expand All @@ -2477,6 +2492,21 @@ github.com/fluxcd/pkg/apis/meta.ReconcileRequestStatus
</p>
</td>
</tr>
<tr>
<td>
<code>ForceRequestStatus</code><br>
<em>
<a href="https://pkg.go.dev/github.com/fluxcd/pkg/apis/meta#ForceRequestStatus">
github.com/fluxcd/pkg/apis/meta.ForceRequestStatus
</a>
</em>
</td>
<td>
<p>
(Members of <code>ForceRequestStatus</code> are embedded into this type.)
</p>
</td>
</tr>
</tbody>
</table>
</div>
Expand Down Expand Up @@ -2531,6 +2561,22 @@ authors. PGP public keys must be stored under keys with the .asc suffix,
and SSH public keys must be stored under keys with the .sshpub suffix.</p>
</td>
</tr>
<tr>
<td>
<code>forwardOnly</code><br>
<em>
bool
</em>
</td>
<td>
<em>(Optional)</em>
<p>ForwardOnly, when true, rejects a revision that is not a descendant of
the last verified revision recorded in
.status.sourceVerificationRevision, such as a branch force-pushed to an
older commit or a tag moved backwards. Not supported with
.spec.ref.semver.</p>
</td>
</tr>
</tbody>
</table>
</div>
Expand Down
132 changes: 131 additions & 1 deletion docs/spec/v1/gitrepositories.md
Original file line number Diff line number Diff line change
Expand Up @@ -630,7 +630,7 @@ spec:
### Verification

`.spec.verify` is an optional field to enable the verification of Git commit
signatures. The field offers two subfields:
signatures. The field offers the following subfields:

- `.mode`, to specify what Git object(s) should be verified. Supported
values are:
Expand All @@ -649,6 +649,10 @@ signatures. The field offers two subfields:
keys must be stored under keys with the `.sshpub` suffix. Keys without a
recognized suffix are treated as PGP key rings for backward compatibility.

- `.forwardOnly`, to additionally require that every new revision descends from
the last verified revision. Defaults to `false`. See [forward-only
verification](#forward-only-verification).

```yaml
---
apiVersion: source.toolkit.fluxcd.io/v1
Expand Down Expand Up @@ -741,6 +745,90 @@ PGP verification reports the PGP key ID in the success message (e.g.
`5982D0279C227FFD`), while SSH verification reports the SHA256 fingerprint
(e.g. `SHA256:uNiVztksCsDhcc0u9e8BgrJXVGDaf6s7kOsTmI9N7sM`).

#### Forward-only verification

`.spec.verify.forwardOnly` is an optional boolean field. When set to `true`, a
revision that passes signature verification is only accepted if it descends
from the last verified revision, as recorded in
[`.status.sourceVerificationRevision`](#source-verification-revision). This
rejects a branch that was force-pushed to an older commit, a tag that was moved
to an older commit, and any rewritten history, even when the resulting HEAD
carries a valid signature of a trusted author.

```yaml
---
apiVersion: source.toolkit.fluxcd.io/v1
kind: GitRepository
metadata:
name: podinfo
namespace: default
spec:
interval: 1m
url: https://github.com/stefanprodan/podinfo
ref:
branch: master
verify:
mode: HEAD
forwardOnly: true
secretRef:
name: pgp-public-keys
```

The check compares history and nothing else: any signed descendant is accepted,
including a work-in-progress commit of a trusted author that someone with push
access fast-forwarded the branch to. It does not replace code review, and it
does not protect against a compromised trusted key. A legitimate rollback is
done with `git revert`, which produces a new signed descendant and is accepted
without further action.

The signature of the HEAD and/or tag object is verified before the history is
inspected, so an unsigned revision is rejected without additional Git
operations.

To inspect the history, the controller clones the last 100 commits of the
reference instead of only its tip whenever the reference changed. If the last
verified revision is not among them, the clone is discarded and the last 500
commits are cloned into a fresh directory. If it is still not found, the
revision is rejected. Each clone runs within [`.spec.timeout`](#timeout), so a
reconciliation of a large repository takes longer than before and may take up
to twice the timeout; raise the timeout if a clone does not complete in time. A revision checked out by `.spec.ref.commit`, or by a
`.spec.ref.name` that is neither a branch nor a tag (for example
`refs/pull/420/head`), is cloned with its full history as before, and the whole
history is inspected. A rebased pull request branch is therefore rejected.

`.spec.ref.semver` is not supported together with `forwardOnly`, because
release tags cut from release branches are frequently not descendants of each
other. The GitRepository is marked as stalled with reason
`InvalidVerificationMode`.

`.status.sourceVerificationRevision` is recorded on every successful
verification, whether `forwardOnly` is set or not, so enabling the field on an
existing object takes effect from the next reconciliation on. When `forwardOnly`
is enabled and no such revision has been recorded yet, the controller verifies
the current revision again, records it as the baseline, and emits the event
`forward-only baseline established at '<revision>'`. The baseline is lost, and
the next revision is accepted without a history check, when the object is
recreated, when it is restored without its status, or when `.spec.verify` is
removed and the repository is checked out again.

When a revision is rejected, the controller sets `SourceVerified=False` and
`Ready=False` with reason `NonFastForwardRevision`, emits a warning event, and
keeps the current Artifact and baseline. Controllers consuming the Artifact,
such as kustomize-controller, keep applying the last verified revision. The
reconciliation is retried with an exponential backoff; for a branch or tag
reference that does not change in the meantime, the retries do not clone the
repository again.

To accept a revision that does not descend from the baseline, for example after
history was deliberately rewritten, verify the revision on the Git server and
request a [forced reconciliation](#triggering-a-reconcile). The revision on the
server at that moment is verified, recorded as the new baseline, and the event
`forward-only baseline reset to '<revision>' by force request` is emitted. The
annotations are not part of the manifest in Git, so this also works for a
GitRepository that is itself applied by Flux, such as `flux-system`, where
pinning `.spec.ref.commit` would require a commit in the rejected history or
would be reverted by kustomize-controller.

### Ignore

`.spec.ignore` is an optional field to specify rules in [the `.gitignore`
Expand Down Expand Up @@ -939,6 +1027,24 @@ Using `flux`:
flux reconcile source git <repository-name>
```

When [forward-only verification](#forward-only-verification) has rejected a
revision that should be accepted anyway, the baseline can be reset by
additionally annotating the GitRepository with `reconcile.fluxcd.io/forceAt`
set to the same value. Both annotations must be set in the same operation. The
revision on the remote at the time of the reconciliation is verified and
recorded as the new baseline, and the value is reported in
[`.status.lastHandledForceAt`](#last-handled-force-at). The value is consumed
by the reconciliation it triggers; if that reconciliation fails before the
revision is verified, for example because the clone times out, request it again
with a new value:

```sh
TOKEN="$(date +%s)"; \
kubectl annotate --field-manager=flux-client-side-apply --overwrite gitrepository/<repository-name> \
"reconcile.fluxcd.io/requestedAt=$TOKEN" \
"reconcile.fluxcd.io/forceAt=$TOKEN"
```

### Waiting for `Ready`

When a change is applied, it is possible to wait for the GitRepository to reach
Expand Down Expand Up @@ -1212,6 +1318,8 @@ factors:
non-existing Secret.
- A specified Include is unavailable.
- The verification of the Git commit signature failed.
- The revision does not descend from the last verified revision while
[forward-only verification](#forward-only-verification) is enabled.
- The credentials in the referenced Secret are invalid.
- The GitRepository spec contains a generic misconfiguration.
- A storage related failure when storing the artifact.
Expand All @@ -1237,6 +1345,10 @@ the following attributes is added to the GitRepository's `.status.conditions`:
- `status: "False"`
- `reason: Failed`

When [forward-only verification](#forward-only-verification) rejects a
revision, the `reason` is `NonFastForwardRevision` and the message names the
rejected revision and the last verified revision.

While the GitRepository has one or more of these Conditions, the controller
will continue to attempt to produce an Artifact for the resource with an
exponential backoff, until it succeeds and the GitRepository is marked as
Expand Down Expand Up @@ -1356,6 +1468,16 @@ 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.

### Source Verification Revision

The source-controller reports the last revision whose Git object(s) were
successfully verified in the GitRepository's
`.status.sourceVerificationRevision`, in the same format as the [Artifact
revision](#artifact). It is recorded whenever verification succeeds and serves
as the baseline for [forward-only verification](#forward-only-verification):
when `.spec.verify.forwardOnly` is set, a new revision must descend from it.
The field is cleared when `.spec.verify` is removed.

### Observed Generation

The source-controller reports an [observed generation][typical-status-properties]
Expand All @@ -1372,5 +1494,13 @@ annotation value it acted on in the `.status.lastHandledReconcileAt` field.
For practical information about this field, see [triggering a
reconcile](#triggering-a-reconcile).

### Last Handled Force At

The source-controller reports the last `reconcile.fluxcd.io/forceAt`
annotation value it handled in the `.status.lastHandledForceAt` field. See
[triggering a reconcile](#triggering-a-reconcile) for how the annotation is
used to reset the [forward-only verification](#forward-only-verification)
baseline.

[typical-status-properties]: https://github.com/kubernetes/community/blob/master/contributors/devel/sig-architecture/api-conventions.md#typical-status-properties
[kstatus-spec]: https://github.com/kubernetes-sigs/cli-utils/tree/master/pkg/kstatus
2 changes: 2 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -412,3 +412,5 @@ require (
)

retract v0.32.0 // Refers to incorrect ./api version.

replace github.com/fluxcd/pkg/git => github.com/ruslan-shaydullin/pkg/git v0.55.1-0.20261015144952-74aba21a4653
4 changes: 2 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -360,8 +360,6 @@ github.com/fluxcd/pkg/auth v0.62.0 h1:ZhrkfoLTaH5Q095sNGLZ47zLYw/AHw4nVnPAspGwT/
github.com/fluxcd/pkg/auth v0.62.0/go.mod h1:46lKYjDvx5Q7fsq09ZC1BxXJcstTOfm2X/NFLLFn7wA=
github.com/fluxcd/pkg/cache v0.17.0 h1:197ST7bSJD2Nw4G01y5cMjtWQpwQsyjpl2ox4MaGeU8=
github.com/fluxcd/pkg/cache v0.17.0/go.mod h1:OW3fncHhPnPZFZav5FdEa//cyMHSndI0JS+IM69LXOU=
github.com/fluxcd/pkg/git v0.55.0 h1:jMdW0uAYuzcwIL5rJq5pmDd+jSrgOe6eIOZfdrcAvO8=
github.com/fluxcd/pkg/git v0.55.0/go.mod h1:s9AK9ZEG9CtAgkcX4QhQphLsaDBTje32tT5V3knpj1I=
github.com/fluxcd/pkg/gittestserver v0.32.0 h1:y1wbnEaOXpEYqAxGMxcsXZTH6W4QjFyTj6m1d1ytjY0=
github.com/fluxcd/pkg/gittestserver v0.32.0/go.mod h1:Cp4hpAePUXnSaeESHKitnvORCn+xT4fOKuZljkRBqVo=
github.com/fluxcd/pkg/helmtestserver v0.44.0 h1:VaJegpUI6KXxGQkiVoMAwzVkvWHNmqdEk77Y74S2vRE=
Expand Down Expand Up @@ -838,6 +836,8 @@ github.com/rs/xid v1.6.0 h1:fV591PaemRlL6JfRxGDEPl69wICngIQ3shQtzfy2gxU=
github.com/rs/xid v1.6.0/go.mod h1:7XoLgs4eV+QndskICGsho+ADou8ySMSjJKDIan90Nz0=
github.com/rubenv/sql-migrate v1.8.1 h1:EPNwCvjAowHI3TnZ+4fQu3a915OpnQoPAjTXCGOy2U0=
github.com/rubenv/sql-migrate v1.8.1/go.mod h1:BTIKBORjzyxZDS6dzoiw6eAFYJ1iNlGAtjn4LGeVjS8=
github.com/ruslan-shaydullin/pkg/git v0.55.1-0.20261015144952-74aba21a4653 h1:k7kScfYftV0S46GpcjXU7kduFYXV2HCgMcVYUsDX5W8=
github.com/ruslan-shaydullin/pkg/git v0.55.1-0.20261015144952-74aba21a4653/go.mod h1:s9AK9ZEG9CtAgkcX4QhQphLsaDBTje32tT5V3knpj1I=
github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/ryanuber/go-glob v1.0.0 h1:iQh3xXAumdQ+4Ufa5b25cRpC5TYKlno6hsv6Cb3pkBk=
Expand Down
Loading