How to Secure Argo CD: A Complete Hardening Guide for Production Clusters
Secure Argo CD by enforcing TLS 1.2, hardening RBAC policies, isolating the repo-server, disabling unused config tools, and enabling security audit logging with CWE identifiers.
Argo CD is a GitOps continuous delivery platform that runs as a tightly coupled set of Kubernetes services within the argoproj/argo-cd repository. To secure Argo CD effectively, operators must address four pillars: authentication, authorization, transport-level protection, and data confidentiality. The following guide provides concrete configuration steps derived directly from the source code and official documentation to harden your deployment against credential leakage, unauthorized cluster modifications, and denial-of-service attacks.
Authentication and Authorization Controls
Argo CD protects API access through JSON Web Tokens (JWT) and external identity providers. All API calls require a valid token issued either by the Argo CD server for the local admin account or through an integrated OIDC/Dex provider.
The JWT’s groups claim is matched against the RBAC policy defined in a ConfigMap. According to docs/operator-manual/rbac.md, rules grant or deny specific API verbs on resources based on these claims. While the default policy grants cluster-admin privileges, you should narrow this to the least-privilege set of namespaces and resources required for your GitOps workflows.
Transport Layer Security (TLS) Configuration
All intra-service traffic between argocd-server, argocd-repo-server, and argocd-application-controller is encrypted via TLS. You can enforce TLS 1.2 minimum via the argocd-cmd-params-cm ConfigMap:
apiVersion: v1
kind: ConfigMap
metadata:
name: argocd-cmd-params-cm
namespace: argocd
data:
server.tls.minversion: "1.2"
Redis communication defaults to plain HTTP but can be secured using the --redis-tls flags passed to the server components, ensuring encrypted data-in-transit for cached application states.
Repository Server Isolation and Hardening
The argocd-repo-server is designed to run without any Kubernetes privileges and never stores credentials long-term. It maintains only a read-only clone of allow-listed repositories, keeping the attack surface minimal.
Disable unused config-management tools to remove code-execution vectors. If you do not use Helm, disable it in argocd-cmd-params-cm:
apiVersion: v1
kind: ConfigMap
metadata:
name: argocd-cmd-params-cm
namespace: argocd
data:
repo.server.enable.helm: "false"
repo.server.enable.kustomize: "true"
To prevent memory exhaustion attacks from maliciously large manifests, set the reposerver.max.combined.directory.manifests.size option. This caps the total size of raw JSON/YAML files an application may load, as documented in docs/operator-manual/security.md:
data:
reposerver.max.combined.directory.manifests.size: "5M"
Cluster Privilege Minimization
By default, the Argo CD controller uses a cluster-admin role. You must edit argocd-manager-role and argocd-application-controller-role.yaml to restrict write access to only your managed namespaces.
A minimal read-only ClusterRole configuration looks like this:
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
name: argocd-manager-readonly
rules:
- apiGroups: [""]
resources: ["pods", "services", "configmaps", "secrets"]
verbs: ["get", "list", "watch"]
- apiGroups: ["apps"]
resources: ["deployments", "statefulsets", "daemonsets"]
verbs: ["get", "list", "watch"]
Apply this with kubectl apply and bind it to the argocd-manager ServiceAccount to reduce the blast radius of a compromised controller.
Data Protection and Audit Logging
Sensitive data redaction is automatic. All API payloads and logs redact cluster credentials, Git tokens, OAuth2 client secrets, and Kubernetes Secret values. The admin password is stored as a bcrypt hash in manifests/base/config/argocd-secret.yaml:
apiVersion: v1
kind: Secret
metadata:
name: argocd-secret
namespace: argocd
type: Opaque
data:
admin.password: "<bcrypt-hash-base64>"
admin.passwordMtime: "2024-07-09T00:00:00Z"
Rotate credentials regularly using the CLI:
argocd account update-password --account admin --current-password <old> --new-password <new>
Every API request is logged with a security field and optional CWE identifier for security-related events. Enable structured JSON logging for SIEM integration:
data:
server.loglevel: "debug"
server.logformat: "json"
Webhook and Network Hardening
Webhook payloads are treated as untrusted input. They trigger only a fast-track refresh of the affected application without executing payload content. Enforce rate-limiting on external webhook traffic at your ingress controller level, and restrict the argocd-repo-server ServiceAccount to read-only access for specific Git repositories.
Step-by-Step Security Implementation
Combine these hardening measures into a single configuration. Your argocd-cmd-params-cm should include TLS enforcement, disabled tools, memory limits, and logging settings:
apiVersion: v1
kind: ConfigMap
metadata:
name: argocd-cmd-params-cm
namespace: argocd
data:
server.tls.minversion: "1.2"
repo.server.enable.helm: "false"
repo.server.enable.kustomize: "true"
reposerver.max.combined.directory.manifests.size: "5M"
server.loglevel: "debug"
server.logformat: "json"
Apply this configuration with kubectl apply -f <file>.yaml and verify the changes by checking the argocd-server deployment logs for the loaded parameters.
Summary
- Enforce TLS 1.2 minimum on all Argo CD components via
argocd-cmd-params-cmto prevent man-in-the-middle attacks. - Harden RBAC by replacing the default cluster-admin role with minimal ClusterRoles that grant only
get,list, andwatchverbs where possible. - Isolate the repo-server by disabling unused tools like Helm and setting
reposerver.max.combined.directory.manifests.sizeto prevent memory DoS. - Protect credentials by relying on bcrypt-hashed passwords in
argocd-secret.yamland rotating the admin password regularly. - Enable audit logging using JSON format and the
securityfield to capture CWE-tagged events for SIEM analysis.
Frequently Asked Questions
How does Argo CD store the admin password securely?
Argo CD stores the admin password as a bcrypt hash in the argocd-secret resource located in manifests/base/config/argocd-secret.yaml. When you update the password via argocd account update-password, the CLI automatically generates the bcrypt hash and base64-encodes it for Kubernetes storage, ensuring plaintext credentials never persist in the cluster.
Can I use OIDC instead of the built-in admin account?
Yes, Argo CD supports external OIDC providers and Dex for Single Sign-On (SSO). Configure your provider in the argocd-cm ConfigMap, and ensure you do not use --insecure-skip-server-verification in production. Tokens issued by OIDC providers expire according to the provider’s configuration and can be revoked through the provider’s console, offering superior control over the default static admin token.
What is the default RBAC policy and why should I change it?
The default RBAC policy grants the application controller cluster-admin privileges via argocd-application-controller-role.yaml. This allows Argo CD to manage resources across all namespaces but violates the principle of least privilege. You should customize the ClusterRole to restrict write access (create, update, delete, patch) to only the specific namespaces and resource types managed by your GitOps workflows.
How do I prevent memory exhaustion attacks on the repo-server?
Set the reposerver.max.combined.directory.manifests.size parameter in argocd-cmd-params-cm to limit the total uncompressed size of JSON/YAML manifests a single application can load into memory. This prevents malicious repositories from submitting oversized files that could crash the repo-server pod or exhaust node resources, effectively mitigating directory-based memory DoS attacks.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →