Repository: traefik/traefik (language: go)

Issue:
Plugin has no option to read config from k8s secret file.
### Welcome!

- [X] Yes, I've searched similar issues on [GitHub](https://github.com/traefik/traefik/issues) and didn't find any.
- [X] Yes, I've searched similar issues on the [Traefik community forum](https://community.traefik.io) and didn't find any.

### What did you expect to see?

I made some custom plugins and some of them need to read secret from k8s secret file. Such as JWT, verify signature should be in the k8s secret, and middleware spec config should read from it.


Discussion / hints:

Fix patch:
diff --git a/pkg/provider/kubernetes/crd/fixtures/with_plugin_deep_read_secret.yml b/pkg/provider/kubernetes/crd/fixtures/with_plugin_deep_read_secret.yml
new file mode 100644
index 0000000000..f99bde07c5
--- /dev/null
+++ b/pkg/provider/kubernetes/crd/fixtures/with_plugin_deep_read_secret.yml
@@ -0,0 +1,24 @@
+apiVersion: v1
+kind: Secret
+metadata:
+  name: name
+  namespace: default
+
+data:
+  key: dGhpc19pc190aGVfdmVyeV9kZWVwX3NlY3JldA==
+
+---
+apiVersion: traefik.containo.us/v1alpha1
+kind: Middleware
+metadata:
+  name: test-secret
+  namespace: default
+
+spec:
+  plugin:
+    test-secret:
+      secret_0:
+        secret_1:
+          secret_2:
+            user: admin
+            secret: urn:k8s:secret:name:key
diff --git a/pkg/provider/kubernetes/crd/fixtures/with_plugin_read_array_of_map_contain_secret.yml b/pkg/provider/kubernetes/crd/fixtures/with_plugin_read_array_of_map_contain_secret.yml
new file mode 100644
index 0000000000..36042fe575
--- /dev/null
+++ b/pkg/provider/kubernetes/crd/fixtures/with_plugin_read_array_of_map_contain_secret.yml
@@ -0,0 +1,25 @@
+apiVersion: v1
+kind: Secret
+metadata:
+  name: name
+  namespace: default
+
+data:
+  key1: YWRtaW5fcGFzc3dvcmQ=
+  key2: dXNlcl9wYXNzd29yZA==
+
+---
+apiVersion: traefik.containo.us/v1alpha1
+kind: Middleware
+metadata:
+  name: test-secret
+  namespace: default
+
+spec:
+  plugin:
+    test-secret:
+      users:
+        - name: admin
+          secret: urn:k8s:secret:name:key1
+        - name: user
+          secret: urn:k8s:secret:name:key2
diff --git a/pkg/provider/kubernetes/crd/fixtures/with_plugin_read_array_of_secret.yml b/pkg/provider/kubernetes/crd/fixtures/with_plugin_read_array_of_secret.yml
new file mode 100644
index 0000000000..7b88c1c42d
--- /dev/null
+++ b/pkg/provider/kubernetes/crd/fixtures/with_plugin_read_array_of_secret.yml
@@ -0,0 +1,23 @@
+apiVersion: v1
+kind: Secret
+metadata:
+  name: name
+  namespace: default
+
+data:
+  key1: c2VjcmV0X2RhdGEx
+  key2: c2VjcmV0X2RhdGEy
+
+---
+apiVersion: traefik.containo.us/v1alpha1
+kind: Middleware
+metadata:
+  name: test-secret
+  namespace: default
+
+spec:
+  plugin:
+    test-secret:
+      secret:
+        - urn:k8s:secret:name:key1
+        - urn:k8s:secret:name:key2
diff --git a/pkg/provider/kubernetes/crd/fixtures/with_plugin_read_not_exist_secret.yml b/pkg/provider/kubernetes/crd/fixtures/with_plugin_read_not_exist_secret.yml
new file mode 100644
index 0000000000..c9923d1d83
--- /dev/null
+++ b/pkg/provider/kubernetes/crd/fixtures/with_plugin_read_not_exist_secret.yml
@@ -0,0 +1,11 @@
+apiVersion: traefik.containo.us/v1alpha1
+kind: Middleware
+metadata:
+  name: test-secret
+  namespace: default
+
+spec:
+  plugin:
+    test-secret:
+      user: admin
+      secret: urn:k8s:secret:notfound:key
diff --git a/pkg/provider/kubernetes/crd/fixtures/with_plugin_read_secret.yml b/pkg/provider/kubernetes/crd/fixtures/with_plugin_read_secret.yml
new file mode 100644
index 0000000000..bcd368619a
--- /dev/null
+++ b/pkg/provider/kubernetes/crd/fixtures/with_plugin_read_secret.yml
@@ -0,0 +1,21 @@
+apiVersion: v1
+kind: Secret
+metadata:
+  name: name
+  namespace: default
+
+data:
+  key: dGhpc19pc190aGVfc2VjcmV0
+
+---
+apiVersion: traefik.containo.us/v1alpha1
+kind: Middleware
+metadata:
+  name: test-secret
+  namespace: default
+
+spec:
+  plugin:
+    test-secret:
+      user: admin
+      secret: urn:k8s:secret:name:key
diff --git a/pkg/provider/kubernetes/crd/kubernetes.go b/pkg/provider/kubernetes/crd/kubernetes.go
index 7270c37d82..bfc9d3fa3f 100644
--- a/pkg/provider/kubernetes/crd/kubernetes.go
+++ b/pkg/provider/kubernetes/crd/kubernetes.go
@@ -232,7 +232,7 @@ func (p *Provider) loadConfigurationFromCRD(ctx context.Context, client Client)
 			conf.HTTP.Services[serviceName] = errorPageService
 		}
 
-		plugin, err := createPluginMiddleware(middleware.Spec.Plugin)
+		plugin, err := createPluginMiddleware(client, middleware.Namespace, middleware.Spec.Plugin)
 		if err != nil {
 			log.FromContext(ctxMid).Errorf("Error while reading plugins middleware: %v", err)
 			continue
@@ -419,7 +419,7 @@ func getServicePort(svc *corev1.Service, port intstr.IntOrString) (*corev1.Servi
 	return &corev1.ServicePort{Port: port.IntVal}, nil
 }
 
-func createPluginMiddleware(plugins map[string]apiextensionv1.JSON) (map[string]dynamic.PluginConf, error) {
+func createPluginMiddleware(k8sClient Client, ns string, plugins map[string]apiextensionv1.JSON) (map[string]dynamic.PluginConf, error) {
 	if plugins == nil {
 		return nil, nil
 	}
@@ -429,13 +429,73 @@ func createPluginMiddleware(plugins map[string]apiextensionv1.JSON) (map[string]
 		return nil, err
 	}
 
-	pc := map[string]dynamic.PluginConf{}
-	err = json.Unmarshal(data, &pc)
-	if err != nil {
+	pcMap := map[string]dynamic.PluginConf{}
+	if err = json.Unmarshal(data, &pcMap); err != nil {
 		return nil, err
 	}
 
-	return pc, nil
+	for _, pc := range pcMap {
+		for key := range pc {
+			if pc[key], err = loadSecretKeys(k8sClient, ns, pc[key]); err != nil {
+				return nil, err
+			}
+		}
+	}
+
+	return pcMap, nil
+}
+
+func loadSecretKeys(k8sClient Client, ns string, i interface{}) (interface{}, error) {
+	var err error
+	switch iv := i.(type) {
+	case string:
+		if !strings.HasPrefix(iv, "urn:k8s:secret:") {
+			return iv, nil
+		}
+
+		return getSecretValue(k8sClient, ns, iv)
+
+	case []interface{}:
+		for i := range iv {
+			if iv[i], err = loadSecretKeys(k8sClient, ns, iv[i]); err != nil {
+				return nil, err
+			}
+		}
+
+	case map[string]interface{}:
+		for k := range iv {
+			if iv[k], err = loadSecretKeys(k8sClient, ns, iv[k]); err != nil {
+				return nil, err
+			}
+		}
+	}
+
+	return i, nil
+}
+
+func getSecretValue(c Client, ns, urn string) (string, error) {
+	parts := strings.Split(urn, ":")
+	if len(parts) != 5 {
+		return "", fmt.Errorf("malformed secret URN %q", urn)
+	}
+
+	secretName := parts[3]
+	secret, ok, err := c.GetSecret(ns, secretName)
+	if err != nil {
+		return "", err
+	}
+
+	if !ok {
+		return "", fmt.Errorf("secret %s/%s is not found", ns, secretName)
+	}
+
+	secretKey := parts[4]
+	secretValue, ok := secret.Data[secretKey]
+	if !ok {
+		return "", fmt.Errorf("key %q not found in secret %s/%s", secretKey, ns, secretName)
+	}
+
+	return string(secretValue), nil
 }
 
 func createCircuitBreakerMiddleware(circuitBreaker *v1alpha1.CircuitBreaker) (*dynamic.CircuitBreaker, error) {


Test patch:
diff --git a/pkg/provider/kubernetes/crd/kubernetes_test.go b/pkg/provider/kubernetes/crd/kubernetes_test.go
index 6c081f0b15..e1c0944036 100644
--- a/pkg/provider/kubernetes/crd/kubernetes_test.go
+++ b/pkg/provider/kubernetes/crd/kubernetes_test.go
@@ -3339,6 +3339,166 @@ func TestLoadIngressRoutes(t *testing.T) {
 				},
 			},
 		},
+		{
+			desc:  "Simple Ingress Route, with test middleware read config from secret",
+			paths: []string{"services.yml", "with_plugin_read_secret.yml"},
+			expected: &dynamic.Configuration{
+				UDP: &dynamic.UDPConfiguration{
+					Routers:  map[string]*dynamic.UDPRouter{},
+					Services: map[string]*dynamic.UDPService{},
+				},
+				TLS: &dynamic.TLSConfiguration{},
+				TCP: &dynamic.TCPConfiguration{
+					Routers:     map[string]*dynamic.TCPRouter{},
+					Middlewares: map[string]*dynamic.TCPMiddleware{},
+					Services:    map[string]*dynamic.TCPService{},
+				},
+				HTTP: &dynamic.HTTPConfiguration{
+					Routers: map[string]*dynamic.Router{},
+					Middlewares: map[string]*dynamic.Middleware{
+						"default-test-secret": {
+							Plugin: map[string]dynamic.PluginConf{
+								"test-secret": map[string]interface{}{
+									"user":   "admin",
+									"secret": "this_is_the_secret",
+								},
+							},
+						},
+					},
+					Services:          map[string]*dynamic.Service{},
+					ServersTransports: map[string]*dynamic.ServersTransport{},
+				},
+			},
+		},
+		{
+			desc:  "Simple Ingress Route, with test middleware read config from deep secret",
+			paths: []string{"services.yml", "with_plugin_deep_read_secret.yml"},
+			expected: &dynamic.Configuration{
+				UDP: &dynamic.UDPConfiguration{
+					Routers:  map[string]*dynamic.UDPRouter{},
+					Services: map[string]*dynamic.UDPService{},
+				},
+				TLS: &dynamic.TLSConfiguration{},
+				TCP: &dynamic.TCPConfiguration{
+					Routers:     map[string]*dynamic.TCPRouter{},
+					Middlewares: map[string]*dynamic.TCPMiddleware{},
+					Services:    map[string]*dynamic.TCPService{},
+				},
+				HTTP: &dynamic.HTTPConfiguration{
+					Routers: map[string]*dynamic.Router{},
+					Middlewares: map[string]*dynamic.Middleware{
+						"default-test-secret": {
+							Plugin: map[string]dynamic.PluginConf{
+								"test-secret": map[string]interface{}{
+									"secret_0": map[string]interface{}{
+										"secret_1": map[string]interface{}{
+											"secret_2": map[string]interface{}{
+												"user":   "admin",
+												"secret": "this_is_the_very_deep_secret",
+											},
+										},
+									},
+								},
+							},
+						},
+					},
+					Services:          map[string]*dynamic.Service{},
+					ServersTransports: map[string]*dynamic.ServersTransport{},
+				},
+			},
+		},
+		{
+			desc:  "Simple Ingress Route, with test middleware read config from an array of secret",
+			paths: []string{"services.yml", "with_plugin_read_array_of_secret.yml"},
+			expected: &dynamic.Configuration{
+				UDP: &dynamic.UDPConfiguration{
+					Routers:  map[string]*dynamic.UDPRouter{},
+					Services: map[string]*dynamic.UDPService{},
+				},
+				TLS: &dynamic.TLSConfiguration{},
+				TCP: &dynamic.TCPConfiguration{
+					Routers:     map[string]*dynamic.TCPRouter{},
+					Middlewares: map[string]*dynamic.TCPMiddleware{},
+					Services:    map[string]*dynamic.TCPService{},
+				},
+				HTTP: &dynamic.HTTPConfiguration{
+					Routers: map[string]*dynamic.Router{},
+					Middlewares: map[string]*dynamic.Middleware{
+						"default-test-secret": {
+							Plugin: map[string]dynamic.PluginConf{
+								"test-secret": map[string]interface{}{
+									"secret": []interface{}{"secret_data1", "secret_data2"},
+								},
+							},
+						},
+					},
+					Services:          map[string]*dynamic.Service{},
+					ServersTransports: map[string]*dynamic.ServersTransport{},
+				},
+			},
+		},
+		{
+			desc:  "Simple Ingress Route, with test middleware read config from an array of secret",
+			paths: []string{"services.yml", "with_plugin_read_array_of_map_contain_secret.yml"},
+			expected: &dynamic.Configuration{
+				UDP: &dynamic.UDPConfiguration{
+					Routers:  map[string]*dynamic.UDPRouter{},
+					Services: map[string]*dynamic.UDPService{},
+				},
+				TLS: &dynamic.TLSConfiguration{},
+				TCP: &dynamic.TCPConfiguration{
+					Routers:     map[string]*dynamic.TCPRouter{},
+					Middlewares: map[string]*dynamic.TCPMiddleware{},
+					Services:    map[string]*dynamic.TCPService{},
+				},
+				HTTP: &dynamic.HTTPConfiguration{
+					Routers: map[string]*dynamic.Router{},
+					Middlewares: map[string]*dynamic.Middleware{
+						"default-test-secret": {
+							Plugin: map[string]dynamic.PluginConf{
+								"test-secret": map[string]interface{}{
+									"users": []interface{}{
+										map[string]interface{}{
+											"name":   "admin",
+											"secret": "admin_password",
+										},
+										map[string]interface{}{
+											"name":   "user",
+											"secret": "user_password",
+										},
+									},
+								},
+							},
+						},
+					},
+					Services:          map[string]*dynamic.Service{},
+					ServersTransports: map[string]*dynamic.ServersTransport{},
+				},
+			},
+		},
+		{
+			desc:                "Simple Ingress Route, with test middleware read config from secret that not found",
+			paths:               []string{"services.yml", "with_plugin_read_not_exist_secret.yml"},
+			allowCrossNamespace: true,
+			expected: &dynamic.Configuration{
+				UDP: &dynamic.UDPConfiguration{
+					Routers:  map[string]*dynamic.UDPRouter{},
+					Services: map[string]*dynamic.UDPService{},
+				},
+				TLS: &dynamic.TLSConfiguration{},
+				TCP: &dynamic.TCPConfiguration{
+					Routers:     map[string]*dynamic.TCPRouter{},
+					Middlewares: map[string]*dynamic.TCPMiddleware{},
+					Services:    map[string]*dynamic.TCPService{},
+				},
+				HTTP: &dynamic.HTTPConfiguration{
+					Routers:           map[string]*dynamic.Router{},
+					Middlewares:       map[string]*dynamic.Middleware{},
+					Services:          map[string]*dynamic.Service{},
+					ServersTransports: map[string]*dynamic.ServersTransport{},
+				},
+			},
+		},
 		{
 			desc:  "Simple Ingress Route, with error page middleware",
 			paths: []string{"services.yml", "with_error_page.yml"},