Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Add opt to configure mTLS for host clients #183

Merged
merged 5 commits into from
Jun 28, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 28 additions & 0 deletions api/v1alpha1/microvmcluster_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,34 @@ type MicrovmClusterSpec struct {
// MicrovmProxy is the proxy server details to use when calling the microvm service. This is an
// alteranative to using the http proxy environment variables and applied purely to the grpc service.
MicrovmProxy *Proxy `json:"microvmProxy,omitempty"`

// mTLS Configuration:
//
// It is recommended that each flintlock host is configured with its own cert
// signed by a common CA, and set to use mTLS.
// The CAPMVM client should be provided with the CA, and a client cert and key
// signed by that CA.
// TLSSecretRef is a reference to the name of a secret which contains TLS cert information
// for connecting to Flintlock hosts.
// The secret should be created in the same namespace as the MicroVMCluster.
// The secret should be of type kubernetes.io/tls https://kubernetes.io/docs/concepts/configuration/secret/#tls-secrets
// with the addition of a ca.crt key.
//
// apiVersion: v1
// kind: Secret
// metadata:
// name: secret-tls
Callisto13 marked this conversation as resolved.
Show resolved Hide resolved
// namespace: default <- same as Cluster
// type: kubernetes.io/tls
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Opaque or tls, that is the question

// data:
// tls.crt: |
// MIIC2DCCAcCgAwIBAgIBATANBgkqh ...
// tls.key: |
// MIIEpgIBAAKCAQEA7yn3bRHQ5FHMQ ...
// ca.crt: |
// MIIEpgIBAAKCAQEA7yn3bRHQ5FHMQ ...
// +optional
TLSSecretRef string `json:"tlsSecretRef,omitempty"`
}

type SSHPublicKey struct {
Expand Down
7 changes: 7 additions & 0 deletions api/v1alpha1/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -184,3 +184,10 @@ type Proxy struct {
// Endpoint is the address of the proxy.
Endpoint string `json:"endpoint"`
}

// TLSConfig represents config for connecting to TLS enabled hosts.
type TLSConfig struct {
Cert string `json:"cert"`
Key string `json:"key"`
CACert string `json:"caCert"`
}
15 changes: 15 additions & 0 deletions api/v1alpha1/zz_generated.deepcopy.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
Expand Up @@ -92,8 +92,10 @@ spec:
basicAuthSecret:
description: "BasicAuthSecret is the name of the secret containing
basic auth info for each host listed in Hosts. The secret
should contain a data entry for each host Endpoint: \n apiVersion:
v1 kind: Secret metadata: name: mybasicauthsecret type:
should be created in the same namespace as the Cluster.
The secret should contain a data entry for each host Endpoint
without the port: \n apiVersion: v1 kind: Secret metadata:
name: mybasicauthsecret namespace: same-as-cluster type:
Opaque data: 1.2.4.5: YWRtaW4= myhost: MWYyZDFlMmU2N2Rm"
type: string
hosts:
Expand Down Expand Up @@ -144,6 +146,21 @@ spec:
type: string
type: object
type: array
tlsSecretRef:
description: "mTLS Configuration: \n It is recommended that each flintlock
host is configured with its own cert signed by a common CA, and
set to use mTLS. The CAPMVM client should be provided with the CA,
and a client cert and key signed by that CA. TLSSecretRef is a reference
to the name of a secret which contains TLS cert information for
connecting to Flintlock hosts. The secret should be created in the
same namespace as the MicroVMCluster. The secret should be of type
kubernetes.io/tls https://kubernetes.io/docs/concepts/configuration/secret/#tls-secrets
with the addition of a ca.crt key. \n apiVersion: v1 kind: Secret
metadata: name: secret-tls namespace: default <- same as Cluster
type: kubernetes.io/tls data: tls.crt: | MIIC2DCCAcCgAwIBAgIBATANBgkqh
... tls.key: | MIIEpgIBAAKCAQEA7yn3bRHQ5FHMQ ... ca.crt: | MIIEpgIBAAKCAQEA7yn3bRHQ5FHMQ
..."
type: string
required:
- placement
type: object
Expand Down
6 changes: 6 additions & 0 deletions controllers/microvmmachine_controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -313,9 +313,15 @@ func (r *MicrovmMachineReconciler) getMicrovmService(
return nil, fmt.Errorf("getting basic auth token: %w", err)
}

tls, err := machineScope.GetTLSConfig()
if err != nil {
return nil, err
}

clientOpts := []flclient.Options{
flclient.WithProxy(machineScope.MvmCluster.Spec.MicrovmProxy),
flclient.WithBasicAuth(token),
flclient.WithTLS(tls),
}

client, err := r.MvmClientFunc(addr, clientOpts...)
Expand Down
3 changes: 1 addition & 2 deletions internal/client/auth.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,5 @@ func (b basicAuth) GetRequestMetadata(ctx context.Context, in ...string) (map[st

// GetRequestMetadata fullfills the credentials.PerRPCCredentials interface.
func (basicAuth) RequireTransportSecurity() bool {
// TODO: change this to true when we add TLS here is a fake issue to make the linter shut up #123
return false
return true
}
43 changes: 42 additions & 1 deletion internal/client/client.go
Original file line number Diff line number Diff line change
@@ -1,12 +1,15 @@
package client

import (
"crypto/tls"
"crypto/x509"
"fmt"
"net/url"

flintlockv1 "github.com/weaveworks-liquidmetal/flintlock/api/services/microvm/v1alpha1"
flgrpc "github.com/weaveworks-liquidmetal/flintlock/client/grpc"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials"
"google.golang.org/grpc/credentials/insecure"

//+kubebuilder:scaffold:imports
Expand All @@ -17,6 +20,7 @@ import (
type clientConfig struct {
basicAuthToken string
proxy *infrav1.Proxy
tls *infrav1.TLSConfig
}

// Options is a func to add a option to the flintlock host client.
Expand All @@ -36,15 +40,32 @@ func WithProxy(p *infrav1.Proxy) Options {
}
}

// WithTLS adds TLS credentials to the client.
func WithTLS(t *infrav1.TLSConfig) Options {
return func(c *clientConfig) {
c.tls = t
}
}

// FactoryFunc is a func to create a new flintlock client.
type FactoryFunc func(address string, opts ...Options) (microvm.Client, error)

// NewFlintlockClient returns a connected client to a flintlock host.
func NewFlintlockClient(address string, opts ...Options) (microvm.Client, error) {
cfg := buildConfig(opts...)
creds := insecure.NewCredentials()

if cfg.tls != nil {
var err error

creds, err = loadTLS(cfg.tls)
if err != nil {
return nil, err
}
}

dialOpts := []grpc.DialOption{
grpc.WithTransportCredentials(insecure.NewCredentials()),
grpc.WithTransportCredentials(creds),
}

if cfg.basicAuthToken != "" {
Expand Down Expand Up @@ -79,3 +100,23 @@ func buildConfig(opts ...Options) clientConfig {

return cfg
}

func loadTLS(cfg *infrav1.TLSConfig) (credentials.TransportCredentials, error) {
certificate, err := tls.LoadX509KeyPair(cfg.Cert, cfg.Key)
if err != nil {
return nil, err
}

capool := x509.NewCertPool()
if !capool.AppendCertsFromPEM([]byte(cfg.CACert)) {
return nil, fmt.Errorf("could not add cert to pool") //nolint: goerr113 // there is no err to wrap
}

tlsConfig := &tls.Config{
MinVersion: tls.VersionTLS13,
Certificates: []tls.Certificate{certificate},
RootCAs: capool,
}

return credentials.NewTLS(tlsConfig), nil
}
8 changes: 8 additions & 0 deletions internal/scope/errors.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,3 +19,11 @@ var (

errFailureDomainNotFound = errors.New("no failure domains found on the cluster")
)

type tlsError struct {
key string
}

func (t *tlsError) Error() string {
return "required key missing from TLS config data: " + t.key
}
66 changes: 65 additions & 1 deletion internal/scope/machine.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ package scope

import (
"context"
"encoding/base64"
"fmt"
"hash/crc32"
"sort"
Expand All @@ -29,6 +30,12 @@ var _ Scoper = &MachineScope{}

const ProviderPrefix = "microvm://"

const (
tlsCert = "tls.crt"
tlsKey = "tls.key"
caCert = "ca.crt"
)

type MachineScopeParams struct {
Cluster *clusterv1.Cluster
MicroVMCluster *infrav1.MicrovmCluster
Expand Down Expand Up @@ -306,14 +313,71 @@ func (m *MachineScope) GetBasicAuthToken(addr string) (string, error) {
token := string(tokenSecret.Data[host])

if token == "" {
m.V(2).Info( //nolint:gomnd // this magic number is fine
m.Info(
"basicAuthToken for host not found in secret", "secret", tokenSecret.Name, "host", host,
)
}

return token, nil
}

// GetTLSConfig will fetch the TLSSecretRef and CASecretRef on the MvmCluster
// and return the TLS config for the client.
// If either are not set, it will be assumed that the hosts are not
// configured will TLS and all client calls will be made without credentials.
func (m *MachineScope) GetTLSConfig() (*infrav1.TLSConfig, error) {
if m.MvmCluster.Spec.TLSSecretRef == "" {
m.Info("no TLS configuration found. will create insecure connection")

return nil, nil
}

secretKey := types.NamespacedName{
Name: m.MvmCluster.Spec.TLSSecretRef,
Namespace: m.MvmCluster.Namespace,
}

tlsSecret := &corev1.Secret{}
if err := m.client.Get(context.TODO(), secretKey, tlsSecret); err != nil {
return nil, err
}

cert, err := decode(tlsSecret.Data, tlsCert)
if err != nil {
return nil, err
}

key, err := decode(tlsSecret.Data, tlsKey)
if err != nil {
return nil, err
}

ca, err := decode(tlsSecret.Data, caCert)
if err != nil {
return nil, err
}

return &infrav1.TLSConfig{
Cert: cert,
Key: key,
CACert: ca,
}, nil
}

func decode(data map[string][]byte, key string) (string, error) {
val, ok := data[key]
if !ok {
return "", &tlsError{key}
}

dec, err := base64.StdEncoding.DecodeString(string(val))
if err != nil {
return "", err
}

return string(dec), nil
}

func (m *MachineScope) getFailureDomainFromProviderID(providerID string) string {
if providerID == "" {
return ""
Expand Down
Loading