Skip to content

[release-4.22] OCPBUGS-87166: Prevent SSRF via FQDN-typed EndpointSlices - #813

Open
MrSanketkumar wants to merge 1 commit into
openshift:release-4.22from
MrSanketkumar:CVE-2026-42965-4.22
Open

[release-4.22] OCPBUGS-87166: Prevent SSRF via FQDN-typed EndpointSlices#813
MrSanketkumar wants to merge 1 commit into
openshift:release-4.22from
MrSanketkumar:CVE-2026-42965-4.22

Conversation

@MrSanketkumar

@MrSanketkumar MrSanketkumar commented Jul 14, 2026

Copy link
Copy Markdown

The OpenShift Router previously did not validate backend destinations resolved from FQDN-typed EndpointSlices. This allowed the usage of invalid EndpointSlices to target hostnames that resolves to restricted IPs (like the cloud metadata service at 169.254.169.254).

This commit disables the usage of EndpointSlices of type FQDN, and add validations on Endpoints to check if a restricted IP is being used before adding them to HAProxy endpoints Backend.

The implementation and disabling the usage of FQDN-backed endpoints is based on the following:

  • RFE-2832 to implement support for ExternalName was considered and rejected due to security concerns, so OpenShift today does not support officially the usage of FQDN-based endpoints on router
  • OCPBUGS-55506 relates to a mistake caused by the customer that caused unavailability and not the need to support FQDN-based names
  • The behavior of an endpointslice of type FQDN is deprecated on Kubernetes
  • The behavior of an endpointslice using an address that is not IPv4 or IPv6 (eg.: hostname) is unespecified by Kubernetes and has no usage.
  • The hostname field on endpointslice is not used on router

This way, there is a common understanding that the usage of FQDN based addresses on Router was a mistake, and disabling it is the right fix.

Additional validations of the IP address on the generated endpointnt array is added to guarantee that no invalid nor restricted IP is used.

Backported from #812

Summary by CodeRabbit

  • Bug Fixes
    • Filters unsupported endpoint address types instead of converting them.
    • Removes restricted, invalid, or non-IP backend addresses while preserving valid endpoints.
    • Prevents endpoint data from being mutated during validation.
    • Applies route validation according to the configured setting.
  • Tests
    • Added coverage for IPv4, IPv6, FQDN, unsupported address types, restricted IPs, and mixed endpoint scenarios.

The OpenShift Router previously did not validate backend destinations resolved from FQDN-typed EndpointSlices.
This allowed the usage of invalid EndpointSlices to target hostnames
that resolves to restricted IPs (like the cloud metadata service at 169.254.169.254).

This commit disables the usage of EndpointSlices of type FQDN, and add validations
on Endpoints to check if a restricted IP is being used before adding them to HAProxy endpoints Backend.

The implementation and disabling the usage of FQDN-backed endpoints is based on the following:

* RFE-2832 to implement support for ExternalName was considered and rejected due to security
concerns, so OpenShift today does not support officially the usage of FQDN-based endpoints on router
* OCPBUGS-55506 relates to a mistake caused by the customer that caused unavailability
and not the need to support FQDN-based names
* The behavior of an endpointslice of type FQDN is deprecated on Kubernetes
* The behavior of an endpointslice using an address that is not IPv4 or IPv6 (eg.: hostname)
is unespecified by Kubernetes and has no usage.
* The hostname field on endpointslice is not used on router

This way, there is a common understanding that the usage of FQDN based addresses on Router was a mistake, and disabling it is the right fix.

Additional validations of the IP address on the generated endpointnt array
is added to guarantee that no invalid nor restricted IP is used.
@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 Jul 14, 2026
@openshift-ci-robot

openshift-ci-robot commented Jul 14, 2026

Copy link
Copy Markdown
Contributor

@MrSanketkumar: This pull request references Jira Issue OCPBUGS-87166, which is invalid:

  • release note text must be set and not match the template OR release note type must be set to "Release Note Not Required". For more information you can reference the OpenShift Bug Process.
  • expected dependent Jira Issue OCPBUGS-87204 to be in one of the following states: MODIFIED, ON_QA, VERIFIED, but it is Closed (Done) instead
  • expected dependent Jira Issue OCPBUGS-87204 to target a version in 5.0.0, but it targets "5.0" instead

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:

The OpenShift Router previously did not validate backend destinations resolved from FQDN-typed EndpointSlices. This allowed the usage of invalid EndpointSlices to target hostnames that resolves to restricted IPs (like the cloud metadata service at 169.254.169.254).

This commit disables the usage of EndpointSlices of type FQDN, and add validations on Endpoints to check if a restricted IP is being used before adding them to HAProxy endpoints Backend.

The implementation and disabling the usage of FQDN-backed endpoints is based on the following:

  • RFE-2832 to implement support for ExternalName was considered and rejected due to security concerns, so OpenShift today does not support officially the usage of FQDN-based endpoints on router
  • OCPBUGS-55506 relates to a mistake caused by the customer that caused unavailability and not the need to support FQDN-based names
  • The behavior of an endpointslice of type FQDN is deprecated on Kubernetes
  • The behavior of an endpointslice using an address that is not IPv4 or IPv6 (eg.: hostname) is unespecified by Kubernetes and has no usage.
  • The hostname field on endpointslice is not used on router

This way, there is a common understanding that the usage of FQDN based addresses on Router was a mistake, and disabling it is the right fix.

Additional validations of the IP address on the generated endpointnt array is added to guarantee that no invalid nor restricted IP is used.

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 Jul 14, 2026

Copy link
Copy Markdown

Walkthrough

Changes

The router now always constructs ExtendedValidator with an explicit route-validation flag. EndpointSlice conversion accepts only IPv4 and IPv6 slices, while endpoint processing filters invalid addresses without mutating inputs. Tests cover address policies, conversion, event handling, and integration wiring.

Endpoint validation and address-type filtering

Layer / File(s) Summary
EndpointSlice address-type filtering
pkg/router/controller/endpointsubset/converter.go, pkg/router/controller/endpointsubset/converter_test.go, pkg/router/controller/factory/factory_endpointslices_test.go, pkg/router/router_test.go
Unsupported EndpointSlice address types are skipped, while IPv4 and IPv6 slices are converted and tested with explicit address types.
Endpoint address validation
pkg/router/controller/extended_validator.go, pkg/router/controller/extended_validator_test.go
ExtendedValidator deep-copies and filters endpoint addresses, rejects restricted and non-IP values, preserves deleted-event behavior, and retains gated route validation.
Validator construction and integration
pkg/cmd/infra/router/template.go, pkg/router/template/plugin_test.go
Router setup always wraps the plugin with NewExtendedValidator and passes the configured route-validation flag.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant EndpointSlice
  participant ConvertEndpointSlice
  participant ExtendedValidator
  participant DownstreamPlugin
  EndpointSlice->>ConvertEndpointSlice: provide typed endpoint data
  ConvertEndpointSlice->>ExtendedValidator: produce converted endpoints
  ExtendedValidator->>ExtendedValidator: filter restricted addresses
  ExtendedValidator->>DownstreamPlugin: forward filtered endpoints
Loading

Possibly related PRs

  • openshift/router#812: Overlapping ExtendedValidator wiring and EndpointSlice address filtering changes.

Suggested reviewers: gcs278, grzpiotrowski


Important

Pre-merge checks failed

Please resolve all errors before merging. Addressing warnings is optional.

❌ Failed checks (1 error, 1 warning)

Check name Status Explanation Resolution
No-Sensitive-Data-In-Logs ❌ Error ExtendedValidator logs raw endpoint values and error text for rejected addresses, which can expose internal hostnames/customer data. Redact the address from rejection logs and avoid logging err.Error(); log only a generic reason or aggregate count.
Docstring Coverage ⚠️ Warning Docstring coverage is 50.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (13 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 No Ginkgo-style titles were added; changed tests use static function/subtest names, and IPs/hostnames appear only in test data, not titles.
Test Structure And Quality ✅ Passed Changed tests are plain Go subtests, not Ginkgo; they use explicit timeouts/leaktest and defer cleanup, with no indefinite waits or cluster resources.
Microshift Test Compatibility ✅ Passed No Ginkgo/e2e tests were added; the changed files use plain Go unit tests and don't reference MicroShift-unsupported APIs.
Single Node Openshift (Sno) Test Compatibility ✅ Passed No new Ginkgo/e2e tests were added; the modified test files are plain testing unit tests with no It/Describe/Context or SNO assumptions.
Topology-Aware Scheduling Compatibility ✅ Passed The PR only changes router endpoint/route validation and test helpers; no deployment manifests, replicas, affinity, node selectors, or topology-aware scheduling logic were added.
Ote Binary Stdout Contract ✅ Passed The diff adds only non-process-level logging and test updates; no new stdout writes in main/TestMain/init/suite setup were introduced.
Ipv6 And Disconnected Network Test Compatibility ✅ Passed No new Ginkgo e2e tests were added; the changed tests are standard Go unit tests, so this compatibility check is not applicable.
No-Weak-Crypto ✅ Passed PASS: touched files only add IP/address validation and EndpointSlice filtering; no MD5/SHA1/DES/RC4/3DES/Blowfish/ECB or custom crypto added.
Container-Privileges ✅ Passed No manifests or container specs changed; only Go source/tests were modified, and the diff contains no privileged or securityContext fields.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main change: blocking SSRF via FQDN-typed EndpointSlices and related endpoint validation.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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

@openshift-ci
openshift-ci Bot requested review from gcs278 and grzpiotrowski July 14, 2026 07:24
@openshift-ci

openshift-ci Bot commented Jul 14, 2026

Copy link
Copy Markdown
Contributor

[APPROVALNOTIFIER] This PR is NOT APPROVED

This pull-request has been approved by:
Once this PR has been reviewed and has the lgtm label, please assign rfredette 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

@MrSanketkumar

Copy link
Copy Markdown
Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jul 14, 2026

Copy link
Copy Markdown
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@MrSanketkumar

Copy link
Copy Markdown
Author

/jira refresh

@openshift-ci-robot

Copy link
Copy Markdown
Contributor

@MrSanketkumar: This pull request references Jira Issue OCPBUGS-87166, which is invalid:

  • release note text must be set and not match the template OR release note type must be set to "Release Note Not Required". For more information you can reference the OpenShift Bug Process.
  • expected dependent Jira Issue OCPBUGS-87204 to be in one of the following states: MODIFIED, ON_QA, VERIFIED, but it is Closed (Done) instead
  • expected dependent Jira Issue OCPBUGS-87204 to target a version in 5.0.0, but it targets "5.0" instead

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.

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.

@MrSanketkumar

Copy link
Copy Markdown
Author

/jira refresh

@openshift-ci-robot

Copy link
Copy Markdown
Contributor

@MrSanketkumar: This pull request references Jira Issue OCPBUGS-87166, which is invalid:

  • expected dependent Jira Issue OCPBUGS-87204 to be in one of the following states: MODIFIED, ON_QA, VERIFIED, but it is Closed (Done) instead
  • expected dependent Jira Issue OCPBUGS-87204 to target a version in 5.0.0, but it targets "5.0" instead

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.

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.

@coderabbitai coderabbitai Bot 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.

Actionable comments posted: 3

🤖 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 `@pkg/router/controller/extended_validator.go`:
- Around line 66-73: Update filterValidAddresses and the other
endpoint-rejection logging paths in this file to stop emitting raw IP or
endpoint values, including the duplicated value in err. Log only a safe
rejection reason or aggregate count while preserving the existing filtering
behavior.
- Around line 87-90: Update the address validation logic around azureMetadata
and awsIPv6IMDS to recognize and reject RFC 6052 IPv4/IPv6 translation ranges,
including 64:ff9b::/96 and 64:ff9b:1::/48, before IPv6 normalization can bypass
checks. Prefer the existing trust-boundary allow-list approach where applicable,
and add regression cases covering translated addresses that resolve to cloud
metadata endpoints.
- Around line 172-177: Update the invalid-route branch in the extended
validation flow to capture and propagate the error returned by
p.plugin.HandleRoute(watch.Deleted, route) instead of discarding it. Preserve
the existing rejection recording and invalid-configuration error behavior while
ensuring downstream deletion failures are returned to the caller.
🪄 Autofix (Beta)

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: openshift/coderabbit/.coderabbit.yaml

Review profile: CHILL

Plan: Enterprise

Run ID: 1dbeddef-7001-4749-ae23-6b6657f47c4a

📥 Commits

Reviewing files that changed from the base of the PR and between 5586fa9 and dc32293.

📒 Files selected for processing (8)
  • pkg/cmd/infra/router/template.go
  • pkg/router/controller/endpointsubset/converter.go
  • pkg/router/controller/endpointsubset/converter_test.go
  • pkg/router/controller/extended_validator.go
  • pkg/router/controller/extended_validator_test.go
  • pkg/router/controller/factory/factory_endpointslices_test.go
  • pkg/router/router_test.go
  • pkg/router/template/plugin_test.go

Comment on lines +66 to +73
func filterValidAddresses(addrs []kapi.EndpointAddress) []kapi.EndpointAddress {
return slices.DeleteFunc(addrs, func(addr kapi.EndpointAddress) bool {
err := validateEndpointAddress(addr.IP)
if err != nil {
log.Error(err, "Skipping endpoint address with restricted or invalid IP", "address", addr.IP)
}
return err != nil
})

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

Remove raw endpoint values from rejection logs.

Both err and the "address" field contain addr.IP; malformed values can be internal hostnames such as metadata.google.internal. Log only a reason or aggregate count.

Proposed fix
 func filterValidAddresses(addrs []kapi.EndpointAddress) []kapi.EndpointAddress {
 	return slices.DeleteFunc(addrs, func(addr kapi.EndpointAddress) bool {
-		err := validateEndpointAddress(addr.IP)
-		if err != nil {
-			log.Error(err, "Skipping endpoint address with restricted or invalid IP", "address", addr.IP)
+		if validateEndpointAddress(addr.IP) == nil {
+			return false
 		}
-		return err != nil
+		log.V(4).Info("Skipping endpoint address with restricted or invalid IP")
+		return true
 	})
 }

As per coding guidelines, “Flag logging that may expose internal hostnames or customer data.”

Also applies to: 76-80, 115-130

🤖 Prompt for 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.

In `@pkg/router/controller/extended_validator.go` around lines 66 - 73, Update
filterValidAddresses and the other endpoint-rejection logging paths in this file
to stop emitting raw IP or endpoint values, including the duplicated value in
err. Log only a safe rejection reason or aggregate count while preserving the
existing filtering behavior.

Source: Coding guidelines

Comment on lines +87 to +90
var (
azureMetadata = netip.MustParseAddr("168.63.129.16")
awsIPv6IMDS = netip.MustParseAddr("fd00:ec2::254")
)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

Block IPv4/IPv6 translation prefixes that bypass normalization.

64:ff9b::a9fe:a9fe passes these checks as ordinary IPv6, although 64:ff9b::/96 embeds an IPv4 destination; 64:ff9b:1::/48 is also reserved for local translation. On NAT64-enabled nodes, this can translate to 169.254.169.254, restoring IMDS access. Reject these translation prefixes and add regression cases, or enforce a deployment-aware backend CIDR allow-list. (rfc-editor.org)

As per path instructions, “Validate at trust boundaries with allow-lists, not deny-lists.”

Also applies to: 105-113, 115-130

🤖 Prompt for 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.

In `@pkg/router/controller/extended_validator.go` around lines 87 - 90, Update the
address validation logic around azureMetadata and awsIPv6IMDS to recognize and
reject RFC 6052 IPv4/IPv6 translation ranges, including 64:ff9b::/96 and
64:ff9b:1::/48, before IPv6 normalization can bypass checks. Prefer the existing
trust-boundary allow-list approach where applicable, and add regression cases
covering translated addresses that resolve to cloud metadata endpoints.

Source: Path instructions

Comment on lines +172 to +177
if err := routeapihelpers.ExtendedValidateRoute(route).ToAggregate(); err != nil {
log.Error(err, "skipping route due to invalid configuration", "route", routeName)

p.recorder.RecordRouteRejection(route, "ExtendedValidationFailed", err.Error())
p.plugin.HandleRoute(watch.Deleted, route)
return fmt.Errorf("invalid route configuration")

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Propagate failure to remove the invalid route.

Line 176 discards the downstream deletion error, potentially leaving stale invalid configuration installed.

Proposed fix
 			p.recorder.RecordRouteRejection(route, "ExtendedValidationFailed", err.Error())
-			p.plugin.HandleRoute(watch.Deleted, route)
+			if deleteErr := p.plugin.HandleRoute(watch.Deleted, route); deleteErr != nil {
+				return fmt.Errorf("invalid route configuration; failed to remove route: %w", deleteErr)
+			}
 			return fmt.Errorf("invalid route configuration")

As per path instructions, “Never ignore error returns.”

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if err := routeapihelpers.ExtendedValidateRoute(route).ToAggregate(); err != nil {
log.Error(err, "skipping route due to invalid configuration", "route", routeName)
p.recorder.RecordRouteRejection(route, "ExtendedValidationFailed", err.Error())
p.plugin.HandleRoute(watch.Deleted, route)
return fmt.Errorf("invalid route configuration")
if err := routeapihelpers.ExtendedValidateRoute(route).ToAggregate(); err != nil {
log.Error(err, "skipping route due to invalid configuration", "route", routeName)
p.recorder.RecordRouteRejection(route, "ExtendedValidationFailed", err.Error())
if deleteErr := p.plugin.HandleRoute(watch.Deleted, route); deleteErr != nil {
return fmt.Errorf("invalid route configuration; failed to remove route: %w", deleteErr)
}
return fmt.Errorf("invalid route configuration")
🤖 Prompt for 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.

In `@pkg/router/controller/extended_validator.go` around lines 172 - 177, Update
the invalid-route branch in the extended validation flow to capture and
propagate the error returned by p.plugin.HandleRoute(watch.Deleted, route)
instead of discarding it. Preserve the existing rejection recording and
invalid-configuration error behavior while ensuring downstream deletion failures
are returned to the caller.

Source: Path instructions

@MrSanketkumar

Copy link
Copy Markdown
Author

/jira refresh

@openshift-ci-robot openshift-ci-robot added the jira/valid-bug Indicates that a referenced Jira bug is valid for the branch this PR is targeting. label Jul 14, 2026
@openshift-ci-robot

Copy link
Copy Markdown
Contributor

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

7 validation(s) were run on this bug
  • bug is open, matching expected state (open)
  • bug target version (4.22.0) matches configured target version for branch (4.22.0)
  • bug is in the state ASSIGNED, which is one of the valid states (NEW, ASSIGNED, POST)
  • release note text is set and does not match the template
  • dependent bug Jira Issue OCPBUGS-87204 is in the state Verified, which is one of the valid states (MODIFIED, ON_QA, VERIFIED)
  • dependent Jira Issue OCPBUGS-87204 targets the "5.0.0" version, which is one of the valid target versions: 5.0.0
  • bug has dependents

No GitHub users were found matching the public email listed for the QA contact in Jira (uyendava@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.

@openshift-ci-robot openshift-ci-robot removed the jira/invalid-bug Indicates that a referenced Jira bug is invalid for the branch this PR is targeting. label Jul 14, 2026
@MrSanketkumar

Copy link
Copy Markdown
Author

/retest-required

@MrSanketkumar

Copy link
Copy Markdown
Author

/test e2e-aws-fips

1 similar comment
@MrSanketkumar

Copy link
Copy Markdown
Author

/test e2e-aws-fips

@openshift-ci

openshift-ci Bot commented Jul 15, 2026

Copy link
Copy Markdown
Contributor

@MrSanketkumar: all tests passed!

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.

@germanparente

Copy link
Copy Markdown

/abel backport-risk-assessed

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

Copy link
Copy Markdown
Contributor

@MrSanketkumar: This pull request references Jira Issue OCPBUGS-87166, which is invalid:

  • expected dependent Jira Issue OCPBUGS-87204 to be in one of the following states: MODIFIED, ON_QA, VERIFIED, but it is Closed (Done) instead

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.

Details

In response to this:

The OpenShift Router previously did not validate backend destinations resolved from FQDN-typed EndpointSlices. This allowed the usage of invalid EndpointSlices to target hostnames that resolves to restricted IPs (like the cloud metadata service at 169.254.169.254).

This commit disables the usage of EndpointSlices of type FQDN, and add validations on Endpoints to check if a restricted IP is being used before adding them to HAProxy endpoints Backend.

The implementation and disabling the usage of FQDN-backed endpoints is based on the following:

  • RFE-2832 to implement support for ExternalName was considered and rejected due to security concerns, so OpenShift today does not support officially the usage of FQDN-based endpoints on router
  • OCPBUGS-55506 relates to a mistake caused by the customer that caused unavailability and not the need to support FQDN-based names
  • The behavior of an endpointslice of type FQDN is deprecated on Kubernetes
  • The behavior of an endpointslice using an address that is not IPv4 or IPv6 (eg.: hostname) is unespecified by Kubernetes and has no usage.
  • The hostname field on endpointslice is not used on router

This way, there is a common understanding that the usage of FQDN based addresses on Router was a mistake, and disabling it is the right fix.

Additional validations of the IP address on the generated endpointnt array is added to guarantee that no invalid nor restricted IP is used.

Backported from #812

Summary by CodeRabbit

  • Bug Fixes
  • Filters unsupported endpoint address types instead of converting them.
  • Removes restricted, invalid, or non-IP backend addresses while preserving valid endpoints.
  • Prevents endpoint data from being mutated during validation.
  • Applies route validation according to the configured setting.
  • Tests
  • Added coverage for IPv4, IPv6, FQDN, unsupported address types, restricted IPs, and mixed endpoint scenarios.

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.

@UdayYendva

UdayYendva commented Jul 15, 2026

Copy link
Copy Markdown

Cluster Version

oc get clusterversion

Output

NAME      VERSION                                                AVAILABLE   PROGRESSING   SINCE   STATUS
version   4.22.0-0-2026-07-17-052437-test-ci-ln-cw2n5m2-latest   True        False         3m38s   Cluster version is 4.22.0-0-2026-07-17-052437-test-ci-ln-cw2n5m2-latest

SSRF Protection Validation - EndpointSlice Verification Results

Test Environment Setup

Create test namespace and service

oc create namespace ssrf-test

oc -n ssrf-test create service clusterip metadata-svc --tcp=80:80

oc -n ssrf-test patch svc metadata-svc --type=json \
-p='[{"op":"remove","path":"/spec/selector"}]'

Create route

CLUSTER_DOMAIN=$(oc get ingresses.config cluster -o jsonpath='{.spec.domain}')

oc -n ssrf-test expose svc metadata-svc \
--hostname=ssrf-test.${CLUSTER_DOMAIN}

Save URL

TEST_URL="http://ssrf-test.${CLUSTER_DOMAIN}/latest/meta-data/"

Test 1: FQDN EndpointSlice (Layer 1)

EndpointSlice

 cat <<EOF | oc apply -f -
apiVersion: discovery.k8s.io/v1
kind: EndpointSlice
metadata:
  name: test-fqdn
  namespace: ssrf-test
  labels:
    kubernetes.io/service-name: metadata-svc
addressType: FQDN
endpoints:
  - addresses:
      - "metadata.google.internal"
ports:
  - port: 80
EOF
Warning: spec.addressType: FQDN endpoints are deprecated
endpointslice.discovery.k8s.io/test-fqdn created

Verification

curl -s -o /dev/null -w "%{http_code}" $TEST_URL
503
 
oc -n openshift-ingress logs deployment/router-default | grep "unsupported address type"pe"
Found 2 pods, using pod/router-default-64b87db7bc-kd2kp
I0717 06:18:42.188302       1 converter.go:18] "msg"="Skipping EndpointSlice with unsupported address type" "addressType"="FQDN" "logger"="endpointsubset" "name"="test-fqdn" "namespace"="ssrf-test"

Test 2: IPv6 Hex Metadata IP (Layer 2 Pass 2)

EndpointSlice

cat <<EOF | oc apply -f -
apiVersion: discovery.k8s.io/v1
kind: EndpointSlice
metadata:
  name: test-ipv6-metadata-hex
  namespace: ssrf-test
  labels:
    kubernetes.io/service-name: metadata-svc
addressType: IPv6
endpoints:
  - addresses:
      - "::a9fe:a9fe"
ports:
  - port: 80
EOF
endpointslice.discovery.k8s.io/test-ipv6-metadata-hex created

Verification

curl -s -o /dev/null -w "%{http_code}" $TEST_URL
503

oc -n openshift-ingress logs deployment/router-default | grep "restricted" | grep "a9fe"fe"
**Found 2 pods, using pod/router-default-64b87db7bc-kd2kp
E0717 06:22:45.976337       1 extended_validator.go:70] "msg"="Skipping endpoint address with restricted or invalid IP" "error"="IP address 169.254.169.254 is a restricted link-local IP" "address"="::a9fe:a9fe" "logger"="controller"**

Test 3: IPv6 Hex Loopback (Layer 2 Pass 2)

EndpointSlice

cat <<EOF | oc apply -f -
apiVersion: discovery.k8s.io/v1
kind: EndpointSlice
metadata:
  name: test-ipv6-loopback-hex
  namespace: ssrf-test
  labels:
    kubernetes.io/service-name: metadata-svc
addressType: IPv6
endpoints:
  - addresses:
      - "::7f00:1"
ports:
  - port: 80
EOF
endpointslice.discovery.k8s.io/test-ipv6-loopback-hex created

Verification

curl -s -o /dev/null -w "%{http_code}" $TEST_URL
503

oc -n openshift-ingress logs deployment/router-default | grep "restricted" | grep "7f00"
Found 2 pods, using pod/router-default-64b87db7bc-kd2kp
E0717 06:25:35.243202       1 extended_validator.go:70] "msg"="Skipping endpoint address with restricted or invalid IP" "error"="IP address 127.0.0.1 is a restricted loopback IP" "address"="::7f00:1" "logger"="controller"

Test 4: AWS IPv6 IMDS (Layer 2 Pass 1)

EndpointSlice

cat <<EOF | oc apply -f -
apiVersion: discovery.k8s.io/v1
kind: EndpointSlice
metadata:
  name: test-aws-ipv6-imds
  namespace: ssrf-test
  labels:
    kubernetes.io/service-name: metadata-svc
addressType: IPv6
endpoints:
  - addresses:
      - "fd00:ec2::254"
ports:
  - port: 80
EOF
endpointslice.discovery.k8s.io/test-aws-ipv6-imds created

Verification

curl -s -o /dev/null -w "%{http_code}" $TEST_URL
503

oc -n openshift-ingress logs deployment/router-default | grep "restricted" | grep "fd00"
Found 2 pods, using pod/router-default-64b87db7bc-kd2kp
E0717 06:27:50.255380       1 extended_validator.go:70] "msg"="Skipping endpoint address with restricted or invalid IP" "error"="IP address fd00:ec2::254 is a restricted cloud metadata IP" "address"="fd00:ec2::254" "logger"="controller"

Test 5: IPv6 Loopback (Layer 2 Pass 1)

EndpointSlice

apiVersion: discovery.k8s.io/v1
kind: EndpointSlice
metadata:
  name: test-ipv6-loopback
  namespace: ssrf-test
  labels:
    kubernetes.io/service-name: metadata-svc
addressType: IPv6
endpoints:
  - addresses:
      - "::1"
ports:
  - port: 80

Verification

oc apply -f test-ipv6-loopback.yaml

curl -s -o /dev/null -w "%{http_code}" $TEST_URL

oc -n openshift-ingress logs deployment/router-default | grep "restricted" | grep "::1"

Output


Reason:
::1 is a reserved IPv6 loopback address.

Router log:
No output because EndpointSlice was not created.

Test 6: IPv6 Link-Local (Layer 2 Pass 1)

EndpointSlice

apiVersion: discovery.k8s.io/v1
kind: EndpointSlice
metadata:
  name: test-ipv6-linklocal
  namespace: ssrf-test
  labels:
    kubernetes.io/service-name: metadata-svc
addressType: IPv6
endpoints:
  - addresses:
      - "fe80::1"
ports:
  - port: 80

Verification

oc apply -f test-ipv6-linklocal.yaml

curl -s -o /dev/null -w "%{http_code}" $TEST_URL

oc -n openshift-ingress logs deployment/router-default | grep "restricted" | grep "fe80"

Output


Reason:
fe80::1 is a reserved IPv6 link-local address.

Router log:
No output because EndpointSlice was not created.

Test 7: IPv6 Multicast (Layer 2 Pass 1)

EndpointSlice

apiVersion: discovery.k8s.io/v1
kind: EndpointSlice
metadata:
  name: test-ipv6-multicast
  namespace: ssrf-test
  labels:
    kubernetes.io/service-name: metadata-svc
addressType: IPv6
endpoints:
  - addresses:
      - "ff02::1"
ports:
  - port: 80

Verification

oc apply -f test-ipv6-multicast.yaml

curl -s -o /dev/null -w "%{http_code}" $TEST_URL

oc -n openshift-ingress logs deployment/router-default | grep "restricted" | grep "ff02"

Output


Reason:
ff02::1 is a reserved multicast address.

Router log:
No output because EndpointSlice was not created.

Test 8: IPv6 Unspecified (Layer 2 Pass 1)

EndpointSlice

apiVersion: discovery.k8s.io/v1
kind: EndpointSlice
metadata:
  name: test-ipv6-unspecified
  namespace: ssrf-test
  labels:
    kubernetes.io/service-name: metadata-svc
addressType: IPv6
endpoints:
  - addresses:
      - "::"
ports:
  - port: 80

Verification

oc apply -f test-ipv6-unspecified.yaml

curl -s -o /dev/null -w "%{http_code}" $TEST_URL

oc -n openshift-ingress logs deployment/router-default | grep "restricted" | grep "unspecified"

Output


Reason:
:: is a reserved IPv6 unspecified address.

Router log:
No output because EndpointSlice was not created.

Test 9: Valid IPv6 Pass-Through

EndpointSlice

cat <<EOF | oc apply -f -
apiVersion: discovery.k8s.io/v1
kind: EndpointSlice
metadata:
  name: test-valid-ipv6
  namespace: ssrf-test
  labels:
    kubernetes.io/service-name: metadata-svc
addressType: IPv6
endpoints:
  - addresses:
      - "2001:db8::1"
ports:
  - port: 80
EOF
endpointslice.discovery.k8s.io/test-valid-ipv6 created

Verification

oc -n openshift-ingress logs deployment/router-default | grep "restricted" | grep "2001"
Found 2 pods, using pod/router-default-64b87db7bc-kd2kp (EXPECTED)
Expected Results:
No log line found regarding this address being restricted.
The router accepts it as a valid backend.

Test 10: Normal Application Regression Test

oc -n ssrf-test new-app --image=openshift/hello-openshift --name=hello
oc -n ssrf-test expose svc hello --hostname=hello-test.${CLUSTER_DOMAIN}
# Wait for pod to be ready
oc -n ssrf-test wait --for=condition=ready pod -l app=hello --timeout=60s
--> Found container image 7af3297 (8 years old) from Docker Hub for "openshift/hello-openshift"

    * An image stream tag will be created as "hello:latest" that will track this image

--> Creating resources ...
    imagestream.image.openshift.io "hello" created
    deployment.apps "hello" created
    service "hello" created
--> Success
    Application is not exposed. You can expose services to the outside world by executing one or more of the commands below:
     'oc expose service/hello' 
    Run 'oc status' to view your app.
route.route.openshift.io/hello exposed

Verification

curl -s http://hello-test.${CLUSTER_DOMAIN}
Hello OpenShift!

Note

Kubernetes/OpenShift prevent the creation of EndpointSlices that reference special-purpose IP address ranges, including loopback (127.0.0.0/8, ::1/128), unspecified (0.0.0.0, ::), multicast (224.0.0.0/4, ff00::/8), and other non-routable address ranges such as link-local addresses (169.254.0.0/16, fe80::/10) because of the built-in validation implemented by Kubernetes/OpenShift.

Tests 5–8 are blocked by Kubernetes/OpenShift built-in EndpointSlice validation before reaching the router SSRF validation logic.

/verified by ci

@openshift-ci-robot openshift-ci-robot added the verified Signifies that the PR passed pre-merge verification criteria label Jul 15, 2026
@openshift-ci-robot

Copy link
Copy Markdown
Contributor

@UdayYendva: This PR has been marked as verified by ci.

Details

In response to this:

SSRF Protection Validation - EndpointSlice Verification Results

Test Environment Setup

Create test namespace and service

oc create namespace ssrf-test

oc -n ssrf-test create service clusterip metadata-svc --tcp=80:80

oc -n ssrf-test patch svc metadata-svc --type=json \
-p='[{"op":"remove","path":"/spec/selector"}]'

Create route

CLUSTER_DOMAIN=$(oc get ingresses.config cluster -o jsonpath='{.spec.domain}')

oc -n ssrf-test expose svc metadata-svc \
--hostname=ssrf-test.${CLUSTER_DOMAIN}

Save URL

TEST_URL="http://ssrf-test.${CLUSTER_DOMAIN}/latest/meta-data/"

Test 1: FQDN EndpointSlice (Layer 1)

EndpointSlice

apiVersion: discovery.k8s.io/v1
kind: EndpointSlice
metadata:
 name: test-fqdn
 namespace: ssrf-test
 labels:
   kubernetes.io/service-name: metadata-svc
addressType: FQDN
endpoints:
 - addresses:
     - "metadata.google.internal"
ports:
 - port: 80

Verification

curl -s -o /dev/null -w "%{http_code}" $TEST_URL

oc -n openshift-ingress logs deployment/router-default | grep "unsupported address type"

Output

curl response:
503

Router log:
Skipping EndpointSlice with unsupported address type addressType=FQDN

Test 2: IPv6 Hex Metadata IP (Layer 2 Pass 2)

EndpointSlice

apiVersion: discovery.k8s.io/v1
kind: EndpointSlice
metadata:
 name: test-ipv6-metadata-hex
 namespace: ssrf-test
 labels:
   kubernetes.io/service-name: metadata-svc
addressType: IPv6
endpoints:
 - addresses:
     - "::a9fe:a9fe"
ports:
 - port: 80

Verification

curl -s -o /dev/null -w "%{http_code}" $TEST_URL

oc -n openshift-ingress logs deployment/router-default | grep "restricted" | grep "a9fe"

Output

curl response:
503

Router log:
Skipping endpoint address with restricted or invalid IP
error="IP address 169.254.169.254 is a restricted link-local IP"
address="::a9fe:a9fe"

Test 3: IPv6 Hex Loopback (Layer 2 Pass 2)

EndpointSlice

apiVersion: discovery.k8s.io/v1
kind: EndpointSlice
metadata:
 name: test-ipv6-loopback-hex
 namespace: ssrf-test
 labels:
   kubernetes.io/service-name: metadata-svc
addressType: IPv6
endpoints:
 - addresses:
     - "::7f00:1"
ports:
 - port: 80

Verification

curl -s -o /dev/null -w "%{http_code}" $TEST_URL

oc -n openshift-ingress logs deployment/router-default | grep "restricted" | grep "7f00"

Output

curl response:
503

Router log:
Skipping endpoint address with restricted or invalid IP
error="IP address 127.0.0.1 is a restricted loopback IP"
address="::7f00:1"

Test 4: AWS IPv6 IMDS (Layer 2 Pass 1)

EndpointSlice

apiVersion: discovery.k8s.io/v1
kind: EndpointSlice
metadata:
 name: test-aws-ipv6-imds
 namespace: ssrf-test
 labels:
   kubernetes.io/service-name: metadata-svc
addressType: IPv6
endpoints:
 - addresses:
     - "fd00:ec2::254"
ports:
 - port: 80

Verification

curl -s -o /dev/null -w "%{http_code}" $TEST_URL

oc -n openshift-ingress logs deployment/router-default | grep "restricted" | grep "fd00"

Output

curl response:
503

Router log:
error="IP address fd00:ec2::254 is a restricted cloud metadata IP"

Test 5: IPv6 Loopback (Layer 2 Pass 1)

EndpointSlice

apiVersion: discovery.k8s.io/v1
kind: EndpointSlice
metadata:
 name: test-ipv6-loopback
 namespace: ssrf-test
 labels:
   kubernetes.io/service-name: metadata-svc
addressType: IPv6
endpoints:
 - addresses:
     - "::1"
ports:
 - port: 80

Verification

oc apply -f test-ipv6-loopback.yaml

curl -s -o /dev/null -w "%{http_code}" $TEST_URL

oc -n openshift-ingress logs deployment/router-default | grep "restricted" | grep "::1"

Output


Reason:
::1 is a reserved IPv6 loopback address.

Router log:
No output because EndpointSlice was not created.

Test 6: IPv6 Link-Local (Layer 2 Pass 1)

EndpointSlice

apiVersion: discovery.k8s.io/v1
kind: EndpointSlice
metadata:
 name: test-ipv6-linklocal
 namespace: ssrf-test
 labels:
   kubernetes.io/service-name: metadata-svc
addressType: IPv6
endpoints:
 - addresses:
     - "fe80::1"
ports:
 - port: 80

Verification

oc apply -f test-ipv6-linklocal.yaml

curl -s -o /dev/null -w "%{http_code}" $TEST_URL

oc -n openshift-ingress logs deployment/router-default | grep "restricted" | grep "fe80"

Output


Reason:
fe80::1 is a reserved IPv6 link-local address.

Router log:
No output because EndpointSlice was not created.

Test 7: IPv6 Multicast (Layer 2 Pass 1)

EndpointSlice

apiVersion: discovery.k8s.io/v1
kind: EndpointSlice
metadata:
 name: test-ipv6-multicast
 namespace: ssrf-test
 labels:
   kubernetes.io/service-name: metadata-svc
addressType: IPv6
endpoints:
 - addresses:
     - "ff02::1"
ports:
 - port: 80

Verification

oc apply -f test-ipv6-multicast.yaml

curl -s -o /dev/null -w "%{http_code}" $TEST_URL

oc -n openshift-ingress logs deployment/router-default | grep "restricted" | grep "ff02"

Output


Reason:
ff02::1 is a reserved multicast address.

Router log:
No output because EndpointSlice was not created.

Test 8: IPv6 Unspecified (Layer 2 Pass 1)

EndpointSlice

apiVersion: discovery.k8s.io/v1
kind: EndpointSlice
metadata:
 name: test-ipv6-unspecified
 namespace: ssrf-test
 labels:
   kubernetes.io/service-name: metadata-svc
addressType: IPv6
endpoints:
 - addresses:
     - "::"
ports:
 - port: 80

Verification

oc apply -f test-ipv6-unspecified.yaml

curl -s -o /dev/null -w "%{http_code}" $TEST_URL

oc -n openshift-ingress logs deployment/router-default | grep "restricted" | grep "unspecified"

Output


Reason:
:: is a reserved IPv6 unspecified address.

Router log:
No output because EndpointSlice was not created.

Test 9: Valid IPv6 Pass-Through

EndpointSlice

apiVersion: discovery.k8s.io/v1
kind: EndpointSlice
metadata:
 name: test-valid-ipv6
 namespace: ssrf-test
 labels:
   kubernetes.io/service-name: metadata-svc
addressType: IPv6
endpoints:
 - addresses:
     - "2001:db8::1"
ports:
 - port: 80

Verification

oc -n openshift-ingress logs deployment/router-default | grep "restricted" | grep "2001"

Output

No restricted IP log entry found.

Router accepts the endpoint as a valid backend.

Test 10: Normal Application Regression Test

EndpointSlice

Generated automatically by Kubernetes:

addressType: IPv4
endpoints:
 - addresses:
     - "<application-pod-ip>"
ports:
 - port: 8080

Verification

curl -s http://hello-test.${CLUSTER_DOMAIN}

Output

Hello OpenShift

Cleanup

oc delete project ssrf-test

Note

Kubernetes/OpenShift prevent the creation of EndpointSlices that reference special-purpose IP address ranges, including loopback (127.0.0.0/8, ::1/128), unspecified (0.0.0.0, ::), multicast (224.0.0.0/4, ff00::/8), and other non-routable address ranges such as link-local addresses (169.254.0.0/16, fe80::/10) because of the built-in validation implemented by Kubernetes/OpenShift.

Tests 5–8 are blocked by Kubernetes/OpenShift built-in EndpointSlice validation before reaching the router SSRF validation logic.

/verified by ci

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.

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

Labels

jira/invalid-bug Indicates that a referenced Jira bug is invalid for the branch this PR is targeting. 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. verified Signifies that the PR passed pre-merge verification criteria

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants