diff --git a/docs/content/middlewares/http/basicauth.md b/docs/content/middlewares/http/basicauth.md index e207a238b..596445a03 100644 --- a/docs/content/middlewares/http/basicauth.md +++ b/docs/content/middlewares/http/basicauth.md @@ -88,6 +88,13 @@ The `users` option is an array of authorized users. Each user must be declared u - If both `users` and `usersFile` are provided, the two are merged. The contents of `usersFile` have precedence over the values in `users`. - For security reasons, the field `users` doesn't exist for Kubernetes IngressRoute, and one should use the `secret` field instead. +!!! note "Kubernetes kubernetes.io/basic-auth secret type" + + Kubernetes supports a special `kubernetes.io/basic-auth` secret type. + This secret must contain two keys: `username` and `password`. + Please note that these keys are not hashed or encrypted in any way, and therefore is less secure than other methods. + You can find more information on the [Kubernetes Basic Authentication Secret Documentation](https://kubernetes.io/docs/concepts/configuration/secret/#basic-authentication-secret) + ```yaml tab="Docker" # Declaring the user list # @@ -118,11 +125,24 @@ kind: Secret metadata: name: authsecret namespace: default - data: users: |2 dGVzdDokYXByMSRINnVza2trVyRJZ1hMUDZld1RyU3VCa1RycUU4d2ovCnRlc3QyOiRhcHIxJGQ5 aHI5SEJCJDRIeHdnVWlyM0hQNEVzZ2dQL1FObzAK + +--- +# This is an alternate auth secret that demonstrates the basic-auth secret type. +# Note: the password is not hashed, and is merely base64 encoded. + +apiVersion: v1 +kind: Secret +metadata: + name: authsecret2 + namespace: default +type: kubernetes.io/basic-auth +data: + username: dXNlcg== # username: user + password: cGFzc3dvcmQ= # password: password ``` ```yaml tab="Consul Catalog" diff --git a/pkg/provider/kubernetes/crd/fixtures/basic_auth_secrets.yml b/pkg/provider/kubernetes/crd/fixtures/basic_auth_secrets.yml new file mode 100644 index 000000000..5d7943e41 --- /dev/null +++ b/pkg/provider/kubernetes/crd/fixtures/basic_auth_secrets.yml @@ -0,0 +1,23 @@ +apiVersion: v1 +kind: Secret +metadata: + name: basic-auth-secret + namespace: default +type: kubernetes.io/basic-auth +data: + username: dXNlcg== # username: user + password: cGFzc3dvcmQ= # password: password + +--- +apiVersion: v1 +kind: Secret +metadata: + name: auth-secret + namespace: default + +data: + # test:test -> test:$apr1$H6uskkkW$IgXLP6ewTrSuBkTrqE8wj/ + # test2:test2 -> test2:$apr1$d9hr9HBB$4HxwgUir3HP4EsggP/QNo0 + users: |2 + dGVzdDokYXByMSRINnVza2trVyRJZ1hMUDZld1RyU3VCa1RycUU4d2ovCnRlc3QyOiRhcHIxJGQ5 + aHI5SEJCJDRIeHdnVWlyM0hQNEVzZ2dQL1FObzAK diff --git a/pkg/provider/kubernetes/crd/kubernetes.go b/pkg/provider/kubernetes/crd/kubernetes.go index 4dd1d208d..35abd4fc6 100644 --- a/pkg/provider/kubernetes/crd/kubernetes.go +++ b/pkg/provider/kubernetes/crd/kubernetes.go @@ -4,7 +4,9 @@ import ( "bufio" "bytes" "context" + "crypto/sha1" "crypto/sha256" + "encoding/base64" "encoding/json" "errors" "fmt" @@ -555,9 +557,38 @@ func createBasicAuthMiddleware(client Client, namespace string, basicAuth *v1alp return nil, nil } - credentials, err := getAuthCredentials(client, basicAuth.Secret, namespace) + if basicAuth.Secret == "" { + return nil, fmt.Errorf("auth secret must be set") + } + + secret, ok, err := client.GetSecret(namespace, basicAuth.Secret) if err != nil { - return nil, err + return nil, fmt.Errorf("failed to fetch secret '%s/%s': %w", namespace, basicAuth.Secret, err) + } + if !ok { + return nil, fmt.Errorf("secret '%s/%s' not found", namespace, basicAuth.Secret) + } + if secret == nil { + return nil, fmt.Errorf("data for secret '%s/%s' must not be nil", namespace, basicAuth.Secret) + } + + if secret.Type == corev1.SecretTypeBasicAuth { + credentials, err := loadBasicAuthCredentials(secret) + if err != nil { + return nil, fmt.Errorf("failed to load basic auth credentials: %w", err) + } + + return &dynamic.BasicAuth{ + Users: credentials, + Realm: basicAuth.Realm, + RemoveHeader: basicAuth.RemoveHeader, + HeaderField: basicAuth.HeaderField, + }, nil + } + + credentials, err := loadAuthCredentials(secret) + if err != nil { + return nil, fmt.Errorf("failed to load basic auth credentials: %w", err) } return &dynamic.BasicAuth{ @@ -573,9 +604,24 @@ func createDigestAuthMiddleware(client Client, namespace string, digestAuth *v1a return nil, nil } - credentials, err := getAuthCredentials(client, digestAuth.Secret, namespace) + if digestAuth.Secret == "" { + return nil, fmt.Errorf("auth secret must be set") + } + + secret, ok, err := client.GetSecret(namespace, digestAuth.Secret) if err != nil { - return nil, err + return nil, fmt.Errorf("failed to fetch secret '%s/%s': %w", namespace, digestAuth.Secret, err) + } + if !ok { + return nil, fmt.Errorf("secret '%s/%s' not found", namespace, digestAuth.Secret) + } + if secret == nil { + return nil, fmt.Errorf("data for secret '%s/%s' must not be nil", namespace, digestAuth.Secret) + } + + credentials, err := loadAuthCredentials(secret) + if err != nil { + return nil, fmt.Errorf("failed to load digest auth credentials: %w", err) } return &dynamic.DigestAuth{ @@ -586,32 +632,23 @@ func createDigestAuthMiddleware(client Client, namespace string, digestAuth *v1a }, nil } -func getAuthCredentials(k8sClient Client, authSecret, namespace string) ([]string, error) { - if authSecret == "" { - return nil, fmt.Errorf("auth secret must be set") +func loadBasicAuthCredentials(secret *corev1.Secret) ([]string, error) { + username, usernameExists := secret.Data["username"] + password, passwordExists := secret.Data["password"] + if !(usernameExists && passwordExists) { + return nil, fmt.Errorf("secret '%s/%s' must contain both username and password keys", secret.Namespace, secret.Name) } - auth, err := loadAuthCredentials(namespace, authSecret, k8sClient) - if err != nil { - return nil, fmt.Errorf("failed to load auth credentials: %w", err) - } + hash := sha1.New() + hash.Write(password) + passwordSum := base64.StdEncoding.EncodeToString(hash.Sum(nil)) - return auth, nil + return []string{fmt.Sprintf("%s:{SHA}%s", username, passwordSum)}, nil } -func loadAuthCredentials(namespace, secretName string, k8sClient Client) ([]string, error) { - secret, ok, err := k8sClient.GetSecret(namespace, secretName) - if err != nil { - return nil, fmt.Errorf("failed to fetch secret '%s/%s': %w", namespace, secretName, err) - } - if !ok { - return nil, fmt.Errorf("secret '%s/%s' not found", namespace, secretName) - } - if secret == nil { - return nil, fmt.Errorf("data for secret '%s/%s' must not be nil", namespace, secretName) - } +func loadAuthCredentials(secret *corev1.Secret) ([]string, error) { if len(secret.Data) != 1 { - return nil, fmt.Errorf("found %d elements for secret '%s/%s', must be single element exactly", len(secret.Data), namespace, secretName) + return nil, fmt.Errorf("found %d elements for secret '%s/%s', must be single element exactly", len(secret.Data), secret.Namespace, secret.Name) } var firstSecret []byte @@ -628,10 +665,10 @@ func loadAuthCredentials(namespace, secretName string, k8sClient Client) ([]stri } } if err := scanner.Err(); err != nil { - return nil, fmt.Errorf("error reading secret for %s/%s: %w", namespace, secretName, err) + return nil, fmt.Errorf("error reading secret for %s/%s: %w", secret.Namespace, secret.Name, err) } if len(credentials) == 0 { - return nil, fmt.Errorf("secret '%s/%s' does not contain any credentials", namespace, secretName) + return nil, fmt.Errorf("secret '%s/%s' does not contain any credentials", secret.Namespace, secret.Name) } return credentials, nil diff --git a/pkg/provider/kubernetes/crd/kubernetes_test.go b/pkg/provider/kubernetes/crd/kubernetes_test.go index c02f9889d..020e890cb 100644 --- a/pkg/provider/kubernetes/crd/kubernetes_test.go +++ b/pkg/provider/kubernetes/crd/kubernetes_test.go @@ -4,9 +4,11 @@ import ( "context" "os" "path/filepath" + "strings" "testing" "time" + auth "github.com/abbot/go-http-auth" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/traefik/paerser/types" @@ -5265,3 +5267,66 @@ func TestExternalNameService(t *testing.T) { }) } } + +func TestCreateBasicAuthCredentials(t *testing.T) { + var k8sObjects []runtime.Object + var crdObjects []runtime.Object + yamlContent, err := os.ReadFile(filepath.FromSlash("./fixtures/basic_auth_secrets.yml")) + if err != nil { + panic(err) + } + + objects := k8s.MustParseYaml(yamlContent) + for _, obj := range objects { + switch o := obj.(type) { + case *corev1.Secret: + k8sObjects = append(k8sObjects, o) + default: + } + } + + kubeClient := kubefake.NewSimpleClientset(k8sObjects...) + crdClient := crdfake.NewSimpleClientset(crdObjects...) + + client := newClientImpl(kubeClient, crdClient) + + stopCh := make(chan struct{}) + + eventCh, err := client.WatchAll([]string{"default"}, stopCh) + require.NoError(t, err) + + if k8sObjects != nil || crdObjects != nil { + // just wait for the first event + <-eventCh + } + + // Testing for username/password components in basic-auth secret + basicAuth, secretErr := createBasicAuthMiddleware(client, "default", &v1alpha1.BasicAuth{Secret: "basic-auth-secret"}) + require.NoError(t, secretErr) + require.Len(t, basicAuth.Users, 1) + + components := strings.Split(basicAuth.Users[0], ":") + require.Len(t, components, 2) + + username := components[0] + hashedPassword := components[1] + + require.Equal(t, "user", username) + require.Equal(t, "{SHA}W6ph5Mm5Pz8GgiULbPgzG37mj9g=", hashedPassword) + assert.True(t, auth.CheckSecret("password", hashedPassword)) + + // Testing for username/password components in htpasswd secret + basicAuth, secretErr = createBasicAuthMiddleware(client, "default", &v1alpha1.BasicAuth{Secret: "auth-secret"}) + require.NoError(t, secretErr) + require.Len(t, basicAuth.Users, 2) + + components = strings.Split(basicAuth.Users[1], ":") + require.Len(t, components, 2) + + username = components[0] + hashedPassword = components[1] + + assert.Equal(t, username, "test2") + assert.Equal(t, hashedPassword, "$apr1$d9hr9HBB$4HxwgUir3HP4EsggP/QNo0") + assert.True(t, auth.CheckSecret("test2", hashedPassword)) +}