Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions internal/kube/secrets/context.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 &&

@c-kruse c-kruse Jul 7, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This last assertion would break at least one (probably untested) thing. Until this regression and the subsequent changes to tls credentials dependencies the controller was designed to mostly turn a blind eye to tlsCredentials secret contents. It mostly tried to sort the contents out in the router/kube-adaptor.

It's probably more work than just this to iron out all of the corner cases, but to tell whether or not a secret is suitable to be used with an sslProfile configuration we need more context. Example: a Connector with spec.useClientCert=false (default) only configures a sslProfile with a CA - no cert or key file.

All that to say - let's either properly solve for this with a path with concrete "reasons" a secret is unacceptable for a particular tlsCredential or return to ye olden days with something wide open like 'secret.Type == "" || opaque || tls'

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not to complicate this more, but just ran into another edge case we've never handled well: Listeners and MultiKeyListeners with requireClientCert=false have no real purpose for a CA certificate. We currently configure the sslProfile with one unconditionally and the router crashes when it is empty/absent. Looks like the kubernetes.io/tls type only requires tls.crt and tls.key (not ca.crt.)

Maybe the least controversial, most backwards compatible IsTlsCredentialSecret would be something like this? Would really count on your judgement here: A cryptic "Secret not found" error message is a big improvement over a crashing router deployment, but a major downgrade from a working link/service generated by regular skupper tooling.

func IsTlsCredentialSecret(secret *corev1.Secret) bool {
	if secret == nil || secret.Data == nil || secret.Type == corev1.SecretTypeBasicAuth {
		return false
	}
	return len(secret.Data["ca.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"`
Expand Down
80 changes: 80 additions & 0 deletions internal/kube/secrets/context_test.go
Original file line number Diff line number Diff line change
@@ -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)
}
})
}
}
8 changes: 4 additions & 4 deletions internal/kube/secrets/manager.go
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand Down Expand Up @@ -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 == "" {
Expand Down Expand Up @@ -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
}
}
Expand Down
6 changes: 3 additions & 3 deletions internal/kube/secrets/sync.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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
Expand Down
45 changes: 45 additions & 0 deletions internal/kube/secrets/sync_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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))
Expand Down Expand Up @@ -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{
Expand Down
Loading