Skip to content

Commit 38440f7

Browse files
k8s-infra-cherrypick-robotfabriziopandinisbueringer
authored
[release-1.10] ✨ Add mTLS support to runtime extension server and client (#12519)
* Add mTLS support to runtime extension server and client * Fix unit test for release-1.10 --------- Co-authored-by: fabriziopandini <fpandini@vmware.com> Co-authored-by: Stefan Bueringer <buringerst@vmware.com>
1 parent 737edc8 commit 38440f7

File tree

4 files changed

+137
-9
lines changed

4 files changed

+137
-9
lines changed

exp/runtime/server/server.go

Lines changed: 12 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,10 @@ type Options struct {
8080
// Note: This option is only used when TLSOpts does not set GetCertificate.
8181
KeyName string
8282

83+
// ClientCAName is the CA certificate name which server used to verify remote(client)'s certificate.
84+
// Defaults to "", which means server does not verify client's certificate.
85+
ClientCAName string
86+
8387
// TLSOpts is used to allow configuring the TLS config used for the server.
8488
// This also allows providing a certificate via GetCertificate.
8589
TLSOpts []func(*tls.Config)
@@ -105,13 +109,14 @@ func New(options Options) (*Server, error) {
105109

106110
webhookServer := webhook.NewServer(
107111
webhook.Options{
108-
Port: options.Port,
109-
Host: options.Host,
110-
CertDir: options.CertDir,
111-
CertName: options.CertName,
112-
KeyName: options.KeyName,
113-
TLSOpts: options.TLSOpts,
114-
WebhookMux: http.NewServeMux(),
112+
Port: options.Port,
113+
Host: options.Host,
114+
ClientCAName: options.ClientCAName,
115+
CertDir: options.CertDir,
116+
CertName: options.CertName,
117+
KeyName: options.KeyName,
118+
TLSOpts: options.TLSOpts,
119+
WebhookMux: http.NewServeMux(),
115120
},
116121
)
117122

internal/runtime/client/client.go

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,8 @@ const defaultDiscoveryTimeout = 10 * time.Second
6060

6161
// Options are creation options for a Client.
6262
type Options struct {
63+
CertFile string // Path of the PEM-encoded client certificate.
64+
KeyFile string // Path of the PEM-encoded client key.
6365
Catalog *runtimecatalog.Catalog
6466
Registry runtimeregistry.ExtensionRegistry
6567
Client ctrlclient.Client
@@ -68,6 +70,8 @@ type Options struct {
6870
// New returns a new Client.
6971
func New(options Options) runtimeclient.Client {
7072
return &client{
73+
certFile: options.CertFile,
74+
keyFile: options.KeyFile,
7175
catalog: options.Catalog,
7276
registry: options.Registry,
7377
client: options.Client,
@@ -77,6 +81,8 @@ func New(options Options) runtimeclient.Client {
7781
var _ runtimeclient.Client = &client{}
7882

7983
type client struct {
84+
certFile string
85+
keyFile string
8086
catalog *runtimecatalog.Catalog
8187
registry runtimeregistry.ExtensionRegistry
8288
client ctrlclient.Client
@@ -102,6 +108,8 @@ func (c *client) Discover(ctx context.Context, extensionConfig *runtimev1.Extens
102108
request := &runtimehooksv1.DiscoveryRequest{}
103109
response := &runtimehooksv1.DiscoveryResponse{}
104110
opts := &httpCallOptions{
111+
certFile: c.certFile,
112+
keyFile: c.keyFile,
105113
catalog: c.catalog,
106114
config: extensionConfig.Spec.ClientConfig,
107115
registrationGVH: hookGVH,
@@ -329,6 +337,8 @@ func (c *client) CallExtension(ctx context.Context, hook runtimecatalog.Hook, fo
329337
}
330338

331339
httpOpts := &httpCallOptions{
340+
certFile: c.certFile,
341+
keyFile: c.keyFile,
332342
catalog: c.catalog,
333343
config: registration.ClientConfig,
334344
registrationGVH: registration.GroupVersionHook,
@@ -396,6 +406,8 @@ func cloneAndAddSettings(request runtimehooksv1.RequestObject, registrationSetti
396406
}
397407

398408
type httpCallOptions struct {
409+
certFile string
410+
keyFile string
399411
catalog *runtimecatalog.Catalog
400412
config runtimev1.ClientConfig
401413
registrationGVH runtimecatalog.GroupVersionHook
@@ -484,6 +496,8 @@ func httpCall(ctx context.Context, request, response runtime.Object, opts *httpC
484496
client := http.DefaultClient
485497
tlsConfig, err := transport.TLSConfigFor(&transport.Config{
486498
TLS: transport.TLSConfig{
499+
CertFile: opts.certFile,
500+
KeyFile: opts.keyFile,
487501
CAData: opts.config.CABundle,
488502
ServerName: extensionURL.Hostname(),
489503
},

internal/runtime/client/client_test.go

Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,10 +19,13 @@ package client
1919
import (
2020
"context"
2121
"crypto/tls"
22+
"crypto/x509"
2223
"encoding/json"
2324
"fmt"
2425
"net/http"
2526
"net/http/httptest"
27+
"os"
28+
"path/filepath"
2629
"regexp"
2730
"testing"
2831

@@ -842,6 +845,102 @@ func TestClient_CallExtension(t *testing.T) {
842845
}
843846
}
844847

848+
func TestClient_CallExtensionWithClientAuthentication(t *testing.T) {
849+
ns := &corev1.Namespace{
850+
TypeMeta: metav1.TypeMeta{
851+
Kind: "Namespace",
852+
APIVersion: corev1.SchemeGroupVersion.String(),
853+
},
854+
ObjectMeta: metav1.ObjectMeta{
855+
Name: "foo",
856+
},
857+
}
858+
859+
validExtensionHandlerWithFailPolicy := runtimev1.ExtensionConfig{
860+
ObjectMeta: metav1.ObjectMeta{
861+
ResourceVersion: "15",
862+
},
863+
Spec: runtimev1.ExtensionConfigSpec{
864+
ClientConfig: runtimev1.ClientConfig{
865+
// Set a fake URL, in test cases where we start the test server the URL will be overridden.
866+
URL: ptr.To("https://127.0.0.1/"),
867+
CABundle: testcerts.CACert,
868+
},
869+
NamespaceSelector: &metav1.LabelSelector{},
870+
},
871+
Status: runtimev1.ExtensionConfigStatus{
872+
Handlers: []runtimev1.ExtensionHandler{
873+
{
874+
Name: "valid-extension",
875+
RequestHook: runtimev1.GroupVersionHook{
876+
APIVersion: fakev1alpha1.GroupVersion.String(),
877+
Hook: "FakeHook",
878+
},
879+
TimeoutSeconds: ptr.To[int32](1),
880+
FailurePolicy: ptr.To(runtimev1.FailurePolicyFail),
881+
},
882+
},
883+
},
884+
}
885+
886+
g := NewWithT(t)
887+
888+
tmpDir := t.TempDir()
889+
clientCertFile := filepath.Join(tmpDir, "tls.crt")
890+
g.Expect(os.WriteFile(clientCertFile, testcerts.ClientCert, 0600)).To(Succeed())
891+
clientKeyFile := filepath.Join(tmpDir, "tls.key")
892+
g.Expect(os.WriteFile(clientKeyFile, testcerts.ClientKey, 0600)).To(Succeed())
893+
894+
var serverCallCount int
895+
srv := createSecureTestServer(testServerConfig{
896+
start: true,
897+
responses: map[string]testServerResponse{
898+
"/*": response(runtimehooksv1.ResponseStatusSuccess),
899+
},
900+
}, func() {
901+
serverCallCount++
902+
})
903+
904+
// Setup the runtime extension server so it requires client authentication with certificates signed by a given CA.
905+
certpool := x509.NewCertPool()
906+
certpool.AppendCertsFromPEM(testcerts.CACert)
907+
srv.TLS.ClientAuth = tls.RequireAndVerifyClientCert
908+
srv.TLS.ClientCAs = certpool
909+
910+
srv.StartTLS()
911+
defer srv.Close()
912+
913+
// Set the URL to the real address of the test server.
914+
validExtensionHandlerWithFailPolicy.Spec.ClientConfig.URL = ptr.To(fmt.Sprintf("https://%s/", srv.Listener.Addr().String()))
915+
916+
cat := runtimecatalog.New()
917+
_ = fakev1alpha1.AddToCatalog(cat)
918+
_ = fakev1alpha2.AddToCatalog(cat)
919+
fakeClient := fake.NewClientBuilder().
920+
WithObjects(ns).
921+
Build()
922+
923+
c := New(Options{
924+
// Add client authentication credentials to the client
925+
CertFile: clientCertFile,
926+
KeyFile: clientKeyFile,
927+
Catalog: cat,
928+
Registry: registry([]runtimev1.ExtensionConfig{validExtensionHandlerWithFailPolicy}),
929+
Client: fakeClient,
930+
})
931+
932+
obj := &clusterv1.Cluster{
933+
ObjectMeta: metav1.ObjectMeta{
934+
Name: "cluster",
935+
Namespace: "foo",
936+
},
937+
}
938+
// Call once without caching.
939+
err := c.CallExtension(context.Background(), fakev1alpha1.FakeHook, obj, "valid-extension", &fakev1alpha1.FakeRequest{}, &fakev1alpha1.FakeResponse{})
940+
g.Expect(err).ToNot(HaveOccurred())
941+
g.Expect(serverCallCount).To(Equal(1))
942+
}
943+
845944
func cacheKeyFunc(extensionName, extensionConfigResourceVersion string, request runtimehooksv1.RequestObject) string {
846945
// Note: extensionName is identical to the value of the name parameter passed into CallExtension.
847946
s := fmt.Sprintf("%s-%s", extensionName, extensionConfigResourceVersion)

main.go

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,8 @@ var (
110110
webhookCertDir string
111111
webhookCertName string
112112
webhookKeyName string
113+
runtimeExtensionCertFile string
114+
runtimeExtensionKeyFile string
113115
healthAddr string
114116
managerOptions = flags.ManagerOptions{}
115117
logOptions = logs.NewOptions()
@@ -259,10 +261,16 @@ func InitFlags(fs *pflag.FlagSet) {
259261
"Webhook cert dir.")
260262

261263
fs.StringVar(&webhookCertName, "webhook-cert-name", "tls.crt",
262-
"Webhook cert name.")
264+
"Name of the file for webhook's server certificate; the file must be placed under webhook-cert-dir.")
263265

264266
fs.StringVar(&webhookKeyName, "webhook-key-name", "tls.key",
265-
"Webhook key name.")
267+
"Name of the file for webhook's server key; the file must be placed under webhook-cert-dir.")
268+
269+
fs.StringVar(&runtimeExtensionCertFile, "runtime-extension-client-cert-file", "",
270+
"Path of the PEM-encoded client certificate to be used when calling runtime extensions.")
271+
272+
fs.StringVar(&runtimeExtensionKeyFile, "runtime-extension-client-key-file", "",
273+
"Path of the PEM-encoded client key to be used when calling runtime extensions.")
266274

267275
fs.StringVar(&healthAddr, "health-addr", ":9440",
268276
"The address the health endpoint binds to.")
@@ -531,6 +539,8 @@ func setupReconcilers(ctx context.Context, mgr ctrl.Manager, watchNamespaces map
531539
if feature.Gates.Enabled(feature.RuntimeSDK) {
532540
// This is the creation of the runtimeClient for the controllers, embedding a shared catalog and registry instance.
533541
runtimeClient = internalruntimeclient.New(internalruntimeclient.Options{
542+
CertFile: runtimeExtensionCertFile,
543+
KeyFile: runtimeExtensionKeyFile,
534544
Catalog: catalog,
535545
Registry: runtimeregistry.New(),
536546
Client: mgr.GetClient(),

0 commit comments

Comments
 (0)