cmd/k8s-operator,k8s-operator: allow optionally using LE staging endpoint for Ingress (#15360)

cmd/k8s-operator,k8s-operator: allow using LE staging endpoint for Ingress

Allow to optionally use LetsEncrypt staging endpoint to issue
certs for Ingress/HA Ingress, so that it is easier to
experiment with initial Ingress setup without hiting rate limits.

Updates tailscale/corp#24795


Signed-off-by: Irbe Krumina <irbe@tailscale.com>
This commit is contained in:
Irbe Krumina 2025-03-21 08:53:41 +00:00 committed by GitHub
parent f3f2f72f96
commit 196ae1cd74
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 413 additions and 168 deletions

View File

@ -2215,6 +2215,22 @@ spec:
https://tailscale.com/kb/1019/subnets#use-your-subnet-routes-from-other-devices
Defaults to false.
type: boolean
useLetsEncryptStagingEnvironment:
description: |-
Set UseLetsEncryptStagingEnvironment to true to issue TLS
certificates for any HTTPS endpoints exposed to the tailnet from
LetsEncrypt's staging environment.
https://letsencrypt.org/docs/staging-environment/
This setting only affects Tailscale Ingress resources.
By default Ingress TLS certificates are issued from LetsEncrypt's
production environment.
Changing this setting true -> false, will result in any
existing certs being re-issued from the production environment.
Changing this setting false (default) -> true, when certs have already
been provisioned from production environment will NOT result in certs
being re-issued from the staging environment before they need to be
renewed.
type: boolean
status:
description: |-
Status of the ProxyClass. This is set and managed automatically.

View File

@ -2685,6 +2685,22 @@ spec:
Defaults to false.
type: boolean
type: object
useLetsEncryptStagingEnvironment:
description: |-
Set UseLetsEncryptStagingEnvironment to true to issue TLS
certificates for any HTTPS endpoints exposed to the tailnet from
LetsEncrypt's staging environment.
https://letsencrypt.org/docs/staging-environment/
This setting only affects Tailscale Ingress resources.
By default Ingress TLS certificates are issued from LetsEncrypt's
production environment.
Changing this setting true -> false, will result in any
existing certs being re-issued from the production environment.
Changing this setting false (default) -> true, when certs have already
been provisioned from production environment will NOT result in certs
being re-issued from the staging environment before they need to be
renewed.
type: boolean
type: object
status:
description: |-

View File

@ -6,6 +6,7 @@
package main
import (
"context"
"testing"
"go.uber.org/zap"
@ -15,17 +16,18 @@ import (
apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/types"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/client/fake"
"tailscale.com/ipn"
tsapi "tailscale.com/k8s-operator/apis/v1alpha1"
"tailscale.com/kube/kubetypes"
"tailscale.com/tstest"
"tailscale.com/types/ptr"
"tailscale.com/util/mak"
)
func TestTailscaleIngress(t *testing.T) {
tsIngressClass := &networkingv1.IngressClass{ObjectMeta: metav1.ObjectMeta{Name: "tailscale"}, Spec: networkingv1.IngressClassSpec{Controller: "tailscale.com/ts-ingress"}}
fc := fake.NewFakeClient(tsIngressClass)
fc := fake.NewFakeClient(ingressClass())
ft := &fakeTSClient{}
fakeTsnetServer := &fakeTSNetServer{certDomains: []string{"foo.com"}}
zl, err := zap.NewDevelopment()
@ -46,45 +48,8 @@ func TestTailscaleIngress(t *testing.T) {
}
// 1. Resources get created for regular Ingress
ing := &networkingv1.Ingress{
TypeMeta: metav1.TypeMeta{Kind: "Ingress", APIVersion: "networking.k8s.io/v1"},
ObjectMeta: metav1.ObjectMeta{
Name: "test",
Namespace: "default",
// The apiserver is supposed to set the UID, but the fake client
// doesn't. So, set it explicitly because other code later depends
// on it being set.
UID: types.UID("1234-UID"),
},
Spec: networkingv1.IngressSpec{
IngressClassName: ptr.To("tailscale"),
DefaultBackend: &networkingv1.IngressBackend{
Service: &networkingv1.IngressServiceBackend{
Name: "test",
Port: networkingv1.ServiceBackendPort{
Number: 8080,
},
},
},
TLS: []networkingv1.IngressTLS{
{Hosts: []string{"default-test"}},
},
},
}
mustCreate(t, fc, ing)
mustCreate(t, fc, &corev1.Service{
ObjectMeta: metav1.ObjectMeta{
Name: "test",
Namespace: "default",
},
Spec: corev1.ServiceSpec{
ClusterIP: "1.2.3.4",
Ports: []corev1.ServicePort{{
Port: 8080,
Name: "http"},
},
},
})
mustCreate(t, fc, ingress())
mustCreate(t, fc, service())
expectReconciled(t, ingR, "default", "test")
@ -114,6 +79,9 @@ func TestTailscaleIngress(t *testing.T) {
mak.Set(&secret.Data, "device_fqdn", []byte("foo.tailnetxyz.ts.net"))
})
expectReconciled(t, ingR, "default", "test")
// Get the ingress and update it with expected changes
ing := ingress()
ing.Finalizers = append(ing.Finalizers, "tailscale.com/finalizer")
ing.Status.LoadBalancer = networkingv1.IngressLoadBalancerStatus{
Ingress: []networkingv1.IngressLoadBalancerIngress{
@ -143,8 +111,7 @@ func TestTailscaleIngress(t *testing.T) {
}
func TestTailscaleIngressHostname(t *testing.T) {
tsIngressClass := &networkingv1.IngressClass{ObjectMeta: metav1.ObjectMeta{Name: "tailscale"}, Spec: networkingv1.IngressClassSpec{Controller: "tailscale.com/ts-ingress"}}
fc := fake.NewFakeClient(tsIngressClass)
fc := fake.NewFakeClient(ingressClass())
ft := &fakeTSClient{}
fakeTsnetServer := &fakeTSNetServer{certDomains: []string{"foo.com"}}
zl, err := zap.NewDevelopment()
@ -165,45 +132,8 @@ func TestTailscaleIngressHostname(t *testing.T) {
}
// 1. Resources get created for regular Ingress
ing := &networkingv1.Ingress{
TypeMeta: metav1.TypeMeta{Kind: "Ingress", APIVersion: "networking.k8s.io/v1"},
ObjectMeta: metav1.ObjectMeta{
Name: "test",
Namespace: "default",
// The apiserver is supposed to set the UID, but the fake client
// doesn't. So, set it explicitly because other code later depends
// on it being set.
UID: types.UID("1234-UID"),
},
Spec: networkingv1.IngressSpec{
IngressClassName: ptr.To("tailscale"),
DefaultBackend: &networkingv1.IngressBackend{
Service: &networkingv1.IngressServiceBackend{
Name: "test",
Port: networkingv1.ServiceBackendPort{
Number: 8080,
},
},
},
TLS: []networkingv1.IngressTLS{
{Hosts: []string{"default-test"}},
},
},
}
mustCreate(t, fc, ing)
mustCreate(t, fc, &corev1.Service{
ObjectMeta: metav1.ObjectMeta{
Name: "test",
Namespace: "default",
},
Spec: corev1.ServiceSpec{
ClusterIP: "1.2.3.4",
Ports: []corev1.ServicePort{{
Port: 8080,
Name: "http"},
},
},
})
mustCreate(t, fc, ingress())
mustCreate(t, fc, service())
expectReconciled(t, ingR, "default", "test")
@ -241,8 +171,10 @@ func TestTailscaleIngressHostname(t *testing.T) {
mak.Set(&secret.Data, "device_fqdn", []byte("foo.tailnetxyz.ts.net"))
})
expectReconciled(t, ingR, "default", "test")
ing.Finalizers = append(ing.Finalizers, "tailscale.com/finalizer")
// Get the ingress and update it with expected changes
ing := ingress()
ing.Finalizers = append(ing.Finalizers, "tailscale.com/finalizer")
expectEqual(t, fc, ing)
// 3. Ingress proxy with capability version >= 110 advertises HTTPS endpoint
@ -299,10 +231,9 @@ func TestTailscaleIngressWithProxyClass(t *testing.T) {
Annotations: map[string]string{"bar.io/foo": "some-val"},
Pod: &tsapi.Pod{Annotations: map[string]string{"foo.io/bar": "some-val"}}}},
}
tsIngressClass := &networkingv1.IngressClass{ObjectMeta: metav1.ObjectMeta{Name: "tailscale"}, Spec: networkingv1.IngressClassSpec{Controller: "tailscale.com/ts-ingress"}}
fc := fake.NewClientBuilder().
WithScheme(tsapi.GlobalScheme).
WithObjects(pc, tsIngressClass).
WithObjects(pc, ingressClass()).
WithStatusSubresource(pc).
Build()
ft := &fakeTSClient{}
@ -326,45 +257,8 @@ func TestTailscaleIngressWithProxyClass(t *testing.T) {
// 1. Ingress is created with no ProxyClass specified, default proxy
// resources get configured.
ing := &networkingv1.Ingress{
TypeMeta: metav1.TypeMeta{Kind: "Ingress", APIVersion: "networking.k8s.io/v1"},
ObjectMeta: metav1.ObjectMeta{
Name: "test",
Namespace: "default",
// The apiserver is supposed to set the UID, but the fake client
// doesn't. So, set it explicitly because other code later depends
// on it being set.
UID: types.UID("1234-UID"),
},
Spec: networkingv1.IngressSpec{
IngressClassName: ptr.To("tailscale"),
DefaultBackend: &networkingv1.IngressBackend{
Service: &networkingv1.IngressServiceBackend{
Name: "test",
Port: networkingv1.ServiceBackendPort{
Number: 8080,
},
},
},
TLS: []networkingv1.IngressTLS{
{Hosts: []string{"default-test"}},
},
},
}
mustCreate(t, fc, ing)
mustCreate(t, fc, &corev1.Service{
ObjectMeta: metav1.ObjectMeta{
Name: "test",
Namespace: "default",
},
Spec: corev1.ServiceSpec{
ClusterIP: "1.2.3.4",
Ports: []corev1.ServicePort{{
Port: 8080,
Name: "http"},
},
},
})
mustCreate(t, fc, ingress())
mustCreate(t, fc, service())
expectReconciled(t, ingR, "default", "test")
@ -432,54 +326,19 @@ func TestTailscaleIngressWithServiceMonitor(t *testing.T) {
ObservedGeneration: 1,
}}},
}
ing := &networkingv1.Ingress{
TypeMeta: metav1.TypeMeta{Kind: "Ingress", APIVersion: "networking.k8s.io/v1"},
ObjectMeta: metav1.ObjectMeta{
Name: "test",
Namespace: "default",
// The apiserver is supposed to set the UID, but the fake client
// doesn't. So, set it explicitly because other code later depends
// on it being set.
UID: types.UID("1234-UID"),
Labels: map[string]string{
"tailscale.com/proxy-class": "metrics",
},
},
Spec: networkingv1.IngressSpec{
IngressClassName: ptr.To("tailscale"),
DefaultBackend: &networkingv1.IngressBackend{
Service: &networkingv1.IngressServiceBackend{
Name: "test",
Port: networkingv1.ServiceBackendPort{
Number: 8080,
},
},
},
TLS: []networkingv1.IngressTLS{
{Hosts: []string{"default-test"}},
},
},
}
svc := &corev1.Service{
ObjectMeta: metav1.ObjectMeta{
Name: "test",
Namespace: "default",
},
Spec: corev1.ServiceSpec{
ClusterIP: "1.2.3.4",
Ports: []corev1.ServicePort{{
Port: 8080,
Name: "http"},
},
},
}
crd := &apiextensionsv1.CustomResourceDefinition{ObjectMeta: metav1.ObjectMeta{Name: serviceMonitorCRD}}
tsIngressClass := &networkingv1.IngressClass{ObjectMeta: metav1.ObjectMeta{Name: "tailscale"}, Spec: networkingv1.IngressClassSpec{Controller: "tailscale.com/ts-ingress"}}
// Create fake client with ProxyClass, IngressClass, Ingress with metrics ProxyClass, and Service
ing := ingress()
ing.Labels = map[string]string{
LabelProxyClass: "metrics",
}
fc := fake.NewClientBuilder().
WithScheme(tsapi.GlobalScheme).
WithObjects(pc, tsIngressClass, ing, svc).
WithObjects(pc, ingressClass(), ing, service()).
WithStatusSubresource(pc).
Build()
ft := &fakeTSClient{}
fakeTsnetServer := &fakeTSNetServer{certDomains: []string{"foo.com"}}
zl, err := zap.NewDevelopment()
@ -560,3 +419,118 @@ func TestTailscaleIngressWithServiceMonitor(t *testing.T) {
expectMissing[corev1.Service](t, fc, "operator-ns", metricsResourceName(shortName))
// ServiceMonitor gets garbage collected when the Service is deleted - we cannot test that here.
}
func TestIngressLetsEncryptStaging(t *testing.T) {
cl := tstest.NewClock(tstest.ClockOpts{})
zl := zap.Must(zap.NewDevelopment())
pcLEStaging, pcLEStagingFalse, pcOther := proxyClassesForLEStagingTest()
testCases := testCasesForLEStagingTests(pcLEStaging, pcLEStagingFalse, pcOther)
for _, tt := range testCases {
t.Run(tt.name, func(t *testing.T) {
builder := fake.NewClientBuilder().
WithScheme(tsapi.GlobalScheme)
builder = builder.WithObjects(pcLEStaging, pcLEStagingFalse, pcOther).
WithStatusSubresource(pcLEStaging, pcLEStagingFalse, pcOther)
fc := builder.Build()
if tt.proxyClassPerResource != "" || tt.defaultProxyClass != "" {
name := tt.proxyClassPerResource
if name == "" {
name = tt.defaultProxyClass
}
setProxyClassReady(t, fc, cl, name)
}
mustCreate(t, fc, ingressClass())
mustCreate(t, fc, service())
ing := ingress()
if tt.proxyClassPerResource != "" {
ing.Labels = map[string]string{
LabelProxyClass: tt.proxyClassPerResource,
}
}
mustCreate(t, fc, ing)
ingR := &IngressReconciler{
Client: fc,
ssr: &tailscaleSTSReconciler{
Client: fc,
tsClient: &fakeTSClient{},
tsnetServer: &fakeTSNetServer{certDomains: []string{"test-host"}},
defaultTags: []string{"tag:test"},
operatorNamespace: "operator-ns",
proxyImage: "tailscale/tailscale:test",
},
logger: zl.Sugar(),
defaultProxyClass: tt.defaultProxyClass,
}
expectReconciled(t, ingR, "default", "test")
_, shortName := findGenName(t, fc, "default", "test", "ingress")
sts := &appsv1.StatefulSet{}
if err := fc.Get(context.Background(), client.ObjectKey{Namespace: "operator-ns", Name: shortName}, sts); err != nil {
t.Fatalf("failed to get StatefulSet: %v", err)
}
if tt.useLEStagingEndpoint {
verifyEnvVar(t, sts, "TS_DEBUG_ACME_DIRECTORY_URL", letsEncryptStagingEndpoint)
} else {
verifyEnvVarNotPresent(t, sts, "TS_DEBUG_ACME_DIRECTORY_URL")
}
})
}
}
func ingressClass() *networkingv1.IngressClass {
return &networkingv1.IngressClass{
ObjectMeta: metav1.ObjectMeta{Name: "tailscale"},
Spec: networkingv1.IngressClassSpec{Controller: "tailscale.com/ts-ingress"},
}
}
func service() *corev1.Service {
return &corev1.Service{
ObjectMeta: metav1.ObjectMeta{
Name: "test",
Namespace: "default",
},
Spec: corev1.ServiceSpec{
ClusterIP: "1.2.3.4",
Ports: []corev1.ServicePort{{
Port: 8080,
Name: "http"},
},
},
}
}
func ingress() *networkingv1.Ingress {
return &networkingv1.Ingress{
TypeMeta: metav1.TypeMeta{Kind: "Ingress", APIVersion: "networking.k8s.io/v1"},
ObjectMeta: metav1.ObjectMeta{
Name: "test",
Namespace: "default",
UID: types.UID("1234-UID"),
},
Spec: networkingv1.IngressSpec{
IngressClassName: ptr.To("tailscale"),
DefaultBackend: &networkingv1.IngressBackend{
Service: &networkingv1.IngressServiceBackend{
Name: "test",
Port: networkingv1.ServiceBackendPort{
Number: 8080,
},
},
},
TLS: []networkingv1.IngressTLS{
{Hosts: []string{"default-test"}},
},
},
}
}

View File

@ -302,7 +302,10 @@ func (r *ProxyGroupReconciler) maybeProvision(ctx context.Context, pg *tsapi.Pro
if err != nil {
return fmt.Errorf("error generating StatefulSet spec: %w", err)
}
ss = applyProxyClassToStatefulSet(proxyClass, ss, nil, logger)
cfg := &tailscaleSTSConfig{
proxyType: string(pg.Spec.Type),
}
ss = applyProxyClassToStatefulSet(proxyClass, ss, cfg, logger)
capver, err := r.capVerForPG(ctx, pg, logger)
if err != nil {
return fmt.Errorf("error getting device info: %w", err)

View File

@ -518,6 +518,60 @@ func TestIngressAdvertiseServicesConfigPreserved(t *testing.T) {
})
}
func proxyClassesForLEStagingTest() (*tsapi.ProxyClass, *tsapi.ProxyClass, *tsapi.ProxyClass) {
pcLEStaging := &tsapi.ProxyClass{
ObjectMeta: metav1.ObjectMeta{
Name: "le-staging",
Generation: 1,
},
Spec: tsapi.ProxyClassSpec{
UseLetsEncryptStagingEnvironment: true,
},
}
pcLEStagingFalse := &tsapi.ProxyClass{
ObjectMeta: metav1.ObjectMeta{
Name: "le-staging-false",
Generation: 1,
},
Spec: tsapi.ProxyClassSpec{
UseLetsEncryptStagingEnvironment: false,
},
}
pcOther := &tsapi.ProxyClass{
ObjectMeta: metav1.ObjectMeta{
Name: "other",
Generation: 1,
},
Spec: tsapi.ProxyClassSpec{},
}
return pcLEStaging, pcLEStagingFalse, pcOther
}
func setProxyClassReady(t *testing.T, fc client.Client, cl *tstest.Clock, name string) *tsapi.ProxyClass {
t.Helper()
pc := &tsapi.ProxyClass{}
if err := fc.Get(context.Background(), client.ObjectKey{Name: name}, pc); err != nil {
t.Fatal(err)
}
pc.Status = tsapi.ProxyClassStatus{
Conditions: []metav1.Condition{{
Type: string(tsapi.ProxyClassReady),
Status: metav1.ConditionTrue,
Reason: reasonProxyClassValid,
Message: reasonProxyClassValid,
LastTransitionTime: metav1.Time{Time: cl.Now().Truncate(time.Second)},
ObservedGeneration: pc.Generation,
}},
}
if err := fc.Status().Update(context.Background(), pc); err != nil {
t.Fatal(err)
}
return pc
}
func verifyProxyGroupCounts(t *testing.T, r *ProxyGroupReconciler, wantIngress, wantEgress int) {
t.Helper()
if r.ingressProxyGroups.Len() != wantIngress {
@ -541,6 +595,16 @@ func verifyEnvVar(t *testing.T, sts *appsv1.StatefulSet, name, expectedValue str
t.Errorf("%s environment variable not found", name)
}
func verifyEnvVarNotPresent(t *testing.T, sts *appsv1.StatefulSet, name string) {
t.Helper()
for _, env := range sts.Spec.Template.Spec.Containers[0].Env {
if env.Name == name {
t.Errorf("environment variable %s should not be present", name)
return
}
}
}
func expectProxyGroupResources(t *testing.T, fc client.WithWatch, pg *tsapi.ProxyGroup, shouldExist bool, cfgHash string, proxyClass *tsapi.ProxyClass) {
t.Helper()
@ -618,3 +682,146 @@ func addNodeIDToStateSecrets(t *testing.T, fc client.WithWatch, pg *tsapi.ProxyG
})
}
}
func TestProxyGroupLetsEncryptStaging(t *testing.T) {
cl := tstest.NewClock(tstest.ClockOpts{})
zl := zap.Must(zap.NewDevelopment())
// Set up test cases- most are shared with non-HA Ingress.
type proxyGroupLETestCase struct {
leStagingTestCase
pgType tsapi.ProxyGroupType
}
pcLEStaging, pcLEStagingFalse, pcOther := proxyClassesForLEStagingTest()
sharedTestCases := testCasesForLEStagingTests(pcLEStaging, pcLEStagingFalse, pcOther)
var tests []proxyGroupLETestCase
for _, tt := range sharedTestCases {
tests = append(tests, proxyGroupLETestCase{
leStagingTestCase: tt,
pgType: tsapi.ProxyGroupTypeIngress,
})
}
tests = append(tests, proxyGroupLETestCase{
leStagingTestCase: leStagingTestCase{
name: "egress_pg_with_staging_proxyclass",
proxyClassPerResource: "le-staging",
useLEStagingEndpoint: false,
},
pgType: tsapi.ProxyGroupTypeEgress,
})
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
builder := fake.NewClientBuilder().
WithScheme(tsapi.GlobalScheme)
// Pre-populate the fake client with ProxyClasses.
builder = builder.WithObjects(pcLEStaging, pcLEStagingFalse, pcOther).
WithStatusSubresource(pcLEStaging, pcLEStagingFalse, pcOther)
fc := builder.Build()
// If the test case needs a ProxyClass to exist, ensure it is set to Ready.
if tt.proxyClassPerResource != "" || tt.defaultProxyClass != "" {
name := tt.proxyClassPerResource
if name == "" {
name = tt.defaultProxyClass
}
setProxyClassReady(t, fc, cl, name)
}
// Create ProxyGroup
pg := &tsapi.ProxyGroup{
ObjectMeta: metav1.ObjectMeta{
Name: "test",
},
Spec: tsapi.ProxyGroupSpec{
Type: tt.pgType,
Replicas: ptr.To[int32](1),
ProxyClass: tt.proxyClassPerResource,
},
}
mustCreate(t, fc, pg)
reconciler := &ProxyGroupReconciler{
tsNamespace: tsNamespace,
proxyImage: testProxyImage,
defaultTags: []string{"tag:test"},
defaultProxyClass: tt.defaultProxyClass,
Client: fc,
tsClient: &fakeTSClient{},
l: zl.Sugar(),
clock: cl,
}
expectReconciled(t, reconciler, "", pg.Name)
// Verify that the StatefulSet created for ProxyGrup has
// the expected setting for the staging endpoint.
sts := &appsv1.StatefulSet{}
if err := fc.Get(context.Background(), client.ObjectKey{Namespace: tsNamespace, Name: pg.Name}, sts); err != nil {
t.Fatalf("failed to get StatefulSet: %v", err)
}
if tt.useLEStagingEndpoint {
verifyEnvVar(t, sts, "TS_DEBUG_ACME_DIRECTORY_URL", letsEncryptStagingEndpoint)
} else {
verifyEnvVarNotPresent(t, sts, "TS_DEBUG_ACME_DIRECTORY_URL")
}
})
}
}
type leStagingTestCase struct {
name string
// ProxyClass set on ProxyGroup or Ingress resource.
proxyClassPerResource string
// Default ProxyClass.
defaultProxyClass string
useLEStagingEndpoint bool
}
// Shared test cases for LE staging endpoint configuration for ProxyGroup and
// non-HA Ingress.
func testCasesForLEStagingTests(pcLEStaging, pcLEStagingFalse, pcOther *tsapi.ProxyClass) []leStagingTestCase {
return []leStagingTestCase{
{
name: "with_staging_proxyclass",
proxyClassPerResource: "le-staging",
useLEStagingEndpoint: true,
},
{
name: "with_staging_proxyclass_false",
proxyClassPerResource: "le-staging-false",
useLEStagingEndpoint: false,
},
{
name: "with_other_proxyclass",
proxyClassPerResource: "other",
useLEStagingEndpoint: false,
},
{
name: "no_proxyclass",
proxyClassPerResource: "",
useLEStagingEndpoint: false,
},
{
name: "with_default_staging_proxyclass",
proxyClassPerResource: "",
defaultProxyClass: "le-staging",
useLEStagingEndpoint: true,
},
{
name: "with_default_other_proxyclass",
proxyClassPerResource: "",
defaultProxyClass: "other",
useLEStagingEndpoint: false,
},
{
name: "with_default_staging_proxyclass_false",
proxyClassPerResource: "",
defaultProxyClass: "le-staging-false",
useLEStagingEndpoint: false,
},
}
}

View File

@ -102,6 +102,8 @@ const (
envVarTSLocalAddrPort = "TS_LOCAL_ADDR_PORT"
defaultLocalAddrPort = 9002 // metrics and health check port
letsEncryptStagingEndpoint = "https://acme-staging-v02.api.letsencrypt.org/directory"
)
var (
@ -783,6 +785,17 @@ func applyProxyClassToStatefulSet(pc *tsapi.ProxyClass, ss *appsv1.StatefulSet,
enableEndpoints(ss, metricsEnabled, debugEnabled)
}
}
if pc.Spec.UseLetsEncryptStagingEnvironment && (stsCfg.proxyType == proxyTypeIngressResource || stsCfg.proxyType == string(tsapi.ProxyGroupTypeIngress)) {
for i, c := range ss.Spec.Template.Spec.Containers {
if c.Name == "tailscale" {
ss.Spec.Template.Spec.Containers[i].Env = append(ss.Spec.Template.Spec.Containers[i].Env, corev1.EnvVar{
Name: "TS_DEBUG_ACME_DIRECTORY_URL",
Value: letsEncryptStagingEndpoint,
})
break
}
}
}
if pc.Spec.StatefulSet == nil {
return ss

View File

@ -517,6 +517,7 @@ _Appears in:_
| `statefulSet` _[StatefulSet](#statefulset)_ | Configuration parameters for the proxy's StatefulSet. Tailscale<br />Kubernetes operator deploys a StatefulSet for each of the user<br />configured proxies (Tailscale Ingress, Tailscale Service, Connector). | | |
| `metrics` _[Metrics](#metrics)_ | Configuration for proxy metrics. Metrics are currently not supported<br />for egress proxies and for Ingress proxies that have been configured<br />with tailscale.com/experimental-forward-cluster-traffic-via-ingress<br />annotation. Note that the metrics are currently considered unstable<br />and will likely change in breaking ways in the future - we only<br />recommend that you use those for debugging purposes. | | |
| `tailscale` _[TailscaleConfig](#tailscaleconfig)_ | TailscaleConfig contains options to configure the tailscale-specific<br />parameters of proxies. | | |
| `useLetsEncryptStagingEnvironment` _boolean_ | Set UseLetsEncryptStagingEnvironment to true to issue TLS<br />certificates for any HTTPS endpoints exposed to the tailnet from<br />LetsEncrypt's staging environment.<br />https://letsencrypt.org/docs/staging-environment/<br />This setting only affects Tailscale Ingress resources.<br />By default Ingress TLS certificates are issued from LetsEncrypt's<br />production environment.<br />Changing this setting true -> false, will result in any<br />existing certs being re-issued from the production environment.<br />Changing this setting false (default) -> true, when certs have already<br />been provisioned from production environment will NOT result in certs<br />being re-issued from the staging environment before they need to be<br />renewed. | | |
#### ProxyClassStatus

View File

@ -66,6 +66,21 @@ type ProxyClassSpec struct {
// parameters of proxies.
// +optional
TailscaleConfig *TailscaleConfig `json:"tailscale,omitempty"`
// Set UseLetsEncryptStagingEnvironment to true to issue TLS
// certificates for any HTTPS endpoints exposed to the tailnet from
// LetsEncrypt's staging environment.
// https://letsencrypt.org/docs/staging-environment/
// This setting only affects Tailscale Ingress resources.
// By default Ingress TLS certificates are issued from LetsEncrypt's
// production environment.
// Changing this setting true -> false, will result in any
// existing certs being re-issued from the production environment.
// Changing this setting false (default) -> true, when certs have already
// been provisioned from production environment will NOT result in certs
// being re-issued from the staging environment before they need to be
// renewed.
// +optional
UseLetsEncryptStagingEnvironment bool `json:"useLetsEncryptStagingEnvironment,omitempty"`
}
type TailscaleConfig struct {