diff --git a/docs/quick-start.md b/docs/quick-start.md index c79f3b9a..be83279b 100644 --- a/docs/quick-start.md +++ b/docs/quick-start.md @@ -302,7 +302,8 @@ NET_DEVICE=$(ip route show | awk '/default/ {print $5}') sudo ./bin/flintlockd run \ --containerd-socket=/run/containerd-dev/containerd.sock \ - --parent-iface="${NET_DEVICE}" + --parent-iface="${NET_DEVICE}" \ + --insecure ``` If you're running `flintlockd` from within a Vagrant VM, or anywhere different @@ -330,7 +331,7 @@ There are both GUI and a CLI option. ### hammertime [Hammertime](https://github.com/Callisto13/hammertime) is a cli client built -with the soel purpose of interacting with Flintlock services. +with the sole purpose of interacting with Flintlock services. ### grpc-client-cli diff --git a/hack/scripts/gen_local_certs.sh b/hack/scripts/gen_local_certs.sh new file mode 100755 index 00000000..1d969f04 --- /dev/null +++ b/hack/scripts/gen_local_certs.sh @@ -0,0 +1,667 @@ +#!/usr/bin/env bash + +# Tool to create certificates for running liquid metal locally + +set -o pipefail + +# general vars +DEFAULT_CA_CN="Liquid Metal Dev CA" +DEFAULT_HOST_NAME="$(hostname)" +DEFAULT_CLIENT_CN_NAME="Liquid Metal Client 1" +DEFAULT_CF_SSL_BINARY + +## HELPER FUNCS +# +# +# Send a green message to stdout, followed by a new line +say() { + [ -t 1 ] && [ -n "$TERM" ] && + echo "$(tput setaf 2)[$MY_NAME]$(tput sgr0) $*" || + echo "[$MY_NAME] $*" +} + +# Send a green message to stdout, without a trailing new line +say_noln() { + [ -t 1 ] && [ -n "$TERM" ] && + echo -n "$(tput setaf 2)[$MY_NAME]$(tput sgr0) $*" || + echo "[$MY_NAME] $*" +} + +# Send a red message to stdout, followed by a new line +say_err() { + [ -t 2 ] && [ -n "$TERM" ] && + echo -e "$(tput setaf 1)[$MY_NAME] $*$(tput sgr0)" 1>&2 || + echo -e "[$MY_NAME] $*" 1>&2 +} + +# Send a yellow message to stdout, followed by a new line +say_warn() { + [ -t 1 ] && [ -n "$TERM" ] && + echo "$(tput setaf 3)[$MY_NAME] $*$(tput sgr0)" || + echo "[$MY_NAME] $*" +} + +# Send a yellow message to stdout, without a trailing new line +say_warn_noln() { + [ -t 1 ] && [ -n "$TERM" ] && + echo -n "$(tput setaf 3)[$MY_NAME] $*$(tput sgr0)" || + echo "[$MY_NAME] $*" +} + +# Exit with an error message and (optional) code +# Usage: die [-c ] +die() { + code=1 + [[ "$1" = "-c" ]] && { + code="$2" + shift 2 + } + say_err "$@" + exit "$code" +} + +# Exit with an error message if the last exit code is not 0 +ok_or_die() { + code=$? + [[ $code -eq 0 ]] || die -c $code "$@" +} + +create_outdir() { + local outdir="$1" + + mkdir -p "$outdir" || die "Failed to make output directory $outdir" +} + +write_cfssl_config() { + local configpath="$1" + + cat <<"EOF" >"$configpath" +{ + "signing": { + "default": { + "expiry": "8760h" + }, + "profiles": { + "intermediate": { + "usages": ["cert sign", "crl sign"], + "expiry": "70080h", + "ca_constraint": { + "is_ca": true, + "max_path_len": 1 + } + }, + "host": { + "usages": [ + "signing", + "digital signing", + "key encipherment", + "server auth" + ], + "expiry": "8760h" + }, + "client": { + "usages": [ + "signing", + "digital signature", + "key encipherment", + "client auth" + ], + "expiry": "8760h" + } + } + } +} +EOF +} + +# Create the certificates for a client (i.e. capmvm) +create_client_cert() { + local cn="$1" + local cfssl="$2" + local cfssljson="$3" + local outdir="$4" + local cfgpath="$outdir/config.json" + local ca="$outdir/intermediate-ca.pem" + local cakey="$outdir/intermediate-ca-key.pem" + + # shellcheck disable=SC2155 + local clientname=$(echo "$cn" | tr -d ' ' | tr '[:upper:]' '[:lower:]') + + clientcsr="$outdir/$clientname-csr.json" + say "Creating client certficate request: $clientcsr" + + cat <"$clientcsr" +{ + "CN": "$cn", + "hosts": [""], + "names": [ + { + "C": "UK", + "L": "London", + "O": "Liquid Metal Internal", + "OU": "Liquid Metal Internal Clients" + } + ] +} +EOF + + clientcert="$outdir/$clientname" + say "Generating client certificate: $clientcert" + $cfssl gencert \ + -ca "$ca" \ + -ca-key "$cakey" \ + -config "$cfgpath" \ + -profile client "$clientcsr" | + "$cfssljson" -bare "$clientcert" + + say "Client certificate created: $cn" +} + +# Create the certificates for a host +create_host_cert() { + local host="$1" + local cn="$2" + local cfssl="$3" + local cfssljson="$4" + local outdir="$5" + local cfgpath="$outdir/config.json" + local ca="$outdir/intermediate-ca.pem" + local cakey="$outdir/intermediate-ca-key.pem" + + hostcsr="$outdir/$host-csr.json" + say "Creating host certficate request: $hostcsr" + + cat <"$hostcsr" +{ + "CN": "$cn", + "hosts": ["$host"], + "names": [ + { + "C": "UK", + "L": "London", + "O": "Liquid Metal Internal", + "OU": "Liquid Metal Internal Hosts" + } + ] +} +EOF + + hostcert="$outdir/$host" + say "Generating host certificate: $hostcert" + $cfssl gencert \ + -ca "$ca" \ + -ca-key "$cakey" \ + -config "$cfgpath" \ + -profile host "$hostcsr" | + "$cfssljson" -bare "$hostcert" + + say "Host certificate created: $cn" +} + +# Create the certificate authority certs. +create_ca() { + local cn="$1" + local cfssl="$2" + local cfssljson="$3" + local outdir="$4" + + rootcsr="$outdir/root-csr.json" + say "Creating root CA certficate request: $rootcsr" + + cat <"$rootcsr" +{ + "CN": "$cn", + "key": { + "algo": "ecdsa", + "size": 256 + }, + "names": [ + { + "C": "UK", + "L": "London", + "O": "Liquid Metal Internal" + } + ], + "ca": { + "expiry": "87600h" + } +} +EOF + + rootca="$outdir/root-ca" + say "Generating root CA certficate: $cn" + $cfssl gencert -initca "$rootcsr" | + "$cfssljson" -bare "$rootca" + + say "CA certificates created: $cn" +} + +# Create the intermediate certificate authority certs. +create_intca() { + local cn="$1" + local cfssl="$2" + local cfssljson="$3" + local outdir="$4" + local cfg="$5" + + intcsr="$outdir/intermediate-csr.json" + say "Creating intermediate CA certficate request: $intcsr" + + cat <"$intcsr" +{ + "CN": "$cn", + "key": { + "algo": "ecdsa", + "size": 256 + }, + "names": [ + { + "C": "UK", + "L": "London", + "O": "Liquid Metal Internal", + "OU": "Liquid Metal Internal Intermediate CA" + } + ] +} +EOF + + intca="$outdir/intermediate-ca" + say "Generating intermediate CA certficate: $cn" + $cfssl genkey "$intcsr" | + "$cfssljson" -bare "$intca" + + say "Signing intermediate CA certificate" + rootca="$outdir/root-ca.pem" + rootcakey="$outdir/root-ca-key.pem" + intreq="$outdir/intermediate-ca.csr" + + $cfssl sign -ca "$rootca" \ + -ca-key "$rootcakey" \ + -config "$cfg" \ + -profile intermediate "$intreq" | + $cfssljson -bare "$intca" + + say "Intermediate CA certificates created: $cn" +} + +do_all_ca() { + local cn="$1" + local cfssl="$2" + local cfssljson="$3" + local outdir="$4" + local cfgpath="$outdir/config.json" + local intcn="$1 Intermediate" + + say "Creating output directory $outdir" + create_outdir "$outdir" + + write_cfssl_config "$cfgpath" + create_ca "$cn" "$cfssl" "$cfssljson" "$outdir" + create_intca "$intcn" "$cfssl" "$cfssljson" "$outdir" "$cfgpath" +} + +## COMMANDS +# +# +cmd_all() { + local host="$DEFAULT_HOST_NAME" + local cacn="$DEFAULT_CA_CN" + local hostcn="$DEFAULT_HOST_NAME" + local clientcn="$DEFAULT_CLIENT_CN_NAME" + local cfssl="" + local cfssljson="" + local outdir="" + + while [ $# -gt 0 ]; do + case "$1" in + "-h" | "--help") + cmd_all_help + exit 1 + ;; + "--name-ca") + shift + cacn="$1" + ;; + "--name-host") + shift + hostcn="$1" + ;; + "--name-client") + shift + clientcn="$1" + ;; + "--host") + shift + host="$1" + ;; + "--cfssl") + shift + cfssl="$1" + ;; + "--cfssljson") + shift + cfssljson="$1" + ;; + "--output") + shift + outdir="$1" + ;; + *) + die "Unknown argument: $1. Please use --help for help." + ;; + esac + shift + done + + if [[ "$cacn" == "" ]]; then + die "You must supply a CA common name" + fi + + if [[ "$hostcn" == "" ]]; then + die "You must supply a Host common name" + fi + + if [[ "$clientcn" == "" ]]; then + die "You must supply a client common name" + fi + + if [[ "$cfssl" == "" ]]; then + die "You must supply the path to the cfssl binary" + fi + + if [[ "$cfssljson" == "" ]]; then + die "You must supply the path to the cfssljson binary" + fi + + if [[ "$outdir" == "" ]]; then + die "You must supply the path to the ouput folder" + fi + + do_all_ca "$cacn" "$cfssl" "$cfssljson" "$outdir" + create_host_cert "$host" "$hostcn" "$cfssl" "$cfssljson" "$outdir" + create_client_cert "$clientcn" "$cfssl" "$cfssljson" "$outdir" +} + +cmd_ca() { + local cn="$DEFAULT_CA_CN" + local cfssl="" + local cfssljson="" + local outdir="" + + while [ $# -gt 0 ]; do + case "$1" in + "-h" | "--help") + cmd_ca_help + exit 1 + ;; + "-n" | "--name") + shift + cn="$1" + ;; + "--cfssl") + shift + cfssl="$1" + ;; + "--cfssljson") + shift + cfssljson="$1" + ;; + "--output") + shift + outdir="$1" + ;; + *) + die "Unknown argument: $1. Please use --help for help." + ;; + esac + shift + done + + if [[ "$cn" == "" ]]; then + die "You must supply a name" + fi + + if [[ "$cfssl" == "" ]]; then + die "You must supply the path to the cfssl binary" + fi + + if [[ "$cfssljson" == "" ]]; then + die "You must supply the path to the cfssljson binary" + fi + + if [[ "$outdir" == "" ]]; then + die "You must supply the path to the ouput folder" + fi + + do_all_ca "$cn" "$cfssl" "$cfssljson" "$outdir" +} + +cmd_host() { + local host="$DEFAULT_HOST_NAME" + local cn="$host" + local cfssl="" + local cfssljson="" + local outdir="" + + while [ $# -gt 0 ]; do + case "$1" in + "-h" | "--help") + cmd_host_help + exit 1 + ;; + "-n" | "--name") + shift + cn="$1" + ;; + "--cfssl") + shift + cfssl="$1" + ;; + "--cfssljson") + shift + cfssljson="$1" + ;; + "--output") + shift + outdir="$1" + ;; + "--host") + shift + host="$1" + ;; + *) + die "Unknown argument: $1. Please use --help for help." + ;; + esac + shift + done + + if [[ "$host" == "" ]]; then + die "You must supply a host name" + fi + + if [[ "$cn" == "" ]]; then + die "You must supply a common name" + fi + + if [[ "$cfssl" == "" ]]; then + die "You must supply the path to the cfssl binary" + fi + + if [[ "$cfssljson" == "" ]]; then + die "You must supply the path to the cfssljson binary" + fi + + if [[ "$outdir" == "" ]]; then + die "You must supply the path to the ouput folder" + fi + + create_host_cert "$host" "$cn" "$cfssl" "$cfssljson" "$outdir" +} + +cmd_client() { + local cn="$DEFAULT_CLIENT_CN_NAME" + local cfssl="" + local cfssljson="" + local outdir="" + + while [ $# -gt 0 ]; do + case "$1" in + "-h" | "--help") + cmd_client_help + exit 1 + ;; + "-n" | "--name") + shift + cn="$1" + ;; + "--cfssl") + shift + cfssl="$1" + ;; + "--cfssljson") + shift + cfssljson="$1" + ;; + "--output") + shift + outdir="$1" + ;; + *) + die "Unknown argument: $1. Please use --help for help." + ;; + esac + shift + done + + if [[ "$cn" == "" ]]; then + die "You must supply a common name" + fi + + if [[ "$cfssl" == "" ]]; then + die "You must supply the path to the cfssl binary" + fi + + if [[ "$cfssljson" == "" ]]; then + die "You must supply the path to the cfssljson binary" + fi + + if [[ "$outdir" == "" ]]; then + die "You must supply the path to the ouput folder" + fi + + create_client_cert "$cn" "$cfssl" "$cfssljson" "$outdir" +} + +## COMMAND HELP FUNCS +# +# + +cmd_all_help() { + hname=$(hostname) + cat < +Script to create certificates for running liquid metal in dev +COMMANDS: +EOF + + cmd_all_help + cmd_ca_help + cmd_host_help + cmd_client_help +} + +## LET'S DO THIS THING +# +# +main() { + if [ $# = 0 ]; then + die "No command provided. Please use \`$0 help\` for help." + fi + + # Parse main command line args. + # + while [ $# -gt 0 ]; do + case "$1" in + -h | --help) + cmd_help + exit 1 + ;; + -*) + die "Unknown arg: $1. Please use \`$0 help\` for help." + ;; + *) + break + ;; + esac + shift + done + + # $1 is now a command name. Check if it is a valid command and, if so, + # run it. + # + declare -f "cmd_$1" >/dev/null + ok_or_die "Unknown command: $1. Please use \`$0 help\` for help." + + cmd=cmd_$1 + shift + + # $@ is now a list of command-specific args + # + $cmd "$@" +} + +main "$@" diff --git a/hack/scripts/send.sh b/hack/scripts/send.sh index c385785b..f2f2863b 100755 --- a/hack/scripts/send.sh +++ b/hack/scripts/send.sh @@ -23,7 +23,7 @@ ${0} [flags] -s service -m method> -s/--service service name of the service; default: ${service} -m/--method method name of the method on the service; default: ${method} --uid uid uuid of the microvm - --namespace namespace namespace to query; default: ${ns1} + --namespace namespace namespace to query; default: ${namespace} -h/--help help Special: diff --git a/internal/command/flags/flags.go b/internal/command/flags/flags.go index f3305700..e4c27c1c 100644 --- a/internal/command/flags/flags.go +++ b/internal/command/flags/flags.go @@ -21,6 +21,11 @@ const ( containerdNamespace = "containerd-ns" maximumRetryFlag = "maximum-retry" basicAuthTokenFlag = "basic-auth-token" + insecureFlag = "insecure" + tlsCertFlag = "tls-cert" + tlsKeyFlag = "tls-key" + tlsClientValidateFlag = "tls-client-validate" + tlsClientCAFlag = "tls-client-ca" ) // AddGRPCServerFlagsToCommand will add gRPC server flags to the supplied command. @@ -67,6 +72,34 @@ func AddAuthFlagsToCommand(cmd *cobra.Command, cfg *config.Config) { "The token to use for very basic token based authentication.") } +// AddTLSFlagsToCommand will add TLS-related flags to the given command. +func AddTLSFlagsToCommand(cmd *cobra.Command, cfg *config.Config) { + cmd.Flags().BoolVar(&cfg.TLS.Insecure, + insecureFlag, + false, + "Run the gRPC server insecurely (i.e. without TLS). Not recommended.") + + cmd.Flags().StringVar(&cfg.TLS.CertFile, + tlsCertFlag, + "", + "Path to the certificate to use for TLS.") + + cmd.Flags().StringVar(&cfg.TLS.KeyFile, + tlsKeyFlag, + "", + "Path to the key to use for TLS.") + + cmd.Flags().BoolVar(&cfg.TLS.ValidateClient, + tlsClientValidateFlag, + false, + "Validate the certificates of clients calling the gRPC server.") + + cmd.Flags().StringVar(&cfg.TLS.ClientCAFile, + tlsClientCAFlag, + "", + "Path to the certificate to use when validating client certificates.") +} + // AddNetworkFlagsToCommand will add various network flags to the command. func AddNetworkFlagsToCommand(cmd *cobra.Command, cfg *config.Config) error { cmd.Flags().StringVar(&cfg.ParentIface, diff --git a/internal/command/run/run.go b/internal/command/run/run.go index 2430cb24..ff5752a6 100644 --- a/internal/command/run/run.go +++ b/internal/command/run/run.go @@ -51,6 +51,7 @@ func NewCommand(cfg *config.Config) (*cobra.Command, error) { cmdflags.AddGRPCServerFlagsToCommand(cmd, cfg) cmdflags.AddAuthFlagsToCommand(cmd, cfg) + cmdflags.AddTLSFlagsToCommand(cmd, cfg) cmdflags.AddContainerDFlagsToCommand(cmd, cfg) cmdflags.AddFirecrackerFlagsToCommand(cmd, cfg) @@ -113,6 +114,10 @@ func runServer(ctx context.Context, cfg *config.Config) error { func serveAPI(ctx context.Context, cfg *config.Config) error { logger := log.GetLogger(ctx) + if err := cfg.TLS.Validate(); err != nil { + return fmt.Errorf("validating tls config: %w", err) + } + ports, err := inject.InitializePorts(cfg) if err != nil { return fmt.Errorf("initialising ports for application: %w", err) @@ -120,7 +125,13 @@ func serveAPI(ctx context.Context, cfg *config.Config) error { app := inject.InitializeApp(cfg, ports) server := inject.InitializeGRPCServer(app) - grpcServer := grpc.NewServer(withOpts(ctx, cfg.BasicAuthToken)...) + + serverOpts, err := generateOpts(ctx, cfg) + if err != nil { + return err + } + + grpcServer := grpc.NewServer(serverOpts...) mvmv1.RegisterMicroVMServer(grpcServer, server) grpc_prometheus.Register(grpcServer) @@ -169,28 +180,43 @@ func runControllers(ctx context.Context, cfg *config.Config) error { return nil } -func withOpts(ctx context.Context, authToken string) []grpc.ServerOption { +func generateOpts(ctx context.Context, cfg *config.Config) ([]grpc.ServerOption, error) { logger := log.GetLogger(ctx) - if authToken != "" { + opts := []grpc.ServerOption{ + grpc.StreamInterceptor(grpc_prometheus.StreamServerInterceptor), + grpc.UnaryInterceptor(grpc_prometheus.UnaryServerInterceptor), + } + + if cfg.BasicAuthToken != "" { logger.Info("basic authentication is enabled") - return []grpc.ServerOption{ + opts = []grpc.ServerOption{ grpc.StreamInterceptor(grpc_mw.ChainStreamServer( grpc_prometheus.StreamServerInterceptor, - grpc_auth.StreamServerInterceptor(auth.BasicAuthFunc(authToken)), + grpc_auth.StreamServerInterceptor(auth.BasicAuthFunc(cfg.BasicAuthToken)), )), grpc.UnaryInterceptor(grpc_mw.ChainUnaryServer( grpc_prometheus.UnaryServerInterceptor, - grpc_auth.UnaryServerInterceptor(auth.BasicAuthFunc(authToken)), + grpc_auth.UnaryServerInterceptor(auth.BasicAuthFunc(cfg.BasicAuthToken)), )), } + } else { + logger.Warn("basic authentication is DISABLED") } - logger.Warn("authentication is DISABLED") + if !cfg.TLS.Insecure { + logger.Info("TLS is enabled") - return []grpc.ServerOption{ - grpc.StreamInterceptor(grpc_prometheus.StreamServerInterceptor), - grpc.UnaryInterceptor(grpc_prometheus.UnaryServerInterceptor), + creds, err := auth.LoadTLSForGRPC(&cfg.TLS) + if err != nil { + return nil, err + } + + opts = append(opts, grpc.Creds(creds)) + } else { + logger.Warn("TLS is DISABLED") } + + return opts, nil } diff --git a/internal/config/config.go b/internal/config/config.go index 99cb8031..da9a0c97 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -42,4 +42,20 @@ type Config struct { DeleteVMTimeout time.Duration // BasicAuthToken is the static token to use for very basic authentication. BasicAuthToken string + // TLS holds the TLS related configuration. + TLS TLSConfig +} + +// TLSConfig holds the configuration for TLS. +type TLSConfig struct { + // Insecure indicates if we should start the server insecurely (i.e. without TLS). + Insecure bool + // CertFile is the path to the certificate file to use. + CertFile string + // KeyFile is the path to the certificate key file to use. + KeyFile string + // ValidateClient indicates if the client certificates should be validated. + ValidateClient bool + // ClientCAFile is the path to a CA certificate file to use when validating client certificates. + ClientCAFile string } diff --git a/internal/config/errors.go b/internal/config/errors.go new file mode 100644 index 00000000..9d598c20 --- /dev/null +++ b/internal/config/errors.go @@ -0,0 +1,29 @@ +package config + +import ( + "errors" + "fmt" +) + +var ( + errNoCertWhenInsecure = errors.New("cannot specify tls certificate details when running insecurely") + errCertRequired = errors.New("certificate file path is required when running securely") + errKeyRequired = errors.New("certificate key file path is required when running securely") + errClientCARequired = errors.New("client certificate key file path is required when running mTLS") +) + +type certMissingError struct { + subject string + target string +} + +func (e *certMissingError) Error() string { + return fmt.Sprintf("%s %s does not exist", e.subject, e.target) +} + +func newCertMissingError(subject, target string) *certMissingError { + return &certMissingError{ + subject: subject, + target: target, + } +} diff --git a/internal/config/validation.go b/internal/config/validation.go new file mode 100644 index 00000000..e652feca --- /dev/null +++ b/internal/config/validation.go @@ -0,0 +1,53 @@ +package config + +import ( + "errors" + "os" +) + +// Validate will validate the TLS config. +func (t TLSConfig) Validate() error { + if t.Insecure { + if isSet(t.KeyFile) || isSet(t.CertFile) { + return errNoCertWhenInsecure + } + + return nil + } + + if isNotSet(t.CertFile) { + return errCertRequired + } + + if isNotSet(t.KeyFile) { + return errKeyRequired + } + + if t.ValidateClient && isNotSet(t.ClientCAFile) { + return errClientCARequired + } + + if _, err := os.Stat(t.CertFile); errors.Is(err, os.ErrNotExist) { + return newCertMissingError("certificate file", t.CertFile) + } + + if _, err := os.Stat(t.KeyFile); errors.Is(err, os.ErrNotExist) { + return newCertMissingError("key file", t.KeyFile) + } + + if t.ValidateClient { + if _, err := os.Stat(t.ClientCAFile); errors.Is(err, os.ErrNotExist) { + return newCertMissingError("client CA file", t.ClientCAFile) + } + } + + return nil +} + +func isSet(val string) bool { + return val != "" +} + +func isNotSet(val string) bool { + return !isSet(val) +} diff --git a/internal/config/validation_test.go b/internal/config/validation_test.go new file mode 100644 index 00000000..3e11d1fb --- /dev/null +++ b/internal/config/validation_test.go @@ -0,0 +1,145 @@ +package config_test + +import ( + "io/ioutil" + "os" + "testing" + + . "github.com/onsi/gomega" + "github.com/weaveworks-liquidmetal/flintlock/internal/config" +) + +func TestValidateTLSConfig(t *testing.T) { + g := NewWithT(t) + + tempFile, err := ioutil.TempFile("", "certfile") + g.Expect(err).NotTo(HaveOccurred()) + t.Cleanup(func() { + g.Expect(os.Remove(tempFile.Name())).To(Succeed()) + }) + + tt := []struct { + name string + expected func(*WithT, error) + tlsConfig config.TLSConfig + }{ + { + name: "when all config is valid, no error should occur", + tlsConfig: config.TLSConfig{ + CertFile: tempFile.Name(), + KeyFile: tempFile.Name(), + ClientCAFile: tempFile.Name(), + ValidateClient: true, + }, + expected: func(g *WithT, err error) { + g.Expect(err).NotTo(HaveOccurred()) + }, + }, + { + name: "when CertFile is not set, an error should be returned", + tlsConfig: config.TLSConfig{ + CertFile: "", + }, + expected: func(g *WithT, err error) { + g.Expect(err).To(HaveOccurred()) + g.Expect(err).To(MatchError("certificate file path is required when running securely")) + }, + }, + { + name: "when KeyFile is not set, an error should be returned", + tlsConfig: config.TLSConfig{ + CertFile: "certfile", + KeyFile: "", + }, + expected: func(g *WithT, err error) { + g.Expect(err).To(HaveOccurred()) + g.Expect(err).To(MatchError("certificate key file path is required when running securely")) + }, + }, + { + name: "when the ValidateClient is set but ClientCAFile is not set, an error should be returned", + tlsConfig: config.TLSConfig{ + CertFile: "certfile", + KeyFile: "keyfile", + ValidateClient: true, + }, + expected: func(g *WithT, err error) { + g.Expect(err).To(HaveOccurred()) + g.Expect(err.Error()).To(ContainSubstring("client certificate key file path is required when running mTLS")) + }, + }, + { + name: "when the given CertFile is not an existing file, an error should be returned", + tlsConfig: config.TLSConfig{ + CertFile: "certfile", + KeyFile: "keyfile", + }, + expected: func(g *WithT, err error) { + g.Expect(err).To(HaveOccurred()) + g.Expect(err.Error()).To(ContainSubstring("certificate file certfile does not exist")) + }, + }, + { + name: "when the given KeyFile is not an existing file, an error should be returned", + tlsConfig: config.TLSConfig{ + CertFile: tempFile.Name(), + KeyFile: "keyfile", + }, + expected: func(g *WithT, err error) { + g.Expect(err).To(HaveOccurred()) + g.Expect(err.Error()).To(ContainSubstring("key file keyfile does not exist")) + }, + }, + { + name: "when the ClientCAFile is set and is not an existing file, an error should be returned", + tlsConfig: config.TLSConfig{ + CertFile: tempFile.Name(), + KeyFile: tempFile.Name(), + ValidateClient: true, + ClientCAFile: "clientcafile", + }, + expected: func(g *WithT, err error) { + g.Expect(err).To(HaveOccurred()) + g.Expect(err.Error()).To(ContainSubstring("client CA file clientcafile does not exist")) + }, + }, + { + name: "when Insecure is set, no error should have occurred", + tlsConfig: config.TLSConfig{ + Insecure: true, + }, + expected: func(g *WithT, err error) { + g.Expect(err).NotTo(HaveOccurred()) + }, + }, + { + name: "when Insecure and CertFile are set, an error should be returned", + tlsConfig: config.TLSConfig{ + Insecure: true, + CertFile: "certfile", + }, + expected: func(g *WithT, err error) { + g.Expect(err).To(HaveOccurred()) + g.Expect(err).To(MatchError("cannot specify tls certificate details when running insecurely")) + }, + }, + { + name: "when Insecure and KeyFile are set, an error should be returned", + tlsConfig: config.TLSConfig{ + Insecure: true, + KeyFile: "keyfile", + }, + expected: func(g *WithT, err error) { + g.Expect(err).To(HaveOccurred()) + g.Expect(err).To(MatchError("cannot specify tls certificate details when running insecurely")) + }, + }, + } + + for _, tc := range tt { + t.Run(tc.name, func(t *testing.T) { + err := tc.tlsConfig.Validate() + tc.expected(g, err) + }) + } +} diff --git a/pkg/auth/tls.go b/pkg/auth/tls.go new file mode 100644 index 00000000..4d2e2057 --- /dev/null +++ b/pkg/auth/tls.go @@ -0,0 +1,46 @@ +package auth + +import ( + "crypto/tls" + "crypto/x509" + "fmt" + "io/ioutil" + + "github.com/weaveworks-liquidmetal/flintlock/internal/config" + "google.golang.org/grpc/credentials" +) + +// LoadTLSForGRPC will process TLS config and return TLS credentials for the gRPC server. +// If ClientCAFile and ValidateClient are set, then the server will be configured +// for mutual TLS. +func LoadTLSForGRPC(tlsCfg *config.TLSConfig) (credentials.TransportCredentials, error) { + serverCert, err := tls.LoadX509KeyPair(tlsCfg.CertFile, tlsCfg.KeyFile) + if err != nil { + return nil, fmt.Errorf("reading tls key pair: %w", err) + } + + grpcTLS := &tls.Config{ + Certificates: []tls.Certificate{serverCert}, + MinVersion: tls.VersionTLS13, + } + + if tlsCfg.ClientCAFile != "" { + caCert, err := ioutil.ReadFile(tlsCfg.ClientCAFile) + if err != nil { + return nil, fmt.Errorf("reading CA cert file %s: %w", tlsCfg.ClientCAFile, err) + } + + pool := x509.NewCertPool() + if !pool.AppendCertsFromPEM(caCert) { + return nil, fmt.Errorf("could not append CACert to cert pool") + } + + grpcTLS.ClientCAs = pool + } + + if tlsCfg.ValidateClient { + grpcTLS.ClientAuth = tls.RequireAndVerifyClientCert + } + + return credentials.NewTLS(grpcTLS), nil +} diff --git a/test/e2e/utils/runner.go b/test/e2e/utils/runner.go index 5e684430..9f0c3a56 100644 --- a/test/e2e/utils/runner.go +++ b/test/e2e/utils/runner.go @@ -209,7 +209,9 @@ func (r *Runner) startFlintlockd() { flCmd := exec.Command(r.flintlockdBin, "run", "--containerd-socket", containerdSocket, "--parent-iface", parentIface, - "--verbosity", r.params.FlintlockdLogLevel) + "--verbosity", r.params.FlintlockdLogLevel, + "--insecure", + ) flSess, err := gexec.Start(flCmd, gk.GinkgoWriter, gk.GinkgoWriter) gm.Expect(err).NotTo(gm.HaveOccurred())