Sources: GitRepository & more
Where desired state comes from.
Every warehouse has a loading dock. A truck backs up to it, someone checks the delivery against the paperwork, signs for it, and only then does the stock move onto the shelves where workers pick from it. Nothing skips the dock. Flux runs your cluster the same way, and the source-controller is that dock: the one place where the description of what your cluster should look like arrives from the outside.
Flux is a GitOps tool, which means it keeps your running cluster in step with files stored in version control. A Git repository, not a person typing kubectl (the command-line tool for driving Kubernetes, the platform that schedules and runs your containers) commands by hand, becomes the source of truth. The files it reads are your desired state: Kubernetes manifests, which are YAML files (YAML is a plain-text format for writing configuration) declaring which Deployments, Services, and config should exist. Those files live outside the cluster. Something has to go fetch them, on a schedule, safely, and hand them to the reconcilers (the controllers that compare desired state against what is actually running and apply the difference). Fetching is the source-controller's entire job. It does nothing else.
What a Source Actually Is
A source is a small Kubernetes object (a custom resource, meaning a record type that Flux taught your cluster to understand) answering two questions: where do I fetch from, and how often. The source-controller reads that object, pulls the content, packs it into an artifact (a single gzip-compressed tar archive, the same kind of .tar.gz bundle you might download), records a digest (a cryptographic fingerprint of those exact bytes), and serves it from inside the cluster. The parts of Flux that apply changes never talk to your Git host. They pull the artifact from the source-controller and check the digest. Fetching and applying are two separate jobs done by two separate controllers, and that split is on purpose.
The most common source is a GitRepository. Here is one.
apiVersion: source.toolkit.fluxcd.io/v1kind: GitRepositorymetadata:name: paymentsnamespace: flux-systemspec:interval: 1murl: https://git.acme.internal/apps/payments.gitref:branch: main # or tag: v1.4.0, semver: ">=1.4.0", commit: <sha>, name: refs/pull/42/headsecretRef:name: git-credentialsignore: |# keep docs and CI config out of the artifact/docs//.github/
interval is how often the dock checks for a new delivery: every minute here. url and ref say which repository and which pointer to track. You can pin to a branch, an exact tag, a semver range (a version pattern like >=1.4.0 that selects the highest matching tag), a specific commit, or a raw ref name like refs/pull/42/head. secretRef names a Kubernetes Secret (an object that holds sensitive values like passwords or tokens) with the credentials for a private repo. ignore trims what lands in the artifact, so documentation and pipeline config never ship to the cluster. Smaller bundle, less to reason about.
Watch a Source Come Alive
You can write the YAML by hand or let the CLI (the flux command-line tool) generate it for you. Create it, and watch what the controller reports back.
flux create source git payments \--url=https://git.acme.internal/apps/payments.git \--branch=main \--interval=1m \--secret-ref=git-credentials
✚ generating GitRepository source► applying GitRepository source✔ GitRepository source created◎ waiting for GitRepository source reconciliation✔ GitRepository source reconciliation completed✔ fetched revision: main@sha1:a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0
That last line carries the weight. The revision is main@sha1:<commit>, the exact commit the artifact was built from. Ask for the list any time you want the current state.
flux get sources git
NAME REVISION SUSPENDED READY MESSAGEpayments main@sha1:a1b2c3d4 False True stored artifact for revision 'main@sha1:a1b2c3d4e5f6...'
READY True means the last fetch succeeded and there is a good artifact on the shelf. Look at the artifact itself to see what the reconcilers will consume.
kubectl -n flux-system get gitrepository payments \-o jsonpath='{.status.artifact}' | jq
{"digest": "sha256:3f8a1c...9d2b","lastUpdateTime": "2026-07-20T09:14:02Z","path": "gitrepository/flux-system/payments/a1b2c3d4e5f6.tar.gz","revision": "main@sha1:a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0","size": 49152,"url": "http://source-controller.flux-system.svc.cluster.local./gitrepository/flux-system/payments/a1b2c3d4e5f6.tar.gz"}
digest is the fingerprint of the tarball. url is internal only: source-controller.flux-system.svc.cluster.local is a cluster-local address, not reachable from outside. When the kustomize-controller (the part that applies plain manifests) or the helm-controller (the part that installs Helm charts) pulls this artifact, it recomputes the digest and rejects the bytes if they do not match what the status recorded. Corruption or tampering in transit gets caught before a single object is applied.
The Other Suppliers
Git is not the only place desired state can live. The source-controller speaks to four kinds of supplier, and they all drop the same kind of artifact on the same shelf, so everything downstream treats them identically.
apiVersion: source.toolkit.fluxcd.io/v1kind: OCIRepositorymetadata:name: paymentsnamespace: flux-systemspec:interval: 10murl: oci://registry.acme.internal/apps/paymentsref:tag: v1.4.0 # or semver, or a pinned digestsecretRef:name: registry-credentials
An OCIRepository pulls manifests or Helm charts packaged as an OCI artifact (Open Container Initiative, the same container-registry format your images already use, so you can store config right beside the images that run it). A HelmRepository points at a chart repository, either a classic HTTP index or an OCI registry, and feeds the helm-controller so it can install Helm charts (Helm is the package manager for Kubernetes, and a chart is one packaged application). A Bucket reads from cloud object storage, meaning Amazon S3 (Simple Storage Service), Google Cloud Storage, Azure Blob, or a self-hosted MinIO, which fits when a pipeline generates your state and drops it in a bucket instead of committing it to Git. Same interval, same artifact, same digest check.
Checking the Delivery Before You Sign for It
Here is the uncomfortable default. A GitRepository trusts whatever sits at the ref it tracks. Point it at branch main, and anyone who can push to main decides what your cluster runs. A stolen developer token, a malicious pull request that gets merged, a compromised CI job (continuous integration, the automation that builds and tests your code) with write access: any of those becomes a live change in production on the next interval. The dock signs for whatever the truck brought.
So make the dock check the paperwork. For Git, the source-controller can verify the cryptographic signature on a commit or tag against a set of public keys you trust. Developers sign commits with GPG (GNU Privacy Guard, the standard tool for signing and encrypting with public and private keys). First, load the trusted public keys into the cluster as a Secret.
gpg --export --armor [email protected] > release-key.asckubectl create secret generic flux-gpg-keys \--namespace=flux-system \--from-file=release-key.asc
secret/flux-gpg-keys created
Then tell the source to verify against them.
apiVersion: source.toolkit.fluxcd.io/v1kind: GitRepositorymetadata:name: paymentsnamespace: flux-systemspec:interval: 1murl: https://git.acme.internal/apps/payments.gitref:branch: mainsecretRef:name: git-credentialsverify:mode: HEAD # verify the signature on the tip commitsecretRef:name: flux-gpg-keys # the trusted public keys loaded above
mode HEAD checks the signature on the tip commit of the branch (HEAD is Git's name for wherever the branch currently points). Tag checks an annotated tag's signature, and TagAndHEAD checks both. Now the part that matters for a defender: what shows up when an attacker pushes an unsigned or wrongly-signed commit.
flux get sources git
NAME REVISION SUSPENDED READY MESSAGEpayments main@sha1:a1b2c3d4 False False failed to verify the signature of commit 'f9e8d7c6'
READY flipped to False, and the REVISION column still reads a1b2c3d4, the last good commit. That is the whole point. The source-controller did not build a new artifact from the bad commit. It kept serving the previous verified one, so the reconcilers keep applying known-good state while the source sits in a failed condition. The bad push never reached the cluster. The events spell out what happened and when.
kubectl -n flux-system describe gitrepository payments
Events:Type Reason Age From Message---- ------ ---- ---- -------Normal NewArtifact 18m source-controller stored artifact for revision 'main@sha1:a1b2c3d4...'Warning VerificationFailed 25s source-controller failed to verify the signature of commit 'f9e8d7c6' with any of the given key rings
Wire that Warning event to the notification-controller and it becomes a Slack message or an alert, so an unsigned push turns into a page instead of a breach. OCIRepository sources use the same idea with cosign (a tool for signing and verifying container artifacts): set verify.provider: cosign with either a public key or a keyless identity match, and an image or chart signed by the wrong identity is turned away at the dock the same way.
Try this
Run flux get sources git on a scratch host or disposable cluster and read the output against what this lesson described. Then change one input so it fails, and re-run: the error you get is the one you will meet in production.
Takeaway
The trap worth remembering here: verification guards the ref, not your whole trust chain. Check that on your own systems before you need to, because it is cheaper to find on a quiet afternoon than during an incident.