[Ep.20] Managing ACM Policies with OpenShift GitOps
In [Ep.18] I recommended ACM policies for the audited baseline of a cluster and Argo CD for everything that needs rendering. [Ep.19] showed how ACM feeds Argo CD with clusters and the different models that can be used in Argo CD. This part turns the integration around: Argo CD deploys ACM policies, and a policy bootstraps Argo CD on every new cluster.
The result is a setup where the policies live in Git, are reviewed in pull requests like any other change, and ACM does what it is good at: distribution, enforcement and compliance reporting.
The topic turned out to be too big for a single article, so I have split it into multiple parts and maybe I should have split into even more. This is the third:
[Ep.18] OpenShift GitOps or Advanced Cluster Management? Choosing the right tool What each tool does, where they overlap and a decision guide.
[Ep.19] ACM and OpenShift GitOps: push, pull and the Argo CD Agent: how ACM registers clusters in Argo CD, and how to set up each of the three delivery models.
[Ep.20] Managing ACM Policies with OpenShift GitOps (this article): policies as code, the PolicyGenerator, hub templates and bootstrapping Argo CD with a policy.
| Everything in this series was written against OpenShift GitOps 1.22 (Argo CD 3.5) and Advanced Cluster Management 2.17 (Open Cluster Management). Technology Preview and GA states move quickly in both products, so check the release notes of your versions before you commit to a design. |
| While I am using the term "Advanced Cluster Management" in this article, I am referring to the product formerly known as Open Cluster Management. Same counts for Argo CD and OpenShift GitOps. |
Three integration patterns
There are three ways to combine policies and Argo CD. They are not exclusive, most mature setups combine the first two.
| Pattern | How it works | Strength | Weakness |
|---|---|---|---|
A: Argo CD deploys policies | Policies live in Git, generated by the PolicyGenerator. The hub Argo CD syncs them to the hub, ACM propagates and evaluates them. | Policy as code with review, compliance dashboard in ACM | Needs the PolicyGenerator plugin in Argo CD |
B: A policy bootstraps Argo CD | One enforced policy installs OpenShift GitOps and a root Application on every placed cluster. From there, Argo CD takes over. | New clusters configure themselves on import | The bootstrap needs proper planning and automation. |
C: Policies configure directly | ConfigurationPolicies apply the cluster configuration themselves. No Argo CD on the spokes. | One tool, compliance built in | Weaker diff and ordering, large YAML in policies |
Pattern C is fine for small, compliance-driven setups. For everything else I recommend A for governance and B for the fleet bootstrap. The rest of this article shows both.
Whatever option you choose, one rule from Ep.18 still applies: one owner per object. If Argo CD manages an object, a policy may only look at it in inform mode.
Preparing the hub Argo CD
You need an Argo CD instance on the hub that can sync to the hub itself. That instance needs three things before it can deploy policies:
permissions for the policy API,
the PolicyGenerator plugin,
optionally, health checks for policies.
A second Argo CD instance for the hub
In Ep.19, the default instance openshift-gitops became the principal of the Argo CD Agent. Its application-controller is disabled, so it cannot sync anything to the hub itself, and the manifests of agent Applications are rendered on the managed clusters anyway. The policies, however, must be created on the hub.
This article therefore uses a second, small Argo CD instance on the hub that only takes care of the hub configuration: policy-gitops in the namespace of the same name. If your default instance still has its application-controller (push or pull model), you can use it instead. Replace policy-gitops with openshift-gitops in the examples below.
apiVersion: v1
kind: Namespace
metadata:
name: policy-gitops
---
apiVersion: argoproj.io/v1beta1
kind: ArgoCD
metadata:
name: policy-gitops (1)
namespace: policy-gitops
spec: {} (2)
---
apiVersion: v1
kind: Namespace
metadata:
name: acm-policies
labels:
argocd.argoproj.io/managed-by: policy-gitops (3)| 1 | The service account of the application-controller is named after the instance: policy-gitops-argocd-application-controller. |
| 2 | The PolicyGenerator plugin is added in the next sections. |
| 3 | policy-gitops is a namespace-scoped instance. The label allows it to manage the namespace acm-policies, where the policies live. The namespace must exist before the first sync, because the instance cannot create namespaces itself. |
Permissions
The application-controller of the instance cannot create policies and Placements. Create a ClusterRole and bind it to the controller’s service account:
| Not required if Argo CD runs as cluster-admin. |
kind: ClusterRole
apiVersion: rbac.authorization.k8s.io/v1
metadata:
name: openshift-gitops-policy-admin
rules:
- apiGroups:
- policy.open-cluster-management.io
resources:
- policies
- configurationpolicies
- certificatepolicies
- operatorpolicies
- policysets
- placementbindings
verbs: ["get", "list", "watch", "create", "update", "patch", "delete"]
- apiGroups:
- cluster.open-cluster-management.io
resources:
- placements
- placements/status
- placementdecisions
- placementdecisions/status
verbs: ["get", "list", "watch", "create", "update", "patch", "delete"]
---
kind: ClusterRoleBinding
apiVersion: rbac.authorization.k8s.io/v1
metadata:
name: policy-gitops-policy-admin
subjects:
- kind: ServiceAccount
name: policy-gitops-argocd-application-controller
namespace: policy-gitops
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: ClusterRole
name: openshift-gitops-policy-adminThe ClusterRole in the ACM documentation also contains placementrules. I left them out on purpose: PlacementRules are deprecated.
The PolicyGenerator plugin
Creating the policy YAML by hand is tough and error-prone. Every manifest has to be wrapped in a ConfigurationPolicy, which is wrapped in a Policy, which needs a Placement and a PlacementBinding. The PolicyGenerator solves this. It is a Kustomize generator plugin that takes plain manifests and wraps them for you.
The plugin is not part of the OpenShift GitOps images. An init container copies it from an ACM image into the repo-server, and Kustomize must be allowed to run alpha plugins.
The setup is described in the ACM documentation: ACM 2.17: Accessing the Policy Generator from OpenShift GitOps.
apiVersion: argoproj.io/v1beta1
kind: ArgoCD
metadata:
name: policy-gitops
namespace: policy-gitops
spec:
kustomizeBuildOptions: --enable-alpha-plugins (1)
repo:
env:
- name: KUSTOMIZE_PLUGIN_HOME
value: /etc/kustomize/plugin
initContainers:
- name: policy-generator-install
image: registry.redhat.io/rhacm2/multicluster-operators-subscription-rhel9:v2.17 (2)
command:
- /bin/bash
args:
- -c
- cp /policy-generator/PolicyGenerator-not-fips-compliant /policy-generator-tmp/PolicyGenerator (3)
volumeMounts:
- mountPath: /policy-generator-tmp
name: policy-generator
volumeMounts:
- mountPath: /etc/kustomize/plugin/policy.open-cluster-management.io/v1/policygenerator
name: policy-generator
volumes:
- name: policy-generator
emptyDir: {}| 1 | Allows Kustomize to execute the generator plugin. |
| 2 | The tag must match the minor version of your ACM hub (oc get multiclusterhub -n open-cluster-management -o jsonpath='{.items[0].status.currentVersion}'). Update it after every hub upgrade, or let a policy do it (see Keeping the image tag in sync). |
| 3 | Copies the plugin from the image to the plugin directory. |
If your policy folders contain Helm charts that should be rendered before the policies are generated, add the environment variable POLICY_GEN_ENABLE_HELM with the value "true" to spec.repo.env.
Keeping the image tag in sync
A fixed tag has one weakness: after a hub upgrade, the PolicyGenerator in the repo-server is still the old one until somebody updates the tag. The ACM documentation provides an example of a policy that keeps the image tag in sync. After an upgrade of the hub, the policy changes the image of the init container automatically.
The policy runs on the hub only, so it needs a Placement for local-cluster. The hub carries the label local-cluster=true, and it is part of the cluster set global, which must be bound to the policy namespace:
apiVersion: cluster.open-cluster-management.io/v1beta2
kind: ManagedClusterSetBinding
metadata:
name: global
namespace: acm-policies
spec:
clusterSet: global (1)
---
apiVersion: policy.open-cluster-management.io/v1
kind: Policy
metadata:
name: argocd-policy-generator
namespace: acm-policies
annotations:
policy.open-cluster-management.io/standards: NIST SP 800-53
policy.open-cluster-management.io/categories: CM Configuration Management
policy.open-cluster-management.io/controls: CM-2 Baseline Configuration
spec:
remediationAction: enforce (2)
disabled: false
policy-templates:
- objectDefinition:
apiVersion: policy.open-cluster-management.io/v1
kind: ConfigurationPolicy
metadata:
name: argocd-policy-generator
spec:
severity: medium
object-templates:
- complianceType: musthave
objectDefinition:
apiVersion: argoproj.io/v1beta1
kind: ArgoCD
metadata:
name: policy-gitops
namespace: policy-gitops
spec:
kustomizeBuildOptions: --enable-alpha-plugins
repo:
env:
- name: KUSTOMIZE_PLUGIN_HOME
value: /etc/kustomize/plugin
initContainers:
- name: policy-generator-install
image: '{{ (index (lookup "apps/v1" "Deployment" "open-cluster-management" "multicluster-operators-hub-subscription").spec.template.spec.containers 0).image }}' (3)
command:
- /bin/bash
args:
- -c
- cp /policy-generator/PolicyGenerator-not-fips-compliant /policy-generator-tmp/PolicyGenerator
volumeMounts:
- mountPath: /policy-generator-tmp
name: policy-generator
volumeMounts:
- mountPath: /etc/kustomize/plugin/policy.open-cluster-management.io/v1/policygenerator
name: policy-generator
volumes:
- name: policy-generator
emptyDir: {}
---
apiVersion: cluster.open-cluster-management.io/v1beta1
kind: Placement
metadata:
name: hub-only
namespace: acm-policies
spec:
clusterSets:
- global
predicates:
- requiredClusterSelector:
labelSelector:
matchLabels:
local-cluster: "true" (4)
---
apiVersion: policy.open-cluster-management.io/v1
kind: PlacementBinding
metadata:
name: argocd-policy-generator
namespace: acm-policies
placementRef:
apiGroup: cluster.open-cluster-management.io
kind: Placement
name: hub-only
subjects:
- apiGroup: policy.open-cluster-management.io
kind: Policy
name: argocd-policy-generator| 1 | The cluster set global contains every managed cluster, including the hub. The Placement below narrows it down to the hub. Creating a ManagedClusterSetBinding requires the permissions managedclustersetbindings and managedclustersets/bind, which are not part of the ClusterRole above. Create it once as cluster administrator, or extend the ClusterRole if Argo CD shall sync it. |
| 2 | We enforce this policy. |
| 3 | lookup reads the deployment of the ACM subscription controller, and the template takes the image of its first container. That is exactly the image the init container needs, in the version of the running hub. |
| 4 | ACM sets this label on the hub’s own ManagedCluster. |
Two things to keep in mind:
Write this policy as plain YAML, not as PolicyGenerator input. It installs the generator, so it cannot depend on it (hen-egg problem). Argo CD can sync it from Git without the plugin.
One owner per object. The policy now manages the plugin part of the ArgoCD resource
policy-gitops. If that resource is also deployed by Argo CD (for example, from a Helm chart for OpenShift GitOps), remove the plugin settings from there, or the two will overwrite each other.
Policy health in Argo CD
By default, Argo CD considers a policy healthy as soon as it exists. That is not very useful: you want to see in Argo CD whether the policy is compliant. The policy collection of the Open Cluster Management project contains a policy that adds the matching health checks (resourceHealthChecks) to the ArgoCD resource: argocd-policy-healthchecks.yaml. The policy adds the health checks to every ArgoCD instance it finds on the cluster, so policy-gitops is covered as well. It is shipped with remediationAction: inform: set it to enforce, otherwise it only reports the missing health checks. After that, Argo CD shows a non-compliant policy as Degraded.
Pattern A: policies as code
Pattern A is the governance part of the integration: the policies live in Git, Argo CD on the hub brings them into the cluster, and ACM does the rest. You never create a policy in the ACM console or with oc apply again. Every change is a pull request, and the history of your compliance is stored in Git.
The example in this section builds two policies that show both sides of a policy:
service-mesh installs Red Hat OpenShift Service Mesh 3 on the selected clusters: the Operator and a control plane. It is enforced and the manifests are applied in a fixed order, because the control plane resources can only be created after the Operator has installed its CRDs.
no-kubeadmin checks that the
kubeadminsecret does not exist anymore. It only informs: ACM reports every cluster where the secret is still present, but nothing is deleted automatically.
Both start as plain Kubernetes manifests in Git. The PolicyGenerator wraps them into Policy, Placement and PlacementBinding, so you do not have to write the policy structure yourself.
This requires the hub Argo CD instance policy-gitops with the PolicyGenerator plugin, as described in the previous section. |
The following diagram shows the complete flow, from Git to the compliance status in ACM and Argo CD:
What happens under the hood:
The hub Argo CD renders the policies from Git and syncs them to the hub.
The result is a set of Policy, Placement and PlacementBinding objects in the namespace
acm-policies.ACM creates a replicated policy in the namespace of every selected cluster on the hub.
The policy framework on each managed cluster pulls its replicated policy, evaluates it and, in
enforcemode, fixes what is different.The compliance status flows back to the hub and shows up in the ACM governance view, per policy and cluster.
With the policy health checks, Argo CD shows a non-compliant policy as Degraded.
Repository layout
acm-policies/
├── kustomization.yaml
├── policy-generator.yaml
└── input
├── service-mesh
│ ├── 01-subscription.yaml # OpenShift Service Mesh 3 Operator
│ ├── 02-namespace-istio-system.yaml
│ ├── 03-namespace-istio-cni.yaml
│ ├── 04-istiocni.yaml # CNI plugin, required on OpenShift
│ └── 05-istio.yaml # the control plane
└── no-kubeadmin
└── kubeadmin-secret.yamlThe files in input/ are plain Kubernetes manifests, exactly as you would apply them with oc apply. The Operator is installed in the namespace openshift-operators, which already has an OperatorGroup for all namespaces, so neither a namespace nor an OperatorGroup is needed for it:
apiVersion: operators.coreos.com/v1alpha1
kind: Subscription
metadata:
name: servicemeshoperator3
namespace: openshift-operators
spec:
channel: stable-3.4 (1)
installPlanApproval: Automatic
name: servicemeshoperator3
source: redhat-operators
sourceNamespace: openshift-marketplace| 1 | Pins the minor version. Use stable to always get the latest version. |
The control plane consists of two cluster-scoped resources, IstioCNI and Istio, each pointing to its own namespace (istio-cni and istio-system):
apiVersion: sailoperator.io/v1
kind: IstioCNI
metadata:
name: default
spec:
namespace: istio-cniapiVersion: sailoperator.io/v1
kind: Istio
metadata:
name: default
spec:
namespace: istio-systemAnd the kubeadmin secret that must not exist:
| if your original kubeadmin secret is still present, you can ignore this yaml. Honestly, I am not even sure if this would work, once you have removed the secret. |
apiVersion: v1
kind: Secret
metadata:
name: kubeadmin
namespace: kube-systemThe kustomization.yaml only references the generator:
generators:
- policy-generator.yamlRender the folder locally before you push. If it does not render on your machine, it will not render in Argo CD either. With the PolicyGenerator binary in your Kustomize plugin path:
kustomize build --enable-alpha-plugins acm-policies/The output contains a Policy, a Placement and a PlacementBinding per policy.
The PolicyGenerator configuration
The file policy-generator.yaml is the heart of the setup. It does not contain any policy itself, it only describes which policies the generator shall create from the manifests in input/. It has two parts:
policyDefaultsapply to every policy: the namespace on the hub where the policies are created, the default remediation action and severity, and the Placement that decides which clusters receive them.policieslists the policies, each with its name, the manifests it contains and the settings that differ from the defaults.
For every entry in policies, the generator creates a Policy, a Placement and a PlacementBinding. The manifests are wrapped into a ConfigurationPolicy inside the Policy: by default all manifests of a policy into one, with consolidateManifests: false one per manifest. Our two policies use this as follows: service-mesh overrides the defaults to enforce and to apply its manifests in order, while no-kubeadmin keeps the defaults and only changes the compliance type.
apiVersion: policy.open-cluster-management.io/v1
kind: PolicyGenerator
metadata:
name: cluster-baseline
policyDefaults:
namespace: acm-policies
remediationAction: inform (1)
severity: medium
placement:
labelSelector:
matchExpressions:
- key: name (5)
operator: In
values:
- sno
- bm
policies:
- name: service-mesh
remediationAction: enforce (2)
orderManifests: true (3)
consolidateManifests: false
manifests:
- path: input/service-mesh/01-subscription.yaml
- path: input/service-mesh/02-namespace-istio-system.yaml
- path: input/service-mesh/03-namespace-istio-cni.yaml
- path: input/service-mesh/04-istiocni.yaml
- path: input/service-mesh/05-istio.yaml
- name: no-kubeadmin
manifests:
- path: input/no-kubeadmin/
complianceType: mustnothave (4)| 1 | Default for all policies: report only. |
| 2 | This policy is enforced. |
| 3 | Creates one ConfigurationPolicy per manifest and makes each one depend on the previous one: first the Subscription, then the namespaces, then IstioCNI and Istio. Note that the Subscription policy is compliant as soon as the Subscription exists, not when the Operator is installed. Until the CRDs are there, the IstioCNI policy reports an error and is evaluated again. If you want to wait for the installed Operator, use an OperatorPolicy instead of the Subscription, as shown in Pattern B. |
| 4 | The kubeadmin secret must not exist. In inform mode, ACM reports every cluster where it still exists. Remove it with care, only after an identity provider is configured. |
| 5 | Selects the clusters sno and bm. ACM sets the label name on every ManagedCluster. For a growing fleet, a label such as environment=production or the cluster set label cluster.open-cluster-management.io/clusterset scales better than a list of names. |
Bind the cluster sets to the policy namespace
A Placement only sees the clusters of cluster sets that are bound to its own namespace. The bindings from Ep.19 live in openshift-gitops, so the Placements in acm-policies would find no cluster at all. ACM then shows the warning "ClusterSets failed to load. Verify that there is at least one ClusterSet bound to your selected namespace."
Bind the cluster set production to acm-policies, and global as well if you use the policy that keeps the PolicyGenerator image in sync (its Placement selects the hub from global):
apiVersion: cluster.open-cluster-management.io/v1beta2
kind: ManagedClusterSetBinding
metadata:
name: production
namespace: acm-policies
spec:
clusterSet: production
---
apiVersion: cluster.open-cluster-management.io/v1beta2
kind: ManagedClusterSetBinding
metadata:
name: global
namespace: acm-policies
spec:
clusterSet: globalThe generated Placements select sno and bm by name, from the cluster sets bound to acm-policies. Check the result:
oc get placementdecisions -n acm-policies \
-o custom-columns=NAME:.metadata.name,CLUSTERS:.status.decisions[*].clusterNameThe Argo CD Application
On the hub, one Application syncs the folder:
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
name: acm-policies
namespace: policy-gitops (1)
spec:
project: default
source:
repoURL: https://github.com/tjungbauer/acm-policies.git
targetRevision: main
path: .
destination:
server: https://kubernetes.default.svc
namespace: acm-policies
syncPolicy:
automated:
prune: true
selfHeal: true| 1 | The Application belongs to the instance policy-gitops. No CreateNamespace=true: the namespace acm-policies already exists with the managed-by label. |
Replicated policies
When ACM propagates a policy, it creates a copy in the namespace of every managed cluster on the hub, the replicated policy. Argo CD could treat these copies as extraneous resources of the Application and show it as OutOfSync or even try to delete them. ACM prevents that: it automatically sets the annotation argocd.argoproj.io/compare-options: IgnoreExtraneous on every replicated policy.
Pattern B: a policy bootstraps Argo CD
The bootstrap question from Ep.18: who installs Argo CD on a new cluster? With ACM, the answer is a policy. As soon as a cluster is imported and matches the Placement of the policy, the policy installs OpenShift GitOps and creates a root Application that points to the cluster’s folder in Git. From there, Argo CD configures the rest.
We extend our repository with:
acm-policies/
└── input
└── gitops-bootstrap
├── 01-namespace.yaml # openshift-gitops-operator
├── 02-operatorpolicy.yaml # OpenShift GitOps Operator
├── 03-controller-rbac.yaml # rights for the local controller
├── 04-repo-credentials.yaml # Git credentials, copied from the hub
└── 05-root-application.yaml # app-of-apps for this clusterThe first file creates the namespace for the Operator. The OperatorPolicy in the next step expects it to exist:
apiVersion: v1
kind: Namespace
metadata:
name: openshift-gitops-operatorapiVersion: policy.open-cluster-management.io/v1beta1
kind: OperatorPolicy (1)
metadata:
name: openshift-gitops-operator
spec:
remediationAction: enforce
severity: high
complianceType: musthave
subscription:
name: openshift-gitops-operator
namespace: openshift-gitops-operator
channel: gitops-1.22 (2)
source: redhat-operators
sourceNamespace: openshift-marketplace
upgradeApproval: Automatic| 1 | The PolicyGenerator adds OperatorPolicy manifests unchanged as a policy template, no extra wrapping. |
| 2 | Pin the channel. Moving to the next version is a one-line pull request. |
The root Application is the app-of-apps that contains the Application for every cluster.
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
name: root
namespace: openshift-gitops
spec:
project: default
source:
repoURL: https://github.com/tjungbauer/openshift-clusterconfig-gitops.git
targetRevision: main
path: '{{hub printf "clusters/%s/bootstrap" .ManagedClusterName hub}}' (1)
destination:
server: https://kubernetes.default.svc
syncPolicy:
automated:
prune: false (2)
selfHeal: true| 1 | A hub template, resolved on the hub for every cluster: sno gets clusters/sno/bootstrap. |
| 2 | Never prune on the root Application. A broken path would otherwise remove everything below it. |
The local Argo CD must be allowed to create whatever the root Application brings in. 03-controller-rbac.yaml grants its application-controller cluster-admin rights, the same as in the pull model of Ep.19:
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
name: argo-admin
subjects:
- kind: ServiceAccount
name: openshift-gitops-argocd-application-controller (1)
namespace: openshift-gitops
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: ClusterRole
name: cluster-admin (2)| 1 | The service account of the default instance openshift-gitops, which the Operator creates after the installation. |
| 2 | Simple and usually fine for platform configuration. For application teams, prefer namespaces labelled with argocd.argoproj.io/managed-by. |
And the generator configuration ties it together:
policies:
- name: gitops-bootstrap
remediationAction: enforce
orderManifests: true (1)
consolidateManifests: false
placement:
labelSelector:
matchExpressions:
- key: name (2)
operator: In
values:
- sno
- bm
manifests:
- path: input/gitops-bootstrap/01-namespace.yaml
- path: input/gitops-bootstrap/02-operatorpolicy.yaml
- path: input/gitops-bootstrap/03-controller-rbac.yaml
- path: input/gitops-bootstrap/04-repo-credentials.yaml
- path: input/gitops-bootstrap/05-root-application.yaml| 1 | The order matters: the Application CRD only exists after the operator is installed. With orderManifests, the root Application is only created when the operator policy is compliant. |
| 2 | The same selection as for the other policies: the clusters sno and bm, selected by the name label that ACM sets on every ManagedCluster. In a larger fleet, a dedicated label such as gitops=enabled makes the bootstrap an opt-in per cluster. |
Pattern B is meant for clusters that get their Argo CD from this policy. If you followed the pull or agent part of Ep.19, the GitOps add-on of ACM has already installed OpenShift GitOps on sno and bm, and the policy and the add-on would then manage the same operator. Decide which of the two owns the operator before you enforce this policy, or test Pattern B on a cluster without the add-on. |
Who owns what afterwards? The policy owns exactly five objects: the namespace, the operator, the ClusterRoleBinding, the repository secret and the root Application. Everything the root Application brings in belongs to Argo CD. Keep it that way: if you also manage the OpenShift GitOps Subscription with Argo CD on the spoke, the two will fight.
The result in ACM & Argo CD
Once Argo CD has synced the repository, the policies show up in the ACM console under Governance.
The list of Policies shows the three policies of this article in the namespace acm-policies: service-mesh, no-kubeadmin and gitops-bootstrap. For each policy you see the remediation action (inform or enforce), the cluster violations and the policy set, if there is one.

A click on a policy opens the details. The Details tab shows the policy templates, here the ConfigurationPolicies and the OperatorPolicy that the PolicyGenerator created from the plain manifests, and the Placement with the selected clusters sno and bm.
The no-kubeadmin policy is a good example for inform: ACM only reports that the secret kubeadmin still exists, it does not delete it.

In the Argo CD UI we can also verify the status (including the resource tree).
The Application acm-policies shows the status Degraded. This is because the policy no-kubeadmin is non-compliant … the secrets are still present.

The resource tree proves this:

Summary
That concludes the mini-series. ACM decides where and checks compliance, Argo CD decides what and keeps it in sync, and Git is the only place where changes are made.
And as always: If it is not in Git, it does not exist. That includes your ACM policies.
| The diagrams in this article have been manually drawn on a whiteboard. AI then generated the SVGs from photos and made them look equal in all articles. |
References
Show all 20 parts
Copyright © 2020 - 2026 Toni Schmidbauer & Thomas Jungbauer
![image from [Ep.20] Managing ACM Policies with OpenShift GitOps](/gitopscollection/images/logos/ep-20-gitops.png)
![Featured image for [Ep.18] OpenShift GitOps or Advanced Cluster Management? Choosing the right tool](/gitopscollection/images/logos/ep-18-gitops.png)
![Featured image for [Ep.19] ACM and OpenShift GitOps: push, pull and the Argo CD Agent](/gitopscollection/images/logos/ep-19-gitops.png)
![Featured image for [Ep.6] Setup & Configure Advanced Cluster Security](/imgproc/static/images/share/series/gitops-episodes_hu_2981692d2a7c77f2.png)
Discussion
Comments are powered by GitHub Discussions. To participate, you'll need a GitHub account.
By loading comments, you agree to GitHub's Privacy Policy. Your data is processed by GitHub, not by this website.