kubelet: Support ClusterTrustBundlePEM projections

This commit is contained in:
Taahir Ahmed
2022-10-21 23:13:42 -07:00
parent e83baddbb1
commit 1ebe5774d0
17 changed files with 1322 additions and 34 deletions

View File

@@ -333,6 +333,13 @@ type KubeletVolumeHost interface {
WaitForCacheSync() error
// Returns hostutil.HostUtils
GetHostUtil() hostutil.HostUtils
// Returns trust anchors from the named ClusterTrustBundle.
GetTrustAnchorsByName(name string, allowMissing bool) ([]byte, error)
// Returns trust anchors from the ClusterTrustBundles selected by signer
// name and label selector.
GetTrustAnchorsBySigner(signerName string, labelSelector *metav1.LabelSelector, allowMissing bool) ([]byte, error)
}
// AttachDetachVolumeHost is a AttachDetach Controller specific interface that plugins can use

View File

@@ -45,6 +45,7 @@ const (
type projectedPlugin struct {
host volume.VolumeHost
kvHost volume.KubeletVolumeHost
getSecret func(namespace, name string) (*v1.Secret, error)
getConfigMap func(namespace, name string) (*v1.ConfigMap, error)
getServiceAccountToken func(namespace, name string, tr *authenticationv1.TokenRequest) (*authenticationv1.TokenRequest, error)
@@ -69,6 +70,7 @@ func getPath(uid types.UID, volName string, host volume.VolumeHost) string {
func (plugin *projectedPlugin) Init(host volume.VolumeHost) error {
plugin.host = host
plugin.kvHost = host.(volume.KubeletVolumeHost)
plugin.getSecret = host.GetSecretFunc()
plugin.getConfigMap = host.GetConfigMapFunc()
plugin.getServiceAccountToken = host.GetServiceAccountTokenFunc()
@@ -353,6 +355,42 @@ func (s *projectedVolumeMounter) collectData(mounterArgs volume.MounterArgs) (ma
Mode: mode,
FsUser: mounterArgs.FsUser,
}
case source.ClusterTrustBundle != nil:
allowEmpty := false
if source.ClusterTrustBundle.Optional != nil && *source.ClusterTrustBundle.Optional {
allowEmpty = true
}
var trustAnchors []byte
if source.ClusterTrustBundle.Name != nil {
var err error
trustAnchors, err = s.plugin.kvHost.GetTrustAnchorsByName(*source.ClusterTrustBundle.Name, allowEmpty)
if err != nil {
errlist = append(errlist, err)
continue
}
} else if source.ClusterTrustBundle.SignerName != nil {
var err error
trustAnchors, err = s.plugin.kvHost.GetTrustAnchorsBySigner(*source.ClusterTrustBundle.SignerName, source.ClusterTrustBundle.LabelSelector, allowEmpty)
if err != nil {
errlist = append(errlist, err)
continue
}
} else {
errlist = append(errlist, fmt.Errorf("ClusterTrustBundle projection requires either name or signerName to be set"))
continue
}
mode := *s.source.DefaultMode
if mounterArgs.FsUser != nil || mounterArgs.FsGroup != nil {
mode = 0600
}
payload[source.ClusterTrustBundle.Path] = volumeutil.FileProjection{
Data: trustAnchors,
Mode: mode,
FsUser: mounterArgs.FsUser,
}
}
}
return payload, utilerrors.NewAggregate(errlist)

View File

@@ -17,7 +17,13 @@ limitations under the License.
package projected
import (
"crypto/ed25519"
"crypto/rand"
"crypto/x509"
"crypto/x509/pkix"
"encoding/pem"
"fmt"
"math/big"
"os"
"path/filepath"
"reflect"
@@ -26,6 +32,7 @@ import (
"github.com/google/go-cmp/cmp"
authenticationv1 "k8s.io/api/authentication/v1"
certificatesv1alpha1 "k8s.io/api/certificates/v1alpha1"
v1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
@@ -872,13 +879,172 @@ func TestCollectDataWithServiceAccountToken(t *testing.T) {
}
}
func TestCollectDataWithClusterTrustBundle(t *testing.T) {
// This test is limited by the use of a fake clientset and volume host. We
// can't meaningfully test that label selectors end up doing the correct
// thing for example.
goodCert1 := mustMakeRoot(t, "root1")
testCases := []struct {
name string
source v1.ProjectedVolumeSource
bundles []runtime.Object
fsUser *int64
fsGroup *int64
wantPayload map[string]util.FileProjection
wantErr error
}{
{
name: "single ClusterTrustBundle by name",
source: v1.ProjectedVolumeSource{
Sources: []v1.VolumeProjection{
{
ClusterTrustBundle: &v1.ClusterTrustBundleProjection{
Name: utilptr.String("foo"),
Path: "bundle.pem",
},
},
},
DefaultMode: utilptr.Int32(0644),
},
bundles: []runtime.Object{
&certificatesv1alpha1.ClusterTrustBundle{
ObjectMeta: metav1.ObjectMeta{
Name: "foo",
},
Spec: certificatesv1alpha1.ClusterTrustBundleSpec{
TrustBundle: string(goodCert1),
},
},
},
wantPayload: map[string]util.FileProjection{
"bundle.pem": {
Data: []byte(goodCert1),
Mode: 0644,
},
},
},
{
name: "single ClusterTrustBundle by signer name",
source: v1.ProjectedVolumeSource{
Sources: []v1.VolumeProjection{
{
ClusterTrustBundle: &v1.ClusterTrustBundleProjection{
SignerName: utilptr.String("foo.example/bar"), // Note: fake client doesn't understand selection by signer name.
LabelSelector: &metav1.LabelSelector{
MatchLabels: map[string]string{
"key": "non-value", // Note: fake client doesn't actually act on label selectors.
},
},
Path: "bundle.pem",
},
},
},
DefaultMode: utilptr.Int32(0644),
},
bundles: []runtime.Object{
&certificatesv1alpha1.ClusterTrustBundle{
ObjectMeta: metav1.ObjectMeta{
Name: "foo:example:bar",
Labels: map[string]string{
"key": "value",
},
},
Spec: certificatesv1alpha1.ClusterTrustBundleSpec{
SignerName: "foo.example/bar",
TrustBundle: string(goodCert1),
},
},
},
wantPayload: map[string]util.FileProjection{
"bundle.pem": {
Data: []byte(goodCert1),
Mode: 0644,
},
},
},
{
name: "single ClusterTrustBundle by name, non-default mode",
source: v1.ProjectedVolumeSource{
Sources: []v1.VolumeProjection{
{
ClusterTrustBundle: &v1.ClusterTrustBundleProjection{
Name: utilptr.String("foo"),
Path: "bundle.pem",
},
},
},
DefaultMode: utilptr.Int32(0600),
},
bundles: []runtime.Object{
&certificatesv1alpha1.ClusterTrustBundle{
ObjectMeta: metav1.ObjectMeta{
Name: "foo",
},
Spec: certificatesv1alpha1.ClusterTrustBundleSpec{
TrustBundle: string(goodCert1),
},
},
},
wantPayload: map[string]util.FileProjection{
"bundle.pem": {
Data: []byte(goodCert1),
Mode: 0600,
},
},
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
pod := &v1.Pod{
ObjectMeta: metav1.ObjectMeta{
Namespace: "default",
UID: types.UID("test_pod_uid"),
},
Spec: v1.PodSpec{ServiceAccountName: "foo"},
}
client := fake.NewSimpleClientset(tc.bundles...)
tempDir, host := newTestHost(t, client)
defer os.RemoveAll(tempDir)
var myVolumeMounter = projectedVolumeMounter{
projectedVolume: &projectedVolume{
sources: tc.source.Sources,
podUID: pod.UID,
plugin: &projectedPlugin{
host: host,
kvHost: host.(volume.KubeletVolumeHost),
},
},
source: tc.source,
pod: pod,
}
gotPayload, err := myVolumeMounter.collectData(volume.MounterArgs{FsUser: tc.fsUser, FsGroup: tc.fsGroup})
if err != nil {
t.Fatalf("Unexpected failure making payload: %v", err)
}
if diff := cmp.Diff(tc.wantPayload, gotPayload); diff != "" {
t.Fatalf("Bad payload; diff (-want +got)\n%s", diff)
}
})
}
}
func newTestHost(t *testing.T, clientset clientset.Interface) (string, volume.VolumeHost) {
tempDir, err := os.MkdirTemp("", "projected_volume_test.")
if err != nil {
t.Fatalf("can't make a temp rootdir: %v", err)
}
return tempDir, volumetest.NewFakeVolumeHost(t, tempDir, clientset, emptydir.ProbeVolumePlugins())
return tempDir, volumetest.NewFakeKubeletVolumeHost(t, tempDir, clientset, emptydir.ProbeVolumePlugins())
}
func TestCanSupport(t *testing.T) {
@@ -1322,3 +1488,30 @@ func doTestCleanAndTeardown(plugin volume.VolumePlugin, podUID types.UID, testVo
t.Errorf("TearDown() failed: %v", err)
}
}
func mustMakeRoot(t *testing.T, cn string) string {
pub, priv, err := ed25519.GenerateKey(rand.Reader)
if err != nil {
t.Fatalf("Error while generating key: %v", err)
}
template := &x509.Certificate{
SerialNumber: big.NewInt(0),
Subject: pkix.Name{
CommonName: cn,
},
IsCA: true,
BasicConstraintsValid: true,
}
cert, err := x509.CreateCertificate(rand.Reader, template, template, pub, priv)
if err != nil {
t.Fatalf("Error while making certificate: %v", err)
}
return string(pem.EncodeToMemory(&pem.Block{
Type: "CERTIFICATE",
Headers: nil,
Bytes: cert,
}))
}

View File

@@ -17,6 +17,7 @@ limitations under the License.
package testing
import (
"bytes"
"context"
"fmt"
"net"
@@ -437,3 +438,30 @@ func (f *fakeKubeletVolumeHost) WaitForCacheSync() error {
func (f *fakeKubeletVolumeHost) GetHostUtil() hostutil.HostUtils {
return f.hostUtil
}
func (f *fakeKubeletVolumeHost) GetTrustAnchorsByName(name string, allowMissing bool) ([]byte, error) {
ctb, err := f.kubeClient.CertificatesV1alpha1().ClusterTrustBundles().Get(context.Background(), name, metav1.GetOptions{})
if err != nil {
return nil, fmt.Errorf("while getting ClusterTrustBundle %s: %w", name, err)
}
return []byte(ctb.Spec.TrustBundle), nil
}
// Note: we do none of the deduplication and sorting that the real deal should do.
func (f *fakeKubeletVolumeHost) GetTrustAnchorsBySigner(signerName string, labelSelector *metav1.LabelSelector, allowMissing bool) ([]byte, error) {
ctbList, err := f.kubeClient.CertificatesV1alpha1().ClusterTrustBundles().List(context.Background(), metav1.ListOptions{})
if err != nil {
return nil, fmt.Errorf("while listing all ClusterTrustBundles: %w", err)
}
fullSet := bytes.Buffer{}
for i, ctb := range ctbList.Items {
fullSet.WriteString(ctb.Spec.TrustBundle)
if i != len(ctbList.Items)-1 {
fullSet.WriteString("\n")
}
}
return fullSet.Bytes(), nil
}