Add JSON Web Tokens as a Non-persistent Token Provider¶
We currently support one token format called fernet. The fernet token format is a non-persistent format based on a spec by Heroku and was made the default token format for keystone.
However, the fernet format has its own problems that make it non-ideal. The fernet spec is largely abandoned, making it hard to get changes into it and thereby into the cryptography implementation of it. Moreover, the fernet spec is not recognized by any standards body and therefore not as closely audited as an IETF standard, making it more susceptible to zero-day vulnerabilities. Addressing these vulnerabilities falls solely on the OpenStack, specifically the keystone, community.
It would be nice to offer a new type of token that is backed by a widely used standard. This also increases the chances of interoperability between the OpenStack ecosystem and other communities that support JWT.
As a operator, I want to use a non-persistent token provider that isn’t coupled to a symmetric encryption or signing implementation. An implementation built on asymmetric signing or encryption allows me to distribute public keys from one node to another instead of syncing a repository of symmetric keys. This makes it easier to deploy keystone nodes with read-only capabilities strictly used for token validation. The specific usecase for this allows me to deploy read-only regions keeping token validation within the region, while having tokens issued from a central identity management system in a separate region.
As a user, I want to be able to authenticate for a token that I can use with other pieces of software outside of the OpenStack ecosystem that prove my identity.
Create a new non-persistent keystone token backend based on the JSON Web Token standard. These will behave in much the same way as our current fernet tokens do.
The token will be a signed JWT (JWS) containing the authentication payload. Note that signed tokens are web safe and integrity verified, but token payload is not opaque to its holder. It is possible to decode a token and inspect the payload with JWS tokens. Using nested JWE tokens are the JSON Web Token equivalent to Fernet tokens and they are encrypted and signed.
This implementation reserves the right to change, modify, or remove items in the payload at any point in time and for any reason. Decoding and relying on attributes within the payload is not a supported API, nor should it be assumed as one. Users should use formal APIs to request information from keystone. The development team will not prevent or stall changes to token payloads, which are internal implementation details of the token provider, due to users relying on those attributes in ways they shouldn’t. Likewise, deployment that consider information in the token payload sensitive should rely on Fernet to prevent that information from being exposed to users.
Similar to the Fernet, JWTs will require a key repository be set up to use for
signing tokens. A new
keystone-manage command will be added to handle
secret generation and rotation which will likely re-use much of the utilities
fernet_rotate commands. The recommended
algorithm to be used is
ES256. Keystone should not expose the ability to
end users to ask for a specific JWS algorithm. Support should be limited to
only supported or trusted algorithms that the end user cannot specify. JWS
tokens will be integrity verified with a private key and validated using a
corresponding public key. Since the
ES256 implementation only uses signing
(as opposed to signed encrypted payloads), this adheres to slightly better
security practices over fernet because private keys never have to be synced
across keystone API nodes. Only public keys need to be transferred to other
keystone API servers to validate tokens across a cluster.
The payload of the JWS will use the following registered claims:
the sub claim will be a required string containing the ID of the user who authenticated for the token
the exp claim will be a required numeric value for token expiration
the iat claim will be a required numeric value for the time a token was issued
The following private claims will be used to relay additional required information and will be prefixed with openstack to avoid collisions with future upstream claims:
openstack_methods will be a required claim denoting the authentication methods used to obtain the token
openstack_audit_ids will be a required claim containing the audit information associated to a token
openstack_system will be an optional claim only present in system-scoped tokens
openstack_domain_id will be an optional claim only present in domain-scoped tokens
openstack_project_id will be an optional claim only present in project-scoped tokens
openstack_trust_id will be an optional claim only present in trust-scoped tokens
openstack_app_cred_id will be an optional claim only present in application credential tokens
openstack_group_ids will be an optional claim only present in federated tokens to carry an ephemeral user’s group assignments
openstack_idp_id will be an optional claim only present in federated tokens to carry the ID of a user’s identity provider
openstack_protocol_id will be an optional claim only present in federated tokens to denote the protocol used by a federated user to authenticate
openstack_access_token will be an optional claim only present in OAuth tokens
The PyJWT library is already present in the requirements repository and
would be a convenient choice to use for this implementation. Both the PyJWT
library and the JWCrypto library implement support for JWS. Since the
implementation detailed in this specification is unique to
ES256, a library
that supports JWE isn’t necessary. If supporting another encrypted token type,
like fernet, is a requirement in the future, then finding or contributing JWE
support to the consuming library would be necessary.
Users will request and present tokens in exactly the same way they currently do with Fernet tokens. There is no need to add or change any APIs.
Key Setup & Rotation¶
Much like the Fernet implementation, a JWT provider will require a key rotation
ES256 relies on asymmetric signing, the suggested rotation
strategy will be slightly different than what is known with Fernet.
The Fernet implementation requires the usage of a staged key, which is just a key with a special name, in order to ensure tokens can be validated during the rotation process. This won’t be required with JWT and the following steps should be sufficient to perform key rotation without token invalidation due to missing signing keys. Assume the following steps are being performed on three different API servers, named A, B, and C, that need to validate tokens issued by each other.
A key pair is created for each API server. A.priv, A.pub for A, B.priv, B.pub for B, and C.priv, C.pub for C.
A copy of each public key is transferred to each API server. A, B, and C all have copies of A.pub, B.pub, and C.pub.
At this point, tokens issued from any API server can be validated anywhere. In the event a single API server needs to rotate key pairs:
A new key pair is created for A called A’.priv and A’.pub. A is not configured to start signing tokens with A’.priv until all other nodes in the cluster have a copy of A’.pub.
A’.pub is copied to the public key repository of each API server. So long as B and C have A’.pub they are ready to validate tokens signed by the new private key A’.priv.
After B and C have been updated with copies of A’.pub, server A can be updated to start signing tokens with the new key A’.priv. Once all tokens signed with A.priv are expired, A.priv and A.pub can be removed from all servers. It is important to allow for a grace period between configuring A to use A’.priv and removing A.priv in order to prevent the premature invalidation of tokens that haven’t expired yet.
Note that the rotation process could be simplified slightly in step #1. The JWS specification goes into detail about serialization and support for multiple signatures in a single token. See JWS section 7 for more information. The PyJWT library does not support multiple signatures on a single token. If it did, it would be possible to configure server A in step #1 to sign tokens with multiple private keys. Servers B and C would still be able to validate tokens from A because they have a copy of the public key (A.pub) used to create one of the token’s signatures. The rest of the rotation process remains the same as far as propagating A’.pub to the other servers and eventually configuring A to only sign tokens with A’.priv. If, or when, PyJWT supports this functionality, support for multiple signatures in the JWS provider can be reconsidered.
Traditional asymmetric keys can be revoked using revocation lists. At this time we are not going to support a revocation list implementation for JWT key pairs. The operator has the ability to sync public keys accordingly when they rotate new keys in and out. Keystone will only use the public keys on disk to validate tokens. Is could change in the future, but for now it keeps the key rotation and key utilities with keystone simpler.
Crypto-Agility & Future Work¶
This specification is targeting a single algorithm for the initial JWT implementation. If and when keystone decides to expand the implementation to include additional algorithms, we should allow for flexibility between configured algorithms, which will make it easier for operators to switch from one algorithm to another if they need to.
For example, the validation process using a JWT token provider might support validating multiple blessed algorithms, allowing multiple tokens signed with different algorithms to be validated without require configuration changes except on the signing node.
For the time being, if a deployment is using JWTs and needs to exercise crypto-agility, it is recommended they convert to Fernet tokens.
Recently, there have been various efforts that help solve authenticated encryption. One of these efforts was sparked by a concern with JWT, namely the JOSE header. The issue detailed in the report was specific to users being able to specify algorithms and exploit a validation weakness in various JWT libraries. All python libraries have been patched, but keystone should specifically rely on validating algorithm usage and never assuming algorithms to be supplied by end users. Please see the full report for details on the vulnerability and why we are going to strictly validate this information.
There is a proof-of-concept implementation for Platform Agnostic Security Tokens, or PASETO that takes a more strict stance on algorithm validation and the intended audience of the token. The strict stance of versioned protocols with PASETO is certainly advantageous, but the implementation and idea are still in the incipient stage. It’s certainly worth noting that we should keep out eye on this development and re-evaluate it if, or when, it gets more adoption.
For now, if keystone supplies strict algorithm validation to the JWT implementation, we should be able to offer a comparable backup option to fernet.
Since JWT is a widely used web standard, this will have a net positive impact on security. The implementation will use asymmetric signing, reducing risk of having to replicate or transfer private keys from one host to another. Since the token payloads are signed, data within the token will be readable to anyone who has the token. The token can only be validated using the corresponding public key of the private key used to sign the token originally. These will still be bearer tokens and so interception of one must still be guarded against.
There is a documented vulnerability that affected several JWT libraries, including one library written in Python.
In most cases, JSON Web Tokens will have a header, payload, and signature where
each section is delimited by a period (
.). The header contains an important
piece of information, which is how the token’s integrity is protected. This is
stored as the
alg attribute of the header. The library verifying the token
uses the algorithm specified in the header to perform an integrity check and
compares its results to the signature portion of the token.
Security concerns have been documented and raised that describe the issues with allowing clients to dictate algorithms used for token verification. This is a concern specifically with applications that support asymmetric and symmetric signing. An attacker could effectively bypass the verification check of a token by using a published, or known, public key to generate a JWT with a symmetric signing algorithm.
This would be applicable if keystone supported signed tokens and encrypted tokens with the same token provider implementation. This vulnerability has been addressed across various libraries after its discovery, but keystone should be aware of the overall technique that lead to it in the first place. We can mitigate this type of vulnerability in keystone by:
Ensuring keystone doesn’t blindly allow end users to specify which algorithm is used to verify the integrity of a token (e.g., only implementing support for
algsupplied in the token header is only ever populated by keystone
Ensure keystone only issues tokens of a single encryption or signing strategy (e.g., not allowing users to get signed token and encrypted tokens from the same server, thus mixing asymmetric and symmetric key usage at runtime)
Specifics about the vulnerability can be found in the report.
Notifications for JWTs will behave in the same way that they do for fernet tokens, including for revocation events.
Other End User Impact¶
This will have no end user impact. They will request and use JWTs in exactly the same way that they currently use fernet tokens.
It will be worth investigating performance differences between token providers that use asymmetric signing (JWT) and symmetric encryption (fernet). These difference, if significant, should be published in documentation as it might be useful for operators when choosing a token provider.
Other Deployer Impact¶
This is an optional, opt-in feature that will not be the default, so deployers will not be affected unless they choose to use JWT. In that case, deployers will need to set up a key repository before using JWTs. The key repository will contain asymmetric key pairs rather than just secret keys. The deployer will need to take care to sync and rotate keys the way they do with fernet tokens.
The new token type will reuse much of the work already done for fernet tokens and will follow similar code paths, so this will be relatively easy to maintain.
- Primary assignee:
Gage Hugo (gagehugo) Lance Bragstad (lbragstad) XiYuan Wang (wxy)
Refactor the fernet utilities modules to be generic enough to work with JWT or inheritable
keystone-managecommand to set up and rotate JWT signing keys
TokenFormatterclass to support JWT
Refactor the fernet token provider module to be inheritable or generic
Add a keystone doctor command to validate the setup in the same way that fernet is validated
There are three different libraries we can use to implement this functionality.
This library only supports token signing, or JWS. It does not support JWE, or authenticated encryption, yet. A minimum version of 1.0.1 is required, but this library is already included in OpenStack global requirements repository.
This library only supports token signing, or JWS. It does not support JWE, or authenticated encryption, yet. This library is not included in OpenStack global requirements.
This library supports both JWS and JWE, but it is not included in OpenStack global requirements.
This library supports both JWS and JWE, but its licensing is incompatible with OpenStack as it is AGPL.
Given the fact that the initial implementation of JWT is not going to rely on nested JWT tokens or encrypted payloads, it’s safe to assume that signing support will be sufficient. The PyJWT library is already included in global requirements and we don’t have a case to not use that specific library, which is compatible with OpenStack licensing.
[token]/provider configuration option will need to be documented,
as will the new