You start from the Catalog's Argo CD base, ask your assistant to write the hardening as a values file, and check every key the assistant wrote. Then you compare the result with the base, object by object, and keep it as a variant of the base with its own staging and production path.
The example uses the Catalog's argo-cd 10.2.1 chart and one set of assumptions. Your team signs in through Okta, every signed-in user can read, and developers may sync staging but not production. If your team signs in with GitHub instead, keep Dex on and give it a GitHub connector under configs.cm.dex.config, as the chart's own defaults show; everything else in this Guide stays the same. Another set of assumptions gives a sibling variant, and the last section shows where those live.
Every step before the last runs on your machine with no ConfigHub account and no cluster. The last step needs an account or a server you run yourself.
Set up the tools
Install the cub CLI and Helm. Then install the Workshop plugin release this Guide was checked with (version 0.6.61).
cub plugin install confighub/cub-workshop@v0.6.61
1. Start from the Catalog base
Open the argo-cd 10.2.1 chart page. Its default configuration uses the chart's own defaults, and the page lists what a static scan of those objects found, including ClusterRoles that can read Secrets. Hardening the settings does not change those roles, so read that list before you rely on the result.
Render the base the same way you will render your variant, so the comparison in step 4 shows only your changes. The Catalog records no values for the default configuration, so the base values file is empty.
echo '{}' > base-values.yaml
cub config values argo-cd --repo https://argoproj.github.io/argo-helm --version 10.2.1 \
--release argo-cd --namespace argocd \
--values base-values.yaml --out base-report.json --render-out base-render.yaml
To keep the Catalog's exact package files as well, install the installer plugin and run the page's setup command with a namespace.
Here is what the command does. cub installer is an open-source plugin for the cub CLI. cub installer setup pulls a catalog package and writes its Kubernetes files locally, leaving delivery to kubectl, Argo CD or Flux. The generated scripts stop before doing any work when the plugin or kustomize is missing.
cub plugin install confighub/installer
cub installer setup --pull oci://europe-west1-docker.pkg.dev/nth-fort-499605-q5/helm-expt/argo-cd-argo-cd:10.2.1@sha256:b933436ed879e10c1b684a2d5f89670a35e759264849ec7bfd7c2f2cec1dd5f8 \
--base default --namespace argocd --work-dir ./argo-base --non-interactive
2. Ask your assistant to write the hardening as values
Give your assistant the article, or your own security requirements, with this task.
Write a values file for the argo-cd Helm chart, version 10.2.1, from
https://argoproj.github.io/argo-helm. Apply only the settings the chart's
values control: single sign-on, the local admin account, RBAC and the
default role, and component settings. Leave AppProjects, sync windows and
repository credentials out, and list them separately. Put no secret value in
the file; refer to secrets by key. Do not tell me the file is correct; I will
check it.
A first pass that follows the article closely looks like this. It sets the URL, turns off admin, adds Okta as the identity provider, sets RBAC and log levels, and turns off Dex, the built-in identity broker that Okta replaces. Save it as hardened-values.yaml.
configs:
cm:
url: https://argocd.example.com
admin.enabled: false
oidc.config: |
name: Okta
issuer: https://example.okta.com
clientID: argocd
clientSecret: $oidc.okta.clientSecret
requestedScopes: [openid, profile, email, groups]
requestedIDTokenClaims:
groups:
essential: true
rbac:
policy.default: role:readonly
policy.csv: |
p, role:platform-admin, applications, *, */*, allow
p, role:developer, applications, get, */*, allow
p, role:developer, applications, sync, production/*, deny
p, role:developer, applications, sync, staging/*, allow
g, platform-engineers, role:platform-admin
g, app-developers, role:developer
params:
server.log.level: info
application-controller.log.level: info
dex:
enabled: false
3. Check every key the assistant wrote
The values check renders the chart with your file, then again with each key taken out, and says what each key did.
cub config values argo-cd --repo https://argoproj.github.io/argo-helm --version 10.2.1 \
--release argo-cd --namespace argocd \
--values hardened-values.yaml --out report.json --render-out hardened.yaml --exit-code
On the first pass it exits 1 and reports two keys that did nothing.
[IGNORED] configs.cm.url matched no key under configs.cm, and it changed nothing. Consider the chart-declared candidate notifications.argocdUrl; review before changing your values.
[APPLIED] configs.cm.admin.enabled changed 4 objects: ... ConfigMap argocd-cm
[IGNORED] configs.params.server.log.level matched no key under configs.params, and it changed nothing.
[APPLIED] configs.params.application-controller.log.level changed 6 objects: ... (not in the chart's documented values)
[APPLIED] dex.enabled changed 12 objects: ...
2 of 8 values did nothing.
Read each line against the chart before you change anything.
configs.cm.urlexists, but this chart computes it fromglobal.domain, whose default isargocd.example.com. The article's URL equals that computed default, so it changed nothing. The check suggestsnotifications.argocdUrl, which sets only the notifications URL. Setglobal.domainto your own host instead; it sets both.configs.params.server.log.levelalso exists, and the chart computes every component's log level fromglobal.logging.level, whose default isinfo. The setting is already in force, so leave it out.application-controller.log.levelchanged the rendered objects, and Argo CD still ignores it. The key Argo CD reads iscontroller.log.level. The check cannot see this, because any key underconfigs.paramslands in the ConfigMap. Compare eachconfigs.paramskey with Argo CD's reference file, argocd-cmd-params-cm.yaml, and eachconfigs.cmkey with argocd-cm.yaml.
The reviewed file sets the domain, drops the log levels, and keeps the rest.
global:
domain: argocd.internal.example
configs:
cm:
admin.enabled: false
oidc.config: |
name: Okta
issuer: https://example.okta.com
clientID: argocd
clientSecret: $oidc.okta.clientSecret
requestedScopes: [openid, profile, email, groups]
requestedIDTokenClaims:
groups:
essential: true
rbac:
policy.default: role:readonly
policy.csv: |
p, role:platform-admin, applications, *, */*, allow
p, role:platform-admin, clusters, *, *, allow
p, role:platform-admin, repositories, *, *, allow
p, role:platform-admin, projects, *, *, allow
p, role:developer, applications, get, */*, allow
p, role:developer, applications, sync, production/*, deny
p, role:developer, applications, sync, staging/*, allow
p, role:developer, applications, delete, */*, deny
g, platform-engineers, role:platform-admin
g, app-developers, role:developer
g, auditors, role:readonly
dex:
enabled: false
Run the same check again. The command does not overwrite an existing report or render, so delete report.json and hardened.yaml from the first pass before you rerun it. Every key reports APPLIED and the command exits 0. The $oidc.okta.clientSecret reference names a key in the argocd-secret Secret. You add that key on the cluster; the rendered Secret carries no data.
4. See exactly what the hardening changes
Compare the base with your variant.
cub config diff base-render.yaml hardened.yaml --summary
cub config diff base-render.yaml hardened.yaml
For this example the summary reports 0 added, 6 removed, 9 changed and 44 unchanged. The six removed objects are Dex's Deployment, Service, ServiceAccount, Role, RoleBinding and NetworkPolicy. The full diff shows the changes in argocd-cm, argocd-rbac-cm, argocd-cmd-params-cm and the notifications URL. It also shows new checksum annotations on five workloads, so applying the variant restarts those pods.
cub config check hardened.yaml then lists what delivery must handle. There are 53 objects, the argocd namespace must already exist, and the three CRDs must be established before any Application or AppProject. The chart render includes a Job that is a Helm pre-install hook and creates the argocd-redis Secret. The Catalog's package leaves that hook out and lists the Secret as something you supply, with kubectl -n argocd create secret generic argocd-redis --from-literal=auth=<value>. Use one route or the other, not both. The lifecycle Guide shows how to handle hooks.
5. Check the advice the tools cannot check
Security articles are recipes, and recipes carry mistakes. The values check proves which keys changed the objects; it cannot tell whether a setting does what the article claims. These are the claims in the article above that need correcting before they reach a base.
| The article says | What is true | How to check it yourself |
|---|---|---|
GitHub sign-in uses the issuer token.actions.githubusercontent.com | That issuer is for GitHub Actions workload identity. Argo CD signs users in with GitHub through Dex's GitHub connector, so keep Dex enabled for GitHub. | The chart's own dex.config GitHub example in helm show values argo-cd |
The application-controller.log.level key sets the controller's log level | The key is controller.log.level | Argo CD's argocd-cmd-params-cm.yaml reference |
PruneLast=true protects an Application from deletion | It only makes pruning run last during a sync | Argo CD's sync options documentation |
The API server needs --grpc-web | --grpc-web is a flag of the argocd CLI, for proxies that cannot carry gRPC | argocd --help |
| A resource allowlist keeps out PodSecurityPolicy | Kubernetes removed PodSecurityPolicy in version 1.25 | The Kubernetes deprecation guide |
Ask your assistant to cite the reference file or documentation page for each setting it writes. A setting with no reference is one to test on a cluster before production.
Keep team settings apart from the base
AppProjects, sync windows and repository credentials belong to each team and each environment, not to the Argo CD install. Keep them as separate files, one AppProject per team, and check each one.
apiVersion: argoproj.io/v1alpha1
kind: AppProject
metadata:
name: payments
namespace: argocd
spec:
sourceRepos:
- https://github.com/example/payments-gitops
destinations:
- namespace: payments-staging
server: https://staging.cluster.internal
- namespace: payments-production
server: https://production.cluster.internal
clusterResourceBlacklist:
- group: ""
kind: Namespace
- group: rbac.authorization.k8s.io
kind: ClusterRole
- group: rbac.authorization.k8s.io
kind: ClusterRoleBinding
syncWindows:
- kind: deny
schedule: "0 0 * * FRI"
duration: 24h
applications:
- "*-production"
cub config check team-payments.yaml reports one AppProject in the argocd namespace. It needs the AppProject CRD from the base, so deliver it after the base. Repository credentials should come from a secret manager, for example through External Secrets, and never sit in either file.
Keep it as a variant in ConfigHub
The reviewed values are a variant of the Catalog's Argo CD base for one set of assumptions. Other assumptions give sibling variants of the same base.
- A platform shared by many teams adds one AppProject per team and a separate project for cluster-wide resources.
- Change-controlled production adds sync windows and manual production sync.
- A GitHub organization keeps Dex and uses its GitHub connector instead of Okta.
Each variant then has its own staging and production copies, promoted in order. Files are enough while one person looks after one install. ConfigHub helps once several clusters or teams share these settings. It keeps each variant with its lineage to the Catalog base, records every later edit as a change, and carries those edits through the next chart version. A configured ChangeWorkflow can require approval before production delivery.
These commands need an account or a server you run yourself, and they were not run for this Guide. Preview the upload with --dry-run first; the preview also needs you signed in.
cub variant upload --component argocd --variant okta-readonly --namespace argocd hardened.yaml
cub variant create staging argocd-okta-readonly --target staging/cluster \
--space-pattern "template:argocd-staging"
cub variant promote argocd-staging --dry-run
cub variant approve argocd-staging
The approval command records an attestation for the current revisions of Units with Targets in the staging Space. Review that selection first. In a Space with no Targets it records nothing, and --all includes every Unit. The upload skips Secrets and names each one in its output.
A live run on 2026-10-08 with cub v0.8.7 used the same upload, promotion preview and approval commands on other content with no Target. The run log holds the output. The --target form of cub variant create was not run. These commands do not create a ChangeWorkflow, bind a ChangeOrder, or configure an approval prerequisite. Recording an attestation alone does not gate delivery.
A variant you make this way is data other teams can pull, just as you pulled the Catalog base. That is the loop the Workshop is built around: pull a checked base, make your variant with your assistant, check it, and keep it where the next team can find it.
What this proves, and what it does not
The checks are local and static. They show which values changed the rendered objects, what the variant adds and removes compared with the base, and what delivery must handle. They do not show that Okta signs anyone in, that the RBAC policy grants what you intend, or that the pods start. Test those on a staging cluster before production.