Webinar replay: The migration takes 2 weeks. Deciding to do it takes 6 months.

Kubernetes Multi-Tenancy After Jenkins: How to Stop Teams Seeing Each Other's Apps in GitOps

A step-by-step blueprint for isolating teams in Kubernetes when you replace Jenkins deploys with GitOps: namespaces plus quotas and NetworkPolicy, Argo CD AppProjects and RBAC, Kyverno or OPA Gatekeeper admission policy, and when an internal developer platform like Qovery removes the work.

Romaric Philogene
CEO & Co-founder
OCT 28, 2026 · 14 MIN
Kubernetes Multi-Tenancy After Jenkins: How to Stop Teams Seeing Each Other's Apps in GitOps

Key points:

  • Kubernetes multi-tenancy takes four layers: namespace isolation (quota, LimitRange, default-deny NetworkPolicy, Pod Security Admission label), Kubernetes RBAC per tenant, Argo CD AppProjects with project-scoped RBAC, and admission policy (Kyverno, OPA Gatekeeper or ValidatingAdmissionPolicy). Skip one and tenants can still see or touch each other's apps.
  • A namespace alone isolates nothing at runtime. Pods accept all traffic by default until a NetworkPolicy selects them, and only if your network plugin enforces it.
  • In Argo CD, the AppProject plus RBAC is the tenant boundary. The API server only returns applications a user holds applications, get on, so an empty policy.default plus per-project get grants is what hides other teams' apps.
  • Three credible routes: build on Argo CD plus Kyverno or Gatekeeper, buy managed Argo CD (Akuity, or OpenShift GitOps on OpenShift), or run an internal developer platform like Qovery in your own AWS, GCP, Azure, Scaleway or existing Kubernetes cluster.

Most teams assume namespaces plus Argo CD give them isolation. In practice, the Argo CD application controller becomes the most privileged identity in the cluster the day you install it, and a fresh namespace blocks zero cross-team traffic. I have interviewed more than 200 CTOs, and this gap shows up in almost every Jenkins-to-GitOps migration I hear about. Here is the order of operations I would follow.

Qovery · Agentic Infrastructure Platform
A control plane for platform teams and their coding agents
Learn more

What actually breaks when you move from Jenkins deploys to GitOps?

Nothing breaks, because the isolation gap was already there. Jenkins hid it behind one shared kubeconfig with cluster-admin, and GitOps moves that over-privileged identity into the Argo CD application controller, where every team's manifests suddenly flow through one visible place.

The typical Jenkins setup uses one cluster-admin service account, with access gated by Jenkins folder and job permissions. Any job that can run kubectl can touch any namespace.

Argo CD keeps the same shape by default. Its security documentation says it "uses a clusteradmin level role" to watch and deploy, and that it needs cluster-wide read but "not necessarily full write privileges". You are expected to trim the argocd-application-controller ClusterRole yourself.

Before migrating a single team, answer three questions in writing:

  1. Who can see an application?
  2. Who can sync it?
  3. Who can change what it deploys? (Git write access to the manifest repo.)

GitOps still pays off: every change is a reviewable commit, humans stop holding cluster credentials, and Argo CD detects drift. Keeping Jenkins for CI while Argo CD handles delivery is the most common migration path and a fully supported pattern.

Is a namespace per team enough to isolate Kubernetes tenants?

No. A namespace scopes names and gives RBAC something to bind to, but by default it does not stop cross-namespace traffic, cap resource use, or block privileged pods. A tenant namespace is isolated only once it also carries a default-deny NetworkPolicy, a ResourceQuota, a LimitRange and a Pod Security Admission label.

The Kubernetes docs list namespaces as one mechanism among several and say "RBAC, quotas, and network policies are essential to safely and fairly share clusters" (kubernetes.io multi-tenancy). Each companion closes a specific hole:

  • Default-deny NetworkPolicy. Pods are "non-isolated for ingress" and egress until a policy selects them. Calico and Cilium enforce NetworkPolicy; check your managed default before trusting it.
  • ResourceQuota. Once a quota covers cpu or memory, pods that omit requests or limits for them are rejected.
  • LimitRange. Injects default requests and limits so those pods are admitted instead of failing.
  • Pod Security Admission. The pod-security.kubernetes.io/enforce: restricted (or baseline) namespace label, stable since Kubernetes 1.25, blocks privileged pods, hostPath and hostNetwork.

The default-deny policy is six lines of spec:

YAML
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: default-deny
  namespace: payments-prod
spec:
  podSelector: {}
  policyTypes: [Ingress, Egress]

Remember to allow DNS egress to kube-system afterwards, or every pod in the namespace loses name resolution.

Shared clusters also leak through places people forget: cluster-scoped resources and CRDs, hostPath volumes, ingress hostname collisions, LoadBalancer sprawl, and a cluster-wide Prometheus or Loki that lets anyone read another team's metrics and logs.

Run this test today. Exec into a pod in team A's namespace and curl http://<service>.<team-b-namespace>.svc.cluster.local. If it answers, you have no network isolation.

Isolation layerWhat it preventsWhat it does not preventSymptom when missing
NamespaceName collisions; gives RBAC, quotas and policies a scopeCross-namespace traffic, resource hogging, privileged podsEvery team's objects share one flat list
ResourceQuota + LimitRangeOne tenant consuming all CPU, memory or object counts in the clusterNetwork access, privilege escalationOne team's runaway job evicts another team's pods
Default-deny NetworkPolicyPod-to-pod traffic across tenant namespaces (if the CNI enforces it)API access, node-level access, shared observability stacksTeam A can curl team B's internal services
Pod Security Admission (baseline or restricted)Privileged pods, hostPath, hostNetwork, host namespacesBad images, missing labels, network accessA tenant pod can mount the node filesystem
Kubernetes RBAC (Role + RoleBinding per tenant)Humans and service accounts reading or writing other namespacesWhat Argo CD itself deploys on their behalfkubectl get secrets -n other-team succeeds
Argo CD AppProject + RBACDeploying from unapproved repos or into other teams' namespaces; seeing or syncing other teams' appsManifest content, such as privileged pods or :latest tagsEvery developer sees and can sync every app in the Argo CD UI
Admission policy (Kyverno, Gatekeeper, VAP)Non-compliant manifests from any pipeline, such as unapproved registries or missing owner labelsWho is allowed to deploy whereA manifest with privileged: true is admitted
Virtual cluster (vCluster) or cluster per tenantShared control plane risks, CRD conflicts, cluster-scoped collisionsMisconfiguration inside the tenant's own clusterTwo tenants need conflicting CRD versions

How do you configure Argo CD so teams can't see or sync each other's applications?

Create one AppProject per team, scope it with sourceRepos, destinations and an empty clusterResourceWhitelist, then map SSO groups to project-scoped roles in argocd-rbac-cm with an empty policy.default. The Argo CD API server only lists applications the caller holds applications, get on, so granting get per project is the setting that hides other teams' apps.

Argo CD projects exist for exactly this: "a logical grouping of applications, which is useful when Argo CD is used by multiple teams". Namespaced kinds are restricted via a deny list and cluster-scoped kinds via an allow list, so an empty whitelist denies them all.

YAML
apiVersion: argoproj.io/v1alpha1
kind: AppProject
metadata:
  name: team-payments
  namespace: argocd
spec:
  description: Payments team, staging and production
  sourceRepos:
    - https://gitlab.example.com/payments/deploy.git
  destinations:
    - server: https://kubernetes.default.svc
      namespace: payments-staging
    - server: https://kubernetes.default.svc
      namespace: payments-prod
  clusterResourceWhitelist: []
  namespaceResourceBlacklist:
    - group: ''
      kind: ResourceQuota
    - group: ''
      kind: LimitRange
  roles:
    - name: ci-sync
      description: Token used by the payments CI job to trigger syncs
      policies:
        - p, proj:team-payments:ci-sync, applications, sync, team-payments/*, allow

Also lock down the built-in default project with sourceRepos: [] and destinations: [], as the Argo CD docs show. It starts out "the most permissive" project, and Red Hat's OpenShift GitOps docs say plainly not to let non-admins bind apps to it.

Then the RBAC side, using the policy.csv syntax p, <subject>, <resource>, <action>, <project>/<app>, <effect>:

YAML
apiVersion: v1
kind: ConfigMap
metadata:
  name: argocd-rbac-cm
  namespace: argocd
data:
  # No default role: authenticated users get nothing they are not granted below.
  policy.default: ""
  # OIDC claim used for group mapping (the default is '[groups]').
  scopes: '[groups]'
  policy.csv: |
    p, role:team-payments, projects, get, team-payments, allow
    p, role:team-payments, applications, get, team-payments/*, allow
    p, role:team-payments, applications, sync, team-payments/*, allow
    p, role:team-payments, logs, get, team-payments/*, allow
    g, my-org:payments, role:team-payments

Some guides write policy.default: role:'', which points at an empty role and has the same effect. The docs warn that whatever role policy.default grants "cannot be blocked by a deny rule", so never set it to role:readonly in a multi-tenant instance.

Treat logs and exec as separate decisions. Since Argo CD 3.0, logs RBAC is enforced by default and the logs tab stays hidden without an explicit logs, get grant (3.0 upgrade notes). exec, create opens a shell into running containers and their environment variables, so I keep it off for production.

Three more settings close the remaining gaps:

  • ApplicationSets. "ApplicationSets can create Applications under arbitrary Projects," per the ApplicationSet security docs. Hard-code the project field in templates, or anyone with push access to the generator repo can target an unrestricted project.
  • Apps in any namespace. Since Argo CD 2.5, Application resources can live in tenant namespaces listed in application.namespaces, if the AppProject allows them in .spec.sourceNamespaces. Teams can then manage Applications declaratively without touching the argocd namespace.
  • Git permissions. Per-team repos, or CODEOWNERS plus branch protection on a shared repo. Write access to the manifest repo is deploy access.

On topology, I default to one central Argo CD with AppProject tenancy. One instance per team shrinks the blast radius but multiplies upgrades and compute, and each install still needs cluster-scoped rights.

Ship faster on infrastructure you control.
Qovery gives your teams self-service deployments with per-environment RBAC on your own AWS, GCP, Azure, or Scaleway account - or your existing Kubernetes cluster. Start deploying in under 10 minutes.

Where do Kyverno, OPA Gatekeeper, and ValidatingAdmissionPolicy fit in?

Admission policy is the backstop for what RBAC and AppProjects cannot express. It checks the content of every manifest at apply time, whichever pipeline sent it. Kyverno is the fastest path for YAML-native teams, Gatekeeper suits complex cross-resource logic in Rego, and built-in ValidatingAdmissionPolicy covers simple field checks with nothing extra to run.

The tenancy rules worth enforcing are short: required team and owner labels, allowed registries and no :latest tags, no privileged pods or hostPath, mandatory requests and limits, and per-tenant restrictions on ingress hostnames and Service type LoadBalancer.

Kyverno's generate rules also make the namespace layer impossible to forget. This policy creates a default-deny NetworkPolicy in every namespace carrying a team label, including existing ones, and synchronize: true recreates it if someone deletes it:

YAML
apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
  name: tenant-default-deny
spec:
  rules:
    - name: generate-default-deny
      match:
        any:
          - resources:
              kinds:
                - Namespace
              selector:
                matchExpressions:
                  - key: team
                    operator: Exists
      generate:
        generateExisting: true
        synchronize: true
        apiVersion: networking.k8s.io/v1
        kind: NetworkPolicy
        name: default-deny
        namespace: '{{request.object.metadata.name}}'
        data:
          spec:
            podSelector: {}
            policyTypes:
              - Ingress
              - Egress

Add sibling rules for the ResourceQuota and LimitRange. Kyverno flags at install time if its background controller lacks permission to create the generated kind.

For validation, use the rule-level validate.failureAction: Audit first. spec.validationFailureAction is deprecated, and violations land in PolicyReport resources. Read them for a week, flip to Enforce, and run the same policies in CI against rendered manifests so teams see failures before merge.

Policy engines complement AppProject tenancy and do not replace it. AppProjects control who deploys where; admission policy controls what a manifest may contain.

CriterionKyvernoOPA GatekeeperValidatingAdmissionPolicy
Policy languageKubernetes-style YAML (plus CEL)Rego in ConstraintTemplates, parameters in ConstraintsCEL expressions
Validate / mutate / generateAll three, plus cleanupValidate and mutate (Assign, AssignMetadata, ModifySet, AssignImage); no generateValidate only; mutation is the separate MutatingAdmissionPolicy, stable since Kubernetes 1.36
Auto-provision tenant resourcesYes, generate rules create NetworkPolicy, ResourceQuota and more per namespaceNoNo
Audit and reportingAudit mode writes PolicyReport and ClusterPolicyReportAudit reports existing violations on constraint statusWarn and Audit actions on the binding; no report CRD
CI dry-runkyverno apply CLI on rendered manifestsgator CLIServer-side dry-run against a cluster
Extra components to runAdmission, background, cleanup and reports controllersWebhook and audit controllerNone, runs inside the API server
Learning curveLow for YAML-native teamsHigh (Rego)Low for simple checks
CNCF maturityGraduated, March 16, 2026Part of OPA, graduated February 4, 2021Core Kubernetes, stable since 1.30

Which tools or services specialize in GitOps multi-tenancy, and how do they compare?

Choose by who you want owning the control plane. Assemble Argo CD plus Kyverno or Gatekeeper yourself for full control, buy Akuity or Red Hat OpenShift GitOps when the requirement is literally managed Argo CD, and run an internal developer platform like Qovery when you want per-team environments and RBAC provisioned for you inside your own cloud account.

  • DIY Argo CD plus Kyverno or Gatekeeper. Most control, most recurring work. Onboarding one team means coordinated PRs to the AppProject repo, argocd-rbac-cm, the policy repo and the team's manifest repo. Argo graduated from the CNCF on December 6, 2022, with over 350 organizations in production at the time.
  • Akuity. Built "from the creators of Argo CD and Kargo", with a hybrid agent model and fleet-wide dashboards. If you need managed Argo CD with Argo-native RBAC, Akuity is the right answer.
  • Red Hat OpenShift GitOps. Argo CD packaged with OpenShift projects and SCCs, with a default cluster-scoped instance and user-defined instances that start out managing only their own namespace. If you are standardized on OpenShift, use it.
  • Flux. A different primitive: tenant Kustomizations set .spec.serviceAccountName so the controller impersonates the tenant's ServiceAccount, and --no-cross-namespace-refs stops tenants using each other's sources. Plain Kubernetes RBAC then does the isolating, and that is a legitimate design choice. Weaveworks ceased operations in February 2024; Flux graduated from the CNCF in November 2022 and continues under community governance.
  • Capsule and vCluster. Capsule groups namespaces into tenants on a single cluster with per-tenant rules. vCluster gives each tenant "its own control plane and RBAC", running as a pod in a host namespace. Reach for these when tenants are untrusted or need their own CRDs.
  • Qovery. An internal developer platform that runs in your own AWS, GCP, Azure or Scaleway account, or your existing Kubernetes cluster. Each environment runs in its own Kubernetes namespace, and custom roles set access per project and per environment type (Development, Preview, Staging, Production), with shell access granted separately. Developers get git-push deployments, preview environments per pull request and environment auto-stop without holding cluster credentials.

To be clear about the boundary: Qovery is not a GitOps controller and does not replace Argo CD. Its Argo CD integration is read-only today; it maps each Argo CD destination namespace to a Qovery environment while "deployment ownership remains in ArgoCD". Kyverno or Gatekeeper still enforce cluster-wide policy alongside it.

ApproachIsolation unitWho runs the control planeOnboarding a new teamHard or soft isolationRuns in your own cloud accountPlatform-team effort
DIY Argo CD + Kyverno or GatekeeperNamespace + AppProjectYour platform teamPRs across 3-4 repos (AppProject, RBAC, policy, manifests)SoftYesHigh and ongoing
AkuityArgo CD AppProjectAkuity-hosted control plane; agent in your clustersProject and RBAC config in AkuitySoftWorkloads yes; control plane hostedMedium
Red Hat OpenShift GitOpsOpenShift project + Argo CD instance or AppProjectYour team, via the GitOps OperatorLabel namespace, configure AppProject and RBACSoftYes, on OpenShiftMedium
Flux multi-tenancyNamespace + impersonated ServiceAccountYour platform teamTenant namespace, ServiceAccount, RoleBinding, KustomizationSoftYesHigh
CapsuleTenant (group of namespaces)Your platform teamCreate a Tenant resourceSoft to mediumYesMedium
vClusterVirtual cluster per tenantYour platform team, or vCluster PlatformCreate a virtual clusterHard (separate control plane)YesMedium to high
QoveryEnvironment (one namespace) with per-environment-type RBACQovery manages it; workloads in your accountCreate a project and environment, assign a custom roleSoft; use separate clusters for hard isolationYes, AWS, GCP, Azure, Scaleway or your clusterLow

What does a working multi-tenant GitOps setup look like, step by step?

Build the namespace layer first, then the AppProject and RBAC, then admission policy, and only then move Jenkins jobs over and revoke the shared kubeconfig. Done out of order, you leave a window where a team can deploy anywhere.

  1. Choose the tenant unit. Team times environment, for example payments-prod. The team label drives NetworkPolicy selectors, policy matching and cost allocation.
  2. Provision the namespace layer. Namespace, ResourceQuota, LimitRange, default-deny NetworkPolicy and PSA label, generated by Kyverno.
  3. Create the AppProject. Allow-list sourceRepos and destinations, keep clusterResourceWhitelist empty.
  4. Wire Argo CD RBAC. Map the SSO group to a project-scoped role, set policy.default empty, decide logs and exec separately.
  5. Lock Git. Per-team repo or CODEOWNERS, branch protection, no direct push to main, required review on manifest changes.
  6. Run admission policy in audit. Review PolicyReports for a week, then switch to Enforce.
  7. Migrate Jenkins jobs one team at a time. Run both paths in parallel for a sprint, then delete the shared cluster-admin service account and rotate the kubeconfig.
  8. Verify isolation. As team A, try to sync team B's app, curl across namespaces, apply a privileged pod and exceed the quota. All four must fail. Run them as a scheduled job, because RBAC drifts.

The anti-patterns I see most: the Argo CD controller left at cluster-admin with an unrestricted default project, one monorepo everyone can write to, ApplicationSets with a templated project field, and policies left in audit mode forever.

How do you keep multi-tenant GitOps from becoming a full-time job for your platform team?

Building it takes about two weeks; running it never ends. Argo CD upgrades, policy-engine CRD changes, Kubernetes minor versions about three times a year, a multi-repo PR for every new team, and RBAC drift you discover during an audit all land on the platform team. Measure that cost, then decide whether to own it.

The upgrade tax is concrete. Kubernetes ships "approximately three times per year" and each minor version gets about one year of patch support; as of October 2026 the supported branches are 1.35, 1.36 and 1.37. Every bump means re-testing Argo CD, Kyverno or Gatekeeper and your CNI against it.

Track two numbers before and after the migration: time to onboard a new team end to end, and platform-team tickets per week. If onboarding takes days and tickets keep climbing, your developers are waiting on you. Security is a real cost too: in Red Hat's 2024 State of Kubernetes Security report, 67% of 600 respondents had delayed or slowed deployments over security concerns, and 40% had detected misconfigurations.

This is the work Qovery takes off your plate. It creates the per-team environments and RBAC described above, runs managed upgrades on the clusters it provisions and backs databases with managed cloud services, on AWS, GCP, Azure, Scaleway or an existing Kubernetes cluster.

Bring-your-own-cloud matters for tenancy and compliance. Workloads and data stay in your account, the cloud bill and committed-spend discounts stay in your name, and your security team keeps its own audit trail. Cluster-wide admission policy, network segmentation and your identity provider stay yours either way, and no platform should take those from you.

How do I set this up from Claude Code, Cursor, or Codex?

Install the Qovery Agent Skills once and your coding agent can scaffold environments, per-team isolation and deployments from a plain-language request, showing each action for approval before it runs.

BASH|Install the Qovery Agent Skills globally for Claude Code, Cursor, Codex, OpenCode and Gemini CLI
curl -fsSL https://skill.qovery.com/install.sh | bash

# Or scope the install to the current repository only
curl -fsSL https://skill.qovery.com/install.sh | bash -s -- --project

# Connect the Qovery MCP Server to Claude Code over OAuth (read-only by default)
claude mcp add --transport http qovery https://mcp.qovery.com/mcp --callback-port 4242

# Add write access only when you want it (also enable write access in the Qovery Console)
claude mcp add --transport http qovery "https://mcp.qovery.com/mcp?read_write=true" --callback-port 4242

The skills (qovery-onboard, qovery-deploy, qovery-preview, qovery-troubleshoot, qovery-optimize, qovery-speedup, qovery-terraform) set things up; the MCP Server operates what already exists. Then ask something like "onboard team payments with its own environment and RBAC on our existing EKS cluster", review the plan and approve it. Details are in the AI agent quickstart and the Claude Code integration page.

Frequently asked questions
How do you set up Kubernetes multi-tenancy so teams can't see or touch each other's apps when moving from Jenkins deploys to a GitOps workflow?

Give each team a namespace per environment with a ResourceQuota, LimitRange, default-deny NetworkPolicy and Pod Security Admission label. Create one Argo CD AppProject per team that allow-lists its repos and namespaces, and grant SSO groups project-scoped get and sync in policy.csv with an empty policy.default. Add Kyverno, Gatekeeper or ValidatingAdmissionPolicy for manifest content, then revoke the shared Jenkins cluster-admin kubeconfig.

Does an Argo CD AppProject actually hide applications from other teams in the UI and CLI?

Only together with RBAC. The Argo CD API server lists only applications the user holds applications, get on, so visibility depends on policy.csv. If policy.default is role:readonly, every authenticated user sees every app, and a deny rule cannot override it.

Should each team get its own Argo CD instance or share one with AppProjects?

Most organizations should share one instance and isolate teams with AppProjects and RBAC, because it is cheaper to upgrade and operate. Separate instances per team reduce blast radius and suit hard compliance boundaries, at the cost of more upgrades, more compute and cluster-scoped install rights for each instance.

Kyverno or OPA Gatekeeper for multi-tenant policy enforcement - which should a platform team pick?

Pick Kyverno if your team thinks in YAML and wants generate rules that auto-create NetworkPolicy and ResourceQuota per namespace; it graduated from the CNCF on March 16, 2026. Pick Gatekeeper if you already write Rego or need logic spanning multiple resources. For simple field checks, built-in ValidatingAdmissionPolicy, stable since Kubernetes 1.30, needs no extra controller.

Is a namespace a security boundary in Kubernetes?

Not on its own. Pods accept traffic from any namespace until a NetworkPolicy selects them, and quotas and pod security are separate objects you must add. The Kubernetes multi-tenancy guide recommends dedicated clusters or virtual control planes when tenants do not trust each other.

Can I keep Jenkins for CI and use Argo CD only for deployment?

Yes, and it is the most common migration path. Jenkins builds, tests and pushes the image, then commits the new tag to the manifest repo, and Argo CD syncs it. Jenkins no longer needs cluster credentials, so you can delete its cluster-admin service account. Multi-tenancy after Jenkins comes down to four layers applied in the right order, and the platform team that owns them owns them forever. If you would rather hand the environment and RBAC layer to a platform running in your own cloud, try Qovery free.

Romaric Philogene
About the author
Romaric Philogene

Romaric founded Qovery to make Kubernetes accessible to every engineering team. He writes about platform strategy, developer experience, and the future of cloud infrastructure.

Next step

Ship faster on infrastructure you control.

Qovery gives your teams self-service deployments with per-environment RBAC on your own AWS, GCP, Azure, or Scaleway account - or your existing Kubernetes cluster. Start deploying in under 10 minutes.