This article tries to explain how tokens generated by Keystone (using the PKI token format, not UUID) can be authenticated by clients (e.g. cinder, neutron, nova, etc.)
The relevant fragment from /etc/keystone/keystone.conf
that specifies the PKI material used to sign Keystone tokens (the signing key, the signing certificate and its corresponding CA certificate, together with key size and key expiration period) usually looks like this (default values are used next):
[signing] token_format = PKI certfile = /etc/keystone/ssl/certs/signing_cert.pem keyfile = /etc/keystone/ssl/private/signing_key.pem ca_certs = /etc/keystone/ssl/certs/ca.pem cert_subject = /C=US/ST=Unset/L=Unset/O=Unset/CN=www.example.com key_size = 2048 valid_days = 3650
The Keystone client middleware — implemented in the keystone client.middleware.auth_token
Python module — verifies the signature of a given Keystone token (data is in IAW CMS syntax). The actual method from this module is cms_verify
. This method relies on its counterpart cms_verify
defined in keystoneclient.common.cms
and requires the actual data, the signing certificate and corresponding CA certificate.
The token’s data, signing certificate and its corresponding CA certificate are stored on local disk, inside a directory specified by the signing_dir
option in the keystone_authtoken
section. By default, this option is set to None
. When None
or absent, a temporary directory is created, as one can see in the verify_signing_dir
method:
def verify_signing_dir(self): if os.path.exists(self.signing_dirname): if not os.access(self.signing_dirname, os.W_OK): raise ConfigurationError( 'unable to access signing_dir %s' % self.signing_dirname) uid = os.getuid() if os.stat(self.signing_dirname).st_uid != uid: self.LOG.warning( 'signing_dir is not owned by %s', uid) current_mode = stat.S_IMODE(os.stat(self.signing_dirname).st_mode) if current_mode != stat.S_IRWXU: self.LOG.warning( 'signing_dir mode is %s instead of %s', oct(current_mode), oct(stat.S_IRWXU)) else: os.makedirs(self.signing_dirname, stat.S_IRWXU)
When debug
is True
for any particular OpenStack service, one can see the value of the signing_dir
option during startup in the logs:
2015-04-15 19:03:25.069 9449 DEBUG glance.common.config [-] keystone_authtoken.signing_dir = None log_opt_values /usr/lib/python2.6/site-packages/oslo/config/cfg.py:1953
The signing certificate and its corresponding CA certificate are retrieved from Keystone via an HTTP request, and stored on local disk. The methods that implement this in keystone client.middleware.auth_token
look like this:
def _fetch_cert_file(self, cert_file_name, cert_type): path = '/v2.0/certificates/' + cert_type response = self._http_request('GET', path) if response.status_code != 200: raise exceptions.CertificateConfigError(response.text) self._atomic_write_to_signing_dir(cert_file_name, response.text) def fetch_signing_cert(self): self._fetch_cert_file(self.signing_cert_file_name, 'signing') def fetch_ca_cert(self): self._fetch_cert_file(self.signing_ca_file_name, 'ca')
Which translates to HTTP requests to Keystone like this:
2015-04-15 19:03:34.704 9462 DEBUG urllib3.connectionpool [-] "GET /v2.0/certificates/signing HTTP/1.1" 200 4251 _make_request /usr/lib/python2.6/site-packages/urllib3/connectionpool.py:295 2015-04-15 19:03:34.727 9462 DEBUG urllib3.connectionpool [-] "GET /v2.0/certificates/ca HTTP/1.1" 200 1277 _make_request /usr/lib/python2.6/site-packages/urllib3/connectionpool.py:295
As said before, in order to verify the Keystone token, the cms_verify
method uses the signing certificate and corresponding CA certificates (as stored on local disk) plus the token data, and passes them to an external openssl
process for verification:
def cms_verify(self, data): """Verifies the signature of the provided data's IAW CMS syntax. If either of the certificate files are missing, fetch them and retry. """ while True: try: output = cms.cms_verify(data, self.signing_cert_file_name, self.signing_ca_file_name) except exceptions.CertificateConfigError as err: if self.cert_file_missing(err.output, self.signing_cert_file_name): self.fetch_signing_cert() continue if self.cert_file_missing(err.output, self.signing_ca_file_name): self.fetch_ca_cert() continue self.LOG.error('CMS Verify output: %s', err.output) raise ...
This translates to having the Keystone middleware spawning a process to run an openssl
command to validate the input (the Keystone token). Something like:
openssl cms -verify -certfile /tmp/keystone-signing-OFShms/signing_cert.pem -CAfile /tmp/keystone-signing-OFShms/cacert.pem -inform PEM -nosmimecap -nodetach -nocerts -noattr << EOF -----BEGIN CMS----- MIIBxgYJKoZIhvcNAQcCoIIBtzCCAbMCAQExCTAHBgUrDgMCGjAeBgkqhkiG9w0B BwGgEQQPeyJyZXZva2VkIjogW119MYIBgTCCAX0CAQEwXDBXMQswCQYDVQQGEwJV UzEOMAwGA1UECAwFVW5zZXQxDjAMBgNVBAcMBVVuc2V0MQ4wDAYDVQQKDAVVbnNl dDEYMBYGA1UEAwwPd3d3LmV4YW1wbGUuY29tAgEBMAcGBSsOAwIaMA0GCSqGSIb3 DQEBAQUABIIBABzCPXw9Kv49gArUWpAOWPsK8WRRnt6WS9gMaACvkllQs8vHEN11 nLBFGmO/dSTQdyXR/gQU4TuohsJfnYdh9rr/lrC3sVp1pCO0TH/GKmf4Lp1axrQO c/gZym7qCpFKDNv8mAAHIbGFWvBa8H8J+sos/jC/RQYDbX++7TgPTCZdCbLlzglh jKZko07P86o3k14Hq6o7VGpMGu9EjOziM6uOg391yylCVbqRazwoSszKm29s/LHH dyvEc+RM9iRaNNTiP5Sa/bU3Oo25Ke6cleTcTqIdBaw+H5C1XakCkhpw3f8z0GkY h0CAN2plwwqkT8xPYavBLjccOz6Hl3MrjSU= -----END CMS----- EOF
One has to pay attention to the purposes of the signing certificate. If its purposes are wrong, tokens generated by Keystone won’t be validated by Keystone clients (middleware). This is reflected in the logs with an error message that typically looks like this:
2015-04-15 18:52:13.027 29533 WARNING keystoneclient.middleware.auth_token [-] Verify error: Command 'openssl' returned non-zero exit status 4 2015-04-15 18:52:13.027 29533 DEBUG keystoneclient.middleware.auth_token [-] Token validation failure. _validate_user_token /usr/lib/python2.6/site-packages/keystoneclient/middleware/auth_token.py:836 2015-04-15 18:52:13.027 29533 TRACE keystoneclient.middleware.auth_token Traceback (most recent call last): 2015-04-15 18:52:13.027 29533 TRACE keystoneclient.middleware.auth_token File "/usr/lib/python2.6/site-packages/keystoneclient/middleware/auth_token.py", line 823, in _validate_user_token 2015-04-15 18:52:13.027 29533 TRACE keystoneclient.middleware.auth_token verified = self.verify_signed_token(user_token) 2015-04-15 18:52:13.027 29533 TRACE keystoneclient.middleware.auth_token File "/usr/lib/python2.6/site-packages/keystoneclient/middleware/auth_token.py", line 1258, in verify_signed_token 2015-04-15 18:52:13.027 29533 TRACE keystoneclient.middleware.auth_token if self.is_signed_token_revoked(signed_text): 2015-04-15 18:52:13.027 29533 TRACE keystoneclient.middleware.auth_token File "/usr/lib/python2.6/site-packages/keystoneclient/middleware/auth_token.py", line 1216, in is_signed_token_revoked 2015-04-15 18:52:13.027 29533 TRACE keystoneclient.middleware.auth_token revocation_list = self.token_revocation_list 2015-04-15 18:52:13.027 29533 TRACE keystoneclient.middleware.auth_token File "/usr/lib/python2.6/site-packages/keystoneclient/middleware/auth_token.py", line 1312, in token_revocation_list 2015-04-15 18:52:13.027 29533 TRACE keystoneclient.middleware.auth_token self.token_revocation_list = self.fetch_revocation_list() 2015-04-15 18:52:13.027 29533 TRACE keystoneclient.middleware.auth_token File "/usr/lib/python2.6/site-packages/keystoneclient/middleware/auth_token.py", line 1358, in fetch_revocation_list 2015-04-15 18:52:13.027 29533 TRACE keystoneclient.middleware.auth_token return self.cms_verify(data['signed']) 2015-04-15 18:52:13.027 29533 TRACE keystoneclient.middleware.auth_token File "/usr/lib/python2.6/site-packages/keystoneclient/middleware/auth_token.py", line 1239, in cms_verify 2015-04-15 18:52:13.027 29533 TRACE keystoneclient.middleware.auth_token self.signing_ca_file_name) 2015-04-15 18:52:13.027 29533 TRACE keystoneclient.middleware.auth_token File "/usr/lib/python2.6/site-packages/keystoneclient/common/cms.py", line 148, in cms_verify 2015-04-15 18:52:13.027 29533 TRACE keystoneclient.middleware.auth_token raise e 2015-04-15 18:52:13.027 29533 TRACE keystoneclient.middleware.auth_token CalledProcessError: Command 'openssl' returned non-zero exit status 4 2015-04-15 18:52:13.027 29533 TRACE keystoneclient.middleware.auth_token 2015-04-15 18:52:13.028 29533 DEBUG keystoneclient.middleware.auth_token [-] Marking token as unauthorized in cache _cache_store_invalid /usr/lib/python2.6/site-packages/keystoneclient/middleware/auth_token.py:1154 2015-04-15 18:52:13.028 29533 WARNING keystoneclient.middleware.auth_token [-] Authorization failed for token 2015-04-15 18:52:13.029 29533 INFO keystoneclient.middleware.auth_token [-] Invalid user token - deferring reject downstream