diff --git a/README.md b/README.md index 782542e..6d784fc 100644 --- a/README.md +++ b/README.md @@ -19,6 +19,16 @@ Kubernetes operator for managing DNS zones and records, with a pluggable backend - `status.nameservers`: authoritative nameservers (derived from class policy) - `status.conditions`: `Accepted`, `Programmed` +- **`DNSZoneTSIGKey`** (namespaced) + - Models a DNS TSIG key for authenticating transfer/update operations (e.g., PowerDNS `tsigkeys` API). + - `spec.dnsZoneRef`: `LocalObjectReference` to a `DNSZone` in the same namespace + - `spec.keyName`: provider-visible TSIG key name (wire name); immutable + - `spec.algorithm`: TSIG algorithm (`hmac-md5`, `hmac-sha1`, `hmac-sha224`, `hmac-sha256`, `hmac-sha384`, `hmac-sha512`); defaults to `hmac-md5` + - `spec.secretRef`: optional `LocalObjectReference` to an existing Secret containing TSIG material (BYO secret) + - `status.secretName`: secret used for this TSIG key (generated or referenced) + - `status.tsigKeyName`: provider-visible name + - `status.conditions`: `Accepted`, `Programmed` + - **`DNSRecordSet`** (namespaced) - `spec.dnsZoneRef`: `LocalObjectReference` to a `DNSZone` in the same namespace - `spec.recordType`: one of `A, AAAA, CNAME, TXT, MX, SRV, CAA, NS, SOA, PTR, TLSA, HTTPS, SVCB` @@ -107,6 +117,20 @@ spec: content: ["192.0.2.10", "192.0.2.11"] ttl: 300 ``` +4. (Optional) Create a `DNSZoneTSIGKey` (see `config/samples/dns_v1alpha1_dnszonetsigkey.yaml`): +```yaml +apiVersion: dns.networking.miloapis.com/v1alpha1 +kind: DNSZoneTSIGKey +metadata: + name: example-com-xfr + namespace: default +spec: + dnsZoneRef: + name: example-com + keyName: datum-example-com-xfr + # algorithm defaults to hmac-md5; override if desired: + # algorithm: hmac-sha256 +``` ### Quickstart: Replicator (upstream → downstream) 1. Create Secret on the replicator namespace containing the downstream kubeconfig (`data.kubeconfig`): diff --git a/api/v1alpha1/tsigkey_types.go b/api/v1alpha1/tsigkey_types.go new file mode 100644 index 0000000..365249a --- /dev/null +++ b/api/v1alpha1/tsigkey_types.go @@ -0,0 +1,104 @@ +// SPDX-License-Identifier: AGPL-3.0-only + +package v1alpha1 + +import ( + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +// +kubebuilder:validation:Enum=hmac-md5;hmac-sha1;hmac-sha224;hmac-sha256;hmac-sha384;hmac-sha512 +type TSIGAlgorithm string + +const ( + TSIGAlgorithmHMACMD5 TSIGAlgorithm = "hmac-md5" + TSIGAlgorithmHMACSHA1 TSIGAlgorithm = "hmac-sha1" + TSIGAlgorithmHMACSHA224 TSIGAlgorithm = "hmac-sha224" + TSIGAlgorithmHMACSHA256 TSIGAlgorithm = "hmac-sha256" + TSIGAlgorithmHMACSHA384 TSIGAlgorithm = "hmac-sha384" + TSIGAlgorithmHMACSHA512 TSIGAlgorithm = "hmac-sha512" +) + +// DNSZoneTSIGKeySpec defines the desired state of DNSZoneTSIGKey. +type DNSZoneTSIGKeySpec struct { + // DNSZoneRef references the DNSZone (same namespace) this TSIG key is associated with. + // The controller derives provider configuration from the referenced zone. + // +kubebuilder:validation:Required + // +kubebuilder:validation:XValidation:rule="self.name != ''",message="dnsZoneRef.name must be set" + DNSZoneRef corev1.LocalObjectReference `json:"dnsZoneRef"` + + // KeyName is the provider-visible name for this TSIG key (the "wire name"). + // +kubebuilder:validation:Required + // +kubebuilder:validation:MinLength=1 + // +kubebuilder:validation:MaxLength=253 + // Allow DNS name syntax with optional trailing dot. TSIG key names are DNS names (case-insensitive). + // +kubebuilder:validation:Pattern=`^([A-Za-z0-9_](?:[-A-Za-z0-9_]{0,61}[A-Za-z0-9_])?)(?:\.([A-Za-z0-9_](?:[-A-Za-z0-9_]{0,61}[A-Za-z0-9_])?))*\.?$` + // +kubebuilder:validation:XValidation:message="keyName is immutable and cannot be changed after creation",rule="oldSelf == '' || self == oldSelf" + KeyName string `json:"keyName"` + + // Algorithm is the TSIG algorithm used for the key. + // +kubebuilder:default=hmac-md5 + // +optional + Algorithm TSIGAlgorithm `json:"algorithm,omitempty"` + + // SecretRef references an existing Secret containing TSIG material (BYO secret). + // When set, the controller reads and validates the Secret, but must not mutate it. + // When omitted, the controller generates and manages a Secret named deterministically. + // +optional + // +kubebuilder:validation:XValidation:rule="self == null || self.name != ''",message="secretRef.name must be set" + SecretRef *corev1.LocalObjectReference `json:"secretRef,omitempty"` +} + +// DNSZoneTSIGKeyStatus defines the observed state of DNSZoneTSIGKey. +type DNSZoneTSIGKeyStatus struct { + // SecretName is the name of the Secret used for this TSIG key (generated or referenced). + // +optional + SecretName string `json:"secretName,omitempty"` + + // TSIGKeyName is the provider-visible name for this TSIG key. + // +optional + TSIGKeyName string `json:"tsigKeyName,omitempty"` + + // Conditions tracks state such as Accepted and Programmed readiness. + // +listType=map + // +listMapKey=type + // +optional + Conditions []metav1.Condition `json:"conditions,omitempty"` +} + +// +kubebuilder:object:root=true +// +kubebuilder:subresource:status +// +kubebuilder:printcolumn:name="Accepted",type=string,JSONPath=.status.conditions[?(@.type=="Accepted")].status +// +kubebuilder:printcolumn:name="Programmed",type=string,JSONPath=.status.conditions[?(@.type=="Programmed")].status +// +kubebuilder:selectablefield:JSONPath=".spec.dnsZoneRef.name" +// +kubebuilder:resource:path=dnszonetsigkeys,shortName=tsig + +// DNSZoneTSIGKey is the Schema for the DNSZone TSIG keys API. +type DNSZoneTSIGKey struct { + metav1.TypeMeta `json:",inline"` + + // metadata is a standard object metadata + // +optional + metav1.ObjectMeta `json:"metadata,omitempty,omitzero"` + + // spec defines the desired state of DNSZoneTSIGKey + // +required + Spec DNSZoneTSIGKeySpec `json:"spec"` + + // status defines the observed state of DNSZoneTSIGKey + // +optional + Status DNSZoneTSIGKeyStatus `json:"status,omitempty,omitzero"` +} + +// +kubebuilder:object:root=true + +// DNSZoneTSIGKeyList contains a list of DNSZoneTSIGKey. +type DNSZoneTSIGKeyList struct { + metav1.TypeMeta `json:",inline"` + metav1.ListMeta `json:"metadata,omitempty"` + Items []DNSZoneTSIGKey `json:"items"` +} + +func init() { + SchemeBuilder.Register(&DNSZoneTSIGKey{}, &DNSZoneTSIGKeyList{}) +} diff --git a/api/v1alpha1/zz_generated.deepcopy.go b/api/v1alpha1/zz_generated.deepcopy.go index 7f2ae71..4bab3f2 100644 --- a/api/v1alpha1/zz_generated.deepcopy.go +++ b/api/v1alpha1/zz_generated.deepcopy.go @@ -8,6 +8,7 @@ package v1alpha1 import ( "go.datum.net/network-services-operator/api/v1alpha" + corev1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/apis/meta/v1" runtime "k8s.io/apimachinery/pkg/runtime" ) @@ -499,6 +500,108 @@ func (in *DNSZoneStatus) DeepCopy() *DNSZoneStatus { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *DNSZoneTSIGKey) DeepCopyInto(out *DNSZoneTSIGKey) { + *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 DNSZoneTSIGKey. +func (in *DNSZoneTSIGKey) DeepCopy() *DNSZoneTSIGKey { + if in == nil { + return nil + } + out := new(DNSZoneTSIGKey) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *DNSZoneTSIGKey) 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 *DNSZoneTSIGKeyList) DeepCopyInto(out *DNSZoneTSIGKeyList) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ListMeta.DeepCopyInto(&out.ListMeta) + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]DNSZoneTSIGKey, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DNSZoneTSIGKeyList. +func (in *DNSZoneTSIGKeyList) DeepCopy() *DNSZoneTSIGKeyList { + if in == nil { + return nil + } + out := new(DNSZoneTSIGKeyList) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *DNSZoneTSIGKeyList) 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 *DNSZoneTSIGKeySpec) DeepCopyInto(out *DNSZoneTSIGKeySpec) { + *out = *in + out.DNSZoneRef = in.DNSZoneRef + if in.SecretRef != nil { + in, out := &in.SecretRef, &out.SecretRef + *out = new(corev1.LocalObjectReference) + **out = **in + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DNSZoneTSIGKeySpec. +func (in *DNSZoneTSIGKeySpec) DeepCopy() *DNSZoneTSIGKeySpec { + if in == nil { + return nil + } + out := new(DNSZoneTSIGKeySpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *DNSZoneTSIGKeyStatus) DeepCopyInto(out *DNSZoneTSIGKeyStatus) { + *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 DNSZoneTSIGKeyStatus. +func (in *DNSZoneTSIGKeyStatus) DeepCopy() *DNSZoneTSIGKeyStatus { + if in == nil { + return nil + } + out := new(DNSZoneTSIGKeyStatus) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *DiscoveredRecordSet) DeepCopyInto(out *DiscoveredRecordSet) { *out = *in diff --git a/config/crd/bases/dns.networking.miloapis.com_dnszonetsigkeys.yaml b/config/crd/bases/dns.networking.miloapis.com_dnszonetsigkeys.yaml new file mode 100644 index 0000000..4859524 --- /dev/null +++ b/config/crd/bases/dns.networking.miloapis.com_dnszonetsigkeys.yaml @@ -0,0 +1,197 @@ +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.19.0 + name: dnszonetsigkeys.dns.networking.miloapis.com +spec: + group: dns.networking.miloapis.com + names: + kind: DNSZoneTSIGKey + listKind: DNSZoneTSIGKeyList + plural: dnszonetsigkeys + shortNames: + - tsig + singular: dnszonetsigkey + scope: Namespaced + versions: + - additionalPrinterColumns: + - jsonPath: .status.conditions[?(@.type=="Accepted")].status + name: Accepted + type: string + - jsonPath: .status.conditions[?(@.type=="Programmed")].status + name: Programmed + type: string + name: v1alpha1 + schema: + openAPIV3Schema: + description: DNSZoneTSIGKey is the Schema for the DNSZone TSIG keys 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: spec defines the desired state of DNSZoneTSIGKey + properties: + algorithm: + default: hmac-md5 + description: Algorithm is the TSIG algorithm used for the key. + enum: + - hmac-md5 + - hmac-sha1 + - hmac-sha224 + - hmac-sha256 + - hmac-sha384 + - hmac-sha512 + type: string + dnsZoneRef: + description: |- + DNSZoneRef references the DNSZone (same namespace) this TSIG key is associated with. + The controller derives provider configuration from the referenced zone. + properties: + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. Instances of this type with an empty value here are + almost certainly wrong. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + type: string + type: object + x-kubernetes-map-type: atomic + x-kubernetes-validations: + - message: dnsZoneRef.name must be set + rule: self.name != '' + keyName: + description: |- + KeyName is the provider-visible name for this TSIG key (the "wire name"). + Allow DNS name syntax with optional trailing dot. TSIG key names are DNS names (case-insensitive). + maxLength: 253 + minLength: 1 + pattern: ^([A-Za-z0-9_](?:[-A-Za-z0-9_]{0,61}[A-Za-z0-9_])?)(?:\.([A-Za-z0-9_](?:[-A-Za-z0-9_]{0,61}[A-Za-z0-9_])?))*\.?$ + type: string + x-kubernetes-validations: + - message: keyName is immutable and cannot be changed after creation + rule: oldSelf == '' || self == oldSelf + secretRef: + description: |- + SecretRef references an existing Secret containing TSIG material (BYO secret). + When set, the controller reads and validates the Secret, but must not mutate it. + When omitted, the controller generates and manages a Secret named deterministically. + properties: + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. Instances of this type with an empty value here are + almost certainly wrong. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + type: string + type: object + x-kubernetes-map-type: atomic + x-kubernetes-validations: + - message: secretRef.name must be set + rule: self == null || self.name != '' + required: + - dnsZoneRef + - keyName + type: object + status: + description: status defines the observed state of DNSZoneTSIGKey + properties: + conditions: + description: Conditions tracks state such as Accepted and Programmed + readiness. + 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 + secretName: + description: SecretName is the name of the Secret used for this TSIG + key (generated or referenced). + type: string + tsigKeyName: + description: TSIGKeyName is the provider-visible name for this TSIG + key. + type: string + type: object + required: + - spec + type: object + selectableFields: + - jsonPath: .spec.dnsZoneRef.name + served: true + storage: true + subresources: + status: {} diff --git a/config/crd/kustomization.yaml b/config/crd/kustomization.yaml index 8fc3e9a..66d60bd 100644 --- a/config/crd/kustomization.yaml +++ b/config/crd/kustomization.yaml @@ -6,6 +6,7 @@ resources: - bases/dns.networking.miloapis.com_dnszones.yaml - bases/dns.networking.miloapis.com_dnsrecordsets.yaml - bases/dns.networking.miloapis.com_dnszonediscoveries.yaml +- bases/dns.networking.miloapis.com_dnszonetsigkeys.yaml # +kubebuilder:scaffold:crdkustomizeresource patches: diff --git a/config/iam/protected-resources/dnszonetsigkeys.yaml b/config/iam/protected-resources/dnszonetsigkeys.yaml new file mode 100644 index 0000000..e0dbf82 --- /dev/null +++ b/config/iam/protected-resources/dnszonetsigkeys.yaml @@ -0,0 +1,23 @@ +--- +apiVersion: iam.miloapis.com/v1alpha1 +kind: ProtectedResource +metadata: + name: dns.networking.miloapis.com-dnszonetsigkey +spec: + serviceRef: + name: "dns.networking.miloapis.com" + kind: DNSZoneTSIGKey + plural: dnszonetsigkeys + singular: dnszonetsigkey + permissions: + - list + - get + - watch + - create + - update + - patch + - delete + parentResources: + - apiGroup: resourcemanager.miloapis.com + kind: Project + diff --git a/config/iam/protected-resources/kustomization.yaml b/config/iam/protected-resources/kustomization.yaml index 2df1df9..f00e5f6 100644 --- a/config/iam/protected-resources/kustomization.yaml +++ b/config/iam/protected-resources/kustomization.yaml @@ -10,3 +10,4 @@ resources: - dnsrecordsets.yaml - dnszoneclasses.yaml - dnszonediscoveries.yaml + - dnszonetsigkeys.yaml \ No newline at end of file diff --git a/config/iam/roles/dns-admin.yaml b/config/iam/roles/dns-admin.yaml index a01343f..30ed338 100644 --- a/config/iam/roles/dns-admin.yaml +++ b/config/iam/roles/dns-admin.yaml @@ -22,5 +22,9 @@ spec: - dns.networking.miloapis.com/dnszonediscoveries.update - dns.networking.miloapis.com/dnszonediscoveries.patch - dns.networking.miloapis.com/dnszonediscoveries.delete + - dns.networking.miloapis.com/dnszonetsigkeys.create + - dns.networking.miloapis.com/dnszonetsigkeys.update + - dns.networking.miloapis.com/dnszonetsigkeys.patch + - dns.networking.miloapis.com/dnszonetsigkeys.delete diff --git a/config/iam/roles/dns-viewer.yaml b/config/iam/roles/dns-viewer.yaml index 97e2de4..cc4eec8 100644 --- a/config/iam/roles/dns-viewer.yaml +++ b/config/iam/roles/dns-viewer.yaml @@ -20,5 +20,8 @@ spec: - dns.networking.miloapis.com/dnszonediscoveries.list - dns.networking.miloapis.com/dnszonediscoveries.get - dns.networking.miloapis.com/dnszonediscoveries.watch + - dns.networking.miloapis.com/dnszonetsigkeys.list + - dns.networking.miloapis.com/dnszonetsigkeys.get + - dns.networking.miloapis.com/dnszonetsigkeys.watch diff --git a/config/samples/dns_v1alpha1_dnszonetsigkey.yaml b/config/samples/dns_v1alpha1_dnszonetsigkey.yaml new file mode 100644 index 0000000..4db418c --- /dev/null +++ b/config/samples/dns_v1alpha1_dnszonetsigkey.yaml @@ -0,0 +1,14 @@ +apiVersion: dns.networking.miloapis.com/v1alpha1 +kind: DNSZoneTSIGKey +metadata: + name: example-com-xfr +spec: + dnsZoneRef: + name: example-com + keyName: datum-example-com-xfr + # algorithm defaults to hmac-md5 (PowerDNS default); override if desired: + # algorithm: hmac-sha256 + # Optional (BYO secret): + # secretRef: + # name: existing-upstream-tsig + diff --git a/config/samples/kustomization.yaml b/config/samples/kustomization.yaml index cd9ddd5..8c91790 100644 --- a/config/samples/kustomization.yaml +++ b/config/samples/kustomization.yaml @@ -3,4 +3,5 @@ resources: - dns_v1alpha1_dnszoneclass.yaml - dns_v1alpha1_dnszone.yaml - dns_v1alpha1_dnsrecordset.yaml +- dns_v1alpha1_dnszonetsigkey.yaml # +kubebuilder:scaffold:manifestskustomizesamples diff --git a/go.mod b/go.mod index 7508678..20a97b9 100644 --- a/go.mod +++ b/go.mod @@ -8,6 +8,8 @@ require ( github.com/onsi/ginkgo/v2 v2.23.4 github.com/onsi/gomega v1.37.0 github.com/projectdiscovery/dnsx v1.2.1 + github.com/projectdiscovery/retryabledns v1.0.58 + github.com/stretchr/testify v1.11.1 go.datum.net/network-services-operator v0.9.0 go.miloapis.com/milo v0.7.4 golang.org/x/sync v0.17.0 @@ -52,12 +54,10 @@ require ( github.com/power-devops/perfstat v0.0.0-20221212215047-62379fc7944b // indirect github.com/projectdiscovery/blackrock v0.0.1 // indirect github.com/projectdiscovery/cdncheck v1.0.9 // indirect - github.com/projectdiscovery/retryabledns v1.0.58 // indirect github.com/projectdiscovery/utils v0.0.81 // indirect github.com/saintfish/chardet v0.0.0-20230101081208-5e3ef4b5456d // indirect github.com/shirou/gopsutil/v4 v4.25.6 // indirect github.com/sirupsen/logrus v1.9.3 // indirect - github.com/stretchr/testify v1.11.1 // indirect github.com/tidwall/gjson v1.14.4 // indirect github.com/tidwall/match v1.1.1 // indirect github.com/tidwall/pretty v1.2.1 // indirect