Flux
Flux is the GitOps reconciler running in both clusters — the on-prem production cluster and the Hetzner edge cluster, both on Talos. Its job is to keep cluster state in sync with the Git repo: any commit to k8s/clusters/<cluster>/ triggers a reconciliation, and SOPS-encrypted secrets are decrypted in-cluster by a matching age key.
Source of truth: k8s/clusters/{talos,edge}/flux-system/. The rest of this page is operational — bootstrap, the Codeberg migration playbook (kept around as a pattern for moving Git hosts), and the few CLI commands worth memorizing.
If you're self-hosting your homelab on Kubernetes with Flux and want to move off GitHub or GitLab, Codeberg is a solid open-source alternative. Here's how to switch your Flux source repository and the flux-system secret to a new Codeberg endpoint without losing your GitOps workflow.
Onboarding Flux with SSH Keys
Flux authenticates to Git over SSH using a deploy key — a dedicated keypair scoped to a single repository. The private key lives in a Kubernetes secret; the public key is registered on the Git host. This is the foundation that every reconciliation loop depends on.
1. Install the Flux CLI
curl -s https://fluxcd.io/install.sh | sudo bash
Verify the install:
flux --version
2. Check cluster prerequisites
flux check --pre
This confirms your cluster version, kubeconfig, and required CRDs are in order before you commit anything.
3. Generate an SSH keypair
ssh-keygen -t ed25519 -C "flux@homelab" -f ./flux_deploy_key -N ""
This produces two files: flux_deploy_key (private, goes into the cluster) and flux_deploy_key.pub (public, goes onto the Git host).
4. Register the public key on Codeberg
Add flux_deploy_key.pub as a deploy key on your repository under Settings → Deploy Keys. Read-only access is sufficient for most setups — only enable write access if Flux needs to push image automation commits.
5. Create the SSH secret in the cluster
flux create secret git flux-system \
--namespace=flux-system \
--url=ssh://git@codeberg.org/johnnycube/homelab.git \
--private-key-file=./flux_deploy_key
The CLI automatically runs ssh-keyscan against Codeberg and bundles the known_hosts entry alongside the private key. You can inspect what was stored:
kubectl -n flux-system get secret flux-system \
-o jsonpath='{.data.known_hosts}' | base64 -d
6. Bootstrap Flux
flux bootstrap git \
--url=ssh://git@codeberg.org/johnnycube/homelab.git \
--branch=main \
--path=k8s/clusters/talos \
--private-key-file=./flux_deploy_key
This installs the Flux controllers into flux-system, commits the manifests to the repo under k8s/clusters/talos, and sets up the initial GitRepository and Kustomization resources. From this point on, any commit to that path triggers a reconciliation automatically. The edge cluster is bootstrapped the same way with --path=k8s/clusters/edge.
Verify everything is running:
flux get all -n flux-system
You should see all sources and kustomizations reporting Ready: True.
Commit signature verification
Flux only applies what is signed. The flux-system GitRepository on both clusters carries spec.verify in mode HEAD, so before a new revision is turned into an artifact its commit signature is checked against the public keys in the Secret flux-commit-signers. A commit that is unsigned, or signed by a key that is not in the Secret, is not applied: the source reports a verification error and the cluster keeps running the last verified revision.
Three keys sign commits on the repository, all SSH (ssh-ed25519):
| Key | Signs | Fingerprint |
|---|---|---|
| Johnny's workstation key | every hand-written commit | SHA256:rMuyBddrbsXVu5UjsBnN1Q2MzdF61mDaC+QMiAYkzMc |
| Gitea's instance signing key | merges and edits made in the Gitea UI | SHA256:NPntKtXgHxAIlCGgIj8/uDkf5nBVxJhVaOHso8jtw2U |
| Renovate's signing key | every Renovate commit | SHA256:d9CnTtyn093M5IPWVSOXRNAO8wMAigqUb0wX6irKlwk |
Both the Secret and the patch that enables verification live next to the generated Flux manifests in k8s/clusters/<cluster>/flux-system/, so a rebuilt cluster verifies from its first reconciliation. The Secret holds public material only and is therefore not SOPS-encrypted; it is on the allowlist of the pre-commit check that otherwise refuses plaintext Secrets.
What this closes: the mirror on Codeberg is what Flux reads, and the mirror is only as trustworthy as whoever can push to it. With verification on, a commit has to be signed by one of the three keys to reach the cluster, regardless of which host served it. The per-app OCI artifacts are covered the same way by a different signature: the build workflow signs each one with the cosign key, and every OCIRepository verifies that signature before an artifact exists for the kustomize-controller to apply (image supply chain). Between the two sits the build workflow itself, which turns a signed commit into a signed artifact on the gitea-runner; its trust rests on the Actions secrets that hold the cosign key. Not covered are the two upstream Git sources (the Kyverno policies, the kubelet cert approver), which are pinned to a commit instead.
Adding a signer is a new .sshpub entry in commit-signers.yaml on both clusters, committed and signed by an existing key first; rotating one is the same in reverse. Mind the order: a key has to be in the Secret before it signs a commit, otherwise that commit is the one Flux refuses.
Migrating Flux GitOps to Codeberg
If you're self-hosting your homelab on Kubernetes with Flux and want to move off GitHub or GitLab, Codeberg is a solid open-source alternative. Here's how to switch your Flux source repository and the flux-system secret to a new Codeberg endpoint without losing your GitOps workflow.
Why Codeberg?
Codeberg is a non-profit, community-driven Git hosting platform built on Gitea. For homelab operators who value self-sovereignty and privacy, it's an appealing alternative to commercial Git hosts. Flux supports any Git server over SSH, so the migration is straightforward.
What You'll Actually Change
Two things need updating:
- The
flux-systemKubernetes secret — this holds your SSH private key and known_hosts entry for the remote Git server. - The
GitRepositoryresource — this tells Flux where to pull manifests from.
Step 1: Generate a Deploy Key
Start by creating a fresh ED25519 keypair:
ssh-keygen -t ed25519 -C "flux@homelab" -f ./flux_deploy_key -N ""
Then add the public key (flux_deploy_key.pub) as a deploy key on your Codeberg repo under Settings → Deploy Keys. Read-only access is sufficient unless Flux needs to push commits or tags.
Step 2: Replace the flux-system Secret
Delete the old secret and recreate it pointing at Codeberg. The flux CLI handles fetching the host key for you:
kubectl -n flux-system delete secret flux-system
flux create secret git flux-system \
--namespace=flux-system \
--url=ssh://git@codeberg.org/johnnycube/homelab.git \
--private-key-file=./flux_deploy_key
This bundles your private key and the codeberg.org known_hosts entry into a single secret. No manual ssh-keyscan step required.
Step 3: Update the GitRepository Source
Patch the existing GitRepository resource with the new URL:
kubectl -n flux-system patch gitrepository flux-system \
--type=merge \
-p '{"spec":{"url":"ssh://git@codeberg.org/johnnycube/homelab.git"}}'
Or re-declare it cleanly with the Flux CLI:
flux create source git flux-system \
--namespace=flux-system \
--url=ssh://git@codeberg.org/johnnycube/homelab.git \
--branch=main \
--secret-ref=flux-system \
--export | kubectl apply -f -
Step 4: Verify
Force a reconciliation and check the status:
flux reconcile source git flux-system -n flux-system
flux get source git -n flux-system
You're looking for Ready: True. If you hit an SSH handshake error, inspect the known_hosts value in the secret:
kubectl -n flux-system get secret flux-system \
-o jsonpath='{.data.known_hosts}' | base64 -d
There should be a line starting with codeberg.org. If it's missing, re-run the flux create secret step — it likely failed silently during host key fetch.
Summary
| What changed | Command |
|---|---|
| SSH secret | flux create secret git flux-system --url=... |
| GitRepository URL | kubectl patch gitrepository or flux create source git |
| Verification | flux reconcile source git + flux get source git |
The whole migration takes under five minutes. Once Flux is reconciling cleanly from Codeberg, you can safely rotate or delete the old deploy key on your previous Git host.