From bfd668054d545e24fe65dc31249f1f4ced1c5080 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Graber?= Date: Wed, 6 Dec 2023 15:05:34 -0500 Subject: [PATCH 1/6] api: ovn_ssl_config MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Stéphane Graber Sponsored-by: Luizalabs (https://luizalabs.com) --- doc/api-extensions.md | 4 ++++ internal/version/api.go | 1 + 2 files changed, 5 insertions(+) diff --git a/doc/api-extensions.md b/doc/api-extensions.md index 25fe2cce3ef..3bacdd093dd 100644 --- a/doc/api-extensions.md +++ b/doc/api-extensions.md @@ -2292,3 +2292,7 @@ This introduces the configuration keys `cephfs.create_missing`, `cephfs.osd_pg_n This API extension provides the ability to use flags `--profile`, `--no-profile`, `--device`, and `--config` when moving an instance between projects and/or storage pools. + +## `ovn_ssl_config` +This introduces new server configuration keys to provide the SSL CA and client key pair to access the OVN databases. +The new configuration keys are `network.ovn.ca_cert`, `network.ovn.client_cert` and `network.ovn.client_key`. diff --git a/internal/version/api.go b/internal/version/api.go index 0632ca9de96..7bc6045e276 100644 --- a/internal/version/api.go +++ b/internal/version/api.go @@ -386,6 +386,7 @@ var APIExtensions = []string{ "disk_io_bus", "storage_cephfs_create_missing", "instance_move_config", + "ovn_ssl_config", } // APIExtensionsCount returns the number of available API extensions. From ac71aa79ee0306fbc8236cdea93d38b02605ca8b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Graber?= Date: Wed, 6 Dec 2023 15:05:55 -0500 Subject: [PATCH 2/6] incusd/cluster/config: Add OVN SSL config keys MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Stéphane Graber Sponsored-by: Luizalabs (https://luizalabs.com) --- internal/server/cluster/config/config.go | 32 ++++++++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/internal/server/cluster/config/config.go b/internal/server/cluster/config/config.go index fe64de86555..740e879fbcb 100644 --- a/internal/server/cluster/config/config.go +++ b/internal/server/cluster/config/config.go @@ -135,6 +135,11 @@ func (c *Config) NetworkOVNNorthboundConnection() string { return c.m.GetString("network.ovn.northbound_connection") } +// NetworkOVNSSL returns all three SSL configuration keys needed for a connection. +func (c *Config) NetworkOVNSSL() (string, string, string) { + return c.m.GetString("network.ovn.ca_cert"), c.m.GetString("network.ovn.client_cert"), c.m.GetString("network.ovn.client_key") +} + // ShutdownTimeout returns the number of minutes to wait for running operation to complete // before the server shuts down. func (c *Config) ShutdownTimeout() time.Duration { @@ -690,6 +695,33 @@ var ConfigSchema = config.Schema{ // defaultdesc: `unix:/var/run/ovn/ovnnb_db.sock` // shortdesc: OVN northbound database connection string "network.ovn.northbound_connection": {Default: "unix:/var/run/ovn/ovnnb_db.sock"}, + + // gendoc:generate(entity=server, group=miscellaneous, key=network.ovn.ca_cert) + // + // --- + // type: string + // scope: global + // defaultdesc: Content of `/etc/ovn/ovn-central.crt` if present + // shortdesc: OVN SSL certificate authority + "network.ovn.ca_cert": {Default: ""}, + + // gendoc:generate(entity=server, group=miscellaneous, key=network.ovn.client_cert) + // + // --- + // type: string + // scope: global + // defaultdesc: Content of `/etc/ovn/cert_host` if present + // shortdesc: OVN SSL client certificate + "network.ovn.client_cert": {Default: ""}, + + // gendoc:generate(entity=server, group=miscellaneous, key=network.ovn.client_key) + // + // --- + // type: string + // scope: global + // defaultdesc: Content of `/etc/ovn/key_host` if present + // shortdesc: OVN SSL client key + "network.ovn.client_key": {Default: ""}, } func expiryValidator(value string) error { From a36b5e50bf91c4c912e252f765f8ce5deb3639fc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Graber?= Date: Wed, 6 Dec 2023 15:12:08 -0500 Subject: [PATCH 3/6] doc: Update configs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Stéphane Graber Sponsored-by: Luizalabs (https://luizalabs.com) --- doc/config_options.txt | 24 ++++++++++++++++++ internal/server/metadata/configuration.json | 27 +++++++++++++++++++++ 2 files changed, 51 insertions(+) diff --git a/doc/config_options.txt b/doc/config_options.txt index e6969776305..502039a41a5 100644 --- a/doc/config_options.txt +++ b/doc/config_options.txt @@ -1645,6 +1645,30 @@ When using custom automatic instance placement logic, this option stores the scr See {ref}`clustering-instance-placement-scriptlet` for more information. ``` +```{config:option} network.ovn.ca_cert server-miscellaneous +:defaultdesc: "Content of `/etc/ovn/ovn-central.crt` if present" +:scope: "global" +:shortdesc: "OVN SSL certificate authority" +:type: "string" + +``` + +```{config:option} network.ovn.client_cert server-miscellaneous +:defaultdesc: "Content of `/etc/ovn/cert_host` if present" +:scope: "global" +:shortdesc: "OVN SSL client certificate" +:type: "string" + +``` + +```{config:option} network.ovn.client_key server-miscellaneous +:defaultdesc: "Content of `/etc/ovn/key_host` if present" +:scope: "global" +:shortdesc: "OVN SSL client key" +:type: "string" + +``` + ```{config:option} network.ovn.integration_bridge server-miscellaneous :defaultdesc: "`br-int`" :scope: "global" diff --git a/internal/server/metadata/configuration.json b/internal/server/metadata/configuration.json index b42916d5a89..77115a38ce5 100644 --- a/internal/server/metadata/configuration.json +++ b/internal/server/metadata/configuration.json @@ -1801,6 +1801,33 @@ "type": "string" } }, + { + "network.ovn.ca_cert": { + "defaultdesc": "Content of `/etc/ovn/ovn-central.crt` if present", + "longdesc": "", + "scope": "global", + "shortdesc": "OVN SSL certificate authority", + "type": "string" + } + }, + { + "network.ovn.client_cert": { + "defaultdesc": "Content of `/etc/ovn/cert_host` if present", + "longdesc": "", + "scope": "global", + "shortdesc": "OVN SSL client certificate", + "type": "string" + } + }, + { + "network.ovn.client_key": { + "defaultdesc": "Content of `/etc/ovn/key_host` if present", + "longdesc": "", + "scope": "global", + "shortdesc": "OVN SSL client key", + "type": "string" + } + }, { "network.ovn.integration_bridge": { "defaultdesc": "`br-int`", From 6db6715ed43c765eab62b6b643d1279f8bd948b2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Graber?= Date: Wed, 6 Dec 2023 15:52:08 -0500 Subject: [PATCH 4/6] incusd/network/openvswitch: Support OVN SSL config keys MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Closes #277 Signed-off-by: Stéphane Graber Sponsored-by: Luizalabs (https://luizalabs.com) --- internal/server/network/openvswitch/ovn.go | 119 +++++++++++++++++++-- 1 file changed, 111 insertions(+), 8 deletions(-) diff --git a/internal/server/network/openvswitch/ovn.go b/internal/server/network/openvswitch/ovn.go index 1255ef70ee0..ff0676add32 100644 --- a/internal/server/network/openvswitch/ovn.go +++ b/internal/server/network/openvswitch/ovn.go @@ -1,8 +1,10 @@ package openvswitch import ( + "context" "fmt" "net" + "os" "strconv" "strings" "time" @@ -179,16 +181,66 @@ type OVNRouterPeering struct { // NewOVN initialises new OVN client wrapper with the connection set in network.ovn.northbound_connection config. func NewOVN(s *state.State) (*OVN, error) { + // Get database connection strings. nbConnection := s.GlobalConfig.NetworkOVNNorthboundConnection() - sbConnection, err := NewOVS().OVNSouthboundDBRemoteAddress() if err != nil { return nil, fmt.Errorf("Failed to get OVN southbound connection string: %w", err) } - client := &OVN{} - client.SetNorthboundDBAddress(nbConnection) - client.SetSouthboundDBAddress(sbConnection) + // Create the OVN struct. + client := &OVN{ + nbDBAddr: nbConnection, + sbDBAddr: sbConnection, + } + + // If using SSL, then get the CA and client key pair. + if strings.Contains(nbConnection, "ssl:") { + sslCACert, sslClientCert, sslClientKey := s.GlobalConfig.NetworkOVNSSL() + + if sslCACert == "" { + content, err := os.ReadFile("/etc/ovn/ovn-central.crt") + if err != nil { + if os.IsNotExist(err) { + return nil, fmt.Errorf("OVN configured to use SSL but no SSL CA certificate defined") + } + + return nil, err + } + + sslCACert = string(content) + } + + if sslClientCert == "" { + content, err := os.ReadFile("/etc/ovn/cert_host") + if err != nil { + if os.IsNotExist(err) { + return nil, fmt.Errorf("OVN configured to use SSL but no SSL client certificate defined") + } + + return nil, err + } + + sslClientCert = string(content) + } + + if sslClientKey == "" { + content, err := os.ReadFile("/etc/ovn/key_host") + if err != nil { + if os.IsNotExist(err) { + return nil, fmt.Errorf("OVN configured to use SSL but no SSL client key defined") + } + + return nil, err + } + + sslClientKey = string(content) + } + + client.sslCACert = sslCACert + client.sslClientCert = sslClientCert + client.sslClientKey = sslClientKey + } return client, nil } @@ -197,6 +249,10 @@ func NewOVN(s *state.State) (*OVN, error) { type OVN struct { nbDBAddr string sbDBAddr string + + sslCACert string + sslClientCert string + sslClientKey string } // SetNorthboundDBAddress sets the address that runs the OVN northbound databases. @@ -253,16 +309,63 @@ func (o *OVN) xbctl(southbound bool, extraArgs ...string) (string, error) { // Figure out args. args := []string{"--timeout=10", "--db", dbAddr} + // Handle SSL args. + files := []*os.File{} if strings.Contains(dbAddr, "ssl:") { + // Handle client certificate. + clientCertFile, err := os.CreateTemp("", "ovn") + if err != nil { + return "", err + } + + defer clientCertFile.Close() + _ = os.Remove(clientCertFile.Name()) + files = append(files, clientCertFile) + + _, err = clientCertFile.WriteString(o.sslClientCert) + if err != nil { + return "", err + } + + // Handle client key. + clientKeyFile, err := os.CreateTemp("", "ovn") + if err != nil { + return "", err + } + + defer clientKeyFile.Close() + _ = os.Remove(clientKeyFile.Name()) + files = append(files, clientKeyFile) + + _, err = clientKeyFile.WriteString(o.sslClientKey) + if err != nil { + return "", err + } + + // Handle CA certificate. + caCertFile, err := os.CreateTemp("", "ovn") + if err != nil { + return "", err + } + + defer caCertFile.Close() + _ = os.Remove(caCertFile.Name()) + files = append(files, caCertFile) + + _, err = caCertFile.WriteString(o.sslCACert) + if err != nil { + return "", err + } + args = append(args, - "-c", "/etc/ovn/cert_host", - "-p", "/etc/ovn/key_host", - "-C", "/etc/ovn/ovn-central.crt", + "-c", "/proc/self/fd/3", + "-p", "/proc/self/fd/4", + "-C", "/proc/self/fd/5", ) } args = append(args, extraArgs...) - return subprocess.RunCommand(cmd, args...) + return subprocess.RunCommandInheritFds(context.Background(), files, cmd, args...) } // LogicalRouterAdd adds a named logical router. From d636126d8b5d555bb8f6e75544b122edd928c510 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Graber?= Date: Wed, 6 Dec 2023 21:25:57 -0500 Subject: [PATCH 5/6] internal/linux: Implement CreateMemfd MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This allows easily converting a byte slice into a usable memfd. Signed-off-by: Stéphane Graber Sponsored-by: Luizalabs (https://luizalabs.com) --- internal/linux/memfd.go | 47 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 47 insertions(+) create mode 100644 internal/linux/memfd.go diff --git a/internal/linux/memfd.go b/internal/linux/memfd.go new file mode 100644 index 00000000000..0064f2c37ca --- /dev/null +++ b/internal/linux/memfd.go @@ -0,0 +1,47 @@ +package linux + +import ( + "os" + + "golang.org/x/sys/unix" + + "github.com/lxc/incus/internal/revert" +) + +// CreateMemfd creates a new memfd for the provided byte slice. +func CreateMemfd(content []byte) (*os.File, error) { + revert := revert.New() + defer revert.Fail() + + // Create the memfd. + fd, err := unix.MemfdCreate("memfd", unix.MFD_CLOEXEC) + if err != nil { + return nil, err + } + + revert.Add(func() { unix.Close(fd) }) + + // Set its size. + err = unix.Ftruncate(fd, int64(len(content))) + if err != nil { + return nil, err + } + + // Prepare the storage. + data, err := unix.Mmap(fd, 0, len(content), unix.PROT_READ|unix.PROT_WRITE, unix.MAP_SHARED) + if err != nil { + return nil, err + } + + // Write the content. + copy(data, content) + + // Cleanup. + err = unix.Munmap(data) + if err != nil { + return nil, err + } + + revert.Success() + return os.NewFile(uintptr(fd), "memfd"), nil +} From 70e59be805c4cf7477199b6be1c40e16e24975a5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Graber?= Date: Wed, 6 Dec 2023 21:29:48 -0500 Subject: [PATCH 6/6] incusd/network/openvswitch: Port to memfd MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Stéphane Graber Sponsored-by: Luizalabs (https://luizalabs.com) --- internal/server/network/openvswitch/ovn.go | 25 ++++------------------ 1 file changed, 4 insertions(+), 21 deletions(-) diff --git a/internal/server/network/openvswitch/ovn.go b/internal/server/network/openvswitch/ovn.go index ff0676add32..17cbfcd6a89 100644 --- a/internal/server/network/openvswitch/ovn.go +++ b/internal/server/network/openvswitch/ovn.go @@ -10,6 +10,7 @@ import ( "time" "github.com/lxc/incus/internal/iprange" + "github.com/lxc/incus/internal/linux" "github.com/lxc/incus/internal/server/state" "github.com/lxc/incus/shared/subprocess" "github.com/lxc/incus/shared/util" @@ -313,50 +314,32 @@ func (o *OVN) xbctl(southbound bool, extraArgs ...string) (string, error) { files := []*os.File{} if strings.Contains(dbAddr, "ssl:") { // Handle client certificate. - clientCertFile, err := os.CreateTemp("", "ovn") + clientCertFile, err := linux.CreateMemfd([]byte(o.sslClientCert)) if err != nil { return "", err } defer clientCertFile.Close() - _ = os.Remove(clientCertFile.Name()) files = append(files, clientCertFile) - _, err = clientCertFile.WriteString(o.sslClientCert) - if err != nil { - return "", err - } - // Handle client key. - clientKeyFile, err := os.CreateTemp("", "ovn") + clientKeyFile, err := linux.CreateMemfd([]byte(o.sslClientKey)) if err != nil { return "", err } defer clientKeyFile.Close() - _ = os.Remove(clientKeyFile.Name()) files = append(files, clientKeyFile) - _, err = clientKeyFile.WriteString(o.sslClientKey) - if err != nil { - return "", err - } - // Handle CA certificate. - caCertFile, err := os.CreateTemp("", "ovn") + caCertFile, err := linux.CreateMemfd([]byte(o.sslCACert)) if err != nil { return "", err } defer caCertFile.Close() - _ = os.Remove(caCertFile.Name()) files = append(files, caCertFile) - _, err = caCertFile.WriteString(o.sslCACert) - if err != nil { - return "", err - } - args = append(args, "-c", "/proc/self/fd/3", "-p", "/proc/self/fd/4",