diff --git a/deployments/openssl/portforwarder.yml b/deployments/openssl/portforwarder.yml index a8021696c..367b0da21 100644 --- a/deployments/openssl/portforwarder.yml +++ b/deployments/openssl/portforwarder.yml @@ -27,3 +27,33 @@ spec: memory: 50Mi limits: memory: 50Mi +--- +apiVersion: v1 +kind: Pod +metadata: + name: port-forwarder-openssl-frontend + namespace: edg-default + labels: + app.kubernetes.io/name: port-forwarder-openssl-frontend +spec: + containers: + - name: port-forwarder + image: "ghcr.io/edgelesssys/contrast/port-forwarder:latest" + env: + - name: LISTEN_PORT + value: "443" + - name: FORWARD_HOST + value: openssl-frontend + - name: FORWARD_PORT + value: "443" + command: + - /bin/bash + - "-c" + - echo Starting port-forward with socat; exec socat -d -d TCP-LISTEN:${LISTEN_PORT},fork TCP:${FORWARD_HOST}:${FORWARD_PORT} + ports: + - containerPort: 443 + resources: + requests: + memory: 50Mi + limits: + memory: 50Mi diff --git a/e2e/internal/kubeclient/portforward.go b/e2e/internal/kubeclient/portforward.go new file mode 100644 index 000000000..f6862191e --- /dev/null +++ b/e2e/internal/kubeclient/portforward.go @@ -0,0 +1,74 @@ +package kubeclient + +import ( + "context" + "fmt" + "net/http" + + "k8s.io/client-go/tools/portforward" + "k8s.io/client-go/transport/spdy" +) + +// PortForwardPod starts a port forward to the selected pod. +// +// On success, the function returns a TCP address that clients can connect to and a function to +// cancel the port forwarding. +func (k *Kubeclient) PortForwardPod(ctx context.Context, namespace, podName, remotePort string) (string, func(), error) { + // This channel sends a stop request to the portforwarding goroutine. + stopCh := make(chan struct{}, 1) + // The portforwarding goroutine closes this channel when it's ready. + readyCh := make(chan struct{}) + // Any error returned by the background port-forwarder is sent to this channel. + errorCh := make(chan error) + + // Ports are forwarded by upgrading this POST request to a SPDY connection. + req := k.client.CoreV1().RESTClient().Post(). + Resource("pods"). + Namespace(namespace). + Name(podName). + SubResource("portforward") + + transport, upgrader, err := spdy.RoundTripperFor(k.config) + if err != nil { + return "", nil, fmt.Errorf("creating round tripper: %w", err) + } + + dialer := spdy.NewDialer(upgrader, &http.Client{Transport: transport}, http.MethodPost, req.URL()) + + fw, err := portforward.NewOnAddresses( + dialer, + []string{"localhost"}, + []string{fmt.Sprintf("0:%s", remotePort)}, + stopCh, readyCh, + nil, nil, + ) + if err != nil { + return "", nil, fmt.Errorf("creating portforwarder: %w", err) + } + + go func() { + if err := fw.ForwardPorts(); err != nil { + errorCh <- err + } + }() + + select { + case <-readyCh: + ports, err := fw.GetPorts() + if err != nil { + close(stopCh) + return "", nil, fmt.Errorf("getting ports: %w", err) + } + cleanUp := func() { + close(stopCh) + } + return fmt.Sprintf("localhost:%d", ports[0].Local), cleanUp, nil + + case <-ctx.Done(): + close(stopCh) + return "", nil, fmt.Errorf("waiting for port forward to be ready: %w", ctx.Err()) + case err := <-errorCh: + close(stopCh) + return "", nil, fmt.Errorf("background port-forwarding routine failed: %w", err) + } +} diff --git a/e2e/openssl/openssl_test.go b/e2e/openssl/openssl_test.go index 16f70c6d3..9e6791874 100644 --- a/e2e/openssl/openssl_test.go +++ b/e2e/openssl/openssl_test.go @@ -5,6 +5,7 @@ package openssl import ( "context" + "crypto/tls" "os" "testing" "time" @@ -16,10 +17,10 @@ import ( // namespace the tests are executed in. const namespaceEnv = "K8S_NAMESPACE" -// TestOpenssl verifies that the certificates minted by the coordinator are accepted by OpenSSL in server and client mode. +// TestBackend verifies that the certificates minted by the coordinator are accepted by OpenSSL in server and client mode. // // The test expects deployments/openssl to be available in the cluster (manifest set and workloads ready). -func TestOpenSSL(t *testing.T) { +func TestFrontend2Backend(t *testing.T) { require := require.New(t) ctx, cancel := context.WithTimeout(context.Background(), 1*time.Minute) @@ -45,3 +46,32 @@ func TestOpenSSL(t *testing.T) { t.Log(stdout) require.NoError(err, "stderr: %q", stderr) } + +// TestFrontend verifies the certificate used by the OpenSSL frontend comes from the coordinator. +func TestFrontend(t *testing.T) { + require := require.New(t) + + ctx, cancel := context.WithTimeout(context.Background(), 1*time.Minute) + defer cancel() + + c := kubeclient.NewForTest(t) + + namespace := os.Getenv(namespaceEnv) + require.NotEmpty(namespace, "environment variable %q must be set", namespaceEnv) + + addr, cancelPortForward, err := c.PortForwardPod(ctx, namespace, "port-forwarder-openssl-frontend", "443") + require.NoError(err) + defer cancelPortForward() + + // TODO(burgerdev): properly test chain to mesh root + dialer := &tls.Dialer{Config: &tls.Config{InsecureSkipVerify: true}} + conn, err := dialer.DialContext(ctx, "tcp", addr) + require.NoError(err) + tlsConn := conn.(*tls.Conn) + + var names []string + for _, cert := range tlsConn.ConnectionState().PeerCertificates { + names = append(names, cert.Subject.CommonName) + } + require.Contains(names, "openssl-frontend") +}