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

tls: add local certificate presented flag for mTLS detection #8464

Closed
wants to merge 5 commits into from

Conversation

kyessenov
Copy link
Contributor

Signed-off-by: Kuat Yessenov kuat@google.com

Description: add a boolean flag to report whether a local cert is presented. This is useful for upstream mTLS detection.
Risk Level: low
Testing: unit tests updated
Docs Changes: none
Release Notes: none

Signed-off-by: Kuat Yessenov <kuat@google.com>
@kyessenov kyessenov requested a review from lizan as a code owner October 2, 2019 18:50
@kyessenov
Copy link
Contributor Author

cc @lizan @nrjpoddar in ref to istio/proxy#2434

Signed-off-by: Kuat Yessenov <kuat@google.com>
@@ -306,6 +306,11 @@ bool SslSocketInfo::peerCertificatePresented() const {
return cert != nullptr;
}

bool SslSocketInfo::localCertificatePresented() const {
bssl::UniquePtr<X509> cert(SSL_get_certificate(ssl_.get()));
Copy link
Member

Choose a reason for hiding this comment

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

IIRC SSL_get_certificate doesn't increment the refcount of X509 so this shouldn't be UniquePtr, ASAN will catch if that's the case likely.

Signed-off-by: Kuat Yessenov <kuat@google.com>
@@ -306,6 +306,11 @@ bool SslSocketInfo::peerCertificatePresented() const {
return cert != nullptr;
}

bool SslSocketInfo::localCertificatePresented() const {
X509* cert = SSL_get_certificate(ssl_.get());

Choose a reason for hiding this comment

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

IIUC this just verifies existence of a client cert and not an acknowledgement of it's verification by the server?

Copy link
Member

Choose a reason for hiding this comment

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

I don't think there's a way to know whether server verified, you know whether the handshake is successful or not, that doesn't indicate server verified though. I might be wrong, cc @PiotrSikora

Copy link
Contributor

Choose a reason for hiding this comment

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

@nrjpoddar is correct, the code in this PR (as of right now) only verifies that the TLS client certificate is configured, and not that the established connection is mutually authenticated.

We could potentially install SSL_CTX_set_cert_cb and call SSL_get_client_CA_list to check if peer requested local certificate, but I'm not aware of any API that allows us to check this directly.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

That actually means that this is not a new problem. When we report mTLS using peerCertificatePresented on the server, that does not mean that the certificate was validated by the server. So at least in istio context, we should make it clear that mTLS attributes are necessary but not sufficient for true mTLS. For telemetry purposes, this is fine, but for policy, it's not safe to rely on peerCertificatePresented or localCertificatePresented. It's entirely possible that both parties present certs but do no validation.

Anyways, I think this PR is still useful for upstream mTLS bit.

Choose a reason for hiding this comment

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

Totally agree, minor nit the name for this API should be localCertificateConfigured for correctness.

Copy link

@nrjpoddar nrjpoddar Oct 4, 2019

Choose a reason for hiding this comment

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

It has been great learning just watching the thread. We are mixing few different things here, that I hope to clarify:

First, how can I tell a particular connection is mTLS reliably from a perspective of one peer i.e. client or server. I don't think this is always possible but can be closely approximated by this logic.

  • client reports mTLS true if it verified server certificate, presented local certificate & handshake was successful
  • server reports mTLS true if it presented local certificate, verified client certificate & handshake was successful
  • If both side reports mTLS false it is definitely plain text
  • If one side reports false and other true, we cannot definitely say if the connection in mTLS, TLS or plaintext
  • If both sides report mTLS true, we can for 99% of cases say that the connection was mTLS.

The second issue we are trying to solve here is get enough information via Envoy to figure out a config distribution problem i.e. control plane says use mTLS but the client/server never presents a certificate. In this scenario exposing the individual bits of information on a peer like requested peer certificate, presented local certificate & verified peer certificate helps.

The SSL session resumption afaik should just carry forward the mTLS boolean and other information bits that is stored the first time the handshake was finished.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Neeraj, agree with the mTLS detection assessment.

Copy link
Contributor

Choose a reason for hiding this comment

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

@davidben I suggested to install callback using SSL_CTX_set_cert_cb AND use SSL_get_client_CA_list inside the callback to detect if server requested client certificate that matches issuer of the configured client certificate (although, we don't do that currently, so it might be an overkill for this PR), i.e. it was a single suggestion, not two. Sorry if that wasn't clear enough.

Regarding session resumption, you are, of course, right (and thank you for the reminder, I keep forgetting about this more often that I should have)...

For clients, we could store this bit (and possibly others) when adding session keys to the client-side cache in Envoy, and then report value used to establish the original connection.

For servers, we rely on BoringSSL for caching, and we don't store anything in Envoy, so that would be tricky... but we include all configuration related to the client certificate verification in SSL_CTX_set_session_id_context, so sessions cannot be resumed across different configurations, which means that we can infer mTLS bit from the successful handshake and the server-side configuration alone.

Copy link
Contributor

Choose a reason for hiding this comment

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

What would be the benefit of checking SSL_get_client_CA_list? BoringSSL sends the client certificate, if configured + requested, independent of SSL_get_client_CA_list. The CA list is purely a hint. On mismatch, likely the handshake will fail (in which case you don't care). If it succeeds anyway, then the server accepted your client certificate. (Or maybe it silently ignored it, but it's always possible for the server to silently ignore it, which gets to the question of what you're even measuring.)

For servers, we rely on BoringSSL for caching, and we don't store anything in Envoy, so that would be tricky

This is easy for servers. Just check if SSL_get_peer_certificate returns anything. We store the peer certificate in the session because that's a critical part of authentication. We don't store the local certificate because that's a waste of memory and ticket size. But, as a server, you always send a local certificate. It's the client certificate that's optional.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Let's not check for SSL_get_client_CA_list. That does not handle hash verification on the server-side. I think checking that a client certificate was requested is sufficient for now.

Signed-off-by: Kuat Yessenov <kuat@google.com>
Signed-off-by: Kuat Yessenov <kuat@google.com>
@kyessenov
Copy link
Contributor Author

@PiotrSikora It looks like envoy only support server session resumption. That means the bit is still correct since we always set it to true on the server side.

ssl_.get(),
[](SSL*, void* arg) -> int {
auto info = static_cast<SslSocketInfo*>(arg);
info->local_cert_presented = true;
Copy link
Contributor

Choose a reason for hiding this comment

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

I don't think this is sufficient, since it's not verifying that the client certificate is configured, and I think that this callback will be called even if it isn't (I might be wrong, so it's worth double-checking that).

This should work better:
info->local_cert_presented = (SSL_get_certificate(ssl) != nullptr);

Ideally, local_cert_presented should be set to true only if the issuer of the client certificate matches one of the CAs that server requested a certificate for (using SSL_get_client_CA_list and SSL_get0_certificate_types), but we're not checking for that before sending the certificate, and server is going to close the connection if they don't match anyway, so it might be an overkill if we want to use this only for telemetry.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

We assume that handshake completes successfully. That means all those checks (there is a client cert, and CA trust chain, and other things) are already done. Why do we need another set of checks that duplicate the handshake?

@@ -301,11 +301,29 @@ void SslSocket::shutdownSsl() {
}
}

SslSocketInfo::SslSocketInfo(bssl::UniquePtr<SSL> ssl, InitialState state) : ssl_(std::move(ssl)) {
if (state == InitialState::Client) {
SSL_set_cert_cb(
Copy link
Contributor

Choose a reason for hiding this comment

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

It would be more efficient to configure this once in ClientContextImpl using SSL_CTX_set_cert_cb instead of here, when it's called for each connection.

Also, installing it in ClientContextImpl means that there is access to the configuration values, so it might make sense to install this callback only when the client certificate is configured.

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'm not sure how to pass SSL info via context. Can you be more specific about your suggestion?

@@ -306,6 +306,11 @@ bool SslSocketInfo::peerCertificatePresented() const {
return cert != nullptr;
}

bool SslSocketInfo::localCertificatePresented() const {
X509* cert = SSL_get_certificate(ssl_.get());
Copy link
Contributor

Choose a reason for hiding this comment

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

@davidben I suggested to install callback using SSL_CTX_set_cert_cb AND use SSL_get_client_CA_list inside the callback to detect if server requested client certificate that matches issuer of the configured client certificate (although, we don't do that currently, so it might be an overkill for this PR), i.e. it was a single suggestion, not two. Sorry if that wasn't clear enough.

Regarding session resumption, you are, of course, right (and thank you for the reminder, I keep forgetting about this more often that I should have)...

For clients, we could store this bit (and possibly others) when adding session keys to the client-side cache in Envoy, and then report value used to establish the original connection.

For servers, we rely on BoringSSL for caching, and we don't store anything in Envoy, so that would be tricky... but we include all configuration related to the client certificate verification in SSL_CTX_set_session_id_context, so sessions cannot be resumed across different configurations, which means that we can infer mTLS bit from the successful handshake and the server-side configuration alone.

Copy link
Contributor

@PiotrSikora PiotrSikora left a comment

Choose a reason for hiding this comment

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

@PiotrSikora It looks like envoy only support server session resumption. That means the bit is still correct since we always set it to true on the server side.

Envoy supports both: server- and client-side session resumption (see: #4791).

@stale
Copy link

stale bot commented Oct 15, 2019

This pull request has been automatically marked as stale because it has not had activity in the last 7 days. It will be closed in 7 days if no further activity occurs. Please feel free to give a status update now, ping for review, or re-open when it's ready. Thank you for your contributions!

@stale stale bot added the stale stalebot believes this issue/PR has not been touched recently label Oct 15, 2019
@stale
Copy link

stale bot commented Oct 22, 2019

This pull request has been automatically closed because it has not had activity in the last 14 days. Please feel free to give a status update now, ping for review, or re-open when it's ready. Thank you for your contributions!

@stale stale bot closed this Oct 22, 2019
@kyessenov kyessenov mentioned this pull request Oct 24, 2019
lizan pushed a commit that referenced this pull request Oct 24, 2019
Description: update CEL runtime to tighten the complexity bounds:
- remove comprehension operators (forall, exists)
- remove list and string concat to avoid memory allocation
- limit RE2 regex max program size
- remove string conversion to avoid string allocation

Risk Level: low
Testing: unit tests
Docs Changes: remove upstream.mtls attribute due to ongoing #8464
Release Notes:

Signed-off-by: Kuat Yessenov <kuat@google.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
stale stalebot believes this issue/PR has not been touched recently
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

5 participants