diff --git a/docs/config.md b/docs/config.md index 8fa77b55b..0b8798bc9 100644 --- a/docs/config.md +++ b/docs/config.md @@ -24,8 +24,16 @@ The explanation and default value of each configuration item are as follows: # systemd_cgroup enables systemd cgroup support. systemd_cgroup = false - # enable_tls_streaming enables the TLS streaming support. + # enable_tls_streaming enables the TLS streaming support. + # It generates a self-sign certificate unless the following x509_key_pair_streaming are both set. enable_tls_streaming = false + + # "plugins.cri.x509_key_pair_streaming" contains a x509 valid key pair to stream with tls. + [plugins.cri.x509_key_pair_streaming] + # tls_cert_file is the filepath to the certificate paired with the "tls_key_file" + tls_cert_file = "" + # tls_key_file is the filepath to the private key paired with the "tls_cert_file" + tls_key_file = "" # max_container_log_line_size is the maximum log line size in bytes for a container. # Log line longer than the limit will be split into multiple lines. -1 means no diff --git a/pkg/config/config.go b/pkg/config/config.go index 6919ce6b8..49288b59a 100644 --- a/pkg/config/config.go +++ b/pkg/config/config.go @@ -114,12 +114,22 @@ type PluginConfig struct { SystemdCgroup bool `toml:"systemd_cgroup" json:"systemdCgroup"` // EnableTLSStreaming indicates to enable the TLS streaming support. EnableTLSStreaming bool `toml:"enable_tls_streaming" json:"enableTLSStreaming"` + // X509KeyPairStreaming is a x509 key pair used for TLS streaming + X509KeyPairStreaming `toml:"x509_key_pair_streaming" json:"x509KeyPairStreaming"` // MaxContainerLogLineSize is the maximum log line size in bytes for a container. // Log line longer than the limit will be split into multiple lines. Non-positive // value means no limit. MaxContainerLogLineSize int `toml:"max_container_log_line_size" json:"maxContainerLogSize"` } +// X509KeyPairStreaming contains the x509 configuration for streaming +type X509KeyPairStreaming struct { + // TLSCertFile is the path to a certificate file + TLSCertFile string `toml:"tls_cert_file" json:"tlsCertFile"` + // TLSKeyFile is the path to a private key file + TLSKeyFile string `toml:"tls_key_file" json:"tlsKeyFile"` +} + // Config contains all configurations for cri server. type Config struct { // PluginConfig is the config for CRI plugin. @@ -152,10 +162,14 @@ func DefaultConfig() PluginConfig { }, NoPivot: false, }, - StreamServerAddress: "127.0.0.1", - StreamServerPort: "0", - EnableSelinux: false, - EnableTLSStreaming: false, + StreamServerAddress: "127.0.0.1", + StreamServerPort: "0", + EnableSelinux: false, + EnableTLSStreaming: false, + X509KeyPairStreaming: X509KeyPairStreaming{ + TLSKeyFile: "", + TLSCertFile: "", + }, SandboxImage: "k8s.gcr.io/pause:3.1", StatsCollectPeriod: 10, SystemdCgroup: false, diff --git a/pkg/server/streaming.go b/pkg/server/streaming.go index 4e103248e..0ed948617 100644 --- a/pkg/server/streaming.go +++ b/pkg/server/streaming.go @@ -34,6 +34,36 @@ import ( ctrdutil "github.com/containerd/cri/pkg/containerd/util" ) +type streamListenerMode int + +const ( + x509KeyPairTLS streamListenerMode = iota + selfSignTLS + withoutTLS +) + +func getStreamListenerMode(c *criService) (streamListenerMode, error) { + if c.config.EnableTLSStreaming { + if c.config.X509KeyPairStreaming.TLSCertFile != "" && c.config.X509KeyPairStreaming.TLSKeyFile != "" { + return x509KeyPairTLS, nil + } + if c.config.X509KeyPairStreaming.TLSCertFile != "" && c.config.X509KeyPairStreaming.TLSKeyFile == "" { + return -1, errors.New("must set X509KeyPairStreaming.TLSKeyFile") + } + if c.config.X509KeyPairStreaming.TLSCertFile == "" && c.config.X509KeyPairStreaming.TLSKeyFile != "" { + return -1, errors.New("must set X509KeyPairStreaming.TLSCertFile") + } + return selfSignTLS, nil + } + if c.config.X509KeyPairStreaming.TLSCertFile != "" { + return -1, errors.New("X509KeyPairStreaming.TLSCertFile is set but EnableTLSStreaming is not set") + } + if c.config.X509KeyPairStreaming.TLSKeyFile != "" { + return -1, errors.New("X509KeyPairStreaming.TLSKeyFile is set but EnableTLSStreaming is not set") + } + return withoutTLS, nil +} + func newStreamServer(c *criService, addr, port string) (streaming.Server, error) { if addr == "" { a, err := k8snet.ChooseBindAddress(nil) @@ -44,8 +74,22 @@ func newStreamServer(c *criService, addr, port string) (streaming.Server, error) } config := streaming.DefaultConfig config.Addr = net.JoinHostPort(addr, port) - runtime := newStreamRuntime(c) - if c.config.EnableTLSStreaming { + run := newStreamRuntime(c) + tlsMode, err := getStreamListenerMode(c) + if err != nil { + return nil, errors.Wrapf(err, "invalid stream server configuration") + } + switch tlsMode { + case x509KeyPairTLS: + tlsCert, err := tls.LoadX509KeyPair(c.config.X509KeyPairStreaming.TLSCertFile, c.config.X509KeyPairStreaming.TLSKeyFile) + if err != nil { + return nil, errors.Wrap(err, "failed to load x509 key pair for stream server") + } + config.TLSConfig = &tls.Config{ + Certificates: []tls.Certificate{tlsCert}, + } + return streaming.NewServer(config, run) + case selfSignTLS: tlsCert, err := newTLSCert() if err != nil { return nil, errors.Wrap(err, "failed to generate tls certificate for stream server") @@ -54,8 +98,12 @@ func newStreamServer(c *criService, addr, port string) (streaming.Server, error) Certificates: []tls.Certificate{tlsCert}, InsecureSkipVerify: true, } + return streaming.NewServer(config, run) + case withoutTLS: + return streaming.NewServer(config, run) + default: + return nil, errors.New("invalid configuration for the stream listener") } - return streaming.NewServer(config, runtime) } type streamRuntime struct { diff --git a/pkg/server/streaming_test.go b/pkg/server/streaming_test.go new file mode 100644 index 000000000..a88903d3b --- /dev/null +++ b/pkg/server/streaming_test.go @@ -0,0 +1,153 @@ +/* +Copyright 2017 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package server + +import ( + "testing" + + "github.com/containerd/cri/pkg/config" + "github.com/stretchr/testify/assert" +) + +func TestValidateStreamServer(t *testing.T) { + for desc, test := range map[string]struct { + *criService + tlsMode streamListenerMode + expectErr bool + }{ + "should pass with default withoutTLS": { + criService: &criService{ + config: config.Config{ + PluginConfig: config.DefaultConfig(), + }, + }, + tlsMode: withoutTLS, + expectErr: false, + }, + "should pass with x509KeyPairTLS": { + criService: &criService{ + config: config.Config{ + PluginConfig: config.PluginConfig{ + EnableTLSStreaming: true, + X509KeyPairStreaming: config.X509KeyPairStreaming{ + TLSKeyFile: "non-empty", + TLSCertFile: "non-empty", + }, + }, + }, + }, + tlsMode: x509KeyPairTLS, + expectErr: false, + }, + "should pass with selfSign": { + criService: &criService{ + config: config.Config{ + PluginConfig: config.PluginConfig{ + EnableTLSStreaming: true, + }, + }, + }, + tlsMode: selfSignTLS, + expectErr: false, + }, + "should return error with X509 keypair but not EnableTLSStreaming": { + criService: &criService{ + config: config.Config{ + PluginConfig: config.PluginConfig{ + EnableTLSStreaming: false, + X509KeyPairStreaming: config.X509KeyPairStreaming{ + TLSKeyFile: "non-empty", + TLSCertFile: "non-empty", + }, + }, + }, + }, + tlsMode: -1, + expectErr: true, + }, + "should return error with X509 TLSCertFile empty": { + criService: &criService{ + config: config.Config{ + PluginConfig: config.PluginConfig{ + EnableTLSStreaming: true, + X509KeyPairStreaming: config.X509KeyPairStreaming{ + TLSKeyFile: "non-empty", + TLSCertFile: "", + }, + }, + }, + }, + tlsMode: -1, + expectErr: true, + }, + "should return error with X509 TLSKeyFile empty": { + criService: &criService{ + config: config.Config{ + PluginConfig: config.PluginConfig{ + EnableTLSStreaming: true, + X509KeyPairStreaming: config.X509KeyPairStreaming{ + TLSKeyFile: "", + TLSCertFile: "non-empty", + }, + }, + }, + }, + tlsMode: -1, + expectErr: true, + }, + "should return error without EnableTLSStreaming and only TLSCertFile set": { + criService: &criService{ + config: config.Config{ + PluginConfig: config.PluginConfig{ + EnableTLSStreaming: false, + X509KeyPairStreaming: config.X509KeyPairStreaming{ + TLSKeyFile: "", + TLSCertFile: "non-empty", + }, + }, + }, + }, + tlsMode: -1, + expectErr: true, + }, + "should return error without EnableTLSStreaming and only TLSKeyFile set": { + criService: &criService{ + config: config.Config{ + PluginConfig: config.PluginConfig{ + EnableTLSStreaming: false, + X509KeyPairStreaming: config.X509KeyPairStreaming{ + TLSKeyFile: "non-empty", + TLSCertFile: "", + }, + }, + }, + }, + tlsMode: -1, + expectErr: true, + }, + } { + t.Run(desc, func(t *testing.T) { + tlsMode, err := getStreamListenerMode(test.criService) + if test.expectErr { + assert.Error(t, err) + return + } + assert.NoError(t, err) + assert.Equal(t, test.tlsMode, tlsMode) + }) + } +}