From 21cf64a24205a0c1bf220c43dd15d5692ccc3811 Mon Sep 17 00:00:00 2001 From: i521907 Date: Wed, 4 Feb 2026 11:42:12 -0500 Subject: [PATCH 01/32] Adding AAA TACACS Support Signed-off-by: i521907 --- .../nx/v1alpha1/zz_generated.deepcopy.go | 3 - api/core/v1alpha1/aaa_types.go | 280 ++++++++++ api/core/v1alpha1/zz_generated.deepcopy.go | 313 +++++++++++- cmd/main.go | 12 + .../networking.metal.ironcore.dev_aaas.yaml | 479 ++++++++++++++++++ config/rbac/role.yaml | 3 + config/samples/networking_v1alpha1_aaa.yaml | 103 ++++ internal/controller/core/aaa_controller.go | 332 ++++++++++++ internal/provider/cisco/nxos/aaa.go | 186 +++++++ internal/provider/cisco/nxos/provider.go | 192 +++++++ internal/provider/provider.go | 23 + 11 files changed, 1920 insertions(+), 6 deletions(-) create mode 100644 api/core/v1alpha1/aaa_types.go create mode 100644 config/crd/bases/networking.metal.ironcore.dev_aaas.yaml create mode 100644 config/samples/networking_v1alpha1_aaa.yaml create mode 100644 internal/controller/core/aaa_controller.go create mode 100644 internal/provider/cisco/nxos/aaa.go diff --git a/api/cisco/nx/v1alpha1/zz_generated.deepcopy.go b/api/cisco/nx/v1alpha1/zz_generated.deepcopy.go index da6230a60..788d481e4 100644 --- a/api/cisco/nx/v1alpha1/zz_generated.deepcopy.go +++ b/api/cisco/nx/v1alpha1/zz_generated.deepcopy.go @@ -1,8 +1,5 @@ //go:build !ignore_autogenerated -// SPDX-FileCopyrightText: 2025 SAP SE or an SAP affiliate company and IronCore contributors -// SPDX-License-Identifier: Apache-2.0 - // Code generated by controller-gen. DO NOT EDIT. package v1alpha1 diff --git a/api/core/v1alpha1/aaa_types.go b/api/core/v1alpha1/aaa_types.go new file mode 100644 index 000000000..6a46e4447 --- /dev/null +++ b/api/core/v1alpha1/aaa_types.go @@ -0,0 +1,280 @@ +// SPDX-FileCopyrightText: 2025 SAP SE or an SAP affiliate company and IronCore contributors +// SPDX-License-Identifier: Apache-2.0 + +package v1alpha1 + +import ( + "sync" + + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime/schema" +) + +// AAASpec defines the desired state of AAA +// +// It models the Authentication, Authorization, and Accounting (AAA) configuration on a network device, +// including TACACS+ server configuration and AAA group/method settings. +type AAASpec 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 of this AAA. + // This reference is used to link the AAA to its provider-specific configuration. + // +optional + ProviderConfigRef *TypedLocalObjectReference `json:"providerConfigRef,omitempty"` + + // TACACSServers is the list of TACACS+ servers to configure. + // +optional + // +listType=map + // +listMapKey=address + // +kubebuilder:validation:MaxItems=16 + TACACSServers []TACACSServer `json:"tacacsServers,omitempty"` + + // TACACSGroup is the TACACS+ server group configuration. + // +optional + TACACSGroup *TACACSGroup `json:"tacacsGroup,omitempty"` + + // Authentication defines the AAA authentication configuration. + // +optional + Authentication *AAAAuthentication `json:"authentication,omitempty"` + + // Authorization defines the AAA authorization configuration. + // +optional + Authorization *AAAAuthorization `json:"authorization,omitempty"` + + // Accounting defines the AAA accounting configuration. + // +optional + Accounting *AAAAccounting `json:"accounting,omitempty"` +} + +// TACACSServer represents a TACACS+ server configuration. +type TACACSServer struct { + // Address is the IP address or hostname of the TACACS+ server. + // +required + // +kubebuilder:validation:MinLength=1 + // +kubebuilder:validation:MaxLength=253 + Address string `json:"address"` + + // Port is the TCP port of the TACACS+ server. + // Defaults to 49 if not specified. + // +optional + // +kubebuilder:validation:Minimum=1 + // +kubebuilder:validation:Maximum=65535 + // +kubebuilder:default=49 + Port int32 `json:"port,omitempty"` + + // KeySecretRef is a reference to a secret containing the shared key for this TACACS+ server. + // The secret must contain a key specified in the SecretKeySelector. + // +required + KeySecretRef SecretKeySelector `json:"keySecretRef"` + + // KeyEncryption specifies the encryption type for the key. + // Type7 is the Cisco Type 7 encryption (reversible). + // Type6 is the AES encryption (more secure). + // Clear means the key is sent in cleartext (not recommended). + // +optional + // +kubebuilder:validation:Enum=Type6;Type7;Clear + // +kubebuilder:default=Type7 + KeyEncryption TACACSKeyEncryption `json:"keyEncryption,omitempty"` + + // Timeout is the timeout in seconds for this TACACS+ server. + // +optional + // +kubebuilder:validation:Minimum=1 + // +kubebuilder:validation:Maximum=60 + Timeout *int32 `json:"timeout,omitempty"` +} + +// TACACSKeyEncryption defines the encryption type for TACACS+ server keys. +// +kubebuilder:validation:Enum=Type6;Type7;Clear +type TACACSKeyEncryption string + +const ( + // TACACSKeyEncryptionType6 uses AES encryption (more secure). + TACACSKeyEncryptionType6 TACACSKeyEncryption = "Type6" + // TACACSKeyEncryptionType7 uses Cisco Type 7 encryption (reversible). + TACACSKeyEncryptionType7 TACACSKeyEncryption = "Type7" + // TACACSKeyEncryptionClear sends the key in cleartext. + TACACSKeyEncryptionClear TACACSKeyEncryption = "Clear" +) + +// TACACSGroup represents a TACACS+ server group configuration. +type TACACSGroup struct { + // Name is the name of the TACACS+ server group. + // +required + // +kubebuilder:validation:MinLength=1 + // +kubebuilder:validation:MaxLength=63 + Name string `json:"name"` + + // Servers is the list of TACACS+ server addresses to include in this group. + // The addresses must match addresses defined in TACACSServers. + // +required + // +listType=set + // +kubebuilder:validation:MinItems=1 + // +kubebuilder:validation:MaxItems=16 + Servers []string `json:"servers"` + + // VRF is the VRF to use for communication with the TACACS+ servers. + // +optional + // +kubebuilder:validation:MaxLength=63 + VRF string `json:"vrf,omitempty"` + + // SourceInterface is the source interface to use for communication with the TACACS+ servers. + // +optional + // +kubebuilder:validation:MaxLength=63 + SourceInterface string `json:"sourceInterface,omitempty"` +} + +// AAAAuthentication defines the AAA authentication configuration. +type AAAAuthentication struct { + // Login defines authentication methods for login. + // +optional + Login *AAAAuthenticationLogin `json:"login,omitempty"` + + // LoginErrorEnable enables login error messages. + // +optional + LoginErrorEnable bool `json:"loginErrorEnable,omitempty"` +} + +// AAAAuthenticationLogin defines the login authentication methods. +type AAAAuthenticationLogin struct { + // Default defines the default authentication method list. + // +optional + Default *AAAMethodList `json:"default,omitempty"` + + // Console defines the console authentication method list. + // +optional + Console *AAAMethodList `json:"console,omitempty"` +} + +// AAAAuthorization defines the AAA authorization configuration. +type AAAAuthorization struct { + // ConfigCommands defines authorization for configuration commands. + // +optional + ConfigCommands *AAAAuthorizationConfigCommands `json:"configCommands,omitempty"` +} + +// AAAAuthorizationConfigCommands defines authorization for configuration commands. +type AAAAuthorizationConfigCommands struct { + // Default defines the default authorization method list. + // +optional + Default *AAAMethodList `json:"default,omitempty"` +} + +// AAAAccounting defines the AAA accounting configuration. +type AAAAccounting struct { + // Default defines the default accounting method list. + // +optional + Default *AAAMethodList `json:"default,omitempty"` +} + +// AAAMethodList defines a list of AAA methods to try in order. +type AAAMethodList struct { + // Methods is the ordered list of authentication/authorization/accounting methods. + // Methods are tried in order until one succeeds or all fail. + // +required + // +listType=atomic + // +kubebuilder:validation:MinItems=1 + // +kubebuilder:validation:MaxItems=4 + Methods []AAAMethod `json:"methods"` +} + +// AAAMethod represents an AAA method. +type AAAMethod struct { + // Type is the type of AAA method. + // +required + // +kubebuilder:validation:Enum=Group;Local;None + Type AAAMethodType `json:"type"` + + // GroupName is the name of the server group when Type is Group. + // +optional + // +kubebuilder:validation:MaxLength=63 + GroupName string `json:"groupName,omitempty"` +} + +// AAAMethodType defines the type of AAA method. +// +kubebuilder:validation:Enum=Group;Local;None +type AAAMethodType string + +const ( + // AAAMethodTypeGroup uses a server group (e.g., TACACS+ group). + AAAMethodTypeGroup AAAMethodType = "Group" + // AAAMethodTypeLocal uses the local user database. + AAAMethodTypeLocal AAAMethodType = "Local" + // AAAMethodTypeNone allows access without authentication. + AAAMethodTypeNone AAAMethodType = "None" +) + +// AAAStatus defines the observed state of AAA. +type AAAStatus struct { + // The conditions are a list of status objects that describe the state of the AAA. + // +listType=map + // +listMapKey=type + // +patchStrategy=merge + // +patchMergeKey=type + // +optional + Conditions []metav1.Condition `json:"conditions,omitempty"` +} + +// +kubebuilder:object:root=true +// +kubebuilder:subresource:status +// +kubebuilder:resource:path=aaas +// +kubebuilder:resource:singular=aaa +// +kubebuilder:resource:shortName=aaa +// +kubebuilder:printcolumn:name="Device",type=string,JSONPath=`.spec.deviceRef.name` +// +kubebuilder:printcolumn:name="TACACS Group",type=string,JSONPath=`.spec.tacacsGroup.name` +// +kubebuilder:printcolumn:name="Ready",type=string,JSONPath=`.status.conditions[?(@.type=="Ready")].status` +// +kubebuilder:printcolumn:name="Age",type="date",JSONPath=".metadata.creationTimestamp" + +// AAA is the Schema for the aaas API +type AAA struct { + metav1.TypeMeta `json:",inline"` + metav1.ObjectMeta `json:"metadata,omitempty"` + + // Specification of the desired state of the resource. + // More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#spec-and-status + // +required + Spec AAASpec `json:"spec"` + + // Status of the resource. This is set and updated automatically. + // Read-only. + // More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#spec-and-status + // +optional + Status AAAStatus `json:"status,omitempty,omitzero"` +} + +// GetConditions implements conditions.Getter. +func (a *AAA) GetConditions() []metav1.Condition { + return a.Status.Conditions +} + +// SetConditions implements conditions.Setter. +func (a *AAA) SetConditions(conditions []metav1.Condition) { + a.Status.Conditions = conditions +} + +// +kubebuilder:object:root=true + +// AAAList contains a list of AAA +type AAAList struct { + metav1.TypeMeta `json:",inline"` + metav1.ListMeta `json:"metadata,omitempty"` + Items []AAA `json:"items"` +} + +var ( + AAADependencies []schema.GroupVersionKind + aaaDependenciesMu sync.Mutex +) + +func RegisterAAADependency(gvk schema.GroupVersionKind) { + aaaDependenciesMu.Lock() + defer aaaDependenciesMu.Unlock() + AAADependencies = append(AAADependencies, gvk) +} + +func init() { + SchemeBuilder.Register(&AAA{}, &AAAList{}) +} diff --git a/api/core/v1alpha1/zz_generated.deepcopy.go b/api/core/v1alpha1/zz_generated.deepcopy.go index 3571a97c0..1861ed01b 100644 --- a/api/core/v1alpha1/zz_generated.deepcopy.go +++ b/api/core/v1alpha1/zz_generated.deepcopy.go @@ -1,8 +1,5 @@ //go:build !ignore_autogenerated -// SPDX-FileCopyrightText: 2025 SAP SE or an SAP affiliate company and IronCore contributors -// SPDX-License-Identifier: Apache-2.0 - // Code generated by controller-gen. DO NOT EDIT. package v1alpha1 @@ -13,6 +10,275 @@ import ( "k8s.io/apimachinery/pkg/util/intstr" ) +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *AAA) DeepCopyInto(out *AAA) { + *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 AAA. +func (in *AAA) DeepCopy() *AAA { + if in == nil { + return nil + } + out := new(AAA) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *AAA) 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 *AAAAccounting) DeepCopyInto(out *AAAAccounting) { + *out = *in + if in.Default != nil { + in, out := &in.Default, &out.Default + *out = new(AAAMethodList) + (*in).DeepCopyInto(*out) + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AAAAccounting. +func (in *AAAAccounting) DeepCopy() *AAAAccounting { + if in == nil { + return nil + } + out := new(AAAAccounting) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *AAAAuthentication) DeepCopyInto(out *AAAAuthentication) { + *out = *in + if in.Login != nil { + in, out := &in.Login, &out.Login + *out = new(AAAAuthenticationLogin) + (*in).DeepCopyInto(*out) + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AAAAuthentication. +func (in *AAAAuthentication) DeepCopy() *AAAAuthentication { + if in == nil { + return nil + } + out := new(AAAAuthentication) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *AAAAuthenticationLogin) DeepCopyInto(out *AAAAuthenticationLogin) { + *out = *in + if in.Default != nil { + in, out := &in.Default, &out.Default + *out = new(AAAMethodList) + (*in).DeepCopyInto(*out) + } + if in.Console != nil { + in, out := &in.Console, &out.Console + *out = new(AAAMethodList) + (*in).DeepCopyInto(*out) + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AAAAuthenticationLogin. +func (in *AAAAuthenticationLogin) DeepCopy() *AAAAuthenticationLogin { + if in == nil { + return nil + } + out := new(AAAAuthenticationLogin) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *AAAAuthorization) DeepCopyInto(out *AAAAuthorization) { + *out = *in + if in.ConfigCommands != nil { + in, out := &in.ConfigCommands, &out.ConfigCommands + *out = new(AAAAuthorizationConfigCommands) + (*in).DeepCopyInto(*out) + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AAAAuthorization. +func (in *AAAAuthorization) DeepCopy() *AAAAuthorization { + if in == nil { + return nil + } + out := new(AAAAuthorization) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *AAAAuthorizationConfigCommands) DeepCopyInto(out *AAAAuthorizationConfigCommands) { + *out = *in + if in.Default != nil { + in, out := &in.Default, &out.Default + *out = new(AAAMethodList) + (*in).DeepCopyInto(*out) + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AAAAuthorizationConfigCommands. +func (in *AAAAuthorizationConfigCommands) DeepCopy() *AAAAuthorizationConfigCommands { + if in == nil { + return nil + } + out := new(AAAAuthorizationConfigCommands) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *AAAList) DeepCopyInto(out *AAAList) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ListMeta.DeepCopyInto(&out.ListMeta) + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]AAA, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AAAList. +func (in *AAAList) DeepCopy() *AAAList { + if in == nil { + return nil + } + out := new(AAAList) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *AAAList) 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 *AAAMethod) DeepCopyInto(out *AAAMethod) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AAAMethod. +func (in *AAAMethod) DeepCopy() *AAAMethod { + if in == nil { + return nil + } + out := new(AAAMethod) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *AAAMethodList) DeepCopyInto(out *AAAMethodList) { + *out = *in + if in.Methods != nil { + in, out := &in.Methods, &out.Methods + *out = make([]AAAMethod, len(*in)) + copy(*out, *in) + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AAAMethodList. +func (in *AAAMethodList) DeepCopy() *AAAMethodList { + if in == nil { + return nil + } + out := new(AAAMethodList) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *AAASpec) DeepCopyInto(out *AAASpec) { + *out = *in + out.DeviceRef = in.DeviceRef + if in.ProviderConfigRef != nil { + in, out := &in.ProviderConfigRef, &out.ProviderConfigRef + *out = new(TypedLocalObjectReference) + **out = **in + } + if in.TACACSServers != nil { + in, out := &in.TACACSServers, &out.TACACSServers + *out = make([]TACACSServer, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + if in.TACACSGroup != nil { + in, out := &in.TACACSGroup, &out.TACACSGroup + *out = new(TACACSGroup) + (*in).DeepCopyInto(*out) + } + if in.Authentication != nil { + in, out := &in.Authentication, &out.Authentication + *out = new(AAAAuthentication) + (*in).DeepCopyInto(*out) + } + if in.Authorization != nil { + in, out := &in.Authorization, &out.Authorization + *out = new(AAAAuthorization) + (*in).DeepCopyInto(*out) + } + if in.Accounting != nil { + in, out := &in.Accounting, &out.Accounting + *out = new(AAAAccounting) + (*in).DeepCopyInto(*out) + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AAASpec. +func (in *AAASpec) DeepCopy() *AAASpec { + if in == nil { + return nil + } + out := new(AAASpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *AAAStatus) DeepCopyInto(out *AAAStatus) { + *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 AAAStatus. +func (in *AAAStatus) DeepCopy() *AAAStatus { + if in == nil { + return nil + } + out := new(AAAStatus) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *ACLEntry) DeepCopyInto(out *ACLEntry) { *out = *in @@ -3687,6 +3953,47 @@ func (in *SyslogStatus) DeepCopy() *SyslogStatus { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *TACACSGroup) DeepCopyInto(out *TACACSGroup) { + *out = *in + if in.Servers != nil { + in, out := &in.Servers, &out.Servers + *out = make([]string, len(*in)) + copy(*out, *in) + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new TACACSGroup. +func (in *TACACSGroup) DeepCopy() *TACACSGroup { + if in == nil { + return nil + } + out := new(TACACSGroup) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *TACACSServer) DeepCopyInto(out *TACACSServer) { + *out = *in + out.KeySecretRef = in.KeySecretRef + if in.Timeout != nil { + in, out := &in.Timeout, &out.Timeout + *out = new(int32) + **out = **in + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new TACACSServer. +func (in *TACACSServer) DeepCopy() *TACACSServer { + if in == nil { + return nil + } + out := new(TACACSServer) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *TLS) DeepCopyInto(out *TLS) { *out = *in diff --git a/cmd/main.go b/cmd/main.go index 82de236ce..c35e45f3a 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -576,6 +576,18 @@ func main() { //nolint:gocyclo os.Exit(1) } + if err := (&corecontroller.AAAReconciler{ + Client: mgr.GetClient(), + Scheme: mgr.GetScheme(), + Recorder: mgr.GetEventRecorderFor("aaa-controller"), + WatchFilterValue: watchFilterValue, + Provider: prov, + Locker: locker, + }).SetupWithManager(mgr); err != nil { + setupLog.Error(err, "unable to create controller", "controller", "AAA") + os.Exit(1) + } + if err := (&corecontroller.PrefixSetReconciler{ Client: mgr.GetClient(), Scheme: mgr.GetScheme(), diff --git a/config/crd/bases/networking.metal.ironcore.dev_aaas.yaml b/config/crd/bases/networking.metal.ironcore.dev_aaas.yaml new file mode 100644 index 000000000..f2813fd6f --- /dev/null +++ b/config/crd/bases/networking.metal.ironcore.dev_aaas.yaml @@ -0,0 +1,479 @@ +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.20.0 + name: aaas.networking.metal.ironcore.dev +spec: + group: networking.metal.ironcore.dev + names: + kind: AAA + listKind: AAAList + plural: aaas + shortNames: + - aaa + singular: aaa + scope: Namespaced + versions: + - additionalPrinterColumns: + - jsonPath: .spec.deviceRef.name + name: Device + type: string + - jsonPath: .spec.tacacsGroup.name + name: TACACS Group + type: string + - jsonPath: .status.conditions[?(@.type=="Ready")].status + name: Ready + type: string + - jsonPath: .metadata.creationTimestamp + name: Age + type: date + name: v1alpha1 + schema: + openAPIV3Schema: + description: AAA is the Schema for the aaas 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: |- + Specification of the desired state of the resource. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#spec-and-status + properties: + accounting: + description: Accounting defines the AAA accounting configuration. + properties: + default: + description: Default defines the default accounting method list. + properties: + methods: + description: |- + Methods is the ordered list of authentication/authorization/accounting methods. + Methods are tried in order until one succeeds or all fail. + items: + description: AAAMethod represents an AAA method. + properties: + groupName: + description: GroupName is the name of the server group + when Type is Group. + maxLength: 63 + type: string + type: + allOf: + - enum: + - Group + - Local + - None + - enum: + - Group + - Local + - None + description: Type is the type of AAA method. + type: string + required: + - type + type: object + maxItems: 4 + minItems: 1 + type: array + x-kubernetes-list-type: atomic + required: + - methods + type: object + type: object + authentication: + description: Authentication defines the AAA authentication configuration. + properties: + login: + description: Login defines authentication methods for login. + properties: + console: + description: Console defines the console authentication method + list. + properties: + methods: + description: |- + Methods is the ordered list of authentication/authorization/accounting methods. + Methods are tried in order until one succeeds or all fail. + items: + description: AAAMethod represents an AAA method. + properties: + groupName: + description: GroupName is the name of the server + group when Type is Group. + maxLength: 63 + type: string + type: + allOf: + - enum: + - Group + - Local + - None + - enum: + - Group + - Local + - None + description: Type is the type of AAA method. + type: string + required: + - type + type: object + maxItems: 4 + minItems: 1 + type: array + x-kubernetes-list-type: atomic + required: + - methods + type: object + default: + description: Default defines the default authentication method + list. + properties: + methods: + description: |- + Methods is the ordered list of authentication/authorization/accounting methods. + Methods are tried in order until one succeeds or all fail. + items: + description: AAAMethod represents an AAA method. + properties: + groupName: + description: GroupName is the name of the server + group when Type is Group. + maxLength: 63 + type: string + type: + allOf: + - enum: + - Group + - Local + - None + - enum: + - Group + - Local + - None + description: Type is the type of AAA method. + type: string + required: + - type + type: object + maxItems: 4 + minItems: 1 + type: array + x-kubernetes-list-type: atomic + required: + - methods + type: object + type: object + loginErrorEnable: + description: LoginErrorEnable enables login error messages. + type: boolean + type: object + authorization: + description: Authorization defines the AAA authorization configuration. + properties: + configCommands: + description: ConfigCommands defines authorization for configuration + commands. + properties: + default: + description: Default defines the default authorization method + list. + properties: + methods: + description: |- + Methods is the ordered list of authentication/authorization/accounting methods. + Methods are tried in order until one succeeds or all fail. + items: + description: AAAMethod represents an AAA method. + properties: + groupName: + description: GroupName is the name of the server + group when Type is Group. + maxLength: 63 + type: string + type: + allOf: + - enum: + - Group + - Local + - None + - enum: + - Group + - Local + - None + description: Type is the type of AAA method. + type: string + required: + - type + type: object + maxItems: 4 + minItems: 1 + type: array + x-kubernetes-list-type: atomic + required: + - methods + type: object + type: object + type: object + 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 + providerConfigRef: + description: |- + ProviderConfigRef is a reference to a resource holding the provider-specific configuration of this AAA. + This reference is used to link the AAA 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 + tacacsGroup: + description: TACACSGroup is the TACACS+ server group configuration. + properties: + name: + description: Name is the name of the TACACS+ server group. + maxLength: 63 + minLength: 1 + type: string + servers: + description: |- + Servers is the list of TACACS+ server addresses to include in this group. + The addresses must match addresses defined in TACACSServers. + items: + type: string + maxItems: 16 + minItems: 1 + type: array + x-kubernetes-list-type: set + sourceInterface: + description: SourceInterface is the source interface to use for + communication with the TACACS+ servers. + maxLength: 63 + type: string + vrf: + description: VRF is the VRF to use for communication with the + TACACS+ servers. + maxLength: 63 + type: string + required: + - name + - servers + type: object + tacacsServers: + description: TACACSServers is the list of TACACS+ servers to configure. + items: + description: TACACSServer represents a TACACS+ server configuration. + properties: + address: + description: Address is the IP address or hostname of the TACACS+ + server. + maxLength: 253 + minLength: 1 + type: string + keyEncryption: + allOf: + - enum: + - Type6 + - Type7 + - Clear + - enum: + - Type6 + - Type7 + - Clear + default: Type7 + description: |- + KeyEncryption specifies the encryption type for the key. + Type7 is the Cisco Type 7 encryption (reversible). + Type6 is the AES encryption (more secure). + Clear means the key is sent in cleartext (not recommended). + type: string + keySecretRef: + description: |- + KeySecretRef is a reference to a secret containing the shared key for this TACACS+ server. + The secret must contain a key specified in the SecretKeySelector. + properties: + key: + description: |- + Key is the of the entry in the secret resource's `data` or `stringData` + field to be used. + maxLength: 253 + minLength: 1 + type: string + name: + description: Name is unique within a namespace to reference + a secret resource. + maxLength: 253 + minLength: 1 + type: string + namespace: + description: |- + Namespace defines the space within which the secret name must be unique. + If omitted, the namespace of the object being reconciled will be used. + maxLength: 63 + minLength: 1 + type: string + required: + - key + - name + type: object + x-kubernetes-map-type: atomic + port: + default: 49 + description: |- + Port is the TCP port of the TACACS+ server. + Defaults to 49 if not specified. + format: int32 + maximum: 65535 + minimum: 1 + type: integer + timeout: + description: Timeout is the timeout in seconds for this TACACS+ + server. + format: int32 + maximum: 60 + minimum: 1 + type: integer + required: + - address + - keySecretRef + type: object + maxItems: 16 + type: array + x-kubernetes-list-map-keys: + - address + x-kubernetes-list-type: map + required: + - deviceRef + type: object + status: + description: |- + Status of the resource. This is set and updated automatically. + Read-only. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#spec-and-status + properties: + conditions: + description: The conditions are a list of status objects that describe + the state of the AAA. + 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 + required: + - spec + type: object + served: true + storage: true + subresources: + status: {} diff --git a/config/rbac/role.yaml b/config/rbac/role.yaml index e9757ff33..e2781fc40 100644 --- a/config/rbac/role.yaml +++ b/config/rbac/role.yaml @@ -42,6 +42,7 @@ rules: - apiGroups: - networking.metal.ironcore.dev resources: + - aaas - accesscontrollists - banners - bgp @@ -77,6 +78,7 @@ rules: - apiGroups: - networking.metal.ironcore.dev resources: + - aaas/finalizers - accesscontrollists/finalizers - banners/finalizers - bgp/finalizers @@ -106,6 +108,7 @@ rules: - apiGroups: - networking.metal.ironcore.dev resources: + - aaas/status - accesscontrollists/status - banners/status - bgp/status diff --git a/config/samples/networking_v1alpha1_aaa.yaml b/config/samples/networking_v1alpha1_aaa.yaml new file mode 100644 index 000000000..1d281c0c9 --- /dev/null +++ b/config/samples/networking_v1alpha1_aaa.yaml @@ -0,0 +1,103 @@ +# Example AAA configuration with TACACS+ servers +# This configures: +# - feature tacacs+ +# - tacacs-server hosts with Type 7 encrypted keys +# - AAA group server for TACACS +# - AAA authentication, authorization, and accounting +--- +apiVersion: v1 +kind: Secret +metadata: + name: tacacs-server-keys + namespace: default +type: Opaque +stringData: + # Replace with your actual TACACS+ server shared secret + server-key: "supersecretkey" +--- +apiVersion: networking.metal.ironcore.dev/v1alpha1 +kind: AAA +metadata: + name: aaa-tacacs + namespace: default +spec: + # Reference to the Device this AAA configuration belongs to + deviceRef: + name: my-switch + + # TACACS+ server configuration + # Equivalent to: + # tacacs-server host 10.16.8.142 key 7 $TYPE_7_SECRET + # tacacs-server host 10.16.8.32 key 7 $TYPE_7_SECRET + # tacacs-server host 169.145.33.51 key 7 $TYPE_7_SECRET + tacacsServers: + - address: "10.16.8.142" + keySecretRef: + name: tacacs-server-keys + key: server-key + keyEncryption: Type7 + - address: "10.16.8.32" + keySecretRef: + name: tacacs-server-keys + key: server-key + keyEncryption: Type7 + - address: "169.145.33.51" + keySecretRef: + name: tacacs-server-keys + key: server-key + keyEncryption: Type7 + + # TACACS+ server group configuration + # Equivalent to: + # aaa group server tacacs+ GR_TACACS + # server 10.16.8.32 + # server 10.16.8.142 + # server 169.145.33.51 + # use-vrf management + # source-interface mgmt0 + tacacsGroup: + name: GR_TACACS + servers: + - "10.16.8.32" + - "10.16.8.142" + - "169.145.33.51" + vrf: management + sourceInterface: mgmt0 + + # AAA authentication configuration + # Equivalent to: + # aaa authentication login default group GR_TACACS + # aaa authentication login console group GR_TACACS local + # aaa authentication login error-enable + authentication: + login: + default: + methods: + - type: Group + groupName: GR_TACACS + console: + methods: + - type: Group + groupName: GR_TACACS + - type: Local + loginErrorEnable: true + + # AAA authorization configuration + # Equivalent to: + # aaa authorization config-commands default group GR_TACACS local + authorization: + configCommands: + default: + methods: + - type: Group + groupName: GR_TACACS + - type: Local + + # AAA accounting configuration + # Equivalent to: + # aaa accounting default group GR_TACACS + accounting: + default: + methods: + - type: Group + groupName: GR_TACACS diff --git a/internal/controller/core/aaa_controller.go b/internal/controller/core/aaa_controller.go new file mode 100644 index 000000000..17733f43f --- /dev/null +++ b/internal/controller/core/aaa_controller.go @@ -0,0 +1,332 @@ +// SPDX-FileCopyrightText: 2025 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/record" + "k8s.io/klog/v2" + 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/handler" + "sigs.k8s.io/controller-runtime/pkg/predicate" + + "github.com/ironcore-dev/network-operator/api/core/v1alpha1" + "github.com/ironcore-dev/network-operator/internal/clientutil" + "github.com/ironcore-dev/network-operator/internal/conditions" + "github.com/ironcore-dev/network-operator/internal/deviceutil" + "github.com/ironcore-dev/network-operator/internal/provider" + "github.com/ironcore-dev/network-operator/internal/resourcelock" +) + +// AAAReconciler reconciles a AAA object +type AAAReconciler 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 record.EventRecorder + + // Provider is the driver that will be used to create & delete the AAA configuration. + Provider provider.ProviderFunc + + // Locker is used to synchronize operations on resources targeting the same device. + Locker *resourcelock.ResourceLocker +} + +// +kubebuilder:rbac:groups=networking.metal.ironcore.dev,resources=aaas,verbs=get;list;watch;create;update;patch;delete +// +kubebuilder:rbac:groups=networking.metal.ironcore.dev,resources=aaas/status,verbs=get;update;patch +// +kubebuilder:rbac:groups=networking.metal.ironcore.dev,resources=aaas/finalizers,verbs=update +// +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.20.2/pkg/reconcile +// +// For more details about the method shape, read up here: +// - https://ahmet.im/blog/controller-pitfalls/#reconcile-method-shape +func (r *AAAReconciler) Reconcile(ctx context.Context, req ctrl.Request) (_ ctrl.Result, reterr error) { + log := ctrl.LoggerFrom(ctx) + log.Info("Reconciling resource") + + obj := new(v1alpha1.AAA) + 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.AAAProvider) + 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.AAAProvider", + }) { + 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 err := r.Locker.AcquireLock(ctx, device.Name, "aaa-controller"); err != nil { + if errors.Is(err, resourcelock.ErrLockAlreadyHeld) { + log.Info("Device is already locked, requeuing reconciliation") + return ctrl.Result{RequeueAfter: time.Second * 5}, nil + } + log.Error(err, "Failed to acquire device lock") + return ctrl.Result{}, err + } + defer func() { + if err := r.Locker.ReleaseLock(ctx, device.Name, "aaa-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 + } + } + + s := &aaaScope{ + Device: device, + AAA: obj, + 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 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 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) { + 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) { + if err := r.Patch(ctx, obj, client.MergeFrom(orig)); err != nil { + log.Error(err, "Failed to update resource metadata") + reterr = kerrors.NewAggregate([]error{reterr, err}) + } + return + } + + 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{}, nil +} + +// SetupWithManager sets up the controller with the Manager. +func (r *AAAReconciler) SetupWithManager(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) + } + + return ctrl.NewControllerManagedBy(mgr). + For(&v1alpha1.AAA{}). + Named("aaa"). + WithEventFilter(filter). + // Watches enqueues AAA for referenced Secret resources. + Watches( + &corev1.Secret{}, + handler.EnqueueRequestsFromMapFunc(r.secretToAAA), + builder.WithPredicates(predicate.GenerationChangedPredicate{}), + ). + Complete(r) +} + +// aaaScope holds the different objects that are read and used during the reconcile. +type aaaScope struct { + Device *v1alpha1.Device + AAA *v1alpha1.AAA + Connection *deviceutil.Connection + ProviderConfig *provider.ProviderConfig + Provider provider.AAAProvider +} + +func (r *AAAReconciler) reconcile(ctx context.Context, s *aaaScope) (reterr error) { + if s.AAA.Labels == nil { + s.AAA.Labels = make(map[string]string) + } + + s.AAA.Labels[v1alpha1.DeviceLabel] = s.Device.Name + + // Ensure the AAA is owned by the Device. + if !controllerutil.HasControllerReference(s.AAA) { + if err := controllerutil.SetOwnerReference(s.Device, s.AAA, r.Scheme, controllerutil.WithBlockOwnerDeletion(true)); err != nil { + return err + } + } + + 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}) + } + }() + + // Load TACACS+ server keys from secrets + c := clientutil.NewClient(r, s.AAA.Namespace) + tacacsKeys := make(map[string]string) + for _, server := range s.AAA.Spec.TACACSServers { + key, err := c.Secret(ctx, &server.KeySecretRef) + if err != nil { + return fmt.Errorf("failed to get TACACS+ key for server %s: %w", server.Address, err) + } + tacacsKeys[server.Address] = string(key) + } + + // Ensure the AAA is realized on the provider. + err := s.Provider.EnsureAAA(ctx, &provider.EnsureAAARequest{ + AAA: s.AAA, + ProviderConfig: s.ProviderConfig, + TACACSServerKeys: tacacsKeys, + }) + + cond := conditions.FromError(err) + // As this resource is configuration only, we use the Configured condition as top-level Ready condition. + cond.Type = v1alpha1.ReadyCondition + conditions.Set(s.AAA, cond) + + return err +} + +func (r *AAAReconciler) finalize(ctx context.Context, s *aaaScope) (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.DeleteAAA(ctx, &provider.DeleteAAARequest{ + AAA: s.AAA, + ProviderConfig: s.ProviderConfig, + }) +} + +// secretToAAA is a [handler.MapFunc] to be used to enqueue requests for reconciliation +// for an AAA to update when one of its referenced Secrets gets updated. +func (r *AAAReconciler) secretToAAA(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, "Secret", klog.KObj(secret)) + + aaas := new(v1alpha1.AAAList) + if err := r.List(ctx, aaas); err != nil { + log.Error(err, "Failed to list AAAs") + return nil + } + + requests := []ctrl.Request{} + for _, a := range aaas.Items { + // Check if any TACACS+ server references this secret + for _, server := range a.Spec.TACACSServers { + if server.KeySecretRef.Name == secret.Name && a.Namespace == secret.Namespace { + log.Info("Enqueuing AAA for reconciliation", "AAA", klog.KObj(&a)) + requests = append(requests, ctrl.Request{ + NamespacedName: client.ObjectKey{ + Name: a.Name, + Namespace: a.Namespace, + }, + }) + break // Only enqueue once per AAA + } + } + } + + return requests +} diff --git a/internal/provider/cisco/nxos/aaa.go b/internal/provider/cisco/nxos/aaa.go new file mode 100644 index 000000000..ff8555250 --- /dev/null +++ b/internal/provider/cisco/nxos/aaa.go @@ -0,0 +1,186 @@ +// SPDX-FileCopyrightText: 2025 SAP SE or an SAP affiliate company and IronCore contributors +// SPDX-License-Identifier: Apache-2.0 + +package nxos + +import ( + "github.com/ironcore-dev/network-operator/api/core/v1alpha1" + "github.com/ironcore-dev/network-operator/internal/provider/cisco/gnmiext/v2" +) + +var ( + _ gnmiext.Configurable = (*TACACSFeature)(nil) + _ gnmiext.Configurable = (*TacacsPlusProvider)(nil) + _ gnmiext.Configurable = (*TacacsPlusProviderGroup)(nil) + _ gnmiext.Configurable = (*AAADefaultAuth)(nil) + _ gnmiext.Configurable = (*AAAConsoleAuth)(nil) + _ gnmiext.Configurable = (*AAADefaultAuthor)(nil) + _ gnmiext.Configurable = (*AAADefaultAcc)(nil) +) + +// TACACSFeature enables/disables the TACACS+ feature on NX-OS. +// Path: System/fm-items/tacacsplus-items/adminSt +type TACACSFeature string + +func (*TACACSFeature) XPath() string { + return "System/fm-items/tacacsplus-items/adminSt" +} + +const ( + TACACSFeatureEnabled TACACSFeature = "enabled" + TACACSFeatureDisabled TACACSFeature = "disabled" +) + +// TacacsPlusProvider represents a TACACS+ server host configuration. +// Path: System/userext-items/tacacsext-items/tacacsplusprovider-items/TacacsPlusProvider-list[name=
] +type TacacsPlusProvider struct { + Name string `json:"name"` + Port int32 `json:"port,omitempty"` + Key string `json:"key,omitempty"` + KeyEnc string `json:"keyEnc,omitempty"` + Timeout int32 `json:"timeout,omitempty"` + Retries int32 `json:"retries,omitempty"` + AuthProtocol string `json:"authProtocol,omitempty"` +} + +func (*TacacsPlusProvider) IsListItem() {} + +func (p *TacacsPlusProvider) XPath() string { + return "System/userext-items/tacacsext-items/tacacsplusprovider-items/TacacsPlusProvider-list[name=" + p.Name + "]" +} + +// TacacsPlusProviderGroup represents a TACACS+ server group configuration. +// Path: System/userext-items/tacacsext-items/tacacsplusprovidergroup-items/TacacsPlusProviderGroup-list[name=] +type TacacsPlusProviderGroup struct { + Name string `json:"name"` + Vrf string `json:"vrf,omitempty"` + SrcIf string `json:"srcIf,omitempty"` + Deadtime int32 `json:"deadtime,omitempty"` + ProviderRefItems TacacsPlusProviderGroupRefItems `json:"providerref-items,omitzero"` +} + +func (*TacacsPlusProviderGroup) IsListItem() {} + +func (g *TacacsPlusProviderGroup) XPath() string { + return "System/userext-items/tacacsext-items/tacacsplusprovidergroup-items/TacacsPlusProviderGroup-list[name=" + g.Name + "]" +} + +type TacacsPlusProviderGroupRefItems struct { + ProviderRefList gnmiext.List[string, *TacacsPlusProviderRef] `json:"ProviderRef-list,omitzero"` +} + +type TacacsPlusProviderRef struct { + Name string `json:"name"` +} + +func (r *TacacsPlusProviderRef) Key() string { return r.Name } + +// AAADefaultAuth represents AAA default authentication configuration. +// Path: System/userext-items/authrealm-items/defaultauth-items +type AAADefaultAuth struct { + Realm string `json:"realm,omitempty"` + ProviderGroup string `json:"providerGroup,omitempty"` + Fallback string `json:"fallback,omitempty"` + Local string `json:"local,omitempty"` + None string `json:"none,omitempty"` + ErrEn bool `json:"errEn,omitempty"` + AuthProtocol string `json:"authProtocol,omitempty"` +} + +func (*AAADefaultAuth) XPath() string { + return "System/userext-items/authrealm-items/defaultauth-items" +} + +// AAAConsoleAuth represents AAA console authentication configuration. +// Path: System/userext-items/authrealm-items/consoleauth-items +type AAAConsoleAuth struct { + Realm string `json:"realm,omitempty"` + ProviderGroup string `json:"providerGroup,omitempty"` + Fallback string `json:"fallback,omitempty"` + Local string `json:"local,omitempty"` + None string `json:"none,omitempty"` + ErrEn bool `json:"errEn,omitempty"` + AuthProtocol string `json:"authProtocol,omitempty"` +} + +func (*AAAConsoleAuth) XPath() string { + return "System/userext-items/authrealm-items/consoleauth-items" +} + +// AAADefaultAuthor represents AAA default authorization configuration for config commands. +// Path: System/userext-items/authrealm-items/defaultauthor-items/DefaultAuthor-list[cmdType=config] +type AAADefaultAuthor struct { + Name string `json:"name"` + CmdType string `json:"cmdType"` + Realm string `json:"realm,omitempty"` + ProviderGroup string `json:"providerGroup,omitempty"` + LocalRbac bool `json:"localRbac,omitempty"` + AuthorMethodNone bool `json:"authorMethodNone,omitempty"` +} + +func (*AAADefaultAuthor) IsListItem() {} + +func (a *AAADefaultAuthor) XPath() string { + return "System/userext-items/authrealm-items/defaultauthor-items/DefaultAuthor-list[cmdType=" + a.CmdType + "]" +} + +// AAADefaultAcc represents AAA default accounting configuration. +// Path: System/userext-items/authrealm-items/defaultacc-items +type AAADefaultAcc struct { + Name string `json:"name,omitempty"` + Realm string `json:"realm,omitempty"` + ProviderGroup string `json:"providerGroup,omitempty"` + LocalRbac bool `json:"localRbac,omitempty"` + AccMethodNone bool `json:"accMethodNone,omitempty"` +} + +func (*AAADefaultAcc) XPath() string { + return "System/userext-items/authrealm-items/defaultacc-items" +} + +// MapKeyEncryption maps the API key encryption type to NX-OS type. +func MapKeyEncryption(enc v1alpha1.TACACSKeyEncryption) string { + switch enc { + case v1alpha1.TACACSKeyEncryptionType6: + return "6" + case v1alpha1.TACACSKeyEncryptionType7: + return "7" + case v1alpha1.TACACSKeyEncryptionClear: + return "0" + default: + return "7" + } +} + +// MapRealmFromMethodType maps the API method type to NX-OS realm. +func MapRealmFromMethodType(method v1alpha1.AAAMethodType, groupName string) string { + switch method { + case v1alpha1.AAAMethodTypeGroup: + return "tacacs" + case v1alpha1.AAAMethodTypeLocal: + return "local" + case v1alpha1.AAAMethodTypeNone: + return "none" + default: + return "local" + } +} + +// MapLocalFromMethodList checks if local is in the method list. +func MapLocalFromMethodList(methods []v1alpha1.AAAMethod) string { + for _, m := range methods { + if m.Type == v1alpha1.AAAMethodTypeLocal { + return "yes" + } + } + return "no" +} + +// MapFallbackFromMethodList determines fallback setting from method list. +func MapFallbackFromMethodList(methods []v1alpha1.AAAMethod) string { + // If there's more than one method, enable fallback + if len(methods) > 1 { + return "yes" + } + return "no" +} diff --git a/internal/provider/cisco/nxos/provider.go b/internal/provider/cisco/nxos/provider.go index 09302ca23..16abb7b33 100644 --- a/internal/provider/cisco/nxos/provider.go +++ b/internal/provider/cisco/nxos/provider.go @@ -62,6 +62,7 @@ var ( _ provider.NVEProvider = (*Provider)(nil) _ provider.LLDPProvider = (*Provider)(nil) _ provider.DHCPRelayProvider = (*Provider)(nil) + _ provider.AAAProvider = (*Provider)(nil) ) type Provider struct { @@ -3270,6 +3271,197 @@ func (p *Provider) GetDHCPRelayStatus(ctx context.Context, req *provider.DHCPRel return s, nil } +func (p *Provider) EnsureAAA(ctx context.Context, req *provider.EnsureAAARequest) error { + var conf []gnmiext.Configurable + + // Enable TACACS+ feature if there are TACACS servers configured + if len(req.AAA.Spec.TACACSServers) > 0 { + tacacsFeature := TACACSFeatureEnabled + conf = append(conf, &tacacsFeature) + } + + // Configure TACACS+ server hosts + for _, server := range req.AAA.Spec.TACACSServers { + provider := &TacacsPlusProvider{ + Name: server.Address, + Port: server.Port, + KeyEnc: MapKeyEncryption(server.KeyEncryption), + } + if key, ok := req.TACACSServerKeys[server.Address]; ok { + provider.Key = key + } + if server.Timeout != nil { + provider.Timeout = *server.Timeout + } + conf = append(conf, provider) + } + + // Configure TACACS+ server group + if req.AAA.Spec.TACACSGroup != nil { + group := &TacacsPlusProviderGroup{ + Name: req.AAA.Spec.TACACSGroup.Name, + Vrf: req.AAA.Spec.TACACSGroup.VRF, + SrcIf: req.AAA.Spec.TACACSGroup.SourceInterface, + } + for _, serverAddr := range req.AAA.Spec.TACACSGroup.Servers { + group.ProviderRefItems.ProviderRefList.Set(&TacacsPlusProviderRef{Name: serverAddr}) + } + conf = append(conf, group) + } + + // Configure AAA default authentication + if req.AAA.Spec.Authentication != nil && req.AAA.Spec.Authentication.Login != nil { + if req.AAA.Spec.Authentication.Login.Default != nil && len(req.AAA.Spec.Authentication.Login.Default.Methods) > 0 { + methods := req.AAA.Spec.Authentication.Login.Default.Methods + authen := &AAADefaultAuth{ + ErrEn: req.AAA.Spec.Authentication.LoginErrorEnable, + Fallback: MapFallbackFromMethodList(methods), + Local: MapLocalFromMethodList(methods), + } + // Set realm and provider group based on first method + if methods[0].Type == v1alpha1.AAAMethodTypeGroup { + authen.Realm = "tacacs" + authen.ProviderGroup = methods[0].GroupName + } else { + authen.Realm = MapRealmFromMethodType(methods[0].Type, "") + } + conf = append(conf, authen) + } + + // Configure AAA console authentication + if req.AAA.Spec.Authentication.Login.Console != nil && len(req.AAA.Spec.Authentication.Login.Console.Methods) > 0 { + methods := req.AAA.Spec.Authentication.Login.Console.Methods + consoleAuth := &AAAConsoleAuth{ + ErrEn: req.AAA.Spec.Authentication.LoginErrorEnable, + Fallback: MapFallbackFromMethodList(methods), + Local: MapLocalFromMethodList(methods), + } + if methods[0].Type == v1alpha1.AAAMethodTypeGroup { + consoleAuth.Realm = "tacacs" + consoleAuth.ProviderGroup = methods[0].GroupName + } else { + consoleAuth.Realm = MapRealmFromMethodType(methods[0].Type, "") + } + conf = append(conf, consoleAuth) + } + } + + // Configure AAA authorization for config commands + if req.AAA.Spec.Authorization != nil && req.AAA.Spec.Authorization.ConfigCommands != nil { + if req.AAA.Spec.Authorization.ConfigCommands.Default != nil && len(req.AAA.Spec.Authorization.ConfigCommands.Default.Methods) > 0 { + methods := req.AAA.Spec.Authorization.ConfigCommands.Default.Methods + author := &AAADefaultAuthor{ + Name: "Author", + CmdType: "config", + LocalRbac: MapLocalFromMethodList(methods) == "yes", + } + if methods[0].Type == v1alpha1.AAAMethodTypeGroup { + author.Realm = "tacacs" + author.ProviderGroup = methods[0].GroupName + } else { + author.Realm = MapRealmFromMethodType(methods[0].Type, "") + } + conf = append(conf, author) + } + } + + // Configure AAA accounting + if req.AAA.Spec.Accounting != nil && req.AAA.Spec.Accounting.Default != nil && len(req.AAA.Spec.Accounting.Default.Methods) > 0 { + methods := req.AAA.Spec.Accounting.Default.Methods + acct := &AAADefaultAcc{ + Name: "Accounting", + LocalRbac: MapLocalFromMethodList(methods) == "yes", + } + if methods[0].Type == v1alpha1.AAAMethodTypeGroup { + acct.Realm = "tacacs" + acct.ProviderGroup = methods[0].GroupName + } else { + acct.Realm = MapRealmFromMethodType(methods[0].Type, "") + } + conf = append(conf, acct) + } + + return p.Patch(ctx, conf...) +} + +func (p *Provider) DeleteAAA(ctx context.Context, req *provider.DeleteAAARequest) error { + // Reset AAA accounting to local + if req.AAA.Spec.Accounting != nil && req.AAA.Spec.Accounting.Default != nil { + acct := &AAADefaultAcc{ + Name: "Accounting", + Realm: "local", + LocalRbac: true, + } + if err := p.Patch(ctx, acct); err != nil { + return err + } + } + + // Reset AAA authorization to local + if req.AAA.Spec.Authorization != nil && req.AAA.Spec.Authorization.ConfigCommands != nil { + author := &AAADefaultAuthor{ + Name: "Author", + CmdType: "config", + Realm: "local", + LocalRbac: true, + } + if err := p.Patch(ctx, author); err != nil { + return err + } + } + + // Reset AAA authentication to local + if req.AAA.Spec.Authentication != nil { + authen := &AAADefaultAuth{ + Realm: "local", + Local: "yes", + Fallback: "yes", + ErrEn: false, + } + if err := p.Patch(ctx, authen); err != nil { + return err + } + + if req.AAA.Spec.Authentication.Login != nil && req.AAA.Spec.Authentication.Login.Console != nil { + consoleAuth := &AAAConsoleAuth{ + Realm: "local", + Local: "yes", + Fallback: "yes", + ErrEn: false, + } + if err := p.Patch(ctx, consoleAuth); err != nil { + return err + } + } + } + + // Delete TACACS+ server group + if req.AAA.Spec.TACACSGroup != nil { + group := &TacacsPlusProviderGroup{Name: req.AAA.Spec.TACACSGroup.Name} + if err := p.client.Delete(ctx, group); err != nil { + return err + } + } + + // Delete TACACS+ server hosts + for _, server := range req.AAA.Spec.TACACSServers { + provider := &TacacsPlusProvider{Name: server.Address} + if err := p.client.Delete(ctx, provider); err != nil { + return err + } + } + + // Disable TACACS+ feature if no servers remain + if len(req.AAA.Spec.TACACSServers) > 0 { + tacacsFeature := TACACSFeatureDisabled + if err := p.Patch(ctx, &tacacsFeature); err != nil { + return err + } + } + + return nil +} + func init() { provider.Register("cisco-nxos-gnmi", NewProvider) } diff --git a/internal/provider/provider.go b/internal/provider/provider.go index a9d7f9883..a43ab49af 100644 --- a/internal/provider/provider.go +++ b/internal/provider/provider.go @@ -625,6 +625,29 @@ type NVEProvider interface { GetNVEStatus(context.Context, *NVERequest) (NVEStatus, error) } +// AAAProvider is the interface for the realization of the AAA objects over different providers. +type AAAProvider interface { + Provider + + // EnsureAAA call is responsible for AAA realization on the provider. + EnsureAAA(context.Context, *EnsureAAARequest) error + // DeleteAAA call is responsible for AAA deletion on the provider. + DeleteAAA(context.Context, *DeleteAAARequest) error +} + +type EnsureAAARequest struct { + AAA *v1alpha1.AAA + ProviderConfig *ProviderConfig + // TACACSServerKeys contains the decrypted keys for each TACACS+ server, + // keyed by server address. + TACACSServerKeys map[string]string +} + +type DeleteAAARequest struct { + AAA *v1alpha1.AAA + ProviderConfig *ProviderConfig +} + type NVERequest struct { NVE *v1alpha1.NetworkVirtualizationEdge SourceInterface *v1alpha1.Interface From 2dbbcd2b44b0a83b1c007f5cbf975c2c422ac878 Mon Sep 17 00:00:00 2001 From: i521907 Date: Wed, 4 Feb 2026 21:35:08 -0500 Subject: [PATCH 02/32] Fix golangci-lint issues Signed-off-by: i521907 --- internal/provider/cisco/nxos/aaa.go | 35 +++++++++++++-------- internal/provider/cisco/nxos/provider.go | 40 ++++++++++++------------ 2 files changed, 42 insertions(+), 33 deletions(-) diff --git a/internal/provider/cisco/nxos/aaa.go b/internal/provider/cisco/nxos/aaa.go index ff8555250..09474f70a 100644 --- a/internal/provider/cisco/nxos/aaa.go +++ b/internal/provider/cisco/nxos/aaa.go @@ -31,6 +31,15 @@ const ( TACACSFeatureDisabled TACACSFeature = "disabled" ) +// AAA configuration constants +const ( + AAARealmTacacs = "tacacs" + AAARealmLocal = "local" + AAARealmNone = "none" + AAAValueYes = "yes" + AAAValueNo = "no" +) + // TacacsPlusProvider represents a TACACS+ server host configuration. // Path: System/userext-items/tacacsext-items/tacacsplusprovider-items/TacacsPlusProvider-list[name=
] type TacacsPlusProvider struct { @@ -52,11 +61,11 @@ func (p *TacacsPlusProvider) XPath() string { // TacacsPlusProviderGroup represents a TACACS+ server group configuration. // Path: System/userext-items/tacacsext-items/tacacsplusprovidergroup-items/TacacsPlusProviderGroup-list[name=] type TacacsPlusProviderGroup struct { - Name string `json:"name"` - Vrf string `json:"vrf,omitempty"` - SrcIf string `json:"srcIf,omitempty"` - Deadtime int32 `json:"deadtime,omitempty"` - ProviderRefItems TacacsPlusProviderGroupRefItems `json:"providerref-items,omitzero"` + Name string `json:"name"` + Vrf string `json:"vrf,omitempty"` + SrcIf string `json:"srcIf,omitempty"` + Deadtime int32 `json:"deadtime,omitempty"` + ProviderRefItems TacacsPlusProviderGroupRefItems `json:"providerref-items,omitzero"` } func (*TacacsPlusProviderGroup) IsListItem() {} @@ -156,13 +165,13 @@ func MapKeyEncryption(enc v1alpha1.TACACSKeyEncryption) string { func MapRealmFromMethodType(method v1alpha1.AAAMethodType, groupName string) string { switch method { case v1alpha1.AAAMethodTypeGroup: - return "tacacs" + return AAARealmTacacs case v1alpha1.AAAMethodTypeLocal: - return "local" + return AAARealmLocal case v1alpha1.AAAMethodTypeNone: - return "none" + return AAARealmNone default: - return "local" + return AAARealmLocal } } @@ -170,17 +179,17 @@ func MapRealmFromMethodType(method v1alpha1.AAAMethodType, groupName string) str func MapLocalFromMethodList(methods []v1alpha1.AAAMethod) string { for _, m := range methods { if m.Type == v1alpha1.AAAMethodTypeLocal { - return "yes" + return AAAValueYes } } - return "no" + return AAAValueNo } // MapFallbackFromMethodList determines fallback setting from method list. func MapFallbackFromMethodList(methods []v1alpha1.AAAMethod) string { // If there's more than one method, enable fallback if len(methods) > 1 { - return "yes" + return AAAValueYes } - return "no" + return AAAValueNo } diff --git a/internal/provider/cisco/nxos/provider.go b/internal/provider/cisco/nxos/provider.go index 16abb7b33..bf1250184 100644 --- a/internal/provider/cisco/nxos/provider.go +++ b/internal/provider/cisco/nxos/provider.go @@ -3282,18 +3282,18 @@ func (p *Provider) EnsureAAA(ctx context.Context, req *provider.EnsureAAARequest // Configure TACACS+ server hosts for _, server := range req.AAA.Spec.TACACSServers { - provider := &TacacsPlusProvider{ + srv := &TacacsPlusProvider{ Name: server.Address, Port: server.Port, KeyEnc: MapKeyEncryption(server.KeyEncryption), } if key, ok := req.TACACSServerKeys[server.Address]; ok { - provider.Key = key + srv.Key = key } if server.Timeout != nil { - provider.Timeout = *server.Timeout + srv.Timeout = *server.Timeout } - conf = append(conf, provider) + conf = append(conf, srv) } // Configure TACACS+ server group @@ -3320,7 +3320,7 @@ func (p *Provider) EnsureAAA(ctx context.Context, req *provider.EnsureAAARequest } // Set realm and provider group based on first method if methods[0].Type == v1alpha1.AAAMethodTypeGroup { - authen.Realm = "tacacs" + authen.Realm = AAARealmTacacs authen.ProviderGroup = methods[0].GroupName } else { authen.Realm = MapRealmFromMethodType(methods[0].Type, "") @@ -3337,7 +3337,7 @@ func (p *Provider) EnsureAAA(ctx context.Context, req *provider.EnsureAAARequest Local: MapLocalFromMethodList(methods), } if methods[0].Type == v1alpha1.AAAMethodTypeGroup { - consoleAuth.Realm = "tacacs" + consoleAuth.Realm = AAARealmTacacs consoleAuth.ProviderGroup = methods[0].GroupName } else { consoleAuth.Realm = MapRealmFromMethodType(methods[0].Type, "") @@ -3353,10 +3353,10 @@ func (p *Provider) EnsureAAA(ctx context.Context, req *provider.EnsureAAARequest author := &AAADefaultAuthor{ Name: "Author", CmdType: "config", - LocalRbac: MapLocalFromMethodList(methods) == "yes", + LocalRbac: MapLocalFromMethodList(methods) == AAAValueYes, } if methods[0].Type == v1alpha1.AAAMethodTypeGroup { - author.Realm = "tacacs" + author.Realm = AAARealmTacacs author.ProviderGroup = methods[0].GroupName } else { author.Realm = MapRealmFromMethodType(methods[0].Type, "") @@ -3370,10 +3370,10 @@ func (p *Provider) EnsureAAA(ctx context.Context, req *provider.EnsureAAARequest methods := req.AAA.Spec.Accounting.Default.Methods acct := &AAADefaultAcc{ Name: "Accounting", - LocalRbac: MapLocalFromMethodList(methods) == "yes", + LocalRbac: MapLocalFromMethodList(methods) == AAAValueYes, } if methods[0].Type == v1alpha1.AAAMethodTypeGroup { - acct.Realm = "tacacs" + acct.Realm = AAARealmTacacs acct.ProviderGroup = methods[0].GroupName } else { acct.Realm = MapRealmFromMethodType(methods[0].Type, "") @@ -3389,7 +3389,7 @@ func (p *Provider) DeleteAAA(ctx context.Context, req *provider.DeleteAAARequest if req.AAA.Spec.Accounting != nil && req.AAA.Spec.Accounting.Default != nil { acct := &AAADefaultAcc{ Name: "Accounting", - Realm: "local", + Realm: AAARealmLocal, LocalRbac: true, } if err := p.Patch(ctx, acct); err != nil { @@ -3402,7 +3402,7 @@ func (p *Provider) DeleteAAA(ctx context.Context, req *provider.DeleteAAARequest author := &AAADefaultAuthor{ Name: "Author", CmdType: "config", - Realm: "local", + Realm: AAARealmLocal, LocalRbac: true, } if err := p.Patch(ctx, author); err != nil { @@ -3413,9 +3413,9 @@ func (p *Provider) DeleteAAA(ctx context.Context, req *provider.DeleteAAARequest // Reset AAA authentication to local if req.AAA.Spec.Authentication != nil { authen := &AAADefaultAuth{ - Realm: "local", - Local: "yes", - Fallback: "yes", + Realm: AAARealmLocal, + Local: AAAValueYes, + Fallback: AAAValueYes, ErrEn: false, } if err := p.Patch(ctx, authen); err != nil { @@ -3424,9 +3424,9 @@ func (p *Provider) DeleteAAA(ctx context.Context, req *provider.DeleteAAARequest if req.AAA.Spec.Authentication.Login != nil && req.AAA.Spec.Authentication.Login.Console != nil { consoleAuth := &AAAConsoleAuth{ - Realm: "local", - Local: "yes", - Fallback: "yes", + Realm: AAARealmLocal, + Local: AAAValueYes, + Fallback: AAAValueYes, ErrEn: false, } if err := p.Patch(ctx, consoleAuth); err != nil { @@ -3445,8 +3445,8 @@ func (p *Provider) DeleteAAA(ctx context.Context, req *provider.DeleteAAARequest // Delete TACACS+ server hosts for _, server := range req.AAA.Spec.TACACSServers { - provider := &TacacsPlusProvider{Name: server.Address} - if err := p.client.Delete(ctx, provider); err != nil { + srv := &TacacsPlusProvider{Name: server.Address} + if err := p.client.Delete(ctx, srv); err != nil { return err } } From bd190c39e837598327a08eac587e15fdae54c86f Mon Sep 17 00:00:00 2001 From: i521907 Date: Tue, 17 Feb 2026 18:35:43 -0500 Subject: [PATCH 03/32] Restructure AAA API to OpenConfig system/aaa model Signed-off-by: i521907 --- api/cisco/nx/v1alpha1/aaaconfig_types.go | 101 ++++ .../nx/v1alpha1/zz_generated.deepcopy.go | 121 +++++ api/core/v1alpha1/aaa_types.go | 195 ++++--- api/core/v1alpha1/zz_generated.deepcopy.go | 183 +++---- .../networking.metal.ironcore.dev_aaas.yaml | 508 ++++++++---------- ...working.metal.ironcore.dev_aaaconfigs.yaml | 136 +++++ config/default/kustomization.yaml | 2 +- config/rbac/role.yaml | 1 + config/samples/networking_v1alpha1_aaa.yaml | 141 +++-- internal/controller/core/aaa_controller.go | 42 +- internal/provider/cisco/nxos/aaa.go | 57 +- internal/provider/cisco/nxos/provider.go | 266 ++++----- 12 files changed, 1036 insertions(+), 717 deletions(-) create mode 100644 api/cisco/nx/v1alpha1/aaaconfig_types.go create mode 100644 config/crd/bases/nx.cisco.networking.metal.ironcore.dev_aaaconfigs.yaml diff --git a/api/cisco/nx/v1alpha1/aaaconfig_types.go b/api/cisco/nx/v1alpha1/aaaconfig_types.go new file mode 100644 index 000000000..160da1e62 --- /dev/null +++ b/api/cisco/nx/v1alpha1/aaaconfig_types.go @@ -0,0 +1,101 @@ +// SPDX-FileCopyrightText: 2025 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" + + "github.com/ironcore-dev/network-operator/api/core/v1alpha1" +) + +// +kubebuilder:rbac:groups=nx.cisco.networking.metal.ironcore.dev,resources=aaaconfigs,verbs=get;list;watch + +// AAAConfigSpec defines the desired state of AAAConfig +type AAAConfigSpec struct { + // LoginErrorEnable enables login error messages (NX-OS specific). + // Maps to: aaa authentication login error-enable + // +optional + LoginErrorEnable bool `json:"loginErrorEnable,omitempty"` + + // KeyEncryption specifies the default encryption type for TACACS+ keys. + // +kubebuilder:validation:Enum=Type6;Type7;Clear + // +kubebuilder:default=Type7 + KeyEncryption TACACSKeyEncryption `json:"keyEncryption,omitempty"` + + // ConsoleAuthentication defines NX-OS console-specific authentication methods. + // Maps to: aaa authentication login console + // +optional + ConsoleAuthentication *NXOSMethodList `json:"consoleAuthentication,omitempty"` + + // ConfigCommandsAuthorization defines NX-OS config-commands authorization methods. + // Maps to: aaa authorization config-commands default + // +optional + ConfigCommandsAuthorization *NXOSMethodList `json:"configCommandsAuthorization,omitempty"` +} + +// TACACSKeyEncryption defines the encryption type for TACACS+ server keys. +// +kubebuilder:validation:Enum=Type6;Type7;Clear +type TACACSKeyEncryption string + +const ( + // TACACSKeyEncryptionType6 uses AES encryption (more secure). + TACACSKeyEncryptionType6 TACACSKeyEncryption = "Type6" + // TACACSKeyEncryptionType7 uses Cisco Type 7 encryption (reversible). + TACACSKeyEncryptionType7 TACACSKeyEncryption = "Type7" + // TACACSKeyEncryptionClear sends the key in cleartext. + TACACSKeyEncryptionClear TACACSKeyEncryption = "Clear" +) + +// NXOSMethodList defines an ordered list of AAA methods for NX-OS specific contexts. +type NXOSMethodList struct { + // Methods is the ordered list of methods. + // +required + // +listType=atomic + // +kubebuilder:validation:MinItems=1 + // +kubebuilder:validation:MaxItems=4 + Methods []NXOSMethod `json:"methods"` +} + +// NXOSMethod represents a single AAA method in an NX-OS context. +type NXOSMethod struct { + // Type is the method type. + // +required + // +kubebuilder:validation:Enum=Group;Local;None + Type string `json:"type"` + + // GroupName is the server group name when Type is Group. + // +optional + // +kubebuilder:validation:MaxLength=63 + GroupName string `json:"groupName,omitempty"` +} + +// +kubebuilder:object:root=true +// +kubebuilder:resource:path=aaaconfigs +// +kubebuilder:resource:singular=aaaconfig +// +kubebuilder:resource:shortName=nxaaa + +// AAAConfig is the Schema for the aaaconfigs API +type AAAConfig struct { + metav1.TypeMeta `json:",inline"` + metav1.ObjectMeta `json:"metadata,omitempty"` + + // Specification of the desired state of the resource. + // More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#spec-and-status + // +required + Spec AAAConfigSpec `json:"spec"` +} + +// +kubebuilder:object:root=true + +// AAAConfigList contains a list of AAAConfig +type AAAConfigList struct { + metav1.TypeMeta `json:",inline"` + metav1.ListMeta `json:"metadata,omitempty"` + Items []AAAConfig `json:"items"` +} + +func init() { + v1alpha1.RegisterAAADependency(GroupVersion.WithKind("AAAConfig")) + SchemeBuilder.Register(&AAAConfig{}, &AAAConfigList{}) +} diff --git a/api/cisco/nx/v1alpha1/zz_generated.deepcopy.go b/api/cisco/nx/v1alpha1/zz_generated.deepcopy.go index 788d481e4..9c9a3b584 100644 --- a/api/cisco/nx/v1alpha1/zz_generated.deepcopy.go +++ b/api/cisco/nx/v1alpha1/zz_generated.deepcopy.go @@ -1,5 +1,8 @@ //go:build !ignore_autogenerated +// SPDX-FileCopyrightText: 2025 SAP SE or an SAP affiliate company and IronCore contributors +// SPDX-License-Identifier: Apache-2.0 + // Code generated by controller-gen. DO NOT EDIT. package v1alpha1 @@ -10,6 +13,89 @@ import ( runtime "k8s.io/apimachinery/pkg/runtime" ) +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *AAAConfig) DeepCopyInto(out *AAAConfig) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) + in.Spec.DeepCopyInto(&out.Spec) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AAAConfig. +func (in *AAAConfig) DeepCopy() *AAAConfig { + if in == nil { + return nil + } + out := new(AAAConfig) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *AAAConfig) 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 *AAAConfigList) DeepCopyInto(out *AAAConfigList) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ListMeta.DeepCopyInto(&out.ListMeta) + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]AAAConfig, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AAAConfigList. +func (in *AAAConfigList) DeepCopy() *AAAConfigList { + if in == nil { + return nil + } + out := new(AAAConfigList) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *AAAConfigList) 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 *AAAConfigSpec) DeepCopyInto(out *AAAConfigSpec) { + *out = *in + if in.ConsoleAuthentication != nil { + in, out := &in.ConsoleAuthentication, &out.ConsoleAuthentication + *out = new(NXOSMethodList) + (*in).DeepCopyInto(*out) + } + if in.ConfigCommandsAuthorization != nil { + in, out := &in.ConfigCommandsAuthorization, &out.ConfigCommandsAuthorization + *out = new(NXOSMethodList) + (*in).DeepCopyInto(*out) + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AAAConfigSpec. +func (in *AAAConfigSpec) DeepCopy() *AAAConfigSpec { + if in == nil { + return nil + } + out := new(AAAConfigSpec) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *AutoRecovery) DeepCopyInto(out *AutoRecovery) { *out = *in @@ -641,6 +727,41 @@ func (in *ManagementAccessConfigSpec) DeepCopy() *ManagementAccessConfigSpec { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *NXOSMethod) DeepCopyInto(out *NXOSMethod) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new NXOSMethod. +func (in *NXOSMethod) DeepCopy() *NXOSMethod { + if in == nil { + return nil + } + out := new(NXOSMethod) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *NXOSMethodList) DeepCopyInto(out *NXOSMethodList) { + *out = *in + if in.Methods != nil { + in, out := &in.Methods, &out.Methods + *out = make([]NXOSMethod, len(*in)) + copy(*out, *in) + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new NXOSMethodList. +func (in *NXOSMethodList) DeepCopy() *NXOSMethodList { + if in == nil { + return nil + } + out := new(NXOSMethodList) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *NetworkVirtualizationEdgeConfig) DeepCopyInto(out *NetworkVirtualizationEdgeConfig) { *out = *in diff --git a/api/core/v1alpha1/aaa_types.go b/api/core/v1alpha1/aaa_types.go index 6a46e4447..7fa0e50a2 100644 --- a/api/core/v1alpha1/aaa_types.go +++ b/api/core/v1alpha1/aaa_types.go @@ -10,10 +10,11 @@ import ( "k8s.io/apimachinery/pkg/runtime/schema" ) -// AAASpec defines the desired state of AAA +// AAASpec defines the desired state of AAA. // // It models the Authentication, Authorization, and Accounting (AAA) configuration on a network device, -// including TACACS+ server configuration and AAA group/method settings. +// aligned with the OpenConfig system/aaa YANG model. +// +kubebuilder:validation:XValidation:rule="has(self.serverGroups) || has(self.authentication) || has(self.authorization) || has(self.accounting)",message="at least one of serverGroups, authentication, authorization, or accounting must be set" type AAASpec struct { // DeviceName is the name of the Device this object belongs to. The Device object must exist in the same namespace. // Immutable. @@ -26,153 +27,141 @@ type AAASpec struct { // +optional ProviderConfigRef *TypedLocalObjectReference `json:"providerConfigRef,omitempty"` - // TACACSServers is the list of TACACS+ servers to configure. + // ServerGroups is the list of AAA server groups. + // OpenConfig: /system/aaa/server-groups/server-group // +optional // +listType=map - // +listMapKey=address - // +kubebuilder:validation:MaxItems=16 - TACACSServers []TACACSServer `json:"tacacsServers,omitempty"` + // +listMapKey=name + // +kubebuilder:validation:MaxItems=8 + ServerGroups []AAAServerGroup `json:"serverGroups,omitempty"` - // TACACSGroup is the TACACS+ server group configuration. - // +optional - TACACSGroup *TACACSGroup `json:"tacacsGroup,omitempty"` - - // Authentication defines the AAA authentication configuration. + // Authentication defines the AAA authentication method list. + // OpenConfig: /system/aaa/authentication // +optional Authentication *AAAAuthentication `json:"authentication,omitempty"` - // Authorization defines the AAA authorization configuration. + // Authorization defines the AAA authorization method list. + // OpenConfig: /system/aaa/authorization // +optional Authorization *AAAAuthorization `json:"authorization,omitempty"` - // Accounting defines the AAA accounting configuration. + // Accounting defines the AAA accounting method list. + // OpenConfig: /system/aaa/accounting // +optional Accounting *AAAAccounting `json:"accounting,omitempty"` } -// TACACSServer represents a TACACS+ server configuration. -type TACACSServer struct { - // Address is the IP address or hostname of the TACACS+ server. - // +required - // +kubebuilder:validation:MinLength=1 - // +kubebuilder:validation:MaxLength=253 - Address string `json:"address"` - - // Port is the TCP port of the TACACS+ server. - // Defaults to 49 if not specified. - // +optional - // +kubebuilder:validation:Minimum=1 - // +kubebuilder:validation:Maximum=65535 - // +kubebuilder:default=49 - Port int32 `json:"port,omitempty"` - - // KeySecretRef is a reference to a secret containing the shared key for this TACACS+ server. - // The secret must contain a key specified in the SecretKeySelector. - // +required - KeySecretRef SecretKeySelector `json:"keySecretRef"` - - // KeyEncryption specifies the encryption type for the key. - // Type7 is the Cisco Type 7 encryption (reversible). - // Type6 is the AES encryption (more secure). - // Clear means the key is sent in cleartext (not recommended). - // +optional - // +kubebuilder:validation:Enum=Type6;Type7;Clear - // +kubebuilder:default=Type7 - KeyEncryption TACACSKeyEncryption `json:"keyEncryption,omitempty"` - - // Timeout is the timeout in seconds for this TACACS+ server. - // +optional - // +kubebuilder:validation:Minimum=1 - // +kubebuilder:validation:Maximum=60 - Timeout *int32 `json:"timeout,omitempty"` -} - -// TACACSKeyEncryption defines the encryption type for TACACS+ server keys. -// +kubebuilder:validation:Enum=Type6;Type7;Clear -type TACACSKeyEncryption string +// AAAServerGroupType defines the protocol type of an AAA server group. +// +kubebuilder:validation:Enum=TACACS;RADIUS +type AAAServerGroupType string const ( - // TACACSKeyEncryptionType6 uses AES encryption (more secure). - TACACSKeyEncryptionType6 TACACSKeyEncryption = "Type6" - // TACACSKeyEncryptionType7 uses Cisco Type 7 encryption (reversible). - TACACSKeyEncryptionType7 TACACSKeyEncryption = "Type7" - // TACACSKeyEncryptionClear sends the key in cleartext. - TACACSKeyEncryptionClear TACACSKeyEncryption = "Clear" + // AAAServerGroupTypeTACACS is a TACACS+ server group. + AAAServerGroupTypeTACACS AAAServerGroupType = "TACACS" + // AAAServerGroupTypeRADIUS is a RADIUS server group. + AAAServerGroupTypeRADIUS AAAServerGroupType = "RADIUS" ) -// TACACSGroup represents a TACACS+ server group configuration. -type TACACSGroup struct { - // Name is the name of the TACACS+ server group. +// AAAServerGroup represents a named group of AAA servers. +// OpenConfig: /system/aaa/server-groups/server-group[name] +// +kubebuilder:validation:XValidation:rule="self.type != 'TACACS' || self.servers.all(s, has(s.tacacs))",message="servers in a TACACS group must have tacacs config" +type AAAServerGroup struct { + // Name is the name of the server group. // +required // +kubebuilder:validation:MinLength=1 // +kubebuilder:validation:MaxLength=63 Name string `json:"name"` - // Servers is the list of TACACS+ server addresses to include in this group. - // The addresses must match addresses defined in TACACSServers. + // Type is the protocol type of this server group. // +required - // +listType=set + Type AAAServerGroupType `json:"type"` + + // Servers is the list of servers in this group. + // OpenConfig: /system/aaa/server-groups/server-group/servers/server + // +required + // +listType=map + // +listMapKey=address // +kubebuilder:validation:MinItems=1 // +kubebuilder:validation:MaxItems=16 - Servers []string `json:"servers"` + Servers []AAAServer `json:"servers"` - // VRF is the VRF to use for communication with the TACACS+ servers. + // VrfName is the VRF to use for communication with the servers in this group. // +optional // +kubebuilder:validation:MaxLength=63 - VRF string `json:"vrf,omitempty"` + VrfName string `json:"vrfName,omitempty"` - // SourceInterface is the source interface to use for communication with the TACACS+ servers. + // SourceInterfaceName is the source interface to use for communication with the servers. // +optional // +kubebuilder:validation:MaxLength=63 - SourceInterface string `json:"sourceInterface,omitempty"` + SourceInterfaceName string `json:"sourceInterfaceName,omitempty"` } -// AAAAuthentication defines the AAA authentication configuration. -type AAAAuthentication struct { - // Login defines authentication methods for login. +// AAAServer represents a single AAA server within a group. +// OpenConfig: /system/aaa/server-groups/server-group/servers/server[address] +type AAAServer struct { + // Address is the IP address or hostname of the server. + // +required + // +kubebuilder:validation:MinLength=1 + // +kubebuilder:validation:MaxLength=253 + Address string `json:"address"` + + // Timeout is the response timeout in seconds for this server. // +optional - Login *AAAAuthenticationLogin `json:"login,omitempty"` + // +kubebuilder:validation:Minimum=1 + // +kubebuilder:validation:Maximum=60 + Timeout *int32 `json:"timeout,omitempty"` - // LoginErrorEnable enables login error messages. + // TACACS contains TACACS+ specific server configuration. + // Required when the parent server group type is TACACS. + // OpenConfig augmentation: /system/aaa/server-groups/server-group/servers/server/tacacs // +optional - LoginErrorEnable bool `json:"loginErrorEnable,omitempty"` + TACACS *AAAServerTACACS `json:"tacacs,omitempty"` } -// AAAAuthenticationLogin defines the login authentication methods. -type AAAAuthenticationLogin struct { - // Default defines the default authentication method list. +// AAAServerTACACS contains TACACS+ specific server configuration. +type AAAServerTACACS struct { + // Port is the TCP port of the TACACS+ server. + // Defaults to 49 if not specified. // +optional - Default *AAAMethodList `json:"default,omitempty"` + // +kubebuilder:validation:Minimum=1 + // +kubebuilder:validation:Maximum=65535 + // +kubebuilder:default=49 + Port int32 `json:"port,omitempty"` - // Console defines the console authentication method list. - // +optional - Console *AAAMethodList `json:"console,omitempty"` + // KeySecretRef is a reference to a secret containing the shared key for this TACACS+ server. + // The secret must contain a key specified in the SecretKeySelector. + // +required + KeySecretRef SecretKeySelector `json:"keySecretRef"` } -// AAAAuthorization defines the AAA authorization configuration. -type AAAAuthorization struct { - // ConfigCommands defines authorization for configuration commands. - // +optional - ConfigCommands *AAAAuthorizationConfigCommands `json:"configCommands,omitempty"` +// AAAAuthentication defines the AAA authentication method list. +// OpenConfig: /system/aaa/authentication +type AAAAuthentication struct { + // Methods is the ordered list of authentication methods. + // Methods are tried in order until one succeeds or all fail. + // +required + // +listType=atomic + // +kubebuilder:validation:MinItems=1 + // +kubebuilder:validation:MaxItems=4 + Methods []AAAMethod `json:"methods"` } -// AAAAuthorizationConfigCommands defines authorization for configuration commands. -type AAAAuthorizationConfigCommands struct { - // Default defines the default authorization method list. - // +optional - Default *AAAMethodList `json:"default,omitempty"` +// AAAAuthorization defines the AAA authorization method list. +// OpenConfig: /system/aaa/authorization +type AAAAuthorization struct { + // Methods is the ordered list of authorization methods. + // Methods are tried in order until one succeeds or all fail. + // +required + // +listType=atomic + // +kubebuilder:validation:MinItems=1 + // +kubebuilder:validation:MaxItems=4 + Methods []AAAMethod `json:"methods"` } -// AAAAccounting defines the AAA accounting configuration. +// AAAAccounting defines the AAA accounting method list. +// OpenConfig: /system/aaa/accounting type AAAAccounting struct { - // Default defines the default accounting method list. - // +optional - Default *AAAMethodList `json:"default,omitempty"` -} - -// AAAMethodList defines a list of AAA methods to try in order. -type AAAMethodList struct { - // Methods is the ordered list of authentication/authorization/accounting methods. + // Methods is the ordered list of accounting methods. // Methods are tried in order until one succeeds or all fail. // +required // +listType=atomic @@ -182,6 +171,7 @@ type AAAMethodList struct { } // AAAMethod represents an AAA method. +// +kubebuilder:validation:XValidation:rule="self.type != 'Group' || self.groupName != \"\"",message="groupName is required when type is Group" type AAAMethod struct { // Type is the type of AAA method. // +required @@ -224,7 +214,6 @@ type AAAStatus struct { // +kubebuilder:resource:singular=aaa // +kubebuilder:resource:shortName=aaa // +kubebuilder:printcolumn:name="Device",type=string,JSONPath=`.spec.deviceRef.name` -// +kubebuilder:printcolumn:name="TACACS Group",type=string,JSONPath=`.spec.tacacsGroup.name` // +kubebuilder:printcolumn:name="Ready",type=string,JSONPath=`.status.conditions[?(@.type=="Ready")].status` // +kubebuilder:printcolumn:name="Age",type="date",JSONPath=".metadata.creationTimestamp" diff --git a/api/core/v1alpha1/zz_generated.deepcopy.go b/api/core/v1alpha1/zz_generated.deepcopy.go index 1861ed01b..d30b52074 100644 --- a/api/core/v1alpha1/zz_generated.deepcopy.go +++ b/api/core/v1alpha1/zz_generated.deepcopy.go @@ -1,5 +1,8 @@ //go:build !ignore_autogenerated +// SPDX-FileCopyrightText: 2025 SAP SE or an SAP affiliate company and IronCore contributors +// SPDX-License-Identifier: Apache-2.0 + // Code generated by controller-gen. DO NOT EDIT. package v1alpha1 @@ -40,10 +43,10 @@ func (in *AAA) DeepCopyObject() runtime.Object { // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *AAAAccounting) DeepCopyInto(out *AAAAccounting) { *out = *in - if in.Default != nil { - in, out := &in.Default, &out.Default - *out = new(AAAMethodList) - (*in).DeepCopyInto(*out) + if in.Methods != nil { + in, out := &in.Methods, &out.Methods + *out = make([]AAAMethod, len(*in)) + copy(*out, *in) } } @@ -60,10 +63,10 @@ func (in *AAAAccounting) DeepCopy() *AAAAccounting { // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *AAAAuthentication) DeepCopyInto(out *AAAAuthentication) { *out = *in - if in.Login != nil { - in, out := &in.Login, &out.Login - *out = new(AAAAuthenticationLogin) - (*in).DeepCopyInto(*out) + if in.Methods != nil { + in, out := &in.Methods, &out.Methods + *out = make([]AAAMethod, len(*in)) + copy(*out, *in) } } @@ -77,38 +80,13 @@ func (in *AAAAuthentication) DeepCopy() *AAAAuthentication { return out } -// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *AAAAuthenticationLogin) DeepCopyInto(out *AAAAuthenticationLogin) { - *out = *in - if in.Default != nil { - in, out := &in.Default, &out.Default - *out = new(AAAMethodList) - (*in).DeepCopyInto(*out) - } - if in.Console != nil { - in, out := &in.Console, &out.Console - *out = new(AAAMethodList) - (*in).DeepCopyInto(*out) - } -} - -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AAAAuthenticationLogin. -func (in *AAAAuthenticationLogin) DeepCopy() *AAAAuthenticationLogin { - if in == nil { - return nil - } - out := new(AAAAuthenticationLogin) - in.DeepCopyInto(out) - return out -} - // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *AAAAuthorization) DeepCopyInto(out *AAAAuthorization) { *out = *in - if in.ConfigCommands != nil { - in, out := &in.ConfigCommands, &out.ConfigCommands - *out = new(AAAAuthorizationConfigCommands) - (*in).DeepCopyInto(*out) + if in.Methods != nil { + in, out := &in.Methods, &out.Methods + *out = make([]AAAMethod, len(*in)) + copy(*out, *in) } } @@ -122,26 +100,6 @@ func (in *AAAAuthorization) DeepCopy() *AAAAuthorization { return out } -// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *AAAAuthorizationConfigCommands) DeepCopyInto(out *AAAAuthorizationConfigCommands) { - *out = *in - if in.Default != nil { - in, out := &in.Default, &out.Default - *out = new(AAAMethodList) - (*in).DeepCopyInto(*out) - } -} - -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AAAAuthorizationConfigCommands. -func (in *AAAAuthorizationConfigCommands) DeepCopy() *AAAAuthorizationConfigCommands { - if in == nil { - return nil - } - out := new(AAAAuthorizationConfigCommands) - in.DeepCopyInto(out) - return out -} - // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *AAAList) DeepCopyInto(out *AAAList) { *out = *in @@ -190,21 +148,64 @@ func (in *AAAMethod) DeepCopy() *AAAMethod { } // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *AAAMethodList) DeepCopyInto(out *AAAMethodList) { +func (in *AAAServer) DeepCopyInto(out *AAAServer) { *out = *in - if in.Methods != nil { - in, out := &in.Methods, &out.Methods - *out = make([]AAAMethod, len(*in)) - copy(*out, *in) + if in.Timeout != nil { + in, out := &in.Timeout, &out.Timeout + *out = new(int32) + **out = **in + } + if in.TACACS != nil { + in, out := &in.TACACS, &out.TACACS + *out = new(AAAServerTACACS) + **out = **in + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AAAServer. +func (in *AAAServer) DeepCopy() *AAAServer { + if in == nil { + return nil } + out := new(AAAServer) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *AAAServerGroup) DeepCopyInto(out *AAAServerGroup) { + *out = *in + if in.Servers != nil { + in, out := &in.Servers, &out.Servers + *out = make([]AAAServer, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AAAServerGroup. +func (in *AAAServerGroup) DeepCopy() *AAAServerGroup { + if in == nil { + return nil + } + out := new(AAAServerGroup) + in.DeepCopyInto(out) + return out } -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AAAMethodList. -func (in *AAAMethodList) DeepCopy() *AAAMethodList { +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *AAAServerTACACS) DeepCopyInto(out *AAAServerTACACS) { + *out = *in + out.KeySecretRef = in.KeySecretRef +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AAAServerTACACS. +func (in *AAAServerTACACS) DeepCopy() *AAAServerTACACS { if in == nil { return nil } - out := new(AAAMethodList) + out := new(AAAServerTACACS) in.DeepCopyInto(out) return out } @@ -218,18 +219,13 @@ func (in *AAASpec) DeepCopyInto(out *AAASpec) { *out = new(TypedLocalObjectReference) **out = **in } - if in.TACACSServers != nil { - in, out := &in.TACACSServers, &out.TACACSServers - *out = make([]TACACSServer, len(*in)) + if in.ServerGroups != nil { + in, out := &in.ServerGroups, &out.ServerGroups + *out = make([]AAAServerGroup, len(*in)) for i := range *in { (*in)[i].DeepCopyInto(&(*out)[i]) } } - if in.TACACSGroup != nil { - in, out := &in.TACACSGroup, &out.TACACSGroup - *out = new(TACACSGroup) - (*in).DeepCopyInto(*out) - } if in.Authentication != nil { in, out := &in.Authentication, &out.Authentication *out = new(AAAAuthentication) @@ -3953,47 +3949,6 @@ func (in *SyslogStatus) DeepCopy() *SyslogStatus { return out } -// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *TACACSGroup) DeepCopyInto(out *TACACSGroup) { - *out = *in - if in.Servers != nil { - in, out := &in.Servers, &out.Servers - *out = make([]string, len(*in)) - copy(*out, *in) - } -} - -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new TACACSGroup. -func (in *TACACSGroup) DeepCopy() *TACACSGroup { - if in == nil { - return nil - } - out := new(TACACSGroup) - in.DeepCopyInto(out) - return out -} - -// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *TACACSServer) DeepCopyInto(out *TACACSServer) { - *out = *in - out.KeySecretRef = in.KeySecretRef - if in.Timeout != nil { - in, out := &in.Timeout, &out.Timeout - *out = new(int32) - **out = **in - } -} - -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new TACACSServer. -func (in *TACACSServer) DeepCopy() *TACACSServer { - if in == nil { - return nil - } - out := new(TACACSServer) - in.DeepCopyInto(out) - return out -} - // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *TLS) DeepCopyInto(out *TLS) { *out = *in diff --git a/config/crd/bases/networking.metal.ironcore.dev_aaas.yaml b/config/crd/bases/networking.metal.ironcore.dev_aaas.yaml index f2813fd6f..ced5f156c 100644 --- a/config/crd/bases/networking.metal.ironcore.dev_aaas.yaml +++ b/config/crd/bases/networking.metal.ironcore.dev_aaas.yaml @@ -3,7 +3,7 @@ apiVersion: apiextensions.k8s.io/v1 kind: CustomResourceDefinition metadata: annotations: - controller-gen.kubebuilder.io/version: v0.20.0 + controller-gen.kubebuilder.io/version: v0.20.1 name: aaas.networking.metal.ironcore.dev spec: group: networking.metal.ironcore.dev @@ -20,9 +20,6 @@ spec: - jsonPath: .spec.deviceRef.name name: Device type: string - - jsonPath: .spec.tacacsGroup.name - name: TACACS Group - type: string - jsonPath: .status.conditions[?(@.type=="Ready")].status name: Ready type: string @@ -57,179 +54,130 @@ spec: More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#spec-and-status properties: accounting: - description: Accounting defines the AAA accounting configuration. + description: |- + Accounting defines the AAA accounting method list. + OpenConfig: /system/aaa/accounting properties: - default: - description: Default defines the default accounting method list. - properties: - methods: - description: |- - Methods is the ordered list of authentication/authorization/accounting methods. - Methods are tried in order until one succeeds or all fail. - items: - description: AAAMethod represents an AAA method. - properties: - groupName: - description: GroupName is the name of the server group - when Type is Group. - maxLength: 63 - type: string - type: - allOf: - - enum: - - Group - - Local - - None - - enum: - - Group - - Local - - None - description: Type is the type of AAA method. - type: string - required: - - type - type: object - maxItems: 4 - minItems: 1 - type: array - x-kubernetes-list-type: atomic - required: - - methods - type: object + methods: + description: |- + Methods is the ordered list of accounting methods. + Methods are tried in order until one succeeds or all fail. + items: + description: AAAMethod represents an AAA method. + properties: + groupName: + description: GroupName is the name of the server group when + Type is Group. + maxLength: 63 + type: string + type: + allOf: + - enum: + - Group + - Local + - None + - enum: + - Group + - Local + - None + description: Type is the type of AAA method. + type: string + required: + - type + type: object + x-kubernetes-validations: + - message: groupName is required when type is Group + rule: self.type != 'Group' || self.groupName != "" + maxItems: 4 + minItems: 1 + type: array + x-kubernetes-list-type: atomic + required: + - methods type: object authentication: - description: Authentication defines the AAA authentication configuration. + description: |- + Authentication defines the AAA authentication method list. + OpenConfig: /system/aaa/authentication properties: - login: - description: Login defines authentication methods for login. - properties: - console: - description: Console defines the console authentication method - list. - properties: - methods: - description: |- - Methods is the ordered list of authentication/authorization/accounting methods. - Methods are tried in order until one succeeds or all fail. - items: - description: AAAMethod represents an AAA method. - properties: - groupName: - description: GroupName is the name of the server - group when Type is Group. - maxLength: 63 - type: string - type: - allOf: - - enum: - - Group - - Local - - None - - enum: - - Group - - Local - - None - description: Type is the type of AAA method. - type: string - required: - - type - type: object - maxItems: 4 - minItems: 1 - type: array - x-kubernetes-list-type: atomic - required: - - methods - type: object - default: - description: Default defines the default authentication method - list. - properties: - methods: - description: |- - Methods is the ordered list of authentication/authorization/accounting methods. - Methods are tried in order until one succeeds or all fail. - items: - description: AAAMethod represents an AAA method. - properties: - groupName: - description: GroupName is the name of the server - group when Type is Group. - maxLength: 63 - type: string - type: - allOf: - - enum: - - Group - - Local - - None - - enum: - - Group - - Local - - None - description: Type is the type of AAA method. - type: string - required: - - type - type: object - maxItems: 4 - minItems: 1 - type: array - x-kubernetes-list-type: atomic - required: - - methods - type: object - type: object - loginErrorEnable: - description: LoginErrorEnable enables login error messages. - type: boolean + methods: + description: |- + Methods is the ordered list of authentication methods. + Methods are tried in order until one succeeds or all fail. + items: + description: AAAMethod represents an AAA method. + properties: + groupName: + description: GroupName is the name of the server group when + Type is Group. + maxLength: 63 + type: string + type: + allOf: + - enum: + - Group + - Local + - None + - enum: + - Group + - Local + - None + description: Type is the type of AAA method. + type: string + required: + - type + type: object + x-kubernetes-validations: + - message: groupName is required when type is Group + rule: self.type != 'Group' || self.groupName != "" + maxItems: 4 + minItems: 1 + type: array + x-kubernetes-list-type: atomic + required: + - methods type: object authorization: - description: Authorization defines the AAA authorization configuration. + description: |- + Authorization defines the AAA authorization method list. + OpenConfig: /system/aaa/authorization properties: - configCommands: - description: ConfigCommands defines authorization for configuration - commands. - properties: - default: - description: Default defines the default authorization method - list. - properties: - methods: - description: |- - Methods is the ordered list of authentication/authorization/accounting methods. - Methods are tried in order until one succeeds or all fail. - items: - description: AAAMethod represents an AAA method. - properties: - groupName: - description: GroupName is the name of the server - group when Type is Group. - maxLength: 63 - type: string - type: - allOf: - - enum: - - Group - - Local - - None - - enum: - - Group - - Local - - None - description: Type is the type of AAA method. - type: string - required: - - type - type: object - maxItems: 4 - minItems: 1 - type: array - x-kubernetes-list-type: atomic - required: - - methods - type: object - type: object + methods: + description: |- + Methods is the ordered list of authorization methods. + Methods are tried in order until one succeeds or all fail. + items: + description: AAAMethod represents an AAA method. + properties: + groupName: + description: GroupName is the name of the server group when + Type is Group. + maxLength: 63 + type: string + type: + allOf: + - enum: + - Group + - Local + - None + - enum: + - Group + - Local + - None + description: Type is the type of AAA method. + type: string + required: + - type + type: object + x-kubernetes-validations: + - message: groupName is required when type is Group + rule: self.type != 'Group' || self.groupName != "" + maxItems: 4 + minItems: 1 + type: array + x-kubernetes-list-type: atomic + required: + - methods type: object deviceRef: description: |- @@ -284,124 +232,136 @@ spec: - name type: object x-kubernetes-map-type: atomic - tacacsGroup: - description: TACACSGroup is the TACACS+ server group configuration. - properties: - name: - description: Name is the name of the TACACS+ server group. - maxLength: 63 - minLength: 1 - type: string - servers: - description: |- - Servers is the list of TACACS+ server addresses to include in this group. - The addresses must match addresses defined in TACACSServers. - items: - type: string - maxItems: 16 - minItems: 1 - type: array - x-kubernetes-list-type: set - sourceInterface: - description: SourceInterface is the source interface to use for - communication with the TACACS+ servers. - maxLength: 63 - type: string - vrf: - description: VRF is the VRF to use for communication with the - TACACS+ servers. - maxLength: 63 - type: string - required: - - name - - servers - type: object - tacacsServers: - description: TACACSServers is the list of TACACS+ servers to configure. + serverGroups: + description: |- + ServerGroups is the list of AAA server groups. + OpenConfig: /system/aaa/server-groups/server-group items: - description: TACACSServer represents a TACACS+ server configuration. + description: |- + AAAServerGroup represents a named group of AAA servers. + OpenConfig: /system/aaa/server-groups/server-group[name] properties: - address: - description: Address is the IP address or hostname of the TACACS+ - server. - maxLength: 253 + name: + description: Name is the name of the server group. + maxLength: 63 minLength: 1 type: string - keyEncryption: - allOf: - - enum: - - Type6 - - Type7 - - Clear - - enum: - - Type6 - - Type7 - - Clear - default: Type7 + servers: description: |- - KeyEncryption specifies the encryption type for the key. - Type7 is the Cisco Type 7 encryption (reversible). - Type6 is the AES encryption (more secure). - Clear means the key is sent in cleartext (not recommended). + Servers is the list of servers in this group. + OpenConfig: /system/aaa/server-groups/server-group/servers/server + items: + description: |- + AAAServer represents a single AAA server within a group. + OpenConfig: /system/aaa/server-groups/server-group/servers/server[address] + properties: + address: + description: Address is the IP address or hostname of + the server. + maxLength: 253 + minLength: 1 + type: string + tacacs: + description: |- + TACACS contains TACACS+ specific server configuration. + Required when the parent server group type is TACACS. + OpenConfig augmentation: /system/aaa/server-groups/server-group/servers/server/tacacs + properties: + keySecretRef: + description: |- + KeySecretRef is a reference to a secret containing the shared key for this TACACS+ server. + The secret must contain a key specified in the SecretKeySelector. + properties: + key: + description: |- + Key is the of the entry in the secret resource's `data` or `stringData` + field to be used. + maxLength: 253 + minLength: 1 + type: string + name: + description: Name is unique within a namespace + to reference a secret resource. + maxLength: 253 + minLength: 1 + type: string + namespace: + description: |- + Namespace defines the space within which the secret name must be unique. + If omitted, the namespace of the object being reconciled will be used. + maxLength: 63 + minLength: 1 + type: string + required: + - key + - name + type: object + x-kubernetes-map-type: atomic + port: + default: 49 + description: |- + Port is the TCP port of the TACACS+ server. + Defaults to 49 if not specified. + format: int32 + maximum: 65535 + minimum: 1 + type: integer + required: + - keySecretRef + type: object + timeout: + description: Timeout is the response timeout in seconds + for this server. + format: int32 + maximum: 60 + minimum: 1 + type: integer + required: + - address + type: object + maxItems: 16 + minItems: 1 + type: array + x-kubernetes-list-map-keys: + - address + x-kubernetes-list-type: map + sourceInterfaceName: + description: SourceInterfaceName is the source interface to + use for communication with the servers. + maxLength: 63 + type: string + type: + description: Type is the protocol type of this server group. + enum: + - TACACS + - RADIUS + type: string + vrfName: + description: VrfName is the VRF to use for communication with + the servers in this group. + maxLength: 63 type: string - keySecretRef: - description: |- - KeySecretRef is a reference to a secret containing the shared key for this TACACS+ server. - The secret must contain a key specified in the SecretKeySelector. - properties: - key: - description: |- - Key is the of the entry in the secret resource's `data` or `stringData` - field to be used. - maxLength: 253 - minLength: 1 - type: string - name: - description: Name is unique within a namespace to reference - a secret resource. - maxLength: 253 - minLength: 1 - type: string - namespace: - description: |- - Namespace defines the space within which the secret name must be unique. - If omitted, the namespace of the object being reconciled will be used. - maxLength: 63 - minLength: 1 - type: string - required: - - key - - name - type: object - x-kubernetes-map-type: atomic - port: - default: 49 - description: |- - Port is the TCP port of the TACACS+ server. - Defaults to 49 if not specified. - format: int32 - maximum: 65535 - minimum: 1 - type: integer - timeout: - description: Timeout is the timeout in seconds for this TACACS+ - server. - format: int32 - maximum: 60 - minimum: 1 - type: integer required: - - address - - keySecretRef + - name + - servers + - type type: object - maxItems: 16 + x-kubernetes-validations: + - message: servers in a TACACS group must have tacacs config + rule: self.type != 'TACACS' || self.servers.all(s, has(s.tacacs)) + maxItems: 8 type: array x-kubernetes-list-map-keys: - - address + - name x-kubernetes-list-type: map required: - deviceRef type: object + x-kubernetes-validations: + - message: at least one of serverGroups, authentication, authorization, + or accounting must be set + rule: has(self.serverGroups) || has(self.authentication) || has(self.authorization) + || has(self.accounting) status: description: |- Status of the resource. This is set and updated automatically. diff --git a/config/crd/bases/nx.cisco.networking.metal.ironcore.dev_aaaconfigs.yaml b/config/crd/bases/nx.cisco.networking.metal.ironcore.dev_aaaconfigs.yaml new file mode 100644 index 000000000..4b1aba88e --- /dev/null +++ b/config/crd/bases/nx.cisco.networking.metal.ironcore.dev_aaaconfigs.yaml @@ -0,0 +1,136 @@ +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.20.1 + name: aaaconfigs.nx.cisco.networking.metal.ironcore.dev +spec: + group: nx.cisco.networking.metal.ironcore.dev + names: + kind: AAAConfig + listKind: AAAConfigList + plural: aaaconfigs + shortNames: + - nxaaa + singular: aaaconfig + scope: Namespaced + versions: + - name: v1alpha1 + schema: + openAPIV3Schema: + description: AAAConfig is the Schema for the aaaconfigs 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: |- + Specification of the desired state of the resource. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#spec-and-status + properties: + configCommandsAuthorization: + description: |- + ConfigCommandsAuthorization defines NX-OS config-commands authorization methods. + Maps to: aaa authorization config-commands default + properties: + methods: + description: Methods is the ordered list of methods. + items: + description: NXOSMethod represents a single AAA method in an + NX-OS context. + properties: + groupName: + description: GroupName is the server group name when Type + is Group. + maxLength: 63 + type: string + type: + description: Type is the method type. + enum: + - Group + - Local + - None + type: string + required: + - type + type: object + maxItems: 4 + minItems: 1 + type: array + x-kubernetes-list-type: atomic + required: + - methods + type: object + consoleAuthentication: + description: |- + ConsoleAuthentication defines NX-OS console-specific authentication methods. + Maps to: aaa authentication login console + properties: + methods: + description: Methods is the ordered list of methods. + items: + description: NXOSMethod represents a single AAA method in an + NX-OS context. + properties: + groupName: + description: GroupName is the server group name when Type + is Group. + maxLength: 63 + type: string + type: + description: Type is the method type. + enum: + - Group + - Local + - None + type: string + required: + - type + type: object + maxItems: 4 + minItems: 1 + type: array + x-kubernetes-list-type: atomic + required: + - methods + type: object + keyEncryption: + allOf: + - enum: + - Type6 + - Type7 + - Clear + - enum: + - Type6 + - Type7 + - Clear + default: Type7 + description: KeyEncryption specifies the default encryption type for + TACACS+ keys. + type: string + loginErrorEnable: + description: |- + LoginErrorEnable enables login error messages (NX-OS specific). + Maps to: aaa authentication login error-enable + type: boolean + type: object + required: + - spec + type: object + served: true + storage: true diff --git a/config/default/kustomization.yaml b/config/default/kustomization.yaml index fd3c7529c..2a2e5cd92 100644 --- a/config/default/kustomization.yaml +++ b/config/default/kustomization.yaml @@ -24,7 +24,7 @@ resources: # [CERTMANAGER] To enable cert-manager, uncomment all sections with 'CERTMANAGER'. 'WEBHOOK' components are required. - ../certmanager # [PROMETHEUS] To enable prometheus monitor, uncomment all sections with 'PROMETHEUS'. -#- ../prometheus +- ../prometheus # [METRICS] Expose the controller manager metrics service. - metrics_service.yaml # [PROVISIONING] Expose the controller manager provisioning service. diff --git a/config/rbac/role.yaml b/config/rbac/role.yaml index e2781fc40..116e4fa05 100644 --- a/config/rbac/role.yaml +++ b/config/rbac/role.yaml @@ -140,6 +140,7 @@ rules: - apiGroups: - nx.cisco.networking.metal.ironcore.dev resources: + - aaaconfigs - bgpconfigs - interfaceconfigs - lldpconfigs diff --git a/config/samples/networking_v1alpha1_aaa.yaml b/config/samples/networking_v1alpha1_aaa.yaml index 1d281c0c9..94e2d6b06 100644 --- a/config/samples/networking_v1alpha1_aaa.yaml +++ b/config/samples/networking_v1alpha1_aaa.yaml @@ -1,8 +1,8 @@ # Example AAA configuration with TACACS+ servers # This configures: # - feature tacacs+ -# - tacacs-server hosts with Type 7 encrypted keys -# - AAA group server for TACACS +# - tacacs-server hosts with encrypted keys +# - AAA server group for TACACS+ # - AAA authentication, authorization, and accounting --- apiVersion: v1 @@ -15,6 +15,30 @@ stringData: # Replace with your actual TACACS+ server shared secret server-key: "supersecretkey" --- +# Cisco NX-OS specific AAA configuration +# This configures vendor-specific settings like key encryption type, +# login error messages, console authentication, and config-commands authorization. +apiVersion: nx.cisco.networking.metal.ironcore.dev/v1alpha1 +kind: AAAConfig +metadata: + name: aaa-tacacs-nxos + namespace: default +spec: + keyEncryption: Type7 + loginErrorEnable: true + # NX-OS: aaa authentication login console group GR_TACACS local + consoleAuthentication: + methods: + - type: Group + groupName: GR_TACACS + - type: Local + # NX-OS: aaa authorization config-commands default group GR_TACACS local + configCommandsAuthorization: + methods: + - type: Group + groupName: GR_TACACS + - type: Local +--- apiVersion: networking.metal.ironcore.dev/v1alpha1 kind: AAA metadata: @@ -25,79 +49,54 @@ spec: deviceRef: name: my-switch - # TACACS+ server configuration - # Equivalent to: - # tacacs-server host 10.16.8.142 key 7 $TYPE_7_SECRET - # tacacs-server host 10.16.8.32 key 7 $TYPE_7_SECRET - # tacacs-server host 169.145.33.51 key 7 $TYPE_7_SECRET - tacacsServers: - - address: "10.16.8.142" - keySecretRef: - name: tacacs-server-keys - key: server-key - keyEncryption: Type7 - - address: "10.16.8.32" - keySecretRef: - name: tacacs-server-keys - key: server-key - keyEncryption: Type7 - - address: "169.145.33.51" - keySecretRef: - name: tacacs-server-keys - key: server-key - keyEncryption: Type7 + # Reference to the Cisco NX-OS specific AAA configuration + providerConfigRef: + group: nx.cisco.networking.metal.ironcore.dev + kind: AAAConfig + name: aaa-tacacs-nxos - # TACACS+ server group configuration - # Equivalent to: - # aaa group server tacacs+ GR_TACACS - # server 10.16.8.32 - # server 10.16.8.142 - # server 169.145.33.51 - # use-vrf management - # source-interface mgmt0 - tacacsGroup: - name: GR_TACACS - servers: - - "10.16.8.32" - - "10.16.8.142" - - "169.145.33.51" - vrf: management - sourceInterface: mgmt0 + # TACACS+ server group with nested servers + # OpenConfig: /system/aaa/server-groups/server-group + serverGroups: + - name: GR_TACACS + type: TACACS + vrfName: management + sourceInterfaceName: mgmt0 + servers: + - address: "10.16.8.142" + tacacs: + keySecretRef: + name: tacacs-server-keys + key: server-key + - address: "10.16.8.32" + tacacs: + keySecretRef: + name: tacacs-server-keys + key: server-key + - address: "169.145.33.51" + tacacs: + keySecretRef: + name: tacacs-server-keys + key: server-key - # AAA authentication configuration - # Equivalent to: - # aaa authentication login default group GR_TACACS - # aaa authentication login console group GR_TACACS local - # aaa authentication login error-enable + # AAA authentication method list + # OpenConfig: /system/aaa/authentication authentication: - login: - default: - methods: - - type: Group - groupName: GR_TACACS - console: - methods: - - type: Group - groupName: GR_TACACS - - type: Local - loginErrorEnable: true + methods: + - type: Group + groupName: GR_TACACS - # AAA authorization configuration - # Equivalent to: - # aaa authorization config-commands default group GR_TACACS local + # AAA authorization method list + # OpenConfig: /system/aaa/authorization authorization: - configCommands: - default: - methods: - - type: Group - groupName: GR_TACACS - - type: Local + methods: + - type: Group + groupName: GR_TACACS + - type: Local - # AAA accounting configuration - # Equivalent to: - # aaa accounting default group GR_TACACS + # AAA accounting method list + # OpenConfig: /system/aaa/accounting accounting: - default: - methods: - - type: Group - groupName: GR_TACACS + methods: + - type: Group + groupName: GR_TACACS diff --git a/internal/controller/core/aaa_controller.go b/internal/controller/core/aaa_controller.go index 17733f43f..0df766229 100644 --- a/internal/controller/core/aaa_controller.go +++ b/internal/controller/core/aaa_controller.go @@ -253,15 +253,19 @@ func (r *AAAReconciler) reconcile(ctx context.Context, s *aaaScope) (reterr erro } }() - // Load TACACS+ server keys from secrets + // Load server keys from secrets c := clientutil.NewClient(r, s.AAA.Namespace) tacacsKeys := make(map[string]string) - for _, server := range s.AAA.Spec.TACACSServers { - key, err := c.Secret(ctx, &server.KeySecretRef) - if err != nil { - return fmt.Errorf("failed to get TACACS+ key for server %s: %w", server.Address, err) + for _, group := range s.AAA.Spec.ServerGroups { + for _, server := range group.Servers { + if server.TACACS != nil { + key, err := c.Secret(ctx, &server.TACACS.KeySecretRef) + if err != nil { + return fmt.Errorf("failed to get key for server %s in group %s: %w", server.Address, group.Name, err) + } + tacacsKeys[server.Address] = string(key) + } } - tacacsKeys[server.Address] = string(key) } // Ensure the AAA is realized on the provider. @@ -313,16 +317,22 @@ func (r *AAAReconciler) secretToAAA(ctx context.Context, obj client.Object) []ct requests := []ctrl.Request{} for _, a := range aaas.Items { - // Check if any TACACS+ server references this secret - for _, server := range a.Spec.TACACSServers { - if server.KeySecretRef.Name == secret.Name && a.Namespace == secret.Namespace { - log.Info("Enqueuing AAA for reconciliation", "AAA", klog.KObj(&a)) - requests = append(requests, ctrl.Request{ - NamespacedName: client.ObjectKey{ - Name: a.Name, - Namespace: a.Namespace, - }, - }) + found := false + for _, group := range a.Spec.ServerGroups { + for _, server := range group.Servers { + if server.TACACS != nil && server.TACACS.KeySecretRef.Name == secret.Name && a.Namespace == secret.Namespace { + log.Info("Enqueuing AAA for reconciliation", "AAA", klog.KObj(&a)) + requests = append(requests, ctrl.Request{ + NamespacedName: client.ObjectKey{ + Name: a.Name, + Namespace: a.Namespace, + }, + }) + found = true + break + } + } + if found { break // Only enqueue once per AAA } } diff --git a/internal/provider/cisco/nxos/aaa.go b/internal/provider/cisco/nxos/aaa.go index 09474f70a..100b54a6c 100644 --- a/internal/provider/cisco/nxos/aaa.go +++ b/internal/provider/cisco/nxos/aaa.go @@ -4,8 +4,9 @@ package nxos import ( + nxv1alpha1 "github.com/ironcore-dev/network-operator/api/cisco/nx/v1alpha1" "github.com/ironcore-dev/network-operator/api/core/v1alpha1" - "github.com/ironcore-dev/network-operator/internal/provider/cisco/gnmiext/v2" + gnmiext "github.com/ironcore-dev/network-operator/internal/provider/cisco/gnmiext/v2" ) var ( @@ -19,7 +20,6 @@ var ( ) // TACACSFeature enables/disables the TACACS+ feature on NX-OS. -// Path: System/fm-items/tacacsplus-items/adminSt type TACACSFeature string func (*TACACSFeature) XPath() string { @@ -41,7 +41,6 @@ const ( ) // TacacsPlusProvider represents a TACACS+ server host configuration. -// Path: System/userext-items/tacacsext-items/tacacsplusprovider-items/TacacsPlusProvider-list[name=
] type TacacsPlusProvider struct { Name string `json:"name"` Port int32 `json:"port,omitempty"` @@ -59,7 +58,6 @@ func (p *TacacsPlusProvider) XPath() string { } // TacacsPlusProviderGroup represents a TACACS+ server group configuration. -// Path: System/userext-items/tacacsext-items/tacacsplusprovidergroup-items/TacacsPlusProviderGroup-list[name=] type TacacsPlusProviderGroup struct { Name string `json:"name"` Vrf string `json:"vrf,omitempty"` @@ -85,7 +83,6 @@ type TacacsPlusProviderRef struct { func (r *TacacsPlusProviderRef) Key() string { return r.Name } // AAADefaultAuth represents AAA default authentication configuration. -// Path: System/userext-items/authrealm-items/defaultauth-items type AAADefaultAuth struct { Realm string `json:"realm,omitempty"` ProviderGroup string `json:"providerGroup,omitempty"` @@ -101,7 +98,6 @@ func (*AAADefaultAuth) XPath() string { } // AAAConsoleAuth represents AAA console authentication configuration. -// Path: System/userext-items/authrealm-items/consoleauth-items type AAAConsoleAuth struct { Realm string `json:"realm,omitempty"` ProviderGroup string `json:"providerGroup,omitempty"` @@ -117,11 +113,9 @@ func (*AAAConsoleAuth) XPath() string { } // AAADefaultAuthor represents AAA default authorization configuration for config commands. -// Path: System/userext-items/authrealm-items/defaultauthor-items/DefaultAuthor-list[cmdType=config] +// Note: "name" and "realm" are read-only operational fields on NX-OS and must not be sent. type AAADefaultAuthor struct { - Name string `json:"name"` CmdType string `json:"cmdType"` - Realm string `json:"realm,omitempty"` ProviderGroup string `json:"providerGroup,omitempty"` LocalRbac bool `json:"localRbac,omitempty"` AuthorMethodNone bool `json:"authorMethodNone,omitempty"` @@ -134,7 +128,6 @@ func (a *AAADefaultAuthor) XPath() string { } // AAADefaultAcc represents AAA default accounting configuration. -// Path: System/userext-items/authrealm-items/defaultacc-items type AAADefaultAcc struct { Name string `json:"name,omitempty"` Realm string `json:"realm,omitempty"` @@ -147,14 +140,14 @@ func (*AAADefaultAcc) XPath() string { return "System/userext-items/authrealm-items/defaultacc-items" } -// MapKeyEncryption maps the API key encryption type to NX-OS type. -func MapKeyEncryption(enc v1alpha1.TACACSKeyEncryption) string { +// MapKeyEncryption maps the Cisco-specific key encryption type to NX-OS type. +func MapKeyEncryption(enc nxv1alpha1.TACACSKeyEncryption) string { switch enc { - case v1alpha1.TACACSKeyEncryptionType6: + case nxv1alpha1.TACACSKeyEncryptionType6: return "6" - case v1alpha1.TACACSKeyEncryptionType7: + case nxv1alpha1.TACACSKeyEncryptionType7: return "7" - case v1alpha1.TACACSKeyEncryptionClear: + case nxv1alpha1.TACACSKeyEncryptionClear: return "0" default: return "7" @@ -162,7 +155,7 @@ func MapKeyEncryption(enc v1alpha1.TACACSKeyEncryption) string { } // MapRealmFromMethodType maps the API method type to NX-OS realm. -func MapRealmFromMethodType(method v1alpha1.AAAMethodType, groupName string) string { +func MapRealmFromMethodType(method v1alpha1.AAAMethodType) string { switch method { case v1alpha1.AAAMethodTypeGroup: return AAARealmTacacs @@ -193,3 +186,35 @@ func MapFallbackFromMethodList(methods []v1alpha1.AAAMethod) string { } return AAAValueNo } + +// MapNXOSRealm maps an NX-OS method type string to NX-OS realm. +func MapNXOSRealm(methodType string) string { + switch methodType { + case "Group": + return AAARealmTacacs + case "Local": + return AAARealmLocal + case "None": + return AAARealmNone + default: + return AAARealmLocal + } +} + +// MapNXOSLocal checks if local is in an NX-OS method list. +func MapNXOSLocal(methods []nxv1alpha1.NXOSMethod) string { + for _, m := range methods { + if m.Type == "Local" { + return AAAValueYes + } + } + return AAAValueNo +} + +// MapNXOSFallback determines fallback setting from an NX-OS method list. +func MapNXOSFallback(methods []nxv1alpha1.NXOSMethod) string { + if len(methods) > 1 { + return AAAValueYes + } + return AAAValueNo +} diff --git a/internal/provider/cisco/nxos/provider.go b/internal/provider/cisco/nxos/provider.go index bf1250184..778b93a6e 100644 --- a/internal/provider/cisco/nxos/provider.go +++ b/internal/provider/cisco/nxos/provider.go @@ -3274,100 +3274,117 @@ func (p *Provider) GetDHCPRelayStatus(ctx context.Context, req *provider.DHCPRel func (p *Provider) EnsureAAA(ctx context.Context, req *provider.EnsureAAARequest) error { var conf []gnmiext.Configurable - // Enable TACACS+ feature if there are TACACS servers configured - if len(req.AAA.Spec.TACACSServers) > 0 { + // Read Cisco-specific config from ProviderConfig + var cfg nxv1alpha1.AAAConfig + if req.ProviderConfig != nil { + if err := req.ProviderConfig.Into(&cfg); err != nil { + return err + } + } + + // Process server groups + for _, group := range req.AAA.Spec.ServerGroups { + if group.Type != v1alpha1.AAAServerGroupTypeTACACS { + continue + } + + // Enable TACACS+ feature tacacsFeature := TACACSFeatureEnabled conf = append(conf, &tacacsFeature) - } - // Configure TACACS+ server hosts - for _, server := range req.AAA.Spec.TACACSServers { - srv := &TacacsPlusProvider{ - Name: server.Address, - Port: server.Port, - KeyEnc: MapKeyEncryption(server.KeyEncryption), + // Configure individual TACACS+ server hosts + for _, server := range group.Servers { + srv := &TacacsPlusProvider{ + Name: server.Address, + KeyEnc: MapKeyEncryption(cfg.Spec.KeyEncryption), + } + if server.TACACS != nil { + srv.Port = server.TACACS.Port + } + if key, ok := req.TACACSServerKeys[server.Address]; ok { + srv.Key = key + } + if server.Timeout != nil { + srv.Timeout = *server.Timeout + } + conf = append(conf, srv) } - if key, ok := req.TACACSServerKeys[server.Address]; ok { - srv.Key = key + + // Configure the server group + grp := &TacacsPlusProviderGroup{ + Name: group.Name, + Vrf: group.VrfName, + SrcIf: group.SourceInterfaceName, } - if server.Timeout != nil { - srv.Timeout = *server.Timeout + for _, server := range group.Servers { + grp.ProviderRefItems.ProviderRefList.Set(&TacacsPlusProviderRef{Name: server.Address}) } - conf = append(conf, srv) + conf = append(conf, grp) } - // Configure TACACS+ server group - if req.AAA.Spec.TACACSGroup != nil { - group := &TacacsPlusProviderGroup{ - Name: req.AAA.Spec.TACACSGroup.Name, - Vrf: req.AAA.Spec.TACACSGroup.VRF, - SrcIf: req.AAA.Spec.TACACSGroup.SourceInterface, + // Configure AAA default authentication (from core API flat method list) + if req.AAA.Spec.Authentication != nil && len(req.AAA.Spec.Authentication.Methods) > 0 { + methods := req.AAA.Spec.Authentication.Methods + authen := &AAADefaultAuth{ + ErrEn: cfg.Spec.LoginErrorEnable, + Fallback: MapFallbackFromMethodList(methods), + Local: MapLocalFromMethodList(methods), } - for _, serverAddr := range req.AAA.Spec.TACACSGroup.Servers { - group.ProviderRefItems.ProviderRefList.Set(&TacacsPlusProviderRef{Name: serverAddr}) + if methods[0].Type == v1alpha1.AAAMethodTypeGroup { + authen.Realm = AAARealmTacacs + authen.ProviderGroup = methods[0].GroupName + } else { + authen.Realm = MapRealmFromMethodType(methods[0].Type) } - conf = append(conf, group) + conf = append(conf, authen) } - // Configure AAA default authentication - if req.AAA.Spec.Authentication != nil && req.AAA.Spec.Authentication.Login != nil { - if req.AAA.Spec.Authentication.Login.Default != nil && len(req.AAA.Spec.Authentication.Login.Default.Methods) > 0 { - methods := req.AAA.Spec.Authentication.Login.Default.Methods - authen := &AAADefaultAuth{ - ErrEn: req.AAA.Spec.Authentication.LoginErrorEnable, - Fallback: MapFallbackFromMethodList(methods), - Local: MapLocalFromMethodList(methods), - } - // Set realm and provider group based on first method - if methods[0].Type == v1alpha1.AAAMethodTypeGroup { - authen.Realm = AAARealmTacacs - authen.ProviderGroup = methods[0].GroupName - } else { - authen.Realm = MapRealmFromMethodType(methods[0].Type, "") - } - conf = append(conf, authen) + // Configure AAA console authentication (from Cisco AAAConfig) + if cfg.Spec.ConsoleAuthentication != nil && len(cfg.Spec.ConsoleAuthentication.Methods) > 0 { + methods := cfg.Spec.ConsoleAuthentication.Methods + consoleAuth := &AAAConsoleAuth{ + ErrEn: cfg.Spec.LoginErrorEnable, + Fallback: MapNXOSFallback(methods), + Local: MapNXOSLocal(methods), } + if methods[0].Type == "Group" { + consoleAuth.Realm = AAARealmTacacs + consoleAuth.ProviderGroup = methods[0].GroupName + } else { + consoleAuth.Realm = MapNXOSRealm(methods[0].Type) + } + conf = append(conf, consoleAuth) + } - // Configure AAA console authentication - if req.AAA.Spec.Authentication.Login.Console != nil && len(req.AAA.Spec.Authentication.Login.Console.Methods) > 0 { - methods := req.AAA.Spec.Authentication.Login.Console.Methods - consoleAuth := &AAAConsoleAuth{ - ErrEn: req.AAA.Spec.Authentication.LoginErrorEnable, - Fallback: MapFallbackFromMethodList(methods), - Local: MapLocalFromMethodList(methods), - } - if methods[0].Type == v1alpha1.AAAMethodTypeGroup { - consoleAuth.Realm = AAARealmTacacs - consoleAuth.ProviderGroup = methods[0].GroupName - } else { - consoleAuth.Realm = MapRealmFromMethodType(methods[0].Type, "") - } - conf = append(conf, consoleAuth) + // Configure AAA authorization (from core API flat method list) + if req.AAA.Spec.Authorization != nil && len(req.AAA.Spec.Authorization.Methods) > 0 { + methods := req.AAA.Spec.Authorization.Methods + author := &AAADefaultAuthor{ + CmdType: "config", + LocalRbac: MapLocalFromMethodList(methods) == AAAValueYes, + } + if methods[0].Type == v1alpha1.AAAMethodTypeGroup { + author.ProviderGroup = methods[0].GroupName } + conf = append(conf, author) } - // Configure AAA authorization for config commands - if req.AAA.Spec.Authorization != nil && req.AAA.Spec.Authorization.ConfigCommands != nil { - if req.AAA.Spec.Authorization.ConfigCommands.Default != nil && len(req.AAA.Spec.Authorization.ConfigCommands.Default.Methods) > 0 { - methods := req.AAA.Spec.Authorization.ConfigCommands.Default.Methods - author := &AAADefaultAuthor{ - Name: "Author", - CmdType: "config", - LocalRbac: MapLocalFromMethodList(methods) == AAAValueYes, - } - if methods[0].Type == v1alpha1.AAAMethodTypeGroup { - author.Realm = AAARealmTacacs - author.ProviderGroup = methods[0].GroupName - } else { - author.Realm = MapRealmFromMethodType(methods[0].Type, "") - } - conf = append(conf, author) + // Configure AAA config-commands authorization (from Cisco AAAConfig) + if cfg.Spec.ConfigCommandsAuthorization != nil && len(cfg.Spec.ConfigCommandsAuthorization.Methods) > 0 { + methods := cfg.Spec.ConfigCommandsAuthorization.Methods + author := &AAADefaultAuthor{ + CmdType: "config", + LocalRbac: MapNXOSLocal(methods) == AAAValueYes, } + if methods[0].Type == "Group" { + author.ProviderGroup = methods[0].GroupName + } + conf = append(conf, author) } - // Configure AAA accounting - if req.AAA.Spec.Accounting != nil && req.AAA.Spec.Accounting.Default != nil && len(req.AAA.Spec.Accounting.Default.Methods) > 0 { - methods := req.AAA.Spec.Accounting.Default.Methods + // Configure AAA accounting (from core API flat method list) + if req.AAA.Spec.Accounting != nil && len(req.AAA.Spec.Accounting.Methods) > 0 { + methods := req.AAA.Spec.Accounting.Methods acct := &AAADefaultAcc{ Name: "Accounting", LocalRbac: MapLocalFromMethodList(methods) == AAAValueYes, @@ -3376,87 +3393,92 @@ func (p *Provider) EnsureAAA(ctx context.Context, req *provider.EnsureAAARequest acct.Realm = AAARealmTacacs acct.ProviderGroup = methods[0].GroupName } else { - acct.Realm = MapRealmFromMethodType(methods[0].Type, "") + acct.Realm = MapRealmFromMethodType(methods[0].Type) } conf = append(conf, acct) } - return p.Patch(ctx, conf...) + return p.Update(ctx, conf...) } func (p *Provider) DeleteAAA(ctx context.Context, req *provider.DeleteAAARequest) error { + var conf []gnmiext.Configurable + + // Read Cisco-specific config from ProviderConfig + var cfg nxv1alpha1.AAAConfig + if req.ProviderConfig != nil { + if err := req.ProviderConfig.Into(&cfg); err != nil { + return err + } + } + // Reset AAA accounting to local - if req.AAA.Spec.Accounting != nil && req.AAA.Spec.Accounting.Default != nil { - acct := &AAADefaultAcc{ + if req.AAA.Spec.Accounting != nil { + conf = append(conf, &AAADefaultAcc{ Name: "Accounting", Realm: AAARealmLocal, LocalRbac: true, - } - if err := p.Patch(ctx, acct); err != nil { - return err - } + }) } // Reset AAA authorization to local - if req.AAA.Spec.Authorization != nil && req.AAA.Spec.Authorization.ConfigCommands != nil { - author := &AAADefaultAuthor{ - Name: "Author", - CmdType: "config", - Realm: AAARealmLocal, - LocalRbac: true, - } - if err := p.Patch(ctx, author); err != nil { - return err - } + if req.AAA.Spec.Authorization != nil || cfg.Spec.ConfigCommandsAuthorization != nil { + conf = append(conf, &AAADefaultAuthor{ + CmdType: "config", + ProviderGroup: "", + LocalRbac: true, + }) } // Reset AAA authentication to local if req.AAA.Spec.Authentication != nil { - authen := &AAADefaultAuth{ + conf = append(conf, &AAADefaultAuth{ + Realm: AAARealmLocal, + Local: AAAValueYes, + Fallback: AAAValueYes, + ErrEn: false, + }) + } + + // Reset console authentication to local + if cfg.Spec.ConsoleAuthentication != nil { + conf = append(conf, &AAAConsoleAuth{ Realm: AAARealmLocal, Local: AAAValueYes, Fallback: AAAValueYes, ErrEn: false, + }) + } + + // Delete TACACS+ server groups and hosts + hasTACACS := false + for _, group := range req.AAA.Spec.ServerGroups { + if group.Type != v1alpha1.AAAServerGroupTypeTACACS { + continue } - if err := p.Patch(ctx, authen); err != nil { + hasTACACS = true + + grp := &TacacsPlusProviderGroup{Name: group.Name} + if err := p.client.Delete(ctx, grp); err != nil { return err } - if req.AAA.Spec.Authentication.Login != nil && req.AAA.Spec.Authentication.Login.Console != nil { - consoleAuth := &AAAConsoleAuth{ - Realm: AAARealmLocal, - Local: AAAValueYes, - Fallback: AAAValueYes, - ErrEn: false, - } - if err := p.Patch(ctx, consoleAuth); err != nil { + for _, server := range group.Servers { + srv := &TacacsPlusProvider{Name: server.Address} + if err := p.client.Delete(ctx, srv); err != nil { return err } } } - // Delete TACACS+ server group - if req.AAA.Spec.TACACSGroup != nil { - group := &TacacsPlusProviderGroup{Name: req.AAA.Spec.TACACSGroup.Name} - if err := p.client.Delete(ctx, group); err != nil { - return err - } - } - - // Delete TACACS+ server hosts - for _, server := range req.AAA.Spec.TACACSServers { - srv := &TacacsPlusProvider{Name: server.Address} - if err := p.client.Delete(ctx, srv); err != nil { - return err - } + // Disable TACACS+ feature + if hasTACACS { + tacacsFeature := TACACSFeatureDisabled + conf = append(conf, &tacacsFeature) } - // Disable TACACS+ feature if no servers remain - if len(req.AAA.Spec.TACACSServers) > 0 { - tacacsFeature := TACACSFeatureDisabled - if err := p.Patch(ctx, &tacacsFeature); err != nil { - return err - } + if len(conf) > 0 { + return p.Update(ctx, conf...) } return nil From 74d0c8b9a68e4d934e3b4baf252bdf66cec4a94e Mon Sep 17 00:00:00 2001 From: i521907 Date: Wed, 18 Feb 2026 10:52:56 -0500 Subject: [PATCH 04/32] [NX-OS] Add RADIUS support to AAA Signed-off-by: i521907 --- api/cisco/nx/v1alpha1/aaaconfig_types.go | 18 +++ api/core/v1alpha1/aaa_types.go | 31 +++++ hack/provider/main.go | 6 +- internal/controller/core/aaa_controller.go | 12 +- internal/deviceutil/deviceutil.go | 78 +++++++++++- internal/provider/cisco/nxos/aaa.go | 81 +++++++++++++ internal/provider/cisco/nxos/ascii.go | 3 +- internal/provider/cisco/nxos/provider.go | 131 ++++++++++++++------- internal/provider/cisco/nxos/user.go | 2 +- internal/provider/provider.go | 5 +- test/e2e/util_test.go | 8 +- test/lab/main_test.go | 2 +- 12 files changed, 319 insertions(+), 58 deletions(-) diff --git a/api/cisco/nx/v1alpha1/aaaconfig_types.go b/api/cisco/nx/v1alpha1/aaaconfig_types.go index 160da1e62..46f3ad925 100644 --- a/api/cisco/nx/v1alpha1/aaaconfig_types.go +++ b/api/cisco/nx/v1alpha1/aaaconfig_types.go @@ -23,6 +23,11 @@ type AAAConfigSpec struct { // +kubebuilder:default=Type7 KeyEncryption TACACSKeyEncryption `json:"keyEncryption,omitempty"` + // RADIUSKeyEncryption specifies the default encryption type for RADIUS server keys. + // +kubebuilder:validation:Enum=Type6;Type7;Clear + // +kubebuilder:default=Type7 + RADIUSKeyEncryption RADIUSKeyEncryption `json:"radiusKeyEncryption,omitempty"` + // ConsoleAuthentication defines NX-OS console-specific authentication methods. // Maps to: aaa authentication login console // +optional @@ -47,6 +52,19 @@ const ( TACACSKeyEncryptionClear TACACSKeyEncryption = "Clear" ) +// RADIUSKeyEncryption defines the encryption type for RADIUS server keys. +// +kubebuilder:validation:Enum=Type6;Type7;Clear +type RADIUSKeyEncryption string + +const ( + // RADIUSKeyEncryptionType6 uses AES encryption (more secure). + RADIUSKeyEncryptionType6 RADIUSKeyEncryption = "Type6" + // RADIUSKeyEncryptionType7 uses Cisco Type 7 encryption (reversible). + RADIUSKeyEncryptionType7 RADIUSKeyEncryption = "Type7" + // RADIUSKeyEncryptionClear sends the key in cleartext. + RADIUSKeyEncryptionClear RADIUSKeyEncryption = "Clear" +) + // NXOSMethodList defines an ordered list of AAA methods for NX-OS specific contexts. type NXOSMethodList struct { // Methods is the ordered list of methods. diff --git a/api/core/v1alpha1/aaa_types.go b/api/core/v1alpha1/aaa_types.go index 7fa0e50a2..098b7d9a0 100644 --- a/api/core/v1alpha1/aaa_types.go +++ b/api/core/v1alpha1/aaa_types.go @@ -65,6 +65,7 @@ const ( // AAAServerGroup represents a named group of AAA servers. // OpenConfig: /system/aaa/server-groups/server-group[name] // +kubebuilder:validation:XValidation:rule="self.type != 'TACACS' || self.servers.all(s, has(s.tacacs))",message="servers in a TACACS group must have tacacs config" +// +kubebuilder:validation:XValidation:rule="self.type != 'RADIUS' || self.servers.all(s, has(s.radius))",message="servers in a RADIUS group must have radius config" type AAAServerGroup struct { // Name is the name of the server group. // +required @@ -116,6 +117,12 @@ type AAAServer struct { // OpenConfig augmentation: /system/aaa/server-groups/server-group/servers/server/tacacs // +optional TACACS *AAAServerTACACS `json:"tacacs,omitempty"` + + // RADIUS contains RADIUS specific server configuration. + // Required when the parent server group type is RADIUS. + // OpenConfig augmentation: /system/aaa/server-groups/server-group/servers/server/radius + // +optional + RADIUS *AAAServerRADIUS `json:"radius,omitempty"` } // AAAServerTACACS contains TACACS+ specific server configuration. @@ -134,6 +141,30 @@ type AAAServerTACACS struct { KeySecretRef SecretKeySelector `json:"keySecretRef"` } +// AAAServerRADIUS contains RADIUS specific server configuration. +type AAAServerRADIUS struct { + // AuthPort is the UDP port for RADIUS authentication requests. + // Defaults to 1812 if not specified. + // +optional + // +kubebuilder:validation:Minimum=1 + // +kubebuilder:validation:Maximum=65535 + // +kubebuilder:default=1812 + AuthPort int32 `json:"authPort,omitempty"` + + // AcctPort is the UDP port for RADIUS accounting requests. + // Defaults to 1813 if not specified. + // +optional + // +kubebuilder:validation:Minimum=1 + // +kubebuilder:validation:Maximum=65535 + // +kubebuilder:default=1813 + AcctPort int32 `json:"acctPort,omitempty"` + + // KeySecretRef is a reference to a secret containing the shared key for this RADIUS server. + // The secret must contain a key specified in the SecretKeySelector. + // +required + KeySecretRef SecretKeySelector `json:"keySecretRef"` +} + // AAAAuthentication defines the AAA authentication method list. // OpenConfig: /system/aaa/authentication type AAAAuthentication struct { diff --git a/hack/provider/main.go b/hack/provider/main.go index b06bec7ca..08e406050 100644 --- a/hack/provider/main.go +++ b/hack/provider/main.go @@ -11,6 +11,7 @@ import ( "net/netip" "os" "os/signal" + "path/filepath" "reflect" "strings" "syscall" @@ -85,7 +86,8 @@ func (r *refStoreReader) List(_ context.Context, _ client.ObjectList, _ ...clien } func usage() { - fmt.Fprintf(os.Stderr, "Usage: %s [flags] \n\n", os.Args[0]) // #nosec G705 + base := filepath.Base(os.Args[0]) + fmt.Fprintf(os.Stderr, "Usage: %s [flags] \n\n", base) //nolint:gosec fmt.Fprintf(os.Stderr, "A debug tool for testing provider implementations.\n\n") fmt.Fprintf(os.Stderr, "This tool allows you to directly test provider implementations by creating or\n") fmt.Fprintf(os.Stderr, "deleting resources on network devices.\n\n") @@ -94,7 +96,7 @@ func usage() { fmt.Fprintf(os.Stderr, "Flags:\n") flag.PrintDefaults() fmt.Fprintf(os.Stderr, "\nExample:\n") - fmt.Fprintf(os.Stderr, " %s -address=192.168.1.1:9339 -username=admin -password=secret -file=config/samples/v1alpha1_interface.yaml create\n", os.Args[0]) // #nosec G705 + fmt.Fprintf(os.Stderr, " %s -address=192.168.1.1:9339 -username=admin -password=secret -file=config/samples/v1alpha1_interface.yaml create\n", base) //nolint:gosec } func validateFlags() error { diff --git a/internal/controller/core/aaa_controller.go b/internal/controller/core/aaa_controller.go index 0df766229..1370d88cd 100644 --- a/internal/controller/core/aaa_controller.go +++ b/internal/controller/core/aaa_controller.go @@ -256,6 +256,7 @@ func (r *AAAReconciler) reconcile(ctx context.Context, s *aaaScope) (reterr erro // Load server keys from secrets c := clientutil.NewClient(r, s.AAA.Namespace) tacacsKeys := make(map[string]string) + radiusKeys := make(map[string]string) for _, group := range s.AAA.Spec.ServerGroups { for _, server := range group.Servers { if server.TACACS != nil { @@ -265,6 +266,13 @@ func (r *AAAReconciler) reconcile(ctx context.Context, s *aaaScope) (reterr erro } tacacsKeys[server.Address] = string(key) } + if server.RADIUS != nil { + key, err := c.Secret(ctx, &server.RADIUS.KeySecretRef) + if err != nil { + return fmt.Errorf("failed to get key for server %s in group %s: %w", server.Address, group.Name, err) + } + radiusKeys[server.Address] = string(key) + } } } @@ -273,6 +281,7 @@ func (r *AAAReconciler) reconcile(ctx context.Context, s *aaaScope) (reterr erro AAA: s.AAA, ProviderConfig: s.ProviderConfig, TACACSServerKeys: tacacsKeys, + RADIUSServerKeys: radiusKeys, }) cond := conditions.FromError(err) @@ -320,7 +329,8 @@ func (r *AAAReconciler) secretToAAA(ctx context.Context, obj client.Object) []ct found := false for _, group := range a.Spec.ServerGroups { for _, server := range group.Servers { - if server.TACACS != nil && server.TACACS.KeySecretRef.Name == secret.Name && a.Namespace == secret.Namespace { + if (server.TACACS != nil && server.TACACS.KeySecretRef.Name == secret.Name && a.Namespace == secret.Namespace) || + (server.RADIUS != nil && server.RADIUS.KeySecretRef.Name == secret.Name && a.Namespace == secret.Namespace) { log.Info("Enqueuing AAA for reconciliation", "AAA", klog.KObj(&a)) requests = append(requests, ctrl.Request{ NamespacedName: client.ObjectKey{ diff --git a/internal/deviceutil/deviceutil.go b/internal/deviceutil/deviceutil.go index 07a48195f..8cff76772 100644 --- a/internal/deviceutil/deviceutil.go +++ b/internal/deviceutil/deviceutil.go @@ -112,7 +112,7 @@ type Connection struct { // Username for basic authentication. Might be empty if the device does not require authentication. Username string // Password for basic authentication. Might be empty if the device does not require authentication. - Password string // #nosec G117 + Password string `json:"-"` // TLS configuration for the connection. TLS *tls.Config } @@ -162,3 +162,79 @@ func GetDeviceConnection(ctx context.Context, r client.Reader, obj *v1alpha1.Dev TLS: conf, }, nil } +// NewGrpcClient creates a new gRPC client connection to a specified device using the provided [Connection]. +// The connection will use TLS if the [Connection.TLS] field is set, otherwise it will use an insecure connection. +// If the [Connection.Username] and [Connection.Password] fields are set, basic authentication in the form of metadata will be used. +func NewGrpcClient(ctx context.Context, conn *Connection, o ...Option) (*grpc.ClientConn, error) { + creds := insecure.NewCredentials() + if conn.TLS != nil { + creds = credentials.NewTLS(conn.TLS) + } + + opts := []grpc.DialOption{grpc.WithTransportCredentials(creds)} + if conn.Username != "" && conn.Password != "" { + opts = append(opts, grpc.WithPerRPCCredentials(&auth{ + Username: conn.Username, + Password: conn.Password, + })) + } + + for _, opt := range o { + dialOpt, err := opt() + if err != nil { + return nil, err + } + opts = append(opts, dialOpt) + } + + return grpc.NewClient(conn.Address, opts...) +} + +type Option func() (grpc.DialOption, error) + +// WithDefaultTimeout returns a gRPC dial option that sets a default timeout for each RPC. +// If a deadline is already present in the context, it will not be modified. +func WithDefaultTimeout(timeout time.Duration) Option { + return func() (grpc.DialOption, error) { + if timeout <= 0 { + return nil, errors.New("timeout must be greater than zero") + } + return grpc.WithUnaryInterceptor(UnaryDefaultTimeoutInterceptor(timeout)), nil + } +} + +type auth struct { + Username string + Password string `json:"-"` + SecureTransportCreds bool +} + +var _ credentials.PerRPCCredentials = (*auth)(nil) + +func (a *auth) GetRequestMetadata(_ context.Context, _ ...string) (map[string]string, error) { + return map[string]string{ + "username": a.Username, + "password": a.Password, + }, nil +} + +func (a *auth) RequireTransportSecurity() bool { + // Only called if the transport credentials are insecure. + return false +} + +// UnaryDefaultTimeoutInterceptor returns a gRPC unary client interceptor that sets a default timeout +// for each RPC. If a deadline is already present , it will not be modified. +func UnaryDefaultTimeoutInterceptor(timeout time.Duration) grpc.UnaryClientInterceptor { + return func(ctx context.Context, method string, req, reply any, cc *grpc.ClientConn, invoker grpc.UnaryInvoker, opts ...grpc.CallOption) error { + if _, ok := ctx.Deadline(); ok { + return invoker(ctx, method, req, reply, cc, opts...) + } + + ctx, cancel := context.WithTimeout(ctx, timeout) + defer cancel() + + return invoker(ctx, method, req, reply, cc, opts...) + } +} + diff --git a/internal/provider/cisco/nxos/aaa.go b/internal/provider/cisco/nxos/aaa.go index 100b54a6c..2c8995871 100644 --- a/internal/provider/cisco/nxos/aaa.go +++ b/internal/provider/cisco/nxos/aaa.go @@ -13,6 +13,8 @@ var ( _ gnmiext.Configurable = (*TACACSFeature)(nil) _ gnmiext.Configurable = (*TacacsPlusProvider)(nil) _ gnmiext.Configurable = (*TacacsPlusProviderGroup)(nil) + _ gnmiext.Configurable = (*RadiusProvider)(nil) + _ gnmiext.Configurable = (*RadiusProviderGroup)(nil) _ gnmiext.Configurable = (*AAADefaultAuth)(nil) _ gnmiext.Configurable = (*AAAConsoleAuth)(nil) _ gnmiext.Configurable = (*AAADefaultAuthor)(nil) @@ -34,6 +36,7 @@ const ( // AAA configuration constants const ( AAARealmTacacs = "tacacs" + AAARealmRadius = "radius" AAARealmLocal = "local" AAARealmNone = "none" AAAValueYes = "yes" @@ -82,6 +85,48 @@ type TacacsPlusProviderRef struct { func (r *TacacsPlusProviderRef) Key() string { return r.Name } +// RadiusProvider represents a RADIUS server host configuration. +type RadiusProvider struct { + Name string `json:"name"` + AuthPort int32 `json:"authPort,omitempty"` + AcctPort int32 `json:"acctPort,omitempty"` + Key string `json:"key,omitempty"` + KeyEnc string `json:"keyEnc,omitempty"` + Timeout int32 `json:"timeout,omitempty"` + Retries int32 `json:"retries,omitempty"` +} + +func (*RadiusProvider) IsListItem() {} + +func (p *RadiusProvider) XPath() string { + return "System/userext-items/radiusext-items/radiusprovider-items/RadiusProvider-list[name=" + p.Name + "]" +} + +// RadiusProviderGroup represents a RADIUS server group configuration. +type RadiusProviderGroup struct { + Name string `json:"name"` + Vrf string `json:"vrf,omitempty"` + SrcIf string `json:"srcIf,omitempty"` + Deadtime int32 `json:"deadtime,omitempty"` + ProviderRefItems RadiusProviderGroupRefItems `json:"providerref-items,omitzero"` +} + +func (*RadiusProviderGroup) IsListItem() {} + +func (g *RadiusProviderGroup) XPath() string { + return "System/userext-items/radiusext-items/radiusprovidergroup-items/RadiusProviderGroup-list[name=" + g.Name + "]" +} + +type RadiusProviderGroupRefItems struct { + ProviderRefList gnmiext.List[string, *RadiusProviderRef] `json:"ProviderRef-list,omitzero"` +} + +type RadiusProviderRef struct { + Name string `json:"name"` +} + +func (r *RadiusProviderRef) Key() string { return r.Name } + // AAADefaultAuth represents AAA default authentication configuration. type AAADefaultAuth struct { Realm string `json:"realm,omitempty"` @@ -154,6 +199,42 @@ func MapKeyEncryption(enc nxv1alpha1.TACACSKeyEncryption) string { } } +// MapRADIUSKeyEncryption maps the Cisco-specific RADIUS key encryption type to NX-OS type. +func MapRADIUSKeyEncryption(enc nxv1alpha1.RADIUSKeyEncryption) string { + switch enc { + case nxv1alpha1.RADIUSKeyEncryptionType6: + return "6" + case nxv1alpha1.RADIUSKeyEncryptionType7: + return "7" + case nxv1alpha1.RADIUSKeyEncryptionClear: + return "0" + default: + return "7" + } +} + +// groupTypeByName returns the AAAServerGroupType for the given group name, +// defaulting to TACACS if not found. +func groupTypeByName(name string, groups []v1alpha1.AAAServerGroup) v1alpha1.AAAServerGroupType { + for _, g := range groups { + if g.Name == name { + return g.Type + } + } + return v1alpha1.AAAServerGroupTypeTACACS +} + +// MapRealmFromGroup returns the NX-OS realm string for the given group name, +// resolving TACACS vs RADIUS from the server group list. +func MapRealmFromGroup(groupName string, groups []v1alpha1.AAAServerGroup) string { + switch groupTypeByName(groupName, groups) { + case v1alpha1.AAAServerGroupTypeRADIUS: + return AAARealmRadius + default: + return AAARealmTacacs + } +} + // MapRealmFromMethodType maps the API method type to NX-OS realm. func MapRealmFromMethodType(method v1alpha1.AAAMethodType) string { switch method { diff --git a/internal/provider/cisco/nxos/ascii.go b/internal/provider/cisco/nxos/ascii.go index 47197f95d..564b30893 100644 --- a/internal/provider/cisco/nxos/ascii.go +++ b/internal/provider/cisco/nxos/ascii.go @@ -18,8 +18,7 @@ func (s ASCIIStr) String() string { if v == "0" { break } - if num, err := strconv.Atoi(v); err == nil { - // #nosec G115 + if num, err := strconv.Atoi(v); err == nil && num >= 0 && num <= 127 { runes = append(runes, rune(num)) } } diff --git a/internal/provider/cisco/nxos/provider.go b/internal/provider/cisco/nxos/provider.go index 778b93a6e..a8f7f8461 100644 --- a/internal/provider/cisco/nxos/provider.go +++ b/internal/provider/cisco/nxos/provider.go @@ -3284,42 +3284,72 @@ func (p *Provider) EnsureAAA(ctx context.Context, req *provider.EnsureAAARequest // Process server groups for _, group := range req.AAA.Spec.ServerGroups { - if group.Type != v1alpha1.AAAServerGroupTypeTACACS { - continue - } - - // Enable TACACS+ feature - tacacsFeature := TACACSFeatureEnabled - conf = append(conf, &tacacsFeature) - - // Configure individual TACACS+ server hosts - for _, server := range group.Servers { - srv := &TacacsPlusProvider{ - Name: server.Address, - KeyEnc: MapKeyEncryption(cfg.Spec.KeyEncryption), + switch group.Type { + case v1alpha1.AAAServerGroupTypeTACACS: + // Enable TACACS+ feature + tacacsFeature := TACACSFeatureEnabled + conf = append(conf, &tacacsFeature) + + // Configure individual TACACS+ server hosts + for _, server := range group.Servers { + srv := &TacacsPlusProvider{ + Name: server.Address, + KeyEnc: MapKeyEncryption(cfg.Spec.KeyEncryption), + } + if server.TACACS != nil { + srv.Port = server.TACACS.Port + } + if key, ok := req.TACACSServerKeys[server.Address]; ok { + srv.Key = key + } + if server.Timeout != nil { + srv.Timeout = *server.Timeout + } + conf = append(conf, srv) } - if server.TACACS != nil { - srv.Port = server.TACACS.Port + + // Configure the TACACS+ server group + grp := &TacacsPlusProviderGroup{ + Name: group.Name, + Vrf: group.VrfName, + SrcIf: group.SourceInterfaceName, } - if key, ok := req.TACACSServerKeys[server.Address]; ok { - srv.Key = key + for _, server := range group.Servers { + grp.ProviderRefItems.ProviderRefList.Set(&TacacsPlusProviderRef{Name: server.Address}) } - if server.Timeout != nil { - srv.Timeout = *server.Timeout + conf = append(conf, grp) + + case v1alpha1.AAAServerGroupTypeRADIUS: + // Configure individual RADIUS server hosts + for _, server := range group.Servers { + srv := &RadiusProvider{ + Name: server.Address, + KeyEnc: MapRADIUSKeyEncryption(cfg.Spec.RADIUSKeyEncryption), + } + if server.RADIUS != nil { + srv.AuthPort = server.RADIUS.AuthPort + srv.AcctPort = server.RADIUS.AcctPort + } + if key, ok := req.RADIUSServerKeys[server.Address]; ok { + srv.Key = key + } + if server.Timeout != nil { + srv.Timeout = *server.Timeout + } + conf = append(conf, srv) } - conf = append(conf, srv) - } - // Configure the server group - grp := &TacacsPlusProviderGroup{ - Name: group.Name, - Vrf: group.VrfName, - SrcIf: group.SourceInterfaceName, - } - for _, server := range group.Servers { - grp.ProviderRefItems.ProviderRefList.Set(&TacacsPlusProviderRef{Name: server.Address}) + // Configure the RADIUS server group + grp := &RadiusProviderGroup{ + Name: group.Name, + Vrf: group.VrfName, + SrcIf: group.SourceInterfaceName, + } + for _, server := range group.Servers { + grp.ProviderRefItems.ProviderRefList.Set(&RadiusProviderRef{Name: server.Address}) + } + conf = append(conf, grp) } - conf = append(conf, grp) } // Configure AAA default authentication (from core API flat method list) @@ -3331,7 +3361,7 @@ func (p *Provider) EnsureAAA(ctx context.Context, req *provider.EnsureAAARequest Local: MapLocalFromMethodList(methods), } if methods[0].Type == v1alpha1.AAAMethodTypeGroup { - authen.Realm = AAARealmTacacs + authen.Realm = MapRealmFromGroup(methods[0].GroupName, req.AAA.Spec.ServerGroups) authen.ProviderGroup = methods[0].GroupName } else { authen.Realm = MapRealmFromMethodType(methods[0].Type) @@ -3348,7 +3378,7 @@ func (p *Provider) EnsureAAA(ctx context.Context, req *provider.EnsureAAARequest Local: MapNXOSLocal(methods), } if methods[0].Type == "Group" { - consoleAuth.Realm = AAARealmTacacs + consoleAuth.Realm = MapRealmFromGroup(methods[0].GroupName, req.AAA.Spec.ServerGroups) consoleAuth.ProviderGroup = methods[0].GroupName } else { consoleAuth.Realm = MapNXOSRealm(methods[0].Type) @@ -3390,7 +3420,7 @@ func (p *Provider) EnsureAAA(ctx context.Context, req *provider.EnsureAAARequest LocalRbac: MapLocalFromMethodList(methods) == AAAValueYes, } if methods[0].Type == v1alpha1.AAAMethodTypeGroup { - acct.Realm = AAARealmTacacs + acct.Realm = MapRealmFromGroup(methods[0].GroupName, req.AAA.Spec.ServerGroups) acct.ProviderGroup = methods[0].GroupName } else { acct.Realm = MapRealmFromMethodType(methods[0].Type) @@ -3450,24 +3480,35 @@ func (p *Provider) DeleteAAA(ctx context.Context, req *provider.DeleteAAARequest }) } - // Delete TACACS+ server groups and hosts + // Delete server groups and hosts hasTACACS := false for _, group := range req.AAA.Spec.ServerGroups { - if group.Type != v1alpha1.AAAServerGroupTypeTACACS { - continue - } - hasTACACS = true + switch group.Type { + case v1alpha1.AAAServerGroupTypeTACACS: + hasTACACS = true - grp := &TacacsPlusProviderGroup{Name: group.Name} - if err := p.client.Delete(ctx, grp); err != nil { - return err - } + grp := &TacacsPlusProviderGroup{Name: group.Name} + if err := p.client.Delete(ctx, grp); err != nil { + return err + } + for _, server := range group.Servers { + srv := &TacacsPlusProvider{Name: server.Address} + if err := p.client.Delete(ctx, srv); err != nil { + return err + } + } - for _, server := range group.Servers { - srv := &TacacsPlusProvider{Name: server.Address} - if err := p.client.Delete(ctx, srv); err != nil { + case v1alpha1.AAAServerGroupTypeRADIUS: + grp := &RadiusProviderGroup{Name: group.Name} + if err := p.client.Delete(ctx, grp); err != nil { return err } + for _, server := range group.Servers { + srv := &RadiusProvider{Name: server.Address} + if err := p.client.Delete(ctx, srv); err != nil { + return err + } + } } } diff --git a/internal/provider/cisco/nxos/user.go b/internal/provider/cisco/nxos/user.go index 844805121..b879976fe 100644 --- a/internal/provider/cisco/nxos/user.go +++ b/internal/provider/cisco/nxos/user.go @@ -25,7 +25,7 @@ type User struct { AllowExpired string `json:"allowExpired"` Expiration string `json:"expiration"` Name string `json:"name"` - Pwd string `json:"pwd,omitempty"` // #nosec G117 + Pwd string `json:"pwd,omitempty"` //nolint:gosec PwdHash PwdHashType `json:"passwordHash,omitempty"` PwdEncryptType PwdEncryptType `json:"pwdEncryptType,omitempty"` SshauthItems struct { diff --git a/internal/provider/provider.go b/internal/provider/provider.go index a43ab49af..be977e3cc 100644 --- a/internal/provider/provider.go +++ b/internal/provider/provider.go @@ -194,7 +194,7 @@ type UserProvider interface { type EnsureUserRequest struct { Username string - Password string // #nosec G117 + Password string `json:"-"` SSHKey string Roles []string ProviderConfig *ProviderConfig @@ -641,6 +641,9 @@ type EnsureAAARequest struct { // TACACSServerKeys contains the decrypted keys for each TACACS+ server, // keyed by server address. TACACSServerKeys map[string]string + // RADIUSServerKeys contains the decrypted shared secrets for each RADIUS server, + // keyed by server address. + RADIUSServerKeys map[string]string } type DeleteAAARequest struct { diff --git a/test/e2e/util_test.go b/test/e2e/util_test.go index 1281d18db..3e4d6cf01 100644 --- a/test/e2e/util_test.go +++ b/test/e2e/util_test.go @@ -11,6 +11,7 @@ import ( "fmt" "os" "os/exec" + "path/filepath" "strings" . "github.com/onsi/ginkgo/v2" @@ -38,8 +39,7 @@ func Run(cmd *exec.Cmd) (string, error) { } command := strings.Join(cmd.Args, " ") - // #nosec G705 - _, _ = fmt.Fprintf(GinkgoWriter, "running: %s\n", command) + _, _ = fmt.Fprintf(GinkgoWriter, "running: %s\n", command) //nolint:gosec output, err := cmd.CombinedOutput() if err != nil { @@ -209,8 +209,8 @@ func LoadImageToKindClusterWithName(ctx context.Context, name string) error { return fmt.Errorf("failed to create temp file: %w", err) } _ = file.Close() - // #nosec G703 - defer func() { _ = os.Remove(file.Name()) }() + imgPath := filepath.Clean(file.Name()) + defer func() { _ = os.Remove(imgPath) }() // https://github.com/containerd/nerdctl/blob/main/docs/command-reference.md#whale-nerdctl-save // https://docs.podman.io/en/v5.3.0/markdown/podman-save.1.html diff --git a/test/lab/main_test.go b/test/lab/main_test.go index 6ebb7f03d..97ae2f891 100644 --- a/test/lab/main_test.go +++ b/test/lab/main_test.go @@ -159,7 +159,7 @@ func Apply() script.Cmd { var Endpoint = struct { Addr string User string - Pass string // #nosec G117 + Pass string `json:"-"` }{} // ReadEnv reads required environment variables and populates the global Endpoint struct. From 659d6daab26235efcc5f6a18d63c47a305beff1b Mon Sep 17 00:00:00 2001 From: i521907 Date: Thu, 12 Mar 2026 10:12:28 -0400 Subject: [PATCH 05/32] Fix unused nolint:gosec directives Signed-off-by: i521907 --- hack/provider/main.go | 4 ++-- internal/provider/cisco/nxos/user.go | 2 +- test/e2e/util_test.go | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/hack/provider/main.go b/hack/provider/main.go index 08e406050..2a03a02fb 100644 --- a/hack/provider/main.go +++ b/hack/provider/main.go @@ -87,7 +87,7 @@ func (r *refStoreReader) List(_ context.Context, _ client.ObjectList, _ ...clien func usage() { base := filepath.Base(os.Args[0]) - fmt.Fprintf(os.Stderr, "Usage: %s [flags] \n\n", base) //nolint:gosec + fmt.Fprintf(os.Stderr, "Usage: %s [flags] \n\n", base) fmt.Fprintf(os.Stderr, "A debug tool for testing provider implementations.\n\n") fmt.Fprintf(os.Stderr, "This tool allows you to directly test provider implementations by creating or\n") fmt.Fprintf(os.Stderr, "deleting resources on network devices.\n\n") @@ -96,7 +96,7 @@ func usage() { fmt.Fprintf(os.Stderr, "Flags:\n") flag.PrintDefaults() fmt.Fprintf(os.Stderr, "\nExample:\n") - fmt.Fprintf(os.Stderr, " %s -address=192.168.1.1:9339 -username=admin -password=secret -file=config/samples/v1alpha1_interface.yaml create\n", base) //nolint:gosec + fmt.Fprintf(os.Stderr, " %s -address=192.168.1.1:9339 -username=admin -password=secret -file=config/samples/v1alpha1_interface.yaml create\n", base) } func validateFlags() error { diff --git a/internal/provider/cisco/nxos/user.go b/internal/provider/cisco/nxos/user.go index b879976fe..5c28feea9 100644 --- a/internal/provider/cisco/nxos/user.go +++ b/internal/provider/cisco/nxos/user.go @@ -25,7 +25,7 @@ type User struct { AllowExpired string `json:"allowExpired"` Expiration string `json:"expiration"` Name string `json:"name"` - Pwd string `json:"pwd,omitempty"` //nolint:gosec + Pwd string `json:"pwd,omitempty"` PwdHash PwdHashType `json:"passwordHash,omitempty"` PwdEncryptType PwdEncryptType `json:"pwdEncryptType,omitempty"` SshauthItems struct { diff --git a/test/e2e/util_test.go b/test/e2e/util_test.go index 3e4d6cf01..357817a1a 100644 --- a/test/e2e/util_test.go +++ b/test/e2e/util_test.go @@ -39,7 +39,7 @@ func Run(cmd *exec.Cmd) (string, error) { } command := strings.Join(cmd.Args, " ") - _, _ = fmt.Fprintf(GinkgoWriter, "running: %s\n", command) //nolint:gosec + _, _ = fmt.Fprintf(GinkgoWriter, "running: %s\n", command) output, err := cmd.CombinedOutput() if err != nil { From 5f593cc832718fd6a20a50dd66da14e64fe754c4 Mon Sep 17 00:00:00 2001 From: i521907 Date: Thu, 26 Mar 2026 06:52:12 -0400 Subject: [PATCH 06/32] fix: address felix review comments on AAA Signed-off-by: i521907 --- api/cisco/nx/v1alpha1/aaaconfig_types.go | 11 +-- api/core/v1alpha1/aaa_types.go | 15 +--- internal/provider/cisco/nxos/aaa.go | 12 +-- internal/provider/cisco/nxos/provider.go | 103 +++++------------------ 4 files changed, 29 insertions(+), 112 deletions(-) diff --git a/api/cisco/nx/v1alpha1/aaaconfig_types.go b/api/cisco/nx/v1alpha1/aaaconfig_types.go index 46f3ad925..c58b7cc27 100644 --- a/api/cisco/nx/v1alpha1/aaaconfig_types.go +++ b/api/cisco/nx/v1alpha1/aaaconfig_types.go @@ -13,28 +13,23 @@ import ( // AAAConfigSpec defines the desired state of AAAConfig type AAAConfigSpec struct { - // LoginErrorEnable enables login error messages (NX-OS specific). - // Maps to: aaa authentication login error-enable + // LoginErrorEnable enables login error messages. // +optional LoginErrorEnable bool `json:"loginErrorEnable,omitempty"` // KeyEncryption specifies the default encryption type for TACACS+ keys. - // +kubebuilder:validation:Enum=Type6;Type7;Clear // +kubebuilder:default=Type7 KeyEncryption TACACSKeyEncryption `json:"keyEncryption,omitempty"` // RADIUSKeyEncryption specifies the default encryption type for RADIUS server keys. - // +kubebuilder:validation:Enum=Type6;Type7;Clear // +kubebuilder:default=Type7 RADIUSKeyEncryption RADIUSKeyEncryption `json:"radiusKeyEncryption,omitempty"` - // ConsoleAuthentication defines NX-OS console-specific authentication methods. - // Maps to: aaa authentication login console + // ConsoleAuthentication defines console-specific authentication methods. // +optional ConsoleAuthentication *NXOSMethodList `json:"consoleAuthentication,omitempty"` - // ConfigCommandsAuthorization defines NX-OS config-commands authorization methods. - // Maps to: aaa authorization config-commands default + // ConfigCommandsAuthorization defines config-commands authorization methods. // +optional ConfigCommandsAuthorization *NXOSMethodList `json:"configCommandsAuthorization,omitempty"` } diff --git a/api/core/v1alpha1/aaa_types.go b/api/core/v1alpha1/aaa_types.go index 098b7d9a0..0382cf191 100644 --- a/api/core/v1alpha1/aaa_types.go +++ b/api/core/v1alpha1/aaa_types.go @@ -12,8 +12,7 @@ import ( // AAASpec defines the desired state of AAA. // -// It models the Authentication, Authorization, and Accounting (AAA) configuration on a network device, -// aligned with the OpenConfig system/aaa YANG model. +// It models the Authentication, Authorization, and Accounting (AAA) configuration on a network device. // +kubebuilder:validation:XValidation:rule="has(self.serverGroups) || has(self.authentication) || has(self.authorization) || has(self.accounting)",message="at least one of serverGroups, authentication, authorization, or accounting must be set" type AAASpec struct { // DeviceName is the name of the Device this object belongs to. The Device object must exist in the same namespace. @@ -28,7 +27,6 @@ type AAASpec struct { ProviderConfigRef *TypedLocalObjectReference `json:"providerConfigRef,omitempty"` // ServerGroups is the list of AAA server groups. - // OpenConfig: /system/aaa/server-groups/server-group // +optional // +listType=map // +listMapKey=name @@ -36,17 +34,14 @@ type AAASpec struct { ServerGroups []AAAServerGroup `json:"serverGroups,omitempty"` // Authentication defines the AAA authentication method list. - // OpenConfig: /system/aaa/authentication // +optional Authentication *AAAAuthentication `json:"authentication,omitempty"` // Authorization defines the AAA authorization method list. - // OpenConfig: /system/aaa/authorization // +optional Authorization *AAAAuthorization `json:"authorization,omitempty"` // Accounting defines the AAA accounting method list. - // OpenConfig: /system/aaa/accounting // +optional Accounting *AAAAccounting `json:"accounting,omitempty"` } @@ -63,7 +58,6 @@ const ( ) // AAAServerGroup represents a named group of AAA servers. -// OpenConfig: /system/aaa/server-groups/server-group[name] // +kubebuilder:validation:XValidation:rule="self.type != 'TACACS' || self.servers.all(s, has(s.tacacs))",message="servers in a TACACS group must have tacacs config" // +kubebuilder:validation:XValidation:rule="self.type != 'RADIUS' || self.servers.all(s, has(s.radius))",message="servers in a RADIUS group must have radius config" type AAAServerGroup struct { @@ -78,7 +72,6 @@ type AAAServerGroup struct { Type AAAServerGroupType `json:"type"` // Servers is the list of servers in this group. - // OpenConfig: /system/aaa/server-groups/server-group/servers/server // +required // +listType=map // +listMapKey=address @@ -98,7 +91,6 @@ type AAAServerGroup struct { } // AAAServer represents a single AAA server within a group. -// OpenConfig: /system/aaa/server-groups/server-group/servers/server[address] type AAAServer struct { // Address is the IP address or hostname of the server. // +required @@ -114,13 +106,11 @@ type AAAServer struct { // TACACS contains TACACS+ specific server configuration. // Required when the parent server group type is TACACS. - // OpenConfig augmentation: /system/aaa/server-groups/server-group/servers/server/tacacs // +optional TACACS *AAAServerTACACS `json:"tacacs,omitempty"` // RADIUS contains RADIUS specific server configuration. // Required when the parent server group type is RADIUS. - // OpenConfig augmentation: /system/aaa/server-groups/server-group/servers/server/radius // +optional RADIUS *AAAServerRADIUS `json:"radius,omitempty"` } @@ -166,7 +156,6 @@ type AAAServerRADIUS struct { } // AAAAuthentication defines the AAA authentication method list. -// OpenConfig: /system/aaa/authentication type AAAAuthentication struct { // Methods is the ordered list of authentication methods. // Methods are tried in order until one succeeds or all fail. @@ -178,7 +167,6 @@ type AAAAuthentication struct { } // AAAAuthorization defines the AAA authorization method list. -// OpenConfig: /system/aaa/authorization type AAAAuthorization struct { // Methods is the ordered list of authorization methods. // Methods are tried in order until one succeeds or all fail. @@ -190,7 +178,6 @@ type AAAAuthorization struct { } // AAAAccounting defines the AAA accounting method list. -// OpenConfig: /system/aaa/accounting type AAAAccounting struct { // Methods is the ordered list of accounting methods. // Methods are tried in order until one succeeds or all fail. diff --git a/internal/provider/cisco/nxos/aaa.go b/internal/provider/cisco/nxos/aaa.go index 2c8995871..6c8e4f68b 100644 --- a/internal/provider/cisco/nxos/aaa.go +++ b/internal/provider/cisco/nxos/aaa.go @@ -268,8 +268,8 @@ func MapFallbackFromMethodList(methods []v1alpha1.AAAMethod) string { return AAAValueNo } -// MapNXOSRealm maps an NX-OS method type string to NX-OS realm. -func MapNXOSRealm(methodType string) string { +// MapRealm maps a method type string to an NX-OS realm. +func MapRealm(methodType string) string { switch methodType { case "Group": return AAARealmTacacs @@ -282,8 +282,8 @@ func MapNXOSRealm(methodType string) string { } } -// MapNXOSLocal checks if local is in an NX-OS method list. -func MapNXOSLocal(methods []nxv1alpha1.NXOSMethod) string { +// MapLocal checks if local is in a method list. +func MapLocal(methods []nxv1alpha1.NXOSMethod) string { for _, m := range methods { if m.Type == "Local" { return AAAValueYes @@ -292,8 +292,8 @@ func MapNXOSLocal(methods []nxv1alpha1.NXOSMethod) string { return AAAValueNo } -// MapNXOSFallback determines fallback setting from an NX-OS method list. -func MapNXOSFallback(methods []nxv1alpha1.NXOSMethod) string { +// MapFallback determines fallback setting from a method list. +func MapFallback(methods []nxv1alpha1.NXOSMethod) string { if len(methods) > 1 { return AAAValueYes } diff --git a/internal/provider/cisco/nxos/provider.go b/internal/provider/cisco/nxos/provider.go index a8f7f8461..567d81dd3 100644 --- a/internal/provider/cisco/nxos/provider.go +++ b/internal/provider/cisco/nxos/provider.go @@ -3282,15 +3282,12 @@ func (p *Provider) EnsureAAA(ctx context.Context, req *provider.EnsureAAARequest } } - // Process server groups for _, group := range req.AAA.Spec.ServerGroups { switch group.Type { case v1alpha1.AAAServerGroupTypeTACACS: - // Enable TACACS+ feature tacacsFeature := TACACSFeatureEnabled conf = append(conf, &tacacsFeature) - // Configure individual TACACS+ server hosts for _, server := range group.Servers { srv := &TacacsPlusProvider{ Name: server.Address, @@ -3308,7 +3305,6 @@ func (p *Provider) EnsureAAA(ctx context.Context, req *provider.EnsureAAARequest conf = append(conf, srv) } - // Configure the TACACS+ server group grp := &TacacsPlusProviderGroup{ Name: group.Name, Vrf: group.VrfName, @@ -3320,7 +3316,6 @@ func (p *Provider) EnsureAAA(ctx context.Context, req *provider.EnsureAAARequest conf = append(conf, grp) case v1alpha1.AAAServerGroupTypeRADIUS: - // Configure individual RADIUS server hosts for _, server := range group.Servers { srv := &RadiusProvider{ Name: server.Address, @@ -3339,7 +3334,6 @@ func (p *Provider) EnsureAAA(ctx context.Context, req *provider.EnsureAAARequest conf = append(conf, srv) } - // Configure the RADIUS server group grp := &RadiusProviderGroup{ Name: group.Name, Vrf: group.VrfName, @@ -3352,7 +3346,6 @@ func (p *Provider) EnsureAAA(ctx context.Context, req *provider.EnsureAAARequest } } - // Configure AAA default authentication (from core API flat method list) if req.AAA.Spec.Authentication != nil && len(req.AAA.Spec.Authentication.Methods) > 0 { methods := req.AAA.Spec.Authentication.Methods authen := &AAADefaultAuth{ @@ -3369,24 +3362,22 @@ func (p *Provider) EnsureAAA(ctx context.Context, req *provider.EnsureAAARequest conf = append(conf, authen) } - // Configure AAA console authentication (from Cisco AAAConfig) if cfg.Spec.ConsoleAuthentication != nil && len(cfg.Spec.ConsoleAuthentication.Methods) > 0 { methods := cfg.Spec.ConsoleAuthentication.Methods consoleAuth := &AAAConsoleAuth{ ErrEn: cfg.Spec.LoginErrorEnable, - Fallback: MapNXOSFallback(methods), - Local: MapNXOSLocal(methods), + Fallback: MapFallback(methods), + Local: MapLocal(methods), } if methods[0].Type == "Group" { consoleAuth.Realm = MapRealmFromGroup(methods[0].GroupName, req.AAA.Spec.ServerGroups) consoleAuth.ProviderGroup = methods[0].GroupName } else { - consoleAuth.Realm = MapNXOSRealm(methods[0].Type) + consoleAuth.Realm = MapRealm(methods[0].Type) } conf = append(conf, consoleAuth) } - // Configure AAA authorization (from core API flat method list) if req.AAA.Spec.Authorization != nil && len(req.AAA.Spec.Authorization.Methods) > 0 { methods := req.AAA.Spec.Authorization.Methods author := &AAADefaultAuthor{ @@ -3399,12 +3390,11 @@ func (p *Provider) EnsureAAA(ctx context.Context, req *provider.EnsureAAARequest conf = append(conf, author) } - // Configure AAA config-commands authorization (from Cisco AAAConfig) if cfg.Spec.ConfigCommandsAuthorization != nil && len(cfg.Spec.ConfigCommandsAuthorization.Methods) > 0 { methods := cfg.Spec.ConfigCommandsAuthorization.Methods author := &AAADefaultAuthor{ CmdType: "config", - LocalRbac: MapNXOSLocal(methods) == AAAValueYes, + LocalRbac: MapLocal(methods) == AAAValueYes, } if methods[0].Type == "Group" { author.ProviderGroup = methods[0].GroupName @@ -3412,7 +3402,6 @@ func (p *Provider) EnsureAAA(ctx context.Context, req *provider.EnsureAAARequest conf = append(conf, author) } - // Configure AAA accounting (from core API flat method list) if req.AAA.Spec.Accounting != nil && len(req.AAA.Spec.Accounting.Methods) > 0 { methods := req.AAA.Spec.Accounting.Methods acct := &AAADefaultAcc{ @@ -3432,97 +3421,43 @@ func (p *Provider) EnsureAAA(ctx context.Context, req *provider.EnsureAAARequest } func (p *Provider) DeleteAAA(ctx context.Context, req *provider.DeleteAAARequest) error { - var conf []gnmiext.Configurable - - // Read Cisco-specific config from ProviderConfig - var cfg nxv1alpha1.AAAConfig - if req.ProviderConfig != nil { - if err := req.ProviderConfig.Into(&cfg); err != nil { - return err - } - } - - // Reset AAA accounting to local - if req.AAA.Spec.Accounting != nil { - conf = append(conf, &AAADefaultAcc{ - Name: "Accounting", - Realm: AAARealmLocal, - LocalRbac: true, - }) - } - - // Reset AAA authorization to local - if req.AAA.Spec.Authorization != nil || cfg.Spec.ConfigCommandsAuthorization != nil { - conf = append(conf, &AAADefaultAuthor{ - CmdType: "config", - ProviderGroup: "", - LocalRbac: true, - }) + // Reset all AAA method config to device defaults unconditionally. + // gNMI deletes are idempotent, so it is safe to reset even if a field + // was never configured. + conf := []gnmiext.Configurable{ + &AAADefaultAcc{Name: "Accounting", Realm: AAARealmLocal, LocalRbac: true}, + &AAADefaultAuthor{CmdType: "config", LocalRbac: true}, + &AAADefaultAuth{Realm: AAARealmLocal, Local: AAAValueYes, Fallback: AAAValueYes}, + &AAAConsoleAuth{Realm: AAARealmLocal, Local: AAAValueYes, Fallback: AAAValueYes}, } - // Reset AAA authentication to local - if req.AAA.Spec.Authentication != nil { - conf = append(conf, &AAADefaultAuth{ - Realm: AAARealmLocal, - Local: AAAValueYes, - Fallback: AAAValueYes, - ErrEn: false, - }) - } - - // Reset console authentication to local - if cfg.Spec.ConsoleAuthentication != nil { - conf = append(conf, &AAAConsoleAuth{ - Realm: AAARealmLocal, - Local: AAAValueYes, - Fallback: AAAValueYes, - ErrEn: false, - }) - } - - // Delete server groups and hosts - hasTACACS := false for _, group := range req.AAA.Spec.ServerGroups { switch group.Type { case v1alpha1.AAAServerGroupTypeTACACS: - hasTACACS = true - - grp := &TacacsPlusProviderGroup{Name: group.Name} - if err := p.client.Delete(ctx, grp); err != nil { + if err := p.client.Delete(ctx, &TacacsPlusProviderGroup{Name: group.Name}); err != nil { return err } for _, server := range group.Servers { - srv := &TacacsPlusProvider{Name: server.Address} - if err := p.client.Delete(ctx, srv); err != nil { + if err := p.client.Delete(ctx, &TacacsPlusProvider{Name: server.Address}); err != nil { return err } } + tacacsFeature := TACACSFeatureDisabled + conf = append(conf, &tacacsFeature) case v1alpha1.AAAServerGroupTypeRADIUS: - grp := &RadiusProviderGroup{Name: group.Name} - if err := p.client.Delete(ctx, grp); err != nil { + if err := p.client.Delete(ctx, &RadiusProviderGroup{Name: group.Name}); err != nil { return err } for _, server := range group.Servers { - srv := &RadiusProvider{Name: server.Address} - if err := p.client.Delete(ctx, srv); err != nil { + if err := p.client.Delete(ctx, &RadiusProvider{Name: server.Address}); err != nil { return err } } } } - // Disable TACACS+ feature - if hasTACACS { - tacacsFeature := TACACSFeatureDisabled - conf = append(conf, &tacacsFeature) - } - - if len(conf) > 0 { - return p.Update(ctx, conf...) - } - - return nil + return p.Update(ctx, conf...) } func init() { From 9172bab6639d9435c5dae3611970ed3200e8cfdd Mon Sep 17 00:00:00 2001 From: i521907 Date: Fri, 27 Mar 2026 09:36:25 -0400 Subject: [PATCH 07/32] fix: address felix review comments Signed-off-by: i521907 --- api/core/v1alpha1/aaa_types.go | 24 ++- api/core/v1alpha1/zz_generated.deepcopy.go | 23 ++- ...=> networking.metal.ironcore.dev_aaa.yaml} | 145 ++++++++++-------- ...working.metal.ironcore.dev_aaaconfigs.yaml | 36 ++--- config/default/kustomization.yaml | 2 +- config/rbac/role.yaml | 6 +- config/samples/networking_v1alpha1_aaa.yaml | 10 -- internal/controller/core/aaa_controller.go | 12 +- internal/provider/cisco/nxos/aaa.go | 7 +- internal/provider/cisco/nxos/provider.go | 12 +- internal/provider/provider.go | 4 +- 11 files changed, 157 insertions(+), 124 deletions(-) rename config/crd/bases/{networking.metal.ironcore.dev_aaas.yaml => networking.metal.ironcore.dev_aaa.yaml} (79%) diff --git a/api/core/v1alpha1/aaa_types.go b/api/core/v1alpha1/aaa_types.go index 0382cf191..a71a826c7 100644 --- a/api/core/v1alpha1/aaa_types.go +++ b/api/core/v1alpha1/aaa_types.go @@ -98,11 +98,9 @@ type AAAServer struct { // +kubebuilder:validation:MaxLength=253 Address string `json:"address"` - // Timeout is the response timeout in seconds for this server. + // Timeout is the response timeout for this server. // +optional - // +kubebuilder:validation:Minimum=1 - // +kubebuilder:validation:Maximum=60 - Timeout *int32 `json:"timeout,omitempty"` + Timeout *metav1.Duration `json:"timeout,omitempty"` // TACACS contains TACACS+ specific server configuration. // Required when the parent server group type is TACACS. @@ -125,7 +123,7 @@ type AAAServerTACACS struct { // +kubebuilder:default=49 Port int32 `json:"port,omitempty"` - // KeySecretRef is a reference to a secret containing the shared key for this TACACS+ server. + // KeySecretRef is a reference to a secret containing the plain text shared key for this TACACS+ server. // The secret must contain a key specified in the SecretKeySelector. // +required KeySecretRef SecretKeySelector `json:"keySecretRef"` @@ -133,23 +131,23 @@ type AAAServerTACACS struct { // AAAServerRADIUS contains RADIUS specific server configuration. type AAAServerRADIUS struct { - // AuthPort is the UDP port for RADIUS authentication requests. + // AuthenticationPort is the UDP port for RADIUS authentication requests. // Defaults to 1812 if not specified. // +optional // +kubebuilder:validation:Minimum=1 // +kubebuilder:validation:Maximum=65535 // +kubebuilder:default=1812 - AuthPort int32 `json:"authPort,omitempty"` + AuthenticationPort int32 `json:"authenticationPort,omitempty"` - // AcctPort is the UDP port for RADIUS accounting requests. + // AccountingPort is the UDP port for RADIUS accounting requests. // Defaults to 1813 if not specified. // +optional // +kubebuilder:validation:Minimum=1 // +kubebuilder:validation:Maximum=65535 // +kubebuilder:default=1813 - AcctPort int32 `json:"acctPort,omitempty"` + AccountingPort int32 `json:"accountingPort,omitempty"` - // KeySecretRef is a reference to a secret containing the shared key for this RADIUS server. + // KeySecretRef is a reference to a secret containing the plain text shared key for this RADIUS server. // The secret must contain a key specified in the SecretKeySelector. // +required KeySecretRef SecretKeySelector `json:"keySecretRef"` @@ -193,7 +191,6 @@ type AAAAccounting struct { type AAAMethod struct { // Type is the type of AAA method. // +required - // +kubebuilder:validation:Enum=Group;Local;None Type AAAMethodType `json:"type"` // GroupName is the name of the server group when Type is Group. @@ -228,14 +225,13 @@ type AAAStatus struct { // +kubebuilder:object:root=true // +kubebuilder:subresource:status -// +kubebuilder:resource:path=aaas +// +kubebuilder:resource:path=aaa // +kubebuilder:resource:singular=aaa -// +kubebuilder:resource:shortName=aaa // +kubebuilder:printcolumn:name="Device",type=string,JSONPath=`.spec.deviceRef.name` // +kubebuilder:printcolumn:name="Ready",type=string,JSONPath=`.status.conditions[?(@.type=="Ready")].status` // +kubebuilder:printcolumn:name="Age",type="date",JSONPath=".metadata.creationTimestamp" -// AAA is the Schema for the aaas API +// AAA is the Schema for the aaa API type AAA struct { metav1.TypeMeta `json:",inline"` metav1.ObjectMeta `json:"metadata,omitempty"` diff --git a/api/core/v1alpha1/zz_generated.deepcopy.go b/api/core/v1alpha1/zz_generated.deepcopy.go index d30b52074..b667e9e71 100644 --- a/api/core/v1alpha1/zz_generated.deepcopy.go +++ b/api/core/v1alpha1/zz_generated.deepcopy.go @@ -152,7 +152,7 @@ func (in *AAAServer) DeepCopyInto(out *AAAServer) { *out = *in if in.Timeout != nil { in, out := &in.Timeout, &out.Timeout - *out = new(int32) + *out = new(v1.Duration) **out = **in } if in.TACACS != nil { @@ -160,6 +160,11 @@ func (in *AAAServer) DeepCopyInto(out *AAAServer) { *out = new(AAAServerTACACS) **out = **in } + if in.RADIUS != nil { + in, out := &in.RADIUS, &out.RADIUS + *out = new(AAAServerRADIUS) + **out = **in + } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AAAServer. @@ -194,6 +199,22 @@ func (in *AAAServerGroup) DeepCopy() *AAAServerGroup { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *AAAServerRADIUS) DeepCopyInto(out *AAAServerRADIUS) { + *out = *in + out.KeySecretRef = in.KeySecretRef +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AAAServerRADIUS. +func (in *AAAServerRADIUS) DeepCopy() *AAAServerRADIUS { + if in == nil { + return nil + } + out := new(AAAServerRADIUS) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *AAAServerTACACS) DeepCopyInto(out *AAAServerTACACS) { *out = *in diff --git a/config/crd/bases/networking.metal.ironcore.dev_aaas.yaml b/config/crd/bases/networking.metal.ironcore.dev_aaa.yaml similarity index 79% rename from config/crd/bases/networking.metal.ironcore.dev_aaas.yaml rename to config/crd/bases/networking.metal.ironcore.dev_aaa.yaml index ced5f156c..d249c62b8 100644 --- a/config/crd/bases/networking.metal.ironcore.dev_aaas.yaml +++ b/config/crd/bases/networking.metal.ironcore.dev_aaa.yaml @@ -4,15 +4,13 @@ kind: CustomResourceDefinition metadata: annotations: controller-gen.kubebuilder.io/version: v0.20.1 - name: aaas.networking.metal.ironcore.dev + name: aaa.networking.metal.ironcore.dev spec: group: networking.metal.ironcore.dev names: kind: AAA listKind: AAAList - plural: aaas - shortNames: - - aaa + plural: aaa singular: aaa scope: Namespaced versions: @@ -29,7 +27,7 @@ spec: name: v1alpha1 schema: openAPIV3Schema: - description: AAA is the Schema for the aaas API + description: AAA is the Schema for the aaa API properties: apiVersion: description: |- @@ -54,9 +52,7 @@ spec: More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#spec-and-status properties: accounting: - description: |- - Accounting defines the AAA accounting method list. - OpenConfig: /system/aaa/accounting + description: Accounting defines the AAA accounting method list. properties: methods: description: |- @@ -71,16 +67,11 @@ spec: maxLength: 63 type: string type: - allOf: - - enum: - - Group - - Local - - None - - enum: - - Group - - Local - - None description: Type is the type of AAA method. + enum: + - Group + - Local + - None type: string required: - type @@ -96,9 +87,8 @@ spec: - methods type: object authentication: - description: |- - Authentication defines the AAA authentication method list. - OpenConfig: /system/aaa/authentication + description: Authentication defines the AAA authentication method + list. properties: methods: description: |- @@ -113,16 +103,11 @@ spec: maxLength: 63 type: string type: - allOf: - - enum: - - Group - - Local - - None - - enum: - - Group - - Local - - None description: Type is the type of AAA method. + enum: + - Group + - Local + - None type: string required: - type @@ -138,9 +123,7 @@ spec: - methods type: object authorization: - description: |- - Authorization defines the AAA authorization method list. - OpenConfig: /system/aaa/authorization + description: Authorization defines the AAA authorization method list. properties: methods: description: |- @@ -155,16 +138,11 @@ spec: maxLength: 63 type: string type: - allOf: - - enum: - - Group - - Local - - None - - enum: - - Group - - Local - - None description: Type is the type of AAA method. + enum: + - Group + - Local + - None type: string required: - type @@ -233,13 +211,9 @@ spec: type: object x-kubernetes-map-type: atomic serverGroups: - description: |- - ServerGroups is the list of AAA server groups. - OpenConfig: /system/aaa/server-groups/server-group + description: ServerGroups is the list of AAA server groups. items: - description: |- - AAAServerGroup represents a named group of AAA servers. - OpenConfig: /system/aaa/server-groups/server-group[name] + description: AAAServerGroup represents a named group of AAA servers. properties: name: description: Name is the name of the server group. @@ -247,13 +221,10 @@ spec: minLength: 1 type: string servers: - description: |- - Servers is the list of servers in this group. - OpenConfig: /system/aaa/server-groups/server-group/servers/server + description: Servers is the list of servers in this group. items: - description: |- - AAAServer represents a single AAA server within a group. - OpenConfig: /system/aaa/server-groups/server-group/servers/server[address] + description: AAAServer represents a single AAA server within + a group. properties: address: description: Address is the IP address or hostname of @@ -261,11 +232,66 @@ spec: maxLength: 253 minLength: 1 type: string + radius: + description: |- + RADIUS contains RADIUS specific server configuration. + Required when the parent server group type is RADIUS. + properties: + accountingPort: + default: 1813 + description: |- + AccountingPort is the UDP port for RADIUS accounting requests. + Defaults to 1813 if not specified. + format: int32 + maximum: 65535 + minimum: 1 + type: integer + authenticationPort: + default: 1812 + description: |- + AuthenticationPort is the UDP port for RADIUS authentication requests. + Defaults to 1812 if not specified. + format: int32 + maximum: 65535 + minimum: 1 + type: integer + keySecretRef: + description: |- + KeySecretRef is a reference to a secret containing the shared key for this RADIUS server. + The secret must contain a key specified in the SecretKeySelector. + properties: + key: + description: |- + Key is the of the entry in the secret resource's `data` or `stringData` + field to be used. + maxLength: 253 + minLength: 1 + type: string + name: + description: Name is unique within a namespace + to reference a secret resource. + maxLength: 253 + minLength: 1 + type: string + namespace: + description: |- + Namespace defines the space within which the secret name must be unique. + If omitted, the namespace of the object being reconciled will be used. + maxLength: 63 + minLength: 1 + type: string + required: + - key + - name + type: object + x-kubernetes-map-type: atomic + required: + - keySecretRef + type: object tacacs: description: |- TACACS contains TACACS+ specific server configuration. Required when the parent server group type is TACACS. - OpenConfig augmentation: /system/aaa/server-groups/server-group/servers/server/tacacs properties: keySecretRef: description: |- @@ -310,12 +336,9 @@ spec: - keySecretRef type: object timeout: - description: Timeout is the response timeout in seconds - for this server. - format: int32 - maximum: 60 - minimum: 1 - type: integer + description: Timeout is the response timeout for this + server. + type: string required: - address type: object @@ -349,6 +372,8 @@ spec: x-kubernetes-validations: - message: servers in a TACACS group must have tacacs config rule: self.type != 'TACACS' || self.servers.all(s, has(s.tacacs)) + - message: servers in a RADIUS group must have radius config + rule: self.type != 'RADIUS' || self.servers.all(s, has(s.radius)) maxItems: 8 type: array x-kubernetes-list-map-keys: diff --git a/config/crd/bases/nx.cisco.networking.metal.ironcore.dev_aaaconfigs.yaml b/config/crd/bases/nx.cisco.networking.metal.ironcore.dev_aaaconfigs.yaml index 4b1aba88e..29571f455 100644 --- a/config/crd/bases/nx.cisco.networking.metal.ironcore.dev_aaaconfigs.yaml +++ b/config/crd/bases/nx.cisco.networking.metal.ironcore.dev_aaaconfigs.yaml @@ -44,9 +44,8 @@ spec: More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#spec-and-status properties: configCommandsAuthorization: - description: |- - ConfigCommandsAuthorization defines NX-OS config-commands authorization methods. - Maps to: aaa authorization config-commands default + description: ConfigCommandsAuthorization defines config-commands authorization + methods. properties: methods: description: Methods is the ordered list of methods. @@ -77,9 +76,8 @@ spec: - methods type: object consoleAuthentication: - description: |- - ConsoleAuthentication defines NX-OS console-specific authentication methods. - Maps to: aaa authentication login console + description: ConsoleAuthentication defines console-specific authentication + methods. properties: methods: description: Methods is the ordered list of methods. @@ -110,24 +108,26 @@ spec: - methods type: object keyEncryption: - allOf: - - enum: - - Type6 - - Type7 - - Clear - - enum: - - Type6 - - Type7 - - Clear default: Type7 description: KeyEncryption specifies the default encryption type for TACACS+ keys. + enum: + - Type6 + - Type7 + - Clear type: string loginErrorEnable: - description: |- - LoginErrorEnable enables login error messages (NX-OS specific). - Maps to: aaa authentication login error-enable + description: LoginErrorEnable enables login error messages. type: boolean + radiusKeyEncryption: + default: Type7 + description: RADIUSKeyEncryption specifies the default encryption + type for RADIUS server keys. + enum: + - Type6 + - Type7 + - Clear + type: string type: object required: - spec diff --git a/config/default/kustomization.yaml b/config/default/kustomization.yaml index 2a2e5cd92..fd3c7529c 100644 --- a/config/default/kustomization.yaml +++ b/config/default/kustomization.yaml @@ -24,7 +24,7 @@ resources: # [CERTMANAGER] To enable cert-manager, uncomment all sections with 'CERTMANAGER'. 'WEBHOOK' components are required. - ../certmanager # [PROMETHEUS] To enable prometheus monitor, uncomment all sections with 'PROMETHEUS'. -- ../prometheus +#- ../prometheus # [METRICS] Expose the controller manager metrics service. - metrics_service.yaml # [PROVISIONING] Expose the controller manager provisioning service. diff --git a/config/rbac/role.yaml b/config/rbac/role.yaml index 116e4fa05..5a8c24f02 100644 --- a/config/rbac/role.yaml +++ b/config/rbac/role.yaml @@ -42,7 +42,7 @@ rules: - apiGroups: - networking.metal.ironcore.dev resources: - - aaas + - aaa - accesscontrollists - banners - bgp @@ -78,7 +78,7 @@ rules: - apiGroups: - networking.metal.ironcore.dev resources: - - aaas/finalizers + - aaa/finalizers - accesscontrollists/finalizers - banners/finalizers - bgp/finalizers @@ -108,7 +108,7 @@ rules: - apiGroups: - networking.metal.ironcore.dev resources: - - aaas/status + - aaa/status - accesscontrollists/status - banners/status - bgp/status diff --git a/config/samples/networking_v1alpha1_aaa.yaml b/config/samples/networking_v1alpha1_aaa.yaml index 94e2d6b06..fe13e6e2f 100644 --- a/config/samples/networking_v1alpha1_aaa.yaml +++ b/config/samples/networking_v1alpha1_aaa.yaml @@ -26,13 +26,11 @@ metadata: spec: keyEncryption: Type7 loginErrorEnable: true - # NX-OS: aaa authentication login console group GR_TACACS local consoleAuthentication: methods: - type: Group groupName: GR_TACACS - type: Local - # NX-OS: aaa authorization config-commands default group GR_TACACS local configCommandsAuthorization: methods: - type: Group @@ -55,8 +53,6 @@ spec: kind: AAAConfig name: aaa-tacacs-nxos - # TACACS+ server group with nested servers - # OpenConfig: /system/aaa/server-groups/server-group serverGroups: - name: GR_TACACS type: TACACS @@ -79,23 +75,17 @@ spec: name: tacacs-server-keys key: server-key - # AAA authentication method list - # OpenConfig: /system/aaa/authentication authentication: methods: - type: Group groupName: GR_TACACS - # AAA authorization method list - # OpenConfig: /system/aaa/authorization authorization: methods: - type: Group groupName: GR_TACACS - type: Local - # AAA accounting method list - # OpenConfig: /system/aaa/accounting accounting: methods: - type: Group diff --git a/internal/controller/core/aaa_controller.go b/internal/controller/core/aaa_controller.go index 1370d88cd..ca65532eb 100644 --- a/internal/controller/core/aaa_controller.go +++ b/internal/controller/core/aaa_controller.go @@ -26,6 +26,7 @@ import ( "sigs.k8s.io/controller-runtime/pkg/predicate" "github.com/ironcore-dev/network-operator/api/core/v1alpha1" + "github.com/ironcore-dev/network-operator/internal/annotations" "github.com/ironcore-dev/network-operator/internal/clientutil" "github.com/ironcore-dev/network-operator/internal/conditions" "github.com/ironcore-dev/network-operator/internal/deviceutil" @@ -52,9 +53,9 @@ type AAAReconciler struct { Locker *resourcelock.ResourceLocker } -// +kubebuilder:rbac:groups=networking.metal.ironcore.dev,resources=aaas,verbs=get;list;watch;create;update;patch;delete -// +kubebuilder:rbac:groups=networking.metal.ironcore.dev,resources=aaas/status,verbs=get;update;patch -// +kubebuilder:rbac:groups=networking.metal.ironcore.dev,resources=aaas/finalizers,verbs=update +// +kubebuilder:rbac:groups=networking.metal.ironcore.dev,resources=aaa,verbs=get;list;watch;create;update;patch;delete +// +kubebuilder:rbac:groups=networking.metal.ironcore.dev,resources=aaa/status,verbs=get;update;patch +// +kubebuilder:rbac:groups=networking.metal.ironcore.dev,resources=aaa/finalizers,verbs=update // +kubebuilder:rbac:groups=core,resources=events,verbs=create;patch // +kubebuilder:rbac:groups=core,resources=secrets,verbs=get;list;watch @@ -101,6 +102,11 @@ func (r *AAAReconciler) Reconcile(ctx context.Context, req ctrl.Request) (_ ctrl return ctrl.Result{}, err } + if annotations.IsPaused(device, obj) { + log.Info("Reconciliation is paused for this object") + return ctrl.Result{}, nil + } + if err := r.Locker.AcquireLock(ctx, device.Name, "aaa-controller"); err != nil { if errors.Is(err, resourcelock.ErrLockAlreadyHeld) { log.Info("Device is already locked, requeuing reconciliation") diff --git a/internal/provider/cisco/nxos/aaa.go b/internal/provider/cisco/nxos/aaa.go index 6c8e4f68b..902a9e54b 100644 --- a/internal/provider/cisco/nxos/aaa.go +++ b/internal/provider/cisco/nxos/aaa.go @@ -22,17 +22,12 @@ var ( ) // TACACSFeature enables/disables the TACACS+ feature on NX-OS. -type TACACSFeature string +type TACACSFeature AdminSt func (*TACACSFeature) XPath() string { return "System/fm-items/tacacsplus-items/adminSt" } -const ( - TACACSFeatureEnabled TACACSFeature = "enabled" - TACACSFeatureDisabled TACACSFeature = "disabled" -) - // AAA configuration constants const ( AAARealmTacacs = "tacacs" diff --git a/internal/provider/cisco/nxos/provider.go b/internal/provider/cisco/nxos/provider.go index 567d81dd3..e1a6193a7 100644 --- a/internal/provider/cisco/nxos/provider.go +++ b/internal/provider/cisco/nxos/provider.go @@ -3285,7 +3285,7 @@ func (p *Provider) EnsureAAA(ctx context.Context, req *provider.EnsureAAARequest for _, group := range req.AAA.Spec.ServerGroups { switch group.Type { case v1alpha1.AAAServerGroupTypeTACACS: - tacacsFeature := TACACSFeatureEnabled + tacacsFeature := TACACSFeature(AdminStEnabled) conf = append(conf, &tacacsFeature) for _, server := range group.Servers { @@ -3300,7 +3300,7 @@ func (p *Provider) EnsureAAA(ctx context.Context, req *provider.EnsureAAARequest srv.Key = key } if server.Timeout != nil { - srv.Timeout = *server.Timeout + srv.Timeout = int32(server.Timeout.Duration.Seconds()) } conf = append(conf, srv) } @@ -3322,14 +3322,14 @@ func (p *Provider) EnsureAAA(ctx context.Context, req *provider.EnsureAAARequest KeyEnc: MapRADIUSKeyEncryption(cfg.Spec.RADIUSKeyEncryption), } if server.RADIUS != nil { - srv.AuthPort = server.RADIUS.AuthPort - srv.AcctPort = server.RADIUS.AcctPort + srv.AuthPort = server.RADIUS.AuthenticationPort + srv.AcctPort = server.RADIUS.AccountingPort } if key, ok := req.RADIUSServerKeys[server.Address]; ok { srv.Key = key } if server.Timeout != nil { - srv.Timeout = *server.Timeout + srv.Timeout = int32(server.Timeout.Duration.Seconds()) } conf = append(conf, srv) } @@ -3442,7 +3442,7 @@ func (p *Provider) DeleteAAA(ctx context.Context, req *provider.DeleteAAARequest return err } } - tacacsFeature := TACACSFeatureDisabled + tacacsFeature := TACACSFeature(AdminStDisabled) conf = append(conf, &tacacsFeature) case v1alpha1.AAAServerGroupTypeRADIUS: diff --git a/internal/provider/provider.go b/internal/provider/provider.go index be977e3cc..938e5bbee 100644 --- a/internal/provider/provider.go +++ b/internal/provider/provider.go @@ -638,10 +638,10 @@ type AAAProvider interface { type EnsureAAARequest struct { AAA *v1alpha1.AAA ProviderConfig *ProviderConfig - // TACACSServerKeys contains the decrypted keys for each TACACS+ server, + // TACACSServerKeys contains the plain text keys for each TACACS+ server, // keyed by server address. TACACSServerKeys map[string]string - // RADIUSServerKeys contains the decrypted shared secrets for each RADIUS server, + // RADIUSServerKeys contains the plain text shared secrets for each RADIUS server, // keyed by server address. RADIUSServerKeys map[string]string } From 3a2b86f8a7f43ff4954eb0846acd79dd84e9465a Mon Sep 17 00:00:00 2001 From: i521907 Date: Fri, 27 Mar 2026 09:45:36 -0400 Subject: [PATCH 08/32] fix: remove embedded Duration field from selector Signed-off-by: i521907 --- internal/provider/cisco/nxos/provider.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/internal/provider/cisco/nxos/provider.go b/internal/provider/cisco/nxos/provider.go index e1a6193a7..cd4861571 100644 --- a/internal/provider/cisco/nxos/provider.go +++ b/internal/provider/cisco/nxos/provider.go @@ -3300,7 +3300,7 @@ func (p *Provider) EnsureAAA(ctx context.Context, req *provider.EnsureAAARequest srv.Key = key } if server.Timeout != nil { - srv.Timeout = int32(server.Timeout.Duration.Seconds()) + srv.Timeout = int32(server.Timeout.Seconds()) } conf = append(conf, srv) } @@ -3329,7 +3329,7 @@ func (p *Provider) EnsureAAA(ctx context.Context, req *provider.EnsureAAARequest srv.Key = key } if server.Timeout != nil { - srv.Timeout = int32(server.Timeout.Duration.Seconds()) + srv.Timeout = int32(server.Timeout.Seconds()) } conf = append(conf, srv) } From 391d7d0ae8505f699b92511445b6ce257d3e3851 Mon Sep 17 00:00:00 2001 From: Pujol Date: Wed, 25 Mar 2026 13:50:21 +0100 Subject: [PATCH 09/32] [NXOS] Add `DHCPRelay` provider implementation Enable or disable the DHCP feature based on AdminState. When enabled, configure DHCP relay on each referenced interface with the specified server addresses. The provider uses the VRF context from VrfRef (or the NXOS default "!unspecified" if no VRF is specified) when configuring server addresses. The implementation uses the Update operation to ensure stale DHCP relay entries are removed when the configuration changes. This also affects entries referencing interfaces not managed by the operator. The entire tree is removed on deletion, affecting non-managed interfaces., It leaves the DHCP feature in its current state. GetDHCPRelayStatus queries the device for all interfaces with DHCP relay configured and returns their names. Signed-off-by: i521907 --- internal/controller/core/aaa_controller.go | 7 +++---- internal/provider/cisco/nxos/provider.go | 1 + 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/internal/controller/core/aaa_controller.go b/internal/controller/core/aaa_controller.go index ca65532eb..f66176b0e 100644 --- a/internal/controller/core/aaa_controller.go +++ b/internal/controller/core/aaa_controller.go @@ -26,10 +26,10 @@ import ( "sigs.k8s.io/controller-runtime/pkg/predicate" "github.com/ironcore-dev/network-operator/api/core/v1alpha1" - "github.com/ironcore-dev/network-operator/internal/annotations" "github.com/ironcore-dev/network-operator/internal/clientutil" "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" ) @@ -102,9 +102,8 @@ func (r *AAAReconciler) Reconcile(ctx context.Context, req ctrl.Request) (_ ctrl return ctrl.Result{}, err } - if annotations.IsPaused(device, obj) { - log.Info("Reconciliation is paused for this object") - return ctrl.Result{}, nil + if isPaused, requeue, err := paused.EnsureCondition(ctx, r.Client, device, obj); isPaused || requeue || err != nil { + return ctrl.Result{}, err } if err := r.Locker.AcquireLock(ctx, device.Name, "aaa-controller"); err != nil { diff --git a/internal/provider/cisco/nxos/provider.go b/internal/provider/cisco/nxos/provider.go index cd4861571..324872613 100644 --- a/internal/provider/cisco/nxos/provider.go +++ b/internal/provider/cisco/nxos/provider.go @@ -63,6 +63,7 @@ var ( _ provider.LLDPProvider = (*Provider)(nil) _ provider.DHCPRelayProvider = (*Provider)(nil) _ provider.AAAProvider = (*Provider)(nil) + _ provider.DHCPRelayProvider = (*Provider)(nil) ) type Provider struct { From 25fbb5214b1164f87e7ba1c74b62171445830d36 Mon Sep 17 00:00:00 2001 From: i521907 Date: Mon, 13 Apr 2026 09:09:38 -0400 Subject: [PATCH 10/32] fix: address AAA PR review comments Signed-off-by: i521907 --- api/cisco/nx/v1alpha1/aaaconfig_types.go | 15 +-- .../nx/v1alpha1/zz_generated.deepcopy.go | 17 +--- cmd/main.go | 2 +- config/default/kustomization.yaml | 2 +- internal/controller/core/aaa_controller.go | 22 ++--- internal/provider/cisco/nxos/aaa.go | 68 +++++++++++--- internal/provider/cisco/nxos/aaa_test.go | 46 ++++++++++ internal/provider/cisco/nxos/provider.go | 91 ++++++++++--------- .../cisco/nxos/testdata/aaa_acct.json | 10 ++ .../cisco/nxos/testdata/aaa_acct.json.txt | 1 + .../cisco/nxos/testdata/aaa_author.json | 14 +++ .../cisco/nxos/testdata/aaa_author.json.txt | 1 + .../cisco/nxos/testdata/aaa_consoleauth.json | 12 +++ .../nxos/testdata/aaa_consoleauth.json.txt | 1 + .../cisco/nxos/testdata/aaa_defaultauth.json | 13 +++ .../nxos/testdata/aaa_defaultauth.json.txt | 2 + .../cisco/nxos/testdata/aaa_tacacs_group.json | 22 +++++ .../nxos/testdata/aaa_tacacs_group.json.txt | 4 + .../nxos/testdata/aaa_tacacs_server.json | 16 ++++ .../nxos/testdata/aaa_tacacs_server.json.txt | 1 + 20 files changed, 258 insertions(+), 102 deletions(-) create mode 100644 internal/provider/cisco/nxos/aaa_test.go create mode 100644 internal/provider/cisco/nxos/testdata/aaa_acct.json create mode 100644 internal/provider/cisco/nxos/testdata/aaa_acct.json.txt create mode 100644 internal/provider/cisco/nxos/testdata/aaa_author.json create mode 100644 internal/provider/cisco/nxos/testdata/aaa_author.json.txt create mode 100644 internal/provider/cisco/nxos/testdata/aaa_consoleauth.json create mode 100644 internal/provider/cisco/nxos/testdata/aaa_consoleauth.json.txt create mode 100644 internal/provider/cisco/nxos/testdata/aaa_defaultauth.json create mode 100644 internal/provider/cisco/nxos/testdata/aaa_defaultauth.json.txt create mode 100644 internal/provider/cisco/nxos/testdata/aaa_tacacs_group.json create mode 100644 internal/provider/cisco/nxos/testdata/aaa_tacacs_group.json.txt create mode 100644 internal/provider/cisco/nxos/testdata/aaa_tacacs_server.json create mode 100644 internal/provider/cisco/nxos/testdata/aaa_tacacs_server.json.txt diff --git a/api/cisco/nx/v1alpha1/aaaconfig_types.go b/api/cisco/nx/v1alpha1/aaaconfig_types.go index c58b7cc27..2959f3d77 100644 --- a/api/cisco/nx/v1alpha1/aaaconfig_types.go +++ b/api/cisco/nx/v1alpha1/aaaconfig_types.go @@ -67,20 +67,7 @@ type NXOSMethodList struct { // +listType=atomic // +kubebuilder:validation:MinItems=1 // +kubebuilder:validation:MaxItems=4 - Methods []NXOSMethod `json:"methods"` -} - -// NXOSMethod represents a single AAA method in an NX-OS context. -type NXOSMethod struct { - // Type is the method type. - // +required - // +kubebuilder:validation:Enum=Group;Local;None - Type string `json:"type"` - - // GroupName is the server group name when Type is Group. - // +optional - // +kubebuilder:validation:MaxLength=63 - GroupName string `json:"groupName,omitempty"` + Methods []v1alpha1.AAAMethod `json:"methods"` } // +kubebuilder:object:root=true diff --git a/api/cisco/nx/v1alpha1/zz_generated.deepcopy.go b/api/cisco/nx/v1alpha1/zz_generated.deepcopy.go index 9c9a3b584..dc52921ab 100644 --- a/api/cisco/nx/v1alpha1/zz_generated.deepcopy.go +++ b/api/cisco/nx/v1alpha1/zz_generated.deepcopy.go @@ -727,27 +727,12 @@ func (in *ManagementAccessConfigSpec) DeepCopy() *ManagementAccessConfigSpec { return out } -// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *NXOSMethod) DeepCopyInto(out *NXOSMethod) { - *out = *in -} - -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new NXOSMethod. -func (in *NXOSMethod) DeepCopy() *NXOSMethod { - if in == nil { - return nil - } - out := new(NXOSMethod) - in.DeepCopyInto(out) - return out -} - // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *NXOSMethodList) DeepCopyInto(out *NXOSMethodList) { *out = *in if in.Methods != nil { in, out := &in.Methods, &out.Methods - *out = make([]NXOSMethod, len(*in)) + *out = make([]corev1alpha1.AAAMethod, len(*in)) copy(*out, *in) } } diff --git a/cmd/main.go b/cmd/main.go index c35e45f3a..d306ff98f 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -579,7 +579,7 @@ func main() { //nolint:gocyclo if err := (&corecontroller.AAAReconciler{ Client: mgr.GetClient(), Scheme: mgr.GetScheme(), - Recorder: mgr.GetEventRecorderFor("aaa-controller"), + Recorder: mgr.GetEventRecorder("aaa-controller"), WatchFilterValue: watchFilterValue, Provider: prov, Locker: locker, diff --git a/config/default/kustomization.yaml b/config/default/kustomization.yaml index fd3c7529c..2a2e5cd92 100644 --- a/config/default/kustomization.yaml +++ b/config/default/kustomization.yaml @@ -24,7 +24,7 @@ resources: # [CERTMANAGER] To enable cert-manager, uncomment all sections with 'CERTMANAGER'. 'WEBHOOK' components are required. - ../certmanager # [PROMETHEUS] To enable prometheus monitor, uncomment all sections with 'PROMETHEUS'. -#- ../prometheus +- ../prometheus # [METRICS] Expose the controller manager metrics service. - metrics_service.yaml # [PROVISIONING] Expose the controller manager provisioning service. diff --git a/internal/controller/core/aaa_controller.go b/internal/controller/core/aaa_controller.go index f66176b0e..c38b1061e 100644 --- a/internal/controller/core/aaa_controller.go +++ b/internal/controller/core/aaa_controller.go @@ -16,7 +16,7 @@ import ( 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/record" + "k8s.io/client-go/tools/events" "k8s.io/klog/v2" ctrl "sigs.k8s.io/controller-runtime" "sigs.k8s.io/controller-runtime/pkg/builder" @@ -44,7 +44,7 @@ type AAAReconciler struct { // Recorder is used to record events for the controller. // More info: https://book.kubebuilder.io/reference/raising-events - Recorder record.EventRecorder + Recorder events.EventRecorder // Provider is the driver that will be used to create & delete the AAA configuration. Provider provider.ProviderFunc @@ -56,7 +56,7 @@ type AAAReconciler struct { // +kubebuilder:rbac:groups=networking.metal.ironcore.dev,resources=aaa,verbs=get;list;watch;create;update;patch;delete // +kubebuilder:rbac:groups=networking.metal.ironcore.dev,resources=aaa/status,verbs=get;update;patch // +kubebuilder:rbac:groups=networking.metal.ironcore.dev,resources=aaa/finalizers,verbs=update -// +kubebuilder:rbac:groups=core,resources=events,verbs=create;patch +// +kubebuilder:rbac:groups=events.k8s.io,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 @@ -69,14 +69,14 @@ type AAAReconciler struct { // - https://ahmet.im/blog/controller-pitfalls/#reconcile-method-shape func (r *AAAReconciler) Reconcile(ctx context.Context, req ctrl.Request) (_ ctrl.Result, reterr error) { log := ctrl.LoggerFrom(ctx) - log.Info("Reconciling resource") + log.V(3).Info("Reconciling resource") obj := new(v1alpha1.AAA) 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") + log.V(3).Info("Resource not found. Ignoring since object must be deleted") return ctrl.Result{}, nil } // Error reading the object - requeue the request. @@ -108,8 +108,8 @@ func (r *AAAReconciler) Reconcile(ctx context.Context, req ctrl.Request) (_ ctrl if err := r.Locker.AcquireLock(ctx, device.Name, "aaa-controller"); err != nil { if errors.Is(err, resourcelock.ErrLockAlreadyHeld) { - log.Info("Device is already locked, requeuing reconciliation") - return ctrl.Result{RequeueAfter: time.Second * 5}, nil + log.V(3).Info("Device is already locked, requeuing reconciliation") + return ctrl.Result{RequeueAfter: Jitter(time.Second), Priority: new(LockWaitPriorityDefault)}, nil } log.Error(err, "Failed to acquire device lock") return ctrl.Result{}, err @@ -154,7 +154,7 @@ func (r *AAAReconciler) Reconcile(ctx context.Context, req ctrl.Request) (_ ctrl return ctrl.Result{}, err } } - log.Info("Resource is being deleted, skipping reconciliation") + log.V(3).Info("Resource is being deleted, skipping reconciliation") return ctrl.Result{}, nil } @@ -165,13 +165,13 @@ func (r *AAAReconciler) Reconcile(ctx context.Context, req ctrl.Request) (_ ctrl log.Error(err, "Failed to add finalizer to resource") return ctrl.Result{}, err } - log.Info("Added finalizer to resource") + log.V(1).Info("Added finalizer to resource") return ctrl.Result{}, nil } orig := obj.DeepCopy() if conditions.InitializeConditions(obj, v1alpha1.ReadyCondition) { - log.Info("Initializing status conditions") + log.V(1).Info("Initializing status conditions") return ctrl.Result{}, r.Status().Update(ctx, obj) } @@ -336,7 +336,7 @@ func (r *AAAReconciler) secretToAAA(ctx context.Context, obj client.Object) []ct for _, server := range group.Servers { if (server.TACACS != nil && server.TACACS.KeySecretRef.Name == secret.Name && a.Namespace == secret.Namespace) || (server.RADIUS != nil && server.RADIUS.KeySecretRef.Name == secret.Name && a.Namespace == secret.Namespace) { - log.Info("Enqueuing AAA for reconciliation", "AAA", klog.KObj(&a)) + log.V(2).Info("Enqueuing AAA for reconciliation", "AAA", klog.KObj(&a)) requests = append(requests, ctrl.Request{ NamespacedName: client.ObjectKey{ Name: a.Name, diff --git a/internal/provider/cisco/nxos/aaa.go b/internal/provider/cisco/nxos/aaa.go index 902a9e54b..2e4b62b96 100644 --- a/internal/provider/cisco/nxos/aaa.go +++ b/internal/provider/cisco/nxos/aaa.go @@ -6,19 +6,19 @@ package nxos import ( nxv1alpha1 "github.com/ironcore-dev/network-operator/api/cisco/nx/v1alpha1" "github.com/ironcore-dev/network-operator/api/core/v1alpha1" - gnmiext "github.com/ironcore-dev/network-operator/internal/provider/cisco/gnmiext/v2" + "github.com/ironcore-dev/network-operator/internal/transport/gnmiext" ) var ( - _ gnmiext.Configurable = (*TACACSFeature)(nil) - _ gnmiext.Configurable = (*TacacsPlusProvider)(nil) - _ gnmiext.Configurable = (*TacacsPlusProviderGroup)(nil) - _ gnmiext.Configurable = (*RadiusProvider)(nil) - _ gnmiext.Configurable = (*RadiusProviderGroup)(nil) - _ gnmiext.Configurable = (*AAADefaultAuth)(nil) - _ gnmiext.Configurable = (*AAAConsoleAuth)(nil) - _ gnmiext.Configurable = (*AAADefaultAuthor)(nil) - _ gnmiext.Configurable = (*AAADefaultAcc)(nil) + _ gnmiext.DataElement = (*TACACSFeature)(nil) + _ gnmiext.DataElement = (*TacacsPlusProvider)(nil) + _ gnmiext.DataElement = (*TacacsPlusProviderGroup)(nil) + _ gnmiext.DataElement = (*RadiusProvider)(nil) + _ gnmiext.DataElement = (*RadiusProviderGroup)(nil) + _ gnmiext.DataElement = (*AAADefaultAuth)(nil) + _ gnmiext.DataElement = (*AAAConsoleAuth)(nil) + _ gnmiext.DataElement = (*AAADefaultAuthor)(nil) + _ gnmiext.DataElement = (*AAADefaultAcc)(nil) ) // TACACSFeature enables/disables the TACACS+ feature on NX-OS. @@ -74,6 +74,26 @@ type TacacsPlusProviderGroupRefItems struct { ProviderRefList gnmiext.List[string, *TacacsPlusProviderRef] `json:"ProviderRef-list,omitzero"` } +// TacacsPlusProviderItems is the container for all TACACS+ server configurations on the device. +// Used for reading the current state — not for writing. +type TacacsPlusProviderItems struct { + ProviderList []TacacsPlusProvider `json:"TacacsPlusProvider-list,omitzero"` +} + +func (*TacacsPlusProviderItems) XPath() string { + return "System/userext-items/tacacsext-items/tacacsplusprovider-items" +} + +// TacacsPlusProviderGroupItems is the container for all TACACS+ server group configurations on the device. +// Used for reading the current state — not for writing. +type TacacsPlusProviderGroupItems struct { + GroupList []TacacsPlusProviderGroup `json:"TacacsPlusProviderGroup-list,omitzero"` +} + +func (*TacacsPlusProviderGroupItems) XPath() string { + return "System/userext-items/tacacsext-items/tacacsplusprovidergroup-items" +} + type TacacsPlusProviderRef struct { Name string `json:"name"` } @@ -116,6 +136,26 @@ type RadiusProviderGroupRefItems struct { ProviderRefList gnmiext.List[string, *RadiusProviderRef] `json:"ProviderRef-list,omitzero"` } +// RadiusProviderItems is the container for all RADIUS server configurations on the device. +// Used for reading the current state — not for writing. +type RadiusProviderItems struct { + ProviderList []RadiusProvider `json:"RadiusProvider-list,omitzero"` +} + +func (*RadiusProviderItems) XPath() string { + return "System/userext-items/radiusext-items/radiusprovider-items" +} + +// RadiusProviderGroupItems is the container for all RADIUS server group configurations on the device. +// Used for reading the current state — not for writing. +type RadiusProviderGroupItems struct { + GroupList []RadiusProviderGroup `json:"RadiusProviderGroup-list,omitzero"` +} + +func (*RadiusProviderGroupItems) XPath() string { + return "System/userext-items/radiusext-items/radiusprovidergroup-items" +} + type RadiusProviderRef struct { Name string `json:"name"` } @@ -168,8 +208,8 @@ func (a *AAADefaultAuthor) XPath() string { } // AAADefaultAcc represents AAA default accounting configuration. +// Note: "name" is a read-only operational field on NX-OS and must not be sent. type AAADefaultAcc struct { - Name string `json:"name,omitempty"` Realm string `json:"realm,omitempty"` ProviderGroup string `json:"providerGroup,omitempty"` LocalRbac bool `json:"localRbac,omitempty"` @@ -278,9 +318,9 @@ func MapRealm(methodType string) string { } // MapLocal checks if local is in a method list. -func MapLocal(methods []nxv1alpha1.NXOSMethod) string { +func MapLocal(methods []v1alpha1.AAAMethod) string { for _, m := range methods { - if m.Type == "Local" { + if m.Type == v1alpha1.AAAMethodTypeLocal { return AAAValueYes } } @@ -288,7 +328,7 @@ func MapLocal(methods []nxv1alpha1.NXOSMethod) string { } // MapFallback determines fallback setting from a method list. -func MapFallback(methods []nxv1alpha1.NXOSMethod) string { +func MapFallback(methods []v1alpha1.AAAMethod) string { if len(methods) > 1 { return AAAValueYes } diff --git a/internal/provider/cisco/nxos/aaa_test.go b/internal/provider/cisco/nxos/aaa_test.go new file mode 100644 index 000000000..b9dfeaeb0 --- /dev/null +++ b/internal/provider/cisco/nxos/aaa_test.go @@ -0,0 +1,46 @@ +// SPDX-FileCopyrightText: 2025 SAP SE or an SAP affiliate company and IronCore contributors +// SPDX-License-Identifier: Apache-2.0 + +package nxos + +func init() { + Register("aaa_tacacs_server", &TacacsPlusProvider{ + Name: "10.1.1.1", + Port: 49, + KeyEnc: "7", + Timeout: 5, + }) + + tacacsGroup := &TacacsPlusProviderGroup{ + Name: "TACACS-SERVERS", + Vrf: "management", + SrcIf: "mgmt0", + } + tacacsGroup.ProviderRefItems.ProviderRefList.Set(&TacacsPlusProviderRef{Name: "10.1.1.1"}) + Register("aaa_tacacs_group", tacacsGroup) + + Register("aaa_defaultauth", &AAADefaultAuth{ + Realm: AAARealmTacacs, + ProviderGroup: "TACACS-SERVERS", + Fallback: AAAValueYes, + Local: AAAValueYes, + ErrEn: true, + }) + + Register("aaa_consoleauth", &AAAConsoleAuth{ + Realm: AAARealmTacacs, + ProviderGroup: "TACACS-SERVERS", + Fallback: AAAValueYes, + Local: AAAValueYes, + }) + + Register("aaa_author", &AAADefaultAuthor{ + CmdType: "config", + ProviderGroup: "TACACS-SERVERS", + }) + + Register("aaa_acct", &AAADefaultAcc{ + Realm: AAARealmTacacs, + ProviderGroup: "TACACS-SERVERS", + }) +} diff --git a/internal/provider/cisco/nxos/provider.go b/internal/provider/cisco/nxos/provider.go index 324872613..26d922b1c 100644 --- a/internal/provider/cisco/nxos/provider.go +++ b/internal/provider/cisco/nxos/provider.go @@ -3273,7 +3273,7 @@ func (p *Provider) GetDHCPRelayStatus(ctx context.Context, req *provider.DHCPRel } func (p *Provider) EnsureAAA(ctx context.Context, req *provider.EnsureAAARequest) error { - var conf []gnmiext.Configurable + var conf []gnmiext.DataElement // Read Cisco-specific config from ProviderConfig var cfg nxv1alpha1.AAAConfig @@ -3292,11 +3292,9 @@ func (p *Provider) EnsureAAA(ctx context.Context, req *provider.EnsureAAARequest for _, server := range group.Servers { srv := &TacacsPlusProvider{ Name: server.Address, + Port: server.TACACS.Port, KeyEnc: MapKeyEncryption(cfg.Spec.KeyEncryption), } - if server.TACACS != nil { - srv.Port = server.TACACS.Port - } if key, ok := req.TACACSServerKeys[server.Address]; ok { srv.Key = key } @@ -3319,12 +3317,10 @@ func (p *Provider) EnsureAAA(ctx context.Context, req *provider.EnsureAAARequest case v1alpha1.AAAServerGroupTypeRADIUS: for _, server := range group.Servers { srv := &RadiusProvider{ - Name: server.Address, - KeyEnc: MapRADIUSKeyEncryption(cfg.Spec.RADIUSKeyEncryption), - } - if server.RADIUS != nil { - srv.AuthPort = server.RADIUS.AuthenticationPort - srv.AcctPort = server.RADIUS.AccountingPort + Name: server.Address, + AuthPort: server.RADIUS.AuthenticationPort, + AcctPort: server.RADIUS.AccountingPort, + KeyEnc: MapRADIUSKeyEncryption(cfg.Spec.RADIUSKeyEncryption), } if key, ok := req.RADIUSServerKeys[server.Address]; ok { srv.Key = key @@ -3370,11 +3366,11 @@ func (p *Provider) EnsureAAA(ctx context.Context, req *provider.EnsureAAARequest Fallback: MapFallback(methods), Local: MapLocal(methods), } - if methods[0].Type == "Group" { + if methods[0].Type == v1alpha1.AAAMethodTypeGroup { consoleAuth.Realm = MapRealmFromGroup(methods[0].GroupName, req.AAA.Spec.ServerGroups) consoleAuth.ProviderGroup = methods[0].GroupName } else { - consoleAuth.Realm = MapRealm(methods[0].Type) + consoleAuth.Realm = MapRealmFromMethodType(methods[0].Type) } conf = append(conf, consoleAuth) } @@ -3397,7 +3393,7 @@ func (p *Provider) EnsureAAA(ctx context.Context, req *provider.EnsureAAARequest CmdType: "config", LocalRbac: MapLocal(methods) == AAAValueYes, } - if methods[0].Type == "Group" { + if methods[0].Type == v1alpha1.AAAMethodTypeGroup { author.ProviderGroup = methods[0].GroupName } conf = append(conf, author) @@ -3406,7 +3402,6 @@ func (p *Provider) EnsureAAA(ctx context.Context, req *provider.EnsureAAARequest if req.AAA.Spec.Accounting != nil && len(req.AAA.Spec.Accounting.Methods) > 0 { methods := req.AAA.Spec.Accounting.Methods acct := &AAADefaultAcc{ - Name: "Accounting", LocalRbac: MapLocalFromMethodList(methods) == AAAValueYes, } if methods[0].Type == v1alpha1.AAAMethodTypeGroup { @@ -3422,42 +3417,48 @@ func (p *Provider) EnsureAAA(ctx context.Context, req *provider.EnsureAAARequest } func (p *Provider) DeleteAAA(ctx context.Context, req *provider.DeleteAAARequest) error { - // Reset all AAA method config to device defaults unconditionally. - // gNMI deletes are idempotent, so it is safe to reset even if a field - // was never configured. - conf := []gnmiext.Configurable{ - &AAADefaultAcc{Name: "Accounting", Realm: AAARealmLocal, LocalRbac: true}, + // Read what is currently on the device rather than relying on the spec. + // This ensures leftover servers/groups from previous reconciles are also removed. + tacacsProviders := new(TacacsPlusProviderItems) + tacacsGroups := new(TacacsPlusProviderGroupItems) + radiusProviders := new(RadiusProviderItems) + radiusGroups := new(RadiusProviderGroupItems) + for _, c := range []gnmiext.DataElement{tacacsProviders, tacacsGroups, radiusProviders, radiusGroups} { + if err := p.client.GetConfig(ctx, c); err != nil && !errors.Is(err, gnmiext.ErrNil) { + return err + } + } + + // Build a single delete list. Groups are placed before servers to avoid + // reference violations when NX-OS processes the delete request. + toDelete := make([]gnmiext.DataElement, 0, len(tacacsGroups.GroupList)+len(tacacsProviders.ProviderList)+len(radiusGroups.GroupList)+len(radiusProviders.ProviderList)) + for i := range tacacsGroups.GroupList { + toDelete = append(toDelete, &tacacsGroups.GroupList[i]) + } + for i := range tacacsProviders.ProviderList { + toDelete = append(toDelete, &tacacsProviders.ProviderList[i]) + } + for i := range radiusGroups.GroupList { + toDelete = append(toDelete, &radiusGroups.GroupList[i]) + } + for i := range radiusProviders.ProviderList { + toDelete = append(toDelete, &radiusProviders.ProviderList[i]) + } + if err := p.client.Delete(ctx, toDelete...); err != nil { + return err + } + + // Reset AAA method config to device defaults. + conf := []gnmiext.DataElement{ + &AAADefaultAcc{Realm: AAARealmLocal, LocalRbac: true}, &AAADefaultAuthor{CmdType: "config", LocalRbac: true}, &AAADefaultAuth{Realm: AAARealmLocal, Local: AAAValueYes, Fallback: AAAValueYes}, &AAAConsoleAuth{Realm: AAARealmLocal, Local: AAAValueYes, Fallback: AAAValueYes}, } - - for _, group := range req.AAA.Spec.ServerGroups { - switch group.Type { - case v1alpha1.AAAServerGroupTypeTACACS: - if err := p.client.Delete(ctx, &TacacsPlusProviderGroup{Name: group.Name}); err != nil { - return err - } - for _, server := range group.Servers { - if err := p.client.Delete(ctx, &TacacsPlusProvider{Name: server.Address}); err != nil { - return err - } - } - tacacsFeature := TACACSFeature(AdminStDisabled) - conf = append(conf, &tacacsFeature) - - case v1alpha1.AAAServerGroupTypeRADIUS: - if err := p.client.Delete(ctx, &RadiusProviderGroup{Name: group.Name}); err != nil { - return err - } - for _, server := range group.Servers { - if err := p.client.Delete(ctx, &RadiusProvider{Name: server.Address}); err != nil { - return err - } - } - } + if len(tacacsProviders.ProviderList) > 0 { + tacacsFeature := TACACSFeature(AdminStDisabled) + conf = append(conf, &tacacsFeature) } - return p.Update(ctx, conf...) } diff --git a/internal/provider/cisco/nxos/testdata/aaa_acct.json b/internal/provider/cisco/nxos/testdata/aaa_acct.json new file mode 100644 index 000000000..22dfa1232 --- /dev/null +++ b/internal/provider/cisco/nxos/testdata/aaa_acct.json @@ -0,0 +1,10 @@ +{ + "userext-items": { + "authrealm-items": { + "defaultacc-items": { + "realm": "tacacs", + "providerGroup": "TACACS-SERVERS" + } + } + } +} diff --git a/internal/provider/cisco/nxos/testdata/aaa_acct.json.txt b/internal/provider/cisco/nxos/testdata/aaa_acct.json.txt new file mode 100644 index 000000000..a9ee523be --- /dev/null +++ b/internal/provider/cisco/nxos/testdata/aaa_acct.json.txt @@ -0,0 +1 @@ +aaa accounting default group TACACS-SERVERS diff --git a/internal/provider/cisco/nxos/testdata/aaa_author.json b/internal/provider/cisco/nxos/testdata/aaa_author.json new file mode 100644 index 000000000..6f18ad65b --- /dev/null +++ b/internal/provider/cisco/nxos/testdata/aaa_author.json @@ -0,0 +1,14 @@ +{ + "userext-items": { + "authrealm-items": { + "defaultauthor-items": { + "DefaultAuthor-list": [ + { + "cmdType": "config", + "providerGroup": "TACACS-SERVERS" + } + ] + } + } + } +} diff --git a/internal/provider/cisco/nxos/testdata/aaa_author.json.txt b/internal/provider/cisco/nxos/testdata/aaa_author.json.txt new file mode 100644 index 000000000..f2daa8348 --- /dev/null +++ b/internal/provider/cisco/nxos/testdata/aaa_author.json.txt @@ -0,0 +1 @@ +aaa authorization config-commands default group TACACS-SERVERS diff --git a/internal/provider/cisco/nxos/testdata/aaa_consoleauth.json b/internal/provider/cisco/nxos/testdata/aaa_consoleauth.json new file mode 100644 index 000000000..f87b22869 --- /dev/null +++ b/internal/provider/cisco/nxos/testdata/aaa_consoleauth.json @@ -0,0 +1,12 @@ +{ + "userext-items": { + "authrealm-items": { + "consoleauth-items": { + "realm": "tacacs", + "providerGroup": "TACACS-SERVERS", + "fallback": "yes", + "local": "yes" + } + } + } +} diff --git a/internal/provider/cisco/nxos/testdata/aaa_consoleauth.json.txt b/internal/provider/cisco/nxos/testdata/aaa_consoleauth.json.txt new file mode 100644 index 000000000..7bc8e3d99 --- /dev/null +++ b/internal/provider/cisco/nxos/testdata/aaa_consoleauth.json.txt @@ -0,0 +1 @@ +aaa authentication login console group TACACS-SERVERS local diff --git a/internal/provider/cisco/nxos/testdata/aaa_defaultauth.json b/internal/provider/cisco/nxos/testdata/aaa_defaultauth.json new file mode 100644 index 000000000..f5dc2ab8e --- /dev/null +++ b/internal/provider/cisco/nxos/testdata/aaa_defaultauth.json @@ -0,0 +1,13 @@ +{ + "userext-items": { + "authrealm-items": { + "defaultauth-items": { + "realm": "tacacs", + "providerGroup": "TACACS-SERVERS", + "fallback": "yes", + "local": "yes", + "errEn": true + } + } + } +} diff --git a/internal/provider/cisco/nxos/testdata/aaa_defaultauth.json.txt b/internal/provider/cisco/nxos/testdata/aaa_defaultauth.json.txt new file mode 100644 index 000000000..0d48ab397 --- /dev/null +++ b/internal/provider/cisco/nxos/testdata/aaa_defaultauth.json.txt @@ -0,0 +1,2 @@ +aaa authentication login default group TACACS-SERVERS local +aaa authentication login error-enable diff --git a/internal/provider/cisco/nxos/testdata/aaa_tacacs_group.json b/internal/provider/cisco/nxos/testdata/aaa_tacacs_group.json new file mode 100644 index 000000000..2709df9ba --- /dev/null +++ b/internal/provider/cisco/nxos/testdata/aaa_tacacs_group.json @@ -0,0 +1,22 @@ +{ + "userext-items": { + "tacacsext-items": { + "tacacsplusprovidergroup-items": { + "TacacsPlusProviderGroup-list": [ + { + "name": "TACACS-SERVERS", + "vrf": "management", + "srcIf": "mgmt0", + "providerref-items": { + "ProviderRef-list": [ + { + "name": "10.1.1.1" + } + ] + } + } + ] + } + } + } +} diff --git a/internal/provider/cisco/nxos/testdata/aaa_tacacs_group.json.txt b/internal/provider/cisco/nxos/testdata/aaa_tacacs_group.json.txt new file mode 100644 index 000000000..51c15cbf6 --- /dev/null +++ b/internal/provider/cisco/nxos/testdata/aaa_tacacs_group.json.txt @@ -0,0 +1,4 @@ +aaa group server tacacs+ TACACS-SERVERS + server 10.1.1.1 + use-vrf management + source-interface mgmt0 diff --git a/internal/provider/cisco/nxos/testdata/aaa_tacacs_server.json b/internal/provider/cisco/nxos/testdata/aaa_tacacs_server.json new file mode 100644 index 000000000..d7bd74c95 --- /dev/null +++ b/internal/provider/cisco/nxos/testdata/aaa_tacacs_server.json @@ -0,0 +1,16 @@ +{ + "userext-items": { + "tacacsext-items": { + "tacacsplusprovider-items": { + "TacacsPlusProvider-list": [ + { + "name": "10.1.1.1", + "port": 49, + "keyEnc": "7", + "timeout": 5 + } + ] + } + } + } +} diff --git a/internal/provider/cisco/nxos/testdata/aaa_tacacs_server.json.txt b/internal/provider/cisco/nxos/testdata/aaa_tacacs_server.json.txt new file mode 100644 index 000000000..cc0519f1f --- /dev/null +++ b/internal/provider/cisco/nxos/testdata/aaa_tacacs_server.json.txt @@ -0,0 +1 @@ +tacacs-server host 10.1.1.1 port 49 timeout 5 key 7 From e69026a7e345ce151d0ec0a7457b2d43202a74e6 Mon Sep 17 00:00:00 2001 From: i521907 Date: Thu, 16 Apr 2026 09:21:37 -0400 Subject: [PATCH 11/32] Regenerate CRDs, RBAC, and API docs for AAA/TACACS support Signed-off-by: i521907 --- .../networking.metal.ironcore.dev_aaa.yaml | 4 +- ...working.metal.ironcore.dev_aaaconfigs.yaml | 24 +- config/rbac/role.yaml | 8 + docs/api-reference/index.md | 316 ++++++++++++++++++ 4 files changed, 340 insertions(+), 12 deletions(-) diff --git a/config/crd/bases/networking.metal.ironcore.dev_aaa.yaml b/config/crd/bases/networking.metal.ironcore.dev_aaa.yaml index d249c62b8..06498e125 100644 --- a/config/crd/bases/networking.metal.ironcore.dev_aaa.yaml +++ b/config/crd/bases/networking.metal.ironcore.dev_aaa.yaml @@ -257,7 +257,7 @@ spec: type: integer keySecretRef: description: |- - KeySecretRef is a reference to a secret containing the shared key for this RADIUS server. + KeySecretRef is a reference to a secret containing the plain text shared key for this RADIUS server. The secret must contain a key specified in the SecretKeySelector. properties: key: @@ -295,7 +295,7 @@ spec: properties: keySecretRef: description: |- - KeySecretRef is a reference to a secret containing the shared key for this TACACS+ server. + KeySecretRef is a reference to a secret containing the plain text shared key for this TACACS+ server. The secret must contain a key specified in the SecretKeySelector. properties: key: diff --git a/config/crd/bases/nx.cisco.networking.metal.ironcore.dev_aaaconfigs.yaml b/config/crd/bases/nx.cisco.networking.metal.ironcore.dev_aaaconfigs.yaml index 29571f455..8eb615645 100644 --- a/config/crd/bases/nx.cisco.networking.metal.ironcore.dev_aaaconfigs.yaml +++ b/config/crd/bases/nx.cisco.networking.metal.ironcore.dev_aaaconfigs.yaml @@ -50,16 +50,15 @@ spec: methods: description: Methods is the ordered list of methods. items: - description: NXOSMethod represents a single AAA method in an - NX-OS context. + description: AAAMethod represents an AAA method. properties: groupName: - description: GroupName is the server group name when Type - is Group. + description: GroupName is the name of the server group when + Type is Group. maxLength: 63 type: string type: - description: Type is the method type. + description: Type is the type of AAA method. enum: - Group - Local @@ -68,6 +67,9 @@ spec: required: - type type: object + x-kubernetes-validations: + - message: groupName is required when type is Group + rule: self.type != 'Group' || self.groupName != "" maxItems: 4 minItems: 1 type: array @@ -82,16 +84,15 @@ spec: methods: description: Methods is the ordered list of methods. items: - description: NXOSMethod represents a single AAA method in an - NX-OS context. + description: AAAMethod represents an AAA method. properties: groupName: - description: GroupName is the server group name when Type - is Group. + description: GroupName is the name of the server group when + Type is Group. maxLength: 63 type: string type: - description: Type is the method type. + description: Type is the type of AAA method. enum: - Group - Local @@ -100,6 +101,9 @@ spec: required: - type type: object + x-kubernetes-validations: + - message: groupName is required when type is Group + rule: self.type != 'Group' || self.groupName != "" maxItems: 4 minItems: 1 type: array diff --git a/config/rbac/role.yaml b/config/rbac/role.yaml index 5a8c24f02..f99879eaf 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: diff --git a/docs/api-reference/index.md b/docs/api-reference/index.md index b333d37e9..c2179d59e 100644 --- a/docs/api-reference/index.md +++ b/docs/api-reference/index.md @@ -15,6 +15,7 @@ SPDX-FileCopyrightText: 2025 SAP SE or an SAP affiliate company and IronCore con SPDX-License-Identifier: Apache-2.0 ### Resource Types +- [AAA](#aaa) - [BGP](#bgp) - [BGPPeer](#bgppeer) - [Banner](#banner) @@ -41,6 +42,242 @@ SPDX-License-Identifier: Apache-2.0 +#### AAA + + + +AAA is the Schema for the aaa API + + + + + +| Field | Description | Default | Validation | +| --- | --- | --- | --- | +| `apiVersion` _string_ | `networking.metal.ironcore.dev/v1alpha1` | | | +| `kind` _string_ | `AAA` | | | +| `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` _[AAASpec](#aaaspec)_ | Specification of the desired state of the resource.
More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#spec-and-status | | Required: \{\}
| +| `status` _[AAAStatus](#aaastatus)_ | Status of the resource. This is set and updated automatically.
Read-only.
More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#spec-and-status | | Optional: \{\}
| + + +#### AAAAccounting + + + +AAAAccounting defines the AAA accounting method list. + + + +_Appears in:_ +- [AAASpec](#aaaspec) + +| Field | Description | Default | Validation | +| --- | --- | --- | --- | +| `methods` _[AAAMethod](#aaamethod) array_ | Methods is the ordered list of accounting methods.
Methods are tried in order until one succeeds or all fail. | | MaxItems: 4
MinItems: 1
Required: \{\}
| + + +#### AAAAuthentication + + + +AAAAuthentication defines the AAA authentication method list. + + + +_Appears in:_ +- [AAASpec](#aaaspec) + +| Field | Description | Default | Validation | +| --- | --- | --- | --- | +| `methods` _[AAAMethod](#aaamethod) array_ | Methods is the ordered list of authentication methods.
Methods are tried in order until one succeeds or all fail. | | MaxItems: 4
MinItems: 1
Required: \{\}
| + + +#### AAAAuthorization + + + +AAAAuthorization defines the AAA authorization method list. + + + +_Appears in:_ +- [AAASpec](#aaaspec) + +| Field | Description | Default | Validation | +| --- | --- | --- | --- | +| `methods` _[AAAMethod](#aaamethod) array_ | Methods is the ordered list of authorization methods.
Methods are tried in order until one succeeds or all fail. | | MaxItems: 4
MinItems: 1
Required: \{\}
| + + +#### AAAMethod + + + +AAAMethod represents an AAA method. + + + +_Appears in:_ +- [AAAAccounting](#aaaaccounting) +- [AAAAuthentication](#aaaauthentication) +- [AAAAuthorization](#aaaauthorization) + +| Field | Description | Default | Validation | +| --- | --- | --- | --- | +| `type` _[AAAMethodType](#aaamethodtype)_ | Type is the type of AAA method. | | Enum: [Group Local None]
Required: \{\}
| +| `groupName` _string_ | GroupName is the name of the server group when Type is Group. | | MaxLength: 63
Optional: \{\}
| + + +#### AAAMethodType + +_Underlying type:_ _string_ + +AAAMethodType defines the type of AAA method. + +_Validation:_ +- Enum: [Group Local None] + +_Appears in:_ +- [AAAMethod](#aaamethod) + +| Field | Description | +| --- | --- | +| `Group` | AAAMethodTypeGroup uses a server group (e.g., TACACS+ group).
| +| `Local` | AAAMethodTypeLocal uses the local user database.
| +| `None` | AAAMethodTypeNone allows access without authentication.
| + + +#### AAAServer + + + +AAAServer represents a single AAA server within a group. + + + +_Appears in:_ +- [AAAServerGroup](#aaaservergroup) + +| Field | Description | Default | Validation | +| --- | --- | --- | --- | +| `address` _string_ | Address is the IP address or hostname of the server. | | MaxLength: 253
MinLength: 1
Required: \{\}
| +| `timeout` _[Duration](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.35/#duration-v1-meta)_ | Timeout is the response timeout for this server. | | Optional: \{\}
| +| `tacacs` _[AAAServerTACACS](#aaaservertacacs)_ | TACACS contains TACACS+ specific server configuration.
Required when the parent server group type is TACACS. | | Optional: \{\}
| +| `radius` _[AAAServerRADIUS](#aaaserverradius)_ | RADIUS contains RADIUS specific server configuration.
Required when the parent server group type is RADIUS. | | Optional: \{\}
| + + +#### AAAServerGroup + + + +AAAServerGroup represents a named group of AAA servers. + + + +_Appears in:_ +- [AAASpec](#aaaspec) + +| Field | Description | Default | Validation | +| --- | --- | --- | --- | +| `name` _string_ | Name is the name of the server group. | | MaxLength: 63
MinLength: 1
Required: \{\}
| +| `type` _[AAAServerGroupType](#aaaservergrouptype)_ | Type is the protocol type of this server group. | | Enum: [TACACS RADIUS]
Required: \{\}
| +| `servers` _[AAAServer](#aaaserver) array_ | Servers is the list of servers in this group. | | MaxItems: 16
MinItems: 1
Required: \{\}
| +| `vrfName` _string_ | VrfName is the VRF to use for communication with the servers in this group. | | MaxLength: 63
Optional: \{\}
| +| `sourceInterfaceName` _string_ | SourceInterfaceName is the source interface to use for communication with the servers. | | MaxLength: 63
Optional: \{\}
| + + +#### AAAServerGroupType + +_Underlying type:_ _string_ + +AAAServerGroupType defines the protocol type of an AAA server group. + +_Validation:_ +- Enum: [TACACS RADIUS] + +_Appears in:_ +- [AAAServerGroup](#aaaservergroup) + +| Field | Description | +| --- | --- | +| `TACACS` | AAAServerGroupTypeTACACS is a TACACS+ server group.
| +| `RADIUS` | AAAServerGroupTypeRADIUS is a RADIUS server group.
| + + +#### AAAServerRADIUS + + + +AAAServerRADIUS contains RADIUS specific server configuration. + + + +_Appears in:_ +- [AAAServer](#aaaserver) + +| Field | Description | Default | Validation | +| --- | --- | --- | --- | +| `authenticationPort` _integer_ | AuthenticationPort is the UDP port for RADIUS authentication requests.
Defaults to 1812 if not specified. | 1812 | Maximum: 65535
Minimum: 1
Optional: \{\}
| +| `accountingPort` _integer_ | AccountingPort is the UDP port for RADIUS accounting requests.
Defaults to 1813 if not specified. | 1813 | Maximum: 65535
Minimum: 1
Optional: \{\}
| +| `keySecretRef` _[SecretKeySelector](#secretkeyselector)_ | KeySecretRef is a reference to a secret containing the plain text shared key for this RADIUS server.
The secret must contain a key specified in the SecretKeySelector. | | Required: \{\}
| + + +#### AAAServerTACACS + + + +AAAServerTACACS contains TACACS+ specific server configuration. + + + +_Appears in:_ +- [AAAServer](#aaaserver) + +| Field | Description | Default | Validation | +| --- | --- | --- | --- | +| `port` _integer_ | Port is the TCP port of the TACACS+ server.
Defaults to 49 if not specified. | 49 | Maximum: 65535
Minimum: 1
Optional: \{\}
| +| `keySecretRef` _[SecretKeySelector](#secretkeyselector)_ | KeySecretRef is a reference to a secret containing the plain text shared key for this TACACS+ server.
The secret must contain a key specified in the SecretKeySelector. | | Required: \{\}
| + + +#### AAASpec + + + +AAASpec defines the desired state of AAA. + +It models the Authentication, Authorization, and Accounting (AAA) configuration on a network device. + + + +_Appears in:_ +- [AAA](#aaa) + +| 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: \{\}
| +| `providerConfigRef` _[TypedLocalObjectReference](#typedlocalobjectreference)_ | ProviderConfigRef is a reference to a resource holding the provider-specific configuration of this AAA.
This reference is used to link the AAA to its provider-specific configuration. | | Optional: \{\}
| +| `serverGroups` _[AAAServerGroup](#aaaservergroup) array_ | ServerGroups is the list of AAA server groups. | | MaxItems: 8
Optional: \{\}
| +| `authentication` _[AAAAuthentication](#aaaauthentication)_ | Authentication defines the AAA authentication method list. | | Optional: \{\}
| +| `authorization` _[AAAAuthorization](#aaaauthorization)_ | Authorization defines the AAA authorization method list. | | Optional: \{\}
| +| `accounting` _[AAAAccounting](#aaaaccounting)_ | Accounting defines the AAA accounting method list. | | Optional: \{\}
| + + +#### AAAStatus + + + +AAAStatus defines the observed state of AAA. + + + +_Appears in:_ +- [AAA](#aaa) + +| 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 AAA. | | Optional: \{\}
| + + #### ACLAction _Underlying type:_ _string_ @@ -1654,6 +1891,7 @@ referenced object inside the same namespace. _Appears in:_ +- [AAASpec](#aaaspec) - [AccessControlListSpec](#accesscontrollistspec) - [Aggregation](#aggregation) - [BGPPeerAddressFamily](#bgppeeraddressfamily) @@ -2805,6 +3043,8 @@ SecretKeySelector contains enough information to select a key of a Secret. _Appears in:_ +- [AAAServerRADIUS](#aaaserverradius) +- [AAAServerTACACS](#aaaservertacacs) - [PasswordSource](#passwordsource) - [SSHPublicKeySource](#sshpublickeysource) - [TLS](#tls) @@ -3089,6 +3329,7 @@ typed referenced object inside the same namespace. _Appears in:_ +- [AAASpec](#aaaspec) - [AccessControlListSpec](#accesscontrollistspec) - [BGPPeerSpec](#bgppeerspec) - [BGPSpec](#bgpspec) @@ -3312,6 +3553,7 @@ _Appears in:_ Package v1alpha1 contains API Schema definitions for the nx.cisco.networking.metal.ironcore.dev v1alpha1 API group. ### Resource Types +- [AAAConfig](#aaaconfig) - [BGPConfig](#bgpconfig) - [BorderGateway](#bordergateway) - [InterfaceConfig](#interfaceconfig) @@ -3323,6 +3565,42 @@ Package v1alpha1 contains API Schema definitions for the nx.cisco.networking.met +#### AAAConfig + + + +AAAConfig is the Schema for the aaaconfigs API + + + + + +| Field | Description | Default | Validation | +| --- | --- | --- | --- | +| `apiVersion` _string_ | `nx.cisco.networking.metal.ironcore.dev/v1alpha1` | | | +| `kind` _string_ | `AAAConfig` | | | +| `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` _[AAAConfigSpec](#aaaconfigspec)_ | Specification of the desired state of the resource.
More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#spec-and-status | | Required: \{\}
| + + +#### AAAConfigSpec + + + +AAAConfigSpec defines the desired state of AAAConfig + + + +_Appears in:_ +- [AAAConfig](#aaaconfig) + +| Field | Description | Default | Validation | +| --- | --- | --- | --- | +| `loginErrorEnable` _boolean_ | LoginErrorEnable enables login error messages. | | Optional: \{\}
| +| `keyEncryption` _[TACACSKeyEncryption](#tacacskeyencryption)_ | KeyEncryption specifies the default encryption type for TACACS+ keys. | Type7 | Enum: [Type6 Type7 Clear]
| +| `radiusKeyEncryption` _[RADIUSKeyEncryption](#radiuskeyencryption)_ | RADIUSKeyEncryption specifies the default encryption type for RADIUS server keys. | Type7 | Enum: [Type6 Type7 Clear]
| + + #### AutoRecovery @@ -3806,6 +4084,25 @@ _Appears in:_ | `l3router` _[Enabled](#enabled)_ | L3Router enables Layer 3 peer-router functionality on this peer. | \{ enabled:false \} | Optional: \{\}
| +#### RADIUSKeyEncryption + +_Underlying type:_ _string_ + +RADIUSKeyEncryption defines the encryption type for RADIUS server keys. + +_Validation:_ +- Enum: [Type6 Type7 Clear] + +_Appears in:_ +- [AAAConfigSpec](#aaaconfigspec) + +| Field | Description | +| --- | --- | +| `Type6` | RADIUSKeyEncryptionType6 uses AES encryption (more secure).
| +| `Type7` | RADIUSKeyEncryptionType7 uses Cisco Type 7 encryption (reversible).
| +| `Clear` | RADIUSKeyEncryptionClear sends the key in cleartext.
| + + #### SSH @@ -3950,6 +4247,25 @@ _Appears in:_ | `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 Banner. | | Optional: \{\}
| +#### TACACSKeyEncryption + +_Underlying type:_ _string_ + +TACACSKeyEncryption defines the encryption type for TACACS+ server keys. + +_Validation:_ +- Enum: [Type6 Type7 Clear] + +_Appears in:_ +- [AAAConfigSpec](#aaaconfigspec) + +| Field | Description | +| --- | --- | +| `Type6` | TACACSKeyEncryptionType6 uses AES encryption (more secure).
| +| `Type7` | TACACSKeyEncryptionType7 uses Cisco Type 7 encryption (reversible).
| +| `Clear` | TACACSKeyEncryptionClear sends the key in cleartext.
| + + #### TrafficType _Underlying type:_ _string_ From 009cb2ba03e501a6a3183c0842ef3490c159a1b5 Mon Sep 17 00:00:00 2001 From: i521907 Date: Thu, 16 Apr 2026 09:23:43 -0400 Subject: [PATCH 12/32] Rename NXOSMethodList to AAAMethodList per review feedback Signed-off-by: i521907 --- api/cisco/nx/v1alpha1/aaaconfig_types.go | 8 ++-- .../nx/v1alpha1/zz_generated.deepcopy.go | 44 +++++++++---------- 2 files changed, 26 insertions(+), 26 deletions(-) diff --git a/api/cisco/nx/v1alpha1/aaaconfig_types.go b/api/cisco/nx/v1alpha1/aaaconfig_types.go index 2959f3d77..ea185dc3a 100644 --- a/api/cisco/nx/v1alpha1/aaaconfig_types.go +++ b/api/cisco/nx/v1alpha1/aaaconfig_types.go @@ -27,11 +27,11 @@ type AAAConfigSpec struct { // ConsoleAuthentication defines console-specific authentication methods. // +optional - ConsoleAuthentication *NXOSMethodList `json:"consoleAuthentication,omitempty"` + ConsoleAuthentication *AAAMethodList `json:"consoleAuthentication,omitempty"` // ConfigCommandsAuthorization defines config-commands authorization methods. // +optional - ConfigCommandsAuthorization *NXOSMethodList `json:"configCommandsAuthorization,omitempty"` + ConfigCommandsAuthorization *AAAMethodList `json:"configCommandsAuthorization,omitempty"` } // TACACSKeyEncryption defines the encryption type for TACACS+ server keys. @@ -60,8 +60,8 @@ const ( RADIUSKeyEncryptionClear RADIUSKeyEncryption = "Clear" ) -// NXOSMethodList defines an ordered list of AAA methods for NX-OS specific contexts. -type NXOSMethodList struct { +// AAAMethodList defines an ordered list of AAA methods. +type AAAMethodList struct { // Methods is the ordered list of methods. // +required // +listType=atomic diff --git a/api/cisco/nx/v1alpha1/zz_generated.deepcopy.go b/api/cisco/nx/v1alpha1/zz_generated.deepcopy.go index dc52921ab..612652b3a 100644 --- a/api/cisco/nx/v1alpha1/zz_generated.deepcopy.go +++ b/api/cisco/nx/v1alpha1/zz_generated.deepcopy.go @@ -76,12 +76,12 @@ func (in *AAAConfigSpec) DeepCopyInto(out *AAAConfigSpec) { *out = *in if in.ConsoleAuthentication != nil { in, out := &in.ConsoleAuthentication, &out.ConsoleAuthentication - *out = new(NXOSMethodList) + *out = new(AAAMethodList) (*in).DeepCopyInto(*out) } if in.ConfigCommandsAuthorization != nil { in, out := &in.ConfigCommandsAuthorization, &out.ConfigCommandsAuthorization - *out = new(NXOSMethodList) + *out = new(AAAMethodList) (*in).DeepCopyInto(*out) } } @@ -96,6 +96,26 @@ func (in *AAAConfigSpec) DeepCopy() *AAAConfigSpec { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *AAAMethodList) DeepCopyInto(out *AAAMethodList) { + *out = *in + if in.Methods != nil { + in, out := &in.Methods, &out.Methods + *out = make([]corev1alpha1.AAAMethod, len(*in)) + copy(*out, *in) + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AAAMethodList. +func (in *AAAMethodList) DeepCopy() *AAAMethodList { + if in == nil { + return nil + } + out := new(AAAMethodList) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *AutoRecovery) DeepCopyInto(out *AutoRecovery) { *out = *in @@ -727,26 +747,6 @@ func (in *ManagementAccessConfigSpec) DeepCopy() *ManagementAccessConfigSpec { return out } -// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *NXOSMethodList) DeepCopyInto(out *NXOSMethodList) { - *out = *in - if in.Methods != nil { - in, out := &in.Methods, &out.Methods - *out = make([]corev1alpha1.AAAMethod, len(*in)) - copy(*out, *in) - } -} - -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new NXOSMethodList. -func (in *NXOSMethodList) DeepCopy() *NXOSMethodList { - if in == nil { - return nil - } - out := new(NXOSMethodList) - in.DeepCopyInto(out) - return out -} - // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *NetworkVirtualizationEdgeConfig) DeepCopyInto(out *NetworkVirtualizationEdgeConfig) { *out = *in From 25abd56b08ca08e0ab2614b29df8276287e43493 Mon Sep 17 00:00:00 2001 From: i521907 Date: Thu, 16 Apr 2026 09:26:38 -0400 Subject: [PATCH 13/32] Simplify DeleteAAA: delete container nodes instead of individual entries Signed-off-by: i521907 --- internal/provider/cisco/nxos/provider.go | 50 +++++++----------------- 1 file changed, 14 insertions(+), 36 deletions(-) diff --git a/internal/provider/cisco/nxos/provider.go b/internal/provider/cisco/nxos/provider.go index 26d922b1c..9c4a284e9 100644 --- a/internal/provider/cisco/nxos/provider.go +++ b/internal/provider/cisco/nxos/provider.go @@ -3417,49 +3417,27 @@ func (p *Provider) EnsureAAA(ctx context.Context, req *provider.EnsureAAARequest } func (p *Provider) DeleteAAA(ctx context.Context, req *provider.DeleteAAARequest) error { - // Read what is currently on the device rather than relying on the spec. - // This ensures leftover servers/groups from previous reconciles are also removed. - tacacsProviders := new(TacacsPlusProviderItems) - tacacsGroups := new(TacacsPlusProviderGroupItems) - radiusProviders := new(RadiusProviderItems) - radiusGroups := new(RadiusProviderGroupItems) - for _, c := range []gnmiext.DataElement{tacacsProviders, tacacsGroups, radiusProviders, radiusGroups} { - if err := p.client.GetConfig(ctx, c); err != nil && !errors.Is(err, gnmiext.ErrNil) { - return err - } - } - - // Build a single delete list. Groups are placed before servers to avoid - // reference violations when NX-OS processes the delete request. - toDelete := make([]gnmiext.DataElement, 0, len(tacacsGroups.GroupList)+len(tacacsProviders.ProviderList)+len(radiusGroups.GroupList)+len(radiusProviders.ProviderList)) - for i := range tacacsGroups.GroupList { - toDelete = append(toDelete, &tacacsGroups.GroupList[i]) - } - for i := range tacacsProviders.ProviderList { - toDelete = append(toDelete, &tacacsProviders.ProviderList[i]) - } - for i := range radiusGroups.GroupList { - toDelete = append(toDelete, &radiusGroups.GroupList[i]) - } - for i := range radiusProviders.ProviderList { - toDelete = append(toDelete, &radiusProviders.ProviderList[i]) - } - if err := p.client.Delete(ctx, toDelete...); err != nil { + // Delete the whole server/group list containers in a single gNMI call. + // Groups are placed before providers to avoid reference violations. + // Deleting the containers also removes any leftover entries from previous reconciles. + if err := p.client.Delete(ctx, + new(TacacsPlusProviderGroupItems), + new(TacacsPlusProviderItems), + new(RadiusProviderGroupItems), + new(RadiusProviderItems), + ); err != nil && !errors.Is(err, gnmiext.ErrNil) { return err } - // Reset AAA method config to device defaults. - conf := []gnmiext.DataElement{ + // Reset AAA method config and TACACS feature to device defaults. + tacacsFeature := TACACSFeature(AdminStDisabled) + return p.Update(ctx, + &tacacsFeature, &AAADefaultAcc{Realm: AAARealmLocal, LocalRbac: true}, &AAADefaultAuthor{CmdType: "config", LocalRbac: true}, &AAADefaultAuth{Realm: AAARealmLocal, Local: AAAValueYes, Fallback: AAAValueYes}, &AAAConsoleAuth{Realm: AAARealmLocal, Local: AAAValueYes, Fallback: AAAValueYes}, - } - if len(tacacsProviders.ProviderList) > 0 { - tacacsFeature := TACACSFeature(AdminStDisabled) - conf = append(conf, &tacacsFeature) - } - return p.Update(ctx, conf...) + ) } func init() { From 3f10d45d3b0e4c345d4a6c335e8a801799e199f9 Mon Sep 17 00:00:00 2001 From: i521907 Date: Thu, 16 Apr 2026 09:37:55 -0400 Subject: [PATCH 14/32] Fix TACACS+/RADIUS struct fields to avoid perpetual reconcile drift Signed-off-by: i521907 --- internal/provider/cisco/nxos/aaa.go | 21 +++++++++------------ internal/provider/cisco/nxos/provider.go | 8 +++++--- 2 files changed, 14 insertions(+), 15 deletions(-) diff --git a/internal/provider/cisco/nxos/aaa.go b/internal/provider/cisco/nxos/aaa.go index 2e4b62b96..902702058 100644 --- a/internal/provider/cisco/nxos/aaa.go +++ b/internal/provider/cisco/nxos/aaa.go @@ -40,13 +40,11 @@ const ( // TacacsPlusProvider represents a TACACS+ server host configuration. type TacacsPlusProvider struct { - Name string `json:"name"` - Port int32 `json:"port,omitempty"` - Key string `json:"key,omitempty"` - KeyEnc string `json:"keyEnc,omitempty"` - Timeout int32 `json:"timeout,omitempty"` - Retries int32 `json:"retries,omitempty"` - AuthProtocol string `json:"authProtocol,omitempty"` + Name string `json:"name"` + Port int32 `json:"port"` + Key string `json:"key,omitempty"` + KeyEnc string `json:"keyEnc"` + Timeout int32 `json:"timeout"` } func (*TacacsPlusProvider) IsListItem() {} @@ -103,12 +101,11 @@ func (r *TacacsPlusProviderRef) Key() string { return r.Name } // RadiusProvider represents a RADIUS server host configuration. type RadiusProvider struct { Name string `json:"name"` - AuthPort int32 `json:"authPort,omitempty"` - AcctPort int32 `json:"acctPort,omitempty"` + AuthPort int32 `json:"authPort"` + AcctPort int32 `json:"acctPort"` Key string `json:"key,omitempty"` - KeyEnc string `json:"keyEnc,omitempty"` - Timeout int32 `json:"timeout,omitempty"` - Retries int32 `json:"retries,omitempty"` + KeyEnc string `json:"keyEnc"` + Timeout int32 `json:"timeout"` } func (*RadiusProvider) IsListItem() {} diff --git a/internal/provider/cisco/nxos/provider.go b/internal/provider/cisco/nxos/provider.go index 9c4a284e9..dec632ca5 100644 --- a/internal/provider/cisco/nxos/provider.go +++ b/internal/provider/cisco/nxos/provider.go @@ -3291,9 +3291,10 @@ func (p *Provider) EnsureAAA(ctx context.Context, req *provider.EnsureAAARequest for _, server := range group.Servers { srv := &TacacsPlusProvider{ - Name: server.Address, - Port: server.TACACS.Port, - KeyEnc: MapKeyEncryption(cfg.Spec.KeyEncryption), + Name: server.Address, + Port: server.TACACS.Port, + KeyEnc: MapKeyEncryption(cfg.Spec.KeyEncryption), + Timeout: 5, // YANG default } if key, ok := req.TACACSServerKeys[server.Address]; ok { srv.Key = key @@ -3321,6 +3322,7 @@ func (p *Provider) EnsureAAA(ctx context.Context, req *provider.EnsureAAARequest AuthPort: server.RADIUS.AuthenticationPort, AcctPort: server.RADIUS.AccountingPort, KeyEnc: MapRADIUSKeyEncryption(cfg.Spec.RADIUSKeyEncryption), + Timeout: 5, // YANG default } if key, ok := req.RADIUSServerKeys[server.Address]; ok { srv.Key = key From 3c056907986dcefcdf74d32cbc501a177d6fd637 Mon Sep 17 00:00:00 2001 From: i521907 Date: Thu, 16 Apr 2026 09:44:21 -0400 Subject: [PATCH 15/32] Address remaining review comments from felix Signed-off-by: i521907 --- config/default/kustomization.yaml | 2 +- internal/provider/cisco/nxos/aaa.go | 8 -------- internal/provider/cisco/nxos/provider.go | 6 ++---- 3 files changed, 3 insertions(+), 13 deletions(-) diff --git a/config/default/kustomization.yaml b/config/default/kustomization.yaml index 2a2e5cd92..fd3c7529c 100644 --- a/config/default/kustomization.yaml +++ b/config/default/kustomization.yaml @@ -24,7 +24,7 @@ resources: # [CERTMANAGER] To enable cert-manager, uncomment all sections with 'CERTMANAGER'. 'WEBHOOK' components are required. - ../certmanager # [PROMETHEUS] To enable prometheus monitor, uncomment all sections with 'PROMETHEUS'. -- ../prometheus +#- ../prometheus # [METRICS] Expose the controller manager metrics service. - metrics_service.yaml # [PROVISIONING] Expose the controller manager provisioning service. diff --git a/internal/provider/cisco/nxos/aaa.go b/internal/provider/cisco/nxos/aaa.go index 902702058..6f785f7f8 100644 --- a/internal/provider/cisco/nxos/aaa.go +++ b/internal/provider/cisco/nxos/aaa.go @@ -10,7 +10,6 @@ import ( ) var ( - _ gnmiext.DataElement = (*TACACSFeature)(nil) _ gnmiext.DataElement = (*TacacsPlusProvider)(nil) _ gnmiext.DataElement = (*TacacsPlusProviderGroup)(nil) _ gnmiext.DataElement = (*RadiusProvider)(nil) @@ -21,13 +20,6 @@ var ( _ gnmiext.DataElement = (*AAADefaultAcc)(nil) ) -// TACACSFeature enables/disables the TACACS+ feature on NX-OS. -type TACACSFeature AdminSt - -func (*TACACSFeature) XPath() string { - return "System/fm-items/tacacsplus-items/adminSt" -} - // AAA configuration constants const ( AAARealmTacacs = "tacacs" diff --git a/internal/provider/cisco/nxos/provider.go b/internal/provider/cisco/nxos/provider.go index dec632ca5..af7896644 100644 --- a/internal/provider/cisco/nxos/provider.go +++ b/internal/provider/cisco/nxos/provider.go @@ -3286,8 +3286,7 @@ func (p *Provider) EnsureAAA(ctx context.Context, req *provider.EnsureAAARequest for _, group := range req.AAA.Spec.ServerGroups { switch group.Type { case v1alpha1.AAAServerGroupTypeTACACS: - tacacsFeature := TACACSFeature(AdminStEnabled) - conf = append(conf, &tacacsFeature) + conf = append(conf, &Feature{Name: "tacacsplus", AdminSt: AdminStEnabled}) for _, server := range group.Servers { srv := &TacacsPlusProvider{ @@ -3432,9 +3431,8 @@ func (p *Provider) DeleteAAA(ctx context.Context, req *provider.DeleteAAARequest } // Reset AAA method config and TACACS feature to device defaults. - tacacsFeature := TACACSFeature(AdminStDisabled) return p.Update(ctx, - &tacacsFeature, + &Feature{Name: "tacacsplus", AdminSt: AdminStDisabled}, &AAADefaultAcc{Realm: AAARealmLocal, LocalRbac: true}, &AAADefaultAuthor{CmdType: "config", LocalRbac: true}, &AAADefaultAuth{Realm: AAARealmLocal, Local: AAAValueYes, Fallback: AAAValueYes}, From 9c93229dff42da04e7be39991de26bdca7b2bcb1 Mon Sep 17 00:00:00 2001 From: i521907 Date: Thu, 16 Apr 2026 13:37:46 -0400 Subject: [PATCH 16/32] Fix RBAC drift after main merge Signed-off-by: i521907 --- charts/network-operator/templates/rbac/manager-role.yaml | 4 ++++ config/rbac/role.yaml | 8 -------- 2 files changed, 4 insertions(+), 8 deletions(-) diff --git a/charts/network-operator/templates/rbac/manager-role.yaml b/charts/network-operator/templates/rbac/manager-role.yaml index 580e6b8bd..3bd8ec1ce 100644 --- a/charts/network-operator/templates/rbac/manager-role.yaml +++ b/charts/network-operator/templates/rbac/manager-role.yaml @@ -48,6 +48,7 @@ rules: - apiGroups: - networking.metal.ironcore.dev resources: + - aaa - accesscontrollists - banners - bgp @@ -83,6 +84,7 @@ rules: - apiGroups: - networking.metal.ironcore.dev resources: + - aaa/finalizers - accesscontrollists/finalizers - banners/finalizers - bgp/finalizers @@ -112,6 +114,7 @@ rules: - apiGroups: - networking.metal.ironcore.dev resources: + - aaa/status - accesscontrollists/status - banners/status - bgp/status @@ -143,6 +146,7 @@ rules: - apiGroups: - nx.cisco.networking.metal.ironcore.dev resources: + - aaaconfigs - bgpconfigs - interfaceconfigs - lldpconfigs diff --git a/config/rbac/role.yaml b/config/rbac/role.yaml index f99879eaf..5a8c24f02 100644 --- a/config/rbac/role.yaml +++ b/config/rbac/role.yaml @@ -21,14 +21,6 @@ rules: - list - update - watch -- apiGroups: - - "" - - events.k8s.io - resources: - - events - verbs: - - create - - patch - apiGroups: - coordination.k8s.io resources: From eeb59e3de42070a020d8ffec851a193e08bb4278 Mon Sep 17 00:00:00 2001 From: Minmin Lin Date: Fri, 17 Apr 2026 17:56:23 +0800 Subject: [PATCH 17/32] Update overview page and architecture runtime flow/sequence diagrams (#307) Signed-off-by: i521907 --- docs/architecture-overview.md | 34 +++++++++++++++++----------------- 1 file changed, 17 insertions(+), 17 deletions(-) diff --git a/docs/architecture-overview.md b/docs/architecture-overview.md index 90f0ab763..bdd3ca79a 100644 --- a/docs/architecture-overview.md +++ b/docs/architecture-overview.md @@ -16,7 +16,7 @@ sequenceDiagram participant CFG as Config Reconciler participant BOOT as Provisioning HTTP / TFTP participant P as Provider / Transport - participant DEVICE as Network Device + participant ND as Network Device U->>API: Apply Device or config CR opt Webhook registered for this kind @@ -34,17 +34,17 @@ sequenceDiagram alt Provisioning is configured and phase is Pending DEV->>API: Set phase=Provisioning and Ready=False DEV-->>API: Requeue for follow-up checks - DEVICE->>+BOOT: Request bootstrap config + ND->>+BOOT: Request bootstrap config BOOT->>API: Read Device, secrets, and bootstrap assets - BOOT-->>DEVICE: Return provisioning config, certs, or boot script - DEVICE->>BOOT: Report provisioning progress + BOOT-->>ND: Return provisioning config, certs, or boot script + ND->>BOOT: Report provisioning progress BOOT->>API: Update provisioning status API-)MGR: Watch event for Device update end DEV->>+P: Connect to provider - P->>DEVICE: Read device facts and ports - DEVICE-->>P: Inventory and operational state + P->>ND: Read device facts and ports + ND-->>P: Inventory and operational state P-->>-DEV: Device details DEV->>API: Patch status, labels, and Ready condition DEV-->>-MGR: Reconcile complete @@ -57,8 +57,8 @@ sequenceDiagram CFG->>CFG: Acquire per-device lock CFG->>API: Ensure finalizer, device label, and owner reference CFG->>+P: Connect using device endpoint and credentials - P->>DEVICE: Ensure intended configuration - DEVICE-->>P: Apply result + P->>ND: Ensure intended configuration + ND-->>P: Apply result P-->>-CFG: Success or error CFG->>API: Patch Ready condition and status CFG->>CFG: Release per-device lock @@ -88,7 +88,7 @@ sequenceDiagram participant DEV as DeviceReconciler participant BOOT as Provisioning HTTP / TFTP participant P as Provider / Transport - participant DEVICE as Network Device + participant ND as Network Device U->>API: Apply Device CR API-->>U: Object persisted @@ -101,18 +101,18 @@ sequenceDiagram alt Provisioning configured and phase is Pending DEV->>API: Set phase=Provisioning and Ready=False DEV-->>API: Requeue for follow-up checks - DEVICE->>+BOOT: Request bootstrap config + ND->>+BOOT: Request bootstrap config BOOT->>API: Read Device, secrets, and bootstrap assets - BOOT-->>DEVICE: Return provisioning config, certs, or boot script - DEVICE->>BOOT: Report provisioning progress + BOOT-->>ND: Return provisioning config, certs, or boot script + ND->>BOOT: Report provisioning progress BOOT->>API: Update provisioning status API-)MGR: Watch event for Device update DEV->>API: Requeue until provisioning completes end DEV->>+P: Connect to provider - P->>DEVICE: Read device facts and ports - DEVICE-->>P: Inventory and operational state + P->>ND: Read device facts and ports + ND-->>P: Inventory and operational state P-->>-DEV: Device details DEV->>API: Patch status, labels, and Ready condition DEV-->>-MGR: Reconcile complete @@ -130,7 +130,7 @@ sequenceDiagram participant MGR as Controller Manager participant CFG as Config Reconciler participant P as Provider / Transport - participant DEVICE as Network Device + participant ND as Network Device U->>API: Apply config CR opt Webhook registered for this kind @@ -147,8 +147,8 @@ sequenceDiagram CFG->>CFG: Acquire per-device lock CFG->>API: Ensure finalizer, device label, and owner reference CFG->>+P: Connect using device endpoint and credentials - P->>DEVICE: Ensure intended configuration - DEVICE-->>P: Apply result + P->>ND: Ensure intended configuration + ND-->>P: Apply result P-->>-CFG: Success or error CFG->>API: Patch Ready condition and status CFG->>CFG: Release per-device lock From 21a0d2c69dc275d886e6747aad3ec109fb4158db Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felix=20K=C3=A4stner?= Date: Fri, 17 Apr 2026 12:20:02 +0200 Subject: [PATCH 18/32] Ignore shorthands in mermaid diagrams inside documentation Signed-off-by: i521907 --- .typos.toml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.typos.toml b/.typos.toml index b9215bcbd..64c1e1211 100644 --- a/.typos.toml +++ b/.typos.toml @@ -17,6 +17,8 @@ mininum = "mininum" specifc = "specifc" # Go built-in keyword cpy = "cpy" +# Documentation +nd = "nd" [files] extend-exclude = [ From 45339b0c635d8e9e752e1151f724d29b8f59576d Mon Sep 17 00:00:00 2001 From: i521907 Date: Mon, 20 Apr 2026 11:42:26 -0400 Subject: [PATCH 19/32] Fix missing AAA CRD entries in kustomization.yaml Signed-off-by: i521907 --- config/crd/kustomization.yaml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/config/crd/kustomization.yaml b/config/crd/kustomization.yaml index 182f057f4..790490555 100644 --- a/config/crd/kustomization.yaml +++ b/config/crd/kustomization.yaml @@ -34,6 +34,8 @@ 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_aaa.yaml +- bases/nx.cisco.networking.metal.ironcore.dev_aaaconfigs.yaml # +kubebuilder:scaffold:crdkustomizeresource patches: [] From 88737f7d4f9eed17e3a4d0f035aaa89e4819fa6e Mon Sep 17 00:00:00 2001 From: i521907 Date: Tue, 21 Apr 2026 08:04:48 -0400 Subject: [PATCH 20/32] Fix AAA stale server cleanup and providerGroup serialization Signed-off-by: i521907 --- .gitignore | 1 + PROJECT | 16 ++++ config/samples/networking_v1alpha1_aaa.yaml | 10 +-- internal/provider/cisco/nxos/aaa.go | 8 +- internal/provider/cisco/nxos/provider.go | 99 +++++++++++++++++---- 5 files changed, 104 insertions(+), 30 deletions(-) diff --git a/.gitignore b/.gitignore index e021a935f..e8a506177 100644 --- a/.gitignore +++ b/.gitignore @@ -32,3 +32,4 @@ charts/**/*.tgz *.swp *.swo *~ +config/samples/test-aaa-qa.yaml diff --git a/PROJECT b/PROJECT index 24face925..ffcfbfcc1 100644 --- a/PROJECT +++ b/PROJECT @@ -289,4 +289,20 @@ 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: AAA + path: github.com/ironcore-dev/network-operator/api/core/v1alpha1 + version: v1alpha1 +- api: + crdVersion: v1 + namespaced: true + domain: cisco.networking.metal.ironcore.dev + group: nx + kind: AAAConfig + path: github.com/ironcore-dev/network-operator/api/cisco/nx/v1alpha1 + version: v1alpha1 version: "3" diff --git a/config/samples/networking_v1alpha1_aaa.yaml b/config/samples/networking_v1alpha1_aaa.yaml index fe13e6e2f..12a28b560 100644 --- a/config/samples/networking_v1alpha1_aaa.yaml +++ b/config/samples/networking_v1alpha1_aaa.yaml @@ -1,9 +1,4 @@ # Example AAA configuration with TACACS+ servers -# This configures: -# - feature tacacs+ -# - tacacs-server hosts with encrypted keys -# - AAA server group for TACACS+ -# - AAA authentication, authorization, and accounting --- apiVersion: v1 kind: Secret @@ -15,9 +10,6 @@ stringData: # Replace with your actual TACACS+ server shared secret server-key: "supersecretkey" --- -# Cisco NX-OS specific AAA configuration -# This configures vendor-specific settings like key encryption type, -# login error messages, console authentication, and config-commands authorization. apiVersion: nx.cisco.networking.metal.ironcore.dev/v1alpha1 kind: AAAConfig metadata: @@ -49,7 +41,7 @@ spec: # Reference to the Cisco NX-OS specific AAA configuration providerConfigRef: - group: nx.cisco.networking.metal.ironcore.dev + apiVersion: nx.cisco.networking.metal.ironcore.dev/v1alpha1 kind: AAAConfig name: aaa-tacacs-nxos diff --git a/internal/provider/cisco/nxos/aaa.go b/internal/provider/cisco/nxos/aaa.go index 6f785f7f8..0ff7f2962 100644 --- a/internal/provider/cisco/nxos/aaa.go +++ b/internal/provider/cisco/nxos/aaa.go @@ -154,7 +154,7 @@ func (r *RadiusProviderRef) Key() string { return r.Name } // AAADefaultAuth represents AAA default authentication configuration. type AAADefaultAuth struct { Realm string `json:"realm,omitempty"` - ProviderGroup string `json:"providerGroup,omitempty"` + ProviderGroup string `json:"providerGroup"` Fallback string `json:"fallback,omitempty"` Local string `json:"local,omitempty"` None string `json:"none,omitempty"` @@ -169,7 +169,7 @@ func (*AAADefaultAuth) XPath() string { // AAAConsoleAuth represents AAA console authentication configuration. type AAAConsoleAuth struct { Realm string `json:"realm,omitempty"` - ProviderGroup string `json:"providerGroup,omitempty"` + ProviderGroup string `json:"providerGroup"` Fallback string `json:"fallback,omitempty"` Local string `json:"local,omitempty"` None string `json:"none,omitempty"` @@ -185,7 +185,7 @@ func (*AAAConsoleAuth) XPath() string { // Note: "name" and "realm" are read-only operational fields on NX-OS and must not be sent. type AAADefaultAuthor struct { CmdType string `json:"cmdType"` - ProviderGroup string `json:"providerGroup,omitempty"` + ProviderGroup string `json:"providerGroup"` LocalRbac bool `json:"localRbac,omitempty"` AuthorMethodNone bool `json:"authorMethodNone,omitempty"` } @@ -200,7 +200,7 @@ func (a *AAADefaultAuthor) XPath() string { // Note: "name" is a read-only operational field on NX-OS and must not be sent. type AAADefaultAcc struct { Realm string `json:"realm,omitempty"` - ProviderGroup string `json:"providerGroup,omitempty"` + ProviderGroup string `json:"providerGroup"` LocalRbac bool `json:"localRbac,omitempty"` AccMethodNone bool `json:"accMethodNone,omitempty"` } diff --git a/internal/provider/cisco/nxos/provider.go b/internal/provider/cisco/nxos/provider.go index af7896644..1efb4677f 100644 --- a/internal/provider/cisco/nxos/provider.go +++ b/internal/provider/cisco/nxos/provider.go @@ -3283,12 +3283,17 @@ func (p *Provider) EnsureAAA(ctx context.Context, req *provider.EnsureAAARequest } } + // Collect desired server addresses per protocol to detect stale entries. + desiredTACACS := map[string]bool{} + desiredRADIUS := map[string]bool{} + for _, group := range req.AAA.Spec.ServerGroups { switch group.Type { case v1alpha1.AAAServerGroupTypeTACACS: conf = append(conf, &Feature{Name: "tacacsplus", AdminSt: AdminStEnabled}) for _, server := range group.Servers { + desiredTACACS[server.Address] = true srv := &TacacsPlusProvider{ Name: server.Address, Port: server.TACACS.Port, @@ -3316,6 +3321,7 @@ func (p *Provider) EnsureAAA(ctx context.Context, req *provider.EnsureAAARequest case v1alpha1.AAAServerGroupTypeRADIUS: for _, server := range group.Servers { + desiredRADIUS[server.Address] = true srv := &RadiusProvider{ Name: server.Address, AuthPort: server.RADIUS.AuthenticationPort, @@ -3414,30 +3420,89 @@ func (p *Provider) EnsureAAA(ctx context.Context, req *provider.EnsureAAARequest conf = append(conf, acct) } - return p.Update(ctx, conf...) -} - -func (p *Provider) DeleteAAA(ctx context.Context, req *provider.DeleteAAARequest) error { - // Delete the whole server/group list containers in a single gNMI call. - // Groups are placed before providers to avoid reference violations. - // Deleting the containers also removes any leftover entries from previous reconciles. - if err := p.client.Delete(ctx, - new(TacacsPlusProviderGroupItems), - new(TacacsPlusProviderItems), - new(RadiusProviderGroupItems), - new(RadiusProviderItems), - ); err != nil && !errors.Is(err, gnmiext.ErrNil) { + if err := p.Update(ctx, conf...); err != nil { return err } - // Reset AAA method config and TACACS feature to device defaults. - return p.Update(ctx, - &Feature{Name: "tacacsplus", AdminSt: AdminStDisabled}, + // Remove TACACS+ server host entries that are on the device but no longer in the spec. + // This must happen after the Update above so the server is first dropped from the group's + // ProviderRef-list — NX-OS rejects deleting a server that is still referenced by a group. + if len(desiredTACACS) > 0 { + current := new(TacacsPlusProviderItems) + if err := p.client.GetConfig(ctx, current); err != nil && !errors.Is(err, gnmiext.ErrNil) { + return err + } + var stale []gnmiext.DataElement + for i := range current.ProviderList { + if !desiredTACACS[current.ProviderList[i].Name] { + stale = append(stale, &TacacsPlusProvider{Name: current.ProviderList[i].Name}) + } + } + if len(stale) > 0 { + if err := p.client.Delete(ctx, stale...); err != nil && !errors.Is(err, gnmiext.ErrNil) { + return err + } + } + } + + // Same for RADIUS server host entries. + if len(desiredRADIUS) > 0 { + current := new(RadiusProviderItems) + if err := p.client.GetConfig(ctx, current); err != nil && !errors.Is(err, gnmiext.ErrNil) { + return err + } + var stale []gnmiext.DataElement + for i := range current.ProviderList { + if !desiredRADIUS[current.ProviderList[i].Name] { + stale = append(stale, &RadiusProvider{Name: current.ProviderList[i].Name}) + } + } + if len(stale) > 0 { + if err := p.client.Delete(ctx, stale...); err != nil && !errors.Is(err, gnmiext.ErrNil) { + return err + } + } + } + + return nil +} + +func (p *Provider) DeleteAAA(ctx context.Context, req *provider.DeleteAAARequest) error { + // Step 1: Reset AAA auth realms to local in a separate call before touching the + // TACACS feature flag. NX-OS rolls back the entire gNMI SET batch if any item + // fails validation, so mixing the feature-disable with auth resets causes all + // resets to be silently reverted when the feature cannot be disabled while groups + // are still present. + if err := p.Update(ctx, &AAADefaultAcc{Realm: AAARealmLocal, LocalRbac: true}, &AAADefaultAuthor{CmdType: "config", LocalRbac: true}, &AAADefaultAuth{Realm: AAARealmLocal, Local: AAAValueYes, Fallback: AAAValueYes}, &AAAConsoleAuth{Realm: AAARealmLocal, Local: AAAValueYes, Fallback: AAAValueYes}, - ) + ); err != nil { + return err + } + + // Step 2: Delete server group and server containers for the configured protocols only. + // Groups must be deleted before servers to avoid reference violations. + // NX-OS does not allow deleting the built-in RADIUS group container when no RADIUS + // servers are configured, so only delete what was actually provisioned. + var toDelete []gnmiext.DataElement + for _, group := range req.AAA.Spec.ServerGroups { + switch group.Type { + case v1alpha1.AAAServerGroupTypeTACACS: + toDelete = append(toDelete, new(TacacsPlusProviderGroupItems), new(TacacsPlusProviderItems)) + case v1alpha1.AAAServerGroupTypeRADIUS: + toDelete = append(toDelete, new(RadiusProviderGroupItems), new(RadiusProviderItems)) + } + } + if len(toDelete) > 0 { + if err := p.client.Delete(ctx, toDelete...); err != nil && !errors.Is(err, gnmiext.ErrNil) { + return err + } + } + + // Step 3: Disable the TACACS feature only after groups are gone. + return p.Update(ctx, &Feature{Name: "tacacsplus", AdminSt: AdminStDisabled}) } func init() { From 453fc131fb86f50bd1c0d7bfcb4cbb3e473a9924 Mon Sep 17 00:00:00 2001 From: i521907 Date: Tue, 21 Apr 2026 11:16:46 -0400 Subject: [PATCH 21/32] Rename AAA sample and split provider config into separate file Signed-off-by: i521907 --- Tiltfile | 6 +++++ .../samples/cisco/nx/v1alpha1_aaaconfig.yaml | 20 ++++++++++++++++ config/samples/kustomization.yaml | 2 ++ ...ng_v1alpha1_aaa.yaml => v1alpha1_aaa.yaml} | 24 ------------------- 4 files changed, 28 insertions(+), 24 deletions(-) create mode 100644 config/samples/cisco/nx/v1alpha1_aaaconfig.yaml rename config/samples/{networking_v1alpha1_aaa.yaml => v1alpha1_aaa.yaml} (66%) diff --git a/Tiltfile b/Tiltfile index b03e5885b..35f1a9992 100644 --- a/Tiltfile +++ b/Tiltfile @@ -139,6 +139,12 @@ k8s_resource(new_name='lldp', objects=['leaf1-lldp:lldp'], trigger_mode=TRIGGER_ k8s_yaml('./config/samples/v1alpha1_dhcprelay.yaml') k8s_resource(new_name='dhcprelay', objects=['dhcprelay:dhcprelay'], resource_deps=['eth1-1'], trigger_mode=TRIGGER_MODE_MANUAL, auto_init=False) +k8s_yaml('./config/samples/v1alpha1_aaa.yaml') +k8s_resource(new_name='aaa', objects=['aaa-tacacs:aaa'], trigger_mode=TRIGGER_MODE_MANUAL, auto_init=False) +# Uncomment the following lines for NX-OS specific AAA config +# k8s_yaml('./config/samples/cisco/nx/v1alpha1_aaaconfig.yaml') +# k8s_resource(new_name='aaaconfig', objects=['aaa-tacacs-nxos:aaaconfig'], trigger_mode=TRIGGER_MODE_MANUAL, auto_init=False) + print('🚀 network-operator development environment') print('👉 Edit the code inside the api/, cmd/, or internal/ directories') print('👉 Tilt will automatically rebuild and redeploy when changes are detected') diff --git a/config/samples/cisco/nx/v1alpha1_aaaconfig.yaml b/config/samples/cisco/nx/v1alpha1_aaaconfig.yaml new file mode 100644 index 000000000..402e20463 --- /dev/null +++ b/config/samples/cisco/nx/v1alpha1_aaaconfig.yaml @@ -0,0 +1,20 @@ +apiVersion: nx.cisco.networking.metal.ironcore.dev/v1alpha1 +kind: AAAConfig +metadata: + labels: + app.kubernetes.io/name: network-operator + app.kubernetes.io/managed-by: kustomize + name: aaa-tacacs-nxos +spec: + keyEncryption: Type7 + loginErrorEnable: true + consoleAuthentication: + methods: + - type: Group + groupName: GR_TACACS + - type: Local + configCommandsAuthorization: + methods: + - type: Group + groupName: GR_TACACS + - type: Local diff --git a/config/samples/kustomization.yaml b/config/samples/kustomization.yaml index ac22ba582..5637224c2 100644 --- a/config/samples/kustomization.yaml +++ b/config/samples/kustomization.yaml @@ -32,4 +32,6 @@ resources: - cisco/nx/v1alpha1_interfaceconfig.yaml - cisco/nx/v1alpha1_lldpconfig.yaml - cisco/nx/v1alpha1_bgpconfig.yaml +- v1alpha1_aaa.yaml +- cisco/nx/v1alpha1_aaaconfig.yaml # +kubebuilder:scaffold:manifestskustomizesamples diff --git a/config/samples/networking_v1alpha1_aaa.yaml b/config/samples/v1alpha1_aaa.yaml similarity index 66% rename from config/samples/networking_v1alpha1_aaa.yaml rename to config/samples/v1alpha1_aaa.yaml index 12a28b560..5c96350af 100644 --- a/config/samples/networking_v1alpha1_aaa.yaml +++ b/config/samples/v1alpha1_aaa.yaml @@ -1,5 +1,3 @@ -# Example AAA configuration with TACACS+ servers ---- apiVersion: v1 kind: Secret metadata: @@ -7,39 +5,17 @@ metadata: namespace: default type: Opaque stringData: - # Replace with your actual TACACS+ server shared secret server-key: "supersecretkey" --- -apiVersion: nx.cisco.networking.metal.ironcore.dev/v1alpha1 -kind: AAAConfig -metadata: - name: aaa-tacacs-nxos - namespace: default -spec: - keyEncryption: Type7 - loginErrorEnable: true - consoleAuthentication: - methods: - - type: Group - groupName: GR_TACACS - - type: Local - configCommandsAuthorization: - methods: - - type: Group - groupName: GR_TACACS - - type: Local ---- apiVersion: networking.metal.ironcore.dev/v1alpha1 kind: AAA metadata: name: aaa-tacacs namespace: default spec: - # Reference to the Device this AAA configuration belongs to deviceRef: name: my-switch - # Reference to the Cisco NX-OS specific AAA configuration providerConfigRef: apiVersion: nx.cisco.networking.metal.ironcore.dev/v1alpha1 kind: AAAConfig From caed82f35055d4a2d4d26e67c8013185f1950932 Mon Sep 17 00:00:00 2001 From: i521907 Date: Tue, 21 Apr 2026 12:34:01 -0400 Subject: [PATCH 22/32] Add RBAC helper roles for AAA and AAAConfig CRDs Signed-off-by: i521907 --- config/rbac/aaa_admin_role.yaml | 27 +++++++++++++++ config/rbac/aaa_editor_role.yaml | 33 +++++++++++++++++++ config/rbac/aaa_viewer_role.yaml | 29 ++++++++++++++++ .../rbac/cisco/nx/aaaconfig_admin_role.yaml | 27 +++++++++++++++ .../rbac/cisco/nx/aaaconfig_editor_role.yaml | 33 +++++++++++++++++++ .../rbac/cisco/nx/aaaconfig_viewer_role.yaml | 29 ++++++++++++++++ config/rbac/kustomization.yaml | 6 ++++ 7 files changed, 184 insertions(+) create mode 100644 config/rbac/aaa_admin_role.yaml create mode 100644 config/rbac/aaa_editor_role.yaml create mode 100644 config/rbac/aaa_viewer_role.yaml create mode 100644 config/rbac/cisco/nx/aaaconfig_admin_role.yaml create mode 100644 config/rbac/cisco/nx/aaaconfig_editor_role.yaml create mode 100644 config/rbac/cisco/nx/aaaconfig_viewer_role.yaml diff --git a/config/rbac/aaa_admin_role.yaml b/config/rbac/aaa_admin_role.yaml new file mode 100644 index 000000000..67f87cec2 --- /dev/null +++ b/config/rbac/aaa_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: aaa-admin-role +rules: +- apiGroups: + - networking.metal.ironcore.dev + resources: + - aaa + verbs: + - '*' +- apiGroups: + - networking.metal.ironcore.dev + resources: + - aaa/status + verbs: + - get diff --git a/config/rbac/aaa_editor_role.yaml b/config/rbac/aaa_editor_role.yaml new file mode 100644 index 000000000..872e7ae93 --- /dev/null +++ b/config/rbac/aaa_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: aaa-editor-role +rules: +- apiGroups: + - networking.metal.ironcore.dev + resources: + - aaa + verbs: + - create + - delete + - get + - list + - patch + - update + - watch +- apiGroups: + - networking.metal.ironcore.dev + resources: + - aaa/status + verbs: + - get diff --git a/config/rbac/aaa_viewer_role.yaml b/config/rbac/aaa_viewer_role.yaml new file mode 100644 index 000000000..f6d1bfcc7 --- /dev/null +++ b/config/rbac/aaa_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: aaa-viewer-role +rules: +- apiGroups: + - networking.metal.ironcore.dev + resources: + - aaa + verbs: + - get + - list + - watch +- apiGroups: + - networking.metal.ironcore.dev + resources: + - aaa/status + verbs: + - get diff --git a/config/rbac/cisco/nx/aaaconfig_admin_role.yaml b/config/rbac/cisco/nx/aaaconfig_admin_role.yaml new file mode 100644 index 000000000..7896cc9df --- /dev/null +++ b/config/rbac/cisco/nx/aaaconfig_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 nx.cisco.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: nx.cisco-aaaconfig-admin-role +rules: +- apiGroups: + - nx.cisco.networking.metal.ironcore.dev + resources: + - aaaconfigs + verbs: + - '*' +- apiGroups: + - nx.cisco.networking.metal.ironcore.dev + resources: + - aaaconfigs/status + verbs: + - get diff --git a/config/rbac/cisco/nx/aaaconfig_editor_role.yaml b/config/rbac/cisco/nx/aaaconfig_editor_role.yaml new file mode 100644 index 000000000..063c57bcd --- /dev/null +++ b/config/rbac/cisco/nx/aaaconfig_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 nx.cisco.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: nx.cisco-aaaconfig-editor-role +rules: +- apiGroups: + - nx.cisco.networking.metal.ironcore.dev + resources: + - aaaconfigs + verbs: + - create + - delete + - get + - list + - patch + - update + - watch +- apiGroups: + - nx.cisco.networking.metal.ironcore.dev + resources: + - aaaconfigs/status + verbs: + - get diff --git a/config/rbac/cisco/nx/aaaconfig_viewer_role.yaml b/config/rbac/cisco/nx/aaaconfig_viewer_role.yaml new file mode 100644 index 000000000..260e50684 --- /dev/null +++ b/config/rbac/cisco/nx/aaaconfig_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 nx.cisco.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: nx.cisco-aaaconfig-viewer-role +rules: +- apiGroups: + - nx.cisco.networking.metal.ironcore.dev + resources: + - aaaconfigs + verbs: + - get + - list + - watch +- apiGroups: + - nx.cisco.networking.metal.ironcore.dev + resources: + - aaaconfigs/status + verbs: + - get diff --git a/config/rbac/kustomization.yaml b/config/rbac/kustomization.yaml index f6aab1b99..7dbbe10e5 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. +- aaa_admin_role.yaml +- aaa_editor_role.yaml +- aaa_viewer_role.yaml - accesscontrollist_admin_role.yaml - accesscontrollist_editor_role.yaml - accesscontrollist_viewer_role.yaml @@ -113,6 +116,9 @@ resources: - cisco/nx/lldpconfig_admin_role.yaml - cisco/nx/lldpconfig_editor_role.yaml - cisco/nx/lldpconfig_viewer_role.yaml +- cisco/nx/aaaconfig_admin_role.yaml +- cisco/nx/aaaconfig_editor_role.yaml +- cisco/nx/aaaconfig_viewer_role.yaml - cisco/nx/bgpconfig_admin_role.yaml - cisco/nx/bgpconfig_editor_role.yaml - cisco/nx/bgpconfig_viewer_role.yaml From 391ad3bac73a2c29bfd674ddf44e0fe5e766d0f5 Mon Sep 17 00:00:00 2001 From: i521907 Date: Tue, 21 Apr 2026 12:59:36 -0400 Subject: [PATCH 23/32] Remove unused authProtocol field from AAA auth structs Signed-off-by: i521907 --- internal/provider/cisco/nxos/aaa.go | 2 -- 1 file changed, 2 deletions(-) diff --git a/internal/provider/cisco/nxos/aaa.go b/internal/provider/cisco/nxos/aaa.go index 0ff7f2962..d62187e97 100644 --- a/internal/provider/cisco/nxos/aaa.go +++ b/internal/provider/cisco/nxos/aaa.go @@ -159,7 +159,6 @@ type AAADefaultAuth struct { Local string `json:"local,omitempty"` None string `json:"none,omitempty"` ErrEn bool `json:"errEn,omitempty"` - AuthProtocol string `json:"authProtocol,omitempty"` } func (*AAADefaultAuth) XPath() string { @@ -174,7 +173,6 @@ type AAAConsoleAuth struct { Local string `json:"local,omitempty"` None string `json:"none,omitempty"` ErrEn bool `json:"errEn,omitempty"` - AuthProtocol string `json:"authProtocol,omitempty"` } func (*AAAConsoleAuth) XPath() string { From ba34f7bce70a97e1e104d09d3c8f3bef250468e2 Mon Sep 17 00:00:00 2001 From: i521907 Date: Wed, 22 Apr 2026 14:42:46 -0400 Subject: [PATCH 24/32] Refactor EnsureAAA and DeleteAAA per review Signed-off-by: i521907 --- .typos.toml | 2 - docs/architecture-overview.md | 34 +++--- internal/provider/cisco/nxos/provider.go | 139 ++++++++++------------- 3 files changed, 75 insertions(+), 100 deletions(-) diff --git a/.typos.toml b/.typos.toml index 64c1e1211..b9215bcbd 100644 --- a/.typos.toml +++ b/.typos.toml @@ -17,8 +17,6 @@ mininum = "mininum" specifc = "specifc" # Go built-in keyword cpy = "cpy" -# Documentation -nd = "nd" [files] extend-exclude = [ diff --git a/docs/architecture-overview.md b/docs/architecture-overview.md index bdd3ca79a..90f0ab763 100644 --- a/docs/architecture-overview.md +++ b/docs/architecture-overview.md @@ -16,7 +16,7 @@ sequenceDiagram participant CFG as Config Reconciler participant BOOT as Provisioning HTTP / TFTP participant P as Provider / Transport - participant ND as Network Device + participant DEVICE as Network Device U->>API: Apply Device or config CR opt Webhook registered for this kind @@ -34,17 +34,17 @@ sequenceDiagram alt Provisioning is configured and phase is Pending DEV->>API: Set phase=Provisioning and Ready=False DEV-->>API: Requeue for follow-up checks - ND->>+BOOT: Request bootstrap config + DEVICE->>+BOOT: Request bootstrap config BOOT->>API: Read Device, secrets, and bootstrap assets - BOOT-->>ND: Return provisioning config, certs, or boot script - ND->>BOOT: Report provisioning progress + BOOT-->>DEVICE: Return provisioning config, certs, or boot script + DEVICE->>BOOT: Report provisioning progress BOOT->>API: Update provisioning status API-)MGR: Watch event for Device update end DEV->>+P: Connect to provider - P->>ND: Read device facts and ports - ND-->>P: Inventory and operational state + P->>DEVICE: Read device facts and ports + DEVICE-->>P: Inventory and operational state P-->>-DEV: Device details DEV->>API: Patch status, labels, and Ready condition DEV-->>-MGR: Reconcile complete @@ -57,8 +57,8 @@ sequenceDiagram CFG->>CFG: Acquire per-device lock CFG->>API: Ensure finalizer, device label, and owner reference CFG->>+P: Connect using device endpoint and credentials - P->>ND: Ensure intended configuration - ND-->>P: Apply result + P->>DEVICE: Ensure intended configuration + DEVICE-->>P: Apply result P-->>-CFG: Success or error CFG->>API: Patch Ready condition and status CFG->>CFG: Release per-device lock @@ -88,7 +88,7 @@ sequenceDiagram participant DEV as DeviceReconciler participant BOOT as Provisioning HTTP / TFTP participant P as Provider / Transport - participant ND as Network Device + participant DEVICE as Network Device U->>API: Apply Device CR API-->>U: Object persisted @@ -101,18 +101,18 @@ sequenceDiagram alt Provisioning configured and phase is Pending DEV->>API: Set phase=Provisioning and Ready=False DEV-->>API: Requeue for follow-up checks - ND->>+BOOT: Request bootstrap config + DEVICE->>+BOOT: Request bootstrap config BOOT->>API: Read Device, secrets, and bootstrap assets - BOOT-->>ND: Return provisioning config, certs, or boot script - ND->>BOOT: Report provisioning progress + BOOT-->>DEVICE: Return provisioning config, certs, or boot script + DEVICE->>BOOT: Report provisioning progress BOOT->>API: Update provisioning status API-)MGR: Watch event for Device update DEV->>API: Requeue until provisioning completes end DEV->>+P: Connect to provider - P->>ND: Read device facts and ports - ND-->>P: Inventory and operational state + P->>DEVICE: Read device facts and ports + DEVICE-->>P: Inventory and operational state P-->>-DEV: Device details DEV->>API: Patch status, labels, and Ready condition DEV-->>-MGR: Reconcile complete @@ -130,7 +130,7 @@ sequenceDiagram participant MGR as Controller Manager participant CFG as Config Reconciler participant P as Provider / Transport - participant ND as Network Device + participant DEVICE as Network Device U->>API: Apply config CR opt Webhook registered for this kind @@ -147,8 +147,8 @@ sequenceDiagram CFG->>CFG: Acquire per-device lock CFG->>API: Ensure finalizer, device label, and owner reference CFG->>+P: Connect using device endpoint and credentials - P->>ND: Ensure intended configuration - ND-->>P: Apply result + P->>DEVICE: Ensure intended configuration + DEVICE-->>P: Apply result P-->>-CFG: Success or error CFG->>API: Patch Ready condition and status CFG->>CFG: Release per-device lock diff --git a/internal/provider/cisco/nxos/provider.go b/internal/provider/cisco/nxos/provider.go index 1efb4677f..16fc1d0e3 100644 --- a/internal/provider/cisco/nxos/provider.go +++ b/internal/provider/cisco/nxos/provider.go @@ -3273,9 +3273,6 @@ func (p *Provider) GetDHCPRelayStatus(ctx context.Context, req *provider.DHCPRel } func (p *Provider) EnsureAAA(ctx context.Context, req *provider.EnsureAAARequest) error { - var conf []gnmiext.DataElement - - // Read Cisco-specific config from ProviderConfig var cfg nxv1alpha1.AAAConfig if req.ProviderConfig != nil { if err := req.ProviderConfig.Into(&cfg); err != nil { @@ -3283,9 +3280,10 @@ func (p *Provider) EnsureAAA(ctx context.Context, req *provider.EnsureAAARequest } } - // Collect desired server addresses per protocol to detect stale entries. - desiredTACACS := map[string]bool{} - desiredRADIUS := map[string]bool{} + desiredTACACS := map[string]struct{}{} + desiredRADIUS := map[string]struct{}{} + + var conf []gnmiext.DataElement for _, group := range req.AAA.Spec.ServerGroups { switch group.Type { @@ -3293,12 +3291,12 @@ func (p *Provider) EnsureAAA(ctx context.Context, req *provider.EnsureAAARequest conf = append(conf, &Feature{Name: "tacacsplus", AdminSt: AdminStEnabled}) for _, server := range group.Servers { - desiredTACACS[server.Address] = true + desiredTACACS[server.Address] = struct{}{} srv := &TacacsPlusProvider{ Name: server.Address, Port: server.TACACS.Port, KeyEnc: MapKeyEncryption(cfg.Spec.KeyEncryption), - Timeout: 5, // YANG default + Timeout: 5, } if key, ok := req.TACACSServerKeys[server.Address]; ok { srv.Key = key @@ -3321,13 +3319,13 @@ func (p *Provider) EnsureAAA(ctx context.Context, req *provider.EnsureAAARequest case v1alpha1.AAAServerGroupTypeRADIUS: for _, server := range group.Servers { - desiredRADIUS[server.Address] = true + desiredRADIUS[server.Address] = struct{}{} srv := &RadiusProvider{ Name: server.Address, AuthPort: server.RADIUS.AuthenticationPort, AcctPort: server.RADIUS.AccountingPort, KeyEnc: MapRADIUSKeyEncryption(cfg.Spec.RADIUSKeyEncryption), - Timeout: 5, // YANG default + Timeout: 5, } if key, ok := req.RADIUSServerKeys[server.Address]; ok { srv.Key = key @@ -3350,9 +3348,12 @@ func (p *Provider) EnsureAAA(ctx context.Context, req *provider.EnsureAAARequest } } + // Always take full ownership of the auth config. If not specified in the spec, + // reset to the device default (local auth). + authen := &AAADefaultAuth{Realm: AAARealmLocal, Local: AAAValueYes, Fallback: AAAValueYes} if req.AAA.Spec.Authentication != nil && len(req.AAA.Spec.Authentication.Methods) > 0 { methods := req.AAA.Spec.Authentication.Methods - authen := &AAADefaultAuth{ + authen = &AAADefaultAuth{ ErrEn: cfg.Spec.LoginErrorEnable, Fallback: MapFallbackFromMethodList(methods), Local: MapLocalFromMethodList(methods), @@ -3363,12 +3364,13 @@ func (p *Provider) EnsureAAA(ctx context.Context, req *provider.EnsureAAARequest } else { authen.Realm = MapRealmFromMethodType(methods[0].Type) } - conf = append(conf, authen) } + conf = append(conf, authen) + consoleAuth := &AAAConsoleAuth{Realm: AAARealmLocal, Local: AAAValueYes, Fallback: AAAValueYes} if cfg.Spec.ConsoleAuthentication != nil && len(cfg.Spec.ConsoleAuthentication.Methods) > 0 { methods := cfg.Spec.ConsoleAuthentication.Methods - consoleAuth := &AAAConsoleAuth{ + consoleAuth = &AAAConsoleAuth{ ErrEn: cfg.Spec.LoginErrorEnable, Fallback: MapFallback(methods), Local: MapLocal(methods), @@ -3379,36 +3381,37 @@ func (p *Provider) EnsureAAA(ctx context.Context, req *provider.EnsureAAARequest } else { consoleAuth.Realm = MapRealmFromMethodType(methods[0].Type) } - conf = append(conf, consoleAuth) } + conf = append(conf, consoleAuth) + // ConfigCommandsAuthorization (Cisco-specific) takes priority over the core Authorization field. + author := &AAADefaultAuthor{CmdType: "config", LocalRbac: true} if req.AAA.Spec.Authorization != nil && len(req.AAA.Spec.Authorization.Methods) > 0 { methods := req.AAA.Spec.Authorization.Methods - author := &AAADefaultAuthor{ + author = &AAADefaultAuthor{ CmdType: "config", LocalRbac: MapLocalFromMethodList(methods) == AAAValueYes, } if methods[0].Type == v1alpha1.AAAMethodTypeGroup { author.ProviderGroup = methods[0].GroupName } - conf = append(conf, author) } - if cfg.Spec.ConfigCommandsAuthorization != nil && len(cfg.Spec.ConfigCommandsAuthorization.Methods) > 0 { methods := cfg.Spec.ConfigCommandsAuthorization.Methods - author := &AAADefaultAuthor{ + author = &AAADefaultAuthor{ CmdType: "config", LocalRbac: MapLocal(methods) == AAAValueYes, } if methods[0].Type == v1alpha1.AAAMethodTypeGroup { author.ProviderGroup = methods[0].GroupName } - conf = append(conf, author) } + conf = append(conf, author) + acct := &AAADefaultAcc{Realm: AAARealmLocal, LocalRbac: true} if req.AAA.Spec.Accounting != nil && len(req.AAA.Spec.Accounting.Methods) > 0 { methods := req.AAA.Spec.Accounting.Methods - acct := &AAADefaultAcc{ + acct = &AAADefaultAcc{ LocalRbac: MapLocalFromMethodList(methods) == AAAValueYes, } if methods[0].Type == v1alpha1.AAAMethodTypeGroup { @@ -3417,62 +3420,43 @@ func (p *Provider) EnsureAAA(ctx context.Context, req *provider.EnsureAAARequest } else { acct.Realm = MapRealmFromMethodType(methods[0].Type) } - conf = append(conf, acct) } + conf = append(conf, acct) - if err := p.Update(ctx, conf...); err != nil { + // Fetch current server lists before applying desired state to compute stale entries. + currentTACACS := new(TacacsPlusProviderItems) + if err := p.client.GetConfig(ctx, currentTACACS); err != nil && !errors.Is(err, gnmiext.ErrNil) { + return err + } + currentRADIUS := new(RadiusProviderItems) + if err := p.client.GetConfig(ctx, currentRADIUS); err != nil && !errors.Is(err, gnmiext.ErrNil) { return err } - // Remove TACACS+ server host entries that are on the device but no longer in the spec. - // This must happen after the Update above so the server is first dropped from the group's - // ProviderRef-list — NX-OS rejects deleting a server that is still referenced by a group. - if len(desiredTACACS) > 0 { - current := new(TacacsPlusProviderItems) - if err := p.client.GetConfig(ctx, current); err != nil && !errors.Is(err, gnmiext.ErrNil) { - return err - } - var stale []gnmiext.DataElement - for i := range current.ProviderList { - if !desiredTACACS[current.ProviderList[i].Name] { - stale = append(stale, &TacacsPlusProvider{Name: current.ProviderList[i].Name}) - } - } - if len(stale) > 0 { - if err := p.client.Delete(ctx, stale...); err != nil && !errors.Is(err, gnmiext.ErrNil) { - return err - } - } + if err := p.Update(ctx, conf...); err != nil { + return err } - // Same for RADIUS server host entries. - if len(desiredRADIUS) > 0 { - current := new(RadiusProviderItems) - if err := p.client.GetConfig(ctx, current); err != nil && !errors.Is(err, gnmiext.ErrNil) { - return err + // Remove server host entries no longer in the spec. The Update above already + // dropped stale entries from the group ProviderRef-lists, so the deletes below + // are safe (NX-OS rejects deleting a server that is still group-referenced). + var stale []gnmiext.DataElement + for i := range currentTACACS.ProviderList { + if _, ok := desiredTACACS[currentTACACS.ProviderList[i].Name]; !ok { + stale = append(stale, &TacacsPlusProvider{Name: currentTACACS.ProviderList[i].Name}) } - var stale []gnmiext.DataElement - for i := range current.ProviderList { - if !desiredRADIUS[current.ProviderList[i].Name] { - stale = append(stale, &RadiusProvider{Name: current.ProviderList[i].Name}) - } - } - if len(stale) > 0 { - if err := p.client.Delete(ctx, stale...); err != nil && !errors.Is(err, gnmiext.ErrNil) { - return err - } + } + for i := range currentRADIUS.ProviderList { + if _, ok := desiredRADIUS[currentRADIUS.ProviderList[i].Name]; !ok { + stale = append(stale, &RadiusProvider{Name: currentRADIUS.ProviderList[i].Name}) } } - - return nil + return p.client.Delete(ctx, stale...) } func (p *Provider) DeleteAAA(ctx context.Context, req *provider.DeleteAAARequest) error { - // Step 1: Reset AAA auth realms to local in a separate call before touching the - // TACACS feature flag. NX-OS rolls back the entire gNMI SET batch if any item - // fails validation, so mixing the feature-disable with auth resets causes all - // resets to be silently reverted when the feature cannot be disabled while groups - // are still present. + // Step 1: Reset auth realms to local before deleting groups. NX-OS may leave auth + // in a broken state if the referenced provider group is removed while still active. if err := p.Update(ctx, &AAADefaultAcc{Realm: AAARealmLocal, LocalRbac: true}, &AAADefaultAuthor{CmdType: "config", LocalRbac: true}, @@ -3482,26 +3466,19 @@ func (p *Provider) DeleteAAA(ctx context.Context, req *provider.DeleteAAARequest return err } - // Step 2: Delete server group and server containers for the configured protocols only. - // Groups must be deleted before servers to avoid reference violations. - // NX-OS does not allow deleting the built-in RADIUS group container when no RADIUS - // servers are configured, so only delete what was actually provisioned. - var toDelete []gnmiext.DataElement - for _, group := range req.AAA.Spec.ServerGroups { - switch group.Type { - case v1alpha1.AAAServerGroupTypeTACACS: - toDelete = append(toDelete, new(TacacsPlusProviderGroupItems), new(TacacsPlusProviderItems)) - case v1alpha1.AAAServerGroupTypeRADIUS: - toDelete = append(toDelete, new(RadiusProviderGroupItems), new(RadiusProviderItems)) - } - } - if len(toDelete) > 0 { - if err := p.client.Delete(ctx, toDelete...); err != nil && !errors.Is(err, gnmiext.ErrNil) { - return err - } + // Step 2: Unconditionally delete all TACACS+ and RADIUS server groups and servers. + // Groups must precede servers in the delete to avoid reference violations. + // ErrNil is returned when a container is already empty, which is safe to ignore. + if err := p.client.Delete(ctx, + new(TacacsPlusProviderGroupItems), + new(TacacsPlusProviderItems), + new(RadiusProviderGroupItems), + new(RadiusProviderItems), + ); err != nil && !errors.Is(err, gnmiext.ErrNil) { + return err } - // Step 3: Disable the TACACS feature only after groups are gone. + // Step 3: Disable the TACACS feature after groups are gone. return p.Update(ctx, &Feature{Name: "tacacsplus", AdminSt: AdminStDisabled}) } From 597a204877d79ede4ebc45c4e646285163a10d31 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felix=20K=C3=A4stner?= Date: Fri, 17 Apr 2026 21:59:36 +0200 Subject: [PATCH 25/32] Use context-aware exec.CommandContext in e2e tests Pass the suite context to all exec.Command calls so that running subprocesses are cancelled automatically when the suite context is cancelled. Signed-off-by: i521907 --- test/e2e/util_test.go | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/test/e2e/util_test.go b/test/e2e/util_test.go index 357817a1a..4610174bc 100644 --- a/test/e2e/util_test.go +++ b/test/e2e/util_test.go @@ -11,7 +11,6 @@ import ( "fmt" "os" "os/exec" - "path/filepath" "strings" . "github.com/onsi/ginkgo/v2" @@ -209,8 +208,8 @@ func LoadImageToKindClusterWithName(ctx context.Context, name string) error { return fmt.Errorf("failed to create temp file: %w", err) } _ = file.Close() - imgPath := filepath.Clean(file.Name()) - defer func() { _ = os.Remove(imgPath) }() + // #nosec G703 + defer func() { _ = os.Remove(file.Name()) }() // https://github.com/containerd/nerdctl/blob/main/docs/command-reference.md#whale-nerdctl-save // https://docs.podman.io/en/v5.3.0/markdown/podman-save.1.html From 181344e00f99054c591e29b094ae4eec113893b8 Mon Sep 17 00:00:00 2001 From: i521907 Date: Thu, 30 Apr 2026 08:43:34 -0400 Subject: [PATCH 26/32] Fix deviceutil: remove spurious NewGrpcClient block from rebase conflict Signed-off-by: i521907 --- internal/deviceutil/deviceutil.go | 76 ------------------------------- 1 file changed, 76 deletions(-) diff --git a/internal/deviceutil/deviceutil.go b/internal/deviceutil/deviceutil.go index 8cff76772..377f7fbab 100644 --- a/internal/deviceutil/deviceutil.go +++ b/internal/deviceutil/deviceutil.go @@ -162,79 +162,3 @@ func GetDeviceConnection(ctx context.Context, r client.Reader, obj *v1alpha1.Dev TLS: conf, }, nil } -// NewGrpcClient creates a new gRPC client connection to a specified device using the provided [Connection]. -// The connection will use TLS if the [Connection.TLS] field is set, otherwise it will use an insecure connection. -// If the [Connection.Username] and [Connection.Password] fields are set, basic authentication in the form of metadata will be used. -func NewGrpcClient(ctx context.Context, conn *Connection, o ...Option) (*grpc.ClientConn, error) { - creds := insecure.NewCredentials() - if conn.TLS != nil { - creds = credentials.NewTLS(conn.TLS) - } - - opts := []grpc.DialOption{grpc.WithTransportCredentials(creds)} - if conn.Username != "" && conn.Password != "" { - opts = append(opts, grpc.WithPerRPCCredentials(&auth{ - Username: conn.Username, - Password: conn.Password, - })) - } - - for _, opt := range o { - dialOpt, err := opt() - if err != nil { - return nil, err - } - opts = append(opts, dialOpt) - } - - return grpc.NewClient(conn.Address, opts...) -} - -type Option func() (grpc.DialOption, error) - -// WithDefaultTimeout returns a gRPC dial option that sets a default timeout for each RPC. -// If a deadline is already present in the context, it will not be modified. -func WithDefaultTimeout(timeout time.Duration) Option { - return func() (grpc.DialOption, error) { - if timeout <= 0 { - return nil, errors.New("timeout must be greater than zero") - } - return grpc.WithUnaryInterceptor(UnaryDefaultTimeoutInterceptor(timeout)), nil - } -} - -type auth struct { - Username string - Password string `json:"-"` - SecureTransportCreds bool -} - -var _ credentials.PerRPCCredentials = (*auth)(nil) - -func (a *auth) GetRequestMetadata(_ context.Context, _ ...string) (map[string]string, error) { - return map[string]string{ - "username": a.Username, - "password": a.Password, - }, nil -} - -func (a *auth) RequireTransportSecurity() bool { - // Only called if the transport credentials are insecure. - return false -} - -// UnaryDefaultTimeoutInterceptor returns a gRPC unary client interceptor that sets a default timeout -// for each RPC. If a deadline is already present , it will not be modified. -func UnaryDefaultTimeoutInterceptor(timeout time.Duration) grpc.UnaryClientInterceptor { - return func(ctx context.Context, method string, req, reply any, cc *grpc.ClientConn, invoker grpc.UnaryInvoker, opts ...grpc.CallOption) error { - if _, ok := ctx.Deadline(); ok { - return invoker(ctx, method, req, reply, cc, opts...) - } - - ctx, cancel := context.WithTimeout(ctx, timeout) - defer cancel() - - return invoker(ctx, method, req, reply, cc, opts...) - } -} - From e52e3795d78793aae126fa65330f55640dc6a00b Mon Sep 17 00:00:00 2001 From: i521907 Date: Thu, 30 Apr 2026 09:00:16 -0400 Subject: [PATCH 27/32] charts: regenerate via make charts Signed-off-by: i521907 Signed-off-by: i521907 --- charts/network-operator/Chart.yaml | 13 +- .../aaa.networking.metal.ironcore.dev.yaml | 468 ++++++++++++++++++ ...x.cisco.networking.metal.ironcore.dev.yaml | 144 ++++++ .../network-policy/allow-metrics-traffic.yaml | 32 -- .../network-policy/allow-webhook-traffic.yaml | 32 -- .../templates/rbac/aaa-admin-role.yaml | 31 ++ .../templates/rbac/aaa-editor-role.yaml | 37 ++ .../templates/rbac/aaa-viewer-role.yaml | 33 ++ .../rbac/nx.cisco-aaaconfig-admin-role.yaml | 31 ++ .../rbac/nx.cisco-aaaconfig-editor-role.yaml | 37 ++ .../rbac/nx.cisco-aaaconfig-viewer-role.yaml | 33 ++ charts/network-operator/values.yaml | 8 +- 12 files changed, 828 insertions(+), 71 deletions(-) create mode 100644 charts/network-operator/templates/crd/aaa.networking.metal.ironcore.dev.yaml create mode 100644 charts/network-operator/templates/crd/aaaconfigs.nx.cisco.networking.metal.ironcore.dev.yaml delete mode 100644 charts/network-operator/templates/network-policy/allow-metrics-traffic.yaml delete mode 100644 charts/network-operator/templates/network-policy/allow-webhook-traffic.yaml create mode 100644 charts/network-operator/templates/rbac/aaa-admin-role.yaml create mode 100644 charts/network-operator/templates/rbac/aaa-editor-role.yaml create mode 100644 charts/network-operator/templates/rbac/aaa-viewer-role.yaml create mode 100644 charts/network-operator/templates/rbac/nx.cisco-aaaconfig-admin-role.yaml create mode 100644 charts/network-operator/templates/rbac/nx.cisco-aaaconfig-editor-role.yaml create mode 100644 charts/network-operator/templates/rbac/nx.cisco-aaaconfig-viewer-role.yaml diff --git a/charts/network-operator/Chart.yaml b/charts/network-operator/Chart.yaml index 6d48691ce..58d14f8be 100644 --- a/charts/network-operator/Chart.yaml +++ b/charts/network-operator/Chart.yaml @@ -1,5 +1,14 @@ apiVersion: v2 name: network-operator -description: A Helm chart to distribute the project network-operator +description: A Helm chart to distribute network-operator type: application -version: 0.0.0+dev + +version: 0.1.0 +appVersion: "0.1.0" + +keywords: + - kubernetes + - operator + +annotations: + kubebuilder.io/generated-by: kubebuilder diff --git a/charts/network-operator/templates/crd/aaa.networking.metal.ironcore.dev.yaml b/charts/network-operator/templates/crd/aaa.networking.metal.ironcore.dev.yaml new file mode 100644 index 000000000..77b71090f --- /dev/null +++ b/charts/network-operator/templates/crd/aaa.networking.metal.ironcore.dev.yaml @@ -0,0 +1,468 @@ +{{- if .Values.crd.enable }} +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + {{- if .Values.crd.keep }} + "helm.sh/resource-policy": keep + {{- end }} + controller-gen.kubebuilder.io/version: v0.20.1 + name: aaa.networking.metal.ironcore.dev +spec: + group: networking.metal.ironcore.dev + names: + kind: AAA + listKind: AAAList + plural: aaa + singular: aaa + scope: Namespaced + versions: + - additionalPrinterColumns: + - jsonPath: .spec.deviceRef.name + name: Device + type: string + - jsonPath: .status.conditions[?(@.type=="Ready")].status + name: Ready + type: string + - jsonPath: .metadata.creationTimestamp + name: Age + type: date + name: v1alpha1 + schema: + openAPIV3Schema: + description: AAA is the Schema for the aaa 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: |- + Specification of the desired state of the resource. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#spec-and-status + properties: + accounting: + description: Accounting defines the AAA accounting method list. + properties: + methods: + description: |- + Methods is the ordered list of accounting methods. + Methods are tried in order until one succeeds or all fail. + items: + description: AAAMethod represents an AAA method. + properties: + groupName: + description: GroupName is the name of the server group when + Type is Group. + maxLength: 63 + type: string + type: + description: Type is the type of AAA method. + enum: + - Group + - Local + - None + type: string + required: + - type + type: object + x-kubernetes-validations: + - message: groupName is required when type is Group + rule: self.type != 'Group' || self.groupName != "" + maxItems: 4 + minItems: 1 + type: array + x-kubernetes-list-type: atomic + required: + - methods + type: object + authentication: + description: Authentication defines the AAA authentication method + list. + properties: + methods: + description: |- + Methods is the ordered list of authentication methods. + Methods are tried in order until one succeeds or all fail. + items: + description: AAAMethod represents an AAA method. + properties: + groupName: + description: GroupName is the name of the server group when + Type is Group. + maxLength: 63 + type: string + type: + description: Type is the type of AAA method. + enum: + - Group + - Local + - None + type: string + required: + - type + type: object + x-kubernetes-validations: + - message: groupName is required when type is Group + rule: self.type != 'Group' || self.groupName != "" + maxItems: 4 + minItems: 1 + type: array + x-kubernetes-list-type: atomic + required: + - methods + type: object + authorization: + description: Authorization defines the AAA authorization method list. + properties: + methods: + description: |- + Methods is the ordered list of authorization methods. + Methods are tried in order until one succeeds or all fail. + items: + description: AAAMethod represents an AAA method. + properties: + groupName: + description: GroupName is the name of the server group when + Type is Group. + maxLength: 63 + type: string + type: + description: Type is the type of AAA method. + enum: + - Group + - Local + - None + type: string + required: + - type + type: object + x-kubernetes-validations: + - message: groupName is required when type is Group + rule: self.type != 'Group' || self.groupName != "" + maxItems: 4 + minItems: 1 + type: array + x-kubernetes-list-type: atomic + required: + - methods + type: object + 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 + providerConfigRef: + description: |- + ProviderConfigRef is a reference to a resource holding the provider-specific configuration of this AAA. + This reference is used to link the AAA 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 + serverGroups: + description: ServerGroups is the list of AAA server groups. + items: + description: AAAServerGroup represents a named group of AAA servers. + properties: + name: + description: Name is the name of the server group. + maxLength: 63 + minLength: 1 + type: string + servers: + description: Servers is the list of servers in this group. + items: + description: AAAServer represents a single AAA server within + a group. + properties: + address: + description: Address is the IP address or hostname of + the server. + maxLength: 253 + minLength: 1 + type: string + radius: + description: |- + RADIUS contains RADIUS specific server configuration. + Required when the parent server group type is RADIUS. + properties: + accountingPort: + default: 1813 + description: |- + AccountingPort is the UDP port for RADIUS accounting requests. + Defaults to 1813 if not specified. + format: int32 + maximum: 65535 + minimum: 1 + type: integer + authenticationPort: + default: 1812 + description: |- + AuthenticationPort is the UDP port for RADIUS authentication requests. + Defaults to 1812 if not specified. + format: int32 + maximum: 65535 + minimum: 1 + type: integer + keySecretRef: + description: |- + KeySecretRef is a reference to a secret containing the plain text shared key for this RADIUS server. + The secret must contain a key specified in the SecretKeySelector. + properties: + key: + description: |- + Key is the of the entry in the secret resource's `data` or `stringData` + field to be used. + maxLength: 253 + minLength: 1 + type: string + name: + description: Name is unique within a namespace + to reference a secret resource. + maxLength: 253 + minLength: 1 + type: string + namespace: + description: |- + Namespace defines the space within which the secret name must be unique. + If omitted, the namespace of the object being reconciled will be used. + maxLength: 63 + minLength: 1 + type: string + required: + - key + - name + type: object + x-kubernetes-map-type: atomic + required: + - keySecretRef + type: object + tacacs: + description: |- + TACACS contains TACACS+ specific server configuration. + Required when the parent server group type is TACACS. + properties: + keySecretRef: + description: |- + KeySecretRef is a reference to a secret containing the plain text shared key for this TACACS+ server. + The secret must contain a key specified in the SecretKeySelector. + properties: + key: + description: |- + Key is the of the entry in the secret resource's `data` or `stringData` + field to be used. + maxLength: 253 + minLength: 1 + type: string + name: + description: Name is unique within a namespace + to reference a secret resource. + maxLength: 253 + minLength: 1 + type: string + namespace: + description: |- + Namespace defines the space within which the secret name must be unique. + If omitted, the namespace of the object being reconciled will be used. + maxLength: 63 + minLength: 1 + type: string + required: + - key + - name + type: object + x-kubernetes-map-type: atomic + port: + default: 49 + description: |- + Port is the TCP port of the TACACS+ server. + Defaults to 49 if not specified. + format: int32 + maximum: 65535 + minimum: 1 + type: integer + required: + - keySecretRef + type: object + timeout: + description: Timeout is the response timeout for this + server. + type: string + required: + - address + type: object + maxItems: 16 + minItems: 1 + type: array + x-kubernetes-list-map-keys: + - address + x-kubernetes-list-type: map + sourceInterfaceName: + description: SourceInterfaceName is the source interface to + use for communication with the servers. + maxLength: 63 + type: string + type: + description: Type is the protocol type of this server group. + enum: + - TACACS + - RADIUS + type: string + vrfName: + description: VrfName is the VRF to use for communication with + the servers in this group. + maxLength: 63 + type: string + required: + - name + - servers + - type + type: object + x-kubernetes-validations: + - message: servers in a TACACS group must have tacacs config + rule: self.type != 'TACACS' || self.servers.all(s, has(s.tacacs)) + - message: servers in a RADIUS group must have radius config + rule: self.type != 'RADIUS' || self.servers.all(s, has(s.radius)) + maxItems: 8 + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + required: + - deviceRef + type: object + x-kubernetes-validations: + - message: at least one of serverGroups, authentication, authorization, + or accounting must be set + rule: has(self.serverGroups) || has(self.authentication) || has(self.authorization) + || has(self.accounting) + status: + description: |- + Status of the resource. This is set and updated automatically. + Read-only. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#spec-and-status + properties: + conditions: + description: The conditions are a list of status objects that describe + the state of the AAA. + 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 + required: + - spec + type: object + served: true + storage: true + subresources: + status: {} +{{- end }} diff --git a/charts/network-operator/templates/crd/aaaconfigs.nx.cisco.networking.metal.ironcore.dev.yaml b/charts/network-operator/templates/crd/aaaconfigs.nx.cisco.networking.metal.ironcore.dev.yaml new file mode 100644 index 000000000..3d8276f0f --- /dev/null +++ b/charts/network-operator/templates/crd/aaaconfigs.nx.cisco.networking.metal.ironcore.dev.yaml @@ -0,0 +1,144 @@ +{{- if .Values.crd.enable }} +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + {{- if .Values.crd.keep }} + "helm.sh/resource-policy": keep + {{- end }} + controller-gen.kubebuilder.io/version: v0.20.1 + name: aaaconfigs.nx.cisco.networking.metal.ironcore.dev +spec: + group: nx.cisco.networking.metal.ironcore.dev + names: + kind: AAAConfig + listKind: AAAConfigList + plural: aaaconfigs + shortNames: + - nxaaa + singular: aaaconfig + scope: Namespaced + versions: + - name: v1alpha1 + schema: + openAPIV3Schema: + description: AAAConfig is the Schema for the aaaconfigs 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: |- + Specification of the desired state of the resource. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#spec-and-status + properties: + configCommandsAuthorization: + description: ConfigCommandsAuthorization defines config-commands authorization + methods. + properties: + methods: + description: Methods is the ordered list of methods. + items: + description: AAAMethod represents an AAA method. + properties: + groupName: + description: GroupName is the name of the server group when + Type is Group. + maxLength: 63 + type: string + type: + description: Type is the type of AAA method. + enum: + - Group + - Local + - None + type: string + required: + - type + type: object + x-kubernetes-validations: + - message: groupName is required when type is Group + rule: self.type != 'Group' || self.groupName != "" + maxItems: 4 + minItems: 1 + type: array + x-kubernetes-list-type: atomic + required: + - methods + type: object + consoleAuthentication: + description: ConsoleAuthentication defines console-specific authentication + methods. + properties: + methods: + description: Methods is the ordered list of methods. + items: + description: AAAMethod represents an AAA method. + properties: + groupName: + description: GroupName is the name of the server group when + Type is Group. + maxLength: 63 + type: string + type: + description: Type is the type of AAA method. + enum: + - Group + - Local + - None + type: string + required: + - type + type: object + x-kubernetes-validations: + - message: groupName is required when type is Group + rule: self.type != 'Group' || self.groupName != "" + maxItems: 4 + minItems: 1 + type: array + x-kubernetes-list-type: atomic + required: + - methods + type: object + keyEncryption: + default: Type7 + description: KeyEncryption specifies the default encryption type for + TACACS+ keys. + enum: + - Type6 + - Type7 + - Clear + type: string + loginErrorEnable: + description: LoginErrorEnable enables login error messages. + type: boolean + radiusKeyEncryption: + default: Type7 + description: RADIUSKeyEncryption specifies the default encryption + type for RADIUS server keys. + enum: + - Type6 + - Type7 + - Clear + type: string + type: object + required: + - spec + type: object + served: true + storage: true +{{- end }} diff --git a/charts/network-operator/templates/network-policy/allow-metrics-traffic.yaml b/charts/network-operator/templates/network-policy/allow-metrics-traffic.yaml deleted file mode 100644 index e86833529..000000000 --- a/charts/network-operator/templates/network-policy/allow-metrics-traffic.yaml +++ /dev/null @@ -1,32 +0,0 @@ -{{- if .Values.networkPolicy.enable }} -# This NetworkPolicy allows ingress traffic -# with Pods running on namespaces labeled with 'metrics: enabled'. Only Pods on those -# namespaces are able to gather data from the metrics endpoint. -apiVersion: networking.k8s.io/v1 -kind: NetworkPolicy -metadata: - labels: - app.kubernetes.io/instance: {{ .Release.Name }} - app.kubernetes.io/managed-by: {{ .Release.Service }} - app.kubernetes.io/name: {{ include "network-operator.name" . }} - helm.sh/chart: {{ .Chart.Name }}-{{ .Chart.Version | replace "+" "_" }} - control-plane: controller-manager - name: {{ include "network-operator.resourceName" (dict "suffix" "allow-metrics-traffic" "context" $) }} - namespace: {{ .Release.Namespace }} -spec: - podSelector: - matchLabels: - control-plane: controller-manager - app.kubernetes.io/name: {{ include "network-operator.name" . }} - policyTypes: - - Ingress - ingress: - # This allows ingress traffic from any namespace with the label metrics: enabled - - from: - - namespaceSelector: - matchLabels: - metrics: enabled # Only from namespaces with this label - ports: - - port: 8443 - protocol: TCP -{{- end -}} diff --git a/charts/network-operator/templates/network-policy/allow-webhook-traffic.yaml b/charts/network-operator/templates/network-policy/allow-webhook-traffic.yaml deleted file mode 100644 index cd0172a66..000000000 --- a/charts/network-operator/templates/network-policy/allow-webhook-traffic.yaml +++ /dev/null @@ -1,32 +0,0 @@ -{{- if .Values.networkPolicy.enable }} -# This NetworkPolicy allows ingress traffic to your webhook server running -# as part of the controller-manager from specific namespaces and pods. CR(s) which uses webhooks -# will only work when applied in namespaces labeled with 'webhook: enabled' -apiVersion: networking.k8s.io/v1 -kind: NetworkPolicy -metadata: - labels: - app.kubernetes.io/instance: {{ .Release.Name }} - app.kubernetes.io/managed-by: {{ .Release.Service }} - app.kubernetes.io/name: {{ include "network-operator.name" . }} - helm.sh/chart: {{ .Chart.Name }}-{{ .Chart.Version | replace "+" "_" }} - control-plane: controller-manager - name: {{ include "network-operator.resourceName" (dict "suffix" "allow-webhook-traffic" "context" $) }} - namespace: {{ .Release.Namespace }} -spec: - podSelector: - matchLabels: - control-plane: controller-manager - app.kubernetes.io/name: {{ include "network-operator.name" . }} - policyTypes: - - Ingress - ingress: - # This allows ingress traffic from any namespace with the label webhook: enabled - - from: - - namespaceSelector: - matchLabels: - webhook: enabled # Only from namespaces with this label - ports: - - port: 443 - protocol: TCP -{{- end -}} diff --git a/charts/network-operator/templates/rbac/aaa-admin-role.yaml b/charts/network-operator/templates/rbac/aaa-admin-role.yaml new file mode 100644 index 000000000..73c50a435 --- /dev/null +++ b/charts/network-operator/templates/rbac/aaa-admin-role.yaml @@ -0,0 +1,31 @@ +{{- if .Values.rbac.helpers.enable }} +apiVersion: rbac.authorization.k8s.io/v1 +{{- if .Values.rbac.namespaced }} +kind: Role +{{- else }} +kind: ClusterRole +{{- end }} +metadata: +{{- if .Values.rbac.namespaced }} + namespace: {{ .Release.Namespace }} +{{- end }} + labels: + app.kubernetes.io/managed-by: {{ .Release.Service }} + app.kubernetes.io/name: {{ include "network-operator.name" . }} + helm.sh/chart: {{ .Chart.Name }}-{{ .Chart.Version | replace "+" "_" }} + app.kubernetes.io/instance: {{ .Release.Name }} + name: {{ include "network-operator.resourceName" (dict "suffix" "aaa-admin-role" "context" $) }} +rules: +- apiGroups: + - networking.metal.ironcore.dev + resources: + - aaa + verbs: + - '*' +- apiGroups: + - networking.metal.ironcore.dev + resources: + - aaa/status + verbs: + - get +{{- end }} diff --git a/charts/network-operator/templates/rbac/aaa-editor-role.yaml b/charts/network-operator/templates/rbac/aaa-editor-role.yaml new file mode 100644 index 000000000..c40e8be40 --- /dev/null +++ b/charts/network-operator/templates/rbac/aaa-editor-role.yaml @@ -0,0 +1,37 @@ +{{- if .Values.rbac.helpers.enable }} +apiVersion: rbac.authorization.k8s.io/v1 +{{- if .Values.rbac.namespaced }} +kind: Role +{{- else }} +kind: ClusterRole +{{- end }} +metadata: +{{- if .Values.rbac.namespaced }} + namespace: {{ .Release.Namespace }} +{{- end }} + labels: + app.kubernetes.io/managed-by: {{ .Release.Service }} + app.kubernetes.io/name: {{ include "network-operator.name" . }} + helm.sh/chart: {{ .Chart.Name }}-{{ .Chart.Version | replace "+" "_" }} + app.kubernetes.io/instance: {{ .Release.Name }} + name: {{ include "network-operator.resourceName" (dict "suffix" "aaa-editor-role" "context" $) }} +rules: +- apiGroups: + - networking.metal.ironcore.dev + resources: + - aaa + verbs: + - create + - delete + - get + - list + - patch + - update + - watch +- apiGroups: + - networking.metal.ironcore.dev + resources: + - aaa/status + verbs: + - get +{{- end }} diff --git a/charts/network-operator/templates/rbac/aaa-viewer-role.yaml b/charts/network-operator/templates/rbac/aaa-viewer-role.yaml new file mode 100644 index 000000000..072a4bb7b --- /dev/null +++ b/charts/network-operator/templates/rbac/aaa-viewer-role.yaml @@ -0,0 +1,33 @@ +{{- if .Values.rbac.helpers.enable }} +apiVersion: rbac.authorization.k8s.io/v1 +{{- if .Values.rbac.namespaced }} +kind: Role +{{- else }} +kind: ClusterRole +{{- end }} +metadata: +{{- if .Values.rbac.namespaced }} + namespace: {{ .Release.Namespace }} +{{- end }} + labels: + app.kubernetes.io/managed-by: {{ .Release.Service }} + app.kubernetes.io/name: {{ include "network-operator.name" . }} + helm.sh/chart: {{ .Chart.Name }}-{{ .Chart.Version | replace "+" "_" }} + app.kubernetes.io/instance: {{ .Release.Name }} + name: {{ include "network-operator.resourceName" (dict "suffix" "aaa-viewer-role" "context" $) }} +rules: +- apiGroups: + - networking.metal.ironcore.dev + resources: + - aaa + verbs: + - get + - list + - watch +- apiGroups: + - networking.metal.ironcore.dev + resources: + - aaa/status + verbs: + - get +{{- end }} diff --git a/charts/network-operator/templates/rbac/nx.cisco-aaaconfig-admin-role.yaml b/charts/network-operator/templates/rbac/nx.cisco-aaaconfig-admin-role.yaml new file mode 100644 index 000000000..b726e0b49 --- /dev/null +++ b/charts/network-operator/templates/rbac/nx.cisco-aaaconfig-admin-role.yaml @@ -0,0 +1,31 @@ +{{- if .Values.rbac.helpers.enable }} +apiVersion: rbac.authorization.k8s.io/v1 +{{- if .Values.rbac.namespaced }} +kind: Role +{{- else }} +kind: ClusterRole +{{- end }} +metadata: +{{- if .Values.rbac.namespaced }} + namespace: {{ .Release.Namespace }} +{{- end }} + labels: + app.kubernetes.io/managed-by: {{ .Release.Service }} + app.kubernetes.io/name: {{ include "network-operator.name" . }} + helm.sh/chart: {{ .Chart.Name }}-{{ .Chart.Version | replace "+" "_" }} + app.kubernetes.io/instance: {{ .Release.Name }} + name: {{ include "network-operator.resourceName" (dict "suffix" "nx" "context" $) }}.cisco-aaaconfig-admin-role +rules: +- apiGroups: + - nx.cisco.networking.metal.ironcore.dev + resources: + - aaaconfigs + verbs: + - '*' +- apiGroups: + - nx.cisco.networking.metal.ironcore.dev + resources: + - aaaconfigs/status + verbs: + - get +{{- end }} diff --git a/charts/network-operator/templates/rbac/nx.cisco-aaaconfig-editor-role.yaml b/charts/network-operator/templates/rbac/nx.cisco-aaaconfig-editor-role.yaml new file mode 100644 index 000000000..d4d01f726 --- /dev/null +++ b/charts/network-operator/templates/rbac/nx.cisco-aaaconfig-editor-role.yaml @@ -0,0 +1,37 @@ +{{- if .Values.rbac.helpers.enable }} +apiVersion: rbac.authorization.k8s.io/v1 +{{- if .Values.rbac.namespaced }} +kind: Role +{{- else }} +kind: ClusterRole +{{- end }} +metadata: +{{- if .Values.rbac.namespaced }} + namespace: {{ .Release.Namespace }} +{{- end }} + labels: + app.kubernetes.io/managed-by: {{ .Release.Service }} + app.kubernetes.io/name: {{ include "network-operator.name" . }} + helm.sh/chart: {{ .Chart.Name }}-{{ .Chart.Version | replace "+" "_" }} + app.kubernetes.io/instance: {{ .Release.Name }} + name: {{ include "network-operator.resourceName" (dict "suffix" "nx" "context" $) }}.cisco-aaaconfig-editor-role +rules: +- apiGroups: + - nx.cisco.networking.metal.ironcore.dev + resources: + - aaaconfigs + verbs: + - create + - delete + - get + - list + - patch + - update + - watch +- apiGroups: + - nx.cisco.networking.metal.ironcore.dev + resources: + - aaaconfigs/status + verbs: + - get +{{- end }} diff --git a/charts/network-operator/templates/rbac/nx.cisco-aaaconfig-viewer-role.yaml b/charts/network-operator/templates/rbac/nx.cisco-aaaconfig-viewer-role.yaml new file mode 100644 index 000000000..7990ee87c --- /dev/null +++ b/charts/network-operator/templates/rbac/nx.cisco-aaaconfig-viewer-role.yaml @@ -0,0 +1,33 @@ +{{- if .Values.rbac.helpers.enable }} +apiVersion: rbac.authorization.k8s.io/v1 +{{- if .Values.rbac.namespaced }} +kind: Role +{{- else }} +kind: ClusterRole +{{- end }} +metadata: +{{- if .Values.rbac.namespaced }} + namespace: {{ .Release.Namespace }} +{{- end }} + labels: + app.kubernetes.io/managed-by: {{ .Release.Service }} + app.kubernetes.io/name: {{ include "network-operator.name" . }} + helm.sh/chart: {{ .Chart.Name }}-{{ .Chart.Version | replace "+" "_" }} + app.kubernetes.io/instance: {{ .Release.Name }} + name: {{ include "network-operator.resourceName" (dict "suffix" "nx" "context" $) }}.cisco-aaaconfig-viewer-role +rules: +- apiGroups: + - nx.cisco.networking.metal.ironcore.dev + resources: + - aaaconfigs + verbs: + - get + - list + - watch +- apiGroups: + - nx.cisco.networking.metal.ironcore.dev + resources: + - aaaconfigs/status + verbs: + - get +{{- end }} diff --git a/charts/network-operator/values.yaml b/charts/network-operator/values.yaml index 180e5c9f8..00f341cab 100644 --- a/charts/network-operator/values.yaml +++ b/charts/network-operator/values.yaml @@ -17,7 +17,9 @@ manager: image: repository: ghcr.io/ironcore-dev/network-operator - tag: latest + ## Image tag (defaults to Chart.appVersion if not set) + ## + # tag: "" pullPolicy: IfNotPresent ## Arguments @@ -174,7 +176,3 @@ webhook: prometheus: enable: false -## NetworkPolicy to restrict access to the controller manager. -## -networkPolicy: - enable: false From bf3a5ea4c7ad1b672b89ea7340ed0ac7ee091078 Mon Sep 17 00:00:00 2001 From: i521907 Date: Thu, 30 Apr 2026 09:20:09 -0400 Subject: [PATCH 28/32] Reduce EnsureAAA cyclomatic complexity below linter threshold Extract TACACS/RADIUS group building and each auth section into dedicated helpers so EnsureAAA stays within the gocyclo limit of 30. Signed-off-by: i521907 --- internal/provider/cisco/nxos/aaa.go | 141 +++++++++++++++++++++++ internal/provider/cisco/nxos/provider.go | 137 ++-------------------- 2 files changed, 149 insertions(+), 129 deletions(-) diff --git a/internal/provider/cisco/nxos/aaa.go b/internal/provider/cisco/nxos/aaa.go index d62187e97..fc77f37d4 100644 --- a/internal/provider/cisco/nxos/aaa.go +++ b/internal/provider/cisco/nxos/aaa.go @@ -6,6 +6,7 @@ package nxos import ( nxv1alpha1 "github.com/ironcore-dev/network-operator/api/cisco/nx/v1alpha1" "github.com/ironcore-dev/network-operator/api/core/v1alpha1" + "github.com/ironcore-dev/network-operator/internal/provider" "github.com/ironcore-dev/network-operator/internal/transport/gnmiext" ) @@ -321,3 +322,143 @@ func MapFallback(methods []v1alpha1.AAAMethod) string { } return AAAValueNo } + +func buildTACACSSGroupConf(group v1alpha1.AAAServerGroup, req *provider.EnsureAAARequest, cfg nxv1alpha1.AAAConfig, desired map[string]struct{}) []gnmiext.DataElement { + var conf []gnmiext.DataElement + conf = append(conf, &Feature{Name: "tacacsplus", AdminSt: AdminStEnabled}) + for _, server := range group.Servers { + desired[server.Address] = struct{}{} + srv := &TacacsPlusProvider{ + Name: server.Address, + Port: server.TACACS.Port, + KeyEnc: MapKeyEncryption(cfg.Spec.KeyEncryption), + Timeout: 5, + } + if key, ok := req.TACACSServerKeys[server.Address]; ok { + srv.Key = key + } + if server.Timeout != nil { + srv.Timeout = int32(server.Timeout.Seconds()) + } + conf = append(conf, srv) + } + grp := &TacacsPlusProviderGroup{ + Name: group.Name, + Vrf: group.VrfName, + SrcIf: group.SourceInterfaceName, + } + for _, server := range group.Servers { + grp.ProviderRefItems.ProviderRefList.Set(&TacacsPlusProviderRef{Name: server.Address}) + } + return append(conf, grp) +} + +func buildRADIUSGroupConf(group v1alpha1.AAAServerGroup, req *provider.EnsureAAARequest, cfg nxv1alpha1.AAAConfig, desired map[string]struct{}) []gnmiext.DataElement { + var conf []gnmiext.DataElement + for _, server := range group.Servers { + desired[server.Address] = struct{}{} + srv := &RadiusProvider{ + Name: server.Address, + AuthPort: server.RADIUS.AuthenticationPort, + AcctPort: server.RADIUS.AccountingPort, + KeyEnc: MapRADIUSKeyEncryption(cfg.Spec.RADIUSKeyEncryption), + Timeout: 5, + } + if key, ok := req.RADIUSServerKeys[server.Address]; ok { + srv.Key = key + } + if server.Timeout != nil { + srv.Timeout = int32(server.Timeout.Seconds()) + } + conf = append(conf, srv) + } + grp := &RadiusProviderGroup{ + Name: group.Name, + Vrf: group.VrfName, + SrcIf: group.SourceInterfaceName, + } + for _, server := range group.Servers { + grp.ProviderRefItems.ProviderRefList.Set(&RadiusProviderRef{Name: server.Address}) + } + return append(conf, grp) +} + +func buildDefaultAuth(req *provider.EnsureAAARequest, cfg nxv1alpha1.AAAConfig) *AAADefaultAuth { + if req.AAA.Spec.Authentication == nil || len(req.AAA.Spec.Authentication.Methods) == 0 { + return &AAADefaultAuth{Realm: AAARealmLocal, Local: AAAValueYes, Fallback: AAAValueYes} + } + methods := req.AAA.Spec.Authentication.Methods + authen := &AAADefaultAuth{ + ErrEn: cfg.Spec.LoginErrorEnable, + Fallback: MapFallbackFromMethodList(methods), + Local: MapLocalFromMethodList(methods), + } + if methods[0].Type == v1alpha1.AAAMethodTypeGroup { + authen.Realm = MapRealmFromGroup(methods[0].GroupName, req.AAA.Spec.ServerGroups) + authen.ProviderGroup = methods[0].GroupName + } else { + authen.Realm = MapRealmFromMethodType(methods[0].Type) + } + return authen +} + +func buildConsoleAuth(req *provider.EnsureAAARequest, cfg nxv1alpha1.AAAConfig) *AAAConsoleAuth { + if cfg.Spec.ConsoleAuthentication == nil || len(cfg.Spec.ConsoleAuthentication.Methods) == 0 { + return &AAAConsoleAuth{Realm: AAARealmLocal, Local: AAAValueYes, Fallback: AAAValueYes} + } + methods := cfg.Spec.ConsoleAuthentication.Methods + auth := &AAAConsoleAuth{ + ErrEn: cfg.Spec.LoginErrorEnable, + Fallback: MapFallback(methods), + Local: MapLocal(methods), + } + if methods[0].Type == v1alpha1.AAAMethodTypeGroup { + auth.Realm = MapRealmFromGroup(methods[0].GroupName, req.AAA.Spec.ServerGroups) + auth.ProviderGroup = methods[0].GroupName + } else { + auth.Realm = MapRealmFromMethodType(methods[0].Type) + } + return auth +} + +func buildAuthorization(req *provider.EnsureAAARequest, cfg nxv1alpha1.AAAConfig) *AAADefaultAuthor { + author := &AAADefaultAuthor{CmdType: "config", LocalRbac: true} + if req.AAA.Spec.Authorization != nil && len(req.AAA.Spec.Authorization.Methods) > 0 { + methods := req.AAA.Spec.Authorization.Methods + author = &AAADefaultAuthor{ + CmdType: "config", + LocalRbac: MapLocalFromMethodList(methods) == AAAValueYes, + } + if methods[0].Type == v1alpha1.AAAMethodTypeGroup { + author.ProviderGroup = methods[0].GroupName + } + } + if cfg.Spec.ConfigCommandsAuthorization != nil && len(cfg.Spec.ConfigCommandsAuthorization.Methods) > 0 { + methods := cfg.Spec.ConfigCommandsAuthorization.Methods + author = &AAADefaultAuthor{ + CmdType: "config", + LocalRbac: MapLocal(methods) == AAAValueYes, + } + if methods[0].Type == v1alpha1.AAAMethodTypeGroup { + author.ProviderGroup = methods[0].GroupName + } + } + return author +} + +func buildAccounting(req *provider.EnsureAAARequest) *AAADefaultAcc { + if req.AAA.Spec.Accounting == nil || len(req.AAA.Spec.Accounting.Methods) == 0 { + return &AAADefaultAcc{Realm: AAARealmLocal, LocalRbac: true} + } + methods := req.AAA.Spec.Accounting.Methods + acct := &AAADefaultAcc{ + LocalRbac: MapLocalFromMethodList(methods) == AAAValueYes, + } + if methods[0].Type == v1alpha1.AAAMethodTypeGroup { + acct.Realm = MapRealmFromGroup(methods[0].GroupName, req.AAA.Spec.ServerGroups) + acct.ProviderGroup = methods[0].GroupName + } else { + acct.Realm = MapRealmFromMethodType(methods[0].Type) + } + return acct +} diff --git a/internal/provider/cisco/nxos/provider.go b/internal/provider/cisco/nxos/provider.go index 16fc1d0e3..8c112854b 100644 --- a/internal/provider/cisco/nxos/provider.go +++ b/internal/provider/cisco/nxos/provider.go @@ -3282,146 +3282,25 @@ func (p *Provider) EnsureAAA(ctx context.Context, req *provider.EnsureAAARequest desiredTACACS := map[string]struct{}{} desiredRADIUS := map[string]struct{}{} - var conf []gnmiext.DataElement for _, group := range req.AAA.Spec.ServerGroups { switch group.Type { case v1alpha1.AAAServerGroupTypeTACACS: - conf = append(conf, &Feature{Name: "tacacsplus", AdminSt: AdminStEnabled}) - - for _, server := range group.Servers { - desiredTACACS[server.Address] = struct{}{} - srv := &TacacsPlusProvider{ - Name: server.Address, - Port: server.TACACS.Port, - KeyEnc: MapKeyEncryption(cfg.Spec.KeyEncryption), - Timeout: 5, - } - if key, ok := req.TACACSServerKeys[server.Address]; ok { - srv.Key = key - } - if server.Timeout != nil { - srv.Timeout = int32(server.Timeout.Seconds()) - } - conf = append(conf, srv) - } - - grp := &TacacsPlusProviderGroup{ - Name: group.Name, - Vrf: group.VrfName, - SrcIf: group.SourceInterfaceName, - } - for _, server := range group.Servers { - grp.ProviderRefItems.ProviderRefList.Set(&TacacsPlusProviderRef{Name: server.Address}) - } - conf = append(conf, grp) - + conf = append(conf, buildTACACSSGroupConf(group, req, cfg, desiredTACACS)...) case v1alpha1.AAAServerGroupTypeRADIUS: - for _, server := range group.Servers { - desiredRADIUS[server.Address] = struct{}{} - srv := &RadiusProvider{ - Name: server.Address, - AuthPort: server.RADIUS.AuthenticationPort, - AcctPort: server.RADIUS.AccountingPort, - KeyEnc: MapRADIUSKeyEncryption(cfg.Spec.RADIUSKeyEncryption), - Timeout: 5, - } - if key, ok := req.RADIUSServerKeys[server.Address]; ok { - srv.Key = key - } - if server.Timeout != nil { - srv.Timeout = int32(server.Timeout.Seconds()) - } - conf = append(conf, srv) - } - - grp := &RadiusProviderGroup{ - Name: group.Name, - Vrf: group.VrfName, - SrcIf: group.SourceInterfaceName, - } - for _, server := range group.Servers { - grp.ProviderRefItems.ProviderRefList.Set(&RadiusProviderRef{Name: server.Address}) - } - conf = append(conf, grp) + conf = append(conf, buildRADIUSGroupConf(group, req, cfg, desiredRADIUS)...) } } // Always take full ownership of the auth config. If not specified in the spec, // reset to the device default (local auth). - authen := &AAADefaultAuth{Realm: AAARealmLocal, Local: AAAValueYes, Fallback: AAAValueYes} - if req.AAA.Spec.Authentication != nil && len(req.AAA.Spec.Authentication.Methods) > 0 { - methods := req.AAA.Spec.Authentication.Methods - authen = &AAADefaultAuth{ - ErrEn: cfg.Spec.LoginErrorEnable, - Fallback: MapFallbackFromMethodList(methods), - Local: MapLocalFromMethodList(methods), - } - if methods[0].Type == v1alpha1.AAAMethodTypeGroup { - authen.Realm = MapRealmFromGroup(methods[0].GroupName, req.AAA.Spec.ServerGroups) - authen.ProviderGroup = methods[0].GroupName - } else { - authen.Realm = MapRealmFromMethodType(methods[0].Type) - } - } - conf = append(conf, authen) - - consoleAuth := &AAAConsoleAuth{Realm: AAARealmLocal, Local: AAAValueYes, Fallback: AAAValueYes} - if cfg.Spec.ConsoleAuthentication != nil && len(cfg.Spec.ConsoleAuthentication.Methods) > 0 { - methods := cfg.Spec.ConsoleAuthentication.Methods - consoleAuth = &AAAConsoleAuth{ - ErrEn: cfg.Spec.LoginErrorEnable, - Fallback: MapFallback(methods), - Local: MapLocal(methods), - } - if methods[0].Type == v1alpha1.AAAMethodTypeGroup { - consoleAuth.Realm = MapRealmFromGroup(methods[0].GroupName, req.AAA.Spec.ServerGroups) - consoleAuth.ProviderGroup = methods[0].GroupName - } else { - consoleAuth.Realm = MapRealmFromMethodType(methods[0].Type) - } - } - conf = append(conf, consoleAuth) - - // ConfigCommandsAuthorization (Cisco-specific) takes priority over the core Authorization field. - author := &AAADefaultAuthor{CmdType: "config", LocalRbac: true} - if req.AAA.Spec.Authorization != nil && len(req.AAA.Spec.Authorization.Methods) > 0 { - methods := req.AAA.Spec.Authorization.Methods - author = &AAADefaultAuthor{ - CmdType: "config", - LocalRbac: MapLocalFromMethodList(methods) == AAAValueYes, - } - if methods[0].Type == v1alpha1.AAAMethodTypeGroup { - author.ProviderGroup = methods[0].GroupName - } - } - if cfg.Spec.ConfigCommandsAuthorization != nil && len(cfg.Spec.ConfigCommandsAuthorization.Methods) > 0 { - methods := cfg.Spec.ConfigCommandsAuthorization.Methods - author = &AAADefaultAuthor{ - CmdType: "config", - LocalRbac: MapLocal(methods) == AAAValueYes, - } - if methods[0].Type == v1alpha1.AAAMethodTypeGroup { - author.ProviderGroup = methods[0].GroupName - } - } - conf = append(conf, author) - - acct := &AAADefaultAcc{Realm: AAARealmLocal, LocalRbac: true} - if req.AAA.Spec.Accounting != nil && len(req.AAA.Spec.Accounting.Methods) > 0 { - methods := req.AAA.Spec.Accounting.Methods - acct = &AAADefaultAcc{ - LocalRbac: MapLocalFromMethodList(methods) == AAAValueYes, - } - if methods[0].Type == v1alpha1.AAAMethodTypeGroup { - acct.Realm = MapRealmFromGroup(methods[0].GroupName, req.AAA.Spec.ServerGroups) - acct.ProviderGroup = methods[0].GroupName - } else { - acct.Realm = MapRealmFromMethodType(methods[0].Type) - } - } - conf = append(conf, acct) + conf = append(conf, + buildDefaultAuth(req, cfg), + buildConsoleAuth(req, cfg), + buildAuthorization(req, cfg), + buildAccounting(req), + ) // Fetch current server lists before applying desired state to compute stale entries. currentTACACS := new(TacacsPlusProviderItems) From 491e1b34c49434aa3e54c4b8a9d2559bfcbad5ed Mon Sep 17 00:00:00 2001 From: i521907 Date: Mon, 4 May 2026 09:57:11 -0400 Subject: [PATCH 29/32] Revert chart changes that should not have been modified Restore Chart.yaml version to 0.0.0+dev, tag: latest in values.yaml, and the network-policy template files deleted by make charts. Signed-off-by: i521907 --- charts/network-operator/Chart.yaml | 13 ++------ .../network-policy/allow-metrics-traffic.yaml | 32 +++++++++++++++++++ .../network-policy/allow-webhook-traffic.yaml | 32 +++++++++++++++++++ charts/network-operator/values.yaml | 9 ++++-- 4 files changed, 72 insertions(+), 14 deletions(-) create mode 100644 charts/network-operator/templates/network-policy/allow-metrics-traffic.yaml create mode 100644 charts/network-operator/templates/network-policy/allow-webhook-traffic.yaml diff --git a/charts/network-operator/Chart.yaml b/charts/network-operator/Chart.yaml index 58d14f8be..6d48691ce 100644 --- a/charts/network-operator/Chart.yaml +++ b/charts/network-operator/Chart.yaml @@ -1,14 +1,5 @@ apiVersion: v2 name: network-operator -description: A Helm chart to distribute network-operator +description: A Helm chart to distribute the project network-operator type: application - -version: 0.1.0 -appVersion: "0.1.0" - -keywords: - - kubernetes - - operator - -annotations: - kubebuilder.io/generated-by: kubebuilder +version: 0.0.0+dev diff --git a/charts/network-operator/templates/network-policy/allow-metrics-traffic.yaml b/charts/network-operator/templates/network-policy/allow-metrics-traffic.yaml new file mode 100644 index 000000000..e86833529 --- /dev/null +++ b/charts/network-operator/templates/network-policy/allow-metrics-traffic.yaml @@ -0,0 +1,32 @@ +{{- if .Values.networkPolicy.enable }} +# This NetworkPolicy allows ingress traffic +# with Pods running on namespaces labeled with 'metrics: enabled'. Only Pods on those +# namespaces are able to gather data from the metrics endpoint. +apiVersion: networking.k8s.io/v1 +kind: NetworkPolicy +metadata: + labels: + app.kubernetes.io/instance: {{ .Release.Name }} + app.kubernetes.io/managed-by: {{ .Release.Service }} + app.kubernetes.io/name: {{ include "network-operator.name" . }} + helm.sh/chart: {{ .Chart.Name }}-{{ .Chart.Version | replace "+" "_" }} + control-plane: controller-manager + name: {{ include "network-operator.resourceName" (dict "suffix" "allow-metrics-traffic" "context" $) }} + namespace: {{ .Release.Namespace }} +spec: + podSelector: + matchLabels: + control-plane: controller-manager + app.kubernetes.io/name: {{ include "network-operator.name" . }} + policyTypes: + - Ingress + ingress: + # This allows ingress traffic from any namespace with the label metrics: enabled + - from: + - namespaceSelector: + matchLabels: + metrics: enabled # Only from namespaces with this label + ports: + - port: 8443 + protocol: TCP +{{- end -}} diff --git a/charts/network-operator/templates/network-policy/allow-webhook-traffic.yaml b/charts/network-operator/templates/network-policy/allow-webhook-traffic.yaml new file mode 100644 index 000000000..cd0172a66 --- /dev/null +++ b/charts/network-operator/templates/network-policy/allow-webhook-traffic.yaml @@ -0,0 +1,32 @@ +{{- if .Values.networkPolicy.enable }} +# This NetworkPolicy allows ingress traffic to your webhook server running +# as part of the controller-manager from specific namespaces and pods. CR(s) which uses webhooks +# will only work when applied in namespaces labeled with 'webhook: enabled' +apiVersion: networking.k8s.io/v1 +kind: NetworkPolicy +metadata: + labels: + app.kubernetes.io/instance: {{ .Release.Name }} + app.kubernetes.io/managed-by: {{ .Release.Service }} + app.kubernetes.io/name: {{ include "network-operator.name" . }} + helm.sh/chart: {{ .Chart.Name }}-{{ .Chart.Version | replace "+" "_" }} + control-plane: controller-manager + name: {{ include "network-operator.resourceName" (dict "suffix" "allow-webhook-traffic" "context" $) }} + namespace: {{ .Release.Namespace }} +spec: + podSelector: + matchLabels: + control-plane: controller-manager + app.kubernetes.io/name: {{ include "network-operator.name" . }} + policyTypes: + - Ingress + ingress: + # This allows ingress traffic from any namespace with the label webhook: enabled + - from: + - namespaceSelector: + matchLabels: + webhook: enabled # Only from namespaces with this label + ports: + - port: 443 + protocol: TCP +{{- end -}} diff --git a/charts/network-operator/values.yaml b/charts/network-operator/values.yaml index 00f341cab..554e04720 100644 --- a/charts/network-operator/values.yaml +++ b/charts/network-operator/values.yaml @@ -17,9 +17,7 @@ manager: image: repository: ghcr.io/ironcore-dev/network-operator - ## Image tag (defaults to Chart.appVersion if not set) - ## - # tag: "" + tag: latest pullPolicy: IfNotPresent ## Arguments @@ -176,3 +174,8 @@ webhook: prometheus: enable: false +## NetworkPolicy to restrict access to the controller manager. +## +networkPolicy: + enable: false + From 3f2c035ce991d70c650d3b2ac9d2b555ca4d6edc Mon Sep 17 00:00:00 2001 From: i521907 Date: Thu, 21 May 2026 09:00:36 -0400 Subject: [PATCH 30/32] Regenerate codegen after rebase onto main (controller-gen v0.21.0) Signed-off-by: i521907 --- .../crd/aaa.networking.metal.ironcore.dev.yaml | 2 +- ...nfigs.nx.cisco.networking.metal.ironcore.dev.yaml | 2 +- .../network-operator/templates/manager/manager.yaml | 12 ++++++------ .../crd/bases/networking.metal.ironcore.dev_aaa.yaml | 2 +- ...sco.networking.metal.ironcore.dev_aaaconfigs.yaml | 2 +- 5 files changed, 10 insertions(+), 10 deletions(-) diff --git a/charts/network-operator/templates/crd/aaa.networking.metal.ironcore.dev.yaml b/charts/network-operator/templates/crd/aaa.networking.metal.ironcore.dev.yaml index 77b71090f..b53675c75 100644 --- a/charts/network-operator/templates/crd/aaa.networking.metal.ironcore.dev.yaml +++ b/charts/network-operator/templates/crd/aaa.networking.metal.ironcore.dev.yaml @@ -6,7 +6,7 @@ metadata: {{- if .Values.crd.keep }} "helm.sh/resource-policy": keep {{- end }} - controller-gen.kubebuilder.io/version: v0.20.1 + controller-gen.kubebuilder.io/version: v0.21.0 name: aaa.networking.metal.ironcore.dev spec: group: networking.metal.ironcore.dev diff --git a/charts/network-operator/templates/crd/aaaconfigs.nx.cisco.networking.metal.ironcore.dev.yaml b/charts/network-operator/templates/crd/aaaconfigs.nx.cisco.networking.metal.ironcore.dev.yaml index 3d8276f0f..349d6a6fd 100644 --- a/charts/network-operator/templates/crd/aaaconfigs.nx.cisco.networking.metal.ironcore.dev.yaml +++ b/charts/network-operator/templates/crd/aaaconfigs.nx.cisco.networking.metal.ironcore.dev.yaml @@ -6,7 +6,7 @@ metadata: {{- if .Values.crd.keep }} "helm.sh/resource-policy": keep {{- end }} - controller-gen.kubebuilder.io/version: v0.20.1 + controller-gen.kubebuilder.io/version: v0.21.0 name: aaaconfigs.nx.cisco.networking.metal.ironcore.dev spec: group: nx.cisco.networking.metal.ironcore.dev diff --git a/charts/network-operator/templates/manager/manager.yaml b/charts/network-operator/templates/manager/manager.yaml index e01e2a22e..931116e18 100644 --- a/charts/network-operator/templates/manager/manager.yaml +++ b/charts/network-operator/templates/manager/manager.yaml @@ -136,9 +136,9 @@ spec: {{- toYaml .Values.manager.extraVolumeMounts | nindent 10 }} {{- end }} {{- if .Values.certManager.enable }} - - mountPath: /tmp/k8s-webhook-server/serving-certs - name: webhook-certs - readOnly: true + - mountPath: /tmp/k8s-webhook-server/serving-certs + name: webhook-certs + readOnly: true {{- end }} securityContext: {{- if .Values.manager.podSecurityContext }} @@ -155,8 +155,8 @@ spec: {{- toYaml .Values.manager.extraVolumes | nindent 8 }} {{- end }} {{- if .Values.certManager.enable }} - - name: webhook-certs - secret: - secretName: webhook-server-cert + - name: webhook-certs + secret: + secretName: webhook-server-cert {{- end }} {{- end }} diff --git a/config/crd/bases/networking.metal.ironcore.dev_aaa.yaml b/config/crd/bases/networking.metal.ironcore.dev_aaa.yaml index 06498e125..1f03feb52 100644 --- a/config/crd/bases/networking.metal.ironcore.dev_aaa.yaml +++ b/config/crd/bases/networking.metal.ironcore.dev_aaa.yaml @@ -3,7 +3,7 @@ apiVersion: apiextensions.k8s.io/v1 kind: CustomResourceDefinition metadata: annotations: - controller-gen.kubebuilder.io/version: v0.20.1 + controller-gen.kubebuilder.io/version: v0.21.0 name: aaa.networking.metal.ironcore.dev spec: group: networking.metal.ironcore.dev diff --git a/config/crd/bases/nx.cisco.networking.metal.ironcore.dev_aaaconfigs.yaml b/config/crd/bases/nx.cisco.networking.metal.ironcore.dev_aaaconfigs.yaml index 8eb615645..4766e8237 100644 --- a/config/crd/bases/nx.cisco.networking.metal.ironcore.dev_aaaconfigs.yaml +++ b/config/crd/bases/nx.cisco.networking.metal.ironcore.dev_aaaconfigs.yaml @@ -3,7 +3,7 @@ apiVersion: apiextensions.k8s.io/v1 kind: CustomResourceDefinition metadata: annotations: - controller-gen.kubebuilder.io/version: v0.20.1 + controller-gen.kubebuilder.io/version: v0.21.0 name: aaaconfigs.nx.cisco.networking.metal.ironcore.dev spec: group: nx.cisco.networking.metal.ironcore.dev From fdad12fa7ecf90927acc9c62e265746c71f9d4b3 Mon Sep 17 00:00:00 2001 From: i521907 Date: Thu, 21 May 2026 09:03:23 -0400 Subject: [PATCH 31/32] Regenerate charts after rebase onto main Signed-off-by: i521907 --- .../network-operator/templates/manager/manager.yaml | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/charts/network-operator/templates/manager/manager.yaml b/charts/network-operator/templates/manager/manager.yaml index 931116e18..e01e2a22e 100644 --- a/charts/network-operator/templates/manager/manager.yaml +++ b/charts/network-operator/templates/manager/manager.yaml @@ -136,9 +136,9 @@ spec: {{- toYaml .Values.manager.extraVolumeMounts | nindent 10 }} {{- end }} {{- if .Values.certManager.enable }} - - mountPath: /tmp/k8s-webhook-server/serving-certs - name: webhook-certs - readOnly: true + - mountPath: /tmp/k8s-webhook-server/serving-certs + name: webhook-certs + readOnly: true {{- end }} securityContext: {{- if .Values.manager.podSecurityContext }} @@ -155,8 +155,8 @@ spec: {{- toYaml .Values.manager.extraVolumes | nindent 8 }} {{- end }} {{- if .Values.certManager.enable }} - - name: webhook-certs - secret: - secretName: webhook-server-cert + - name: webhook-certs + secret: + secretName: webhook-server-cert {{- end }} {{- end }} From 6d26ff024bfa80a693549ad1314d4069d712242b Mon Sep 17 00:00:00 2001 From: i521907 Date: Thu, 21 May 2026 09:25:01 -0400 Subject: [PATCH 32/32] Fix gofumpt formatting in provider.go Signed-off-by: i521907 --- internal/provider/cisco/nxos/provider.go | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/internal/provider/cisco/nxos/provider.go b/internal/provider/cisco/nxos/provider.go index 8c112854b..fd7269f08 100644 --- a/internal/provider/cisco/nxos/provider.go +++ b/internal/provider/cisco/nxos/provider.go @@ -3295,7 +3295,8 @@ func (p *Provider) EnsureAAA(ctx context.Context, req *provider.EnsureAAARequest // Always take full ownership of the auth config. If not specified in the spec, // reset to the device default (local auth). - conf = append(conf, + conf = append( + conf, buildDefaultAuth(req, cfg), buildConsoleAuth(req, cfg), buildAuthorization(req, cfg), @@ -3336,7 +3337,8 @@ func (p *Provider) EnsureAAA(ctx context.Context, req *provider.EnsureAAARequest func (p *Provider) DeleteAAA(ctx context.Context, req *provider.DeleteAAARequest) error { // Step 1: Reset auth realms to local before deleting groups. NX-OS may leave auth // in a broken state if the referenced provider group is removed while still active. - if err := p.Update(ctx, + if err := p.Update( + ctx, &AAADefaultAcc{Realm: AAARealmLocal, LocalRbac: true}, &AAADefaultAuthor{CmdType: "config", LocalRbac: true}, &AAADefaultAuth{Realm: AAARealmLocal, Local: AAAValueYes, Fallback: AAAValueYes}, @@ -3348,7 +3350,8 @@ func (p *Provider) DeleteAAA(ctx context.Context, req *provider.DeleteAAARequest // Step 2: Unconditionally delete all TACACS+ and RADIUS server groups and servers. // Groups must precede servers in the delete to avoid reference violations. // ErrNil is returned when a container is already empty, which is safe to ignore. - if err := p.client.Delete(ctx, + if err := p.client.Delete( + ctx, new(TacacsPlusProviderGroupItems), new(TacacsPlusProviderItems), new(RadiusProviderGroupItems),