diff --git a/PROJECT b/PROJECT
index 24face925..5cb5ac48c 100644
--- a/PROJECT
+++ b/PROJECT
@@ -289,4 +289,12 @@ resources:
kind: DHCPRelay
path: github.com/ironcore-dev/network-operator/api/core/v1alpha1
version: v1alpha1
+- api:
+ crdVersion: v1
+ namespaced: true
+ controller: true
+ domain: networking.metal.ironcore.dev
+ kind: MacSec
+ path: github.com/ironcore-dev/network-operator/api/v1alpha1
+ version: v1alpha1
version: "3"
diff --git a/Tiltfile b/Tiltfile
index b03e5885b..9bd7c35ac 100644
--- a/Tiltfile
+++ b/Tiltfile
@@ -70,6 +70,10 @@ k8s_resource(new_name='acl', objects=['acl:accesscontrollist'], trigger_mode=TRI
k8s_yaml('./config/samples/v1alpha1_certificate.yaml')
k8s_resource(new_name='trustpoint', objects=['network-operator:issuer', 'network-operator-ca:certificate', 'trustpoint:certificate'], trigger_mode=TRIGGER_MODE_MANUAL, auto_init=False)
+#k8s_yaml('./config/samples/v1alpha1_macsec.yaml')
+#k8s_resource(new_name='macsec-eth1-1', objects=['macsec-sample:macsec'], resource_deps=['leaf1', 'eth1-1'], trigger_mode=TRIGGER_MODE_MANUAL, auto_init=False)
+#k8s_resource(new_name='macsec-secrets', objects=['customer-secret:secret', 'customer-secret-rollover:secret'], trigger_mode=TRIGGER_MODE_MANUAL, auto_init=False)
+
k8s_yaml('./config/samples/v1alpha1_snmp.yaml')
k8s_resource(new_name='snmp', objects=['snmp:snmp'], trigger_mode=TRIGGER_MODE_MANUAL, auto_init=False)
diff --git a/api/core/v1alpha1/macsec_types.go b/api/core/v1alpha1/macsec_types.go
new file mode 100644
index 000000000..c2f248ea9
--- /dev/null
+++ b/api/core/v1alpha1/macsec_types.go
@@ -0,0 +1,129 @@
+// SPDX-FileCopyrightText: 2026 SAP SE or an SAP affiliate company and IronCore contributors
+// SPDX-License-Identifier: Apache-2.0
+
+package v1alpha1
+
+import (
+ metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+)
+
+// / ConfidentialityOffset represents the number of octets in an Ethernet frame that are sent in unencrypted plain-text.
+type ConfidentialityOffset uint8
+
+// +kubebuilder:validation:Enum=0;30;50
+const (
+ // ZeroBytes represents broadcast traffic.
+ ZeroBytes ConfidentialityOffset = 0
+ // ThirtyBytes represents multicast traffic.
+ ThirtyBytes ConfidentialityOffset = 30
+ // FiftyBytes represents unicast traffic.
+ FiftyBytes ConfidentialityOffset = 50
+)
+
+// MacSecSpec defines the desired state of MacSec.
+type MacSecSpec struct {
+ // DeviceName is the name of the Device this object belongs to. The Device object must exist in the same namespace.
+ // Immutable.
+ // +required
+ // +kubebuilder:validation:XValidation:rule="self == oldSelf",message="DeviceRef is immutable"
+ DeviceRef LocalObjectReference `json:"deviceRef"`
+
+ // ProviderConfigRef is a reference to a resource holding the provider-specific configuration.
+ // This reference is used to link the Interface to its provider-specific configuration.
+ // +optional
+ ProviderConfigRef *TypedLocalObjectReference `json:"providerConfigRef,omitempty"`
+
+ // Name is the name of the interface.
+ // +required
+ // +kubebuilder:validation:MinLength=1
+ // +kubebuilder:validation:MaxLength=255
+ // +kubebuilder:validation:XValidation:rule="self == oldSelf",message="Name is immutable"
+ Name string `json:"name"`
+
+ // Description provides a human-readable description of the macsec policy.
+ // +optional
+ // +kubebuilder:validation:MaxLength=255
+ Description string `json:"description,omitempty"`
+
+ // InterfaceRef is a reference to the interface on which MACsec is enabled. The interface must exist on the Device specified by deviceRef.
+ // Immutable.
+ // +required
+ // +kubebuilder:validation:XValidation:rule="self == oldSelf",message="InterfaceRef is immutable"
+ InterfaceRef LocalObjectReference `json:"interfaceRef,omitempty"`
+
+ // PreSharedKeyRef is a list of references to pre-shared keys used for MACSec encryption.
+ // +optional
+ PreSharedKeyRef []LocalObjectReference `json:"preSharedkeyRef,omitempty"`
+
+ // Policy defines the MACSec policy configuration.
+ // +optional
+ Policy *MacSecPolicy `json:"policy,omitempty"`
+}
+
+type MacSecPolicy struct {
+ CipherSuite string `json:"cipherSuite,omitempty"`
+
+ // Number of octets in an Ethernet frame that are sent in unencrypted plain-text
+ // +optional
+ ConfidentialityOffset ConfidentialityOffset `json:"confidentialityOffset,omitempty"`
+
+ // Specifies the key server priority used by the MACsec Key Agreement (MKA) protocol to select the key server when
+ // MACsec is enabled using static connectivity association key (CAK) security mode.
+ // +optional
+ KeyServerPriority uint8 `json:"keyServerPriority,omitempty"`
+
+ // Number of out-of-order packets that can be received before the secure channel is considered to be inoperable.
+ // A value of 0 means that frames are accepted only in the correct order.
+ // +optional
+ RelayProtection uint16 `json:"relayProtection,omitempty"`
+}
+
+// MacSecStatus defines the observed state of MacSec.
+type MacSecStatus struct {
+ // The conditions are a list of status objects that describe the state of the MacSec.
+ //+listType=map
+ //+listMapKey=type
+ //+patchStrategy=merge
+ //+patchMergeKey=type
+ //+optional
+ Conditions []metav1.Condition `json:"conditions,omitempty" patchStrategy:"merge" patchMergeKey:"type"`
+}
+
+// +kubebuilder:object:root=true
+// +kubebuilder:subresource:status
+// +kubebuilder:printcolumn:name="Device",type="string",JSONPath=".spec.deviceRef.name",description="The device this MacSec is configured on"
+// +kubebuilder:printcolumn:name="Interface",type="string",JSONPath=".spec.interfaceRef.name",description="The interface this MacSec is configured on"
+// +kubebuilder:printcolumn:name="Ready",type="string",JSONPath=".status.conditions[?(@.type=='Ready')].status",description="Ready status of the MacSec"
+// +kubebuilder:printcolumn:name="Age",type="date",JSONPath=".metadata.creationTimestamp"
+
+// MacSec is the Schema for the macsecs API.
+type MacSec struct {
+ metav1.TypeMeta `json:",inline"`
+ metav1.ObjectMeta `json:"metadata,omitempty"`
+
+ Spec MacSecSpec `json:"spec,omitempty"`
+ Status MacSecStatus `json:"status,omitempty"`
+}
+
+// +kubebuilder:object:root=true
+
+// MacSecList contains a list of MacSec.
+type MacSecList struct {
+ metav1.TypeMeta `json:",inline"`
+ metav1.ListMeta `json:"metadata,omitempty"`
+ Items []MacSec `json:"items"`
+}
+
+// GetConditions implements conditions.Getter.
+func (m *MacSec) GetConditions() []metav1.Condition {
+ return m.Status.Conditions
+}
+
+// SetConditions implements conditions.Setter.
+func (m *MacSec) SetConditions(conditions []metav1.Condition) {
+ m.Status.Conditions = conditions
+}
+
+func init() {
+ SchemeBuilder.Register(&MacSec{}, &MacSecList{})
+}
diff --git a/api/core/v1alpha1/zz_generated.deepcopy.go b/api/core/v1alpha1/zz_generated.deepcopy.go
index 3571a97c0..217594819 100644
--- a/api/core/v1alpha1/zz_generated.deepcopy.go
+++ b/api/core/v1alpha1/zz_generated.deepcopy.go
@@ -2090,6 +2090,134 @@ func (in *LogServer) DeepCopy() *LogServer {
return out
}
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *MacSec) DeepCopyInto(out *MacSec) {
+ *out = *in
+ out.TypeMeta = in.TypeMeta
+ in.ObjectMeta.DeepCopyInto(&out.ObjectMeta)
+ in.Spec.DeepCopyInto(&out.Spec)
+ in.Status.DeepCopyInto(&out.Status)
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new MacSec.
+func (in *MacSec) DeepCopy() *MacSec {
+ if in == nil {
+ return nil
+ }
+ out := new(MacSec)
+ in.DeepCopyInto(out)
+ return out
+}
+
+// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
+func (in *MacSec) DeepCopyObject() runtime.Object {
+ if c := in.DeepCopy(); c != nil {
+ return c
+ }
+ return nil
+}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *MacSecList) DeepCopyInto(out *MacSecList) {
+ *out = *in
+ out.TypeMeta = in.TypeMeta
+ in.ListMeta.DeepCopyInto(&out.ListMeta)
+ if in.Items != nil {
+ in, out := &in.Items, &out.Items
+ *out = make([]MacSec, len(*in))
+ for i := range *in {
+ (*in)[i].DeepCopyInto(&(*out)[i])
+ }
+ }
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new MacSecList.
+func (in *MacSecList) DeepCopy() *MacSecList {
+ if in == nil {
+ return nil
+ }
+ out := new(MacSecList)
+ in.DeepCopyInto(out)
+ return out
+}
+
+// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
+func (in *MacSecList) DeepCopyObject() runtime.Object {
+ if c := in.DeepCopy(); c != nil {
+ return c
+ }
+ return nil
+}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *MacSecPolicy) DeepCopyInto(out *MacSecPolicy) {
+ *out = *in
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new MacSecPolicy.
+func (in *MacSecPolicy) DeepCopy() *MacSecPolicy {
+ if in == nil {
+ return nil
+ }
+ out := new(MacSecPolicy)
+ in.DeepCopyInto(out)
+ return out
+}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *MacSecSpec) DeepCopyInto(out *MacSecSpec) {
+ *out = *in
+ out.DeviceRef = in.DeviceRef
+ if in.ProviderConfigRef != nil {
+ in, out := &in.ProviderConfigRef, &out.ProviderConfigRef
+ *out = new(TypedLocalObjectReference)
+ **out = **in
+ }
+ out.InterfaceRef = in.InterfaceRef
+ if in.PreSharedKeyRef != nil {
+ in, out := &in.PreSharedKeyRef, &out.PreSharedKeyRef
+ *out = make([]LocalObjectReference, len(*in))
+ copy(*out, *in)
+ }
+ if in.Policy != nil {
+ in, out := &in.Policy, &out.Policy
+ *out = new(MacSecPolicy)
+ **out = **in
+ }
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new MacSecSpec.
+func (in *MacSecSpec) DeepCopy() *MacSecSpec {
+ if in == nil {
+ return nil
+ }
+ out := new(MacSecSpec)
+ in.DeepCopyInto(out)
+ return out
+}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *MacSecStatus) DeepCopyInto(out *MacSecStatus) {
+ *out = *in
+ if in.Conditions != nil {
+ in, out := &in.Conditions, &out.Conditions
+ *out = make([]v1.Condition, len(*in))
+ for i := range *in {
+ (*in)[i].DeepCopyInto(&(*out)[i])
+ }
+ }
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new MacSecStatus.
+func (in *MacSecStatus) DeepCopy() *MacSecStatus {
+ if in == nil {
+ return nil
+ }
+ out := new(MacSecStatus)
+ in.DeepCopyInto(out)
+ return out
+}
+
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *ManagementAccess) DeepCopyInto(out *ManagementAccess) {
*out = *in
diff --git a/charts/network-operator/templates/rbac/manager-role.yaml b/charts/network-operator/templates/rbac/manager-role.yaml
index 580e6b8bd..55f0b65f7 100644
--- a/charts/network-operator/templates/rbac/manager-role.yaml
+++ b/charts/network-operator/templates/rbac/manager-role.yaml
@@ -27,6 +27,14 @@ rules:
- list
- update
- watch
+- apiGroups:
+ - ""
+ - events.k8s.io
+ resources:
+ - events
+ verbs:
+ - create
+ - patch
- apiGroups:
- coordination.k8s.io
resources:
@@ -38,13 +46,6 @@ rules:
- list
- update
- watch
-- apiGroups:
- - events.k8s.io
- resources:
- - events
- verbs:
- - create
- - patch
- apiGroups:
- networking.metal.ironcore.dev
resources:
@@ -60,6 +61,7 @@ rules:
- interfaces
- isis
- lldps
+ - macsecs
- managementaccesses
- networkvirtualizationedges
- ntp
@@ -95,6 +97,7 @@ rules:
- interfaces/finalizers
- isis/finalizers
- lldps/finalizers
+ - macsecs/finalizers
- managementaccesses/finalizers
- networkvirtualizationedges/finalizers
- ntp/finalizers
@@ -124,6 +127,7 @@ rules:
- interfaces/status
- isis/status
- lldps/status
+ - macsecs/status
- managementaccesses/status
- networkvirtualizationedges/status
- ntp/status
diff --git a/cmd/main.go b/cmd/main.go
index 82de236ce..d5a0042bd 100644
--- a/cmd/main.go
+++ b/cmd/main.go
@@ -625,6 +625,18 @@ func main() { //nolint:gocyclo
os.Exit(1)
}
+ if err := (&corecontroller.MacSecReconciler{
+ Client: mgr.GetClient(),
+ Scheme: mgr.GetScheme(),
+ Recorder: mgr.GetEventRecorder("macsec-controller"),
+ Provider: prov,
+ Locker: locker,
+ RequeueInterval: requeueInterval,
+ }).SetupWithManager(ctx, mgr); err != nil {
+ setupLog.Error(err, "unable to create controller", "controller", "MacSec")
+ os.Exit(1)
+ }
+
if os.Getenv("ENABLE_WEBHOOKS") != "false" {
if err := webhookv1alpha1.SetupVRFWebhookWithManager(mgr); err != nil {
setupLog.Error(err, "unable to create webhook", "webhook", "VRF")
diff --git a/config/crd/bases/networking.metal.ironcore.dev_macsecs.yaml b/config/crd/bases/networking.metal.ironcore.dev_macsecs.yaml
new file mode 100644
index 000000000..9bcb9f1d9
--- /dev/null
+++ b/config/crd/bases/networking.metal.ironcore.dev_macsecs.yaml
@@ -0,0 +1,257 @@
+---
+apiVersion: apiextensions.k8s.io/v1
+kind: CustomResourceDefinition
+metadata:
+ annotations:
+ controller-gen.kubebuilder.io/version: v0.21.0
+ name: macsecs.networking.metal.ironcore.dev
+spec:
+ group: networking.metal.ironcore.dev
+ names:
+ kind: MacSec
+ listKind: MacSecList
+ plural: macsecs
+ singular: macsec
+ scope: Namespaced
+ versions:
+ - additionalPrinterColumns:
+ - description: The device this MacSec is configured on
+ jsonPath: .spec.deviceRef.name
+ name: Device
+ type: string
+ - description: The interface this MacSec is configured on
+ jsonPath: .spec.interfaceRef.name
+ name: Interface
+ type: string
+ - description: Ready status of the MacSec
+ jsonPath: .status.conditions[?(@.type=='Ready')].status
+ name: Ready
+ type: string
+ - jsonPath: .metadata.creationTimestamp
+ name: Age
+ type: date
+ name: v1alpha1
+ schema:
+ openAPIV3Schema:
+ description: MacSec is the Schema for the macsecs API.
+ properties:
+ apiVersion:
+ description: |-
+ APIVersion defines the versioned schema of this representation of an object.
+ Servers should convert recognized schemas to the latest internal value, and
+ may reject unrecognized values.
+ More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources
+ type: string
+ kind:
+ description: |-
+ Kind is a string value representing the REST resource this object represents.
+ Servers may infer this from the endpoint the client submits requests to.
+ Cannot be updated.
+ In CamelCase.
+ More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds
+ type: string
+ metadata:
+ type: object
+ spec:
+ description: MacSecSpec defines the desired state of MacSec.
+ properties:
+ description:
+ description: Description provides a human-readable description of
+ the macsec policy.
+ maxLength: 255
+ type: string
+ deviceRef:
+ description: |-
+ DeviceName is the name of the Device this object belongs to. The Device object must exist in the same namespace.
+ Immutable.
+ properties:
+ name:
+ description: |-
+ Name of the referent.
+ More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names
+ maxLength: 63
+ minLength: 1
+ type: string
+ required:
+ - name
+ type: object
+ x-kubernetes-map-type: atomic
+ x-kubernetes-validations:
+ - message: DeviceRef is immutable
+ rule: self == oldSelf
+ interfaceRef:
+ description: |-
+ InterfaceRef is a reference to the interface on which MACsec is enabled. The interface must exist on the Device specified by deviceRef.
+ Immutable.
+ properties:
+ name:
+ description: |-
+ Name of the referent.
+ More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names
+ maxLength: 63
+ minLength: 1
+ type: string
+ required:
+ - name
+ type: object
+ x-kubernetes-map-type: atomic
+ x-kubernetes-validations:
+ - message: InterfaceRef is immutable
+ rule: self == oldSelf
+ name:
+ description: Name is the name of the interface.
+ maxLength: 255
+ minLength: 1
+ type: string
+ x-kubernetes-validations:
+ - message: Name is immutable
+ rule: self == oldSelf
+ policy:
+ description: Policy defines the MACSec policy configuration.
+ properties:
+ cipherSuite:
+ type: string
+ confidentialityOffset:
+ description: Number of octets in an Ethernet frame that are sent
+ in unencrypted plain-text
+ type: integer
+ keyServerPriority:
+ description: |-
+ Specifies the key server priority used by the MACsec Key Agreement (MKA) protocol to select the key server when
+ MACsec is enabled using static connectivity association key (CAK) security mode.
+ type: integer
+ relayProtection:
+ description: |-
+ Number of out-of-order packets that can be received before the secure channel is considered to be inoperable.
+ A value of 0 means that frames are accepted only in the correct order.
+ type: integer
+ type: object
+ preSharedkeyRef:
+ description: PreSharedKeyRef is a list of references to pre-shared
+ keys used for MACSec encryption.
+ items:
+ description: |-
+ LocalObjectReference contains enough information to locate a
+ referenced object inside the same namespace.
+ properties:
+ name:
+ description: |-
+ Name of the referent.
+ More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names
+ maxLength: 63
+ minLength: 1
+ type: string
+ required:
+ - name
+ type: object
+ x-kubernetes-map-type: atomic
+ type: array
+ providerConfigRef:
+ description: |-
+ ProviderConfigRef is a reference to a resource holding the provider-specific configuration.
+ This reference is used to link the Interface to its provider-specific configuration.
+ properties:
+ apiVersion:
+ description: APIVersion is the api group version of the resource
+ being referenced.
+ maxLength: 253
+ minLength: 1
+ pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*\/)?([a-z0-9]([-a-z0-9]*[a-z0-9])?)$
+ type: string
+ kind:
+ description: |-
+ Kind of the resource being referenced.
+ Kind must consist of alphanumeric characters or '-', start with an alphabetic character, and end with an alphanumeric character.
+ maxLength: 63
+ minLength: 1
+ pattern: ^[a-zA-Z]([-a-zA-Z0-9]*[a-zA-Z0-9])?$
+ type: string
+ name:
+ description: |-
+ Name of the resource being referenced.
+ Name must consist of lower case alphanumeric characters, '-' or '.', and must start and end with an alphanumeric character.
+ maxLength: 253
+ minLength: 1
+ pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$
+ type: string
+ required:
+ - apiVersion
+ - kind
+ - name
+ type: object
+ x-kubernetes-map-type: atomic
+ required:
+ - deviceRef
+ - interfaceRef
+ - name
+ type: object
+ status:
+ description: MacSecStatus defines the observed state of MacSec.
+ properties:
+ conditions:
+ description: The conditions are a list of status objects that describe
+ the state of the MacSec.
+ items:
+ description: Condition contains details for one aspect of the current
+ state of this API Resource.
+ properties:
+ lastTransitionTime:
+ description: |-
+ lastTransitionTime is the last time the condition transitioned from one status to another.
+ This should be when the underlying condition changed. If that is not known, then using the time when the API field changed is acceptable.
+ format: date-time
+ type: string
+ message:
+ description: |-
+ message is a human readable message indicating details about the transition.
+ This may be an empty string.
+ maxLength: 32768
+ type: string
+ observedGeneration:
+ description: |-
+ observedGeneration represents the .metadata.generation that the condition was set based upon.
+ For instance, if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration is 9, the condition is out of date
+ with respect to the current state of the instance.
+ format: int64
+ minimum: 0
+ type: integer
+ reason:
+ description: |-
+ reason contains a programmatic identifier indicating the reason for the condition's last transition.
+ Producers of specific condition types may define expected values and meanings for this field,
+ and whether the values are considered a guaranteed API.
+ The value should be a CamelCase string.
+ This field may not be empty.
+ maxLength: 1024
+ minLength: 1
+ pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$
+ type: string
+ status:
+ description: status of the condition, one of True, False, Unknown.
+ enum:
+ - "True"
+ - "False"
+ - Unknown
+ type: string
+ type:
+ description: type of condition in CamelCase or in foo.example.com/CamelCase.
+ maxLength: 316
+ pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$
+ type: string
+ required:
+ - lastTransitionTime
+ - message
+ - reason
+ - status
+ - type
+ type: object
+ type: array
+ x-kubernetes-list-map-keys:
+ - type
+ x-kubernetes-list-type: map
+ type: object
+ type: object
+ served: true
+ storage: true
+ subresources:
+ status: {}
diff --git a/config/crd/kustomization.yaml b/config/crd/kustomization.yaml
index 182f057f4..d1311bd53 100644
--- a/config/crd/kustomization.yaml
+++ b/config/crd/kustomization.yaml
@@ -34,6 +34,7 @@ resources:
- bases/nx.cisco.networking.metal.ironcore.dev_interfaceconfigs.yaml
- bases/nx.cisco.networking.metal.ironcore.dev_lldpconfigs.yaml
- bases/nx.cisco.networking.metal.ironcore.dev_bgpconfigs.yaml
+- bases/networking.metal.ironcore.dev_macsecs.yaml
# +kubebuilder:scaffold:crdkustomizeresource
patches: []
diff --git a/config/rbac/kustomization.yaml b/config/rbac/kustomization.yaml
index f6aab1b99..3b2fbf04e 100644
--- a/config/rbac/kustomization.yaml
+++ b/config/rbac/kustomization.yaml
@@ -22,6 +22,9 @@ resources:
# default, aiding admins in cluster management. Those roles are
# not used by the network-operator itself. You can comment the following lines
# if you do not want those helpers be installed with your Project.
+- macsec_admin_role.yaml
+- macsec_editor_role.yaml
+- macsec_viewer_role.yaml
- accesscontrollist_admin_role.yaml
- accesscontrollist_editor_role.yaml
- accesscontrollist_viewer_role.yaml
diff --git a/config/rbac/macsec_admin_role.yaml b/config/rbac/macsec_admin_role.yaml
new file mode 100644
index 000000000..81f6da500
--- /dev/null
+++ b/config/rbac/macsec_admin_role.yaml
@@ -0,0 +1,27 @@
+# This rule is not used by the project network-operator itself.
+# It is provided to allow the cluster admin to help manage permissions for users.
+#
+# Grants full permissions ('*') over networking.metal.ironcore.dev.
+# This role is intended for users authorized to modify roles and bindings within the cluster,
+# enabling them to delegate specific permissions to other users or groups as needed.
+
+apiVersion: rbac.authorization.k8s.io/v1
+kind: ClusterRole
+metadata:
+ labels:
+ app.kubernetes.io/name: network-operator
+ app.kubernetes.io/managed-by: kustomize
+ name: macsec-admin-role
+rules:
+- apiGroups:
+ - networking.metal.ironcore.dev
+ resources:
+ - macsecs
+ verbs:
+ - '*'
+- apiGroups:
+ - networking.metal.ironcore.dev
+ resources:
+ - macsecs/status
+ verbs:
+ - get
diff --git a/config/rbac/macsec_editor_role.yaml b/config/rbac/macsec_editor_role.yaml
new file mode 100644
index 000000000..966f3dd39
--- /dev/null
+++ b/config/rbac/macsec_editor_role.yaml
@@ -0,0 +1,33 @@
+# This rule is not used by the project network-operator itself.
+# It is provided to allow the cluster admin to help manage permissions for users.
+#
+# Grants permissions to create, update, and delete resources within the networking.metal.ironcore.dev.
+# This role is intended for users who need to manage these resources
+# but should not control RBAC or manage permissions for others.
+
+apiVersion: rbac.authorization.k8s.io/v1
+kind: ClusterRole
+metadata:
+ labels:
+ app.kubernetes.io/name: network-operator
+ app.kubernetes.io/managed-by: kustomize
+ name: macsec-editor-role
+rules:
+- apiGroups:
+ - networking.metal.ironcore.dev
+ resources:
+ - macsecs
+ verbs:
+ - create
+ - delete
+ - get
+ - list
+ - patch
+ - update
+ - watch
+- apiGroups:
+ - networking.metal.ironcore.dev
+ resources:
+ - macsecs/status
+ verbs:
+ - get
diff --git a/config/rbac/macsec_viewer_role.yaml b/config/rbac/macsec_viewer_role.yaml
new file mode 100644
index 000000000..1c8ccb06f
--- /dev/null
+++ b/config/rbac/macsec_viewer_role.yaml
@@ -0,0 +1,29 @@
+# This rule is not used by the project network-operator itself.
+# It is provided to allow the cluster admin to help manage permissions for users.
+#
+# Grants read-only access to networking.metal.ironcore.dev resources.
+# This role is intended for users who need visibility into these resources
+# without permissions to modify them. It is ideal for monitoring purposes and limited-access viewing.
+
+apiVersion: rbac.authorization.k8s.io/v1
+kind: ClusterRole
+metadata:
+ labels:
+ app.kubernetes.io/name: network-operator
+ app.kubernetes.io/managed-by: kustomize
+ name: macsec-viewer-role
+rules:
+- apiGroups:
+ - networking.metal.ironcore.dev
+ resources:
+ - macsecs
+ verbs:
+ - get
+ - list
+ - watch
+- apiGroups:
+ - networking.metal.ironcore.dev
+ resources:
+ - macsecs/status
+ verbs:
+ - get
diff --git a/config/rbac/role.yaml b/config/rbac/role.yaml
index e9757ff33..fbf876e6b 100644
--- a/config/rbac/role.yaml
+++ b/config/rbac/role.yaml
@@ -21,6 +21,14 @@ rules:
- list
- update
- watch
+- apiGroups:
+ - ""
+ - events.k8s.io
+ resources:
+ - events
+ verbs:
+ - create
+ - patch
- apiGroups:
- coordination.k8s.io
resources:
@@ -32,13 +40,6 @@ rules:
- list
- update
- watch
-- apiGroups:
- - events.k8s.io
- resources:
- - events
- verbs:
- - create
- - patch
- apiGroups:
- networking.metal.ironcore.dev
resources:
@@ -54,6 +55,7 @@ rules:
- interfaces
- isis
- lldps
+ - macsecs
- managementaccesses
- networkvirtualizationedges
- ntp
@@ -89,6 +91,7 @@ rules:
- interfaces/finalizers
- isis/finalizers
- lldps/finalizers
+ - macsecs/finalizers
- managementaccesses/finalizers
- networkvirtualizationedges/finalizers
- ntp/finalizers
@@ -118,6 +121,7 @@ rules:
- interfaces/status
- isis/status
- lldps/status
+ - macsecs/status
- managementaccesses/status
- networkvirtualizationedges/status
- ntp/status
diff --git a/config/samples/kustomization.yaml b/config/samples/kustomization.yaml
index ac22ba582..240c38591 100644
--- a/config/samples/kustomization.yaml
+++ b/config/samples/kustomization.yaml
@@ -24,6 +24,7 @@ resources:
- v1alpha1_nve.yaml
- v1alpha1_prefixset.yaml
- v1alpha1_routingpolicy.yaml
+- v1alpha1_macsec.yaml
- cisco/nx/v1alpha1_bordergateway.yaml
- cisco/nx/v1alpha1_managementaccessconfig.yaml
- cisco/nx/v1alpha1_nveconfig.yaml
diff --git a/config/samples/v1alpha1_macsec.yaml b/config/samples/v1alpha1_macsec.yaml
new file mode 100644
index 000000000..94e2c15f7
--- /dev/null
+++ b/config/samples/v1alpha1_macsec.yaml
@@ -0,0 +1,37 @@
+ apiVersion: networking.metal.ironcore.dev/v1alpha1
+ kind: MacSec
+ metadata:
+ labels:
+ app.kubernetes.io/name: network-operator
+ app.kubernetes.io/managed-by: kustomize
+ networking.metal.ironcore.dev/device-name: leaf1
+ name: macsec-sample
+ spec:
+ deviceRef:
+ name: leaf1
+ interfaceRef:
+ name: eth1-1
+ name: macsec-eth1-1
+ description: "MACSec configuration for eth1/1 interface"
+ preSharedkeyRef:
+ - name: customer-secret
+ policy:
+ cipherSuite: gcm-aes-128
+ confidentialityOffset: 30
+ keyServerPriority: 0
+ relayProtection: 0
+---
+apiVersion: v1
+kind: Secret
+metadata:
+ name: customer-secret
+ labels:
+ app.kubernetes.io/name: network-operator
+ app.kubernetes.io/managed-by: kustomize
+type: Opaque
+data:
+ key: Y3VzdG9tZXItc2VjcmV0LWtleQ== # preshared key
+stringdata:
+ keyId: "1234"
+ lifetime: "3600"
+ algorithm: "aes-128-cmac"
diff --git a/docs/api-reference/index.md b/docs/api-reference/index.md
index b333d37e9..1dc81fdde 100644
--- a/docs/api-reference/index.md
+++ b/docs/api-reference/index.md
@@ -26,6 +26,7 @@ SPDX-License-Identifier: Apache-2.0
- [ISIS](#isis)
- [Interface](#interface)
- [LLDP](#lldp)
+- [MacSec](#macsec)
- [ManagementAccess](#managementaccess)
- [NTP](#ntp)
- [NetworkVirtualizationEdge](#networkvirtualizationedge)
@@ -804,6 +805,19 @@ _Appears in:_
| `MD5` | |
+#### ConfidentialityOffset
+
+_Underlying type:_ _integer_
+
+/ ConfidentialityOffset represents the number of octets in an Ethernet frame that are sent in unencrypted plain-text.
+
+
+
+_Appears in:_
+- [MacSecPolicy](#macsecpolicy)
+
+
+
#### ConfigMapKeySelector
@@ -1677,6 +1691,7 @@ _Appears in:_
- [KeepAlive](#keepalive)
- [LLDPInterface](#lldpinterface)
- [LLDPSpec](#lldpspec)
+- [MacSecSpec](#macsecspec)
- [ManagementAccessSpec](#managementaccessspec)
- [NTPSpec](#ntpspec)
- [NetworkVirtualizationEdgeSpec](#networkvirtualizationedgespec)
@@ -1739,6 +1754,81 @@ _Appears in:_
| `port` _integer_ | The destination port number for syslog UDP messages to
the server. The default is 514. | 514 | Optional: \{\}
|
+#### MacSec
+
+
+
+MacSec is the Schema for the macsecs API.
+
+
+
+
+
+| Field | Description | Default | Validation |
+| --- | --- | --- | --- |
+| `apiVersion` _string_ | `networking.metal.ironcore.dev/v1alpha1` | | |
+| `kind` _string_ | `MacSec` | | |
+| `metadata` _[ObjectMeta](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.35/#objectmeta-v1-meta)_ | Refer to Kubernetes API documentation for fields of `metadata`. | | |
+| `spec` _[MacSecSpec](#macsecspec)_ | | | |
+| `status` _[MacSecStatus](#macsecstatus)_ | | | |
+
+
+#### MacSecPolicy
+
+
+
+
+
+
+
+_Appears in:_
+- [MacSecSpec](#macsecspec)
+
+| Field | Description | Default | Validation |
+| --- | --- | --- | --- |
+| `cipherSuite` _string_ | | | |
+| `confidentialityOffset` _[ConfidentialityOffset](#confidentialityoffset)_ | Number of octets in an Ethernet frame that are sent in unencrypted plain-text | | Optional: \{\}
|
+| `keyServerPriority` _integer_ | Specifies the key server priority used by the MACsec Key Agreement (MKA) protocol to select the key server when
MACsec is enabled using static connectivity association key (CAK) security mode. | | Optional: \{\}
|
+| `relayProtection` _integer_ | Number of out-of-order packets that can be received before the secure channel is considered to be inoperable.
A value of 0 means that frames are accepted only in the correct order. | | Optional: \{\}
|
+
+
+#### MacSecSpec
+
+
+
+MacSecSpec defines the desired state of MacSec.
+
+
+
+_Appears in:_
+- [MacSec](#macsec)
+
+| Field | Description | Default | Validation |
+| --- | --- | --- | --- |
+| `deviceRef` _[LocalObjectReference](#localobjectreference)_ | DeviceName is the name of the Device this object belongs to. The Device object must exist in the same namespace.
Immutable. | | Required: \{\}
|
+| `interfaceRef` _[LocalObjectReference](#localobjectreference)_ | InterfaceRef is a reference to the interface on which MACsec is enabled. The interface must exist on the Device specified by deviceRef.
Immutable. | | Required: \{\}
|
+| `name` _string_ | Name is the name of the interface. | | MaxLength: 255
MinLength: 1
Required: \{\}
|
+| `description` _string_ | Description provides a human-readable description of the macsec policy. | | MaxLength: 255
Optional: \{\}
|
+| `preSharedkeyRef` _[LocalObjectReference](#localobjectreference) array_ | PreSharedKeyRef is a list of references to pre-shared keys used for MACSec encryption. | | Optional: \{\}
|
+| `policy` _[MacSecPolicy](#macsecpolicy)_ | Policy defines the MACSec policy configuration. | | Optional: \{\}
|
+
+
+#### MacSecStatus
+
+
+
+MacSecStatus defines the observed state of MacSec.
+
+
+
+_Appears in:_
+- [MacSec](#macsec)
+
+| Field | Description | Default | Validation |
+| --- | --- | --- | --- |
+| `conditions` _[Condition](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.35/#condition-v1-meta) array_ | The conditions are a list of status objects that describe the state of the MacSec. | | Optional: \{\}
|
+
+
#### ManagementAccess
diff --git a/internal/controller/core/macsec_controller.go b/internal/controller/core/macsec_controller.go
new file mode 100644
index 000000000..418bee772
--- /dev/null
+++ b/internal/controller/core/macsec_controller.go
@@ -0,0 +1,436 @@
+// SPDX-FileCopyrightText: 2026 SAP SE or an SAP affiliate company and IronCore contributors
+// SPDX-License-Identifier: Apache-2.0
+
+package core
+
+import (
+ "context"
+ "errors"
+ "fmt"
+ "time"
+
+ corev1 "k8s.io/api/core/v1"
+
+ "k8s.io/apimachinery/pkg/api/equality"
+ apierrors "k8s.io/apimachinery/pkg/api/errors"
+ "k8s.io/apimachinery/pkg/api/meta"
+ metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+ "k8s.io/apimachinery/pkg/runtime"
+ kerrors "k8s.io/apimachinery/pkg/util/errors"
+ "k8s.io/client-go/tools/events"
+ ctrl "sigs.k8s.io/controller-runtime"
+ "sigs.k8s.io/controller-runtime/pkg/builder"
+ "sigs.k8s.io/controller-runtime/pkg/client"
+ "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
+ "sigs.k8s.io/controller-runtime/pkg/event"
+ "sigs.k8s.io/controller-runtime/pkg/handler"
+ "sigs.k8s.io/controller-runtime/pkg/predicate"
+
+ "github.com/ironcore-dev/network-operator/api/core/v1alpha1"
+ "github.com/ironcore-dev/network-operator/internal/conditions"
+ "github.com/ironcore-dev/network-operator/internal/deviceutil"
+ "github.com/ironcore-dev/network-operator/internal/paused"
+ "github.com/ironcore-dev/network-operator/internal/provider"
+ "github.com/ironcore-dev/network-operator/internal/resourcelock"
+)
+
+// MacSecReconciler reconciles a MacSec object
+type MacSecReconciler struct {
+ client.Client
+ Scheme *runtime.Scheme
+
+ // WatchFilterValue is the label value used to filter events prior to reconciliation.
+ WatchFilterValue string
+
+ // Recorder is used to record events for the controller.
+ // More info: https://book.kubebuilder.io/reference/raising-events
+ Recorder events.EventRecorder
+
+ // Provider is the driver that will be used to create & delete the macsec.
+ Provider provider.ProviderFunc
+
+ // Locker is used to synchronize operations on resources targeting the same device.
+ Locker *resourcelock.ResourceLocker
+
+ // RequeueInterval is the duration after which the controller should requeue the reconciliation,
+ // regardless of changes.
+ RequeueInterval time.Duration
+}
+
+// +kubebuilder:rbac:groups=networking.metal.ironcore.dev,resources=macsecs,verbs=get;list;watch;create;update;patch;delete
+// +kubebuilder:rbac:groups=networking.metal.ironcore.dev,resources=macsecs/status,verbs=get;update;patch
+// +kubebuilder:rbac:groups=networking.metal.ironcore.dev,resources=macsecs/finalizers,verbs=update
+// +kubebuilder:rbac:groups=networking.metal.ironcore.dev,resources=devices,verbs=get;list;watch
+// +kubebuilder:rbac:groups=networking.metal.ironcore.dev,resources=interfaces,verbs=get;list;watch
+// +kubebuilder:rbac:groups=core,resources=events,verbs=create;patch
+// +kubebuilder:rbac:groups=core,resources=secrets,verbs=get;list;watch
+
+// Reconcile is part of the main kubernetes reconciliation loop which aims to
+// move the current state of the cluster closer to the desired state.
+//
+// For more details, check Reconcile and its Result here:
+// - https://pkg.go.dev/sigs.k8s.io/controller-runtime@v0.21.0/pkg/reconcile
+func (r *MacSecReconciler) Reconcile(ctx context.Context, req ctrl.Request) (_ ctrl.Result, reterr error) {
+ log := ctrl.LoggerFrom(ctx)
+ log.Info("Reconciling resource")
+
+ obj := new(v1alpha1.MacSec)
+ if err := r.Get(ctx, req.NamespacedName, obj); err != nil {
+ if apierrors.IsNotFound(err) {
+ // If the custom resource is not found then it usually means that it was deleted or not created
+ // In this way, we will stop the reconciliation
+ log.Info("Resource not found. Ignoring since object must be deleted")
+ return ctrl.Result{}, nil
+ }
+ // Error reading the object - requeue the request.
+ log.Error(err, "Failed to get resource")
+ return ctrl.Result{}, err
+ }
+
+ prov, ok := r.Provider().(provider.MacSecProvider)
+ if !ok {
+ if meta.SetStatusCondition(&obj.Status.Conditions, metav1.Condition{
+ Type: v1alpha1.ReadyCondition,
+ Status: metav1.ConditionFalse,
+ Reason: v1alpha1.NotImplementedReason,
+ Message: "Provider does not implement provider.MacSecProvider",
+ }) {
+ return ctrl.Result{}, r.Status().Update(ctx, obj)
+ }
+ return ctrl.Result{}, nil
+ }
+
+ device, err := deviceutil.GetDeviceByName(ctx, r, obj.Namespace, obj.Spec.DeviceRef.Name)
+ if err != nil {
+ return ctrl.Result{}, err
+ }
+
+ if isPaused, requeue, err := paused.EnsureCondition(ctx, r.Client, device, obj); isPaused || requeue || err != nil {
+ return ctrl.Result{Requeue: requeue}, err
+ }
+
+ if err := r.Locker.AcquireLock(ctx, device.Name, "macsec-controller"); err != nil {
+ if errors.Is(err, resourcelock.ErrLockAlreadyHeld) {
+ log.Info("Device is already locked, requeuing reconciliation")
+ return ctrl.Result{RequeueAfter: Jitter(time.Second), Priority: new(LockWaitPriorityHigh)}, nil
+ }
+ log.Error(err, "Failed to acquire device lock")
+ return ctrl.Result{}, err
+ }
+ defer func() {
+ if err := r.Locker.ReleaseLock(ctx, device.Name, "macsec-controller"); err != nil {
+ log.Error(err, "Failed to release device lock")
+ reterr = kerrors.NewAggregate([]error{reterr, err})
+ }
+ }()
+
+ conn, err := deviceutil.GetDeviceConnection(ctx, r, device)
+ if err != nil {
+ return ctrl.Result{}, err
+ }
+
+ var cfg *provider.ProviderConfig
+ if obj.Spec.ProviderConfigRef != nil {
+ cfg, err = provider.GetProviderConfig(ctx, r, obj.Namespace, obj.Spec.ProviderConfigRef)
+ if err != nil {
+ return ctrl.Result{}, err
+ }
+ }
+
+ // Validate that the referenced interface exists
+ intf, err := GetInterfaceByName(ctx, r, obj.Namespace, obj.Spec.InterfaceRef.Name)
+ if err != nil {
+ return ctrl.Result{}, err
+ }
+
+ // Validate that all pre-shared key secrets exist
+ secrets, err := r.validatePreSharedKeySecrets(ctx, obj)
+ if err != nil {
+ log.Error(err, "Pre-shared key validation failed")
+ if meta.SetStatusCondition(&obj.Status.Conditions, metav1.Condition{
+ Type: v1alpha1.ReadyCondition,
+ Status: metav1.ConditionFalse,
+ Reason: v1alpha1.ErrorReason,
+ Message: fmt.Sprintf("Pre-shared key validation failed: %v", err),
+ }) {
+ return ctrl.Result{}, r.Status().Update(ctx, obj)
+ }
+ return ctrl.Result{}, nil
+ }
+
+ s := &macSecScope{
+ Device: device,
+ MacSec: obj,
+ Interface: intf,
+ Secrets: secrets,
+ Connection: conn,
+ ProviderConfig: cfg,
+ Provider: prov,
+ }
+
+ if !obj.DeletionTimestamp.IsZero() {
+ if controllerutil.ContainsFinalizer(obj, v1alpha1.FinalizerName) {
+ if err := r.finalize(ctx, s); err != nil {
+ log.Error(err, "Failed to finalize MacSec resource")
+ return ctrl.Result{}, err
+ }
+ controllerutil.RemoveFinalizer(obj, v1alpha1.FinalizerName)
+ if err := r.Update(ctx, obj); err != nil {
+ log.Error(err, "Failed to remove finalizer from MacSec resource")
+ return ctrl.Result{}, err
+ }
+ }
+ log.Info("Resource is being deleted, skipping reconciliation")
+ return ctrl.Result{}, nil
+ }
+
+ // More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/finalizers
+ if !controllerutil.ContainsFinalizer(obj, v1alpha1.FinalizerName) {
+ controllerutil.AddFinalizer(obj, v1alpha1.FinalizerName)
+ if err := r.Update(ctx, obj); err != nil {
+ log.Error(err, "Failed to add finalizer to resource")
+ return ctrl.Result{}, err
+ }
+ log.Info("Added finalizer to resource")
+ return ctrl.Result{}, nil
+ }
+
+ orig := obj.DeepCopy()
+ if conditions.InitializeConditions(obj, v1alpha1.ReadyCondition, v1alpha1.ConfiguredCondition, v1alpha1.OperationalCondition) {
+ log.Info("Initializing status conditions")
+ return ctrl.Result{}, r.Status().Update(ctx, obj)
+ }
+
+ // Always attempt to update the metadata/status after reconciliation
+ defer func() {
+ if !equality.Semantic.DeepEqual(orig.ObjectMeta, obj.ObjectMeta) {
+ // Pass obj.DeepCopy() to avoid Patch() modifying obj and interfering with status update below
+ if err := r.Patch(ctx, obj.DeepCopy(), client.MergeFrom(orig)); err != nil {
+ log.Error(err, "Failed to update resource metadata")
+ reterr = kerrors.NewAggregate([]error{reterr, err})
+ }
+ }
+ if !equality.Semantic.DeepEqual(orig.Status, obj.Status) {
+ if err := r.Status().Patch(ctx, obj, client.MergeFrom(orig)); err != nil {
+ log.Error(err, "Failed to update status")
+ reterr = kerrors.NewAggregate([]error{reterr, err})
+ }
+ }
+ }()
+
+ if err := r.reconcile(ctx, s); err != nil {
+ log.Error(err, "Failed to reconcile resource")
+ return ctrl.Result{}, err
+ }
+
+ return ctrl.Result{RequeueAfter: Jitter(r.RequeueInterval)}, nil
+}
+
+// SetupWithManager sets up the controller with the Manager.
+func (r *MacSecReconciler) SetupWithManager(ctx context.Context, mgr ctrl.Manager) error {
+ labelSelector := metav1.LabelSelector{}
+ if r.WatchFilterValue != "" {
+ labelSelector.MatchLabels = map[string]string{v1alpha1.WatchLabel: r.WatchFilterValue}
+ }
+
+ filter, err := predicate.LabelSelectorPredicate(labelSelector)
+ if err != nil {
+ return fmt.Errorf("failed to create label selector predicate: %w", err)
+ }
+
+ if err := mgr.GetFieldIndexer().IndexField(ctx, &v1alpha1.MacSec{}, v1alpha1.DeviceRefIndexKey, func(obj client.Object) []string {
+ m := obj.(*v1alpha1.MacSec)
+ return []string{m.Spec.DeviceRef.Name}
+ }); err != nil {
+ return err
+ }
+
+ return ctrl.NewControllerManagedBy(mgr).
+ For(&v1alpha1.MacSec{}).
+ Named("macsec").
+ WithEventFilter(filter).
+ // Watches enqueues MacSecs for referenced Secret resources.
+ Watches(
+ &corev1.Secret{},
+ handler.EnqueueRequestsFromMapFunc(r.secretToMacSec),
+ builder.WithPredicates(predicate.GenerationChangedPredicate{}),
+ ).
+ // Watches enqueues MacSecs for updates in referenced Device resources.
+ // Triggers on create, delete, and update events when the device's effective pause state changes.
+ Watches(
+ &v1alpha1.Device{},
+ handler.EnqueueRequestsFromMapFunc(r.deviceToMacSecs),
+ builder.WithPredicates(predicate.Funcs{
+ UpdateFunc: func(e event.UpdateEvent) bool {
+ return paused.DevicePausedChanged(e.ObjectOld, e.ObjectNew)
+ },
+ GenericFunc: func(e event.GenericEvent) bool {
+ return false
+ },
+ }),
+ ).
+ Complete(r)
+}
+
+// scope holds the different objects that are read and used during the reconcile.
+type macSecScope struct {
+ Device *v1alpha1.Device
+ MacSec *v1alpha1.MacSec
+ Connection *deviceutil.Connection
+ ProviderConfig *provider.ProviderConfig
+ Interface *v1alpha1.Interface
+ Secrets []corev1.Secret
+ Provider provider.MacSecProvider
+}
+
+func (r *MacSecReconciler) reconcile(ctx context.Context, s *macSecScope) (reterr error) {
+ if s.MacSec.Labels == nil {
+ s.MacSec.Labels = make(map[string]string)
+ }
+
+ s.MacSec.Labels[v1alpha1.DeviceLabel] = s.Device.Name
+
+ // Ensure the MacSec is owned by the Device.
+ if !controllerutil.HasControllerReference(s.MacSec) {
+ if err := controllerutil.SetOwnerReference(s.Device, s.MacSec, r.Scheme, controllerutil.WithBlockOwnerDeletion(true)); err != nil {
+ return err
+ }
+ }
+
+ defer func() {
+ conditions.RecomputeReady(s.MacSec)
+ }()
+
+ if err := s.Provider.Connect(ctx, s.Connection); err != nil {
+ return fmt.Errorf("failed to connect to provider: %w", err)
+ }
+ defer func() {
+ if err := s.Provider.Disconnect(ctx, s.Connection); err != nil {
+ reterr = kerrors.NewAggregate([]error{reterr, err})
+ }
+ }()
+ // Ensure the MacSec is realized on the provider.
+ err := s.Provider.EnsureMacSec(ctx, &provider.EnsureMacSecRequest{
+ MacSec: s.MacSec,
+ Secrets: s.Secrets,
+ })
+
+ cond := conditions.FromError(err)
+ conditions.Set(s.MacSec, cond)
+
+ return nil
+}
+
+func (r *MacSecReconciler) finalize(ctx context.Context, s *macSecScope) (reterr error) {
+ if err := s.Provider.Connect(ctx, s.Connection); err != nil {
+ return fmt.Errorf("failed to connect to provider: %w", err)
+ }
+ defer func() {
+ if err := s.Provider.Disconnect(ctx, s.Connection); err != nil {
+ reterr = kerrors.NewAggregate([]error{reterr, err})
+ }
+ }()
+
+ return s.Provider.DeleteMacSec(ctx, &provider.DeleteMacSecRequest{
+ MacSec: s.MacSec,
+ })
+}
+
+// validatePreSharedKeySecrets validates that all pre-shared key secrets referenced in the MacSec spec exist
+func (r *MacSecReconciler) validatePreSharedKeySecrets(ctx context.Context, macSec *v1alpha1.MacSec) ([]corev1.Secret, error) {
+ secrets := []corev1.Secret{}
+ for _, psk := range macSec.Spec.PreSharedKeyRef {
+ secret := new(corev1.Secret)
+ if err := r.Get(ctx, client.ObjectKey{
+ Namespace: macSec.Namespace,
+ Name: psk.Name,
+ }, secret); err != nil {
+ return nil, fmt.Errorf("pre-shared key secret not found: %s", psk.Name)
+ }
+ secrets = append(secrets, *secret)
+ for _, key := range []string{"lifetime", "connectivityKeyName", "algorithm"} {
+ _, ok := secret.Data[key]
+ if !ok {
+ return nil, fmt.Errorf("pre-shared key secret %s does not contain a '%s' field", psk.Name, key)
+ }
+ }
+ }
+ return secrets, nil
+}
+
+func GetInterfaceByName(ctx context.Context, r client.Reader, namespace, name string) (*v1alpha1.Interface, error) {
+ obj := new(v1alpha1.Interface)
+ if err := r.Get(ctx, client.ObjectKey{Namespace: namespace, Name: name}, obj); err != nil {
+ return nil, fmt.Errorf("failed to get %s/%s: %w", v1alpha1.GroupVersion.WithKind("Interface").String(), name, err)
+ }
+ return obj, nil
+}
+
+// secretToMacSec is a [handler.MapFunc] to be used to enqueue requests for reconciliation
+// for a MacSec to update when one of its referenced Secrets gets updated.
+func (r *MacSecReconciler) secretToMacSec(ctx context.Context, obj client.Object) []ctrl.Request {
+ secret, ok := obj.(*corev1.Secret)
+ if !ok {
+ panic(fmt.Sprintf("Expected a Secret but got a %T", obj))
+ }
+
+ log := ctrl.LoggerFrom(ctx)
+
+ macsecs := new(v1alpha1.MacSecList)
+ if err := r.List(ctx, macsecs); err != nil {
+ log.Error(err, "Failed to list MacSecs")
+ return nil
+ }
+
+ requests := []ctrl.Request{}
+ for _, macsec := range macsecs.Items {
+ // Check if this secret is referenced by any of the pre-shared key references
+ for _, psk := range macsec.Spec.PreSharedKeyRef {
+ if psk.Name == secret.Name && macsec.Namespace == secret.Namespace {
+ log.Info("Enqueuing MacSec for reconciliation")
+ requests = append(requests, ctrl.Request{
+ NamespacedName: client.ObjectKey{
+ Name: macsec.Name,
+ Namespace: macsec.Namespace,
+ },
+ })
+ }
+ }
+ }
+
+ return requests
+}
+
+// deviceToMacSecs is a [handler.MapFunc] to be used to enqueue requests for reconciliation
+// for MacSecs when their referenced Device's effective pause state changes.
+func (r *MacSecReconciler) deviceToMacSecs(ctx context.Context, obj client.Object) []ctrl.Request {
+ device, ok := obj.(*v1alpha1.Device)
+ if !ok {
+ panic(fmt.Sprintf("Expected a Device but got a %T", obj))
+ }
+
+ log := ctrl.LoggerFrom(ctx)
+
+ macsecs := new(v1alpha1.MacSecList)
+ if err := r.List(
+ ctx, macsecs,
+ client.InNamespace(device.Namespace),
+ client.MatchingFields{v1alpha1.DeviceRefIndexKey: device.Name},
+ ); err != nil {
+ log.Error(err, "Failed to list MacSecs")
+ return nil
+ }
+
+ requests := make([]ctrl.Request, 0, len(macsecs.Items))
+ for _, m := range macsecs.Items {
+ log.V(2).Info("Enqueuing MacSec for reconciliation", "MacSec", m.Name)
+ requests = append(requests, ctrl.Request{
+ NamespacedName: client.ObjectKey{
+ Name: m.Name,
+ Namespace: m.Namespace,
+ },
+ })
+ }
+
+ return requests
+}
diff --git a/internal/controller/core/macsec_controller_test.go b/internal/controller/core/macsec_controller_test.go
new file mode 100644
index 000000000..a9ff88511
--- /dev/null
+++ b/internal/controller/core/macsec_controller_test.go
@@ -0,0 +1,218 @@
+// SPDX-FileCopyrightText: 2025 SAP SE or an SAP affiliate company and IronCore contributors
+// SPDX-License-Identifier: Apache-2.0
+
+package core
+
+import (
+ "context"
+ "time"
+
+ . "github.com/onsi/ginkgo/v2"
+ . "github.com/onsi/gomega"
+ corev1 "k8s.io/api/core/v1"
+ "k8s.io/apimachinery/pkg/api/errors"
+ "k8s.io/apimachinery/pkg/types"
+ "sigs.k8s.io/controller-runtime/pkg/client"
+ "sigs.k8s.io/controller-runtime/pkg/reconcile"
+
+ "github.com/ironcore-dev/network-operator/api/core/v1alpha1"
+
+ metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+
+ "github.com/ironcore-dev/network-operator/internal/provider"
+ "github.com/ironcore-dev/network-operator/internal/resourcelock"
+)
+
+var _ = Describe("MacSec Controller", func() {
+ Context("When reconciling a resource", func() {
+ const resourceName = "macsec-test"
+ const deviceName = "macsec-device"
+ const intName = "macsec-interface"
+
+ ctx = context.Background()
+
+ typeNamespacedName := types.NamespacedName{
+ Name: resourceName,
+ Namespace: metav1.NamespaceDefault,
+ }
+
+ macSecSecKey := types.NamespacedName{
+ Name: resourceName,
+ Namespace: metav1.NamespaceDefault,
+ }
+ secretKey := client.ObjectKey{Name: deviceName, Namespace: metav1.NamespaceDefault}
+ deviceKey := client.ObjectKey{Name: deviceName, Namespace: metav1.NamespaceDefault}
+ intKey := client.ObjectKey{Name: intName, Namespace: metav1.NamespaceDefault}
+
+ BeforeEach(func() {
+ By("Creating the endpoint credentials as a Secret")
+ deviceCreds := &corev1.Secret{}
+ if err := k8sClient.Get(ctx, secretKey, deviceCreds); errors.IsNotFound(err) {
+ resource := &corev1.Secret{
+ ObjectMeta: metav1.ObjectMeta{
+ Name: deviceName,
+ Namespace: metav1.NamespaceDefault,
+ },
+ Data: map[string][]byte{
+ corev1.BasicAuthUsernameKey: []byte("user"),
+ corev1.BasicAuthPasswordKey: []byte("password"),
+ },
+ Type: corev1.SecretTypeBasicAuth,
+ }
+ Expect(k8sClient.Create(ctx, resource)).To(Succeed())
+ }
+
+ By("Creating the device that will be referenced by the MacSec resource")
+ device := &v1alpha1.Device{
+ ObjectMeta: metav1.ObjectMeta{
+ Name: deviceName,
+ Namespace: metav1.NamespaceDefault,
+ },
+ Spec: v1alpha1.DeviceSpec{
+ Endpoint: v1alpha1.Endpoint{
+ Address: "192.168.10.2:9339",
+ SecretRef: &v1alpha1.SecretReference{
+ Name: deviceName,
+ },
+ },
+ },
+ }
+ Expect(k8sClient.Create(ctx, device)).To(Succeed())
+
+ By("Creating an Interface resource that will be referenced by the MacSec resource")
+ intf := &v1alpha1.Interface{
+ ObjectMeta: metav1.ObjectMeta{
+ Name: intName,
+ Namespace: metav1.NamespaceDefault,
+ },
+ Spec: v1alpha1.InterfaceSpec{
+ DeviceRef: v1alpha1.LocalObjectReference{Name: deviceName},
+ Name: intName,
+ AdminState: v1alpha1.AdminStateUp,
+ Description: "Test",
+ MTU: 9000,
+ Type: v1alpha1.InterfaceTypePhysical,
+ },
+ }
+ Expect(k8sClient.Create(ctx, intf)).To(Succeed())
+
+ By("Creating the secret that will be referenced by the MacSec resource")
+ macsecSecret := &corev1.Secret{}
+ if err := k8sClient.Get(ctx, macSecSecKey, macsecSecret); err != nil && errors.IsNotFound(err) {
+ resource := &corev1.Secret{
+ ObjectMeta: metav1.ObjectMeta{
+ Name: resourceName,
+ Namespace: metav1.NamespaceDefault,
+ },
+ StringData: map[string]string{
+ "lifetime": "3600",
+ "keyId": "someID",
+ "algorithm": "aes-256",
+ },
+ Type: corev1.SecretTypeOpaque,
+ }
+ Expect(k8sClient.Create(ctx, resource)).To(Succeed())
+ }
+ })
+
+ AfterEach(func() {
+ resource := &v1alpha1.MacSec{}
+ err := k8sClient.Get(ctx, typeNamespacedName, resource)
+ Expect(err).NotTo(HaveOccurred())
+
+ By("Cleanup the specific resource instance MacSec")
+ Expect(k8sClient.Delete(ctx, resource)).To(Succeed())
+
+ By("Cleanup the specific resource instance MacSec")
+
+ By("Cleanup the specific resource instance Interface")
+ intf := &v1alpha1.Interface{}
+ err = k8sClient.Get(ctx, intKey, intf)
+ Expect(err).NotTo(HaveOccurred())
+ Expect(k8sClient.Delete(ctx, intf)).To(Succeed())
+
+ By("Cleanup the specific resource instance Device and Secret")
+ device := &v1alpha1.Device{}
+ err = k8sClient.Get(ctx, deviceKey, device)
+ Expect(err).NotTo(HaveOccurred())
+ Expect(k8sClient.Delete(ctx, device)).To(Succeed())
+
+ secret := &corev1.Secret{}
+ err = k8sClient.Get(ctx, deviceKey, secret)
+ Expect(err).NotTo(HaveOccurred())
+ Expect(k8sClient.Delete(ctx, secret)).To(Succeed())
+ })
+
+ It("should successfully reconcile the resource", func() {
+ By("Creating the custom resource for the Kind MacSec")
+ macSec := &v1alpha1.MacSec{}
+ err := k8sClient.Get(ctx, typeNamespacedName, macSec)
+ if err != nil && errors.IsNotFound(err) {
+ macSec = &v1alpha1.MacSec{
+ ObjectMeta: metav1.ObjectMeta{
+ Name: resourceName,
+ Namespace: "default",
+ },
+ Spec: v1alpha1.MacSecSpec{
+ DeviceRef: v1alpha1.LocalObjectReference{Name: deviceName},
+ InterfaceRef: v1alpha1.LocalObjectReference{Name: intName},
+ Name: "test-macsec",
+ Description: "Test MacSec resource",
+ PreSharedKeyRef: []v1alpha1.LocalObjectReference{
+ {
+ Name: "macsec-test",
+ },
+ },
+ Policy: &v1alpha1.MacSecPolicy{
+ CipherSuite: "GCM-AES-256",
+ },
+ },
+ }
+ }
+ Expect(k8sClient.Create(ctx, macSec)).To(Succeed())
+ By("Reconciling Kind MacSec")
+ locker, err := resourcelock.NewResourceLocker(
+ k8sManager.GetClient(), metav1.NamespaceDefault,
+ 15*time.Second,
+ 10*time.Second)
+ Expect(err).NotTo(HaveOccurred())
+
+ controllerReconciler := &MacSecReconciler{
+ Client: k8sClient,
+ Scheme: k8sClient.Scheme(),
+ Provider: func() provider.Provider { return testProvider },
+ Locker: locker,
+ }
+
+ // Add Finalizer
+ //nolint:errcheck
+ controllerReconciler.Reconcile(ctx, reconcile.Request{
+ NamespacedName: typeNamespacedName,
+ })
+
+ // Add status condition
+ //nolint:errcheck
+ controllerReconciler.Reconcile(ctx, reconcile.Request{
+ NamespacedName: typeNamespacedName,
+ })
+
+ // reconcile & update metadata
+ //nolint:errcheck
+ controllerReconciler.Reconcile(ctx, reconcile.Request{
+ NamespacedName: typeNamespacedName,
+ })
+
+ // update status
+ //nolint:errcheck
+ controllerReconciler.Reconcile(ctx, reconcile.Request{
+ NamespacedName: typeNamespacedName,
+ })
+
+ // update macsec to compare status
+ Expect(k8sClient.Get(ctx, typeNamespacedName, macSec)).To(Succeed())
+ Expect(macSec.Status.Conditions).To(HaveLen(1))
+ Expect(macSec.Status.Conditions[0].Type).To(Equal(v1alpha1.ReadyCondition))
+ Expect(macSec.Status.Conditions[0].Status).To(Equal(metav1.ConditionTrue))
+ })
+ })
+})
diff --git a/internal/controller/core/suite_test.go b/internal/controller/core/suite_test.go
index 0244c354c..acb195ad1 100644
--- a/internal/controller/core/suite_test.go
+++ b/internal/controller/core/suite_test.go
@@ -409,6 +409,7 @@ var (
_ provider.NVEProvider = (*Provider)(nil)
_ provider.LLDPProvider = (*Provider)(nil)
_ provider.DHCPRelayProvider = (*Provider)(nil)
+ _ provider.MacSecProvider = (*Provider)(nil)
)
// Provider is a simple in-memory provider for testing purposes only.
@@ -445,6 +446,7 @@ type Provider struct {
LLDPOperStatus bool
LLDPNeighbors map[string]*provider.LLDPAdjacency
DHCPRelay *v1alpha1.DHCPRelay
+ MacSec sets.Set[string]
}
func NewProvider() *Provider {
@@ -464,6 +466,7 @@ func NewProvider() *Provider {
RoutingPolicies: sets.New[string](),
LLDPOperStatus: true,
LLDPNeighbors: make(map[string]*provider.LLDPAdjacency),
+ MacSec: sets.New[string](),
}
}
@@ -964,3 +967,25 @@ func (p *Provider) SetLLDPNeighbor(interfaceName, sysName, chassisID, portID str
TTL: time.Duration(ttl) * time.Second,
}
}
+
+func (p *Provider) EnsureMacSec(_ context.Context, req *provider.EnsureMacSecRequest) error {
+ p.Lock()
+ defer p.Unlock()
+ p.MacSec.Insert(req.MacSec.Spec.Name)
+ return nil
+}
+
+func (p *Provider) DeleteMacSec(_ context.Context, req *provider.DeleteMacSecRequest) error {
+ p.Lock()
+ defer p.Unlock()
+ p.MacSec.Delete(req.MacSec.Spec.Name)
+ return nil
+}
+
+func (p *Provider) GetMacSecStatus(_ context.Context, req *provider.EnsureMacSecRequest) (provider.MacSecStatus, error) {
+ p.Lock()
+ defer p.Unlock()
+ return provider.MacSecStatus{
+ OverallStatus: true,
+ }, nil
+}
diff --git a/internal/provider/cisco/iosxr/macsec.go b/internal/provider/cisco/iosxr/macsec.go
new file mode 100644
index 000000000..be71670ce
--- /dev/null
+++ b/internal/provider/cisco/iosxr/macsec.go
@@ -0,0 +1,198 @@
+// SPDX-FileCopyrightText: 2026 SAP SE or an SAP affiliate company and IronCore contributors
+// SPDX-License-Identifier: Apache-2.0
+
+package iosxr
+
+import (
+ "fmt"
+ "strings"
+ "time"
+
+ "github.com/ironcore-dev/network-operator/internal/transport/gnmiext"
+)
+
+var (
+ _ gnmiext.DataElement = (*MacSecPolicy)(nil)
+ _ gnmiext.DataElement = (*KeyChain)(nil)
+ _ gnmiext.DataElement = (*KeyChainOperData)(nil)
+)
+
+const ConfOffsetPrefix = "CONF-OFFSET-%d"
+
+type CipherSuite string
+
+const (
+ CipherSuiteGcmAes256 CipherSuite = "GCM-AES-256"
+ CipherSuiteGcmAes128 CipherSuite = "GCM-AES-128"
+ CipherSuiteGcmAesXpn256 CipherSuite = "GCM-AES-XPN-256"
+ CipherSuiteGcmAesXpn128 CipherSuite = "GCM-AES-XPN-128"
+)
+
+type CryptographicAlgorithm string
+
+const (
+ CryptographicAlgorithmMd5 CryptographicAlgorithm = "md5"
+ CryptographicAlgorithmSha1 CryptographicAlgorithm = "sha-1"
+ CryptographicAlgorithmHmacMd5 CryptographicAlgorithm = "hmac-md5"
+ CryptographicAlgorithmHmacSha1_20 CryptographicAlgorithm = "hmac-sha1-20"
+ CryptographicAlgorithmHmacSha1_12 CryptographicAlgorithm = "hmac-sha1-12"
+ CryptographicAlgorithmHmacSha1_96 CryptographicAlgorithm = "hmac-sha1-96"
+ CryptographicAlgorithmHmacSha256 CryptographicAlgorithm = "hmac-sha-256"
+ CryptographicAlgorithmAes128Cmac96 CryptographicAlgorithm = "aes-128-cmac-96"
+)
+
+type MacSecPolicy struct {
+ Name string `json:"-"`
+ CipherSuite CipherSuite `json:"cipher-suite,omitzero"`
+ ConfOffset string `json:"conf-offset,omitzero"`
+ KeyServerPriority uint8 `json:"key-server-priority,omitzero"`
+ RelayProtection uint16 `json:"window-size,omitzero"`
+}
+
+func (m *MacSecPolicy) XPath() string {
+ return "Cisco-IOS-XR-um-macsec-cfg:macsec-policy/policy-names/policy-name[policy-name=" + m.Name + "]"
+}
+
+func ExtractCipherSuite(cipherSuite string) (CipherSuite, error) {
+ switch cipherSuite {
+ case "GCM-AES-256":
+ return CipherSuiteGcmAes256, nil
+ case "GCM-AES-128":
+ return CipherSuiteGcmAes128, nil
+ case "GCM-AES-XPN-256":
+ return CipherSuiteGcmAesXpn256, nil
+ case "GCM-AES-XPN-128":
+ return CipherSuiteGcmAesXpn128, nil
+ default:
+ return "", fmt.Errorf("unsupported cipher suite: %s", cipherSuite)
+ }
+}
+
+func ExtractCryptographicAlgorithm(cipherSuite string) (string, error) {
+ switch cipherSuite {
+ case "md5":
+ return string(CryptographicAlgorithmMd5), nil
+ case "sha-1":
+ return string(CryptographicAlgorithmSha1), nil
+ case "hmac-md5":
+ return string(CryptographicAlgorithmHmacMd5), nil
+ case "hmac-sha1-20":
+ return string(CryptographicAlgorithmHmacSha1_20), nil
+ case "hmac-sha1-12":
+ return string(CryptographicAlgorithmHmacSha1_12), nil
+ case "hmac-sha1-96":
+ return string(CryptographicAlgorithmHmacSha1_96), nil
+ case "hmac-sha-256":
+ return string(CryptographicAlgorithmHmacSha256), nil
+ case "aes-128-cmac-96":
+ return string(CryptographicAlgorithmAes128Cmac96), nil
+ default:
+ return "", fmt.Errorf("unsupported cryptographic algorithm: %s", cipherSuite)
+ }
+}
+
+type KeyChain struct {
+ Name string `json:"-"`
+ Keys Keys `json:"keys,omitzero"`
+}
+
+type Keys struct {
+ Key []Key `json:"key,omitzero"`
+}
+
+type Key struct {
+ ID string `json:"key-name,omitzero"`
+ CryptographicAlgorithm string `json:"cryptographic-algorithm,omitzero"`
+ PreSharedKey PreSharedKey `json:"key-string,omitzero"`
+ StartLifetime Lifetime `json:"send-lifetime,omitzero"`
+ AcceptLifetime Lifetime `json:"accept-lifetime,omitzero"`
+}
+
+type PreSharedKey struct {
+ Secret string `json:"password,omitzero"`
+}
+
+type Lifetime struct {
+ Duration uint32 `json:"duration,omitzero"`
+ StartTime CiscoTime `json:"start-time,omitzero"`
+}
+
+type CiscoTime struct {
+ DayOfMonth int `json:"day-of-month,omitzero"`
+ Hour int `json:"hour,omitzero"`
+ Minute int `json:"minute,omitzero"`
+ Month string `json:"month,omitzero"`
+ Second int `json:"second,omitzero"`
+ Year int `json:"year,omitzero"`
+}
+
+func (k *KeyChain) XPath() string {
+ return "Cisco-IOS-XR-um-key-chain-cfg:key/chains/chain[key-chain-name=" + k.Name + "]"
+}
+
+func NewLifetime(endTime string) (Lifetime, error) {
+ start := time.Now()
+
+ end, err := time.Parse(time.RFC3339, endTime)
+ if err != nil {
+ return Lifetime{}, err
+ }
+ duration := end.Sub(start)
+
+ t := new(CiscoTime)
+ t.DayOfMonth = end.Day()
+ t.Hour = end.Hour()
+ t.Minute = end.Minute()
+ t.Month = strings.ToLower(end.Month().String())
+ t.Second = end.Second()
+ t.Year = end.Year()
+
+ return Lifetime{
+ Duration: uint32(duration.Seconds()),
+ StartTime: *t,
+ }, nil
+}
+
+func (k *KeyChainOperData) XPath() string {
+ return "Cisco-IOS-XR-lib-keychain-oper:keychain/keys/key[key-name=" + k.Name + "]"
+}
+
+// KeyChain operational data structures for reading state information
+type KeyChainOperData struct {
+ Name string `json:"-"`
+ Key KeyOper `json:"key"`
+}
+
+type KeyOper struct {
+ Keys []KeyStatus `json:"key-id"`
+}
+
+type KeyStatus struct {
+ ID string `json:"key-id"`
+ AcceptLifetime StatusLifetime `json:"accept-lifetime"`
+ CryptographicAlgorithm string `json:"cryptographic-algorithm"`
+ SendLifetime StatusLifetime `json:"send-lifetime"`
+ Type string `json:"type"`
+}
+
+type StatusLifetime struct {
+ Duration string `json:"duration"`
+ IsAlwaysValid bool `json:"is-always-valid"`
+ IsValidNow bool `json:"is-valid-now"`
+ Start string `json:"start"`
+}
+
+type KeyIDOperational struct {
+ AcceptLifetime LifetimeOperational `json:"accept-lifetime"`
+ CryptographicAlgorithm string `json:"cryptographic-algorithm"`
+ KeyID string `json:"key-id"`
+ SendLifetime LifetimeOperational `json:"send-lifetime"`
+ Type string `json:"type"`
+}
+
+type LifetimeOperational struct {
+ Duration string `json:"duration"`
+ IsAlwaysValid bool `json:"is-always-valid"`
+ IsValidNow bool `json:"is-valid-now"`
+ Start string `json:"start"`
+}
diff --git a/internal/provider/cisco/iosxr/provider.go b/internal/provider/cisco/iosxr/provider.go
index 23258ca67..e36578af1 100644
--- a/internal/provider/cisco/iosxr/provider.go
+++ b/internal/provider/cisco/iosxr/provider.go
@@ -24,6 +24,7 @@ var (
_ provider.DeviceProvider = &Provider{}
_ provider.InterfaceProvider = &Provider{}
_ provider.VRFProvider = &Provider{}
+ _ provider.MacSecProvider = &Provider{}
)
type Provider struct {
@@ -410,6 +411,66 @@ func (p *Provider) DeleteVRF(ctx context.Context, req *provider.VRFRequest) erro
return p.client.Delete(ctx, vrf)
}
+func (p *Provider) EnsureMacSec(ctx context.Context, req *provider.EnsureMacSecRequest) error {
+ //Configure MacSec Policy
+
+ cipherSuite, err := ExtractCipherSuite(req.MacSec.Spec.Policy.CipherSuite)
+ if err != nil {
+ return err
+ }
+
+ policy := &MacSecPolicy{
+ Name: req.MacSec.Spec.Name,
+ CipherSuite: cipherSuite,
+ ConfOffset: fmt.Sprintf(ConfOffsetPrefix, req.MacSec.Spec.Policy.ConfidentialityOffset),
+ KeyServerPriority: req.MacSec.Spec.Policy.KeyServerPriority,
+ RelayProtection: req.MacSec.Spec.Policy.RelayProtection,
+ }
+
+ //Configure KeyChain
+
+ chain := new(KeyChain)
+ chain.Name = req.MacSec.Spec.Name
+
+ for _, psk := range req.Secrets {
+ lifeTime, err := NewLifetime(string(psk.Data["lifetime"]))
+ if err != nil {
+ return err
+ }
+
+ key := Key{
+ ID: string(psk.Data["connectivityKeyName"]),
+ CryptographicAlgorithm: string(psk.Data["algorithm"]),
+ StartLifetime: lifeTime,
+ AcceptLifetime: lifeTime,
+ }
+ chain.Keys.Key = append(chain.Keys.Key, key)
+ }
+
+ return p.client.Update(ctx, policy, chain)
+}
+
+func (p *Provider) DeleteMacSec(ctx context.Context, req *provider.DeleteMacSecRequest) error {
+ policy := new(MacSecPolicy)
+ policy.Name = req.MacSec.Spec.Name
+
+ keyChain := new(KeyChain)
+ keyChain.Name = req.MacSec.Spec.Name
+
+ return p.client.Delete(ctx, policy, keyChain)
+}
+
+func (p *Provider) GetMacSecStatus(ctx context.Context, req *provider.EnsureMacSecRequest) (provider.MacSecStatus, error) {
+ status := new(KeyChainOperData)
+ status.Name = req.MacSec.Spec.Name
+
+ err := p.client.GetState(ctx, status)
+ if err != nil {
+ return provider.MacSecStatus{}, fmt.Errorf("failed to get MacSec status for %s: %w", req.MacSec.Spec.Name, err)
+ }
+ return provider.MacSecStatus{}, nil
+}
+
func init() {
provider.Register("cisco-iosxr-gnmi", NewProvider)
}
diff --git a/internal/provider/cisco/iosxr/provider_test.go b/internal/provider/cisco/iosxr/provider_test.go
index 49083fb8f..292f03787 100644
--- a/internal/provider/cisco/iosxr/provider_test.go
+++ b/internal/provider/cisco/iosxr/provider_test.go
@@ -116,6 +116,10 @@ func (m *MockClient) GetConfig(ctx context.Context, configs ...gnmiext.DataEleme
return nil
}
+func (m *MockClient) Create(ctx context.Context, conf ...gnmiext.DataElement) error {
+ return nil
+}
+
func (m *MockClient) GetState(ctx context.Context, states ...gnmiext.DataElement) error {
if m.GetStateFunc != nil {
return m.GetStateFunc(ctx, states...)
diff --git a/internal/provider/cisco/nxos/provider.go b/internal/provider/cisco/nxos/provider.go
index 09302ca23..68167bbff 100644
--- a/internal/provider/cisco/nxos/provider.go
+++ b/internal/provider/cisco/nxos/provider.go
@@ -48,6 +48,7 @@ var (
_ provider.EVPNInstanceProvider = (*Provider)(nil)
_ provider.InterfaceProvider = (*Provider)(nil)
_ provider.ISISProvider = (*Provider)(nil)
+ _ provider.MacSecProvider = (*Provider)(nil)
_ provider.ManagementAccessProvider = (*Provider)(nil)
_ provider.NTPProvider = (*Provider)(nil)
_ provider.OSPFProvider = (*Provider)(nil)
@@ -3270,6 +3271,27 @@ func (p *Provider) GetDHCPRelayStatus(ctx context.Context, req *provider.DHCPRel
return s, nil
}
+// EnsureMacSec is a dummy implementation for MacSec provisioning.
+// This method currently returns an error indicating that MacSec is not yet supported.
+func (p *Provider) EnsureMacSec(ctx context.Context, req *provider.EnsureMacSecRequest) error {
+ // TODO(sven-rosenzweig): Implement MacSec
+ return nil
+}
+
+// DeleteMacSec is a dummy implementation for MacSec deletion.
+// This method currently returns an error indicating that MacSec is not yet supported.
+func (p *Provider) DeleteMacSec(ctx context.Context, req *provider.DeleteMacSecRequest) error {
+ // TODO(sven-rosenzweig) : Implement MacSec deletion
+ return nil
+}
+
+// DeleteMacSec is a dummy implementation for MacSec status retrieval.
+// This method currently returns an error indicating that MacSec is not yet supported.
+func (p *Provider) GetMacSecStatus(ctx context.Context, req *provider.EnsureMacSecRequest) (provider.MacSecStatus, error) {
+ // TODO(sven-rosenzweig): Implement MacSec
+ return provider.MacSecStatus{}, nil
+}
+
func init() {
provider.Register("cisco-nxos-gnmi", NewProvider)
}
diff --git a/internal/provider/provider.go b/internal/provider/provider.go
index a9d7f9883..e7ca21298 100644
--- a/internal/provider/provider.go
+++ b/internal/provider/provider.go
@@ -1,5 +1,6 @@
// SPDX-FileCopyrightText: 2025 SAP SE or an SAP affiliate company and IronCore contributors
// SPDX-License-Identifier: Apache-2.0
+
package provider
import (
@@ -12,6 +13,7 @@ import (
"sync"
"time"
+ corev1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/runtime"
"sigs.k8s.io/controller-runtime/pkg/client"
@@ -614,6 +616,41 @@ type DeleteRoutingPolicyRequest struct {
Name string
}
+// MacSecProvider is the interface for the realization of the MacSec objects over different providers.
+type MacSecProvider interface {
+ Provider
+
+ // EnsureMacSec call is responsible for MacSec realization on the provider.
+ EnsureMacSec(context.Context, *EnsureMacSecRequest) error
+ // DeleteMacSec call is responsible for MacSec deletion on the provider.
+ DeleteMacSec(context.Context, *DeleteMacSecRequest) error
+ // GetMacSecStatus call is responsible for retrieving the current status of the MacSec from the provider.
+ GetMacSecStatus(context.Context, *EnsureMacSecRequest) (MacSecStatus, error)
+}
+
+type EnsureMacSecRequest struct {
+ MacSec *v1alpha1.MacSec
+ Secrets []corev1.Secret
+ // SourceInterface is the interface on which MacSec is configured.
+ SourceInterface *v1alpha1.Interface
+}
+
+type DeleteMacSecRequest struct {
+ MacSec *v1alpha1.MacSec
+}
+
+type MacSecStatus struct {
+ // OverallStatus indicates whether all keys are valid
+ OverallStatus bool
+ // KeyStatuses provides the validity status for each individual key configured on the device.
+ KeyStatus []KeyValidity
+}
+
+type KeyValidity struct {
+ KeyName string
+ Valid bool
+}
+
type NVEProvider interface {
Provider