Skip to content
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
5 changes: 5 additions & 0 deletions .github/workflows/windows-macos.yml
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,11 @@ jobs:
python \
wget

- name: Install dependencies from pip
if: ${{ runner.os == 'macOS' }}
run: |
${ARCH_WRAPPER} python3 -m pip install --user --upgrade distlib

- name: Install specific QEMU from Choco
if: ${{ runner.os == 'Windows' }}
uses: crazy-max/ghaction-chocolatey@v3
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ This document demonstrates how to configure the location where Multipass stores
```{caution}
**Caveats:**
- Multipass will not migrate your existing data; this article explains how to do it manually. If you do not transfer the data, you will have to re-download any Ubuntu images and reinitialise any instances that you need.
- When uninstalling Multipass, the uninstaller will not remove data stored in custom locations, so you'll have to deleted it manually.
- When uninstalling Multipass, the uninstaller will not remove data stored in custom locations, so you'll have to delete it manually.
```

`````{tabs}
Expand Down Expand Up @@ -80,7 +80,7 @@ sudo snap start multipass
You can delete the original data at your discretion, to free up space:

```{code-block} text
sudo rm -rf /var/snap/multipass/common/data/multipassd
sudo rm -rf /var/snap/multipass/common/data/multipassd/vault
sudo rm -rf /var/snap/multipass/common/cache/multipassd
```

Expand Down Expand Up @@ -128,6 +128,12 @@ launchctl load /Library/LaunchDaemons/com.canonical.multipassd.plist

First, open a PowerShell prompt with administration privileges.

Stop Multipass instances:

```{code-block} powershell
multipass stop --all
```

Stop the Multipass daemon:

```{code-block} powershell
Expand All @@ -144,13 +150,18 @@ Set-ItemProperty -Path "HKLM:System\CurrentControlSet\Control\Session Manager\En
Now you can transfer the data from its original location to the new location:

```{code-block} powershell
Copy-Item -Path "C:\ProgramData\Multipass\*" -Destination "<path>" -Recurse
Copy-Item -Path "C:\ProgramData\Multipass\*" -Recurse -Force -Destination "<path>"
```

```{caution}
It is important to copy any existing data to the new location. This avoids unauthenticated client issues, permission issues, and in general, to have any previously created instances available.
```

You also need to edit several settings so that the specified paths point to the new Multipass storage directory, otherwise your instances will fail to start:

* `<path>/data/vault/multipassd-instance-image-records.json`: Update the "path" key for each instance.
* Open Hyper-V Manager > For each instance right-click the instance and open the settings. Navigate to SCSI Controller > Hard Drive and update the Media path. Do the same for SCSI Controller > DVD Drive > Media Image file.

Finally, start the Multipass daemon:

```{code-block} powershell
Expand All @@ -160,7 +171,8 @@ Start-Service Multipass
You can delete the original data at your discretion, to free up space:

```{code-block} powershell
Remove-Item -Path "C:\ProgramData\Multipass\*" -Recurse
Remove-Item -Path "C:\ProgramData\Multipass\cache\*" -Recurse
Remove-Item -Path "C:\ProgramData\Multipass\data\vault\*" -Recurse
```

````
Expand Down Expand Up @@ -200,6 +212,11 @@ sudo cp -r <path>/data /var/snap/multipass/common/data/multipassd
sudo cp -r <path>/cache /var/snap/multipass/common/cache/multipassd
```

You also need to edit the following configuration files so that the specified paths point to the original Multipass storage directory, otherwise your instances will fail to start:

* `multipass-vm-instances.json`: Update the absolute path of the instance images in the "arguments" key for each instance.
* `vault/multipassd-instance-image-records.json`: Update the "path" key for each instance.

Finally, start the Multipass daemon:

```{code-block} text
Expand Down Expand Up @@ -252,6 +269,12 @@ launchctl load /Library/LaunchDaemons/com.canonical.multipassd.plist

First, open a PowerShell prompt with administrator privileges.

Stop Multipass instances:

```{code-block} powershell
multipass stop --all
```

Stop the Multipass daemon:

```{code-block} powershell
Expand All @@ -267,9 +290,11 @@ Remove-ItemProperty -Path "HKLM:System\CurrentControlSet\Control\Session Manager
Now you can transfer the data back to its original location:

```{code-block} powershell
Copy-Item -Path "<path>\*" -Destination "C:\ProgramData\Multipass" -Recurse
Copy-Item -Path "<path>\*" -Destination "C:\ProgramData\Multipass" -Recurse -Force
```

Follow the same instructions from setting up the custom image location to update the paths to their original location.

Finally, start the Multipass daemon:

```{code-block} powershell
Expand All @@ -279,7 +304,7 @@ Start-Service Multipass
You can delete the data from the custom location at your discretion, to free up space:

```{code-block} powershell
Remove-Item -Path "<path>" -Recurse
Remove-Item -Path "<path>" -Recurse -Force
```

````
Expand Down
10 changes: 9 additions & 1 deletion include/multipass/platform.h
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,9 @@ class Platform : public Singleton<Platform>
[[nodiscard]] virtual std::string bridge_nomenclature() const;
virtual int get_cpus() const;
virtual long long get_total_ram() const;
[[nodiscard]] virtual std::filesystem::path get_root_cert_path() const;

[[nodiscard]] virtual std::filesystem::path get_root_cert_dir() const;
[[nodiscard]] std::filesystem::path get_root_cert_path() const;
};

QString interpret_setting(const QString& key, const QString& val);
Expand Down Expand Up @@ -112,3 +114,9 @@ std::string host_version();
inline multipass::platform::Platform::Platform(const PrivatePass& pass) noexcept : Singleton(pass)
{
}

inline std::filesystem::path multipass::platform::Platform::get_root_cert_path() const
{
constexpr auto* root_cert_file_name = "multipass_root_cert.pem";
return get_root_cert_dir() / root_cert_file_name;
}
84 changes: 78 additions & 6 deletions src/cert/ssl_cert_provider.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
*/

#include <multipass/format.h>
#include <multipass/logging/log.h>
#include <multipass/platform.h>
#include <multipass/ssl_cert_provider.h>
#include <multipass/utils.h>
Expand All @@ -33,9 +34,11 @@
#include <vector>

namespace mp = multipass;
namespace mpl = mp::logging;

namespace
{
constexpr auto kLogCategory = "ssl-cert-provider";
// utility function for checking return code or raw pointer from openssl C-apis
// TODO: constrain T to int or raw pointer once C++20 concepts is available
template <typename T>
Expand Down Expand Up @@ -196,6 +199,22 @@ void set_random_serial_number(X509* cert)
openssl_check(X509_set_serialNumber(cert, serial), "Failed to set serial number!\n");
}

/**
* Check whether this certificate is the issuer (signer) of the given certificate.
*
* @param [in] signed_cert The certificate to check
* @return True if this certificate signed @p signed_cert; false otherwise.
*/
bool is_issuer_of(X509& issuer, X509& signed_cert)
{
// Get the public key of this certificate (issuer)
std::unique_ptr<EVP_PKEY, decltype(&EVP_PKEY_free)> pubkey{X509_get_pubkey(&issuer),
&EVP_PKEY_free};
openssl_check(pubkey.get(), "Failed to get public key from certificate");
// Verify that signed_cert is issued by this certificate.
return X509_verify(&signed_cert, pubkey.get()) == 1;
}

class X509Cert
{
public:
Expand Down Expand Up @@ -336,6 +355,15 @@ class X509Cert
std::unique_ptr<X509, decltype(&X509_free)> cert{X509_new(), X509_free};
};

std::unique_ptr<X509, decltype(&X509_free)> load_cert_from_file(const std::string& path)
{
std::unique_ptr<FILE, int (*)(FILE*)> file{fopen(path.c_str(), "r"), &fclose};
if (!file)
return {nullptr, X509_free};

return {PEM_read_X509(file.get(), nullptr, nullptr, nullptr), X509_free};
}

mp::SSLCertProvider::KeyCertificatePair make_cert_key_pair(const QDir& cert_dir,
const std::string& server_name)
{
Expand All @@ -351,13 +379,52 @@ mp::SSLCertProvider::KeyCertificatePair make_cert_key_pair(const QDir& cert_dir,
if (std::filesystem::exists(root_cert_path) && QFile::exists(priv_key_path) &&
QFile::exists(cert_path))
{
// Unlike other daemon files, the root certificate needs to be accessible by everyone
MP_PLATFORM.set_permissions(root_cert_path,
std::filesystem::perms::owner_all |
std::filesystem::perms::group_read |
std::filesystem::perms::others_read);
return {mp::utils::contents_of(cert_path), mp::utils::contents_of(priv_key_path)};
// Ensure that we can load both certificates
const auto root_cert = load_cert_from_file(root_cert_path.string());
const auto cert = load_cert_from_file(cert_path.toStdString());

if (root_cert && cert)
{
mpl::debug(kLogCategory,
"Certificates for the gRPC server (root: {}, subordinate: {}) are valid "
"X.509 files",
root_cert_path,
cert_path.toStdString());

// FIXME: Also check the validity period of the certificates to decide if they need
// to be re-generated

// Validate root cert is the issuer(signer) of the subordinate certificate
if (is_issuer_of(*root_cert.get(), *cert.get()))
{
mpl::info(kLogCategory, "Re-using existing certificates for the gRPC server");

// Unlike other daemon files, the root certificate needs to be accessible by
// everyone
MP_PLATFORM.set_permissions(root_cert_path,
std::filesystem::perms::owner_all |
std::filesystem::perms::group_read |
std::filesystem::perms::others_read);
return {mp::utils::contents_of(cert_path),
mp::utils::contents_of(priv_key_path)};
}

mpl::warn(kLogCategory,
"Existing root certificate (`{}`) is not the signer of the gRPC "
"server certificate (`{}`)",
root_cert_path,
cert_path.toStdString());
}
else
{
mpl::warn(kLogCategory,
"Could not load either of the root (`{}`) or subordinate (`{}`) "
"certificates for the gRPC server",
root_cert_path,
cert_path.toStdString());
}
}
mpl::info(kLogCategory, "Regenerating certificates for the gRPC server");

const auto priv_root_key_path = cert_dir.filePath(prefix + "_root_key.pem");

Expand All @@ -380,9 +447,14 @@ mp::SSLCertProvider::KeyCertificatePair make_cert_key_pair(const QDir& cert_dir,
{
if (QFile::exists(priv_key_path) && QFile::exists(cert_path))
{
// FIXME: The client does not respect the log level and this always get printed
// even on `multipass list`
// Re-enable it after fixing.
// mpl::trace(kLogCategory, "Re-using existing certificates for the gRPC client");
return {mp::utils::contents_of(cert_path), mp::utils::contents_of(priv_key_path)};
}

// mpl::trace(kLogCategory, "Regenerating certificates for the gRPC client");
const EVPKey client_cert_key{};
const X509Cert client_cert{client_cert_key, X509Cert::CertType::Client};
client_cert_key.write(priv_key_path);
Expand Down
3 changes: 3 additions & 0 deletions src/daemon/daemon_config.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,9 @@ std::unique_ptr<const mp::DaemonConfig> mp::DaemonConfigBuilder::build()
auto multiplexing_logger = std::make_shared<mpl::MultiplexingLogger>(std::move(logger));
mpl::set_logger(multiplexing_logger);

MP_UTILS.make_dir(QString::fromStdU16String(MP_PLATFORM.get_root_cert_dir().u16string()),
fs::perms::owner_all | fs::perms::group_exec | fs::perms::others_exec);

MP_PLATFORM.setup_permission_inheritance(true);

auto storage_path = MP_PLATFORM.multipass_storage_location();
Expand Down
21 changes: 7 additions & 14 deletions src/platform/platform_linux.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -189,14 +189,6 @@ std::string get_alias_script_path(const std::string& alias)

return aliases_folder.absoluteFilePath(QString::fromStdString(alias)).toStdString();
}

std::filesystem::path multipass_final_storage_location()
{
const auto user_specified_mp_storage = MP_PLATFORM.multipass_storage_location();
const auto mp_final_storage = user_specified_mp_storage.isEmpty() ? mp::utils::snap_common_dir()
: user_specified_mp_storage;
return std::filesystem::path{mp_final_storage.toStdString()};
}
} // namespace

std::unique_ptr<QFile> multipass::platform::detail::find_os_release()
Expand Down Expand Up @@ -471,11 +463,12 @@ std::string multipass::platform::host_version()
: fmt::format("{}-{}", QSysInfo::productType(), QSysInfo::productVersion());
}

std::filesystem::path mp::platform::Platform::get_root_cert_path() const
std::filesystem::path mp::platform::Platform::get_root_cert_dir() const
{
constexpr auto* root_cert_file_name = "multipass_root_cert.pem";
return mp::utils::in_multipass_snap()
? multipass_final_storage_location() / "data" / daemon_name / "certificates" /
root_cert_file_name
: std::filesystem::path{"/usr/local/share/ca-certificates"} / root_cert_file_name;
using Path = std::filesystem::path;
const auto base_dir = utils::in_multipass_snap()
? Path{utils::snap_common_dir().toStdString()} / "data"
: Path{"/usr/local/etc"};

return base_dir / daemon_name;
}
5 changes: 3 additions & 2 deletions src/platform/platform_osx.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -386,7 +386,8 @@ std::string mp::platform::reinterpret_interface_id(const std::string& ux_id)
return ux_id;
}

std::filesystem::path mp::platform::Platform::get_root_cert_path() const
std::filesystem::path mp::platform::Platform::get_root_cert_dir() const
{
return std::filesystem::path{"/Library/Keychains/multipass_root_cert.pem"};
static const std::filesystem::path base_dir = "/usr/local/etc";
return base_dir / daemon_name;
}
Loading
Loading