Skip to content

OCPBUGS-77781: fix(certs): normalize IP SANs to stop dual-stack KAS cert churn - #9286

Open
RamLavi wants to merge 1 commit into
openshift:mainfrom
RamLavi:OCPBUGS-77781
Open

OCPBUGS-77781: fix(certs): normalize IP SANs to stop dual-stack KAS cert churn#9286
RamLavi wants to merge 1 commit into
openshift:mainfrom
RamLavi:OCPBUGS-77781

Conversation

@RamLavi

@RamLavi RamLavi commented Aug 12, 2026

Copy link
Copy Markdown

What this PR does / why we need it

On dual-stack HostedClusters, ValidateKeyPair compared certificate IP SANs with cmp.Diff + SortSlices(bytes.Compare) on raw net.IP byte slices.

After an X.509 round-trip, IPv4 SANs are stored as 4-byte values, while desired IPs from net.ParseIP are 16-byte IPv4-mapped. Mixed lengths change sort order for some IPv6 service CIDRs (e.g. 2001:db8:…), so validation falsely reports that IP addresses differ even when the logical set is unchanged.

CPO then resigns kas-server-crt every reconcile → kube-apiserver Deployment config-hash / generation churn → ReplicaSet pile-up.

Fix: normalize both sides with net.IP.To16() before comparing.

Test: unit test reconciles twice for ipv4-only and dual-stack IPs and asserts tls.crt is not regenerated.

Which issue(s) this PR fixes

Fixes OCPBUGS-77781

Special notes for your reviewer

Manual verification (kubevirt HCP, stock CPO → fixed CPO)

Dual-stack HC with serviceNetwork including 2001:db8:2::/112 (ULA fd02::/112 alone often does not trigger the sort mismatch).

Before (stock CPO): between two samples, RS count and Deployment generation climb while SANs stay identical:

# --- sample 1 ---

# Number of kube-apiserver ReplicaSets right now.
root@hitchhiker-02:~# oc get rs -n $NS -l app=kube-apiserver --no-headers | wc -l
5484

# Current Deployment generation (bumps when the Deployment spec changes).
root@hitchhiker-02:~# oc get deploy kube-apiserver -n $NS -o jsonpath='{.metadata.generation}{"\n"}'
5556

# kas-server-crt has dual-stack IP SANs (IPv4 + 2001:db8:2::1) — the condition that triggers the bug.
root@hitchhiker-02:~# oc get secret kas-server-crt -n $NS -o jsonpath='{.data.tls\.crt}' | base64 -d | openssl x509 -noout -text | grep -A2 'Subject Alternative Name'
            X509v3 Subject Alternative Name: 
                DNS:localhost, DNS:kubernetes, DNS:kubernetes.default, DNS:kubernetes.default.svc, DNS:kubernetes.default.svc.cluster.local, DNS:openshift, DNS:openshift.default, DNS:openshift.default.svc, DNS:openshift.default.svc.cluster.local, DNS:api.ocpbugs-77781.hypershift.local, IP Address:127.0.0.1, IP Address:0:0:0:0:0:0:0:1, IP Address:172.31.0.1, IP Address:2001:DB8:2:0:0:0:0:1, IP Address:172.20.0.1, IP Address:192.168.122.240
    Signature Algorithm: sha256WithRSAEncryption

# --- sample 2 (same checks, shortly after) ---
# RS count increased (5484 → 5502) without any intentional rollout from us.
root@hitchhiker-02:~# oc get rs -n $NS -l app=kube-apiserver --no-headers | wc -l
5502

# Generation increased (5556 → 5563) — Deployment spec is still being rewritten.
root@hitchhiker-02:~# oc get deploy kube-apiserver -n $NS -o jsonpath='{.metadata.generation}{"\n"}'
5563

# Same SAN set as sample 1. Desired names/IPs did not change, yet the Deployment keeps rolling.
root@hitchhiker-02:~# oc get secret kas-server-crt -n $NS -o jsonpath='{.data.tls\.crt}' | base64 -d | openssl x509 -noout -text | grep -A2 'Subject Alternative Name'
            X509v3 Subject Alternative Name: 
                DNS:localhost, DNS:kubernetes, DNS:kubernetes.default, DNS:kubernetes.default.svc, DNS:kubernetes.default.svc.cluster.local, DNS:openshift, DNS:openshift.default, DNS:openshift.default.svc, DNS:openshift.default.svc.cluster.local, DNS:api.ocpbugs-77781.hypershift.local, IP Address:127.0.0.1, IP Address:0:0:0:0:0:0:0:1, IP Address:172.31.0.1, IP Address:2001:DB8:2:0:0:0:0:1, IP Address:172.20.0.1, IP Address:192.168.122.240
    Signature Algorithm: sha256WithRSAEncryption

After (CPO image with this fix): over 60s, metrics stay flat:

root@hitchhiker-02:~# oc get rs -n $NS -l app=kube-apiserver --no-headers | wc -l
4
root@hitchhiker-02:~# oc get deploy kube-apiserver -n $NS -o jsonpath='{.metadata.generation}{"\n"}'
5721
root@hitchhiker-02:~# oc get secret kas-server-crt -n $NS -o jsonpath='{.metadata.resourceVersion}{"\n"}'
2141595
root@hitchhiker-02:~# sleep 60
root@hitchhiker-02:~# oc get rs -n $NS -l app=kube-apiserver --no-headers | wc -l
4
root@hitchhiker-02:~# oc get deploy kube-apiserver -n $NS -o jsonpath='{.metadata.generation}{"\n"}'
5721
root@hitchhiker-02:~# oc get secret kas-server-crt -n $NS -o jsonpath='{.metadata.resourceVersion}{"\n"}'
2141595

Checklist

  • Subject and description added to both, commit and PR.
  • Relevant issues have been referenced.
  • This change includes docs.
  • This change includes unit tests.

Summary by CodeRabbit

  • Bug Fixes

    • Improved certificate validation for IPv4 and IPv6 address configurations.
    • Prevented unnecessary certificate and key changes during repeated reconciliation.
  • Tests

    • Added coverage confirming certificate reconciliation remains stable for IPv4-only and dual-stack configurations.

@openshift-merge-bot

Copy link
Copy Markdown
Contributor

Pipeline controller notification
This repo is configured to use the pipeline controller. Second-stage tests will be triggered either automatically or after lgtm label is added, depending on the repository configuration. The pipeline controller will automatically detect which contexts are required and will utilize /test Prow commands to trigger the second stage.

For optional jobs, comment /test ? to see a list of all defined jobs. To trigger manually all jobs from second stage use /pipeline required command.

This repository is configured in: LGTM mode

@openshift-ci-robot openshift-ci-robot added jira/severity-important Referenced Jira bug's severity is important for the branch this PR is targeting. jira/valid-reference Indicates that this PR references a valid Jira ticket of any type. jira/invalid-bug Indicates that a referenced Jira bug is invalid for the branch this PR is targeting. labels Aug 12, 2026
@openshift-ci-robot

Copy link
Copy Markdown

@RamLavi: This pull request references Jira Issue OCPBUGS-77781, which is invalid:

  • expected the bug to target the "5.0.0" version, but no target version was set

Comment /jira refresh to re-evaluate validity if changes to the Jira bug are made, or edit the title of this pull request to link to a different bug.

The bug has been updated to refer to the pull request using the external bug tracker.

Details

In response to this:

ValidateKeyPair compared cert IP SANs with raw byte slices. After X.509 round-trip, IPv4 SANs are 4 bytes while desired IPs from net.ParseIP are 16-byte IPv4-mapped, so dual-stack SANs mis-sorted and falsely failed validation. CPO then resigned kas-server-crt every reconcile, churning the kube-apiserver Deployment.

Normalize IPs with net.IP.To16() before compare. Add a unit test that reconciles twice for ipv4-only and dual-stack and asserts the cert is not regenerated.

Assisted-by: Claude Sonnet 4.6 noreply@anthropic.com

What this PR does / why we need it:

Which issue(s) this PR fixes:

Fixes

Special notes for your reviewer:

Checklist:

  • Subject and description added to both, commit and PR.
  • Relevant issues have been referenced.
  • This change includes docs.
  • This change includes unit tests.

Instructions for interacting with me using PR comments are available here. If you have questions or suggestions related to my behavior, please file an issue against the openshift-eng/jira-lifecycle-plugin repository.

@coderabbitai

coderabbitai Bot commented Aug 12, 2026

Copy link
Copy Markdown
Contributor

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Repository YAML (base), Central YAML (inherited)

Review profile: CHILL

Plan: Pro Plus

Run ID: 69f2da50-c904-44d1-bffb-ec086ca05106

📥 Commits

Reviewing files that changed from the base of the PR and between 680582f and 593ea7f.

📒 Files selected for processing (1)
  • support/certs/tls_test.go
🚧 Files skipped from review as they are similar to previous changes (1)
  • support/certs/tls_test.go

📝 Walkthrough

Walkthrough

ValidateKeyPair now converts certificate and configured IP addresses to 16-byte representations before comparison. Invalid addresses are excluded during normalization. Tests cover repeated ReconcileSignedCert calls for IPv4-only and dual-stack IP configurations and verify that tls.crt and tls.key remain unchanged.

Suggested reviewers: ironcladlou, bryan-cox

🚥 Pre-merge checks | ✅ 11
✅ Passed checks (11 passed)
Check name Status Explanation
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Stable And Deterministic Test Names ✅ Passed The added Go test uses the static name TestReconcileSignedCertIdempotentWithDualStackIPs and static subtest names ipv4-only and dual-stack; no Ginkgo titles or dynamic values were introduced.
Test Structure And Quality ✅ Passed The added test is a standard Go unit test, not Ginkgo. It uses local Secret objects, has no cluster waits or resources, and all failure checks include diagnostic messages.
Topology-Aware Scheduling Compatibility ✅ Passed The diff only changes certificate IP normalization and cert reconciliation tests; it adds no deployment manifests, replicas, affinities, topology spread, node selectors, tolerations, or PDBs.
Ipv6 And Disconnected Network Test Compatibility ✅ Passed The added test is a standard Go testing.Test, not a Ginkgo e2e test; it has no external connectivity. The check is therefore not applicable.
No-Weak-Crypto ✅ Passed The diff adds IP normalization and idempotency tests only; it introduces no weak cipher/hash, custom crypto, or security-sensitive non-constant-time comparison. Existing MD5 usage is unchanged.
Container-Privileges ✅ Passed The PR changes only Go source and tests. The changed lines add no container or Kubernetes manifest privilege settings.
No-Sensitive-Data-In-Logs ✅ Passed The diff adds no production logging. Test failures report only generic iteration and test-case labels; no passwords, tokens, API keys, PII, customer data, or sensitive identifiers are logged.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly identifies the certificate fix and its purpose: normalizing IP SANs to prevent dual-stack certificate churn.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@RamLavi RamLavi changed the title OCPBUGS-77781: fix dual-stack cert revalidation churn OCPBUGS-77781: fix dual-stack IP SAN compare causing KAS cert churn Aug 12, 2026
@openshift-ci openshift-ci Bot added area/control-plane-operator Indicates the PR includes changes for the control plane operator - in an OCP release area/hypershift-operator Indicates the PR includes changes for the hypershift operator and API - outside an OCP release and removed do-not-merge/needs-area labels Aug 12, 2026

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@support/certs/tls_test.go`:
- Around line 198-201: Update the “ipv4-only” test case in the test table to
remove the IPv6 loopback address 0:0:0:0:0:0:0:1, leaving only IPv4 addresses so
the case provides true IPv4-only coverage; keep IPv6 coverage in the existing
dual-stack case.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository YAML (base), Central YAML (inherited)

Review profile: CHILL

Plan: Pro Plus

Run ID: a207c001-b99f-4245-bbd4-ee75c377cce9

📥 Commits

Reviewing files that changed from the base of the PR and between 488ab43 and 680582f.

📒 Files selected for processing (2)
  • support/certs/tls.go
  • support/certs/tls_test.go

Comment thread support/certs/tls_test.go
@codecov

codecov Bot commented Aug 12, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 45.69%. Comparing base (ab71ef1) to head (593ea7f).
⚠️ Report is 29 commits behind head on main.

Additional details and impacted files
@@            Coverage Diff             @@
##             main    #9286      +/-   ##
==========================================
+ Coverage   45.67%   45.69%   +0.01%     
==========================================
  Files         781      781              
  Lines       97726    97736      +10     
==========================================
+ Hits        44641    44660      +19     
+ Misses      50019    50010       -9     
  Partials     3066     3066              
Files with missing lines Coverage Δ
support/certs/tls.go 66.57% <100.00%> (+3.63%) ⬆️
Flag Coverage Δ
cmd-support 38.84% <100.00%> (+0.04%) ⬆️
cpo-hostedcontrolplane 47.84% <ø> (ø)
cpo-other 45.89% <ø> (ø)
hypershift-operator 56.97% <ø> (ø)
other 34.38% <ø> (ø)

Flags with carried forward coverage won't be shown. Click here to find out more.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

OCPBUGS-77781: ValidateKeyPair compared cert IP SANs with raw byte
slices. After X.509 round-trip, IPv4 SANs are 4 bytes while desired IPs
from net.ParseIP are 16-byte IPv4-mapped, so dual-stack SANs mis-sorted
and falsely failed validation. CPO then resigned kas-server-crt every
reconcile, churning the kube-apiserver Deployment.

Normalize IPs with net.IP.To16() before compare. Add a unit test that
reconciles twice for ipv4-only and dual-stack and asserts the cert is
not regenerated.

Assisted-by: Claude Opus 4.6 <noreply@anthropic.com>
Signed-off-by: Ram Lavi <ralavi@redhat.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
@RamLavi RamLavi changed the title OCPBUGS-77781: fix dual-stack IP SAN compare causing KAS cert churn OCPBUGS-77781: fix(certs): normalize IP SANs to stop dual-stack KAS cert churn Aug 12, 2026
@RamLavi

RamLavi commented Aug 12, 2026

Copy link
Copy Markdown
Author

Change: fix CR review.

Change:fix gitlint

@RamLavi

RamLavi commented Aug 12, 2026

Copy link
Copy Markdown
Author

/jira refresh

@openshift-ci-robot openshift-ci-robot added jira/valid-bug Indicates that a referenced Jira bug is valid for the branch this PR is targeting. and removed jira/invalid-bug Indicates that a referenced Jira bug is invalid for the branch this PR is targeting. labels Aug 12, 2026
@openshift-ci-robot

Copy link
Copy Markdown

@RamLavi: This pull request references Jira Issue OCPBUGS-77781, which is valid. The bug has been moved to the POST state.

3 validation(s) were run on this bug
  • bug is open, matching expected state (open)
  • bug target version (5.0.0) matches configured target version for branch (5.0.0)
  • bug is in the state New, which is one of the valid states (NEW, ASSIGNED, POST)

No GitHub users were found matching the public email listed for the QA contact in Jira (yli2@redhat.com), skipping review request.

Details

In response to this:

/jira refresh

Instructions for interacting with me using PR comments are available here. If you have questions or suggestions related to my behavior, please file an issue against the openshift-eng/jira-lifecycle-plugin repository.

@maiqueb maiqueb left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just left a recommendation to improve the unit test's readability.

Feel free to leave it as is.

Comment thread support/certs/tls_test.go
Comment on lines +217 to +237
if err := certs.ReconcileSignedCert(
secret,
ca,
"kubernetes",
[]string{"kubernetes"},
pki.X509UsageServerAuth,
corev1.TLSCertKey,
corev1.TLSPrivateKeyKey,
"",
dnsNames,
tc.ips,
); err != nil {
t.Fatalf("ReconcileSignedCert iteration %d failed: %v", i, err)
}

cert := secret.Data[corev1.TLSCertKey]
key := secret.Data[corev1.TLSPrivateKeyKey]
if len(cert) == 0 || len(key) == 0 {
t.Fatalf("iteration %d did not populate tls.crt/tls.key", i)
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This test is very nice, but I think we could improve its readability.

How about we move this code into an helper (let's assume it would be called reconcileCert), and invoke it twice from the test ?

Then the test would read something like:

secret := &corev1.Secret{Type: corev1.SecretTypeTLS}

initialCert, initialKey, err := reconcileCert(secret, ca, tc.ips)
if err != nil {
    t.Fatalf("initial certificate generation failed: %w", err)
    return
}

laterCert, laterKey, err := reconcileCert(secret, ca, tc.ips)
if err != nil {
    t.Fatalf("subsequent certificate generation failed: %w", err)
    return
}
 
if !bytes.Equal(firstCert, laterCert) {
    t.Fatalf("tls.crt was regenerated for %s (revalidation should have been a no-op)", i, tc.name)
}

if !bytes.Equal(firstKey, laterKey) {
    t.Fatalf("tls.key was regenerated  for %s (revalidation should have been a no-op)", i, tc.name)
}

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

will leave as is as you suggest, if that OK.

@openshift-ci openshift-ci Bot added the lgtm Indicates that a PR is ready to be merged. label Aug 13, 2026
@openshift-merge-bot

Copy link
Copy Markdown
Contributor

Scheduling tests matching the pipeline_run_if_changed or not excluded by pipeline_skip_if_only_changed parameters:
/test e2e-aks
/test e2e-aws
/test e2e-aws-upgrade-hypershift-operator
/test e2e-kubevirt-aws-ovn-reduced
/test e2e-v2-aws
/test e2e-v2-azure-self-managed
/test e2e-v2-gke

@cwbotbot

cwbotbot commented Aug 13, 2026

Copy link
Copy Markdown

Test Results

e2e-aws

e2e-aks

@nunnatsa nunnatsa left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

/lgtm

@openshift-ci

openshift-ci Bot commented Aug 13, 2026

Copy link
Copy Markdown
Contributor

[APPROVALNOTIFIER] This PR is NOT APPROVED

This pull-request has been approved by: maiqueb, nunnatsa, RamLavi
Once this PR has been reviewed and has the lgtm label, please assign devguyio for approval. For more information see the Code Review Process.

The full list of commands accepted by this bot can be found here.

Details Needs approval from an approver in each of these files:

Approvers can indicate their approval by writing /approve in a comment
Approvers can cancel approval by writing /approve cancel in a comment

@bryan-cox

Copy link
Copy Markdown
Member

/retest

@openshift-ci

openshift-ci Bot commented Aug 17, 2026

Copy link
Copy Markdown
Contributor

@RamLavi: The following tests failed, say /retest to rerun all failed tests or /retest-required to rerun all mandatory failed tests:

Test name Commit Details Required Rerun command
ci/prow/e2e-v2-azure-self-managed 593ea7f link true /test e2e-v2-azure-self-managed
ci/prow/e2e-kubevirt-aws-ovn-reduced 593ea7f link true /test e2e-kubevirt-aws-ovn-reduced
ci/prow/e2e-aws-upgrade-hypershift-operator 593ea7f link true /test e2e-aws-upgrade-hypershift-operator

Full PR test history. Your PR dashboard.

Details

Instructions for interacting with me using PR comments are available here. If you have questions or suggestions related to my behavior, please file an issue against the kubernetes-sigs/prow repository. I understand the commands that are listed here.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area/control-plane-operator Indicates the PR includes changes for the control plane operator - in an OCP release area/hypershift-operator Indicates the PR includes changes for the hypershift operator and API - outside an OCP release jira/severity-important Referenced Jira bug's severity is important for the branch this PR is targeting. jira/valid-bug Indicates that a referenced Jira bug is valid for the branch this PR is targeting. jira/valid-reference Indicates that this PR references a valid Jira ticket of any type. lgtm Indicates that a PR is ready to be merged.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants