-
Notifications
You must be signed in to change notification settings - Fork 782
Add support for automatic client authentication via TLS certificate fields #1920
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
base: unstable
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -138,6 +138,7 @@ struct connection { | |
ConnectionCallbackFunc conn_handler; | ||
ConnectionCallbackFunc write_handler; | ||
ConnectionCallbackFunc read_handler; | ||
sds cert_user; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This should probably be on the underlying tlsConnection, not the higher level abstraction. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Actually, I'm not sure this needs to be on the struct, looks like it could just be stack allocated inside the function itself. |
||
}; | ||
|
||
#define CONFIG_BINDADDR_MAX 16 | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -38,6 +38,7 @@ | |
|
||
#include <openssl/conf.h> | ||
#include <openssl/ssl.h> | ||
#include <openssl/x509.h> | ||
#include <openssl/err.h> | ||
#include <openssl/rand.h> | ||
#include <openssl/pem.h> | ||
|
@@ -677,12 +678,69 @@ static void updateSSLState(connection *conn_) { | |
updatePendingData(conn); | ||
} | ||
|
||
static int getCertFieldByName(X509 *cert, const char *field, char *out, size_t outlen) { | ||
if (!cert || !field || !out) return 0; | ||
|
||
int nid = -1; | ||
|
||
if (!strcasecmp(field, "CN")) | ||
nid = NID_commonName; | ||
else if (!strcasecmp(field, "O")) | ||
nid = NID_organizationName; | ||
// Add more mappings here as needed | ||
Comment on lines
+686
to
+690
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Using "O" makes no sense to me, just like "C" (Country) makes no sense. Fields like that are not unique. If we start supporting it, I suspect we might regret it in the future. @madolson you mentioned "O" in a comment. Was it a serious suggestion or just an example? Out of the top-level fields, only CN (common name) makes sense to me. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Someone asked for O once inside AWS, but I don't know if they would have been okay with just CN as well. We can always just start with "CN" and see if anyone asks for more in the future? The enum just allows us to add more in the future. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Yes, I agree enums in general are good for extensibility, but I doubt what we want to add in the future are top-level fields. It's more likely that we want to add
But if you were asked once to use O for username, then I'm OK with it. It's an actual ask from a user. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I followed up with the folks, they did indeed ask for "O". They sign their certificates by team, and they want the auth to happen at the team level (imagine a bunch of apps with multiple services, they want all services to have the same permission). They said "CN" would be fine, they would just need to provision more users, but that would be "OK" for them. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Then, I think we shouldn't name the config There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. How about boolean config There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I would prefer to focus on the fact we are authenticating the user of the client than on what field is being used. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @zuiderkwast so we will still be now using the field name right like CN and O, your suggestion is to just change the name of the config to |
||
|
||
if (nid == -1) return 0; | ||
|
||
X509_NAME *subject = X509_get_subject_name(cert); | ||
if (!subject) return 0; | ||
|
||
return X509_NAME_get_text_by_NID(subject, nid, out, outlen) > 0; | ||
} | ||
|
||
static void autoAuthenticateClientFromCert(tls_connection *conn) { | ||
if (!conn || !SSL_is_init_finished(conn->ssl)) return; | ||
|
||
const char *field = server.tls_ctx_config.client_auth_field; | ||
if (!field) return; | ||
|
||
serverLog(LL_NOTICE, "TLS: Config Auto Auth Field = %s", field); | ||
|
||
X509 *cert = SSL_get_peer_certificate(conn->ssl); | ||
if (!cert) { | ||
serverLog(LL_WARNING, "TLS: No peer certificate found after handshake"); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. WARNING is a serious log level. It means the server is in trouble and needs an administrator's help. Missing cert is not an error. Client certificate can be configured to be optional. I think we shouldn't log anything here, not even a notice. |
||
return; | ||
} | ||
|
||
char field_value[256]; | ||
if (!getCertFieldByName(cert, field, field_value, sizeof(field_value))) { | ||
serverLog(LL_WARNING, "TLS: Failed to extract field %s from certificate", field); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The should probably just be a NOTICE. |
||
X509_free(cert); | ||
return; | ||
} | ||
|
||
serverLog(LL_NOTICE, "TLS: Config Auto Auth %s = %s", field, field_value); | ||
conn->c.cert_user = sdsnew(field_value); | ||
X509_free(cert); | ||
|
||
client *c = connGetPrivateData(&conn->c); | ||
if (!c) return; | ||
|
||
user *u = ACLGetUserByName(conn->c.cert_user, sdslen(conn->c.cert_user)); | ||
serverLog(LL_NOTICE, "TLS: ACL lookup user: %s", u ? u->name : "NOT FOUND"); | ||
if (u) { | ||
c->user = u; | ||
c->flag.authenticated = true; | ||
serverLog(LL_NOTICE, "TLS: Auto-authenticated client as %s", u->name); | ||
Comment on lines
+729
to
+733
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Logging twice here is too verbose IMO. Let's log only the second one, when it has been found and authenticated. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If many clients connect successfully, are we're flooding the logs too much if we log here? Is it better to log only in the NOT FOUND case? It's the exception case that shouldn't happen often. @madolson WDYT? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We also mark u->name as sensitive in other places, we should add it behind the hide user data flag. I would drop the log level to verbose if we want to keep the second one. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Good point. If we redact this log message, there isn't much to log at all. Shall we just skip logging here? If we want it, it would be something like this: serverLog(LL_VERBOSE,
"TLS: Auto-authenticated client as %s",
server.hide_user_data_from_log ? "*redacted*" : u->name); |
||
} | ||
} | ||
|
||
static void TLSAccept(void *_conn) { | ||
tls_connection *conn = (tls_connection *)_conn; | ||
ERR_clear_error(); | ||
int ret = SSL_accept(conn->ssl); | ||
if (ret > 0) { | ||
conn->flags |= TLS_CONN_FLAG_ACCEPT_SUCCESS; | ||
autoAuthenticateClientFromCert(conn); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We can't do this here, it's executed from the IO threads and the ACL code is not thread safe, we probably need to grab the user and stuff it on the connection to evaluate later. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I am thinking of moving the authentication logic to There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think it should be on the tls connection object, since it's not relevant for TLS disabled. Other than that sounds OK. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think we can only add the |
||
} else if (handleSSLReturnCode(conn, ret)) { | ||
conn->flags |= TLS_CONN_FLAG_ACCEPT_ERROR; | ||
} | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The config should probably be an enum config, so we can validate that only values that we can handle are allowed.