tls: inline delivery of TLS certificates and private keys.#2248
tls: inline delivery of TLS certificates and private keys.#2248mattklein123 merged 11 commits intoenvoyproxy:masterfrom
Conversation
File-based BoringSSL APIs are no longer used and everything is processed as if it was delivered inline in order to provide consistent behavior. Additionally, DataSource files are now read during configuration parsing, which means that the configuration is considered invalid if it refers to non-existing files. Fixes envoyproxy#1357. Signed-off-by: Piotr Sikora <piotrsikora@google.com>
Signed-off-by: Piotr Sikora <piotrsikora@google.com>
|
@mattklein123 @ggreenway @jvshahid Please take a look. |
| "alt_alpn_protocols": "http/1.1", | ||
| "cert_chain_file": "/etc/envoy/cert.pem", | ||
| "private_key_file": "/etc/envoy/key.pem" | ||
| "cert_chain_file": "certs/servercert.pem", |
There was a problem hiding this comment.
Quick comment which unfortunately is going to make this more complicated: We are going to need to fix config_test to work with some type of mock loader that doesn't read files from disk. Optimally it would verify everything else in SSL and possibly just use fake valid cert data. The reason this is going to be needed is we run at Lyft config tests for configs where the certs are not available on the box where the tests are run.
Ultimately I think we know this is working when none of the example config stuff here changed as part of this diff.
There was a problem hiding this comment.
I have mixed feelings about validity of this approach, but I'll add this if you need it.
There was a problem hiding this comment.
I'm sorry, but we definitely need it. I would just figure out whatever is easiest in that test, even if you have to mock out more stuff.
There was a problem hiding this comment.
Sorry, I think I might have misunderstood what you were saying.
I assumed that you meant letting //test/config_test:example_configs_test pass without reading certificates from disk, but after some digging, I've noticed that //test/tools/config_load_check:config_load_check_tool uses the same code, so you probably meant that you're using this tool to verify production configs and want to test them without access to the real certificates? Is that correct?
There was a problem hiding this comment.
We actually use both a private version of the config test as well as the config load check tool (in different places). We need both to run in a mode that doesn't require the actual cert files to exist (because yes we test prod configs where the certs don't live). IMO it's fine to just have them both work the same way for now (don't load) which is not a regression from what happened before, so any kind of mock is fine. The optimal solution would be to not use a full mock TLS context loader so we can check other config options, but to only mock the actual cert loading part. I have no idea if that is easy or not.
There was a problem hiding this comment.
We can open an issue to do what you describe, but if we want to merge this sooner we need to not break existing behavior. IMO having a flag on whether to check the cert or not makes a lot of sense.
There was a problem hiding this comment.
I agree with @ggreenway, it's one thing to mock things in tests, but faking certificates when testing production config sounds like a really bad idea, and if you're testing those on a machine without access to real certificates, then you should provide self-signed certs. Otherwise, you might refer to certificates that don't exist (e.g. typo).
Furthermore, we'd need to add same hacks to --mode validate, which sounds even worse.
Yes, it's change of current behavior, but I don't see how this justifies adding error-prone hacks to the code.
There was a problem hiding this comment.
I hear what you are saying, but, that's the way it works today, and this PR will break us. If we want to hold this PR until we fix the way we test things, we can do that, but it will take 1-2 weeks since we will need to schedule the work.
There was a problem hiding this comment.
I'd prefer to wait, then.
There was a problem hiding this comment.
Alright I will make an internal ticket and get someone to work on cutting us over to fake certs for testing next week.
| @@ -135,12 +153,7 @@ ServerContextConfigImpl::ServerContextConfigImpl(const envoy::api::v2::Downstrea | |||
| // TODO(PiotrSikora): Support multiple TLS certificates. | |||
| // TODO(mattklein123): All of the ASSERTs in this file need to be converted to exceptions with | |||
There was a problem hiding this comment.
@PiotrSikora as long as you are in here, can you take a look at the rest of the asserts in this file? I think some of them could potentially happen/crash with native v2 config. I might be wrong, but could you take a look and potentially fix to throw/add tests? (Follow up is fine if that is better).
There was a problem hiding this comment.
Sure, I'll do in a separate PR.
| uint32_t num_tested = 0; | ||
| for (const std::string& filename : TestUtility::listFiles(directory, true)) { | ||
| // Change working directory, otherwise we won't be able to read files using relative paths. | ||
| RELEASE_ASSERT(::chdir(directory.c_str()) == 0); |
There was a problem hiding this comment.
Per above we need this to still work with cert files that can't be read. Sorry. :(
| : ""), | ||
| cert_chain_(config.tls_certificates().empty() | ||
| ? "" | ||
| : readDataSource(config.tls_certificates()[0].certificate_chain(), true)), |
There was a problem hiding this comment.
Is there a reasonable way to make the initializors here a bit cleaner, e.g. with anonymous namespace functions etc.
There was a problem hiding this comment.
FWIW I prefer to have it all inline; having to track down a bunch of functions to understand how everything is initialized makes it harder to read, IMO.
There was a problem hiding this comment.
While I agree that this isn't the prettiest code, I'm with Greg on this, it's much more readable here.
| for (;;) { | ||
| bssl::UniquePtr<X509> cert(PEM_read_bio_X509(bio.get(), nullptr, nullptr, nullptr)); | ||
| if (cert == nullptr) { | ||
| break; |
There was a problem hiding this comment.
Do we have full test coverage for all these branches and exception cases?
There was a problem hiding this comment.
Unlikely, I'll add some.
| bssl::UniquePtr<BIO> bio( | ||
| BIO_new_mem_buf(const_cast<char*>(config.caCert().data()), config.caCert().size())); | ||
| RELEASE_ASSERT(bio != nullptr); | ||
| // Based on BoringSSL's X509_load_cert_crl_file(). |
There was a problem hiding this comment.
Not familiar enough with BoringSSL to know if this is the best we can do. Is there no way to do a non-filesystem version of things like SSL_CTX_load_verify_locations other than reimplementing bits of BoringSSL? Seems like the kind of place tmpfs could be useful but this isn't portable or even easy to manage at user privilege levels. @davidben for thoughts as well.
There was a problem hiding this comment.
It's unfortunate that the openssl/boringssl has nice APIs for files, but not an analogous API for a BIO. But since that seems to be the way it is for now, I think this is the best we can do. I think a tmpfs is a huge can of worms to open up, with security and operational issues.
There was a problem hiding this comment.
Unfortunately, there are no non-filesystem equivalents of the APIs we use, and even if I could add them today, we're on the chromium-stable branch, which is ~2 months behind the master branch, so it wouldn't help anyway.
I have a mid-term plan to migrate away from X509 APIs to BoringSSL-specific CRYPTO_BUFFERs and associated APIs, but that's further down the line, and it requires custom certificate verification logic.
| * @return The CA certificate to use for peer validation. | ||
| */ | ||
| virtual const std::string& caCertFile() const PURE; | ||
| virtual const std::string& caCert() const PURE; |
There was a problem hiding this comment.
Would it make more sense to return std::vector<uint8_t> or something? Although protobuf uses std::string for non-string buffers, so we might be stuck with this.
There was a problem hiding this comment.
Maybe? I'm fine with either, but as you noted, protobuf uses std::string for the bytes inline field, so we might as well stick to it.
There was a problem hiding this comment.
Agreed; let's stick with what you had.
| int rc = SSL_CTX_load_verify_locations(ctx_.get(), config.caCertFile().c_str(), nullptr); | ||
| if (0 == rc) { | ||
| if (!config.caCert().empty()) { | ||
| ca_file_path_ = config.caCertPath(); |
There was a problem hiding this comment.
The only place this value is used is in ContextImpl::getCaCertInformation() (just to print the path of the cert, which is output by an admin handler). Can we just get rid of it entirely? It made sense when all config pointed to a path on disk; it makes less sense now and adds complication.
There was a problem hiding this comment.
It's also used in the error messages.
| bssl::UniquePtr<BIO> bio( | ||
| BIO_new_mem_buf(const_cast<char*>(config.caCert().data()), config.caCert().size())); | ||
| RELEASE_ASSERT(bio != nullptr); | ||
| // Based on BoringSSL's X509_load_cert_crl_file(). |
There was a problem hiding this comment.
It's unfortunate that the openssl/boringssl has nice APIs for files, but not an analogous API for a BIO. But since that seems to be the way it is for now, I think this is the best we can do. I think a tmpfs is a huge can of worms to open up, with security and operational issues.
…ine. Signed-off-by: Piotr Sikora <piotrsikora@google.com>
Signed-off-by: Piotr Sikora <piotrsikora@google.com>
Signed-off-by: Piotr Sikora <piotrsikora@google.com>
Signed-off-by: Piotr Sikora <piotrsikora@google.com>
Signed-off-by: Piotr Sikora <piotrsikora@google.com>
Signed-off-by: Piotr Sikora <piotrsikora@google.com>
ggreenway
left a comment
There was a problem hiding this comment.
Overall this looks good. A few small comments.
And we need to wait until @mattklein123 or someone from lyft says that they're ready for the config-test change in behavior.
include/envoy/ssl/context_config.h
Outdated
|
|
||
| /** | ||
| * @return The certificate chain file used to identify the local side. | ||
| * @return Path of the CA certificate to use for peer validation. |
There was a problem hiding this comment.
For each of the *Path() functions, please document that it will return an empty string in the case of inline material.
| ? "" | ||
| : config.tls_certificates()[0].private_key().filename()), | ||
| ca_cert_(readDataSource(config.validation_context().trusted_ca(), true)), | ||
| ca_cert_path_(config.validation_context().trusted_ca().specifier_case() == |
There was a problem hiding this comment.
Please add a helper for getting the path in the case it is kFilename, since this code is repeated several times.
| namespace Envoy { | ||
| namespace Ssl { | ||
|
|
||
| static const std::string INLINE_STRING = "<inline>"; |
There was a problem hiding this comment.
From an interface point of view, I'd rather have all these functions return empty string if they're inline, and then change that to this human-readable string right before it is presented to a user. But I don't feel strongly about it.
There was a problem hiding this comment.
From an interface point of view, I think it's better to have this logic self-contained here, instead of intermixed with the code. I don't feel too strongly about it, but I'm going to leave it as-is.
There was a problem hiding this comment.
That's fine; if someone ever needs it different, it's easy enough to change then.
source/common/ssl/context_impl.cc
Outdated
| fmt::format("Failed to load certificate chain from {}", config.certChainPath())); | ||
| } | ||
| // Read rest of the certificate chain. | ||
| for (;;) { |
There was a problem hiding this comment.
"while (true)" is the convention used in this codebase
|
@rodaine is going to fix our internal tests. He can comment when it is done. Hopefully this week. |
Signed-off-by: Piotr Sikora <piotrsikora@google.com>
include/envoy/ssl/context_config.h
Outdated
|
|
||
| /** | ||
| * @return The certificate chain file used to identify the local side. | ||
| * @return Path of the CA certificate to use for peer validation or an empty string |
There was a problem hiding this comment.
This returns "<inline>" not "" for inline data.
Sorry, I made my original comment before I'd gotten to the implementation of the interface.
There was a problem hiding this comment.
Sigh, I should have known better! Thanks.
Signed-off-by: Piotr Sikora <piotrsikora@google.com>
|
@PiotrSikora You also have a merge conflict in the release notes that needs resolution. |
mattklein123
left a comment
There was a problem hiding this comment.
Thanks, looks sweet once we fix our internal tooling.
|
@ggreenway yeah, I know... but even if I resolve it now, I expect that there will be another conflict on the release notes before we get a green light. |
|
Working on this today. Will keep you posted! |
…ine. Signed-off-by: Piotr Sikora <piotrsikora@google.com>
303a4c1
| RELEASE_ASSERT(::chdir(directory.c_str()) == 0); | ||
| uint32_t num_tested = 0; | ||
| for (const std::string& filename : TestUtility::listFiles(directory, true)) { | ||
| for (const std::string& filename : TestUtility::listFiles(directory, false)) { |
There was a problem hiding this comment.
Out of curiosity, is there a reason this recursive flag was flipped to false?
There was a problem hiding this comment.
Unit test certificates are in the certs sub-directory, and we don't want to load configuration from them.
There was a problem hiding this comment.
mattklein123
left a comment
There was a problem hiding this comment.
I think we are good to go with this. Thank you!
Bintray has been deprecated and should be removed as a source. Signed-off-by: ben@ben.cm Signed-off-by: JP Simard <jp@jpsim.com>
Bintray has been deprecated and should be removed as a source. Signed-off-by: ben@ben.cm Signed-off-by: JP Simard <jp@jpsim.com>
File-based BoringSSL APIs are no longer used and everything is processed
as if it was delivered inline in order to provide consistent behavior.
Additionally, DataSource files are now read during configuration parsing,
which means that the configuration is considered invalid if it refers to
non-existing files.
Fixes #1357.
Signed-off-by: Piotr Sikora piotrsikora@google.com