GitOps Security Model: Separation of Duties, Drift Detection, and Rollback Controls
Problem
GitOps centralizes deployment authority in Git repositories. Tools like ArgoCD and Flux watch Git repositories and reconcile cluster state to match committed manifests. This model provides an audit trail and declarative deployments, but it concentrates power in a single control plane. Anyone with write access to the deployment repository can deploy any workload to any namespace. A malicious pull request that passes review can deploy a privileged container, mount host filesystems, or exfiltrate secrets from the cluster.
Default ArgoCD installations run with cluster-admin privileges. Flux controllers reconcile with broad permissions. Neither tool restricts what can be deployed by default. Without additional controls, GitOps transforms a Git access control problem into a Kubernetes privilege escalation problem.
The security model requires layered controls: repository-level access restrictions, ArgoCD RBAC scoped to specific namespaces and resource types, admission policies that reject dangerous manifests regardless of Git approval, drift detection to catch out-of-band changes, and rollback mechanisms that do not require emergency cluster access.
Threat Model
- Adversary: Malicious insider with repository write access, compromised developer account, or attacker who gains access to the GitOps controller’s credentials.
- Objective: Deploy malicious workloads (cryptominers, data exfiltration containers), escalate privileges within the cluster, or cause denial of service by deleting critical resources.
- Blast radius: Without namespace scoping, a single compromised ArgoCD Application can modify any resource in any namespace across the cluster.
Configuration
ArgoCD AppProject Scoping
Restrict each team to specific namespaces, resource types, and source repositories:
# argocd/appprojects/team-payments.yaml
apiVersion: argoproj.io/v1alpha1
kind: AppProject
metadata:
name: team-payments
namespace: argocd
spec:
description: "Payment service team - restricted to payments namespace"
# Only allow manifests from the team's repository
sourceRepos:
- "https://github.com/your-org/payments-manifests"
# Restrict deployable namespaces
destinations:
- namespace: payments
server: https://kubernetes.default.svc
- namespace: payments-staging
server: https://kubernetes.default.svc
# Block dangerous resource types
clusterResourceBlacklist:
- group: ""
kind: Namespace
- group: rbac.authorization.k8s.io
kind: ClusterRole
- group: rbac.authorization.k8s.io
kind: ClusterRoleBinding
# Only allow specific namespace-scoped resources
namespaceResourceWhitelist:
- group: ""
kind: ConfigMap
- group: ""
kind: Service
- group: apps
kind: Deployment
- group: apps
kind: StatefulSet
- group: networking.k8s.io
kind: Ingress
- group: autoscaling
kind: HorizontalPodAutoscaler
# Require manual sync for production (no auto-sync)
syncWindows:
- kind: deny
schedule: "* * * * *"
duration: 24h
namespaces:
- payments
manualSync: true # Allow manual sync only
ArgoCD RBAC for Team Isolation
# argocd/argocd-rbac-cm.yaml (ConfigMap data)
# Role: team-payments can only manage their own project's applications
p, role:team-payments, applications, get, team-payments/*, allow
p, role:team-payments, applications, sync, team-payments/*, allow
p, role:team-payments, applications, action/*, team-payments/*, allow
p, role:team-payments, logs, get, team-payments/*, allow
# Deny access to other projects
p, role:team-payments, applications, *, default/*, deny
# Bind SSO groups to roles
g, payments-team@company.com, role:team-payments
g, platform-admins@company.com, role:admin
# argocd/argocd-cm.yaml - disable anonymous access and configure SSO
apiVersion: v1
kind: ConfigMap
metadata:
name: argocd-cm
namespace: argocd
data:
# Disable anonymous access
users.anonymous.enabled: "false"
# OIDC configuration
oidc.config: |
name: Okta
issuer: https://company.okta.com/oauth2/default
clientID: argocd-client-id
clientSecret: $oidc.okta.clientSecret
requestedScopes: ["openid", "profile", "email", "groups"]
Flux Multi-Tenancy with Namespace Isolation
# flux-system/tenants/team-payments.yaml
apiVersion: source.toolkit.fluxcd.io/v1
kind: GitRepository
metadata:
name: payments-manifests
namespace: payments
spec:
interval: 5m
url: https://github.com/your-org/payments-manifests
ref:
branch: main
secretRef:
name: payments-git-credentials
---
apiVersion: kustomize.toolkit.fluxcd.io/v1
kind: Kustomization
metadata:
name: payments-app
namespace: payments
spec:
interval: 10m
sourceRef:
kind: GitRepository
name: payments-manifests
path: ./production
prune: true
# Restrict to own namespace only
targetNamespace: payments
# Service account with namespace-scoped permissions only
serviceAccountName: flux-payments-reconciler
# Health checks before considering sync successful
healthChecks:
- apiVersion: apps/v1
kind: Deployment
name: payments-api
namespace: payments
timeout: 5m
Create a restricted service account for each tenant:
# flux-system/tenants/team-payments-rbac.yaml
apiVersion: v1
kind: ServiceAccount
metadata:
name: flux-payments-reconciler
namespace: payments
---
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
name: flux-payments-reconciler
namespace: payments
rules:
- apiGroups: ["", "apps", "networking.k8s.io", "autoscaling"]
resources: ["deployments", "services", "configmaps", "ingresses", "horizontalpodautoscalers"]
verbs: ["get", "list", "watch", "create", "update", "patch", "delete"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
name: flux-payments-reconciler
namespace: payments
subjects:
- kind: ServiceAccount
name: flux-payments-reconciler
namespace: payments
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: Role
name: flux-payments-reconciler
Admission Policy to Block Dangerous Manifests
Even if a manifest passes Git review, enforce security constraints at admission time:
# kyverno/policies/block-privileged-from-gitops.yaml
apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
name: block-privileged-containers
spec:
validationFailureAction: Enforce
background: true
rules:
- name: deny-privileged
match:
any:
- resources:
kinds:
- Pod
validate:
message: "Privileged containers are not allowed."
pattern:
spec:
containers:
- securityContext:
privileged: "false|!(true)"
- name: deny-host-namespaces
match:
any:
- resources:
kinds:
- Pod
validate:
message: "Host namespaces (PID, network, IPC) are not allowed."
pattern:
spec:
=(hostPID): false
=(hostIPC): false
=(hostNetwork): false
Drift Detection and Alerting
# argocd/application-payments.yaml
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
name: payments-api
namespace: argocd
spec:
project: team-payments
source:
repoURL: https://github.com/your-org/payments-manifests
targetRevision: main
path: production
destination:
server: https://kubernetes.default.svc
namespace: payments
syncPolicy:
# Do NOT enable auto-sync for production
# Manual sync only, so drift is detected but not auto-corrected
automated: null
syncOptions:
- Validate=true
- PruneLast=true
Alert on drift with a Prometheus rule:
# monitoring/argocd-drift-alerts.yaml
apiVersion: monitoring.coreos.com/v1
kind: PrometheusRule
metadata:
name: argocd-drift
namespace: monitoring
spec:
groups:
- name: argocd.drift
rules:
- alert: ArgoCDApplicationOutOfSync
expr: |
argocd_app_info{sync_status="OutOfSync"} == 1
for: 10m
labels:
severity: warning
annotations:
summary: "ArgoCD application {{ $labels.name }} is out of sync"
description: "Application has been out of sync for 10 minutes. This indicates drift between Git and the cluster. Investigate whether someone made a manual change."
Git Repository Protection for Deployment Manifests
# CODEOWNERS - require platform team review for production manifests
/production/ @your-org/platform-team
/base/ @your-org/platform-team
Branch protection rules for the deployment repository:
- Require at least 2 PR reviews before merge
- CODEOWNERS review is required (not just any reviewer)
- No direct pushes to main
- Require signed commits
- Require status checks (manifest validation, policy scan) before merge
Expected Behaviour
- Each team can only deploy to their assigned namespaces using manifests from their approved repositories
- ArgoCD RBAC restricts application management to the owning team
- Cluster-scoped resources (Namespaces, ClusterRoles) cannot be created through GitOps
- Kyverno blocks privileged or host-namespace pods regardless of Git approval
- Drift is detected within 10 minutes and triggers an alert
- Production syncs require manual approval rather than auto-sync
- All manifest changes require platform team CODEOWNERS review
Trade-offs
| Control | Impact | Risk | Mitigation |
|---|---|---|---|
| AppProject namespace restriction | Teams cannot deploy cross-namespace resources | Legitimate cross-namespace needs (shared ConfigMaps) are blocked | Create shared resources through platform team’s project. |
| Disabled auto-sync for production | Manual sync adds friction to deployments | Delayed rollout of critical fixes | Allow auto-sync for staging. Production manual sync with fast-track process for incidents. |
| Kyverno admission policies | Blocks some legitimate advanced workloads | Overly strict policies prevent valid deployments | Maintain policy exceptions with documented justification. Review exceptions quarterly. |
| CODEOWNERS on manifests | Platform team reviews every deployment change | Bottleneck if platform team is unavailable | Pool of 4+ reviewers. Commit to 4-hour review SLA during business hours. |
Failure Modes
| Failure | Symptom | Detection | Recovery |
|---|---|---|---|
| ArgoCD controller compromise | Attacker deploys arbitrary workloads across all namespaces | Unexpected Application resources or sync operations in ArgoCD audit log | Revoke ArgoCD’s cluster credentials. Rotate all secrets in affected namespaces. Rebuild ArgoCD from known-good manifests. |
| Drift from manual kubectl change | Cluster state diverges from Git | ArgoCD shows OutOfSync status; Prometheus alert fires | Either sync from Git (overwriting the manual change) or commit the change to Git. Investigate who made the manual change. |
| Kyverno policy blocks legitimate deploy | ArgoCD sync fails with admission webhook denied | ArgoCD application shows sync error with Kyverno denial message | Add a policy exception for the specific workload, or modify the manifest to comply. |
| Git repository credentials leaked | Attacker pushes malicious manifests to deployment repo | Unexpected commits from unknown authors; branch protection bypass alerts | Rotate Git credentials. Review all recent commits. Force-push to revert malicious changes. Sync ArgoCD. |
When to Consider a Managed Alternative
Running ArgoCD or Flux in high-availability mode across multiple clusters requires dedicated platform engineering effort. For teams managing fewer than 3 clusters, managed Kubernetes providers with integrated GitOps features reduce the operational burden. Grafana Cloud (#108) provides alerting infrastructure for drift detection without self-managed Prometheus. For teams outgrowing self-managed ArgoCD, Akuity (the company behind ArgoCD) offers a managed control plane with enterprise RBAC and multi-cluster management.
Premium content pack: ArgoCD hardened configuration pack. Includes AppProject templates for multi-team isolation, RBAC configuration, Kyverno policies for admission control, Prometheus alerting rules for drift detection, and CODEOWNERS templates for deployment repositories.