Support Kubernetes basic-auth secrets
Co-authored-by: Romain <rtribotte@users.noreply.github.com>
This commit is contained in:
parent
60ff50a675
commit
7ff13c3e3e
4 changed files with 172 additions and 27 deletions
|
@ -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"
|
||||
|
|
23
pkg/provider/kubernetes/crd/fixtures/basic_auth_secrets.yml
Normal file
23
pkg/provider/kubernetes/crd/fixtures/basic_auth_secrets.yml
Normal file
|
@ -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
|
|
@ -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
|
||||
|
|
|
@ -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))
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue