DevSecOps

Flux CD GitOps Security Practices

Hardening Flux CD deployments with multi-tenancy, RBAC, secret encryption, and image verification for secure GitOps workflows.

Michael
Kubernetes Security Engineer
5 min read

Flux CD takes a different approach to GitOps than Argo CD. Where Argo provides a centralized UI and API server, Flux runs as a set of Kubernetes controllers with no built-in UI. This design means Flux's security model is deeply tied to Kubernetes RBAC and namespace isolation rather than application-level access controls.

That architectural choice has security implications — some positive, some that require careful configuration. This guide covers how to run Flux securely in production.

Flux's Security Architecture

Flux v2 consists of several controllers:

  • Source Controller: Fetches manifests from Git, Helm, and OCI repositories.
  • Kustomize Controller: Applies Kustomize overlays and deploys manifests.
  • Helm Controller: Manages Helm releases.
  • Notification Controller: Handles webhooks and alerts.
  • Image Reflector/Automation Controllers: Monitor container registries and update manifests.

Each controller runs as a separate deployment with its own service account. This separation of concerns means you can grant each controller only the permissions it needs.

Multi-Tenancy with Namespace Isolation

Flux supports multi-tenancy through namespace-scoped resources. Each tenant gets their own namespace with Flux resources scoped to it:

apiVersion: kustomize.toolkit.fluxcd.io/v1
kind: Kustomization
metadata:
  name: team-frontend
  namespace: team-frontend
spec:
  sourceRef:
    kind: GitRepository
    name: team-frontend-repo
  path: ./manifests
  prune: true
  targetNamespace: team-frontend
  serviceAccountName: team-frontend-deployer

The serviceAccountName field is crucial. Instead of using Flux's default service account (which has broad permissions), each tenant's Kustomization uses a dedicated service account with permissions limited to their namespace:

apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
  name: team-frontend-deployer
  namespace: team-frontend
subjects:
  - kind: ServiceAccount
    name: team-frontend-deployer
    namespace: team-frontend
roleRef:
  kind: ClusterRole
  name: flux-tenant-role
  apiGroup: rbac.authorization.k8s.io

This prevents a compromised tenant repository from deploying resources into other namespaces or modifying cluster-scoped resources.

Secret Encryption with SOPS

Flux integrates natively with Mozilla SOPS for secret encryption. Secrets are encrypted in Git and decrypted only in-cluster by the Kustomize controller.

apiVersion: kustomize.toolkit.fluxcd.io/v1
kind: Kustomization
metadata:
  name: infrastructure
spec:
  decryption:
    provider: sops
    secretRef:
      name: sops-age-key

Encryption options:

  • Age: Simple, modern encryption. Generate a key pair, encrypt with the public key, store the private key as a Kubernetes Secret.
  • AWS KMS / GCP KMS / Azure Key Vault: Use cloud provider KMS for key management. The Flux controller authenticates to the KMS service to decrypt.
  • PGP: Legacy option. Works but Age is preferred for new setups.

The workflow:

  1. Developer encrypts secrets locally: sops --encrypt --age <public-key> secret.yaml > secret.enc.yaml
  2. Encrypted file is committed to Git.
  3. Flux's Kustomize controller decrypts and applies the secret in-cluster.

Plaintext secrets never exist in Git. This is non-negotiable for GitOps — if your secrets are in Git unencrypted, your Git provider has your credentials.

Image Verification

Flux can verify container image signatures before deploying them. This prevents deployment of tampered or unauthorized images:

apiVersion: image.toolkit.fluxcd.io/v1beta2
kind: ImagePolicy
metadata:
  name: app-policy
spec:
  imageRepositoryRef:
    name: app-images
  policy:
    semver:
      range: '>=1.0.0'
  verify:
    provider: cosign
    secretRef:
      name: cosign-public-key

With Cosign verification enabled, Flux refuses to update image references to unsigned images. This closes the loop on supply chain security — you sign images in CI, and Flux verifies signatures at deployment time.

Source Authentication

Flux needs to pull from Git repositories and container registries. Secure the authentication:

apiVersion: source.toolkit.fluxcd.io/v1
kind: GitRepository
metadata:
  name: production-manifests
spec:
  url: ssh://git@github.com/example/production-manifests
  secretRef:
    name: git-ssh-credentials
  interval: 5m
  ref:
    branch: main

Best practices:

  • Use SSH keys instead of HTTPS tokens for Git authentication. SSH keys can be scoped and are harder to accidentally leak.
  • Use deploy keys with read-only access. Flux only needs to read repositories, not write to them.
  • For container registries, use short-lived tokens or IRSA (IAM Roles for Service Accounts) on AWS.
  • Rotate credentials on a schedule. Kubernetes Secrets do not expire — you need an external process to rotate them.

Git Verification

Flux can verify that Git commits are signed before deploying them:

apiVersion: source.toolkit.fluxcd.io/v1
kind: GitRepository
metadata:
  name: production-manifests
spec:
  url: ssh://git@github.com/example/production-manifests
  verify:
    mode: head
    secretRef:
      name: gpg-public-keys

With verify.mode: head, Flux only deploys commits signed by trusted GPG keys. This prevents an attacker who compromises Git credentials from deploying unsigned malicious commits.

Network Policies

Flux controllers need specific network access. Lock down everything else:

apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: flux-source-controller
  namespace: flux-system
spec:
  podSelector:
    matchLabels:
      app: source-controller
  policyTypes:
    - Egress
  egress:
    - to: []
      ports:
        - port: 443
          protocol: TCP
        - port: 22
          protocol: TCP

The source controller needs outbound HTTPS (443) for Git over HTTPS and registry access, and SSH (22) for Git over SSH. The Kustomize and Helm controllers need access to the Kubernetes API server. No controller needs inbound access from outside the cluster (except the notification controller for webhooks).

Monitoring and Alerting

Flux emits Kubernetes events and Prometheus metrics. Use both:

apiVersion: notification.toolkit.fluxcd.io/v1beta3
kind: Alert
metadata:
  name: security-alerts
  namespace: flux-system
spec:
  providerRef:
    name: slack-security
  eventSeverity: error
  eventSources:
    - kind: Kustomization
      name: '*'
    - kind: HelmRelease
      name: '*'

Alert on:

  • Failed reconciliations (could indicate tampered manifests).
  • Health check failures after deployment.
  • Image policy violations (unsigned or unverified images).
  • Source fetch failures (could indicate credential compromise or repository tampering).

Least Privilege for Flux Controllers

Review the default ClusterRoleBindings that Flux creates during bootstrap. For multi-tenant setups, the default permissions are too broad:

  • Scope the Kustomize controller's permissions using impersonation and tenant-specific service accounts.
  • Restrict the Helm controller to specific namespaces if not all namespaces need Helm releases.
  • Limit the image automation controller to specific repositories and namespaces.

How Safeguard.sh Helps

Safeguard.sh integrates with your Flux-managed clusters to provide security visibility across the entire GitOps workflow. It monitors the Git repositories Flux watches for insecure manifest patterns, tracks the SBOM of every container image Flux deploys, and verifies that encryption and signature verification are properly configured. When a CVE affects a deployed container image, Safeguard.sh maps the vulnerability back through Flux's image automation to show exactly which clusters and namespaces are affected — and which Git repository needs the fix.

Never miss an update

Weekly insights on software supply chain security, delivered to your inbox.