Skip to content

Architecture

Component breakdown of the GitOps Toolkit, reconciliation model, multi-tenancy, and deployment topology.

Component Overview

FluxCD v2 is built on the GitOps Toolkit (GOTK) -- a set of composable Kubernetes operators, each with its own CRDs, controller binary, and release cycle. Unlike the monolithic control plane of ArgoCD, FluxCD deploys each controller as an independent Deployment in the flux-system namespace.

Controller CRDs Managed Role
source-controller GitRepository, OCIRepository, HelmRepository, HelmChart, Bucket Acquires artifacts from external sources (Git, OCI, Helm, S3). Stores them as tarballs in-cluster. Acts as the source-of-truth for all downstream controllers
kustomize-controller Kustomization Fetches source artifacts, runs Kustomize builds, applies resulting manifests to the cluster via server-side apply
helm-controller HelmRelease Reconciles Helm chart installations and upgrades. Pulls charts from source-controller. Runs helm upgrade with drift correction
notification-controller Alert, Provider, Receiver Routes events from other controllers to external systems (Slack, Discord, Teams, GitHub commit status, webhooks)
image-reflector-controller ImageRepository, ImagePolicy Scans container image registries. Evaluates semantic version policies. Exposes latest image tags for automation
image-automation-controller ImageUpdateAutomation Automates Git commits to update image tags in source manifests based on ImagePolicy evaluations

System Architecture

graph TB
    subgraph Sources["External Sources"]
        Git["Git Repository"]
        OCI["OCI Registry"]
        HelmRepo["Helm Repository"]
        S3["S3 Bucket"]
    end

    subgraph Flux["Flux Control Plane (flux-system namespace)"]
        SC["source-controller"]
        KC["kustomize-controller"]
        HC["helm-controller"]
        NC["notification-controller"]
        IRC["image-reflector-controller"]
        IAC["image-automation-controller"]
    end

    subgraph K8s["Kubernetes API Server"]
        API["API Server"]
    end

    subgraph Alerts["External Alerts"]
        Slack["Slack"]
        Teams["MS Teams"]
        GHStatus["GitHub Commit Status"]
    end

    Git --> SC
    OCI --> SC
    HelmRepo --> SC
    S3 --> SC
    SC -->|"source artifacts (tarballs)"| KC
    SC -->|"source artifacts (tarballs)"| HC
    KC -->|"server-side apply"| API
    HC -->|"helm upgrade"| API
    KC -->|"events"| NC
    HC -->|"events"| NC
    SC -->|"events"| NC
    IRC -->|"image tags"| IAC
    IAC -->|"git commit"| Git
    NC --> Slack
    NC --> Teams
    NC --> GHStatus

Reconciliation Model

Every Flux controller runs an independent reconciliation loop driven by the spec.interval field on its CRDs. There is no central scheduler.

sequenceDiagram
    participant Git as Git Repository
    participant SC as source-controller
    participant KC as kustomize-controller
    participant K8s as Kubernetes API

    loop Every spec.interval (e.g. 5m)
        SC->>Git: git fetch (or OCI pull, Helm index download)
        SC->>SC: Detect revision change
        alt New revision detected
            SC->>SC: Build artifact tarball
            SC->>K8s: Update GitRepository status (Ready, artifact URL)
        end
    end

    loop Every spec.interval (e.g. 10m)
        KC->>SC: Fetch artifact tarball
        KC->>KC: Run kustomize build
        KC->>K8s: server-side apply manifests
        KC->>KC: Detect drift between desired and live
        alt Drift detected and spec.prune=true
            KC->>K8s: Delete stale resources
        end
        KC->>K8s: Update Kustomization status (Ready/Healthy)
    end

Key reconciliation behaviors: - Interval-based: Each resource defines its own spec.interval (minimum 1m, typically 5-10m for production) - Event-driven acceleration: Webhook receivers (via notification-controller) can trigger immediate reconciliation instead of waiting for the interval - Retry on failure: spec.retryInterval controls backoff when reconciliation fails (default: equal to spec.interval) - Garbage collection: When spec.prune=true, the controller deletes resources that were previously applied but are no longer present in the source

Source Controller: Artifact Management

The source-controller is the foundation of the toolkit. It acts as an artifact proxy:

  • Git repositories: Clones the repo at the specified ref (branch, tag, semver, commit SHA), applies spec.ignore path filtering, and packages the result as a tarball
  • OCI registries: Pulls OCI artifacts and exposes them as in-cluster artifacts
  • Helm repositories: Downloads the Helm chart index and individual charts
  • S3 buckets: Fetches objects from S3-compatible storage

Monorepo Performance

Using a monorepo with 10,000+ files can degrade source-controller performance. The controller must clone and package the entire repository even with path filtering. Use spec.ignore to exclude irrelevant paths, or use a separate deploy branch to keep the artifact small. For large monorepos, consider splitting into multiple GitRepository resources with narrow spec.ignore rules, or use OCI artifacts built in CI instead.

Source filtering example for monorepos:

apiVersion: source.toolkit.fluxcd.io/v1
kind: GitRepository
metadata:
  name: monorepo
  namespace: flux-system
spec:
  interval: 1m
  url: https://github.com/org/monorepo
  ref:
    branch: main
  ignore: |
    # exclude everything
    /*
    # include only the deploy directory
    !/deploy/

Deployment Topology

Single-Cluster (Default)

All controllers run in the flux-system namespace. The flux bootstrap command installs the toolkit and creates a Kustomization resource that syncs the cluster state from a Git repository.

graph LR
    Git["Git Repo"] --> SC["source-controller"]
    SC --> KC["kustomize-controller"]
    SC --> HC["helm-controller"]
    KC --> K8s["Kubernetes API"]
    HC --> K8s

Multi-Cluster (Hub and Spoke)

FluxCD supports multi-cluster deployments without a central management server. Two patterns exist:

  1. Hub and Spoke: One cluster runs Flux and reconciles resources on remote clusters using kubeconfig secrets
  2. Per-Cluster Bootstrap: Each cluster runs its own Flux instance, bootstrapped independently from the same or different Git repositories
graph TB
    subgraph Hub["Hub Cluster"]
        FluxHub["Flux (flux-system)"]
    end
    subgraph Spoke1["Spoke Cluster A"]
        K8sA["Kubernetes API"]
    end
    subgraph Spoke2["Spoke Cluster B"]
        K8sB["Kubernetes API"]
    end
    subgraph Git["Git Organization"]
        RepoA["cluster-a manifest repo"]
        RepoB["cluster-b manifest repo"]
    end

    FluxHub -->|"kubeconfig secret"| K8sA
    FluxHub -->|"kubeconfig secret"| K8sB
    RepoA --> FluxHub
    RepoB --> FluxHub

Per-Cluster Bootstrap vs Hub-Spoke

The per-cluster bootstrap pattern is recommended for security isolation: the Flux instance of each cluster only has credentials for its own Git repository. The hub-spoke pattern is simpler to manage but requires the hub cluster to hold kubeconfig secrets for all spoke clusters.

Multi-Tenancy

FluxCD implements multi-tenancy through Kubernetes-native mechanisms:

  • Namespace isolation: Each tenant team deploys to their own namespace with their own GitRepository and Kustomization CRDs
  • ServiceAccount impersonation: The kustomize-controller and helm-controller impersonate the ServiceAccount specified in .spec.serviceAccountName and inherit the RBAC permissions of that account
  • Cross-namespace reference blocking: Set --no-cross-namespace-refs=true on all controllers to prevent tenants from referencing sources or secrets in other namespaces
  • Remote base blocking: Set --no-remote-bases=true on kustomize-controller to prevent Kustomize remote bases from arbitrary URLs
# Kustomize patches for multi-tenancy lockdown
patches:
  - patch: |
      - op: add
        path: /spec/template/spec/containers/0/args/-
        value: --no-cross-namespace-refs=true
    target:
      kind: Deployment
      name: "(kustomize-controller|helm-controller|notification-controller)"
  - patch: |
      - op: add
        path: /spec/template/spec/containers/0/args/-
        value: --default-service-account=default
    target:
      kind: Deployment
      name: "(kustomize-controller|helm-controller)"

Storage Model

FluxCD is stateless by design. All persistent state lives in Kubernetes resources:

Data Storage
Source artifacts PersistentVolumeClaim (source-controller artifact storage)
Reconciliation state status subresource on each CRD (GitRepository, Kustomization, HelmRelease, and more)
Git credentials Secret resources in flux-system namespace
Tenant ServiceAccounts ServiceAccount + RoleBinding per tenant namespace
Controller logs Stdout (collected by cluster logging)

How It Works

GitOps Toolkit (GOTK) controllers, pull-based reconciliation, and image automation pipeline.

Controller Reconciliation Model

flowchart TB
    subgraph GOTK["GitOps Toolkit Controllers"]
        SC["Source Controller\n(acquires artifacts)"]
        KC["Kustomize Controller\n(applies Kustomizations)"]
        HC["Helm Controller\n(applies HelmReleases)"]
        NC["Notification Controller\n(alerts)"]
    end

    subgraph Sources["Sources"]
        GitRepo["GitRepository\n(poll interval: 1min)"]
        HelmRepo["HelmRepository"]
        OCIRepo["OCIRepository"]
    end

    subgraph Cluster["Kubernetes Cluster"]
        API["K8s API Server"]
        Resources["Deployed Resources"]
    end

    GitRepo --> SC
    HelmRepo --> SC
    OCIRepo --> SC
    SC -->|"artifact ready\nevent"| KC
    SC -->|"chart ready\nevent"| HC
    KC -->|"Server-Side Apply"| API
    HC -->|"Helm SDK"| API
    API --> Resources

Image Automation Pipeline

sequenceDiagram
    participant Registry as Container Registry
    participant IAC as Image Reflector Controller
    participant IUA as Image Update Automation
    participant Git as Git Repository
    participant SC as Source Controller
    participant KC as Kustomize Controller

    IAC->>Registry: Poll for new image tags
    IAC->>IAC: Match tag pattern (semver, regex)
    IAC->>IUA: New image: myapp:v2.1.0
    IUA->>Git: Push commit (update image tag in YAML)
    SC->>Git: Detect new commit
    SC->>KC: Trigger reconciliation
    KC->>KC: Apply updated manifests

Source Controller Artifact Management

The source controller fetches and stores artifacts (Git commits, Helm charts, OCI blobs) as gzip-compressed tarballs in its .spec.artifact storage:

  1. Acquire: Clone/fetch the Git repository or pull the Helm chart
  2. Verify: Validate checksum (SHA256) and GPG signature (if configured)
  3. Package: Create a tar.gz artifact
  4. Store: Write to the configured storage (local disk or S3-compatible bucket)
  5. Notify: Emit a Kubernetes Event and update the source resource status with the artifact URL

Other controllers watch for artifact readiness events and begin their reconciliation when a new artifact is available.

Server-Side Apply

Both the Kustomize and Helm controllers use Kubernetes Server-Side Apply (SSA) to apply resources:

  • SSA tracks field ownership via metadata.managedFields. This prevents accidental overwrites by other controllers
  • Conflicts are detected when two controllers manage the same field -- Flux will not overwrite fields it does not own
  • This enables safe coexistence with other tools (for example, cert-manager injecting annotations, external-dns modifying DNS records)

Reconciliation Triggers

Flux controllers reconcile on:

Trigger Controller Mechanism
Poll interval Source Controller spec.interval (default: 1m for GitRepository)
Git webhook Source Controller HTTP endpoint receives push events, bypasses poll wait
Artifact ready Kustomize/Helm Controller Watches source resource status changes
Manual trigger Any flux reconcile kustomization <name> --force
Dependency Kustomize Controller spec.dependsOn -- waits for upstream Kustomization to be Ready

Sources


Benchmarks

Scope

Performance characteristics, scaling limits, and resource consumption for FluxCD.

Reconciliation Performance

Scale GitRepositories HelmReleases Kustomizations Reconcile Interval
Small 10 20 10 1m
Medium 50 100 50 5m
Large 200 500 200 10m

Resource Consumption

Component CPU (idle) CPU (reconcile) Memory
source-controller 50m 500m 256Mi
kustomize-controller 50m 300m 256Mi
helm-controller 50m 500m 512Mi
image-automation 20m 200m 128Mi

Scaling Limits

Dimension Tested Recommended Bottleneck
GitRepositories per cluster 500 200 Source controller memory
HelmReleases per cluster 1,000 500 Helm controller CPU
Kustomizations per cluster 500 200 API server load

Sourcing Status

Unsourced Performance Data

The performance numbers in this document are estimated from vendor documentation, community benchmarks, and engineering judgment. They do not represent controlled benchmarks with documented test conditions. Specific hardware configurations, software versions, and test methodologies were not recorded.

Use these figures as rough guidance only. For production capacity planning, run your own benchmarks against your specific workload and infrastructure.

Sources


Security

Authentication model, multi-tenancy authorization, encryption, source verification, and hardening for FluxCD.

FluxCD delegates security almost entirely to Kubernetes-native mechanisms. Unlike ArgoCD, which ships its own Casbin RBAC engine and Dex SSO server, FluxCD has no built-in identity provider or authorization system. Security is enforced through Kubernetes RBAC, ServiceAccount impersonation, and controller-level flags.

Authentication

Controller-to-Cluster Authentication

Each Flux controller runs under a ServiceAccount in the flux-system namespace. The controller uses the credentials of that ServiceAccount to communicate with the Kubernetes API server. There is no separate authentication layer.

  • source-controller: Authenticates to Git repositories via SSH keys, HTTPS tokens, or basic auth credentials stored as Kubernetes Secrets
  • kustomize-controller / helm-controller: Impersonates the ServiceAccount specified in .spec.serviceAccountName on each Kustomization or HelmRelease resource and inherits the RBAC permissions of that account
  • notification-controller: Uses its own ServiceAccount to post events and read Alert/Provider CRDs

Git Repository Authentication

FluxCD supports multiple authentication methods for private Git repositories:

Method Secret Fields Use Case
SSH (ed25519/rsa) identity (private key), known_hosts Most common for GitHub/GitLab private repos
HTTPS with token username, password (personal access token) Azure DevOps, GitHub App tokens
Basic auth username, password Self-hosted Git servers
OIDC (GitHub App) clientID, clientSecret, idToken Short-lived tokens from GitHub OIDC
# SSH authentication for a private GitRepository
apiVersion: source.toolkit.fluxcd.io/v1
kind: GitRepository
metadata:
  name: private-repo
  namespace: flux-system
spec:
  interval: 5m
  url: ssh://[email protected]/org/private-repo.git
  ref:
    branch: main
  secretRef:
    name: ssh-credentials

Authorization (Multi-Tenancy and RBAC)

FluxCD implements multi-tenancy through Kubernetes RBAC and controller-level isolation flags. There is no separate RBAC engine.

ServiceAccount Impersonation

The kustomize-controller and helm-controller impersonate the ServiceAccount specified in the .spec.serviceAccountName of each resource. This means:

  • Each tenant team creates a dedicated ServiceAccount in their namespace
  • The controller inherits only the permissions granted to that ServiceAccount via Role and RoleBinding
  • If no serviceAccountName is specified, the controller uses the --default-service-account flag value (set to default in multi-tenant setups)

Multi-Tenancy Lockdown

To enforce hard multi-tenancy isolation, apply these controller flags during bootstrap:

Flag Controllers Effect
--no-cross-namespace-refs=true kustomize-controller, helm-controller, notification-controller, image-reflector-controller, image-automation-controller Prevents tenants from referencing Flux resources (sources, secrets) in other namespaces
--no-remote-bases=true kustomize-controller Prevents Kustomize from fetching remote bases from arbitrary URLs
--default-service-account=default kustomize-controller, helm-controller Falls back to the default ServiceAccount in the tenant namespace when no serviceAccountName is specified
# Kustomize patches applied during flux bootstrap
patches:
  - patch: |
      - op: add
        path: /spec/template/spec/containers/0/args/-
        value: --no-cross-namespace-refs=true
    target:
      kind: Deployment
      name: "(kustomize-controller|helm-controller|notification-controller)"
  - patch: |
      - op: add
        path: /spec/template/spec/containers/0/args/-
        value: --no-remote-bases=true
    target:
      kind: Deployment
      name: "kustomize-controller"
  - patch: |
      - op: add
        path: /spec/template/spec/containers/0/args/-
        value: --default-service-account=default
    target:
      kind: Deployment
      name: "(kustomize-controller|helm-controller)"

Tenant Isolation Model

graph TB
    subgraph Flux["flux-system namespace"]
        SC["source-controller"]
        KC["kustomize-controller"]
    end

    subgraph TenantA["team-a namespace"]
        SA_A["ServiceAccount: team-a"]
        RB_A["RoleBinding"]
        GitRepoA["GitRepository"]
        KustA["Kustomization"]
    end

    subgraph TenantB["team-b namespace"]
        SA_B["ServiceAccount: team-b"]
        RB_B["RoleBinding"]
        GitRepoB["GitRepository"]
        KustB["Kustomization"]
    end

    GitRepoA --> SC
    GitRepoB --> SC
    KustA -->|"impersonates SA: team-a"| KC
    KustB -->|"impersonates SA: team-b"| KC
    RB_A -.->|"grants limited permissions"| SA_A
    RB_B -.->|"grants limited permissions"| SA_B

With --no-cross-namespace-refs=true, the team-a Kustomization cannot reference the team-b GitRepository, and vice versa.

Encryption and Secret Management

SOPS Integration

FluxCD natively supports Mozilla SOPS (Secrets OPerationS) for encrypting secrets in Git. The kustomize-controller decrypts SOPS-encrypted files at reconciliation time.

Supported encryption backends:

Backend Configuration Use Case
Age age.agekey in a K8s Secret Recommended. Simple, modern encryption
PGP/GPG pgp in .sops.yaml Legacy. Broader key management ecosystem
AWS KMS kms in .sops.yaml AWS-native deployments
GCP KMS gcp_kms in .sops.yaml GCP-native deployments
Azure Key Vault azure_kv in .sops.yaml Azure-native deployments

The decryption key is stored as a Kubernetes Secret in the namespace of the controller:

apiVersion: v1
kind: Secret
metadata:
  name: sops-age
  namespace: flux-system
stringData:
  age.agekey: |
    AGE-SECRET-KEY-1...

Configure the kustomize-controller to use the decryption key via Kustomize patches:

apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
patches:
  - patch: |
      apiVersion: apps/v1
      kind: Deployment
      metadata:
        name: kustomize-controller
      spec:
        template:
          spec:
            containers:
              - name: manager
                env:
                  - name: SOPS_AGE_KEY_FILE
                    value: /sops-age/age.agekey
                volumeMounts:
                  - name: sops-age
                    mountPath: /sops-age
            volumes:
              - name: sops-age
                secret:
                  secretName: sops-age

Key Management

The SOPS decryption private key must be stored as a Kubernetes Secret in the cluster. Protect this Secret with RBAC (grant read access only to kustomize-controller) and consider using an external secret management system to inject it.

Source Verification

FluxCD can verify the authenticity and integrity of source artifacts before reconciling them.

GPG Commit Signing Verification

For GitRepository resources, FluxCD can verify that commits are signed by a trusted GPG key:

apiVersion: source.toolkit.fluxcd.io/v1
kind: GitRepository
metadata:
  name: verified-repo
  namespace: flux-system
spec:
  interval: 5m
  url: https://github.com/org/secure-repo
  ref:
    branch: main
  verify:
    mode: HEAD
    secretRef:
      name: gpg-public-keys

Cosign OCI Artifact Verification

For OCIRepository resources, FluxCD can verify signatures using Sigstore Cosign:

apiVersion: source.toolkit.fluxcd.io/v1
kind: OCIRepository
metadata:
  name: verified-oci
  namespace: flux-system
spec:
  interval: 5m
  url: oci://ghcr.io/org/manifests
  ref:
    semver: ">=1.0.0"
  verify:
    provider: cosign
    secretRef:
      name: cosign-pub

Container Image Verification

All Flux controller images are signed using Cosign with GitHub OIDC (keyless signing). Verify at deployment time:

cosign verify ghcr.io/fluxcd/source-controller:v1.0.0 \
  --certificate-identity-regexp='^https://github\.com/fluxcd/.*$' \
  --certificate-oidc-issuer=https://token.actions.githubusercontent.com

Enforce verification at the cluster level using a Kyverno policy that rejects unsigned Flux images.

Hardening Checklist

  • Enable --no-cross-namespace-refs=true on all controllers for multi-tenancy
  • Enable --no-remote-bases=true on kustomize-controller
  • Set --default-service-account=default on kustomize-controller and helm-controller
  • Use SOPS or External Secrets Operator for secrets management (never commit plaintext)
  • Enable GPG or Cosign source verification on all GitRepository and OCIRepository resources
  • Store SOPS decryption keys in K8s Secrets with strict RBAC (only controller SA has read)
  • Apply NetworkPolicies to restrict controller egress (source-controller to Git hosts only)
  • Run controllers with SecurityContext (non-root, read-only root filesystem, drop all capabilities)
  • Verify Flux controller image signatures with Cosign or Kyverno admission policies
  • Use short-lived SSH keys or OIDC tokens for Git repository authentication
  • Keep flux-system namespace restricted. Tenants must not have access to it