Argo CD often begins as a simple GitOps tool for one application team. A platform engineer installs it, connects a Git repository, creates an Application, and lets Argo CD synchronize Kubernetes manifests. The design becomes more difficult when the same Argo CD installation must serve multiple teams.

Without clear boundaries, one team may be able to view another team’s applications, synchronize workloads into the wrong namespace, use an unapproved Git repository, or deploy cluster-scoped resources. A shared GitOps platform therefore needs more than separate folders. It needs enforceable controls at the Argo CD and Kubernetes layers.

In this guide, we will build a practical multi-tenant Argo CD configuration for two application teams: payments and catalog. Each team will receive its own trusted repositories, Kubernetes namespaces, allowed resource types, and SSO groups. Developers will be able to view and synchronize their own applications without gaining administrative access to other projects.

The examples use Argo CD v3.5.3, which was the latest stable release when this article was written. Pin and test the version you approve rather than automatically installing a moving branch in production.

What You Will Learn

By the end of this tutorial, you will know how to:

  • Choose an appropriate Argo CD multi-tenancy model
  • Understand the difference between AppProject controls, Argo CD RBAC, and Kubernetes RBAC
  • Lock down the permissive default AppProject
  • Restrict teams to approved Git repositories and destination namespaces
  • Restrict the Kubernetes resource kinds a project can deploy
  • Map OIDC groups to project-scoped roles
  • Configure a least-privilege global RBAC policy
  • Connect Argo CD to an existing OIDC identity provider
  • Create team-specific Argo CD applications
  • Prove that repository, namespace, resource, and user isolation work
  • Troubleshoot common multi-tenancy failures
  • Apply production security and operational best practices

What Argo CD Multi-Tenancy Means

Multi-tenancy means that multiple teams share a platform while operating within separate administrative boundaries. In Argo CD, a tenant is usually a team, department, business unit, or environment that owns a defined set of applications.

Argo CD supports several possible tenancy models.

Argo CD tenancy models A comparison of four Argo CD tenancy models by relative isolation strength and operational effort. Argo CD Tenancy Models Relative isolation and operational effort Operational effort increases Isolation strength increases Low High Logical Strong 1 Shared instance with AppProjects Trusted internal teams • Low effort 2 Instance per cluster Strong cluster boundary Medium operational effort 3 Instance per team Separate administrators Medium-to-high effort 4 Central management + workload instances Strong, scalable • High effort
Choose the simplest model that satisfies your security and operational boundaries.

This tutorial uses one shared, cluster-wide Argo CD instance. It is a common model for internal teams that trust the central platform administrators but must not control one another’s applications.

A shared instance is not the same as a hard security boundary. Argo CD controllers may hold powerful credentials for destination clusters, and the repo-server processes content from multiple tenants. If tenants are mutually untrusted, belong to different customers, or require strict regulatory separation, use separate Argo CD instances and consider separate Kubernetes clusters.

The Three Authorization Layers

A secure design uses three different control layers. They complement each other and should not be treated as interchangeable.

The three authorization layers A deployment request must pass Argo CD RBAC, AppProject restrictions, and Kubernetes policy before a workload can run. The Three Authorization Layers Every deployment request must pass all three controls User requests an Argo CD action 1 Argo CD RBAC WHO may perform the action? Controls UI, CLI, and API permissions: get, sync, update, delete, logs, and exec. IDENTITY 2 AppProject WHAT may be deployed, and WHERE? Restricts Git sources, destination clusters, namespaces, resource kinds, roles, and sync windows. BOUNDARY 3 Kubernetes Policy HOW may the workload run? Enforces Kubernetes RBAC, admission policy, Pod Security, NetworkPolicy, and quotas. RUNTIME Deployment is allowed
Argo CD authorization and Kubernetes runtime policy solve different parts of the security problem.

An AppProject validates an Argo CD application’s declared source, destination, and resource types. It does not isolate network traffic between Pods, limit CPU consumption, or prevent a Kubernetes user from using kubectl directly. Those responsibilities remain with Kubernetes.

Argo CD RBAC controls actions through the Argo CD API, CLI, and web interface. It does not replace the restrictions inside an AppProject. A user might have permission to create an application, but the AppProject must still approve that application’s repository and destination.

Architecture Used in This Guide

We will create the following structure:

Architecture used in this guide An OIDC identity provider authenticates users to a shared Argo CD control plane. Argo CD separates payments and catalog teams through individual AppProjects, repositories, and Kubernetes namespaces. Architecture Used in This Guide One shared control plane with two isolated team boundaries ID OIDC Identity Provider Platform, payments, and catalog groups Shared Argo CD Control Plane Authentication • Global RBAC • Reconciliation • Audit trail P Payments AppProject Group: acme:payments-developers Approved source • destinations • resource kinds C Catalog AppProject Group: acme:catalog-developers Approved source • destinations • resource kinds Payments GitOps Trusted repository payments-dev payments-prod Catalog GitOps Trusted repository catalog-dev catalog-prod
OIDC groups control user access, while each AppProject restricts its team's source repository and deployment destinations.

The Application custom resources will remain in the argocd namespace. Argo CD will deploy their workloads into team namespaces. Keeping application definitions in the control-plane namespace provides a simpler and safer starting point than immediately allowing teams to create Application resources in arbitrary namespaces.

The platform repository can use this structure:

1
2
3
4
5
6
7
8
9
10
11
argocd-platform/
├── applications/
│ ├── catalog-api.yaml
│ └── payments-api.yaml
├── projects/
│ ├── catalog-project.yaml
│ ├── default-project.yaml
│ └── payments-project.yaml
└── security/
├── argocd-cm-oidc-patch.yaml
└── argocd-rbac-cm.yaml

The application source repositories can independently store each team’s Helm charts, Kustomize overlays, or plain Kubernetes manifests.

Prerequisites

Before starting, make sure you have:

  • A Kubernetes cluster and cluster-admin access for the initial platform configuration
  • kubectl configured for the correct cluster
  • Argo CD installed in the argocd namespace
  • The Argo CD CLI installed
  • Two Git repositories, one for each example team
  • An OIDC identity provider such as Okta, Microsoft Entra ID, Keycloak, or another compatible provider
  • Permission to create an OIDC client and group claims in that provider
  • A DNS name and valid TLS configuration for the production Argo CD endpoint

Confirm the local tools and Kubernetes context:

1
2
3
4
kubectl version --client
argocd version --client
kubectl config current-context
kubectl get nodes

Do not continue until the current context points to the intended cluster.

Install or Verify Argo CD

If Argo CD is already installed, inspect the running image and skip to the next section:

1
2
3
4
kubectl -n argocd get deployment argocd-server \
-o jsonpath='{.spec.template.spec.containers[0].image}{"\n"}'

kubectl -n argocd get pods

For a new lab installation, pin the release instead of using an unversioned production deployment:

1
2
3
4
5
6
7
8
9
10
11
12
13
export ARGOCD_VERSION="v3.5.3"

kubectl create namespace argocd

kubectl apply \
--namespace argocd \
--server-side \
--force-conflicts \
--filename "https://raw.githubusercontent.com/argoproj/argo-cd/${ARGOCD_VERSION}/manifests/install.yaml"

kubectl -n argocd rollout status deployment/argocd-server
kubectl -n argocd rollout status deployment/argocd-repo-server
kubectl -n argocd rollout status statefulset/argocd-application-controller

The --force-conflicts option is suitable for this fresh installation command. Do not run it casually against an existing customized installation because fields owned by another deployment method may be replaced. Production platforms should manage Argo CD with a tested Helm or Kustomize configuration, use the high-availability manifests where appropriate, and follow the supported minor-version upgrade path.

For an initial lab login, start a local port forward in one terminal:

1
kubectl -n argocd port-forward service/argocd-server 8080:443

In another terminal, display the generated password and log in. The login command will prompt for the password instead of placing it in the shell history:

1
2
3
4
5
argocd admin initial-password --namespace argocd

argocd login localhost:8080 \
--username admin \
--insecure

The --insecure flag is used only because the local port forward reaches the default self-signed certificate. Use trusted TLS and remove that flag when connecting to the production hostname.

Create the Tenant Namespaces

The AppProjects in this tutorial do not allow teams to create cluster-scoped Namespace resources. The platform team creates and governs the namespaces before onboarding applications.

Create tenant-namespaces.yaml:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
apiVersion: v1
kind: Namespace
metadata:
name: payments-dev
labels:
platform.codingtricks.io/team: payments
platform.codingtricks.io/environment: development
---
apiVersion: v1
kind: Namespace
metadata:
name: payments-prod
labels:
platform.codingtricks.io/team: payments
platform.codingtricks.io/environment: production
---
apiVersion: v1
kind: Namespace
metadata:
name: catalog-dev
labels:
platform.codingtricks.io/team: catalog
platform.codingtricks.io/environment: development
---
apiVersion: v1
kind: Namespace
metadata:
name: catalog-prod
labels:
platform.codingtricks.io/team: catalog
platform.codingtricks.io/environment: production

Apply and verify the namespaces:

1
2
3
4
kubectl apply -f tenant-namespaces.yaml

kubectl get namespaces \
-l platform.codingtricks.io/team

In production, add default-deny NetworkPolicies, ResourceQuotas, LimitRanges, Pod Security Admission labels, and any required admission policies during namespace provisioning. AppProjects do not provide those runtime controls.

Lock Down the Default AppProject

Every Argo CD application belongs to an AppProject. When no project is specified, the application uses the built-in default project. Its initial configuration is intentionally permissive, which is convenient for a first test but unsafe as the foundation of a multi-tenant platform.

Before restricting it, list applications that currently use it:

1
2
kubectl -n argocd get applications \
-o custom-columns='NAME:.metadata.name,PROJECT:.spec.project'

Move existing applications into purpose-built projects before continuing. Otherwise, they will stop reconciling after the default project is locked down.

Create default-project.yaml:

1
2
3
4
5
6
7
8
9
10
11
12
13
apiVersion: argoproj.io/v1alpha1
kind: AppProject
metadata:
name: default
namespace: argocd
spec:
description: Deny-by-default project. Applications must use an approved tenant project.
sourceRepos: []
sourceNamespaces: []
destinations: []
namespaceResourceBlacklist:
- group: '*'
kind: '*'

Apply it:

1
2
kubectl apply -f default-project.yaml
kubectl -n argocd get appproject default -o yaml

The default project remains present because Argo CD requires it, but it no longer authorizes deployments.

Create the Payments AppProject

An AppProject should grant only the repositories, destinations, and resource kinds required by a tenant. Avoid using '*' merely to make an error disappear.

Create payments-project.yaml:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
apiVersion: argoproj.io/v1alpha1
kind: AppProject
metadata:
name: payments
namespace: argocd
labels:
platform.codingtricks.io/team: payments
spec:
description: GitOps boundary for the payments team

sourceRepos:
- [email protected]:your-org/payments-gitops.git

destinations:
- server: https://kubernetes.default.svc
namespace: payments-dev
- server: https://kubernetes.default.svc
namespace: payments-prod

clusterResourceWhitelist: []

namespaceResourceWhitelist:
- group: ''
kind: ConfigMap
- group: ''
kind: Service
- group: ''
kind: ServiceAccount
- group: apps
kind: Deployment
- group: apps
kind: StatefulSet
- group: autoscaling
kind: HorizontalPodAutoscaler
- group: batch
kind: CronJob
- group: batch
kind: Job
- group: networking.k8s.io
kind: Ingress
- group: networking.k8s.io
kind: NetworkPolicy
- group: policy
kind: PodDisruptionBudget

orphanedResources:
warn: true

roles:
- name: developer
description: View and synchronize payments applications
groups:
- acme:payments-developers
policies:
- p, proj:payments:developer, applications, get, payments/*, allow
- p, proj:payments:developer, applications, sync, payments/*, allow
- p, proj:payments:developer, logs, get, payments/*, allow

- name: viewer
description: Read-only access to payments applications
groups:
- acme:payments-viewers
policies:
- p, proj:payments:viewer, applications, get, payments/*, allow
- p, proj:payments:viewer, logs, get, payments/*, allow

Replace the repository URL and group names with values from your organization.

Several controls in this manifest are important:

  • sourceRepos uses an exact repository rather than a global wildcard.
  • destinations lists only the two approved namespaces.
  • clusterResourceWhitelist: [] prevents this project from managing cluster-scoped resources.
  • namespaceResourceWhitelist creates an explicit list of namespaced resource kinds.
  • orphanedResources.warn helps find resources in the destination namespace that no longer belong to a tracked application.
  • The project roles grant get, sync, and log access without granting application deletion, project changes, repository administration, or terminal access.

The example intentionally excludes Kubernetes Secret resources. Storing plaintext secrets in Git is unsafe. Integrate a secret-management solution such as External Secrets Operator, Sealed Secrets, or a secrets-management plugin, and then allow only the custom resources required by that solution.

Apply and inspect the project:

1
2
3
kubectl apply -f payments-project.yaml

argocd proj get payments

Create the Catalog AppProject

Create catalog-project.yaml with an independent source, destination, and identity boundary:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
apiVersion: argoproj.io/v1alpha1
kind: AppProject
metadata:
name: catalog
namespace: argocd
labels:
platform.codingtricks.io/team: catalog
spec:
description: GitOps boundary for the catalog team

sourceRepos:
- [email protected]:your-org/catalog-gitops.git

destinations:
- server: https://kubernetes.default.svc
namespace: catalog-dev
- server: https://kubernetes.default.svc
namespace: catalog-prod

clusterResourceWhitelist: []

namespaceResourceWhitelist:
- group: ''
kind: ConfigMap
- group: ''
kind: Service
- group: ''
kind: ServiceAccount
- group: apps
kind: Deployment
- group: apps
kind: StatefulSet
- group: autoscaling
kind: HorizontalPodAutoscaler
- group: batch
kind: CronJob
- group: batch
kind: Job
- group: networking.k8s.io
kind: Ingress
- group: networking.k8s.io
kind: NetworkPolicy
- group: policy
kind: PodDisruptionBudget

orphanedResources:
warn: true

roles:
- name: developer
description: View and synchronize catalog applications
groups:
- acme:catalog-developers
policies:
- p, proj:catalog:developer, applications, get, catalog/*, allow
- p, proj:catalog:developer, applications, sync, catalog/*, allow
- p, proj:catalog:developer, logs, get, catalog/*, allow

- name: viewer
description: Read-only access to catalog applications
groups:
- acme:catalog-viewers
policies:
- p, proj:catalog:viewer, applications, get, catalog/*, allow
- p, proj:catalog:viewer, logs, get, catalog/*, allow

Apply it:

1
2
3
4
kubectl apply -f catalog-project.yaml

kubectl -n argocd get appprojects
argocd proj get catalog

Project role names must follow the proj:<project>:<role> pattern inside policy rules. A policy stored in the payments project must also remain scoped to payments/*; project roles should not be used to create cross-project access.

Configure Global Argo CD RBAC

Project roles handle team-specific permissions. Global RBAC handles platform-wide responsibilities such as trusted administrators.

The built-in role:readonly can view resources across the entire Argo CD instance, so it is usually too broad as the default role in a tenant-aware system. Create a minimal authenticated default role and grant access through explicit group mappings.

On an existing installation, inspect the current ConfigMap first and merge any policies that still need to exist:

1
kubectl -n argocd get configmap argocd-rbac-cm -o yaml

Create argocd-rbac-cm.yaml:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
apiVersion: v1
kind: ConfigMap
metadata:
name: argocd-rbac-cm
namespace: argocd
labels:
app.kubernetes.io/name: argocd-rbac-cm
app.kubernetes.io/part-of: argocd
data:
policy.default: role:authenticated
policy.matchMode: glob
scopes: '[groups, email]'
policy.csv: |
g, acme:platform-admins, role:admin

Apply and validate it:

1
2
3
4
kubectl apply -f argocd-rbac-cm.yaml

argocd admin settings rbac validate \
--namespace argocd

Every authenticated user inherits policy.default. Permissions granted there cannot later be removed with a more specific deny rule. That is why role:authenticated intentionally contains no broad permissions.

Only a tightly controlled identity-provider group should map to role:admin. Anyone in that group can administer every Argo CD project and application.

If Helm or another GitOps controller owns this ConfigMap, make the change through that controller’s values or overlay so that it is not reverted.

Validate the Global RBAC Policy

The administrative RBAC command validates policies stored in argocd-rbac-cm. Use it to check syntax and test the global platform group mapping:

1
2
3
argocd admin settings rbac can \
acme:platform-admins get application 'payments/payments-api-dev' \
--namespace argocd

This check should return Yes. A user with no global mapping should not inherit application access from role:authenticated:

1
2
3
argocd admin settings rbac can \
[email protected] get application 'payments/payments-api-dev' \
--namespace argocd

This check should return No.

The team permissions are stored inside AppProject roles rather than the global ConfigMap. Test those policies end to end after logging in through OIDC, as shown later in this guide.

Configure OIDC SSO

Argo CD can authenticate directly against an existing OIDC provider. The exact provider screens differ, but the common configuration requires:

  • Argo CD public URL: https://argocd.example.com
  • OIDC callback URL: https://argocd.example.com/auth/callback
  • Authorization Code flow
  • Group claims in the ID token or UserInfo response
  • A confidential client ID and client secret

Create the client in your identity provider first. Then export the secret only in the current shell:

1
export OIDC_CLIENT_SECRET='replace-with-the-real-client-secret'

Create a dedicated Kubernetes Secret. Do not commit the real client secret to Git:

1
2
3
4
5
6
7
8
9
10
kubectl -n argocd create secret generic argocd-oidc-secret \
--from-literal=client-secret="$OIDC_CLIENT_SECRET" \
--dry-run=client \
-o yaml | kubectl apply -f -

kubectl -n argocd label secret argocd-oidc-secret \
app.kubernetes.io/part-of=argocd \
--overwrite

unset OIDC_CLIENT_SECRET

Create argocd-cm-oidc-patch.yaml:

1
2
3
4
5
6
7
8
9
10
data:
url: https://argocd.example.com
oidc.config: |
name: Company SSO
issuer: https://identity.example.com/oauth2/default
clientID: argocd
clientSecret: $argocd-oidc-secret:client-secret
requestedScopes: ["openid", "profile", "email", "groups"]
requestedIDTokenClaims: {"groups": {"essential": true}}
enablePKCEAuthentication: true

Replace the public URL, issuer, and client ID. The issuer must match the provider’s discovery metadata and usually exposes a .well-known/openid-configuration endpoint.

Patch the existing Argo CD ConfigMap without replacing unrelated settings:

1
2
3
4
5
6
kubectl -n argocd patch configmap argocd-cm \
--type merge \
--patch-file argocd-cm-oidc-patch.yaml

kubectl -n argocd rollout restart deployment argocd-server
kubectl -n argocd rollout status deployment argocd-server

Argo CD normally reloads many settings automatically. The controlled restart ensures that the server immediately reads the new Secret and OIDC configuration.

In a GitOps-managed production installation, express the same configuration through the owning Helm values or Kustomize overlay. A manual patch can otherwise be reverted during the next reconciliation.

When Groups Are Not in the ID Token

Some providers return group membership only from the UserInfo endpoint. If your login works but all project access is denied, inspect the token and provider behavior. Argo CD can retrieve groups from UserInfo when configured appropriately:

1
2
3
4
5
6
7
8
9
10
data:
oidc.config: |
name: Company SSO
issuer: https://identity.example.com/oauth2/default
clientID: argocd
clientSecret: $argocd-oidc-secret:client-secret
requestedScopes: ["openid", "profile", "email", "groups"]
enableUserInfoGroups: true
userInfoPath: /userinfo
userInfoCacheExpiration: 5m

Use this only when it matches your provider. The exact group-claim configuration is identity-provider specific.

After logging in, inspect the identity that Argo CD received:

1
argocd account get-user-info

The output must include group strings that exactly match the values in the AppProjects and argocd-rbac-cm. Group matching is case-sensitive.

Register Team Repositories

For public repositories, the URL in the AppProject may be enough. Private repositories also require credentials.

Project-scope the repository when the credentials should only be usable by one project:

1
2
3
4
5
6
7
argocd repo add [email protected]:your-org/payments-gitops.git \
--project payments \
--ssh-private-key-path ./payments-deploy-key

argocd repo add [email protected]:your-org/catalog-gitops.git \
--project catalog \
--ssh-private-key-path ./catalog-deploy-key

Prefer read-only deploy keys or short-lived workload identities. Avoid a single personal access token that can read every repository in the organization. In a declarative setup, encrypt repository credentials or obtain them from a secret manager instead of committing them as plaintext.

Verify connectivity:

1
argocd repo list

The repository status should be Successful before creating applications.

Create the Team Applications

The Application resources stay in the argocd namespace, while their workloads are deployed into tenant namespaces.

Create payments-api.yaml:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
name: payments-api-dev
namespace: argocd
labels:
platform.codingtricks.io/team: payments
platform.codingtricks.io/environment: development
spec:
project: payments

source:
repoURL: [email protected]:your-org/payments-gitops.git
targetRevision: main
path: environments/dev

destination:
server: https://kubernetes.default.svc
namespace: payments-dev

syncPolicy:
automated:
prune: true
selfHeal: true
syncOptions:
- ApplyOutOfSyncOnly=true
- PruneLast=true
- CreateNamespace=false

Create catalog-api.yaml:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
name: catalog-api-dev
namespace: argocd
labels:
platform.codingtricks.io/team: catalog
platform.codingtricks.io/environment: development
spec:
project: catalog

source:
repoURL: [email protected]:your-org/catalog-gitops.git
targetRevision: main
path: environments/dev

destination:
server: https://kubernetes.default.svc
namespace: catalog-dev

syncPolicy:
automated:
prune: true
selfHeal: true
syncOptions:
- ApplyOutOfSyncOnly=true
- PruneLast=true
- CreateNamespace=false

Update the paths to match your repositories, then apply the applications:

1
2
3
4
kubectl apply -f payments-api.yaml
kubectl apply -f catalog-api.yaml

kubectl -n argocd get applications

Using CreateNamespace=false is intentional. Namespace creation remains a platform responsibility, and neither AppProject allows the cluster-scoped Namespace kind.

Inspect both applications:

1
2
argocd app get payments-api-dev
argocd app get catalog-api-dev

When the repositories and paths are valid, each application should eventually report Synced and Healthy.

Prove the Tenant Isolation

Configuration is not complete until its negative security cases have been tested.

Test 1: Block a Repository Owned by Another Team

Create a temporary application that assigns the catalog repository to the payments project:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
name: payments-wrong-repository
namespace: argocd
spec:
project: payments
source:
repoURL: [email protected]:your-org/catalog-gitops.git
targetRevision: main
path: environments/dev
destination:
server: https://kubernetes.default.svc
namespace: payments-dev

Apply it and inspect the status condition:

1
2
3
4
kubectl apply -f payments-wrong-repository.yaml

kubectl -n argocd get application payments-wrong-repository \
-o jsonpath='{.status.conditions[*].message}{"\n"}'

Argo CD should reject the source repository because the payments AppProject does not allow it.

Remove the negative test:

1
kubectl -n argocd delete application payments-wrong-repository

Test 2: Block a Cross-Team Namespace

Change the temporary application’s repository back to the payments repository but set the destination namespace to catalog-dev. The project should reject that destination.

You can also inspect the permitted destinations directly:

1
argocd proj get payments

Only payments-dev and payments-prod should appear.

Test 3: Block a Cluster-Scoped Resource

Add a ClusterRole, ClusterRoleBinding, CustomResourceDefinition, or Namespace to the payments repository in a temporary branch. Argo CD should report that the resource kind is not permitted by the project.

Do not add broad cluster-resource access to fix the test. If a team genuinely requires a cluster-scoped resource, review it separately and let the platform team manage it from a more privileged project.

Test 4: Block Cross-Project User Access

Log in as a member of acme:payments-developers and confirm that the user can view and synchronize payments applications but cannot access catalog applications. Repeat the reverse test for a catalog developer.

The logged-in payments user can check the live authorization result from the CLI:

1
2
3
4
argocd account can-i get applications 'payments/payments-api-dev'
argocd account can-i sync applications 'payments/payments-api-dev'
argocd account can-i get applications 'catalog/catalog-api-dev'
argocd account can-i delete applications 'payments/payments-api-dev'

The first two checks should return yes; the cross-project and delete checks should return no.

Also confirm that neither team can:

  • Delete an Application
  • Update an AppProject
  • Add a repository or cluster
  • Use the web terminal
  • View or synchronize the other team’s applications

Add Sync Windows for Production

Project roles determine who can request a synchronization. Sync windows determine when synchronization is allowed or denied. They are useful for maintenance freezes and production change windows.

The following example denies automated and manual synchronization of payments production applications during a weekly freeze:

1
2
3
4
5
6
7
8
spec:
syncWindows:
- kind: deny
schedule: '0 17 * * 5'
duration: 60h
applications:
- '*-prod'
manualSync: false

This example begins at 17:00 every Friday and lasts 60 hours. Cron evaluation follows the controller’s configured time zone, so verify the effective schedule before using it for a real production freeze.

Inspect active and configured windows:

1
argocd proj windows list payments

Treat sync windows as a change-control feature, not a substitute for RBAC or AppProject boundaries.

Optional: Applications in Team Namespaces

Argo CD can reconcile Application resources outside the argocd namespace. This enables stronger self-service because each team can declare its own applications in a controlled namespace.

Do not enable this feature only to simplify YAML placement. It expands the configuration surface and can introduce privilege escalation if a team-controlled namespace is allowed to use a powerful AppProject.

Three conditions are required:

  1. Argo CD must be installed cluster-wide.
  2. The application controller and API server must explicitly allow the source namespaces through --application-namespaces.
  3. Each AppProject must list its permitted application namespaces in spec.sourceNamespaces.

For example:

1
2
3
4
5
6
7
8
apiVersion: argoproj.io/v1alpha1
kind: AppProject
metadata:
name: payments
namespace: argocd
spec:
sourceNamespaces:
- payments-gitops

An Application created in payments-gitops could then reference the payments project, but it must not be allowed to reference a privileged project. Never add a user-controlled namespace to sourceNamespaces on the default or platform administration project.

When applications live outside argocd, RBAC object names use three segments:

1
<project>/<application-namespace>/<application-name>

For example:

1
payments/payments-gitops/payments-api-dev

Use annotation-based resource tracking because the combined namespace and application name can exceed the Kubernetes label-length limit.

Centralized applications are sufficient for this tutorial. Enable namespace-based self-service only after threat modeling, testing source namespace restrictions, and updating the RBAC patterns.

Troubleshooting

Application Is Not Allowed in the Project

Inspect the project’s source and destinations:

1
2
3
argocd proj get payments

kubectl -n argocd get application payments-api-dev -o yaml

Compare the repository URL character for character. HTTPS and SSH repository forms are different strings, and a .git suffix mismatch can matter. Also confirm the exact cluster server and namespace.

Resource Is Not Permitted in Project

List the resources produced from the Git source:

1
argocd app manifests payments-api-dev

Identify the denied API group and kind. Add it only after deciding that the tenant should control that resource. Cluster-scoped resources deserve a separate platform review.

SSO Login Works but the User Has No Access

Inspect the user information:

1
argocd account get-user-info

Check whether the token includes the expected group claim and whether the value exactly matches the AppProject group. Then validate the live policy:

1
argocd admin settings rbac validate --namespace argocd

If groups exist only at the UserInfo endpoint, configure enableUserInfoGroups for the provider.

User Can See Every Application

Inspect policy.default:

1
2
kubectl -n argocd get configmap argocd-rbac-cm \
-o jsonpath='{.data.policy\.default}{"\n"}'

If it is role:readonly, all authenticated users inherit read access across the instance. Replace it with a minimal custom role and grant project-specific access explicitly.

RBAC Policy Does Not Match

Validate the global policy syntax, then test the current logged-in user’s effective project permissions:

1
2
3
4
argocd admin settings rbac validate --namespace argocd

argocd account get-user-info
argocd account can-i sync applications 'payments/payments-api-dev'

Remember that policy matching is case-sensitive. In glob mode, / is not treated as a special separator, so use specific, fully formed object patterns.

OIDC Redirect Loop or Invalid Callback

Confirm that data.url in argocd-cm matches the public HTTPS address and that the identity provider allows:

1
https://argocd.example.com/auth/callback

Then review the server logs:

1
2
kubectl -n argocd logs deployment/argocd-server \
--since=15m

Check the issuer, client ID, client secret reference, TLS trust, token audience, and proxy headers.

Project Change Has No Effect

Confirm that the AppProject was updated in the same namespace as the Argo CD installation:

1
2
kubectl -n argocd get appproject payments \
-o yaml

If Argo CD itself is managed by Helm or GitOps, check whether the owning controller reverted a manual change.

Application Is Valid but Cannot Deploy

AppProject validation may succeed while Kubernetes rejects a resource. Inspect the operation and controller messages:

1
2
3
4
argocd app get payments-api-dev

kubectl -n argocd logs statefulset/argocd-application-controller \
--since=15m

Common causes include Kubernetes admission policies, missing CRDs, destination-cluster RBAC, quota limits, invalid manifests, and immutable fields.

Cleanup

Deleting an Argo CD Application with its resources can remove the workloads it manages. Run the following commands only for the demonstration environment created in this tutorial.

Delete the applications first:

1
kubectl -n argocd delete application payments-api-dev catalog-api-dev

If an application contains the Argo CD resources finalizer, deletion may cascade to its managed resources. Verify that the application objects are gone before deleting the projects:

1
2
3
kubectl -n argocd get applications

kubectl -n argocd delete appproject payments catalog

Delete the demonstration namespaces:

1
2
3
kubectl delete namespace \
payments-dev payments-prod \
catalog-dev catalog-prod

Remove the dedicated OIDC Secret only if it is no longer used:

1
kubectl -n argocd delete secret argocd-oidc-secret

Do not delete the Argo CD namespace or restore a permissive default project on a shared production platform.

Conclusion

Argo CD multi-tenancy is not created by naming folders after teams. A reliable shared platform combines AppProjects, Argo CD RBAC, identity-provider groups, and Kubernetes policy.

AppProjects define where each application’s trust begins and ends: the approved Git repositories, destination clusters and namespaces, and deployable resource types. Project roles map team identities to narrow application operations, while global RBAC reserves cross-project administration for the platform team. Kubernetes then provides runtime isolation through namespaces, network policy, admission control, quotas, and ordinary RBAC.

Start with centralized Application resources, exact allow lists, a locked-down default project, and deny-by-default user access. Prove both allowed and forbidden actions before onboarding real workloads. If the organization later needs stronger self-service, enable Applications in team namespaces only with explicit source namespace controls and updated three-segment RBAC patterns.

For trusted internal teams, this design provides a practical balance between centralized governance and team autonomy. When stronger security boundaries are required, move beyond logical projects and deploy separate Argo CD control planes.