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

Standard grpc mTLS #3909

Open
wants to merge 111 commits into
base: change-permissions-on-instances
Choose a base branch
from

Conversation

georgeliao
Copy link
Contributor

@georgeliao georgeliao commented Jan 30, 2025

MULTI-1765
Public side of https://github.com/canonical/multipass-private/pull/719

A few things to note
The make_cert_key_pair function originally handles the generation of both server and client key-certificate pairs . The server_name parameter determines the use case, when server_name is an empty string, it indicated the client key-certificate case. In the standard gRPC TLS scheme, the root key-certificate pair is added.

Now, the role of the make_cert_key_pair function is still generating and holding server and client key-certificate pairs. However, the server certificate has evolved into a signed server certificate. As the result, the server side branch must first generate root key-certificate pair and use it to sign the newly created server certificate.

Previously the constructor of X509Cert only handled server and client certifcate generation, distinguishing between them based on whether server_address was empty. Now, with the introduction of root, server and client certifcate, the dispatch is managed by using the CertType enumeration.

Additionally, the X509Cert constructor has been refined to generate certificates in a standard format. Key differences between before and after include adjustments to the serial number format and the inclusion of X509v3 extensions. To inspect a certificate, you can use the following command: openssl x509 -in <cert_path> -noout -text

The certificate paths are as follows:

  • Server certificate:/root/.local/share/multipassd/certificates/localhost.pem
  • Client certificate: /home/<user name>/.local/share/multipass-client-certificate/multipass_cert.pem
  • Root certificate: /usr/local/share/ca-certificates/multipass_root_cert.pem

Snap environment considerations
In the snap environment, the root certificate is stored at /var/snap/multipass/common/data/multipassd/certificates/multipass_root_cert.pem
Unlike /root/, /var/snap/ directory allows other users view its files, making this setup feasible.

Certificate regeneration and migration
The root certificate and server key-certificate pairs area automatically regenerated if either is missing. This mechanism ensures a smooth server key-certificate pair migration when updating Multipass. Upon upgrading, the server startup process will automatically generates root key-certificate pair and use it to sign a fresh server certificate. Consequently, the original server key-certificate pair will be overwritten, enabling successful verification under the new standard grpc TLS.

The Multipass upgrade process should be included in the functional testing as well, both the cmd and gui clients should be tested.

Unit test adaptations
The unit tests have been modified to accommodate changes in the gRPC TLS verification process. The key adjustments include:

  1. Mocking the get_root_cert_path to allow usage of string-based certificates.
  2. Merge the two server and client key-certificate pairs into one, With MockCertProvider being used on both server side and client to provide key-certificate pair. In the unit testing environment, they can be the same.

Following the commit history and messages is also a helpful way to understand changes that have been made.

@georgeliao georgeliao changed the title Standard grpc mTLs Standard grpc mTLS Jan 30, 2025
@georgeliao georgeliao marked this pull request as draft January 30, 2025 11:42
Copy link

codecov bot commented Jan 31, 2025

Codecov Report

Attention: Patch coverage is 94.07407% with 8 lines in your changes missing coverage. Please review.

Project coverage is 89.19%. Comparing base (e6fee0a) to head (c3ea497).

Files with missing lines Patch % Lines
src/cert/ssl_cert_provider.cpp 96.52% 4 Missing ⚠️
src/daemon/daemon_config.cpp 57.14% 3 Missing ⚠️
src/utils/utils.cpp 66.66% 1 Missing ⚠️
Additional details and impacted files
@@                         Coverage Diff                         @@
##           change-permissions-on-instances    #3909      +/-   ##
===================================================================
+ Coverage                            89.11%   89.19%   +0.07%     
===================================================================
  Files                                  256      256              
  Lines                                14643    14684      +41     
===================================================================
+ Hits                                 13049    13097      +48     
+ Misses                                1594     1587       -7     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@georgeliao georgeliao force-pushed the standard_grpc_mTLS branch 5 times, most recently from 5957e41 to c26c20c Compare February 7, 2025 09:03
@georgeliao georgeliao marked this pull request as ready for review February 7, 2025 09:59
Copy link
Contributor

@andrei-toterman andrei-toterman left a comment

Choose a reason for hiding this comment

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

Just a quick pass with some nitpicks, didn't test functionally yet.

@andrei-toterman andrei-toterman self-requested a review February 7, 2025 15:01
xmkg
xmkg previously approved these changes Feb 11, 2025
Copy link
Contributor

@xmkg xmkg left a comment

Choose a reason for hiding this comment

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

LGTM, except for one small nit.

EDIT: Updated review for the new code changes.

Copy link
Contributor

@andrei-toterman andrei-toterman left a comment

Choose a reason for hiding this comment

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

Hey, @georgeliao! So now, in the local environment, the GUI works fine both when upgrading from the old certificates and when starting with no certificates. But in the case of the CLI, it works when starting with no certificates, but if I upgrade from old certificates, I get the following error

E0211 14:23:49.406977684   36809 ssl_transport_security.cc:1316]       Handshake failed with fatal error SSL_ERROR_SSL: error:0A000086:SSL routines::certificate verify failed.

And in the snap, both when starting fresh or when upgrading, I get the following

[error] [client] Caught an unhandled exception: failed to open file '/var/snap/multipass/commondata/multipassd/certificates/multipass_root_cert.pem': No such file or directory(2)

@georgeliao
Copy link
Contributor Author

E0211 14:23:49.406977684 36809 ssl_transport_security.cc:1316] Handshake failed with fatal error SSL_ERROR_SSL: error:0A000086:SSL routines::certificate verify failed.

About this root certificate is unsync with server certificate issue in development environment, maybe we can add a check at here, which checks not only the existence of the certificates but also whether the root certificate is the one who signed the server certificate. If not, everything will be re-created. This check can be done by openssl c-api. However, not sure the juice worth the squeeze.

[error] [client] Caught an unhandled exception: failed to open file '/var/snap/multipass/commondata/multipassd/certificates/multipass_root_cert.pem': No such file or directory(2)

This is fixed in the latest version of the PR.

@andrei-toterman
Copy link
Contributor

Yeah, for the dev environment, I don't think it's worth doing the effort. We already have to remove stuff manually, so this is just an extra thing to remove.

Copy link
Contributor

@andrei-toterman andrei-toterman left a comment

Choose a reason for hiding this comment

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

Great, now both the CLI and the GUI work as expected, both locally and in the snap, both when upgrading or installing fresh. Thanks, @georgeliao!

Now, one thing to discuss with everyone: the purpose of these changes was so that we could remove our grpc patches, and that goal is accomplished by this PR. And now the CLI verifies the server certificate, which couldn't be avoided without the patches. But the GUI can avoid verifying the server certificate, using the plain vanilla grpc dart library. My question is: should the GUI also verify the server certificate, to be in line with the CLI, or should we keep the existing, functioning behavior of not having the GUI verify the server certificate?

@ricab
Copy link
Collaborator

ricab commented Feb 12, 2025

As discussed elsewhere, let's have the GUI also adhere to the new scheme, preferably in a separate PR.

Copy link
Collaborator

@ricab ricab left a comment

Choose a reason for hiding this comment

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

Thanks @georgeliao, lots of good work in here!

I am only doing a tertiary review in this case, so I glossed over most of the scary low-level certificate stuff. @andrei-toterman and @xmkg, we're relying on you on that front 💪 If you could provide assurance that you've verified sanity of all the nitty-gritty, that would be much appreciated. All of this would be for nothing if security were somehow broken on the foundation...

Other than that, I have some proposals for path derivation and the initialization. Let me know what you think.

@georgeliao georgeliao force-pushed the standard_grpc_mTLS branch 2 times, most recently from ac0e4ee to ee42308 Compare February 17, 2025 10:05
…ter instead of raw pointer
…an alias on unique pointer type.
@georgeliao georgeliao force-pushed the standard_grpc_mTLS branch from 72123e8 to b277f18 Compare March 4, 2025 14:24

Verified

This commit was created on GitHub.com and signed with GitHub’s verified signature.

Verified

This commit was created on GitHub.com and signed with GitHub’s verified signature.
}
else
{
if (result <= 0)
Copy link
Contributor

@xmkg xmkg Mar 5, 2025

Choose a reason for hiding this comment

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

I think this check will depend on the C API being used. Some of the C API's use 0 to indicate success. We can decouple the checker implementation to have more flexibility:

// tag type to distinguish checker type
struct checker_base{};

// generic enough, can be placed in utils
struct nullptr_checker : private checker_base {
    bool operator()(const void* v){
        return v == nullptr;
    }
};

// openssl specific checker, better to be placed in a more localized file
struct ossl_checker : private checker_base{
    bool operator()(int v){
        return v == 1;
    }
};

template <typename CheckerT = nullptr_checker, typename T>
auto check(T result, const std::string& errorMessage) -> typename std::enable_if<std::is_base_of_v<checker_base, CheckerT>, void>::type
{
    if(!CheckerT{}(result)){
        throw std::runtime_error{errorMessage};
    }
}

usage:

int test(){
    return 5;
}

void* test_2(){
return nullptr;
}

int main(void){
    check<ossl_checker>(test(), "");
    check<>(test_2(), "failure");
    // check<int>(test(), ""); -> wont compile
}

Copy link
Contributor

Choose a reason for hiding this comment

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

It'd also be good for troubleshooting if we could incorporate the error code into the thrown error.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

The error code can be added. However, about custom checker injection. It becomes that the user have to specify the checker even within the openssl c-api return pointer or integers cases. That defies one of the purpose of the utility function, abstraction. For now, I will move the utility function into ssl_cert_provider.cpp as a dedicated openssl c-api checker instead of trying to be a global c-api checker. Making a general checker for all c-apis (openssl, hyperv, libssh) can be done in another PR.

Copy link
Contributor Author

@georgeliao georgeliao Mar 5, 2025

Choose a reason for hiding this comment

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

It is a very good concern you brought up.

Copy link
Contributor

Choose a reason for hiding this comment

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

It becomes that the user have to specify the checker even within the openssl c-api return pointer or integers cases

We can create local aliases for such cases, but the gist of it is that we have to leave a customization point for the condition (e.g., a predicate or a dedicated type) if we want it to be a global utility, so different API result codes can be accommodated too.

For now, I will move the utility function into ssl_cert_provider.cpp as a dedicated openssl c-api checker instead of trying to be a global c-api checker.

I'm OK with that, we can migrate to the global one later if we decide to implement one.

Copy link
Contributor

Choose a reason for hiding this comment

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

... or we can adopt a rather simple approach:

struct PreconditionFailureError : std::runtime_error {
    using std::runtime_error::runtime_error;
};

void expects(bool what, const std::string& failure_msg) {
    if (!what) {
        throw PreconditionFailureError(failure_msg);
    }
}

int test() { return 5; }
int main(void) { 
    expects(test() == 4, "test failed."); 
}

Copy link
Contributor Author

@georgeliao georgeliao Mar 6, 2025

Choose a reason for hiding this comment

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

Yep, that definitely can be a candidate for a global C-api checker.

@georgeliao georgeliao force-pushed the standard_grpc_mTLS branch from bd1143f to f17700d Compare March 5, 2025 14:22
Copy link
Collaborator

@ricab ricab left a comment

Choose a reason for hiding this comment

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

Hey @georgeliao, I've reviewed the permissions part and it looks mostly good to me, provided you're in sync with @Sploder12 on it. Only a couple of things inline

@@ -109,7 +109,7 @@ 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_PLATFORM.setup_permission_inheritance();
MP_PLATFORM.setup_permission_inheritance(false);
Copy link
Collaborator

Choose a reason for hiding this comment

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

Why keep a line of code that has no effect though?

Also, couldn't we keep this true and override selectively?

Comment on lines 199 to 203
MP_PLATFORM.set_permissions(storage_path.toStdU16String(), fs::perms::owner_all | fs::perms::others_exec);
}
else
{
MP_PERMISSIONS.restrict_permissions(data_directory.toStdU16String());
MP_PLATFORM.set_permissions(data_directory.toStdU16String(), fs::perms::owner_all | fs::perms::others_exec);
Copy link
Collaborator

Choose a reason for hiding this comment

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

What happens to the "takeown" part of the replaced calls? Is it no longer necessary? is it applied somewhere else?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I did not get this question. What is "takeown" part ? You mean setting owner only permission?

Copy link
Collaborator

Choose a reason for hiding this comment

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

The calls to restrict_permissions, which are now replaced, used to take ownership of the files (beside setting permissions). I wonder if you really meant to skip that and, if so, why. Do we no longer need it, or are we doing the same somewhere else?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Ok, now I get the question. That is a very good point got brought up. I am also confused the fact that why we need it in the first place. Were all files and folders in the storage location owned by root user already? @Sploder12 Maybe you can share some context?

Copy link
Contributor

Choose a reason for hiding this comment

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

If multipassd created the files and folders, they'd be owned by the root user already. So it's usually redundant and probably unneeded

georgeliao and others added 4 commits March 6, 2025 10:30

Verified

This commit was created on GitHub.com and signed with GitHub’s verified signature.
Co-authored-by: Ricardo Abreu <[email protected]>
Signed-off-by: George Liao <[email protected]>

Verified

This commit was created on GitHub.com and signed with GitHub’s verified signature.
…p the certificates folder and cert files.
…ts sub-folder."

This reverts commit 26415bc.

Revert "[qemu platform] enforce ower_all permission to network sub-folder."

This reverts commit 6a1879f.

Revert "[open ssh] enforce  permission to ssh-keys sub-folder."

This reverts commit b2afed4.

Revert "[vault] enforce ower_all permission to vault sub-folder."

This reverts commit 1ad954d.

Revert "[daemon] enforce ower_all permission to multipassd-vm-instances.json file."

This reverts commit 72f55a9.
Step1: recursively restrict everything before the creation of certificates directory, opens up the data directory only for other user read.
Step 2: create certificate directory and certificates with the right permission. Or overwrite the permissions in the case of existing certificates directory and certificate files.
Copy link
Contributor

@xmkg xmkg left a comment

Choose a reason for hiding this comment

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

LGTM 👍 Good work!

Copy link
Collaborator

@ricab ricab left a comment

Choose a reason for hiding this comment

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

Yep, looks good to me, modulo a small request inline. Thanks Jia!

Comment on lines 283 to 285
const std::filesystem::path cert_path_std = cert_path.toStdU16String();
MP_PLATFORM.set_permissions(cert_path_std,
std::filesystem::perms::owner_all | std::filesystem::perms::others_read);
Copy link
Collaborator

Choose a reason for hiding this comment

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

I would add group_all just to be safe. Otherwise, a user who happened to be in the group but wasn't root would be unable to read, I think.

The same reasoning would apply in all other cases where you're giving others permissions.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I would add group_all just to be safe. Otherwise, a user who happened to be in the group but wasn't root would be unable to read, I think.

I did not get this part. Whether the user is in the group or not, he is still the other user and std::filesystem::perms::others_read guarantees that he can read.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

After looking into some literature, I think I have a misconception and the group read permission can be added.

@georgeliao georgeliao force-pushed the standard_grpc_mTLS branch from 1a67a88 to 180db32 Compare March 7, 2025 14:05
@georgeliao georgeliao force-pushed the standard_grpc_mTLS branch from 45a6b64 to b4bfdce Compare March 7, 2025 21:19
…ssing root certificate.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

5 participants