Skip to main content
BlogDevOps

Argo CD 3.5: mTLS, Source Integrity and Helm 4 Upgrade Guide

A close reading of the Argo CD 3.5 release and upgrade notes: repo-server mTLS, Source Integrity, Helm 4 OCI changes, ApplicationSet concurrency and what to audit on 3.4.

Matthews Wong

Software Engineer

October 202611 min read
Argo CD 3.5: mTLS, Source Integrity and Helm 4 Upgrade Guide

Key Takeaway

Argo CD 3.5, released on 4 August 2026, adds opt-in mTLS for repo-server traffic, Source Integrity commit verification configured per AppProject, Helm 4 rendering and concurrent ApplicationSet updates. Before upgrading from 3.4, audit plain-HTTP OCI repositories, SSH known_hosts, UI extensions, custom gRPC clients and any GnuPG signatureKeys.

The upgrade I would have been caught by is the quiet one. A Helm chart pulled from an internal registry over plain HTTP renders fine on 3.4, and on 3.5 it stops, because Helm 4 no longer assumes what Helm 3 did. Nothing in the release headline mentions it.

This post reads the v3.5.0 release page, the 3.4 to 3.5 upgrade notes and the new mTLS and Source Integrity documentation, then lays out what changed, what breaks, and the order I would roll it out in. I have not run 3.5 in production, so every behaviour below is attributed to the document that states it.

What 3.5 actually ships

The v3.5.0 release is dated 4 August 2026 and the page lists 855 commits since the previous tag. Container images are signed with cosign, and provenance is generated for images and CLI binaries at SLSA Level 3. The features list includes mTLS in the repo-server (#26715), a Helm 4 migration (#28076), ApplicationSet concurrency when managing applications (#26642), impersonation for server operations (#26898) and Git tag path prefixes (#27290).

Three maturity changes matter more than any single feature. The documentation entries promote the Source Hydrator to beta (#28229), impersonation to beta (#27576) and ApplicationSet in any namespace to stable (#27417). Stable is the one that lets you stop treating a multi-tenant ApplicationSet layout as an experiment.

InfoQ's write-up, published on 26 June while 3.5 was still a release candidate, adds the new ApplicationSet UI with list, filter and detail views and a Preview Apps tab. That detail comes from InfoQ, not from the release page I read, so treat it as reported rather than verified.

The upgrade notes: what to check before 3.5

The 3.4 to 3.5 upgrade page splits its content into breaking changes, behavioural changes and deprecations. I turned them into a table of who is affected and what to do. If a row says nobody is affected, you can skip it, but you have to know which row you are.

Change in 3.5Who is hitWhat to do
Helm 4 renders chartsPlain-HTTP OCI repos and OCI dependenciesRe-register with the insecure-oci-force-http option
React 19 UITeams with installed UI extensionsRebuild extensions with react/jsx-runtime externalised
Event gRPC methods return an Argo CD EventListCustom gRPC clients and generated REST clientsRegenerate clients; REST paths and JSON are unchanged
Impersonation covers all API server operationsAnyone using destinationServiceAccountsGrant those service accounts get, patch, list and create where needed
SSH host keys from argocd-ssh-known-hosts-cmRepos without explicit credentials relying on a mounted known_hostsMove the host keys into the ConfigMap
Per-application CSS class gets a user-app- prefixCustom UI CSS targeting application namesAdd the prefix to selectors

Notice what is absent. The upgrade page lists no entries under API changes or security changes, and no item on the release page is labelled breaking. The risk is concentrated in behaviour, which is exactly the kind that a green rollout hides until the first sync.

Helm 4 and the plain-HTTP OCI trap

Argo CD 3.5 bundles Helm 4. The upgrade notes call out the change that bites: Helm v4 requires an explicit plain-HTTP flag for plain HTTP OCI registries, and Helm v3 accessed OCI dependencies without registration, which no longer works. Existing repositories need the insecureOCIForceHttp field set to the string true in the repository Secret, or the CLI 3.5 flag with upsert.

# Re-register a plain-HTTP OCI chart repo for Helm 4 (Argo CD CLI 3.5).
# Without --insecure-oci-force-http, Helm 4 will not fall back to HTTP.
# Do NOT also pass --insecure-skip-server-verification: the upgrade notes say
# Helm 4 then drops --plain-http and fails with
# "http: server gave HTTP response to HTTPS client". No workaround is listed.
argocd repo add registry.internal:5000/charts \
  --type helm --name internal-charts --enable-oci \
  --insecure-oci-force-http --upsert

A spec.source.helm.version value such as v3 is ignored once you are on 3.5, and Helm 4 renders the chart anyway. You do not have to remove it, but it is now a comment that lies, so I would clean it up in the same pull request that audits your charts for Helm 4 differences.

The documented limitation is the nasty part. Combining the skip-server-verification flag with the force-HTTP flag on the same chain makes Helm 4 drop its plain-http option, and the result is the error http: server gave HTTP response to HTTPS client. The notes state there is no workaround when both are required.

Repo-server mTLS, and what it does not do

Repo-server mTLS is opt-in and off by default. Create a Secret named argocd-repo-server-mtls holding client-ca.crt, client.crt and client.key. Argo CD mounts it at /app/config/reposerver/mtls in the relevant pods, and mTLS switches on once the files exist. The repo-server generates an ephemeral client certificate for its own liveness check, so probes need no change.

# The shared Secret from the 3.4 to 3.5 upgrade page. mTLS turns on
# automatically once these files exist at /app/config/reposerver/mtls.
apiVersion: v1
kind: Secret
metadata:
  name: argocd-repo-server-mtls
  namespace: argocd
type: Opaque
stringData:
  client-ca.crt: |
    <PEM-encoded CA certificate that signed the client certs>
  client.crt: |
    <PEM-encoded shared client certificate>
  client.key: |
    <PEM-encoded private key for the shared client certificate>

# Deprecated 3.4 way vs the 3.5 way, per the mTLS page:
# Wrong: --repo-server-strict-tls            (works in 3.5 with warnings)
# Right: --repo-server-ca-cert-path=/app/config/server/tls/ca.crt

Read the server-side paragraph of the mTLS page twice. The repo-server loads one CA pool and accepts any client whose certificate chains to it. It does not check Subject or SAN. So mTLS here authenticates callers but does not authorise them: the application controller and a stray pod holding the same shared certificate look identical. Per-component authorisation needs your own layer, such as an Envoy sidecar or a gRPC interceptor.

There is also a flag migration. The repo-server-strict-tls flag is deprecated on argocd-server, the application controller, the applicationset controller and the notifications controller. Point repo-server-ca-cert-path at /app/config/server/tls/ca.crt and keep the volume mount. The old flag still works in 3.5 with warnings, and the notes say it may be removed in v3.6 or later.

To verify mTLS is really enforced, look for the log line Generated ephemeral health-check client certificate in the repo-server, then confirm that a client without a certificate fails the TLS handshake. Both checks come straight from the mTLS page; a rollout that only checks pods are Ready proves neither.

Source Integrity replaces GnuPG signatureKeys

Source Integrity is configured in each AppProject under spec.sourceIntegrity. If the criteria are not met, the application does not sync and reports a ResourceComparison error. Each policy has a list of repo URL globs, where a leading exclamation mark negates a pattern, and a gpg block with a mode and a list of trusted key IDs. Only one policy applies to a given repository, and a source matching no policy is not verified.

The mode decides how much history you are betting on. The table shows the three values the documentation defines.

gpg modeWhat is verifiedWhere it hurts
noneNothing. Unsigned commits and invalid signatures are acceptedIt is an explicit opt-out, so it hides intent from reviewers
headOnly the target revision: the tag signature for an annotated tag, else the commit signatureMatches the legacy signatureKeys behaviour, so it is the safe migration target
strictThe target revision and every ancestorOld unsigned commits or retired keys block syncs; seal commits are the documented fix
# AppProject: start in head mode, the closest match to legacy signatureKeys.
apiVersion: argoproj.io/v1alpha1
kind: AppProject
metadata:
  name: payments
  namespace: argocd
spec:
  sourceIntegrity:
    git:
      policies:
        - repos:
            - url: "https://github.com/my-group/*"
            - url: "!https://github.com/my-group/ignored.git"   # leading ! excludes
          gpg:
            mode: head                       # none | head | strict
            keys:
              - "D56C4FCA57A46444"           # key ID from the docs example
# Import the public key first; the repo-server keyring is rebuilt from
# argocd-gpg-keys-cm on restart, so manual edits inside the pod are lost.
#   argocd gpg add --from ./release-key.asc
# Do not keep .spec.signatureKeys here: the two cannot be used together.

Migration is mechanical. Remove spec.signatureKeys, define a policy with url star, mode head and the same key IDs, and you have the legacy behaviour. If sourceIntegrity is absent but signatureKeys is set, Argo CD converts it automatically, but the docs recommend migrating by hand because signatureKeys will be removed, and the two cannot coexist in one project.

Impersonation, ApplicationSet concurrency and the UI

Impersonation now applies to every API server operation, where previously it covered sync only. Viewing, patching, deleting, listing events, reading pod logs and running resource actions all use the service account from the AppProject destinationServiceAccounts. If those accounts were scoped to the verbs a sync needs, the UI will start returning permission errors after the upgrade.

ApplicationSet concurrency comes from pull request 26642. It lets the controller update the Applications it manages in parallel, where it used to go one at a time, which the PR says could take many minutes for hundreds of applications. The flag is concurrent-application-updates with an environment variable ARGOCD_APPLICATIONSET_CONTROLLER_CONCURRENT_APPLICATION_UPDATES. The default of 1, which keeps the sequential behaviour, is stated in the PR review summary rather than in code I read.

# ApplicationSet controller: the flag and env var named in PR 26642.
# The PR review summary gives the default as 1 (sequential). Step up slowly.
env:
  - name: ARGOCD_APPLICATIONSET_CONTROLLER_CONCURRENT_APPLICATION_UPDATES
    value: "4"        # example only: my own starting point, not a measured value

Raising it is a trade against your API server and the repo-server. I would step it up in small increments while watching reconcile latency, not set a large number because the option exists. I have no measurement of the right value and will not invent one.

Where it can go wrong

Source Integrity has documented limits that matter for a real fleet. Only Git sources are supported, so Helm and OCI sources cannot be verified. Signature verification is not supported for ApplicationSets populated by the git generator when the project field is templated. And with source integrity enforced, argocd app sync with the local flag no longer works, which removes a debugging habit many teams rely on.

The keyring is the second trap. The keyring inside repo-server pods is rebuilt from the argocd-gpg-keys-cm ConfigMap on restart, so manual changes in the pod are lost, and if the keyring falls out of sync the documented fix is to restart the repo-server pods. A key import can also take time to propagate across the cluster, which looks exactly like a signature failure.

Do not start in strict mode on a long-lived repository. The documentation warns that full-history verification can fail on older unsigned commits or retired keys. Start with head, then add a seal commit before you move a repo to strict.

Two source conflicts are worth knowing about. InfoQ describes Source Integrity as sourceIntegrity.required on the Application spec with an app set flag, while the Argo CD documentation puts it in the AppProject under spec.sourceIntegrity. I followed the documentation. The upgrade page also names Helm 4.2.0 in one section and 4.2.1 in another without reconciling them, so check the version your image actually reports.

What to do on Monday

Do the audit on 3.4 first, because every item is checkable before you change anything. This is the order I would follow.

  1. Search every Application and repository Secret for plain-HTTP OCI registries and OCI dependencies, and decide whether to move them to HTTPS or register them with the force-HTTP option.
  2. List AppProjects that still use signatureKeys and write the equivalent head-mode policy, so the migration is a reviewed diff rather than an automatic conversion.
  3. Find the deprecated repo-server-strict-tls flag in your manifests and replace it with repo-server-ca-cert-path before it disappears.
  4. Check destinationServiceAccounts RBAC for the verbs the UI and API now use, and move any known_hosts you mounted into argocd-ssh-known-hosts-cm.
  5. Upgrade a non-production instance, enable mTLS there, confirm a certificate-less client is refused, and only then plan production.

The commands below cover the audits that can be scripted. Adjust the namespace to yours.

# 1. AppProjects still on the deprecated GnuPG config
kubectl -n argocd get appprojects -o json \
  | jq -r '.items[] | select(.spec.signatureKeys) | .metadata.name'

# 2. Applications that pin a Helm version (ignored on 3.5, Helm 4 renders)
kubectl get applications -A -o json \
  | jq -r '.items[] | select(.spec.source.helm.version != null)
           | [.metadata.namespace, .metadata.name, .spec.source.helm.version] | @tsv'

# 3. The deprecated strict-TLS flag in your workloads
kubectl -n argocd get deploy,sts -o yaml | grep -n -- '--repo-server-strict-tls'

# 4. Which Helm version the repo-server image reports (the notes say 4.2.0 and 4.2.1)
kubectl -n argocd exec deploy/argocd-repo-server -- helm version

My rule for this release: the features are optional, the behaviour changes are not. Treat Helm 4, impersonation scope and SSH host keys as the real upgrade, and adopt mTLS and Source Integrity afterwards, in head mode, one project at a time.

FAQ

Frequently Asked Questions

Short answers to what readers ask most about this topic.

When was Argo CD 3.5 released and what is in it?

The v3.5.0 release page is dated 4 August 2026. Its feature list includes repo-server mTLS, a Helm 4 migration, ApplicationSet concurrency, impersonation for server operations and Source Integrity. Images are signed with cosign and provenance meets SLSA Level 3.

What breaks when upgrading from Argo CD 3.4 to 3.5?

The upgrade notes list Helm 4 requiring explicit plain-HTTP flags for OCI registries, UI extensions needing rebuilds for React 19, and event gRPC methods returning a new EventList type. Impersonation now covers all API server operations, and SSH host keys must live in argocd-ssh-known-hosts-cm. No item is labelled security or API change.

How do I enable mTLS for the Argo CD repo-server?

Create a Secret named argocd-repo-server-mtls in the argocd namespace with client-ca.crt, client.crt and client.key. Argo CD mounts it at /app/config/reposerver/mtls and mTLS turns on once the files exist. It is off by default and cannot be combined with the disable-tls flag.

What is Argo CD Source Integrity and how does it differ from signatureKeys?

Source Integrity is configured in AppProject spec.sourceIntegrity and requires Git commit signatures before an application syncs. Policies match repository URLs and use a gpg mode of none, head or strict. It replaces the deprecated spec.signatureKeys, which cannot be used in the same project.

Why does my plain-HTTP Helm OCI repository fail after Argo CD 3.5?

Argo CD 3.5 uses Helm 4, which requires an explicit plain-HTTP option. Re-register the repository with the insecure-oci-force-http option or set insecureOCIForceHttp to true in its Secret. Combining it with skip-server-verification on the same chain is a documented limitation with no workaround.