Skip to content

Commit 644fd05

Browse files
committed
Add CA issuer, hub-spoke trust config, and root CA monitoring
Adds CA ClusterIssuer for disconnected environments with existing PKI, KlusterletConfig for hub-spoke CA trust distribution, and root CA expiration monitoring via ACM Policy. The KlusterletConfig with UseCustomCABundles ensures spokes trust the hub's root CA (CA:TRUE) instead of the leaf serving cert (CA:FALSE). This makes cert rotations seamless and eliminates the chicken-and-egg problem where spokes can't receive ManifestWork updates because they don't trust the hub's new cert. Tested on OCP 4.21.15 hub+spoke with cert-manager v1.19.0. Full test results: https://gist.github.com/sebrandon1/7265d68c5add6adb1313dce5b695e40d Signed-off-by: Brandon Palm <bpalm@redhat.com>
1 parent abd1ee2 commit 644fd05

20 files changed

Lines changed: 405 additions & 22 deletions

File tree

telco-core/configuration/reference-crs-kube-compare/compare_ignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,3 +38,6 @@ custom-manifests/subscription-validator.yaml
3838

3939
# This is an object-template-raw used only to validate pre-caching state
4040
custom-manifests/precache-validator.yaml
41+
42+
# cert-manager config CRs (user-specific)
43+
optional/cert-manager/certManagerClusterIssuerCA.yaml
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
---
2+
apiVersion: cert-manager.io/v1
3+
kind: ClusterIssuer
4+
metadata:
5+
name: {{ .metadata.name | default "ca-issuer" }}
6+
spec:
7+
ca:
8+
secretName: {{ .spec.ca.secretName | default "root-ca-secret" }}

telco-core/configuration/reference-crs/optional/cert-manager/README.md

Lines changed: 51 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,12 @@ Cert-manager automates the management and issuance of TLS certificates from vari
1616
- `certManagerOperatorgroup.yaml` - Creates the OperatorGroup for cert-manager
1717
- `certManagerSubscription.yaml` - Installs the OpenShift cert-manager operator
1818

19-
### Certificate Issuer
20-
- `certManagerClusterIssuer.yaml` - Configures an ACME ClusterIssuer using Let's Encrypt with DNS-01 challenge
19+
### Certificate Issuers
20+
21+
Two ClusterIssuer types are available. Choose one based on your environment:
22+
23+
- `certManagerClusterIssuer.yaml` - **ACME issuer** using Let's Encrypt with DNS-01 challenge (requires internet connectivity)
24+
- `certManagerClusterIssuerCA.yaml` - **CA issuer** using an internal certificate authority (disconnected environments with existing PKI)
2125

2226
### Certificate Resources
2327
- `apiServerCertificate.yaml` - Creates a certificate for the API Server endpoint
@@ -31,10 +35,18 @@ Cert-manager automates the management and issuance of TLS certificates from vari
3135

3236
Before applying these configurations, you must customize the following:
3337

34-
1. **ClusterIssuer** (`certManagerClusterIssuer.yaml`):
38+
1. **ClusterIssuer** — choose one of the following issuer configurations:
39+
40+
**Option A: ACME issuer** (`certManagerClusterIssuer.yaml`) — for connected environments:
3541
- Update `email` with your contact email
3642
- Configure the appropriate DNS provider for DNS-01 challenge (example shows Route53)
37-
- Add necessary credentials for your DNS provider
43+
- Reference pre-created Secrets for DNS provider credentials via `secretRef` — do not commit credentials in manifests
44+
45+
**Option B: Existing internal CA** (disconnected environments with existing PKI):
46+
- This issuer does **not** generate its own CA — it requires a pre-existing CA from your organization's PKI
47+
- Create a Secret named `root-ca-secret` in the `cert-manager` namespace containing your CA's `tls.crt` and `tls.key`
48+
- Deploy only `certManagerClusterIssuerCA.yaml`
49+
- Update the Certificate resources to reference `ca-issuer` in the `issuerRef` field
3850

3951
2. **Certificates** (`apiServerCertificate.yaml` and `ingressCertificate.yaml`):
4052
- Update `commonName` and `dnsNames` to match your cluster's domain
@@ -51,7 +63,8 @@ Before applying these configurations, you must customize the following:
5163
3. Deploy the ClusterIssuer
5264
4. Deploy the Certificate resources
5365
5. Wait for certificates to be issued and secrets created
54-
6. Apply the APIServer and IngressController configurations
66+
6. **(Option B only)** Update kubeconfig to trust the new root CA (see "Kubeconfig Trust" section below)
67+
7. Apply the APIServer and IngressController configurations
5568

5669
## Certificate Verification
5770

@@ -61,9 +74,41 @@ After applying these configurations, verify that:
6174
- API Server is using the certificate: Test HTTPS connection to API endpoint
6275
- Ingress is using the certificate: Test HTTPS connection to any route
6376

77+
## Important: Kubeconfig Trust After API Server Cert Replacement
78+
79+
> **Note:** For Option B (Existing internal CA), you must complete this kubeconfig update
80+
> *before* applying the APIServer configuration (step 7 in the deployment order above).
81+
> Applying the APIServer configuration first will lock you out.
82+
83+
> **Warning:** When cert-manager replaces the API server certificate with one signed by a custom CA,
84+
> existing kubeconfig files become invalid. The embedded `certificate-authority-data` still references
85+
> the original cluster CA and cannot verify the new certificate. All `oc` and API client commands
86+
> will fail with `x509: certificate signed by unknown authority`.
87+
88+
### Updating kubeconfig
89+
90+
1. Extract the new root CA certificate:
91+
```bash
92+
oc get secret root-ca-secret -n cert-manager -o jsonpath='{.data.tls\.crt}' | base64 -d > /tmp/root-ca.crt
93+
```
94+
95+
2. Update your kubeconfig to trust the new CA:
96+
```bash
97+
oc config set-cluster $(oc config current-context | cut -d/ -f2) \
98+
--certificate-authority=/tmp/root-ca.crt --embed-certs
99+
```
100+
101+
3. Verify connectivity:
102+
```bash
103+
oc cluster-info
104+
```
105+
106+
### Best practice for PKI environments
107+
108+
Generate a root CA once and use it as the root for your PKI (the ACME issuer or CA issuer your clusters will use). Add the root CA PEM to `/etc/pki/ca-trust/source/anchors/` on your workstation and run `update-ca-trust`. All certificates issued from that root CA will then be trusted without per-cluster kubeconfig updates.
109+
64110
## References
65111

66112
- [OpenShift Cert-Manager Operator Documentation](https://docs.openshift.com/container-platform/latest/security/cert_manager_operator/index.html)
67113
- [Cert-Manager Documentation](https://cert-manager.io/docs/)
68114
- [ACME DNS-01 Challenge Configuration](https://cert-manager.io/docs/configuration/acme/dns01/)
69-
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
---
2+
apiVersion: cert-manager.io/v1
3+
kind: ClusterIssuer
4+
metadata:
5+
name: ca-issuer
6+
spec:
7+
ca:
8+
# References a Secret in the cert-manager namespace containing tls.crt and tls.key
9+
# for your internal CA. Create this Secret before applying the ClusterIssuer.
10+
secretName: root-ca-secret

telco-hub/configuration/reference-crs-kube-compare/compare_ignore

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,15 @@ required/gitops/addPluginsPolicy.yaml
3333
# not include the full policy content due to policy templating.
3434
required/acm/observabilityRoutePolicy.yaml
3535

36+
# cert-manager config CRs (user-specific, and policies with hub-side templating)
37+
optional/cert-manager/certManagerClusterIssuerCA.yaml
38+
optional/cert-manager/certManagerRootCAExpirationPolicy.yaml
39+
optional/cert-manager/certManagerRootCAExpirationPolicyPlacement.yaml
40+
optional/cert-manager/certManagerRootCAExpirationPolicyPlacementBinding.yaml
41+
optional/cert-manager/certManagerHubCAConfigMap.yaml
42+
optional/cert-manager/certManagerKlusterletConfig.yaml
43+
optional/cert-manager/kustomization.yaml
44+
3645
required/gitops/extra-manifests-policy.yaml
3746
# ArgoCD files
3847
kustomization.yaml

telco-hub/configuration/reference-crs-kube-compare/metadata.yaml

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -186,9 +186,6 @@ fieldsToOmit:
186186
isPrefix: true
187187
- pathToKey: metadata.annotations."installer.open-cluster-management.io"
188188
isPrefix: true
189-
- pathToKey: metadata.annotations.createdAt
190-
- pathToKey: metadata.annotations.createdBy
191-
- pathToKey: metadata.annotations."oc-mirror_version"
192189
- pathToKey: metadata.annotations."security.openshift.io/MinimallySufficientPodSecurityStandard"
193190
- pathToKey: metadata.labels."kubernetes.io/metadata.name"
194191
- pathToKey: metadata.labels."security.openshift.io/scc.podSecurityLabelSync"

telco-hub/configuration/reference-crs-kube-compare/optional/cert-manager/certManagerCertificatePolicy.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ spec:
1919
spec:
2020
minimumDuration: {{ .spec.minimumDuration | default "720h" }}
2121
namespaceSelector:
22-
include:{{- template "unorderedListAllowExtra" (list .spec.namespaceSelector.include (list "openshift-ingress" "openshift-config") ) }}
22+
include:{{- template "unorderedListAllowExtra" (list .spec.namespaceSelector.include (list "openshift-ingress" "openshift-config" "cert-manager") ) }}
2323
remediationAction: inform
2424
severity: low
2525
remediationAction: inform
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
---
2+
apiVersion: cert-manager.io/v1
3+
kind: ClusterIssuer
4+
metadata:
5+
name: {{ .metadata.name | default "ca-issuer" }}
6+
annotations:
7+
argocd.argoproj.io/sync-wave: "-28"
8+
argocd.argoproj.io/sync-options: SkipDryRunOnMissingResource=true
9+
spec:
10+
ca:
11+
secretName: {{ .spec.ca.secretName | default "root-ca-secret" }}

telco-hub/configuration/reference-crs/optional/cert-manager/README.md

Lines changed: 108 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,12 @@ Cert-manager automates the management and issuance of TLS certificates from vari
1616
- `certManagerOperatorgroup.yaml` - Creates the OperatorGroup for cert-manager
1717
- `certManagerSubscription.yaml` - Installs the OpenShift cert-manager operator
1818

19-
### Certificate Issuer
20-
- `certManagerClusterIssuer.yaml` - Configures an ACME ClusterIssuer using Let's Encrypt with DNS-01 challenge
19+
### Certificate Issuers
20+
21+
Two ClusterIssuer types are available. Choose one based on your environment:
22+
23+
- `certManagerClusterIssuer.yaml` - **ACME issuer** using Let's Encrypt with DNS-01 challenge (requires internet connectivity)
24+
- `certManagerClusterIssuerCA.yaml` - **CA issuer** using an internal certificate authority (disconnected environments with existing PKI)
2125

2226
### Certificate Resources
2327
- `apiServerCertificate.yaml` - Creates a certificate for the API Server endpoint
@@ -31,10 +35,18 @@ Cert-manager automates the management and issuance of TLS certificates from vari
3135

3236
Before applying these configurations, you must customize the following:
3337

34-
1. **ClusterIssuer** (`certManagerClusterIssuer.yaml`):
38+
1. **ClusterIssuer** — choose one of the following issuer configurations:
39+
40+
**Option A: ACME issuer** (`certManagerClusterIssuer.yaml`) — for connected environments:
3541
- Update `email` with your contact email
3642
- Configure the appropriate DNS provider for DNS-01 challenge (example shows Route53)
37-
- Add necessary credentials for your DNS provider
43+
- Reference pre-created Secrets for DNS provider credentials via `secretRef` — do not commit credentials in manifests
44+
45+
**Option B: Existing internal CA** (disconnected environments with existing PKI):
46+
- This issuer does **not** generate its own CA — it requires a pre-existing CA from your organization's PKI
47+
- Create a Secret named `root-ca-secret` in the `cert-manager` namespace containing your CA's `tls.crt` and `tls.key`
48+
- Deploy only `certManagerClusterIssuerCA.yaml`
49+
- Update the Certificate resources to reference `ca-issuer` in the `issuerRef` field
3850

3951
2. **Certificates** (`apiServerCertificate.yaml` and `ingressCertificate.yaml`):
4052
- Update `commonName` and `dnsNames` to match your cluster's domain
@@ -49,8 +61,10 @@ Before applying these configurations, you must customize the following:
4961
1. Deploy operator installation files (NS, OperatorGroup, Subscription)
5062
2. Wait for operator to be ready
5163
3. Deploy the ClusterIssuer
52-
4. Wait for certificates to be issued and secrets created
53-
5. Apply the APIServer and IngressController configurations
64+
4. Deploy the Certificate resources
65+
5. Wait for certificates to be issued and secrets created
66+
6. **(Option B only)** Update kubeconfig to trust the new root CA (see "Kubeconfig Trust" section below)
67+
7. Apply the APIServer and IngressController configurations
5468

5569
## Certificate Verification
5670

@@ -60,9 +74,96 @@ After applying these configurations, verify that:
6074
- API Server is using the certificate: Test HTTPS connection to API endpoint
6175
- Ingress is using the certificate: Test HTTPS connection to any route
6276

77+
## Important: Kubeconfig Trust After API Server Cert Replacement
78+
79+
> **Note:** For Option B (Existing internal CA), you must complete this kubeconfig update
80+
> *before* applying the APIServer configuration (step 7 in the deployment order above).
81+
> Applying the APIServer configuration first will lock you out.
82+
83+
> **Warning:** When cert-manager replaces the API server certificate with one signed by a custom CA,
84+
> existing kubeconfig files become invalid. The embedded `certificate-authority-data` still references
85+
> the original cluster CA and cannot verify the new certificate. All `oc` and API client commands
86+
> will fail with `x509: certificate signed by unknown authority`.
87+
88+
### Updating kubeconfig
89+
90+
1. Extract the new root CA certificate:
91+
```bash
92+
oc get secret root-ca-secret -n cert-manager -o jsonpath='{.data.tls\.crt}' | base64 -d > /tmp/root-ca.crt
93+
```
94+
95+
2. Update your kubeconfig to trust the new CA:
96+
```bash
97+
oc config set-cluster $(oc config current-context | cut -d/ -f2) \
98+
--certificate-authority=/tmp/root-ca.crt --embed-certs
99+
```
100+
101+
3. Verify connectivity:
102+
```bash
103+
oc cluster-info
104+
```
105+
106+
### Best practice for PKI environments
107+
108+
Generate a root CA once and use it as the root for your PKI (the ACME issuer or CA issuer your clusters will use). Add the root CA PEM to `/etc/pki/ca-trust/source/anchors/` on your workstation and run `update-ca-trust`. All certificates issued from that root CA will then be trusted without per-cluster kubeconfig updates.
109+
110+
## Hub-Spoke Trust with ACM
111+
112+
When cert-manager issues certificates for the hub's API server and ingress, managed spokes must trust the cert-manager root CA to maintain connectivity. The reference configuration includes a `KlusterletConfig` and CA ConfigMap to distribute the root CA to spokes automatically.
113+
114+
### Hub-spoke trust files
115+
116+
- `certManagerHubCAConfigMap.yaml` — ConfigMap in `multicluster-engine` namespace containing the cert-manager root CA, labeled for the import controller
117+
- `certManagerKlusterletConfig.yaml` — KlusterletConfig that switches spoke CA verification from auto-detected leaf cert to the custom root CA bundle
118+
119+
After deploying these resources, annotate each managed cluster:
120+
121+
```bash
122+
oc annotate managedcluster <spoke-name> \
123+
agent.open-cluster-management.io/klusterlet-config=cert-manager-ca-config
124+
```
125+
126+
### Why this is needed
127+
128+
By default, ACM embeds the hub's leaf serving cert (`CA:FALSE`) in the klusterlet bootstrap kubeconfig. This means every cert rotation requires a ManifestWork update, and a full CA replacement breaks all spokes immediately. The `KlusterletConfig` with `UseCustomCABundles` replaces the leaf cert with the root CA (`CA:TRUE`), so any certificate signed by that root — current or rotated — is automatically trusted.
129+
130+
### Greenfield (cert-manager before spoke deployment)
131+
132+
1. Deploy cert-manager on the hub, create the CA, issue hub API/ingress certs
133+
2. Deploy `certManagerHubCAConfigMap.yaml` and `certManagerKlusterletConfig.yaml`
134+
3. Annotate the managed cluster (or configure as a default KlusterletConfig)
135+
4. Deploy spokes via ZTP — they register with the root CA in their trust store
136+
5. Cert rotations are seamless with no intervention required
137+
138+
### Brownfield (cert-manager on existing hub with connected spokes)
139+
140+
The order matters — distribute the CA **before** replacing the hub certs:
141+
142+
1. Install cert-manager on the hub, create the CA — but **do not apply certs to the APIServer/IngressController yet**
143+
2. Deploy `certManagerHubCAConfigMap.yaml` with the root CA PEM
144+
3. Deploy `certManagerKlusterletConfig.yaml`
145+
4. Annotate all managed clusters
146+
5. Wait for the import controller to regenerate bootstrap kubeconfigs (check logs for `create a new bootstrap kubeconfig`)
147+
6. **Now** apply the cert-manager certs to the APIServer and IngressController
148+
7. Spokes stay connected because they already trust the root CA
149+
150+
### Cert rotation
151+
152+
Once the root CA is in the klusterlet's trust store, cert rotations are seamless. The klusterlet trusts any certificate signed by the root CA regardless of serial number, with no ManifestWork timing dependency.
153+
154+
## Root CA Expiration Monitoring
155+
156+
The `certManagerRootCAExpirationPolicy.yaml` creates a PrometheusRule that monitors the root CA certificate expiration using the `certmanager_certificate_expiration_timestamp_seconds` metric:
157+
158+
- **Warning** at 90 days before expiry
159+
- **Critical** at 30 days before expiry
160+
161+
This is distinct from the existing `certManagerCertificatePolicy.yaml` which monitors leaf certificate expiration in `openshift-ingress`, `openshift-config`, and `cert-manager` namespaces via ACM CertificatePolicy. Both should be deployed together for comprehensive certificate monitoring.
162+
63163
## References
64164

65165
- [OpenShift Cert-Manager Operator Documentation](https://docs.openshift.com/container-platform/latest/security/cert_manager_operator/index.html)
66166
- [Cert-Manager Documentation](https://cert-manager.io/docs/)
67167
- [ACME DNS-01 Challenge Configuration](https://cert-manager.io/docs/configuration/acme/dns01/)
68-
168+
- [Hub-Spoke Trust — Complete Solution](https://gist.github.com/sebrandon1/7265d68c5add6adb1313dce5b695e40d)
169+
- [Hub-Spoke Trust Test Results](https://gist.github.com/sebrandon1/483180614951d23174c4e365a9a02a34)

telco-hub/configuration/reference-crs/optional/cert-manager/certManagerCertificatePolicy.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ spec:
2222
include:
2323
- openshift-ingress
2424
- openshift-config
25+
- cert-manager
2526
remediationAction: inform
2627
severity: low
2728
remediationAction: inform

0 commit comments

Comments
 (0)