mirror of
https://github.com/rancher/k3k.git
synced 2026-08-19 04:16:16 +00:00
Fix projected volume token translation and different service accounts (#1128)
* Fix projected volume token translation and different service accounts Signed-off-by: galal-hussein <hussein.galal.ahmed.11@gmail.com> * wsl Signed-off-by: galal-hussein <hussein.galal.ahmed.11@gmail.com> * Fix unsanitized audience names and token paths Signed-off-by: galal-hussein <hussein.galal.ahmed.11@gmail.com> --------- Signed-off-by: galal-hussein <hussein.galal.ahmed.11@gmail.com>
This commit is contained in:
@@ -2,12 +2,14 @@ package provider
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"k8s.io/apimachinery/pkg/types"
|
||||
"k8s.io/utils/ptr"
|
||||
"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
|
||||
|
||||
authv1 "k8s.io/api/authentication/v1"
|
||||
@@ -29,6 +31,10 @@ func (p *Provider) transformTokens(ctx context.Context, virtualPod, hostPod *cor
|
||||
logger := p.logger.WithValues("namespace", virtualPod.Namespace, "name", virtualPod.Name, "serviceAccountName", virtualPod.Spec.ServiceAccountName)
|
||||
logger.V(1).Info("Transforming service account tokens")
|
||||
|
||||
hostPod.Spec.ServiceAccountName = ""
|
||||
hostPod.Spec.DeprecatedServiceAccount = ""
|
||||
hostPod.Spec.AutomountServiceAccountToken = new(false)
|
||||
|
||||
// transform projected service account token
|
||||
if err := p.transformProjectedTokens(ctx, virtualPod, hostPod); err != nil {
|
||||
return err
|
||||
@@ -77,10 +83,6 @@ func (p *Provider) transformKubeAccessToken(ctx context.Context, virtualPod, hos
|
||||
return err
|
||||
}
|
||||
|
||||
hostPod.Spec.ServiceAccountName = ""
|
||||
hostPod.Spec.DeprecatedServiceAccount = ""
|
||||
hostPod.Spec.AutomountServiceAccountToken = ptr.To(false)
|
||||
|
||||
removeKubeAccessVolume(hostPod)
|
||||
addKubeAccessVolume(hostPod, hostSecret.Name)
|
||||
|
||||
@@ -292,8 +294,8 @@ func addKubeAccessVolume(pod *corev1.Pod, hostSecretName string) {
|
||||
func generateTokenSecretName(serviceAccountName, tokenPath string, tokenReq *authv1.TokenRequest) string {
|
||||
nameComponents := []string{serviceAccountName}
|
||||
|
||||
if tokenReq.Spec.Audiences != nil {
|
||||
nameComponents = append(nameComponents, tokenReq.Spec.Audiences...)
|
||||
for _, aud := range tokenReq.Spec.Audiences {
|
||||
nameComponents = append(nameComponents, sanitizeNameComponent(aud))
|
||||
}
|
||||
|
||||
if exp := tokenReq.Spec.ExpirationSeconds; exp != nil {
|
||||
@@ -301,8 +303,25 @@ func generateTokenSecretName(serviceAccountName, tokenPath string, tokenReq *aut
|
||||
}
|
||||
|
||||
if tokenPath != "" {
|
||||
nameComponents = append(nameComponents, tokenPath)
|
||||
nameComponents = append(nameComponents, sanitizeNameComponent(tokenPath))
|
||||
}
|
||||
|
||||
return k3kcontroller.SafeConcatNameWithPrefix(nameComponents...)
|
||||
}
|
||||
|
||||
var invalidNameChars = regexp.MustCompile(`[^a-z0-9.-]+`)
|
||||
|
||||
// sanitizeNameComponent lowercases s and replaces any character that is invalid in a Kubernetes
|
||||
// name with "-". If the result is identical to s it is returned as is, otherwise a 6 character
|
||||
// sha256 digest of the original is appended to keep distinct inputs distinct, in the form
|
||||
// <sanitized-string>-<digest>.
|
||||
func sanitizeNameComponent(s string) string {
|
||||
sanitized := strings.Trim(invalidNameChars.ReplaceAllString(strings.ToLower(s), "-"), "-.")
|
||||
if sanitized == s {
|
||||
return sanitized
|
||||
}
|
||||
|
||||
digest := sha256.Sum256([]byte(s))
|
||||
|
||||
return sanitized + "-" + hex.EncodeToString(digest[:])[:6]
|
||||
}
|
||||
|
||||
@@ -4,7 +4,6 @@ import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"k8s.io/utils/ptr"
|
||||
|
||||
authv1 "k8s.io/api/authentication/v1"
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
@@ -217,12 +216,12 @@ func Test_addKubeAccessVolume(t *testing.T) {
|
||||
}
|
||||
|
||||
func Test_virtualSecret(t *testing.T) {
|
||||
s := virtualSecret("my-secret", "my-ns", "my-sa")
|
||||
s := virtualSecret("my-secret", "my-ns", "my-serviceaccount")
|
||||
|
||||
assert.Equal(t, "my-secret", s.Name)
|
||||
assert.Equal(t, "my-ns", s.Namespace)
|
||||
assert.Equal(t, corev1.SecretTypeServiceAccountToken, s.Type)
|
||||
assert.Equal(t, "my-sa", s.Annotations[corev1.ServiceAccountNameKey])
|
||||
assert.Equal(t, "my-serviceaccount", s.Annotations[corev1.ServiceAccountNameKey])
|
||||
assert.Equal(t, "Secret", s.Kind)
|
||||
assert.Equal(t, "v1", s.APIVersion)
|
||||
}
|
||||
@@ -236,7 +235,7 @@ func Test_generateTokenSecretName(t *testing.T) {
|
||||
want string
|
||||
}{
|
||||
{
|
||||
name: "no audiences, no expiration, no path",
|
||||
name: "empty token request",
|
||||
serviceAccountName: "default",
|
||||
tokenReq: &authv1.TokenRequest{
|
||||
Spec: authv1.TokenRequestSpec{},
|
||||
@@ -244,84 +243,166 @@ func Test_generateTokenSecretName(t *testing.T) {
|
||||
want: "k3k-default",
|
||||
},
|
||||
{
|
||||
name: "no audiences, with expiration",
|
||||
name: "with expiration",
|
||||
serviceAccountName: "default",
|
||||
tokenPath: "token",
|
||||
tokenReq: &authv1.TokenRequest{
|
||||
Spec: authv1.TokenRequestSpec{
|
||||
ExpirationSeconds: ptr.To(int64(3600)),
|
||||
ExpirationSeconds: new(int64(3600)),
|
||||
},
|
||||
},
|
||||
want: "k3k-default-3600-token",
|
||||
want: "k3k-default-3600",
|
||||
},
|
||||
{
|
||||
name: "with single audience and expiration",
|
||||
serviceAccountName: "my-sa",
|
||||
tokenPath: "token",
|
||||
name: "with audience",
|
||||
serviceAccountName: "my-serviceaccount",
|
||||
tokenReq: &authv1.TokenRequest{
|
||||
Spec: authv1.TokenRequestSpec{
|
||||
Audiences: []string{"api"},
|
||||
ExpirationSeconds: ptr.To(int64(3600)),
|
||||
Audiences: []string{"api", "vault"},
|
||||
},
|
||||
},
|
||||
want: "k3k-my-sa-api-3600-token",
|
||||
want: "k3k-my-serviceaccount-api-vault",
|
||||
},
|
||||
{
|
||||
name: "with multiple audiences and expiration",
|
||||
serviceAccountName: "my-sa",
|
||||
tokenPath: "token",
|
||||
name: "with empty audiences slice",
|
||||
serviceAccountName: "my-serviceaccount",
|
||||
tokenReq: &authv1.TokenRequest{
|
||||
Spec: authv1.TokenRequestSpec{
|
||||
Audiences: []string{},
|
||||
},
|
||||
},
|
||||
want: "k3k-my-serviceaccount",
|
||||
},
|
||||
{
|
||||
name: "with unsanitized audience",
|
||||
serviceAccountName: "my-serviceaccount",
|
||||
tokenReq: &authv1.TokenRequest{
|
||||
Spec: authv1.TokenRequestSpec{
|
||||
Audiences: []string{"https://unsanitized-audience"},
|
||||
},
|
||||
},
|
||||
want: "k3k-my-serviceaccount-https-unsanitized-audience-23d739",
|
||||
},
|
||||
{
|
||||
name: "with uppercase audience",
|
||||
serviceAccountName: "my-serviceaccount",
|
||||
tokenReq: &authv1.TokenRequest{
|
||||
Spec: authv1.TokenRequestSpec{
|
||||
Audiences: []string{"MyAudience"},
|
||||
},
|
||||
},
|
||||
want: "k3k-my-serviceaccount-myaudience-753a30",
|
||||
},
|
||||
{
|
||||
name: "with sanitized and unsanitized audiences",
|
||||
serviceAccountName: "my-serviceaccount",
|
||||
tokenReq: &authv1.TokenRequest{
|
||||
Spec: authv1.TokenRequestSpec{
|
||||
Audiences: []string{"api", "vault.example.com:8200"},
|
||||
},
|
||||
},
|
||||
want: "k3k-my-serviceaccount-api-vault.example.com-8200-2a7048",
|
||||
},
|
||||
{
|
||||
name: "with path",
|
||||
serviceAccountName: "my-serviceaccount",
|
||||
tokenPath: "token",
|
||||
tokenReq: &authv1.TokenRequest{},
|
||||
want: "k3k-my-serviceaccount-token",
|
||||
},
|
||||
{
|
||||
name: "with unsanitized path",
|
||||
serviceAccountName: "my-serviceaccount",
|
||||
tokenPath: "other-path/..",
|
||||
tokenReq: &authv1.TokenRequest{},
|
||||
want: "k3k-my-serviceaccount-other-path-0e329c",
|
||||
},
|
||||
{
|
||||
name: "with expiration and audience",
|
||||
serviceAccountName: "my-serviceaccount",
|
||||
tokenReq: &authv1.TokenRequest{
|
||||
Spec: authv1.TokenRequestSpec{
|
||||
ExpirationSeconds: new(int64(3600)),
|
||||
Audiences: []string{"api", "vault"},
|
||||
ExpirationSeconds: ptr.To(int64(3600)),
|
||||
},
|
||||
},
|
||||
want: "k3k-my-sa-api-vault-3600-token",
|
||||
want: "k3k-my-serviceaccount-api-vault-3600",
|
||||
},
|
||||
{
|
||||
name: "with audiences, no expiration",
|
||||
serviceAccountName: "my-sa",
|
||||
tokenPath: "vault-token",
|
||||
name: "with expiration and unsanitized audience",
|
||||
serviceAccountName: "my-serviceaccount",
|
||||
tokenReq: &authv1.TokenRequest{
|
||||
Spec: authv1.TokenRequestSpec{
|
||||
Audiences: []string{"api"},
|
||||
ExpirationSeconds: new(int64(3600)),
|
||||
Audiences: []string{"https://unsanitized-audience"},
|
||||
},
|
||||
},
|
||||
want: "k3k-my-sa-api-vault-token",
|
||||
want: "k3k-my-serviceaccount-https-unsanitized-audience-23d739-3600",
|
||||
},
|
||||
{
|
||||
name: "different paths produce different names",
|
||||
serviceAccountName: "my-sa",
|
||||
tokenPath: "other-path",
|
||||
name: "with expiration and path",
|
||||
serviceAccountName: "my-serviceaccount",
|
||||
tokenPath: "token",
|
||||
tokenReq: &authv1.TokenRequest{
|
||||
Spec: authv1.TokenRequestSpec{
|
||||
Audiences: []string{"api"},
|
||||
ExpirationSeconds: ptr.To(int64(3600)),
|
||||
ExpirationSeconds: new(int64(3600)),
|
||||
},
|
||||
},
|
||||
want: "k3k-my-sa-api-3600-other-path",
|
||||
want: "k3k-my-serviceaccount-3600-token",
|
||||
},
|
||||
{
|
||||
name: "long name gets truncated with hash",
|
||||
serviceAccountName: "my-very-long-service-account-name",
|
||||
tokenPath: "some-very-long-token-path-value",
|
||||
name: "with audience and path",
|
||||
serviceAccountName: "my-serviceaccount",
|
||||
tokenPath: "token",
|
||||
tokenReq: &authv1.TokenRequest{
|
||||
Spec: authv1.TokenRequestSpec{
|
||||
Audiences: []string{"some-very-long-audience-string"},
|
||||
ExpirationSeconds: ptr.To(int64(3600)),
|
||||
Audiences: []string{"api", "vault"},
|
||||
},
|
||||
},
|
||||
want: "k3k-my-serviceaccount-api-vault-token",
|
||||
},
|
||||
{
|
||||
name: "with unsanitized audience and unsanitized path",
|
||||
serviceAccountName: "my-serviceaccount",
|
||||
tokenPath: "other-path/..",
|
||||
tokenReq: &authv1.TokenRequest{
|
||||
Spec: authv1.TokenRequestSpec{
|
||||
Audiences: []string{"https://unsanitized-audience"},
|
||||
},
|
||||
},
|
||||
want: "k3k-my-serviceaccount-https-unsanitized-audience-23d739-o-0aa54",
|
||||
},
|
||||
{
|
||||
name: "with expiration, audience and path",
|
||||
serviceAccountName: "my-serviceaccount",
|
||||
tokenPath: "token",
|
||||
tokenReq: &authv1.TokenRequest{
|
||||
Spec: authv1.TokenRequestSpec{
|
||||
ExpirationSeconds: new(int64(3600)),
|
||||
Audiences: []string{"api", "vault"},
|
||||
},
|
||||
},
|
||||
want: "k3k-my-serviceaccount-api-vault-3600-token",
|
||||
},
|
||||
{
|
||||
name: "with expiration, unsanitized audience and unsanitized path",
|
||||
serviceAccountName: "my-serviceaccount",
|
||||
tokenPath: "other-path/..",
|
||||
tokenReq: &authv1.TokenRequest{
|
||||
Spec: authv1.TokenRequestSpec{
|
||||
ExpirationSeconds: new(int64(3600)),
|
||||
Audiences: []string{"https://unsanitized-audience"},
|
||||
},
|
||||
},
|
||||
// since the full name is longer than 63 characters, SafeConcatName() will truncate the
|
||||
// name and append the first 5 characters of the sha256-encoded full name
|
||||
want: "k3k-my-serviceaccount-https-unsanitized-audience-23d739-3-39d9d",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got := generateTokenSecretName(tt.serviceAccountName, tt.tokenPath, tt.tokenReq)
|
||||
if tt.want != "" {
|
||||
assert.Equal(t, tt.want, got)
|
||||
}
|
||||
|
||||
assert.Less(t, len(got), 64, "name should be under 64 characters")
|
||||
assert.Equal(t, tt.want, got)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"k8s.io/client-go/kubernetes"
|
||||
@@ -14,6 +15,7 @@ import (
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
|
||||
"github.com/rancher/k3k/k3k-kubelet/translate"
|
||||
"github.com/rancher/k3k/pkg/controller"
|
||||
fwk3k "github.com/rancher/k3k/tests/framework/k3k"
|
||||
|
||||
. "github.com/onsi/ginkgo/v2"
|
||||
@@ -546,4 +548,106 @@ var _ = Context("In a shared cluster", Label(podTestsLabel), Ordered, func() {
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
When("creating a Pod with a projected serviceaccount token", func() {
|
||||
var (
|
||||
pod *corev1.Pod
|
||||
serviceAccountName = "sa-nginx"
|
||||
serviceAccountTokenExp = int64(3600)
|
||||
serviceAccountTokenPath = "token"
|
||||
serviceAccountTokenAudience = "https://kubernetes.default.svc.cluster.local"
|
||||
serviceAccountTokenSanitizedAudience = "https-kubernetes.default.svc.cluster.local-4f3059"
|
||||
)
|
||||
|
||||
BeforeAll(func(ctx context.Context) {
|
||||
var err error
|
||||
|
||||
serviceaccount := &corev1.ServiceAccount{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: serviceAccountName,
|
||||
Namespace: "default",
|
||||
},
|
||||
}
|
||||
serviceaccount, err = virtualCluster.Client.CoreV1().ServiceAccounts(serviceaccount.Namespace).Create(ctx, serviceaccount, metav1.CreateOptions{})
|
||||
Expect(err).To(Not(HaveOccurred()))
|
||||
|
||||
pod = &corev1.Pod{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
GenerateName: "nginx-",
|
||||
Namespace: "default",
|
||||
},
|
||||
Spec: corev1.PodSpec{
|
||||
AutomountServiceAccountToken: new(false),
|
||||
Containers: []corev1.Container{{
|
||||
Name: "nginx",
|
||||
Image: "nginx",
|
||||
VolumeMounts: []corev1.VolumeMount{
|
||||
{
|
||||
Name: "projected-serviceaccount-token-vol",
|
||||
MountPath: "/var/run/secrets/kubernetes.io/serviceaccount",
|
||||
},
|
||||
},
|
||||
}},
|
||||
ServiceAccountName: serviceaccount.Name,
|
||||
Volumes: []corev1.Volume{
|
||||
{
|
||||
Name: "projected-serviceaccount-token-vol",
|
||||
VolumeSource: corev1.VolumeSource{
|
||||
Projected: &corev1.ProjectedVolumeSource{
|
||||
Sources: []corev1.VolumeProjection{
|
||||
{
|
||||
ServiceAccountToken: &corev1.ServiceAccountTokenProjection{
|
||||
Audience: serviceAccountTokenAudience,
|
||||
ExpirationSeconds: new(serviceAccountTokenExp),
|
||||
Path: serviceAccountTokenPath,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
pod, err = virtualCluster.Client.CoreV1().Pods(pod.Namespace).Create(ctx, pod, metav1.CreateOptions{})
|
||||
Expect(err).To(Not(HaveOccurred()))
|
||||
})
|
||||
|
||||
It("should have translated projected serviceaccount token to secret", func(ctx context.Context) {
|
||||
Eventually(func(g Gomega) {
|
||||
hostPodName := translator.NamespacedName(pod)
|
||||
|
||||
// there is no way to get the result of the token request from the API, so the
|
||||
// name of the secret holding it has to be rebuilt the same way the provider does
|
||||
virtualSecretName := controller.SafeConcatNameWithPrefix([]string{serviceAccountName, serviceAccountTokenSanitizedAudience, strconv.FormatInt(serviceAccountTokenExp, 10), serviceAccountTokenPath}...)
|
||||
hostSecret := &corev1.Secret{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: virtualSecretName,
|
||||
Namespace: pod.Namespace,
|
||||
},
|
||||
}
|
||||
translator.TranslateTo(hostSecret)
|
||||
|
||||
hostPod, err := k8s.CoreV1().Pods(hostPodName.Namespace).Get(ctx, hostPodName.Name, metav1.GetOptions{})
|
||||
g.Expect(err).NotTo(HaveOccurred())
|
||||
g.Expect(hostPod.Spec.Volumes).To(HaveLen(1))
|
||||
g.Expect(hostPod.Spec.Volumes[0].Name).To(Equal("projected-serviceaccount-token-vol"))
|
||||
g.Expect(hostPod.Spec.Volumes[0].Projected).To(Not(BeNil()))
|
||||
g.Expect(hostPod.Spec.Volumes[0].Projected.Sources).To(HaveLen(1))
|
||||
g.Expect(hostPod.Spec.Volumes[0].Projected.Sources[0].Secret).To(Not(BeNil()))
|
||||
g.Expect(hostPod.Spec.Volumes[0].Projected.Sources[0].ServiceAccountToken).To(BeNil())
|
||||
g.Expect(hostPod.Spec.Volumes[0].Projected.Sources[0].Secret.Name).To(Equal(hostSecret.Name))
|
||||
|
||||
// verifying that the secret is created on the host cluster
|
||||
hostSecret, err = k8s.CoreV1().Secrets(hostPodName.Namespace).Get(ctx, hostSecret.Name, metav1.GetOptions{})
|
||||
g.Expect(err).NotTo(HaveOccurred())
|
||||
g.Expect(hostSecret.Data).NotTo(BeNil())
|
||||
g.Expect(hostSecret.Data[serviceAccountTokenPath]).NotTo(BeEmpty())
|
||||
}).
|
||||
WithPolling(time.Second).
|
||||
WithTimeout(time.Minute).
|
||||
Should(Succeed())
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user