From 7f36045a165aede3a09751e1e7c7821f45a5ab00 Mon Sep 17 00:00:00 2001 From: David Bond Date: Tue, 16 Dec 2025 12:31:47 +0000 Subject: [PATCH] k8s-operator: add "tailnet" field to custom resources (#18174) This commit modifies our `Connector`, `ProxyGroup` and `Recorder` resources by adding a new field named "tailnet" that will be used to specify the set of oauth credentials that should be used to generate the auth key that authenticates them. This field is optional and when set should contain the name of an existing `Tailnet` resource. Updates: https://github.com/tailscale/corp/issues/35132 Signed-off-by: David Bond --- .../deploy/crds/tailscale.com_connectors.yaml | 8 +++++++ .../crds/tailscale.com_proxygroups.yaml | 8 +++++++ .../deploy/crds/tailscale.com_recorders.yaml | 8 +++++++ .../deploy/manifests/operator.yaml | 24 +++++++++++++++++++ k8s-operator/api.md | 3 +++ k8s-operator/apis/v1alpha1/types_connector.go | 6 +++++ .../apis/v1alpha1/types_proxygroup.go | 6 +++++ k8s-operator/apis/v1alpha1/types_recorder.go | 6 +++++ 8 files changed, 69 insertions(+) diff --git a/cmd/k8s-operator/deploy/crds/tailscale.com_connectors.yaml b/cmd/k8s-operator/deploy/crds/tailscale.com_connectors.yaml index 74d32d53d..03c51c755 100644 --- a/cmd/k8s-operator/deploy/crds/tailscale.com_connectors.yaml +++ b/cmd/k8s-operator/deploy/crds/tailscale.com_connectors.yaml @@ -181,6 +181,14 @@ spec: items: type: string pattern: ^tag:[a-zA-Z][a-zA-Z0-9-]*$ + tailnet: + description: |- + Tailnet specifies the tailnet this Connector should join. If blank, the default tailnet is used. When set, this + name must match that of a valid Tailnet resource. This field is immutable and cannot be changed once set. + type: string + x-kubernetes-validations: + - rule: self == oldSelf + message: Connector tailnet is immutable x-kubernetes-validations: - rule: has(self.subnetRouter) || (has(self.exitNode) && self.exitNode == true) || has(self.appConnector) message: A Connector needs to have at least one of exit node, subnet router or app connector configured. diff --git a/cmd/k8s-operator/deploy/crds/tailscale.com_proxygroups.yaml b/cmd/k8s-operator/deploy/crds/tailscale.com_proxygroups.yaml index 98ca1c378..0254f01b8 100644 --- a/cmd/k8s-operator/deploy/crds/tailscale.com_proxygroups.yaml +++ b/cmd/k8s-operator/deploy/crds/tailscale.com_proxygroups.yaml @@ -139,6 +139,14 @@ spec: items: type: string pattern: ^tag:[a-zA-Z][a-zA-Z0-9-]*$ + tailnet: + description: |- + Tailnet specifies the tailnet this ProxyGroup should join. If blank, the default tailnet is used. When set, this + name must match that of a valid Tailnet resource. This field is immutable and cannot be changed once set. + type: string + x-kubernetes-validations: + - rule: self == oldSelf + message: ProxyGroup tailnet is immutable type: description: |- Type of the ProxyGroup proxies. Supported types are egress, ingress, and kube-apiserver. diff --git a/cmd/k8s-operator/deploy/crds/tailscale.com_recorders.yaml b/cmd/k8s-operator/deploy/crds/tailscale.com_recorders.yaml index 48db3ef4b..e8cce7b0b 100644 --- a/cmd/k8s-operator/deploy/crds/tailscale.com_recorders.yaml +++ b/cmd/k8s-operator/deploy/crds/tailscale.com_recorders.yaml @@ -1688,6 +1688,14 @@ spec: items: type: string pattern: ^tag:[a-zA-Z][a-zA-Z0-9-]*$ + tailnet: + description: |- + Tailnet specifies the tailnet this Recorder should join. If blank, the default tailnet is used. When set, this + name must match that of a valid Tailnet resource. This field is immutable and cannot be changed once set. + type: string + x-kubernetes-validations: + - rule: self == oldSelf + message: Recorder tailnet is immutable x-kubernetes-validations: - rule: '!(self.replicas > 1 && (!has(self.storage) || !has(self.storage.s3)))' message: S3 storage must be used when deploying multiple Recorder replicas diff --git a/cmd/k8s-operator/deploy/manifests/operator.yaml b/cmd/k8s-operator/deploy/manifests/operator.yaml index 71c0b89b1..9219636a9 100644 --- a/cmd/k8s-operator/deploy/manifests/operator.yaml +++ b/cmd/k8s-operator/deploy/manifests/operator.yaml @@ -206,6 +206,14 @@ spec: pattern: ^tag:[a-zA-Z][a-zA-Z0-9-]*$ type: string type: array + tailnet: + description: |- + Tailnet specifies the tailnet this Connector should join. If blank, the default tailnet is used. When set, this + name must match that of a valid Tailnet resource. This field is immutable and cannot be changed once set. + type: string + x-kubernetes-validations: + - message: Connector tailnet is immutable + rule: self == oldSelf type: object x-kubernetes-validations: - message: A Connector needs to have at least one of exit node, subnet router or app connector configured. @@ -3145,6 +3153,14 @@ spec: pattern: ^tag:[a-zA-Z][a-zA-Z0-9-]*$ type: string type: array + tailnet: + description: |- + Tailnet specifies the tailnet this ProxyGroup should join. If blank, the default tailnet is used. When set, this + name must match that of a valid Tailnet resource. This field is immutable and cannot be changed once set. + type: string + x-kubernetes-validations: + - message: ProxyGroup tailnet is immutable + rule: self == oldSelf type: description: |- Type of the ProxyGroup proxies. Supported types are egress, ingress, and kube-apiserver. @@ -4968,6 +4984,14 @@ spec: pattern: ^tag:[a-zA-Z][a-zA-Z0-9-]*$ type: string type: array + tailnet: + description: |- + Tailnet specifies the tailnet this Recorder should join. If blank, the default tailnet is used. When set, this + name must match that of a valid Tailnet resource. This field is immutable and cannot be changed once set. + type: string + x-kubernetes-validations: + - message: Recorder tailnet is immutable + rule: self == oldSelf type: object x-kubernetes-validations: - message: S3 storage must be used when deploying multiple Recorder replicas diff --git a/k8s-operator/api.md b/k8s-operator/api.md index db5402b27..31f351013 100644 --- a/k8s-operator/api.md +++ b/k8s-operator/api.md @@ -141,6 +141,7 @@ _Appears in:_ | `appConnector` _[AppConnector](#appconnector)_ | AppConnector defines whether the Connector device should act as a Tailscale app connector. A Connector that is
configured as an app connector cannot be a subnet router or an exit node. If this field is unset, the
Connector does not act as an app connector.
Note that you will need to manually configure the permissions and the domains for the app connector via the
Admin panel.
Note also that the main tested and supported use case of this config option is to deploy an app connector on
Kubernetes to access SaaS applications available on the public internet. Using the app connector to expose
cluster workloads or other internal workloads to tailnet might work, but this is not a use case that we have
tested or optimised for.
If you are using the app connector to access SaaS applications because you need a predictable egress IP that
can be whitelisted, it is also your responsibility to ensure that cluster traffic from the connector flows
via that predictable IP, for example by enforcing that cluster egress traffic is routed via an egress NAT
device with a static IP address.
https://tailscale.com/kb/1281/app-connectors | | | | `exitNode` _boolean_ | ExitNode defines whether the Connector device should act as a Tailscale exit node. Defaults to false.
This field is mutually exclusive with the appConnector field.
https://tailscale.com/kb/1103/exit-nodes | | | | `replicas` _integer_ | Replicas specifies how many devices to create. Set this to enable
high availability for app connectors, subnet routers, or exit nodes.
https://tailscale.com/kb/1115/high-availability. Defaults to 1. | | Minimum: 0
| +| `tailnet` _string_ | Tailnet specifies the tailnet this Connector should join. If blank, the default tailnet is used. When set, this
name must match that of a valid Tailnet resource. This field is immutable and cannot be changed once set. | | | #### ConnectorStatus @@ -743,6 +744,7 @@ _Appears in:_ | `hostnamePrefix` _[HostnamePrefix](#hostnameprefix)_ | HostnamePrefix is the hostname prefix to use for tailnet devices created
by the ProxyGroup. Each device will have the integer number from its
StatefulSet pod appended to this prefix to form the full hostname.
HostnamePrefix can contain lower case letters, numbers and dashes, it
must not start with a dash and must be between 1 and 62 characters long. | | Pattern: `^[a-z0-9][a-z0-9-]{0,61}$`
Type: string
| | `proxyClass` _string_ | ProxyClass is the name of the ProxyClass custom resource that contains
configuration options that should be applied to the resources created
for this ProxyGroup. If unset, and there is no default ProxyClass
configured, the operator will create resources with the default
configuration. | | | | `kubeAPIServer` _[KubeAPIServerConfig](#kubeapiserverconfig)_ | KubeAPIServer contains configuration specific to the kube-apiserver
ProxyGroup type. This field is only used when Type is set to "kube-apiserver". | | | +| `tailnet` _string_ | Tailnet specifies the tailnet this ProxyGroup should join. If blank, the default tailnet is used. When set, this
name must match that of a valid Tailnet resource. This field is immutable and cannot be changed once set. | | | #### ProxyGroupStatus @@ -903,6 +905,7 @@ _Appears in:_ | `enableUI` _boolean_ | Set to true to enable the Recorder UI. The UI lists and plays recorded sessions.
The UI will be served at :443. Defaults to false.
Corresponds to --ui tsrecorder flag https://tailscale.com/kb/1246/tailscale-ssh-session-recording#deploy-a-recorder-node.
Required if S3 storage is not set up, to ensure that recordings are accessible. | | | | `storage` _[Storage](#storage)_ | Configure where to store session recordings. By default, recordings will
be stored in a local ephemeral volume, and will not be persisted past the
lifetime of a specific pod. | | | | `replicas` _integer_ | Replicas specifies how many instances of tsrecorder to run. Defaults to 1. | | Minimum: 0
| +| `tailnet` _string_ | Tailnet specifies the tailnet this Recorder should join. If blank, the default tailnet is used. When set, this
name must match that of a valid Tailnet resource. This field is immutable and cannot be changed once set. | | | #### RecorderStatefulSet diff --git a/k8s-operator/apis/v1alpha1/types_connector.go b/k8s-operator/apis/v1alpha1/types_connector.go index 58457500f..ebedea18f 100644 --- a/k8s-operator/apis/v1alpha1/types_connector.go +++ b/k8s-operator/apis/v1alpha1/types_connector.go @@ -133,6 +133,12 @@ type ConnectorSpec struct { // +optional // +kubebuilder:validation:Minimum=0 Replicas *int32 `json:"replicas,omitempty"` + + // Tailnet specifies the tailnet this Connector should join. If blank, the default tailnet is used. When set, this + // name must match that of a valid Tailnet resource. This field is immutable and cannot be changed once set. + // +optional + // +kubebuilder:validation:XValidation:rule="self == oldSelf",message="Connector tailnet is immutable" + Tailnet string `json:"tailnet,omitempty"` } // SubnetRouter defines subnet routes that should be exposed to tailnet via a diff --git a/k8s-operator/apis/v1alpha1/types_proxygroup.go b/k8s-operator/apis/v1alpha1/types_proxygroup.go index 28fd9e009..8cbcc2d19 100644 --- a/k8s-operator/apis/v1alpha1/types_proxygroup.go +++ b/k8s-operator/apis/v1alpha1/types_proxygroup.go @@ -97,6 +97,12 @@ type ProxyGroupSpec struct { // ProxyGroup type. This field is only used when Type is set to "kube-apiserver". // +optional KubeAPIServer *KubeAPIServerConfig `json:"kubeAPIServer,omitempty"` + + // Tailnet specifies the tailnet this ProxyGroup should join. If blank, the default tailnet is used. When set, this + // name must match that of a valid Tailnet resource. This field is immutable and cannot be changed once set. + // +optional + // +kubebuilder:validation:XValidation:rule="self == oldSelf",message="ProxyGroup tailnet is immutable" + Tailnet string `json:"tailnet,omitempty"` } type ProxyGroupStatus struct { diff --git a/k8s-operator/apis/v1alpha1/types_recorder.go b/k8s-operator/apis/v1alpha1/types_recorder.go index 67cffbf09..d5a22e82c 100644 --- a/k8s-operator/apis/v1alpha1/types_recorder.go +++ b/k8s-operator/apis/v1alpha1/types_recorder.go @@ -81,6 +81,12 @@ type RecorderSpec struct { // +optional // +kubebuilder:validation:Minimum=0 Replicas *int32 `json:"replicas,omitzero"` + + // Tailnet specifies the tailnet this Recorder should join. If blank, the default tailnet is used. When set, this + // name must match that of a valid Tailnet resource. This field is immutable and cannot be changed once set. + // +optional + // +kubebuilder:validation:XValidation:rule="self == oldSelf",message="Recorder tailnet is immutable" + Tailnet string `json:"tailnet,omitempty"` } type RecorderStatefulSet struct {