diff --git a/internal/kube/secrets/context.go b/internal/kube/secrets/context.go index 62eac27f6..4f7d79819 100644 --- a/internal/kube/secrets/context.go +++ b/internal/kube/secrets/context.go @@ -18,6 +18,24 @@ const ( annotationKeySslProfileContext = "internal.skupper.io/tls-profile-context" ) +// IsTlsCredentialSecret reports whether a Secret carries TLS credential material. +// kubernetes.io/tls secrets are always accepted; Opaque secrets with standard TLS +// keys are also accepted for backward compatibility with pre-generated link tokens. +func IsTlsCredentialSecret(secret *corev1.Secret) bool { + if secret == nil { + return false + } + if secret.Type == corev1.SecretTypeTLS { + return true + } + if secret.Type != corev1.SecretTypeOpaque && secret.Type != "" { + return false + } + return len(secret.Data["tls.crt"]) > 0 && + len(secret.Data["tls.key"]) > 0 && + len(secret.Data["ca.crt"]) > 0 +} + type profileContext struct { ProfileName string `json:"profileName"` Ordinal uint64 `json:"ordinal"` diff --git a/internal/kube/secrets/context_test.go b/internal/kube/secrets/context_test.go new file mode 100644 index 000000000..860ef97fe --- /dev/null +++ b/internal/kube/secrets/context_test.go @@ -0,0 +1,80 @@ +package secrets + +import ( + "testing" + + corev1 "k8s.io/api/core/v1" +) + +func TestIsTlsCredentialSecret(t *testing.T) { + tlsData := map[string][]byte{ + "ca.crt": []byte("ca"), + "tls.crt": []byte("cert"), + "tls.key": []byte("key"), + } + + tests := []struct { + name string + secret *corev1.Secret + want bool + }{ + { + name: "kubernetes.io/tls", + secret: &corev1.Secret{ + Type: corev1.SecretTypeTLS, + Data: tlsData, + }, + want: true, + }, + { + name: "opaque with tls keys", + secret: &corev1.Secret{ + Type: corev1.SecretTypeOpaque, + Data: tlsData, + }, + want: true, + }, + { + name: "empty type with tls keys", + secret: &corev1.Secret{ + Data: tlsData, + }, + want: true, + }, + { + name: "opaque missing ca.crt", + secret: &corev1.Secret{ + Type: corev1.SecretTypeOpaque, + Data: map[string][]byte{ + "tls.crt": []byte("cert"), + "tls.key": []byte("key"), + }, + }, + want: false, + }, + { + name: "basic-auth secret", + secret: &corev1.Secret{ + Type: corev1.SecretTypeBasicAuth, + Data: map[string][]byte{ + "username": []byte("user"), + "password": []byte("pass"), + }, + }, + want: false, + }, + { + name: "nil secret", + secret: nil, + want: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := IsTlsCredentialSecret(tt.secret); got != tt.want { + t.Fatalf("IsTlsCredentialSecret() = %v, want %v", got, tt.want) + } + }) + } +} diff --git a/internal/kube/secrets/manager.go b/internal/kube/secrets/manager.go index 46d30c5ba..e7dacaade 100644 --- a/internal/kube/secrets/manager.go +++ b/internal/kube/secrets/manager.go @@ -72,8 +72,8 @@ func (w *ProfilesWatcher) handleSecret(key string, secret *corev1.Secret) error } secretName := secret.ObjectMeta.Name changed := false - switch secret.Type { - case "kubernetes.io/tls": + switch { + case IsTlsCredentialSecret(secret): var secretsContext profileContextSet for _, profileName := range secretProfiles(secretName) { state, ok := w.state[profileName] @@ -119,7 +119,7 @@ func (w *ProfilesWatcher) handleSecret(key string, secret *corev1.Secret) error slog.Any("context", secretsContext), ) return w.update(w) - case "kubernetes.io/basic-auth": + case secret.Type == "kubernetes.io/basic-auth": state, ok := w.state[secretName] if ok { if state.SecretKey == "" { @@ -249,7 +249,7 @@ func (w *ProfilesWatcher) TlsCredentialSecretPresent(credentialName string) bool } for _, secretName := range profileSecrets(credentialName) { secret, err := w.Cache.Get(w.keyfunc(secretName)) - if err == nil && secret != nil && secret.Type == corev1.SecretTypeTLS { + if err == nil && secret != nil && IsTlsCredentialSecret(secret) { return true } } diff --git a/internal/kube/secrets/sync.go b/internal/kube/secrets/sync.go index 9ae994555..083bb4328 100644 --- a/internal/kube/secrets/sync.go +++ b/internal/kube/secrets/sync.go @@ -155,8 +155,8 @@ func (s *Sync) handle(key string, secret *corev1.Secret) error { return nil } - switch secret.Type { - case "kubernetes.io/tls": + switch { + case IsTlsCredentialSecret(secret): metadata, found, err := fromSecret(secret) if err != nil { return fmt.Errorf("failed to decode secret metadata: %s", err) @@ -174,7 +174,7 @@ func (s *Sync) handle(key string, secret *corev1.Secret) error { s.doCallback(profileName) } } - case "kubernetes.io/basic-auth": + case secret.Type == "kubernetes.io/basic-auth": namespace, _, err := cache.SplitMetaNamespaceKey(key) if err != nil { return err diff --git a/internal/kube/secrets/sync_test.go b/internal/kube/secrets/sync_test.go index 8715a3cfc..37d96817b 100644 --- a/internal/kube/secrets/sync_test.go +++ b/internal/kube/secrets/sync_test.go @@ -105,6 +105,34 @@ func TestSyncHandler(t *testing.T) { assertFiles(t, path.Join(tmpdir, "test-tls"), expectedFiles) } +func TestSyncHandlerOpaqueTlsSecret(t *testing.T) { + tmpdir := t.TempDir() + tlog := slog.New(slog.NewTextHandler(io.Discard, nil)) + sCache := secretsCacheFactoryFixture(t, "testing") + secretSync := secrets.NewSync(sCache.Factory, nil, tlog) + secretSync.Recover() + + configuredProfiles := map[string]qdr.SslProfile{ + "link-tls-profile": fixtureSslProfile("link-tls-profile", tmpdir, 0, 0, false), + } + delta := secretSync.ExpectSslProfiles(configuredProfiles) + if len(delta.Missing) != 1 { + t.Fatalf("expected missing profile link-tls-profile: %s", delta.Error()) + } + + sCache.Secrets["testing/link-tls"] = fixtureOpaqueTlsSecret("link-tls", "testing") + sCache.Secrets["testing/link-tls"].Annotations[tlsProfKey] = `[{"profileName": "link-tls-profile", "ordinal": 0}]` + if err := sCache.HandlerFn("testing/link-tls", sCache.Secrets["testing/link-tls"]); err != nil { + t.Fatalf("unexpected error handling opaque tls secret: %s", err) + } + + delta = secretSync.ExpectSslProfiles(configuredProfiles) + if !delta.Empty() { + t.Fatalf("expected opaque tls secret to satisfy profile: %s", delta.Error()) + } + assertFiles(t, path.Join(tmpdir, "link-tls-profile"), expectedFiles) +} + func TestSyncCallback(t *testing.T) { tmpdir := t.TempDir() tlog := slog.New(slog.NewTextHandler(io.Discard, nil)) @@ -168,6 +196,23 @@ func assertFiles(t *testing.T, dir string, fileNames []string) { } } +func fixtureOpaqueTlsSecret(name, namespace string) *corev1.Secret { + return &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: name, + Namespace: namespace, + Annotations: map[string]string{}, + }, + Data: map[string][]byte{ + "ca.crt": []byte("ca.crt - " + name), + "tls.crt": []byte("tls.crt - " + name), + "tls.key": []byte("tls.key - " + name), + "connect.json": []byte("{}"), + }, + Type: corev1.SecretTypeOpaque, + } +} + func fixtureTlsSecret(name, namespace string) *corev1.Secret { return &corev1.Secret{ ObjectMeta: metav1.ObjectMeta{