Skip to content

[release-4.21] OCPBUGS-86729: Prevent SSRF via FQDN-typed EndpointSlices - #818

Open
MrSanketkumar wants to merge 2 commits into
openshift:release-4.21from
MrSanketkumar:CVE-2026-42965-4.21
Open

[release-4.21] OCPBUGS-86729: Prevent SSRF via FQDN-typed EndpointSlices#818
MrSanketkumar wants to merge 2 commits into
openshift:release-4.21from
MrSanketkumar:CVE-2026-42965-4.21

Conversation

@MrSanketkumar

@MrSanketkumar MrSanketkumar commented Jul 22, 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.

Backport :#813

Summary by CodeRabbit

  • Security & Reliability

    • Filters invalid, restricted, and unsafe endpoint IP addresses before they are routed.
    • Ignores unsupported endpoint address types while preserving valid IPv4 and IPv6 endpoints.
    • Adds optional extended route validation to identify and filter invalid routes.
  • Bug Fixes

    • Prevents invalid backend addresses from being exposed to routing.
    • Preserves input endpoint data during validation and handles deleted endpoints correctly.
  • Tests

    • Added coverage for endpoint address types, restricted IPs, route validation, and endpoint conversion behavior.

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 22, 2026
@openshift-ci-robot

openshift-ci-robot commented Jul 22, 2026

Copy link
Copy Markdown
Contributor

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

  • expected dependent Jira Issue OCPBUGS-87166 to be in one of the following states: VERIFIED, RELEASE PENDING, CLOSED (ERRATA), CLOSED (CURRENT RELEASE), CLOSED (DONE), CLOSED (DONE-ERRATA), but it is POST instead
  • expected dependent Jira Issue OCPBUGS-87166 to target a version in 4.22.0, but it targets "4.22.z" 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.

Backport :#813

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 22, 2026

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Repository: openshift/coderabbit/.coderabbit.yaml

Review profile: CHILL

Plan: Enterprise

Run ID: d2c5a55f-d5f1-4619-88e4-59f64273d73d

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

Walkthrough

Changes

Endpoint validation

Layer / File(s) Summary
EndpointSlice address-type filtering
pkg/router/controller/endpointsubset/converter.go, pkg/router/controller/endpointsubset/converter_test.go
ConvertEndpointSlice now logs and skips non-IPv4/IPv6 slices, with coverage for supported, unsupported, and mixed address types.
Endpoint and route validation
pkg/router/controller/extended_validator.go, pkg/router/controller/extended_validator_test.go
ExtendedValidator filters restricted endpoint addresses on non-deleted events, preserves deleted-event behavior, avoids input mutation, and conditionally performs extended route validation.
Router wiring and test fixtures
pkg/cmd/infra/router/template.go, pkg/router/controller/factory/factory_endpointslices_test.go, pkg/router/router_test.go, pkg/router/template/plugin_test.go
Router setup always wraps plugins with the validator, passes the route-validation flag, and updates EndpointSlice fixtures with explicit address types.

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

Sequence Diagram(s)

sequenceDiagram
  participant TemplateRouterOptions.Run
  participant ExtendedValidator
  participant router.Plugin
  participant RouteStatusRecorder
  TemplateRouterOptions.Run->>ExtendedValidator: wrap plugin with extendedRouteValidation
  ExtendedValidator->>ExtendedValidator: filter non-deleted endpoint addresses
  ExtendedValidator->>router.Plugin: forward filtered endpoint event
  ExtendedValidator->>RouteStatusRecorder: record invalid route rejection
  ExtendedValidator->>router.Plugin: forward rejected route as deleted
Loading

Suggested reviewers: davidesalerno, gcs278


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 FAIL: extended_validator.go logs rejected endpoint IPs (address, addr.IP), exposing backend/customer network details. Redact/remove raw IPs and resource names from logs; log only generic reasons/counts or hashed identifiers.
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 titles were added, and all new t.Run names are static strings without run-specific data or generated identifiers.
Test Structure And Quality ✅ Passed Touched tests are table-driven unit tests; cluster-facing ones use leaktest and explicit timeouts, with no Ginkgo wait/cleanup gaps introduced.
Microshift Test Compatibility ✅ Passed No new Ginkgo e2e tests were added; the changed files are plain Go unit tests using testing.T and no MicroShift-only APIs.
Single Node Openshift (Sno) Test Compatibility ✅ Passed All added tests are plain Go unit tests (testing package); no new Ginkgo It/Describe/Context/When e2e tests or SNO assumptions were added.
Topology-Aware Scheduling Compatibility ✅ Passed The PR only changes route/endpoint validation and tests; no deployment, replicas, affinity, nodeSelector, topology spread, PDB, or toleration changes were introduced.
Ote Binary Stdout Contract ✅ Passed PASS: The patch only adds EndpointSlice AddressType/validation logic; no new main/init/TestMain or stdout-printing calls were added.
Ipv6 And Disconnected Network Test Compatibility ✅ Passed No new Ginkgo/e2e tests were added; changed files are standard Go unit tests and controller code, so the IPv6/disconnected check is not applicable.
No-Weak-Crypto ✅ Passed No weak crypto or secret comparisons were added; the patch only changes router endpoint validation and constructor wiring. Existing SHA1 test fixtures are unchanged.
Container-Privileges ✅ Passed The PR only changes Go code and tests; none of the touched files contain privileged, hostPID/Network/IPC, SYS_ADMIN, or allowPrivilegeEscalation settings.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main security fix: blocking SSRF via FQDN-typed EndpointSlices.
✨ 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 davidesalerno and gcs278 July 22, 2026 11:06
@openshift-ci

openshift-ci Bot commented Jul 22, 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 rikatz 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 22, 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 MrSanketkumar changed the title [release-4.21]OCPBUGS-86729: Prevent SSRF via FQDN-typed EndpointSlices [release-4.21] OCPBUGS-86729: Prevent SSRF via FQDN-typed EndpointSlices Jul 22, 2026

@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: 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 `@pkg/router/controller/extended_validator.go`:
- Line 176: The watch.Deleted branch in the route validation flow ignores the
error returned by downstream p.plugin.HandleRoute. Capture that error and log it
using the component’s existing logger, while preserving the current propagation
behavior and route arguments.
🪄 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: 50bfb0a6-1ede-457c-a324-9eac8330eada

📥 Commits

Reviewing files that changed from the base of the PR and between dc4c01d and 389b9ac.

📒 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

log.Error(err, "skipping route due to invalid configuration", "route", routeName)

p.recorder.RecordRouteRejection(route, "ExtendedValidationFailed", err.Error())
p.plugin.HandleRoute(watch.Deleted, route)

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 | 🟡 Minor | ⚡ Quick win

Ignored error from downstream HandleRoute.

The watch.Deleted propagation to the next plugin drops its returned error. If the downstream delete fails while rejecting an invalid route, that failure is silently lost. At minimum log it.

As per path instructions: "Never ignore error returns".

🔧 Suggested change
 			p.recorder.RecordRouteRejection(route, "ExtendedValidationFailed", err.Error())
-			p.plugin.HandleRoute(watch.Deleted, route)
+			if derr := p.plugin.HandleRoute(watch.Deleted, route); derr != nil {
+				log.Error(derr, "failed to propagate deletion for invalid route", "route", routeName)
+			}
 			return fmt.Errorf("invalid route configuration")
📝 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
p.plugin.HandleRoute(watch.Deleted, route)
if derr := p.plugin.HandleRoute(watch.Deleted, route); derr != nil {
log.Error(derr, "failed to propagate deletion for invalid route", "route", routeName)
}
🤖 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` at line 176, The watch.Deleted
branch in the route validation flow ignores the error returned by downstream
p.plugin.HandleRoute. Capture that error and log it using the component’s
existing logger, while preserving the current propagation behavior and route
arguments.

Source: Path instructions

@openshift-ci

openshift-ci Bot commented Jul 22, 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.

@UdayYendva

UdayYendva commented Jul 23, 2026

Copy link
Copy Markdown

Cluster Version

oc get clusterversion

Output

NAME      VERSION                                                AVAILABLE   PROGRESSING   SINCE   STATUS
version   4.21.0-0-2026-07-23-053307-test-ci-ln-ndrq0kb-latest   True        False         22m     Cluster version is 4.21.0-0-2026-07-23-053307-test-ci-ln-ndrq0kb-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-6549ffc5ff-54zzg
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-6549ffc5ff-54zzg
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-6549ffc5ff-54zzg
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-6549ffc5ff-54zzg
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-6549ffc5ff-54zzg (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 23, 2026
@openshift-ci-robot

Copy link
Copy Markdown
Contributor

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

Details

In response to this:

Cluster Version

oc get clusterversion

Output

NAME      VERSION                                                AVAILABLE   PROGRESSING   SINCE   STATUS
version   4.21.0-0-2026-07-23-053307-test-ci-ln-ndrq0kb-latest   True        False         22m     Cluster version is 4.21.0-0-2026-07-23-053307-test-ci-ln-ndrq0kb-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

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.

4 participants