scylladb / scylla-cqlsh

A fork of the cqlsh code
Apache License 2.0
11 stars 29 forks source link

Host name validation fails in cqlsh, if a scylladb server SSL certificate uses SAN DNS records only #75

Closed mark-bb closed 3 months ago

mark-bb commented 3 months ago
$ openssl x509 -in dbserver.crt -text -noout | grep -E 'X509v3|DNS'
        X509v3 extensions:
            X509v3 Subject Alternative Name:
                DNS:dbserver.fqdn, DNS:dbserver

Currently we get the following error running cqlsh with such a server certificate and hostname=dbserver[.fqdn] in the [connection] section of the cqlshrc file.

cassandra.cluster.NoHostAvailable: ('Unable to connect to any servers', {'XXX.XXX.XXX.XXX:19142': PermissionError(1, "Tried connecting to [('XXX.XXX.XXX.XXX', 19142)]. Last error: [SSL: CERTIFICATE_VERIFY_FAILED] certificate verify failed: IP address mismatch, certificate is not valid for 'XXX.XXX.XXX.XXX'. (_ssl.c:997)")})

The workaround is to add the last line after the 1-st one in the /opt/scylladb/share/cassandra/libexec/cqlsh.py file.

                kwargs['ssl_context'] = sslhandling.ssl_settings(hostname, CONFIG_FILE) if ssl else None
                kwargs['ssl_options'] = {'server_hostname': self.hostname} if ssl else None  # The line to add
fruch commented 3 months ago

can you share your full cqlshrc ?

it should be working if it's defined in the certfile

as a workaround you can use validate=false in cqlshrc

mark-bb commented 3 months ago

The server certificate hostname checking was turned off in fact previously, at least in 5.1. The way cqlsh worked with certificates resembled the one used by Cassandra at the moment - there are no visible attempts to set the server_name ssl option like above and ssl_context.check_hostname.

https://docs.python.org/3/library/ssl.html#ssl.SSLContext.check_hostname

The PROTOCOL_TLS_CLIENT protocol enables hostname checking by default. With other protocols, hostname checking must be enabled explicitly.

According to the citation above this means, that hostname verification was not used previously, when cqlsh used PROTOCOL_TLS or PROTOCOL_TLS_CLIENTv1_2 protocols. Now I see, that ssl_context.check_hostname is used explicitly based on client's validate option setting in cqlshrc, which is actually not fully correct. We may require the server cert verification, but not its hostname validation. But there is no distinct client option for hostname validation, and your rule "to verify cert and validate its hostname" or "not verify cert and not validate hostname" looks ok. But, if you set ssl_context.check_hostname = True, you must use server_name ssl_option equal to one of the certificate SAN DNS names. If you don't do this, then it works with [connection].hostname = IP in cqlshrc and the same IP specified in the SAN IP only.

mark-bb commented 3 months ago

as a workaround you can use validate=false in cqlshrc

This is really a workaround, but it turns off the server cert verification as well. It may be inappropriate in some real cases, of course.

fruch commented 3 months ago

as a workaround you can use validate=false in cqlshrc

This is really a workaround, but it turns off the server cert verification as well. It may be inappropriate in some real cases, of course.

o.k. so if we introduce a new check_hostname parameter to cqlsh, it would solve the issue you are having ? and you can disable the hostname checks ?

fruch commented 3 months ago

look at the driver code:

    def _wrap_socket_from_context(self):
        ssl_options = self.ssl_options or {}
        # PYTHON-1186: set the server_hostname only if the SSLContext has
        # check_hostname enabled and it is not already provided by the EndPoint ssl options
        if (self.ssl_context.check_hostname and
                'server_hostname' not in ssl_options):
            ssl_options = ssl_options.copy()
            ssl_options['server_hostname'] = self.endpoint.address
        self._socket = self.ssl_context.wrap_socket(self._socket, **ssl_options)

seems like it would work only for ip addresses, and not for DNS names (they were lost) so it's a bit driver bug as well

fruch commented 3 months ago

so to sum it up:

1) I think we should do the suggestion you proposed at the beginning - we should need to use the ssl_options since driver doesn't handle it correctly (yet) 2) plus add option to control the check_hostname on it's own

mark-bb commented 3 months ago

o.k. so if we introduce a new check_hostname parameter to cqlsh, it would solve the issue you are having ? and you can disable the hostname checks ?

No, it doesn't resolve my goal - to have an ability to verify certificate and validate its hostname correctly.

Use case: I don't want to allow use of certificate issued for server1 on server2.

The handy feature (not existing in Cassandra, afaik) you suggest may really help, when I have, say, hundreds servers in a cluster, and I don't want to issue distinct certs for each of them. I issue a server certificate without SAN DNS at all and use this new check_hostname=false with verify=true. I still have an ability to verify this cert without checking its non-existing DNS name there. The current workaround for this scenario is only setting verify=false which disables my cert verification.

seems like it would work only for ip addresses, and not for DNS names (they were lost) so it's a bit driver bug as well

Yes, I see. Seems, that this is why the workaround I suggested initially needed.

mark-bb commented 3 months ago

@fruch I've created another issue on the proposed check_hostname cqlshrc property discussed here. https://github.com/scylladb/scylla-cqlsh/issues/77