// Copyright (c) Tailscale Inc & AUTHORS // SPDX-License-Identifier: BSD-3-Clause package e2e import ( "context" "encoding/json" "fmt" "strings" "testing" "time" corev1 "k8s.io/api/core/v1" rbacv1 "k8s.io/api/rbac/v1" apierrors "k8s.io/apimachinery/pkg/api/errors" "k8s.io/client-go/rest" "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/client/config" "tailscale.com/client/tailscale" "tailscale.com/tsnet" "tailscale.com/tstest" ) // See [TestMain] for test requirements. func TestProxy(t *testing.T) { if tsClient == nil { t.Skip("TestProxy requires credentials for a tailscale client") } ctx := context.Background() cfg := config.GetConfigOrDie() cl, err := client.New(cfg, client.Options{}) if err != nil { t.Fatal(err) } // Create role and role binding to allow a group we'll impersonate to do stuff. createAndCleanup(t, ctx, cl, &rbacv1.Role{ ObjectMeta: objectMeta("tailscale", "read-secrets"), Rules: []rbacv1.PolicyRule{{ APIGroups: []string{""}, Verbs: []string{"get"}, Resources: []string{"secrets"}, }}, }) createAndCleanup(t, ctx, cl, &rbacv1.RoleBinding{ ObjectMeta: objectMeta("tailscale", "read-secrets"), Subjects: []rbacv1.Subject{{ Kind: "Group", Name: "ts:e2e-test-proxy", }}, RoleRef: rbacv1.RoleRef{ Kind: "Role", Name: "read-secrets", }, }) // Get operator host name from kube secret. operatorSecret := corev1.Secret{ ObjectMeta: objectMeta("tailscale", "operator"), } if err := get(ctx, cl, &operatorSecret); err != nil { t.Fatal(err) } // Connect to tailnet with test-specific tag so we can use the // [testGrants] ACLs when connecting to the API server proxy ts := tsnetServerWithTag(t, ctx, "tag:e2e-test-proxy") proxyCfg := &rest.Config{ Host: fmt.Sprintf("https://%s:443", hostNameFromOperatorSecret(t, operatorSecret)), Dial: ts.Dial, } proxyCl, err := client.New(proxyCfg, client.Options{}) if err != nil { t.Fatal(err) } // Expect success. allowedSecret := corev1.Secret{ ObjectMeta: objectMeta("tailscale", "operator"), } // Wait for up to a minute the first time we use the proxy, to give it time // to provision the TLS certs. if err := tstest.WaitFor(time.Second*60, func() error { return get(ctx, proxyCl, &allowedSecret) }); err != nil { t.Fatal(err) } // Expect forbidden. forbiddenSecret := corev1.Secret{ ObjectMeta: objectMeta("default", "operator"), } if err := get(ctx, proxyCl, &forbiddenSecret); err == nil || !apierrors.IsForbidden(err) { t.Fatalf("expected forbidden error fetching secret from default namespace: %s", err) } } func tsnetServerWithTag(t *testing.T, ctx context.Context, tag string) *tsnet.Server { caps := tailscale.KeyCapabilities{ Devices: tailscale.KeyDeviceCapabilities{ Create: tailscale.KeyDeviceCreateCapabilities{ Reusable: false, Preauthorized: true, Ephemeral: true, Tags: []string{tag}, }, }, } authKey, authKeyMeta, err := tsClient.CreateKey(ctx, caps) if err != nil { t.Fatal(err) } t.Cleanup(func() { if err := tsClient.DeleteKey(ctx, authKeyMeta.ID); err != nil { t.Errorf("error deleting auth key: %s", err) } }) ts := &tsnet.Server{ Hostname: "test-proxy", Ephemeral: true, Dir: t.TempDir(), AuthKey: authKey, } _, err = ts.Up(ctx) if err != nil { t.Fatal(err) } t.Cleanup(func() { if err := ts.Close(); err != nil { t.Errorf("error shutting down tsnet.Server: %s", err) } }) return ts } func hostNameFromOperatorSecret(t *testing.T, s corev1.Secret) string { profiles := map[string]any{} if err := json.Unmarshal(s.Data["_profiles"], &profiles); err != nil { t.Fatal(err) } key, ok := strings.CutPrefix(string(s.Data["_current-profile"]), "profile-") if !ok { t.Fatal(string(s.Data["_current-profile"])) } profile, ok := profiles[key] if !ok { t.Fatal(profiles) } return ((profile.(map[string]any))["Name"]).(string) }