From 09c55ac0f98b2e4657aa06c28173554111886687 Mon Sep 17 00:00:00 2001 From: Jason Kary Date: Tue, 10 Mar 2026 10:36:56 -0400 Subject: [PATCH 1/2] Grant DPU service account lease RBAC Add dpu-rbac.yaml template rendered only when DPU host or DPU mode nodes exist. Grants the DPU service account lease access via RoleBinding to the existing openshift-ovn-kubernetes-node-limited Role. Uses system:ovn-nodes group when node identity is enabled. --- bindata/network/ovn-kubernetes/dpu-rbac.yaml | 16 +++++++++ pkg/network/ovn_kubernetes.go | 8 +++++ pkg/network/ovn_kubernetes_dpu_host_test.go | 35 ++++++++++++++++++++ 3 files changed, 59 insertions(+) create mode 100644 bindata/network/ovn-kubernetes/dpu-rbac.yaml diff --git a/bindata/network/ovn-kubernetes/dpu-rbac.yaml b/bindata/network/ovn-kubernetes/dpu-rbac.yaml new file mode 100644 index 0000000000..d028530b8c --- /dev/null +++ b/bindata/network/ovn-kubernetes/dpu-rbac.yaml @@ -0,0 +1,16 @@ +--- +# Grant lease permissions to the DPU service account so the DPU can renew +# the health check lease in the openshift-ovn-kubernetes namespace. +apiVersion: rbac.authorization.k8s.io/v1 +kind: RoleBinding +metadata: + name: openshift-ovn-kubernetes-node-dpu-service-limited + namespace: openshift-ovn-kubernetes +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: Role + name: openshift-ovn-kubernetes-node-limited +subjects: +- kind: ServiceAccount + name: ovn-kubernetes-node-dpu-service + namespace: openshift-ovn-kubernetes diff --git a/pkg/network/ovn_kubernetes.go b/pkg/network/ovn_kubernetes.go index 0ba767b963..82b3fec10f 100644 --- a/pkg/network/ovn_kubernetes.go +++ b/pkg/network/ovn_kubernetes.go @@ -473,6 +473,14 @@ func renderOVNKubernetes(conf *operv1.NetworkSpec, bootstrapResult *bootstrap.Bo objs = append(objs, manifests...) } + if len(bootstrapResult.OVN.OVNKubernetesConfig.DpuHostModeNodes) > 0 || len(bootstrapResult.OVN.OVNKubernetesConfig.DpuModeNodes) > 0 { + manifests, err = render.RenderTemplate(filepath.Join(manifestDir, "network/ovn-kubernetes/dpu-rbac.yaml"), &data) + if err != nil { + return nil, progressing, errors.Wrap(err, "failed to render DPU RBAC manifests") + } + objs = append(objs, manifests...) + } + if len(bootstrapResult.OVN.OVNKubernetesConfig.DpuModeNodes) > 0 { // "OVN_NODE_MODE" not set when render.RenderDir() called above, // so render just the error-cni.yaml with "OVN_NODE_MODE" set. diff --git a/pkg/network/ovn_kubernetes_dpu_host_test.go b/pkg/network/ovn_kubernetes_dpu_host_test.go index 3b8aef7bf8..ba44011061 100644 --- a/pkg/network/ovn_kubernetes_dpu_host_test.go +++ b/pkg/network/ovn_kubernetes_dpu_host_test.go @@ -1,6 +1,7 @@ package network import ( + "strings" "testing" "github.com/ghodss/yaml" @@ -355,3 +356,37 @@ func TestOVNKubernetesNodeSelectorOperator(t *testing.T) { } } } + +func TestOVNKubernetesDPURBAC(t *testing.T) { + g := NewGomegaWithT(t) + rbacTemplatePath := "../../bindata/network/ovn-kubernetes/dpu-rbac.yaml" + + data := render.MakeRenderData() + + objs, err := render.RenderTemplate(rbacTemplatePath, &data) + g.Expect(err).NotTo(HaveOccurred()) + + var foundRoleBinding bool + for _, obj := range objs { + if obj.GetKind() != "RoleBinding" || !strings.HasPrefix(obj.GetName(), "openshift-ovn-kubernetes-node-dpu-service") { + continue + } + foundRoleBinding = true + g.Expect(obj.GetName()).To(Equal("openshift-ovn-kubernetes-node-dpu-service-limited")) + + subjects, found, err := uns.NestedSlice(obj.Object, "subjects") + g.Expect(err).NotTo(HaveOccurred()) + g.Expect(found).To(BeTrue()) + g.Expect(subjects).To(HaveLen(1)) + + subj := subjects[0].(map[string]interface{}) + kind, _, _ := uns.NestedString(subj, "kind") + name, _, _ := uns.NestedString(subj, "name") + g.Expect(kind).To(Equal("ServiceAccount")) + g.Expect(name).To(Equal("ovn-kubernetes-node-dpu-service")) + + roleRefName, _, _ := uns.NestedString(obj.Object, "roleRef", "name") + g.Expect(roleRefName).To(Equal("openshift-ovn-kubernetes-node-limited")) + } + g.Expect(foundRoleBinding).To(BeTrue(), "DPU service RoleBinding should be present") +} From fb0d4f058ec31925c78ca8ec9dd38381be5eb176 Mon Sep 17 00:00:00 2001 From: Jason Kary Date: Tue, 10 Mar 2026 11:34:32 -0400 Subject: [PATCH 2/2] Add DPU node identity support to DPU RBAC Switch DPU lease RoleBinding subject from ServiceAccount to system:ovn-nodes group when node identity is enabled, matching the existing pattern in 002-rbac-node.yaml. Add impersonation ClusterRole/ClusterRoleBinding so the DPU service account can impersonate host node users and groups (system:nodes, system:authenticated) when authenticating through the node identity system. --- bindata/network/ovn-kubernetes/dpu-rbac.yaml | 47 ++++++++ pkg/network/ovn_kubernetes_dpu_host_test.go | 107 ++++++++++++++----- 2 files changed, 128 insertions(+), 26 deletions(-) diff --git a/bindata/network/ovn-kubernetes/dpu-rbac.yaml b/bindata/network/ovn-kubernetes/dpu-rbac.yaml index d028530b8c..13a4cbea6a 100644 --- a/bindata/network/ovn-kubernetes/dpu-rbac.yaml +++ b/bindata/network/ovn-kubernetes/dpu-rbac.yaml @@ -4,13 +4,60 @@ apiVersion: rbac.authorization.k8s.io/v1 kind: RoleBinding metadata: +{{ if .NETWORK_NODE_IDENTITY_ENABLE }} + name: openshift-ovn-kubernetes-node-dpu-service-identity-limited +{{ else }} name: openshift-ovn-kubernetes-node-dpu-service-limited +{{ end }} namespace: openshift-ovn-kubernetes roleRef: apiGroup: rbac.authorization.k8s.io kind: Role name: openshift-ovn-kubernetes-node-limited subjects: +{{ if .NETWORK_NODE_IDENTITY_ENABLE }} +- kind: Group + name: system:ovn-nodes + apiGroup: rbac.authorization.k8s.io +{{ else }} +- kind: ServiceAccount + name: ovn-kubernetes-node-dpu-service + namespace: openshift-ovn-kubernetes +{{ end }} + +{{ if .NETWORK_NODE_IDENTITY_ENABLE }} +--- +# Allow the DPU service account to impersonate node users and groups +# so it can act on behalf of the DPU host node. +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + name: openshift-ovn-kubernetes-node-dpu-host-impersonator +rules: +- apiGroups: [""] + resources: + - users + verbs: + - impersonate +- apiGroups: [""] + resources: + - groups + verbs: + - impersonate + resourceNames: + - system:nodes + - system:authenticated +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + name: openshift-ovn-kubernetes-node-dpu-host-impersonator +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: openshift-ovn-kubernetes-node-dpu-host-impersonator +subjects: - kind: ServiceAccount name: ovn-kubernetes-node-dpu-service namespace: openshift-ovn-kubernetes +{{ end }} diff --git a/pkg/network/ovn_kubernetes_dpu_host_test.go b/pkg/network/ovn_kubernetes_dpu_host_test.go index ba44011061..93f754d6b0 100644 --- a/pkg/network/ovn_kubernetes_dpu_host_test.go +++ b/pkg/network/ovn_kubernetes_dpu_host_test.go @@ -358,35 +358,90 @@ func TestOVNKubernetesNodeSelectorOperator(t *testing.T) { } func TestOVNKubernetesDPURBAC(t *testing.T) { - g := NewGomegaWithT(t) rbacTemplatePath := "../../bindata/network/ovn-kubernetes/dpu-rbac.yaml" - data := render.MakeRenderData() + testCases := []struct { + name string + identityEnable bool + expectRoleBinding string + expectSubjectKind string + expectSubjectName string + expectImpersonation bool + }{ + { + name: "without identity", + identityEnable: false, + expectRoleBinding: "openshift-ovn-kubernetes-node-dpu-service-limited", + expectSubjectKind: "ServiceAccount", + expectSubjectName: "ovn-kubernetes-node-dpu-service", + expectImpersonation: false, + }, + { + name: "with identity enabled", + identityEnable: true, + expectRoleBinding: "openshift-ovn-kubernetes-node-dpu-service-identity-limited", + expectSubjectKind: "Group", + expectSubjectName: "system:ovn-nodes", + expectImpersonation: true, + }, + } - objs, err := render.RenderTemplate(rbacTemplatePath, &data) - g.Expect(err).NotTo(HaveOccurred()) + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + g := NewGomegaWithT(t) - var foundRoleBinding bool - for _, obj := range objs { - if obj.GetKind() != "RoleBinding" || !strings.HasPrefix(obj.GetName(), "openshift-ovn-kubernetes-node-dpu-service") { - continue - } - foundRoleBinding = true - g.Expect(obj.GetName()).To(Equal("openshift-ovn-kubernetes-node-dpu-service-limited")) - - subjects, found, err := uns.NestedSlice(obj.Object, "subjects") - g.Expect(err).NotTo(HaveOccurred()) - g.Expect(found).To(BeTrue()) - g.Expect(subjects).To(HaveLen(1)) - - subj := subjects[0].(map[string]interface{}) - kind, _, _ := uns.NestedString(subj, "kind") - name, _, _ := uns.NestedString(subj, "name") - g.Expect(kind).To(Equal("ServiceAccount")) - g.Expect(name).To(Equal("ovn-kubernetes-node-dpu-service")) - - roleRefName, _, _ := uns.NestedString(obj.Object, "roleRef", "name") - g.Expect(roleRefName).To(Equal("openshift-ovn-kubernetes-node-limited")) + data := render.MakeRenderData() + data.Data["NETWORK_NODE_IDENTITY_ENABLE"] = tc.identityEnable + + objs, err := render.RenderTemplate(rbacTemplatePath, &data) + g.Expect(err).NotTo(HaveOccurred()) + + var foundRoleBinding bool + var foundImpersonationCR, foundImpersonationCRB bool + + for _, obj := range objs { + kind := obj.GetKind() + name := obj.GetName() + + if kind == "RoleBinding" && strings.HasPrefix(name, "openshift-ovn-kubernetes-node-dpu-service") { + foundRoleBinding = true + g.Expect(name).To(Equal(tc.expectRoleBinding)) + + subjects, found, err := uns.NestedSlice(obj.Object, "subjects") + g.Expect(err).NotTo(HaveOccurred()) + g.Expect(found).To(BeTrue()) + g.Expect(subjects).To(HaveLen(1)) + + subj := subjects[0].(map[string]interface{}) + subjKind, _, _ := uns.NestedString(subj, "kind") + subjName, _, _ := uns.NestedString(subj, "name") + g.Expect(subjKind).To(Equal(tc.expectSubjectKind)) + g.Expect(subjName).To(Equal(tc.expectSubjectName)) + + roleRefName, _, _ := uns.NestedString(obj.Object, "roleRef", "name") + g.Expect(roleRefName).To(Equal("openshift-ovn-kubernetes-node-limited")) + } + + if kind == "ClusterRole" && name == "openshift-ovn-kubernetes-node-dpu-host-impersonator" { + foundImpersonationCR = true + rules, found, err := uns.NestedSlice(obj.Object, "rules") + g.Expect(err).NotTo(HaveOccurred()) + g.Expect(found).To(BeTrue()) + g.Expect(len(rules)).To(BeNumerically(">=", 2)) + + rule0 := rules[0].(map[string]interface{}) + verbs0, _, _ := uns.NestedStringSlice(rule0, "verbs") + g.Expect(verbs0).To(ContainElement("impersonate")) + } + + if kind == "ClusterRoleBinding" && name == "openshift-ovn-kubernetes-node-dpu-host-impersonator" { + foundImpersonationCRB = true + } + } + + g.Expect(foundRoleBinding).To(BeTrue(), "DPU service RoleBinding should be present") + g.Expect(foundImpersonationCR).To(Equal(tc.expectImpersonation)) + g.Expect(foundImpersonationCRB).To(Equal(tc.expectImpersonation)) + }) } - g.Expect(foundRoleBinding).To(BeTrue(), "DPU service RoleBinding should be present") }