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:
- Developer encrypts secrets locally:
sops --encrypt --age <public-key> secret.yaml > secret.enc.yaml - Encrypted file is committed to Git.
- 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.