OAuth 2.0 Mutual-TLS Support

This spec proposes to Provide the option for users to
proof-of-possession of OAuth2.0 access token based on RFC8705 OAuth 2.0
Mutual-TLS Client Authentication and Certificate-Bound Access Tokens.
Users will be able to authenticate their OAuth2.0 client with a client
certificate instead of using Basic authentication with
client_id/client_secret to prevent a token from being used by a
malicious client. This protects Keystone Identity and other OpenStack
services from spoofed OAuth clients.

Change-Id: I67e030c183631bd421cc93ceb767f60fa178238a
This commit is contained in:
Hiromu Asahina 2022-05-30 11:41:30 +09:00
parent 75b4fb25c5
commit 12f37d3548
3 changed files with 704 additions and 9 deletions

View File

@ -22,14 +22,6 @@ keystone
specs/keystone/2023.1/*
Yoga approved specs:
.. toctree::
:glob:
:maxdepth: 1
specs/keystone/yoga/*
Backlog:
.. toctree::
@ -102,6 +94,14 @@ Implemented Identity Program Specifications
keystone
--------
Yoga approved specs:
.. toctree::
:glob:
:maxdepth: 1
specs/keystone/yoga/*
Ussuri approved specs:
`Ussuri Roadmap <https://trello.com/b/ClKW9C8x/keystone-ussuri-roadmap>`_

View File

@ -0,0 +1,695 @@
..
This work is licensed under a Creative Commons Attribution 3.0 Unported
License.
http://creativecommons.org/licenses/by/3.0/legalcode
============================
Support OAuth 2.0 Mutual-TLS
============================
Provide the option for users to proof-of-possession of OAuth2.0 access token
based on `RFC8705 OAuth 2.0 Mutual-TLS Client Authentication and
Certificate-Bound Access Tokens` [#oauth2_specification]_ (hereafter OAuth2.0
mutual TLS). Requires v3.0+ of the Identity API and assumes TLS 1.2 for mutual
TLS. This method will protect Keystone Identity and other OpenStack services
from spoofed OAuth clients.
Problem Description
===================
OAuth2.0 Client Credentials Grant implemented in Keystone uses id/password to
request an access token. However, passwords can be defeated using various
techniques such as dictionary attacks. In addition, the authorization server
(i.e., Keystone) cannot detect the spoofed password and more importantly it
cannot detect a spoofed access token. For this reason, OpenStack Tacker, which
provides NFV orchestration APIs, needs OAuth2.0 mutual TLS support to meet a
well-known NFV standard referred to as `ETSI NFV`` [#nfv_sol013]_. As described
in that standard, such attacks are far more difficult when both sides have to
authenticate with TLS certificates, i.e., `mutual TLS` [#oauth_mtls]_.
Proposed Change
===============
The proposed change is to support OAuth2.0 mutual TLS. This method ensures that
only the party in possession of the private key corresponding to the X.509 TLS
client certificate can utilize the token to access the protected resources.
For this to happen, the following items have to be implemented.
* Add client certificate-based authentication using mutual TLS to Keystone
* Add option to create certificate-bound access tokens to Keystone identity and
ability to issue certificate bound to access tokens
* Add verification of the client certificate to Keystone middleware
* Add confirmation of proof-of-possession of an access token to Keystone
middleware
* Add configuration parameters needed to use mutual TLS (e.g., client
certificate, CA certificate) to keystoneauth
.. note::
In order to use mutual TLS, a client has to generate a client certificate
from its public/private key pair. The client certificate must be an X509
certificate signed by a private/public Certificate Authority (CA) whose
certificate must be available on both client and Keystone. These files
should be externally generated.
.. note::
In order to use OAuth2.0 mutual TLS, mutual TLS should be configured on
Keystone. Although enabling mutual TLS also on the other service endpoints
makes system more secure, the details of such configuration are out of the
scope of the present document.
Terminology
-----------
- *Client:* An application making protected resource requests. The Client can
be a user created by Identity API whose attributes must identical with the
subject DN of the client certificate.
- *Access token:* A token used by the Client to make protected resource
requests with the delegated roles.
- *Client certificate:* A certificate used by the Client to proof-of-possession
of an access token
OAuth2.0 mutual TLS Flow
------------------------
.. seqdiag::
seqdiag {
Client;
server-os [label="Web Server\n(OpenStack Service)"];
middleware [label="Keystone\nMiddleware"];
server-ks [label="Web Server\n(Keystone)"];
Keystone;
os-service [label="OpenStack\nService"];
Client -> server-ks
[label = "1. POST\n /identity/v3/OS-OAUTH2/token\n w/ client ID + client certificate (over mutual TLS)", note = "A client certificates is retrieved from a request sent over mutual TLS"];
server-ks <-- server-ks
[label = "2. Validate trust chain of the client certificate"];
server-ks --> Keystone
Keystone <-- Keystone
[label = "3. Issue access token and bind client certificate thumbprint to the access token"];
server-ks <-- Keystone
Client <-- server-ks
[label = "Response 200 OK\n w/ access token"];
Client -> server-os
[label = "4. request\n OpenStack Service API\n w/ access token + client certificate (over mutual TLS)", note = "A client certificates is retrieved from a request sent over mutual TLS"];
server-os <-- server-os
[label = "5. Validate trust chain of the client certificate"];
server-os --> middleware
middleware -> Keystone
[label = "GET\n /v3/auth/tokens\n w/ access token"];
middleware <-- Keystone
[label = "Response 200 OK\n w/ access token metadata"];
middleware <-- middleware
[label = "6. Verify that certificate thumbprint in metadata matches thumbprint of certificate used For mutual TLS"];
middleware -> os-service
[label = "forward API requst\n w/ access token metadata"];
server-os <-- os-service
Client <-- server-os
[label = "API response"];
}
The flow consists of the following steps as illustrated in the above sequence:
#. Client requests a new access token to Keystone
A Client authenticates with Keystone and requests a new access token. For
the authentication, the Client uses its client certificate as a credential.
The Client must be registered as a Keystone user [#user_api]_ in advance and
its attributes such as ``username``, ``project`` and ``domain``, must be
identical with the subject of the client certificate.
#. Web server for Keystone verifies the validity of the client certificate
Upon obtaining the client certificate from the request, a web server for
Keystone (i.e., Apache) verifies a trust chain of the certificate and the
possession of the private key that corresponds to the certificate, then
continues to the next step only if it's valid (e.g., it was signed by a
known, trusted CA and the client presents the possession of the private
key), otherwise rejects the request.
#. Keystone issues a certificate-bound access token to Client
Keystone creates an access token with the confirmation of whether the
subject DN of the client certificate matches the user attributes. The
mapping rules between the subject DN of client certificates and user
attributes are described by the OS-FEDERATION API [#federation_mapping]_. If
the token is successfully created, Keystone binds the certificate to the
token and issues the token to the Client as an OAuth2.0 access token.
#. Client makes API requests using the issued access token over mutual TLS
Client makes an API request with an issued access token over the mutual TLS.
The mutual TLS is triggered by the server, and thus the requirements for the
client is just to be capable of setting its certificate to API requests.
#. Web server for OpenStack Service verifies the validity of the client
certificate
Upon obtaining the client certificate from the request, a web server for
OpenStack Service verifies a trust chain of the certificate and the
possession of the private key that corresponds to the certificate, then
continues to the next step only if it's valid, otherwise rejects the
request.
#. Keystone Middleware verifies the validity of the access token
Keystone verifies the validity of the access token contained in the request
by checking that the certificate thumbprint in metadata matches the
thumbprint of the certificate in mutual TLS and the access token is not
expired. The thumbprint and the status of token can be obtained from
Keystone Authentication and token management API
[#authentication_and_token_management]_.
The following existing API should be modified to handle requests using
OAuth2.0 mutual TLS.
* Access Token API
* Create Access Token
Also, to serve client certificate thumbprints from Keystone to Keystone
Middleware, the response of the following existing API has to be modified.
* Keystone Authentication and token management API
* Validate and Show Information for Token
API Resources
-------------
Access Token API
----------------
Access Token API has already been implemented. Therefore, only the changes
required to handle OAuth2.0 mutual TLS requests are described in the present
document.
Create Access Token
~~~~~~~~~~~~~~~~~~~
::
POST /identity/v3/OS-OAUTH2/token
Request:
::
Host: server.example.com
Content-Type: application/x-www-form-urlencoded
grant_type=client_credentials
client_id=jFtpUlndpRGaAHuh9TsP3wtj
The client authentication in the existing patches uses
``client_id``/``client_secret`` as credential, whereas OAuth2.0 mutual TLS
assumes a client certificate is used as a credential. This API will be changed
to authenticate clients with their client certificate to handle both cases.
When a request is sent over the mutual TLS, a client is successfully
authenticated only if the client certificate is valid and the subject DN of the
certificate matches the user attributes. Note that the validity of client
certificate can be checked by the general process of mutual TLS. It is also
noted that the mapping rules between the subject DN and user attributes can be
configured by a the mapping in the OS-FEDERATION API.
The following is an example of mapping rules. This example contains two mapping
rules for different CAs (i.e., ``root_a.openstack.host`` and
``root_b.openstack.host``). When the CN name of the issuer of the client
certificate is ``root_a.openstack.host``, the client certificate must contain
the five fields defined in ``remote`` block of the rule (i.e.,
``SSL_CLIENT_SUBJECT_DN_CN``, ``SSL_CLIENT_SUBJECT_DN_UID``,
``SSL_CLIENT_SUBJECT_DN_EMAILADDRESS``, ``SSL_CLIENT_SUBJECT_DN_O``, and
``SSL_CLIENT_SUBJECT_DN_DC``) and those fields must match an existing Keystone
user's attributes specified in ``local`` block of the rule (i.e., ``name``,
``id``, ``email``, ``domain name``, ``domain id``). If the ``remote`` fields do
not match corresponding ``local`` fields, Keystone doesn't issue a token.
Likewise, when the CN name is ``root_b.openstack.host``, Keystone requires a
client certificate that have two fields defined in ``remote`` block and
requires the values of those fields match the values of an existing Keystone
user attributes defined in the ``local`` block.
.. code-block:: json
[
{
"local": [
{
"user": {
"name": "{0}",
"id": "{1}",
"email": "{2}",
"domain": {
"name": "{3}",
"id": "{4}"
}
}
}
],
"remote": [
{
"type": "SSL_CLIENT_SUBJECT_DN_CN"
},
{
"type": "SSL_CLIENT_SUBJECT_DN_UID"
},
{
"type": "SSL_CLIENT_SUBJECT_DN_EMAILADDRESS"
},
{
"type": "SSL_CLIENT_SUBJECT_DN_O"
},
{
"type": "SSL_CLIENT_SUBJECT_DN_DC"
},
{
"type": "SSL_CLIENT_ISSUER_DN_CN",
"any_one_of": [
"root_a.openstack.host"
]
}
]
},
{
"local": [
{
"user": {
"id": "{0}",
"domain": {
"id": "{1}"
}
}
}
],
"remote": [
{
"type": "SSL_CLIENT_SUBJECT_DN_UID"
},
{
"type": "SSL_CLIENT_SUBJECT_DN_DC"
},
{
"type": "SSL_CLIENT_ISSUER_DN_CN",
"any_one_of": [
"root_b.openstack.host"
]
}
]
}
]
If the authentication is successful, Keystone binds the client certificate to
the access token. Assuming the fernet token is used as an access token, this
can be done by adding the thumbprint of a client certificate into the payload
of the fernet token.
Keystone Authentication and token management API
------------------------------------------------
Keystone Authentication and token management API has already been implemented.
Therefore, only the changes required to handle OAuth2.0 mutual TLS are
described in the present document.
Validate and Show Information for Token
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
::
GET /v3/auth/tokens
Response:
::
HTTP/1.1 200 OK
Content-Type: application/json
{
"token": {
"audit_ids": [
"mAjXQhiYRyKwkB4qygdLVg"
],
"catalog": [
{
"type": "identity",
"name": "keystone",
"endpoints": [
{
"region": "RegionOne",
"adminURL": "http://10.10.1.100/identity",
"publicURL": "http://10.10.1.100/identity"
}
]
}
],
"expires_at": "2015-11-07T02:58:43.578887Z",
"is_domain": false,
"issued_at": "2015-11-07T01:58:43.578929Z",
"methods": [
"password"
],
"project": {
"domain": { "id": "default", "name": "Default" },
"id": "f2796050af304441b5f1eabecb33e808",
"name": "service"
},
"roles": [
{
"description": null,
"domain_id": null,
"id": "d229bd3566fe4abe96a5d02c211e2f10",
"name": "admin",
"options": { "immutable": true }
},
{
"description": null,
"domain_id": null,
"id": "c9b1b27aeff440959db75bdc91dd8a84",
"name": "member",
"options": { "immutable": true }
}
],
"user": {
"domain": { "id": "default", "name": "Default" },
"id": "da0e3ae640584af98c015343b0552ec0",
"name": "client",
"password_expires_at": null
}
"OS-OAUTH2": {
"x5t#S256": "bwcK0esc3ACC3DB2Y5_lESsXE8o9ltc05O89jdN-dg2"
}
}
}
The difference from the current API response is to add the ``OS-OAUTH2`` field
that contains the client certificate subject DN corresponding to the access
token in ``x5t#S256`` field. This field is added only when the token is issued
by OAuth2.0 access token API and the response status is 200. The other fields
and error responses are the same as the current API implementation.
The Keystone Middleware sends requests to this modified ``Keystone
Authentication and token management API`` with the access token contained in an
API request in order to check that the certificate thumbprint in metadata
matches the thumbprint of the certificate presented during the mutual TLS and
the status of the access token is not expired.
If the token passes the validation, the Keystone Middleware updates request
headers with the metadata. If a token is invalid or an error response is
returned, it rejects a request and returns ``401 Unauthorized``.
According to RFC6749, the "bearer" token type defined in RFC6750
[#bearer_token]_ is utilized for including the access token string in the API
request. The Keystone Middleware has to obtain an access token from a request
with the Authorization header. An example of such a request is shown below.
::
GET /resource HTTP/1.1
Host: server.example.com
Authorization: Bearer f69c9fb6947c47329b8955d629ac5722
Alternatives
------------
Verification of Token Validity
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
We can avoid modifying the existing Keystone Authentication and token
management API, by adding a new Token introspection API [#token_introspection]_
as follows.
::
POST /identity/v3/auth/OS-OAUTH2/introspect
Request:
::
Host: server.example.com
Content-Type: application/x-www-form-urlencoded
client_id=jFtpUlndpRGaAHuh9TsP3wtj&token=f69c9fb6947c47329b8955d629ac5722&token_type_hint=access_token
Response:
::
HTTP/1.1 200 OK
Content-Type: application/json
{
"active": true,
"cnf":{
"x5t#S256": "bwcK0esc3ACC3DB2Y5_lESsXE8o9ltc05O89jdN-dg2"
}
"audit_ids": [
"mAjXQhiYRyKwkB4qygdLVg"
],
"catalog": [
{
"type": "identity",
"name": "keystone",
"endpoints": [
{
"region": "RegionOne",
"adminURL": "http://10.10.1.100/identity",
"publicURL": "http://10.10.1.100/identity"
}
]
}
],
"expires_at": "2015-11-07T02:58:43.578887Z",
"is_domain": false,
"issued_at": "2015-11-07T01:58:43.578929Z",
"methods": [
"password"
],
"project": {
"domain": { "id": "default", "name": "Default" },
"id": "f2796050af304441b5f1eabecb33e808",
"name": "service"
},
"roles": [
{
"description": null,
"domain_id": null,
"id": "d229bd3566fe4abe96a5d02c211e2f10",
"name": "admin",
"options": { "immutable": true }
},
{
"description": null,
"domain_id": null,
"id": "c9b1b27aeff440959db75bdc91dd8a84",
"name": "member",
"options": { "immutable": true }
}
],
"user": {
"domain": { "id": "default", "name": "Default" },
"id": "da0e3ae640584af98c015343b0552ec0",
"name": "client",
"password_expires_at": null
}
}
Response (expired token):
::
HTTP/1.1 200 OK
Content-Type: application/json
{
"active": false,
}
Error response:
::
HTTP/1.1 401 Unauthorized
Content-Type: application/json
WWW-Authenticate: Keystone uri="http://keysone.identity.host/identity/v3/users/{user_id}/application_credentials"
Cache-Control: no-store
Pragma: no-cache
{
"error": "invalid_client",
"error_description": "The client_id is not found or client_certificate is invalid."
}
The Keystone Middleware requests the Token Introspection with the access token
contained in an API request. Keystone returns the metadata corresponding to the
token, such as the thumbprint of client certificate, the service catalog, user
Id, token validity, etc, if the credential is valid. Otherwise, it returns an
error response.
The Keystone receiving this API request has to obtain the token metadata
through the two steps:
#. Validate and obtain information for token via Keystone API
`/v3/auth/tokens`
#. Retrieve the thumbprint of a client certificate subject DN
If the token passes the validation and it's not expired, Keystone set the JSON
object merging the required field in `RFC7662: OAuth 2.0 Token Introspection`
(i.e., `active`) into the response from `/v3/auth/tokens`. Specifically, the
fields in the response are considered the `extension_field`. The client
certificate subject DN is set to ``x5t#S256`` field. In the cases where the
token is expired and authentication fails, Keystone returns 200 responses with
the body indicating `active: false` and 401 error response, respectively.
The Keystone Middleware updates request headers with the metadata only if a
token is valid. If a token is invalid or an error response is returned, it
rejects a request and returns ``401 Unauthorized``. The validity of a token is
determined by the value of the ``active`` field in a response, i.e., a token is
valid if the value is ``true``, and invalid if the value is ``false``.
Another alternative is to use JWT including the thumbprint of a client
certificate as a field (See `RFC8705: 3.1 JWT Certificate Thumbprint
Confirmation Method` [#oauth2_mtls_jwt]_). In this case, we can omit the token
introspection API.
Creation of Certificate-bound Access Tokens
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
As an alternative of the fernet token, we can use JWT token which is already
supported by Keystone if we can't modify the fernet token contents freely. In
that case, the thumbprint of a client certificate or ``credential Id`` which
will be used to retrieve the certificate from the credentials table is included
as a field of JWT (See `RFC8705: 3.1 JWT Certificate Thumbprint Confirmation
Method`).
Security Impact
---------------
No negative impact on the Keystone security as using mutual TLS makes OAuth2.0
safer.
Notifications Impact
--------------------
None
Other End User Impact
---------------------
End users have to configure their clients to use the feature described in the
present specification. For this reason, appropriate user documents needs to be
added.
Performance Impact
------------------
To store the thumbprint, the fernet token size will be slightly increased. This
might increase delay, computational cost, etc. However, in general, the
thumbprints are created by a hash function like SHA256, their size is
negligibly small.
Other Deployer Impact
---------------------
Configuration of Authorization Server
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
As we would provide OAuth2.0 functionality as an extension, it doesn't affect
the existing deployers. A deployer can enable this feature by adding
configuration blocks. The following is an example of the configuration.
::
[oauth2]
driver = sql
token_endpoint_auth_method = tls_client_auth
[auth]
methods = external,password,token,oauth2
Configuration of Keystone Middleware
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
To use OAuth2.0 access token, a deployer has to configure Keystone Middleware
in OpenStack services by changing ``[filter:authtoken]`` in, for example,
``/etc/tacker/api-paste.ini`` as shown below. If ``paste.filter_factory`` is
``keystonemiddleware.oauth2_mtls_token:filter_factory``, the Keystone
Middleware expects to mutual TLS is used for API request and the access token
in the request is bound to the client certificates.
::
[filter:authtoken]
paste.filter_factory = keystonemiddleware.oauth2_mtls_token:filter_factory
Developer Impact
----------------
None
Implementation
==============
Assignee(s)
-----------
Primary assignee:
* Hiromu Asahina (h-asahina) <hiromu.asahina.az@hco.ntt.co.jp>
Other contributors:
* Yusuke Niimi <niimi.yusuke@fujitsu.com>
* Keiichiro Yamakawa <yamakawa.keiich@fujitsu.com>
Work Items
----------
* Add client certificate-based authentication using mutual TLS to Keystone
* Add option to create certificate-bound access tokens to Keystone identity and
ability to issue certificate bound to access tokens
* Add verification of the client certificate to Keystone middleware
* Add confirmation of proof-of-possession of an access token to Keystone
middleware
* Add configuration parameters needed to use mutual TLS (e.g., client
certificate, CA certificate) to keystoneauth
Dependencies
============
None
Documentation Impact
====================
* We would need to update the user API docs and Authentication Mechanisms.
* We would need to update the user API docs and Middleware Architecture.
References
==========
.. [#oauth2_specification] https://tools.ietf.org/html/rfc6749
.. [#nfv_sol013] https://www.etsi.org/deliver/etsi_gs/NFV-SOL/001_099/013/02.06.01_60/gs_nfv-sol013v020601p.pdf
.. [#oauth_mtls] https://datatracker.ietf.org/doc/html/rfc8705
.. [#user_api] https://docs.openstack.org/api-ref/identity/v3/#users
.. [#federation_mapping] https://docs.openstack.org/api-ref/identity/v3-ext/index.html?expanded=create-a-mapping-detail#mappings
.. [#authentication_and_token_management]
https://docs.openstack.org/api-ref/identity/v3/index.html?expanded=validate-and-show-information-for-token-detail#authentication-and-token-management
.. [#bearer_token] https://datatracker.ietf.org/doc/html/rfc6750
.. [#token_introspection] https://datatracker.ietf.org/doc/html/rfc7662
.. [#oauth2_mtls_jwt] https://datatracker.ietf.org/doc/html/rfc8705#section-3.1

View File

@ -6,7 +6,7 @@ ignore_basepython_conflict = True
[testenv]
basepython = python3
usedevelop = True
; usedevelop = True
setenv = VIRTUAL_ENV={envdir}
deps = -c{env:TOX_CONSTRAINTS_FILE:https://releases.openstack.org/constraints/upper/master}
-r{toxinidir}/requirements.txt