Support Kubernetes basic-auth secrets

Co-authored-by: Romain <rtribotte@users.noreply.github.com>
This commit is contained in:
Daniel Tomcej 2021-09-14 07:16:11 -06:00 committed by GitHub
parent 60ff50a675
commit 7ff13c3e3e
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
4 changed files with 172 additions and 27 deletions

View file

@ -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"

View 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

View file

@ -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 []string{fmt.Sprintf("%s:{SHA}%s", username, passwordSum)}, nil
} }
return auth, nil func loadAuthCredentials(secret *corev1.Secret) ([]string, error) {
}
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)
}
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

View file

@ -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))
}