Restrict who can deploy where: ArgoCD Projects for multi-tenancy, repository and cluster whitelists, JWT-based roles, and tenancy patterns based on teams, environments, and applications.

In episode 9 we registered many clusters into one ArgoCD and placed applications on any destination. That's great, but there's one big unanswered question: who can deploy where? If everyone with ArgoCD access can deploy to the production cluster, one typo could take down a service. In this episode we discuss ArgoCD Projects — the isolation and access control mechanism that turns ArgoCD from a personal tool into a secure multi-tenancy platform.
Why does this matter? In a real company, ArgoCD is shared by many teams. Team A must not change team B's configuration; the frontend team must not deploy to the data center cluster; and new developers must not touch production. Projects are that dividing wall — and the foundation for the operational patterns we'll use in all the next episodes, including ApplicationSet in episode 11.
A Project (AppProject) is a logical grouping of Applications with restrictions and the people allowed to operate them. Every Application must be inside a project — if not specified, it goes into the built-in project named default.
The default project exists since installation and is used by all Applications that don't mention a project. Its restrictions are minimal: it allows all source repositories and all destinations. This is not safe for tenancy — the best practice in production environments is to leave default empty and require every Application to name an explicit project.
There are two ways: declarative (a YAML manifest in Git — best suited to GitOps) or the CLI:
apiVersion: argoproj.io/v1alpha1
kind: AppProject
metadata:
name: team-billing
namespace: argocd
spec:
sourceRepos:
- "https://github.com/devnull/billing-repo.git"
destinations:
- server: "https://kubernetes.default.svc"
namespace: "billing-*"
- server: "https://eks-prod.example.com"
namespace: "billing-*"
clusterResourceWhitelist:
- group: ""
kind: Namespaceargocd proj create team-billing \
--src https://github.com/devnull/billing-repo.git \
--dest https://kubernetes.default.svc,billing-prod \
--description "Project milik tim billing"The declarative way is preferred in GitOps because the project itself is versioned in Git — consistent with the "everything from Git" principle.
Tip
The project name will appear in the UI, in the CLI, and in the application list. Use names that carry context (team or environment), e.g. team-billing-prod — much clearer than project-1.
The core of a Project is restriction. There are five main types of restrictions:
sourceRepos determines which Git repositories Applications inside the project may use. Leave it empty to deny all sources (an "empty" project), or list the allowed URLs. Wildcards are supported, but use them carefully.
destinations restricts the combination of destination cluster and namespace. Notice the billing-* pattern in the earlier example — ArgoCD supports wildcards on namespaces, so this project can deploy to the billing-dev, billing-staging, and billing-prod namespaces, but not to other teams' namespaces.
clusterResourceWhitelist — cluster-level resources that may be managed (e.g. only Namespace).clusterResourceBlacklist — cluster-level resources that are forbidden (e.g. ClusterRole, PersistentVolume).namespaceResourceBlacklist — in-namespace resources that are forbidden (e.g. overly powerful ServiceAccount).This is the second line of defense: even if the manifest source in Git is "safe", dangerous resources are still intercepted by the project.
How you model projects depends on the organization's needs. Three common patterns:
| Pattern | Project per... | Example | Suitable for |
|---|---|---|---|
| Team-based | Team | team-billing, team-orders | Organizations with many teams |
| Environment-based | Environment | core-prod, core-staging | One platform, many environments |
| Application-based | Application | api, web, worker | Strictest per-application isolation |
Team-based is the most common: each team has a project with its own repositories and destinations. Environment-based helps separate production permissions from development — for example only leads have access to *-prod projects. A combination is also valid: team-billing-prod and team-billing-dev projects give two-axis granularity at once.
A Project can also have roles with their own RBAC policies — the right way to give CLI access without sharing admin credentials.
argocd proj role create team-billing deployer
argocd proj role add-policy team-billing deployer \
--action get --permission allow --object "*"
argocd proj role add-policy team-billing deployer \
--action sync --permission allow --object "*"Policies use the ArgoCD RBAC format: p, <role>, <resource>, <action>, <object>. With the two lines above, the deployer role can view and sync all Applications belonging to the team-billing project.
Every role can issue a JWT token used to log into the CLI with rights limited to that project only:
argocd proj role create-token team-billing deployer
argocd login argocd.example.com --auth-token <TOKEN>
argocd app listWith this token, argocd app list only shows Applications in the team-billing project — not the whole cluster. This is the right pattern for integrating ArgoCD into CI/CD pipelines: each pipeline uses its own project token, not the admin one.
Warning
A leaked JWT token can be used by anyone to operate Applications in the related project. Set the token's expiration with the --expires-in flag (e.g. 24h), and rotate tokens regularly. Never commit tokens to a repository.
ArgoCD Projects don't have a built-in quota mechanism — resource quotas are actually enforced by the Kubernetes ResourceQuota on the destination namespace:
apiVersion: v1
kind: ResourceQuota
metadata:
name: billing-quota
namespace: billing-prod
spec:
hard:
requests.cpu: "8"
requests.memory: 16Gi
limits.cpu: "16"
limits.memory: 32Gi
persistentvolumeclaims: "4"The correct combination: the Project restricts what may be deployed (repositories, destinations, resource kinds), while the ResourceQuota restricts how much resource may be used. The two complement each other — a ResourceQuota also prevents one team from consuming the entire cluster capacity.
default project. The built-in project is loose and open; in production, empty it and require explicit projects.sourceRepos: ["*"] or a * namespace destroys the purpose of isolation. Always list the specific ones.* access. The --object "*" policy gives full access to every Application in the project. If needed, restrict objects with name patterns.--expires-in.clusterResourceBlacklist, managed manifests could include a ClusterRole with high privileges. Define blacklists for risky resources.This episode made ArgoCD a platform usable by many teams: the Project concept and the default project, source repository restrictions, destination cluster/namespace restrictions, resource whitelist/blacklist, team/environment/application-based tenancy patterns, project roles with JWT tokens for CLI access, and collaboration with Kubernetes ResourceQuota.
The points you should take with you:
sourceRepos and destinations are the two most important limits — a whitelist is always better than leaving things open.Managing projects and Applications manually is still tiring when there are dozens of them. In the next episode 11 we discuss ApplicationSets - Advanced Application Management: how to define an Application template that is automatically generated from a list, cluster, Git, and Pull Requests — automation that turns 100 Applications into a single file. See you in episode 11!