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`.
|
- 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.
|
- 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"
|
```yaml tab="Docker"
|
||||||
# Declaring the user list
|
# Declaring the user list
|
||||||
#
|
#
|
||||||
|
@ -118,11 +125,24 @@ kind: Secret
|
||||||
metadata:
|
metadata:
|
||||||
name: authsecret
|
name: authsecret
|
||||||
namespace: default
|
namespace: default
|
||||||
|
|
||||||
data:
|
data:
|
||||||
users: |2
|
users: |2
|
||||||
dGVzdDokYXByMSRINnVza2trVyRJZ1hMUDZld1RyU3VCa1RycUU4d2ovCnRlc3QyOiRhcHIxJGQ5
|
dGVzdDokYXByMSRINnVza2trVyRJZ1hMUDZld1RyU3VCa1RycUU4d2ovCnRlc3QyOiRhcHIxJGQ5
|
||||||
aHI5SEJCJDRIeHdnVWlyM0hQNEVzZ2dQL1FObzAK
|
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"
|
```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"
|
"bufio"
|
||||||
"bytes"
|
"bytes"
|
||||||
"context"
|
"context"
|
||||||
|
"crypto/sha1"
|
||||||
"crypto/sha256"
|
"crypto/sha256"
|
||||||
|
"encoding/base64"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
@ -555,9 +557,38 @@ func createBasicAuthMiddleware(client Client, namespace string, basicAuth *v1alp
|
||||||
return nil, nil
|
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 {
|
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{
|
return &dynamic.BasicAuth{
|
||||||
|
@ -573,9 +604,24 @@ func createDigestAuthMiddleware(client Client, namespace string, digestAuth *v1a
|
||||||
return nil, nil
|
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 {
|
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{
|
return &dynamic.DigestAuth{
|
||||||
|
@ -586,32 +632,23 @@ func createDigestAuthMiddleware(client Client, namespace string, digestAuth *v1a
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func getAuthCredentials(k8sClient Client, authSecret, namespace string) ([]string, error) {
|
func loadBasicAuthCredentials(secret *corev1.Secret) ([]string, error) {
|
||||||
if authSecret == "" {
|
username, usernameExists := secret.Data["username"]
|
||||||
return nil, fmt.Errorf("auth secret must be set")
|
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)
|
hash := sha1.New()
|
||||||
if err != nil {
|
hash.Write(password)
|
||||||
return nil, fmt.Errorf("failed to load auth credentials: %w", err)
|
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) {
|
func loadAuthCredentials(secret *corev1.Secret) ([]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)
|
|
||||||
}
|
|
||||||
if len(secret.Data) != 1 {
|
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
|
var firstSecret []byte
|
||||||
|
@ -628,10 +665,10 @@ func loadAuthCredentials(namespace, secretName string, k8sClient Client) ([]stri
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if err := scanner.Err(); err != nil {
|
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 {
|
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
|
return credentials, nil
|
||||||
|
|
|
@ -4,9 +4,11 @@ import (
|
||||||
"context"
|
"context"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
auth "github.com/abbot/go-http-auth"
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
"github.com/traefik/paerser/types"
|
"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