Skip to content

Commit ff9f91e

Browse files
authored
Merge pull request #12517 from fabriziopandini/add-mTLS-to-runtime-extensions
✨ Add mTLS support to runtime extension server and client
2 parents fb0e2da + be7a741 commit ff9f91e

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

@@ -840,6 +843,102 @@ func TestClient_CallExtension(t *testing.T) {
840843
}
841844
}
842845

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

main.go

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,8 @@ var (
114114
webhookCertDir string
115115
webhookCertName string
116116
webhookKeyName string
117+
runtimeExtensionCertFile string
118+
runtimeExtensionKeyFile string
117119
healthAddr string
118120
managerOptions = flags.ManagerOptions{}
119121
logOptions = logs.NewOptions()
@@ -264,10 +266,16 @@ func InitFlags(fs *pflag.FlagSet) {
264266
"Webhook cert dir.")
265267

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

269271
fs.StringVar(&webhookKeyName, "webhook-key-name", "tls.key",
270-
"Webhook key name.")
272+
"Name of the file for webhook's server key; the file must be placed under webhook-cert-dir.")
273+
274+
fs.StringVar(&runtimeExtensionCertFile, "runtime-extension-client-cert-file", "",
275+
"Path of the PEM-encoded client certificate to be used when calling runtime extensions.")
276+
277+
fs.StringVar(&runtimeExtensionKeyFile, "runtime-extension-client-key-file", "",
278+
"Path of the PEM-encoded client key to be used when calling runtime extensions.")
271279

272280
fs.StringVar(&healthAddr, "health-addr", ":9440",
273281
"The address the health endpoint binds to.")
@@ -536,6 +544,8 @@ func setupReconcilers(ctx context.Context, mgr ctrl.Manager, watchNamespaces map
536544
if feature.Gates.Enabled(feature.RuntimeSDK) {
537545
// This is the creation of the runtimeClient for the controllers, embedding a shared catalog and registry instance.
538546
runtimeClient = internalruntimeclient.New(internalruntimeclient.Options{
547+
CertFile: runtimeExtensionCertFile,
548+
KeyFile: runtimeExtensionKeyFile,
539549
Catalog: catalog,
540550
Registry: runtimeregistry.New(),
541551
Client: mgr.GetClient(),

0 commit comments

Comments
 (0)