Fixes #684
Split the discussion of v2 authentication into two parts: 1) A specification of the handshake between the client, registry and authentication service. 2) A description of how `docker/distribution` implements this using JWT. This should make it clearer that `#2` is an implementation detail, and that clients should regard tokens as opaque entities that only the registry and authentication service should understand. Signed-off-by: Matt Moore <mattmoor@google.com>
This commit is contained in:
parent
f8109a78f9
commit
ca7c845dc0
2 changed files with 340 additions and 293 deletions
324
docs/spec/auth/jwt.md
Normal file
324
docs/spec/auth/jwt.md
Normal file
|
@ -0,0 +1,324 @@
|
||||||
|
<!--[metadata]>
|
||||||
|
+++
|
||||||
|
title = "Token Authentication Implementation"
|
||||||
|
description = "Describe the reference implementation of the Docker Registry v2 authentication schema"
|
||||||
|
keywords = ["registry, on-prem, images, tags, repository, distribution, JWT authentication, advanced"]
|
||||||
|
[menu.main]
|
||||||
|
parent="smn_registry_ref"
|
||||||
|
+++
|
||||||
|
<![end-metadata]-->
|
||||||
|
|
||||||
|
# Docker Registry v2 Bearer token specification
|
||||||
|
|
||||||
|
This specification covers the `docker/distribution` implementation of the
|
||||||
|
v2 Registry's authentication schema. Specifically, it describes the JSON
|
||||||
|
Web Token schema that `docker/distribution` has adopted to implement the
|
||||||
|
client-opaque Bearer token issued by an authentication service and
|
||||||
|
understood by the registry.
|
||||||
|
|
||||||
|
This document borrows heavily from the [JSON Web Token Draft Spec](https://tools.ietf.org/html/draft-ietf-oauth-json-web-token-32)
|
||||||
|
|
||||||
|
## Getting a Bearer Token
|
||||||
|
|
||||||
|
For this example, the client makes an HTTP GET request to the following URL:
|
||||||
|
|
||||||
|
```
|
||||||
|
https://auth.docker.io/token?service=registry.docker.io&scope=repository:samalba/my-app:pull,push
|
||||||
|
```
|
||||||
|
|
||||||
|
The token server should first attempt to authenticate the client using any
|
||||||
|
authentication credentials provided with the request. As of Docker 1.8, the
|
||||||
|
registry client in the Docker Engine only supports Basic Authentication to
|
||||||
|
these token servers. If an attempt to authenticate to the token server fails,
|
||||||
|
the token server should return a `401 Unauthorized` response indicating that
|
||||||
|
the provided credentials are invalid.
|
||||||
|
|
||||||
|
Whether the token server requires authentication is up to the policy of that
|
||||||
|
access control provider. Some requests may require authentication to determine
|
||||||
|
access (such as pushing or pulling a private repository) while others may not
|
||||||
|
(such as pulling from a public repository).
|
||||||
|
|
||||||
|
After authenticating the client (which may simply be an anonymous client if
|
||||||
|
no attempt was made to authenticate), the token server must next query its
|
||||||
|
access control list to determine whether the client has the requested scope. In
|
||||||
|
this example request, if I have authenticated as user `jlhawn`, the token
|
||||||
|
server will determine what access I have to the repository `samalba/my-app`
|
||||||
|
hosted by the entity `registry.docker.io`.
|
||||||
|
|
||||||
|
Once the token server has determined what access the client has to the
|
||||||
|
resources requested in the `scope` parameter, it will take the intersection of
|
||||||
|
the set of requested actions on each resource and the set of actions that the
|
||||||
|
client has in fact been granted. If the client only has a subset of the
|
||||||
|
requested access **it must not be considered an error** as it is not the
|
||||||
|
responsibility of the token server to indicate authorization errors as part of
|
||||||
|
this workflow.
|
||||||
|
|
||||||
|
Continuing with the example request, the token server will find that the
|
||||||
|
client's set of granted access to the repository is `[pull, push]` which when
|
||||||
|
intersected with the requested access `[pull, push]` yields an equal set. If
|
||||||
|
the granted access set was found only to be `[pull]` then the intersected set
|
||||||
|
would only be `[pull]`. If the client has no access to the repository then the
|
||||||
|
intersected set would be empty, `[]`.
|
||||||
|
|
||||||
|
It is this intersected set of access which is placed in the returned token.
|
||||||
|
|
||||||
|
The server will now construct a JSON Web Token to sign and return. A JSON Web
|
||||||
|
Token has 3 main parts:
|
||||||
|
|
||||||
|
1. Headers
|
||||||
|
|
||||||
|
The header of a JSON Web Token is a standard JOSE header. The "typ" field
|
||||||
|
will be "JWT" and it will also contain the "alg" which identifies the
|
||||||
|
signing algorithm used to produce the signature. It will also usually have
|
||||||
|
a "kid" field, the ID of the key which was used to sign the token.
|
||||||
|
|
||||||
|
Here is an example JOSE Header for a JSON Web Token (formatted with
|
||||||
|
whitespace for readability):
|
||||||
|
|
||||||
|
```
|
||||||
|
{
|
||||||
|
"typ": "JWT",
|
||||||
|
"alg": "ES256",
|
||||||
|
"kid": "PYYO:TEWU:V7JH:26JV:AQTZ:LJC3:SXVJ:XGHA:34F2:2LAQ:ZRMK:Z7Q6"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
It specifies that this object is going to be a JSON Web token signed using
|
||||||
|
the key with the given ID using the Elliptic Curve signature algorithm
|
||||||
|
using a SHA256 hash.
|
||||||
|
|
||||||
|
2. Claim Set
|
||||||
|
|
||||||
|
The Claim Set is a JSON struct containing these standard registered claim
|
||||||
|
name fields:
|
||||||
|
|
||||||
|
<dl>
|
||||||
|
<dt>
|
||||||
|
<code>iss</code> (Issuer)
|
||||||
|
</dt>
|
||||||
|
<dd>
|
||||||
|
The issuer of the token, typically the fqdn of the authorization
|
||||||
|
server.
|
||||||
|
</dd>
|
||||||
|
<dt>
|
||||||
|
<code>sub</code> (Subject)
|
||||||
|
</dt>
|
||||||
|
<dd>
|
||||||
|
The subject of the token; the name or id of the client which
|
||||||
|
requested it. This should be empty (`""`) if the client did not
|
||||||
|
authenticate.
|
||||||
|
</dd>
|
||||||
|
<dt>
|
||||||
|
<code>aud</code> (Audience)
|
||||||
|
</dt>
|
||||||
|
<dd>
|
||||||
|
The intended audience of the token; the name or id of the service
|
||||||
|
which will verify the token to authorize the client/subject.
|
||||||
|
</dd>
|
||||||
|
<dt>
|
||||||
|
<code>exp</code> (Expiration)
|
||||||
|
</dt>
|
||||||
|
<dd>
|
||||||
|
The token should only be considered valid up to this specified date
|
||||||
|
and time.
|
||||||
|
</dd>
|
||||||
|
<dt>
|
||||||
|
<code>nbf</code> (Not Before)
|
||||||
|
</dt>
|
||||||
|
<dd>
|
||||||
|
The token should not be considered valid before this specified date
|
||||||
|
and time.
|
||||||
|
</dd>
|
||||||
|
<dt>
|
||||||
|
<code>iat</code> (Issued At)
|
||||||
|
</dt>
|
||||||
|
<dd>
|
||||||
|
Specifies the date and time which the Authorization server
|
||||||
|
generated this token.
|
||||||
|
</dd>
|
||||||
|
<dt>
|
||||||
|
<code>jti</code> (JWT ID)
|
||||||
|
</dt>
|
||||||
|
<dd>
|
||||||
|
A unique identifier for this token. Can be used by the intended
|
||||||
|
audience to prevent replays of the token.
|
||||||
|
</dd>
|
||||||
|
</dl>
|
||||||
|
|
||||||
|
The Claim Set will also contain a private claim name unique to this
|
||||||
|
authorization server specification:
|
||||||
|
|
||||||
|
<dl>
|
||||||
|
<dt>
|
||||||
|
<code>access</code>
|
||||||
|
</dt>
|
||||||
|
<dd>
|
||||||
|
An array of access entry objects with the following fields:
|
||||||
|
|
||||||
|
<dl>
|
||||||
|
<dt>
|
||||||
|
<code>type</code>
|
||||||
|
</dt>
|
||||||
|
<dd>
|
||||||
|
The type of resource hosted by the service.
|
||||||
|
</dd>
|
||||||
|
<dt>
|
||||||
|
<code>name</code>
|
||||||
|
</dt>
|
||||||
|
<dd>
|
||||||
|
The name of the resource of the given type hosted by the
|
||||||
|
service.
|
||||||
|
</dd>
|
||||||
|
<dt>
|
||||||
|
<code>actions</code>
|
||||||
|
</dt>
|
||||||
|
<dd>
|
||||||
|
An array of strings which give the actions authorized on
|
||||||
|
this resource.
|
||||||
|
</dd>
|
||||||
|
</dl>
|
||||||
|
</dd>
|
||||||
|
</dl>
|
||||||
|
|
||||||
|
Here is an example of such a JWT Claim Set (formatted with whitespace for
|
||||||
|
readability):
|
||||||
|
|
||||||
|
```
|
||||||
|
{
|
||||||
|
"iss": "auth.docker.com",
|
||||||
|
"sub": "jlhawn",
|
||||||
|
"aud": "registry.docker.com",
|
||||||
|
"exp": 1415387315,
|
||||||
|
"nbf": 1415387015,
|
||||||
|
"iat": 1415387015,
|
||||||
|
"jti": "tYJCO1c6cnyy7kAn0c7rKPgbV1H1bFws",
|
||||||
|
"access": [
|
||||||
|
{
|
||||||
|
"type": "repository",
|
||||||
|
"name": "samalba/my-app",
|
||||||
|
"actions": [
|
||||||
|
"pull",
|
||||||
|
"push"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
3. Signature
|
||||||
|
|
||||||
|
The authorization server will produce a JOSE header and Claim Set with no
|
||||||
|
extraneous whitespace, i.e., the JOSE Header from above would be
|
||||||
|
|
||||||
|
```
|
||||||
|
{"typ":"JWT","alg":"ES256","kid":"PYYO:TEWU:V7JH:26JV:AQTZ:LJC3:SXVJ:XGHA:34F2:2LAQ:ZRMK:Z7Q6"}
|
||||||
|
```
|
||||||
|
|
||||||
|
and the Claim Set from above would be
|
||||||
|
|
||||||
|
```
|
||||||
|
{"iss":"auth.docker.com","sub":"jlhawn","aud":"registry.docker.com","exp":1415387315,"nbf":1415387015,"iat":1415387015,"jti":"tYJCO1c6cnyy7kAn0c7rKPgbV1H1bFws","access":[{"type":"repository","name":"samalba/my-app","actions":["push","pull"]}]}
|
||||||
|
```
|
||||||
|
|
||||||
|
The utf-8 representation of this JOSE header and Claim Set are then
|
||||||
|
url-safe base64 encoded (sans trailing '=' buffer), producing:
|
||||||
|
|
||||||
|
```
|
||||||
|
eyJ0eXAiOiJKV1QiLCJhbGciOiJFUzI1NiIsImtpZCI6IlBZWU86VEVXVTpWN0pIOjI2SlY6QVFUWjpMSkMzOlNYVko6WEdIQTozNEYyOjJMQVE6WlJNSzpaN1E2In0
|
||||||
|
```
|
||||||
|
|
||||||
|
for the JOSE Header and
|
||||||
|
|
||||||
|
```
|
||||||
|
eyJpc3MiOiJhdXRoLmRvY2tlci5jb20iLCJzdWIiOiJqbGhhd24iLCJhdWQiOiJyZWdpc3RyeS5kb2NrZXIuY29tIiwiZXhwIjoxNDE1Mzg3MzE1LCJuYmYiOjE0MTUzODcwMTUsImlhdCI6MTQxNTM4NzAxNSwianRpIjoidFlKQ08xYzZjbnl5N2tBbjBjN3JLUGdiVjFIMWJGd3MiLCJhY2Nlc3MiOlt7InR5cGUiOiJyZXBvc2l0b3J5IiwibmFtZSI6InNhbWFsYmEvbXktYXBwIiwiYWN0aW9ucyI6WyJwdXNoIl19XX0
|
||||||
|
```
|
||||||
|
|
||||||
|
for the Claim Set. These two are concatenated using a '.' character,
|
||||||
|
yielding the string:
|
||||||
|
|
||||||
|
```
|
||||||
|
eyJ0eXAiOiJKV1QiLCJhbGciOiJFUzI1NiIsImtpZCI6IlBZWU86VEVXVTpWN0pIOjI2SlY6QVFUWjpMSkMzOlNYVko6WEdIQTozNEYyOjJMQVE6WlJNSzpaN1E2In0.eyJpc3MiOiJhdXRoLmRvY2tlci5jb20iLCJzdWIiOiJqbGhhd24iLCJhdWQiOiJyZWdpc3RyeS5kb2NrZXIuY29tIiwiZXhwIjoxNDE1Mzg3MzE1LCJuYmYiOjE0MTUzODcwMTUsImlhdCI6MTQxNTM4NzAxNSwianRpIjoidFlKQ08xYzZjbnl5N2tBbjBjN3JLUGdiVjFIMWJGd3MiLCJhY2Nlc3MiOlt7InR5cGUiOiJyZXBvc2l0b3J5IiwibmFtZSI6InNhbWFsYmEvbXktYXBwIiwiYWN0aW9ucyI6WyJwdXNoIl19XX0
|
||||||
|
```
|
||||||
|
|
||||||
|
This is then used as the payload to a the `ES256` signature algorithm
|
||||||
|
specified in the JOSE header and specified fully in [Section 3.4 of the JSON Web Algorithms (JWA)
|
||||||
|
draft specification](https://tools.ietf.org/html/draft-ietf-jose-json-web-algorithms-38#section-3.4)
|
||||||
|
|
||||||
|
This example signature will use the following ECDSA key for the server:
|
||||||
|
|
||||||
|
```
|
||||||
|
{
|
||||||
|
"kty": "EC",
|
||||||
|
"crv": "P-256",
|
||||||
|
"kid": "PYYO:TEWU:V7JH:26JV:AQTZ:LJC3:SXVJ:XGHA:34F2:2LAQ:ZRMK:Z7Q6",
|
||||||
|
"d": "R7OnbfMaD5J2jl7GeE8ESo7CnHSBm_1N2k9IXYFrKJA",
|
||||||
|
"x": "m7zUpx3b-zmVE5cymSs64POG9QcyEpJaYCD82-549_Q",
|
||||||
|
"y": "dU3biz8sZ_8GPB-odm8Wxz3lNDr1xcAQQPQaOcr1fmc"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
A resulting signature of the above payload using this key is:
|
||||||
|
|
||||||
|
```
|
||||||
|
QhflHPfbd6eVF4lM9bwYpFZIV0PfikbyXuLx959ykRTBpe3CYnzs6YBK8FToVb5R47920PVLrh8zuLzdCr9t3w
|
||||||
|
```
|
||||||
|
|
||||||
|
Concatenating all of these together with a `.` character gives the
|
||||||
|
resulting JWT:
|
||||||
|
|
||||||
|
```
|
||||||
|
eyJ0eXAiOiJKV1QiLCJhbGciOiJFUzI1NiIsImtpZCI6IlBZWU86VEVXVTpWN0pIOjI2SlY6QVFUWjpMSkMzOlNYVko6WEdIQTozNEYyOjJMQVE6WlJNSzpaN1E2In0.eyJpc3MiOiJhdXRoLmRvY2tlci5jb20iLCJzdWIiOiJqbGhhd24iLCJhdWQiOiJyZWdpc3RyeS5kb2NrZXIuY29tIiwiZXhwIjoxNDE1Mzg3MzE1LCJuYmYiOjE0MTUzODcwMTUsImlhdCI6MTQxNTM4NzAxNSwianRpIjoidFlKQ08xYzZjbnl5N2tBbjBjN3JLUGdiVjFIMWJGd3MiLCJhY2Nlc3MiOlt7InR5cGUiOiJyZXBvc2l0b3J5IiwibmFtZSI6InNhbWFsYmEvbXktYXBwIiwiYWN0aW9ucyI6WyJwdXNoIl19XX0.QhflHPfbd6eVF4lM9bwYpFZIV0PfikbyXuLx959ykRTBpe3CYnzs6YBK8FToVb5R47920PVLrh8zuLzdCr9t3w
|
||||||
|
```
|
||||||
|
|
||||||
|
This can now be placed in an HTTP response and returned to the client to use to
|
||||||
|
authenticate to the audience service:
|
||||||
|
|
||||||
|
|
||||||
|
```
|
||||||
|
HTTP/1.1 200 OK
|
||||||
|
Content-Type: application/json
|
||||||
|
|
||||||
|
{"token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJFUzI1NiIsImtpZCI6IlBZWU86VEVXVTpWN0pIOjI2SlY6QVFUWjpMSkMzOlNYVko6WEdIQTozNEYyOjJMQVE6WlJNSzpaN1E2In0.eyJpc3MiOiJhdXRoLmRvY2tlci5jb20iLCJzdWIiOiJqbGhhd24iLCJhdWQiOiJyZWdpc3RyeS5kb2NrZXIuY29tIiwiZXhwIjoxNDE1Mzg3MzE1LCJuYmYiOjE0MTUzODcwMTUsImlhdCI6MTQxNTM4NzAxNSwianRpIjoidFlKQ08xYzZjbnl5N2tBbjBjN3JLUGdiVjFIMWJGd3MiLCJhY2Nlc3MiOlt7InR5cGUiOiJyZXBvc2l0b3J5IiwibmFtZSI6InNhbWFsYmEvbXktYXBwIiwiYWN0aW9ucyI6WyJwdXNoIl19XX0.QhflHPfbd6eVF4lM9bwYpFZIV0PfikbyXuLx959ykRTBpe3CYnzs6YBK8FToVb5R47920PVLrh8zuLzdCr9t3w"}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Using the signed token
|
||||||
|
|
||||||
|
Once the client has a token, it will try the registry request again with the
|
||||||
|
token placed in the HTTP `Authorization` header like so:
|
||||||
|
|
||||||
|
```
|
||||||
|
Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJFUzI1NiIsImtpZCI6IkJWM0Q6MkFWWjpVQjVaOktJQVA6SU5QTDo1RU42Ok40SjQ6Nk1XTzpEUktFOkJWUUs6M0ZKTDpQT1RMIn0.eyJpc3MiOiJhdXRoLmRvY2tlci5jb20iLCJzdWIiOiJCQ0NZOk9VNlo6UUVKNTpXTjJDOjJBVkM6WTdZRDpBM0xZOjQ1VVc6NE9HRDpLQUxMOkNOSjU6NUlVTCIsImF1ZCI6InJlZ2lzdHJ5LmRvY2tlci5jb20iLCJleHAiOjE0MTUzODczMTUsIm5iZiI6MTQxNTM4NzAxNSwiaWF0IjoxNDE1Mzg3MDE1LCJqdGkiOiJ0WUpDTzFjNmNueXk3a0FuMGM3cktQZ2JWMUgxYkZ3cyIsInNjb3BlIjoiamxoYXduOnJlcG9zaXRvcnk6c2FtYWxiYS9teS1hcHA6cHVzaCxwdWxsIGpsaGF3bjpuYW1lc3BhY2U6c2FtYWxiYTpwdWxsIn0.Y3zZSwaZPqy4y9oRBVRImZyv3m_S9XDHF1tWwN7mL52C_IiA73SJkWVNsvNqpJIn5h7A2F8biv_S2ppQ1lgkbw
|
||||||
|
```
|
||||||
|
|
||||||
|
This is also described in [Section 2.1 of RFC 6750: The OAuth 2.0 Authorization Framework: Bearer Token Usage](https://tools.ietf.org/html/rfc6750#section-2.1)
|
||||||
|
|
||||||
|
## Verifying the token
|
||||||
|
|
||||||
|
The registry must now verify the token presented by the user by inspecting the
|
||||||
|
claim set within. The registry will:
|
||||||
|
|
||||||
|
- Ensure that the issuer (`iss` claim) is an authority it trusts.
|
||||||
|
- Ensure that the registry identifies as the audience (`aud` claim).
|
||||||
|
- Check that the current time is between the `nbf` and `exp` claim times.
|
||||||
|
- If enforcing single-use tokens, check that the JWT ID (`jti` claim) value has
|
||||||
|
not been seen before.
|
||||||
|
- To enforce this, the registry may keep a record of `jti`s it has seen for
|
||||||
|
up to the `exp` time of the token to prevent token replays.
|
||||||
|
- Check the `access` claim value and use the identified resources and the list
|
||||||
|
of actions authorized to determine whether the token grants the required
|
||||||
|
level of access for the operation the client is attempting to perform.
|
||||||
|
- Verify that the signature of the token is valid.
|
||||||
|
|
||||||
|
If any of these requirements are not met, the registry will return a
|
||||||
|
`403 Forbidden` response to indicate that the token is invalid.
|
||||||
|
|
||||||
|
**Note**: it is only at this point in the workflow that an authorization error
|
||||||
|
may occur. The token server should *not* return errors when the user does not
|
||||||
|
have the requested authorization. Instead, the returned token should indicate
|
||||||
|
whatever of the requested scope the client does have (the intersection of
|
||||||
|
requested and granted access). If the token does not supply proper
|
||||||
|
authorization then the registry will return the appropriate error.
|
||||||
|
|
||||||
|
At no point in this process should the registry need to call back to the
|
||||||
|
authorization server. The registry only needs to be supplied with the trusted
|
||||||
|
public keys to verify the token signatures.
|
|
@ -1,8 +1,8 @@
|
||||||
<!--[metadata]>
|
<!--[metadata]>
|
||||||
+++
|
+++
|
||||||
title = "Token Authentication"
|
title = "Token Authentication Specification"
|
||||||
description = "Introduces the Docker Registry v2 authentication"
|
description = "Specifies the Docker Registry v2 authentication"
|
||||||
keywords = ["registry, on-prem, images, tags, repository, distribution, JWT authentication, advanced"]
|
keywords = ["registry, on-prem, images, tags, repository, distribution, Bearer authentication, advanced"]
|
||||||
[menu.main]
|
[menu.main]
|
||||||
parent="smn_registry_ref"
|
parent="smn_registry_ref"
|
||||||
+++
|
+++
|
||||||
|
@ -10,43 +10,7 @@ parent="smn_registry_ref"
|
||||||
|
|
||||||
# Docker Registry v2 authentication via central service
|
# Docker Registry v2 authentication via central service
|
||||||
|
|
||||||
Today a Docker Registry can run in standalone mode in which there are no
|
This document outlines the v2 Docker registry authentication scheme:
|
||||||
authorization checks. While adding your own HTTP authorization requirements in
|
|
||||||
a proxy placed between the client and the registry can give you greater access
|
|
||||||
control, we'd like a native authorization mechanism that's public key based
|
|
||||||
with access control lists managed separately with the ability to have fine
|
|
||||||
granularity in access control on a by-key, by-user, by-namespace, and
|
|
||||||
by-repository basis. In v1 this can be configured by specifying an
|
|
||||||
`index_endpoint` in the registry's config. Clients present tokens generated by
|
|
||||||
the index and tokens are validated on-line by the registry with every request.
|
|
||||||
This results in a complex authentication and authorization loop that occurs
|
|
||||||
with every registry operation. Some people are very familiar with this image:
|
|
||||||
|
|
||||||
![index auth](https://docs.docker.com/static_files/docker_pull_chart.png)
|
|
||||||
|
|
||||||
The above image outlines the 6-step process in accessing the Official Docker
|
|
||||||
Registry.
|
|
||||||
|
|
||||||
1. Contact the Docker Hub to know where I should download “samalba/busybox”
|
|
||||||
2. Docker Hub replies:
|
|
||||||
a. samalba/busybox is on Registry A
|
|
||||||
b. here are the checksums for samalba/busybox (for all layers)
|
|
||||||
c. token
|
|
||||||
3. Contact Registry A to receive the layers for samalba/busybox (all of them to
|
|
||||||
the base image). Registry A is authoritative for “samalba/busybox” but keeps
|
|
||||||
a copy of all inherited layers and serve them all from the same location.
|
|
||||||
4. Registry contacts Docker Hub to verify if token/user is allowed to download
|
|
||||||
images.
|
|
||||||
5. Docker Hub returns true/false lettings registry know if it should proceed or
|
|
||||||
error out.
|
|
||||||
6. Get the payload for all layers.
|
|
||||||
|
|
||||||
The goal of this document is to outline a way to eliminate steps 4 and 5 from
|
|
||||||
the above process by using cryptographically signed tokens and no longer
|
|
||||||
require the client to authenticate each request with a username and password
|
|
||||||
stored locally in plain text.
|
|
||||||
|
|
||||||
The v2 registry token workflow is more like this:
|
|
||||||
|
|
||||||
![v2 registry auth](https://docs.google.com/drawings/d/1EHZU9uBLmcH0kytDClBv6jv6WR4xZjE8RKEUw1mARJA/pub?w=480&h=360)
|
![v2 registry auth](https://docs.google.com/drawings/d/1EHZU9uBLmcH0kytDClBv6jv6WR4xZjE8RKEUw1mARJA/pub?w=480&h=360)
|
||||||
|
|
||||||
|
@ -54,13 +18,13 @@ The v2 registry token workflow is more like this:
|
||||||
2. If the registry requires authorization it will return a `401 Unauthorized`
|
2. If the registry requires authorization it will return a `401 Unauthorized`
|
||||||
HTTP response with information on how to authenticate.
|
HTTP response with information on how to authenticate.
|
||||||
3. The registry client makes a request to the authorization service for a
|
3. The registry client makes a request to the authorization service for a
|
||||||
signed JSON Web Token.
|
Bearer token.
|
||||||
4. The authorization service returns an opaque token representing the client's
|
4. The authorization service returns an opaque Bearer token representing the
|
||||||
authorized access.
|
client's authorized access.
|
||||||
5. The client retries the original request with the token embedded in the
|
5. The client retries the original request with the Bearer token embedded in
|
||||||
request header.
|
the request's Authorization header.
|
||||||
6. The Registry authorizes the client by validating the token and the claim set
|
6. The Registry authorizes the client by validating the Bearer token and the
|
||||||
embedded within it and begins the push/pull session as usual.
|
claim set embedded within it and begins the push/pull session as usual.
|
||||||
|
|
||||||
## Requirements
|
## Requirements
|
||||||
|
|
||||||
|
@ -75,13 +39,11 @@ The v2 registry token workflow is more like this:
|
||||||
|
|
||||||
## Authorization Server Endpoint Descriptions
|
## Authorization Server Endpoint Descriptions
|
||||||
|
|
||||||
This document borrows heavily from the [JSON Web Token Draft Spec](https://tools.ietf.org/html/draft-ietf-oauth-json-web-token-32)
|
|
||||||
|
|
||||||
The described server is meant to serve as a standalone access control manager
|
The described server is meant to serve as a standalone access control manager
|
||||||
for resources hosted by other services which wish to authenticate and manage
|
for resources hosted by other services which wish to authenticate and manage
|
||||||
authorizations using a separate access control manager.
|
authorizations using a separate access control manager.
|
||||||
|
|
||||||
Such a service could be used by the official Docker Registry to authenticate
|
A service like this is used by the official Docker Registry to authenticate
|
||||||
clients and verify their authorization to Docker image repositories.
|
clients and verify their authorization to Docker image repositories.
|
||||||
|
|
||||||
As of Docker 1.6, the registry client within the Docker Engine has been updated
|
As of Docker 1.6, the registry client within the Docker Engine has been updated
|
||||||
|
@ -151,7 +113,7 @@ challenge, the client will need to make a `GET` request to the URL
|
||||||
</dd>
|
</dd>
|
||||||
</dl>
|
</dl>
|
||||||
|
|
||||||
#### Example Token Request
|
#### Example
|
||||||
|
|
||||||
For this example, the client makes an HTTP GET request to the following URL:
|
For this example, the client makes an HTTP GET request to the following URL:
|
||||||
|
|
||||||
|
@ -195,218 +157,10 @@ intersected set would be empty, `[]`.
|
||||||
|
|
||||||
It is this intersected set of access which is placed in the returned token.
|
It is this intersected set of access which is placed in the returned token.
|
||||||
|
|
||||||
The server will now construct a JSON Web Token to sign and return. A JSON Web
|
The server then constructs an implementation-specific token with this
|
||||||
Token has 3 main parts:
|
intersected set of access, and returns it to the Docker client to use to
|
||||||
|
|
||||||
1. Headers
|
|
||||||
|
|
||||||
The header of a JSON Web Token is a standard JOSE header. The "typ" field
|
|
||||||
will be "JWT" and it will also contain the "alg" which identifies the
|
|
||||||
signing algorithm used to produce the signature. It will also usually have
|
|
||||||
a "kid" field, the ID of the key which was used to sign the token.
|
|
||||||
|
|
||||||
Here is an example JOSE Header for a JSON Web Token (formatted with
|
|
||||||
whitespace for readability):
|
|
||||||
|
|
||||||
```
|
|
||||||
{
|
|
||||||
"typ": "JWT",
|
|
||||||
"alg": "ES256",
|
|
||||||
"kid": "PYYO:TEWU:V7JH:26JV:AQTZ:LJC3:SXVJ:XGHA:34F2:2LAQ:ZRMK:Z7Q6"
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
It specifies that this object is going to be a JSON Web token signed using
|
|
||||||
the key with the given ID using the Elliptic Curve signature algorithm
|
|
||||||
using a SHA256 hash.
|
|
||||||
|
|
||||||
2. Claim Set
|
|
||||||
|
|
||||||
The Claim Set is a JSON struct containing these standard registered claim
|
|
||||||
name fields:
|
|
||||||
|
|
||||||
<dl>
|
|
||||||
<dt>
|
|
||||||
<code>iss</code> (Issuer)
|
|
||||||
</dt>
|
|
||||||
<dd>
|
|
||||||
The issuer of the token, typically the fqdn of the authorization
|
|
||||||
server.
|
|
||||||
</dd>
|
|
||||||
<dt>
|
|
||||||
<code>sub</code> (Subject)
|
|
||||||
</dt>
|
|
||||||
<dd>
|
|
||||||
The subject of the token; the name or id of the client which
|
|
||||||
requested it. This should be empty (`""`) if the client did not
|
|
||||||
authenticate.
|
|
||||||
</dd>
|
|
||||||
<dt>
|
|
||||||
<code>aud</code> (Audience)
|
|
||||||
</dt>
|
|
||||||
<dd>
|
|
||||||
The intended audience of the token; the name or id of the service
|
|
||||||
which will verify the token to authorize the client/subject.
|
|
||||||
</dd>
|
|
||||||
<dt>
|
|
||||||
<code>exp</code> (Expiration)
|
|
||||||
</dt>
|
|
||||||
<dd>
|
|
||||||
The token should only be considered valid up to this specified date
|
|
||||||
and time.
|
|
||||||
</dd>
|
|
||||||
<dt>
|
|
||||||
<code>nbf</code> (Not Before)
|
|
||||||
</dt>
|
|
||||||
<dd>
|
|
||||||
The token should not be considered valid before this specified date
|
|
||||||
and time.
|
|
||||||
</dd>
|
|
||||||
<dt>
|
|
||||||
<code>iat</code> (Issued At)
|
|
||||||
</dt>
|
|
||||||
<dd>
|
|
||||||
Specifies the date and time which the Authorization server
|
|
||||||
generated this token.
|
|
||||||
</dd>
|
|
||||||
<dt>
|
|
||||||
<code>jti</code> (JWT ID)
|
|
||||||
</dt>
|
|
||||||
<dd>
|
|
||||||
A unique identifier for this token. Can be used by the intended
|
|
||||||
audience to prevent replays of the token.
|
|
||||||
</dd>
|
|
||||||
</dl>
|
|
||||||
|
|
||||||
The Claim Set will also contain a private claim name unique to this
|
|
||||||
authorization server specification:
|
|
||||||
|
|
||||||
<dl>
|
|
||||||
<dt>
|
|
||||||
<code>access</code>
|
|
||||||
</dt>
|
|
||||||
<dd>
|
|
||||||
An array of access entry objects with the following fields:
|
|
||||||
|
|
||||||
<dl>
|
|
||||||
<dt>
|
|
||||||
<code>type</code>
|
|
||||||
</dt>
|
|
||||||
<dd>
|
|
||||||
The type of resource hosted by the service.
|
|
||||||
</dd>
|
|
||||||
<dt>
|
|
||||||
<code>name</code>
|
|
||||||
</dt>
|
|
||||||
<dd>
|
|
||||||
The name of the resource of the given type hosted by the
|
|
||||||
service.
|
|
||||||
</dd>
|
|
||||||
<dt>
|
|
||||||
<code>actions</code>
|
|
||||||
</dt>
|
|
||||||
<dd>
|
|
||||||
An array of strings which give the actions authorized on
|
|
||||||
this resource.
|
|
||||||
</dd>
|
|
||||||
</dl>
|
|
||||||
</dd>
|
|
||||||
</dl>
|
|
||||||
|
|
||||||
Here is an example of such a JWT Claim Set (formatted with whitespace for
|
|
||||||
readability):
|
|
||||||
|
|
||||||
```
|
|
||||||
{
|
|
||||||
"iss": "auth.docker.com",
|
|
||||||
"sub": "jlhawn",
|
|
||||||
"aud": "registry.docker.com",
|
|
||||||
"exp": 1415387315,
|
|
||||||
"nbf": 1415387015,
|
|
||||||
"iat": 1415387015,
|
|
||||||
"jti": "tYJCO1c6cnyy7kAn0c7rKPgbV1H1bFws",
|
|
||||||
"access": [
|
|
||||||
{
|
|
||||||
"type": "repository",
|
|
||||||
"name": "samalba/my-app",
|
|
||||||
"actions": [
|
|
||||||
"pull",
|
|
||||||
"push"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
3. Signature
|
|
||||||
|
|
||||||
The authorization server will produce a JOSE header and Claim Set with no
|
|
||||||
extraneous whitespace, i.e., the JOSE Header from above would be
|
|
||||||
|
|
||||||
```
|
|
||||||
{"typ":"JWT","alg":"ES256","kid":"PYYO:TEWU:V7JH:26JV:AQTZ:LJC3:SXVJ:XGHA:34F2:2LAQ:ZRMK:Z7Q6"}
|
|
||||||
```
|
|
||||||
|
|
||||||
and the Claim Set from above would be
|
|
||||||
|
|
||||||
```
|
|
||||||
{"iss":"auth.docker.com","sub":"jlhawn","aud":"registry.docker.com","exp":1415387315,"nbf":1415387015,"iat":1415387015,"jti":"tYJCO1c6cnyy7kAn0c7rKPgbV1H1bFws","access":[{"type":"repository","name":"samalba/my-app","actions":["push","pull"]}]}
|
|
||||||
```
|
|
||||||
|
|
||||||
The utf-8 representation of this JOSE header and Claim Set are then
|
|
||||||
url-safe base64 encoded (sans trailing '=' buffer), producing:
|
|
||||||
|
|
||||||
```
|
|
||||||
eyJ0eXAiOiJKV1QiLCJhbGciOiJFUzI1NiIsImtpZCI6IlBZWU86VEVXVTpWN0pIOjI2SlY6QVFUWjpMSkMzOlNYVko6WEdIQTozNEYyOjJMQVE6WlJNSzpaN1E2In0
|
|
||||||
```
|
|
||||||
|
|
||||||
for the JOSE Header and
|
|
||||||
|
|
||||||
```
|
|
||||||
eyJpc3MiOiJhdXRoLmRvY2tlci5jb20iLCJzdWIiOiJqbGhhd24iLCJhdWQiOiJyZWdpc3RyeS5kb2NrZXIuY29tIiwiZXhwIjoxNDE1Mzg3MzE1LCJuYmYiOjE0MTUzODcwMTUsImlhdCI6MTQxNTM4NzAxNSwianRpIjoidFlKQ08xYzZjbnl5N2tBbjBjN3JLUGdiVjFIMWJGd3MiLCJhY2Nlc3MiOlt7InR5cGUiOiJyZXBvc2l0b3J5IiwibmFtZSI6InNhbWFsYmEvbXktYXBwIiwiYWN0aW9ucyI6WyJwdXNoIl19XX0
|
|
||||||
```
|
|
||||||
|
|
||||||
for the Claim Set. These two are concatenated using a '.' character,
|
|
||||||
yielding the string:
|
|
||||||
|
|
||||||
```
|
|
||||||
eyJ0eXAiOiJKV1QiLCJhbGciOiJFUzI1NiIsImtpZCI6IlBZWU86VEVXVTpWN0pIOjI2SlY6QVFUWjpMSkMzOlNYVko6WEdIQTozNEYyOjJMQVE6WlJNSzpaN1E2In0.eyJpc3MiOiJhdXRoLmRvY2tlci5jb20iLCJzdWIiOiJqbGhhd24iLCJhdWQiOiJyZWdpc3RyeS5kb2NrZXIuY29tIiwiZXhwIjoxNDE1Mzg3MzE1LCJuYmYiOjE0MTUzODcwMTUsImlhdCI6MTQxNTM4NzAxNSwianRpIjoidFlKQ08xYzZjbnl5N2tBbjBjN3JLUGdiVjFIMWJGd3MiLCJhY2Nlc3MiOlt7InR5cGUiOiJyZXBvc2l0b3J5IiwibmFtZSI6InNhbWFsYmEvbXktYXBwIiwiYWN0aW9ucyI6WyJwdXNoIl19XX0
|
|
||||||
```
|
|
||||||
|
|
||||||
This is then used as the payload to a the `ES256` signature algorithm
|
|
||||||
specified in the JOSE header and specified fully in [Section 3.4 of the JSON Web Algorithms (JWA)
|
|
||||||
draft specification](https://tools.ietf.org/html/draft-ietf-jose-json-web-algorithms-38#section-3.4)
|
|
||||||
|
|
||||||
This example signature will use the following ECDSA key for the server:
|
|
||||||
|
|
||||||
```
|
|
||||||
{
|
|
||||||
"kty": "EC",
|
|
||||||
"crv": "P-256",
|
|
||||||
"kid": "PYYO:TEWU:V7JH:26JV:AQTZ:LJC3:SXVJ:XGHA:34F2:2LAQ:ZRMK:Z7Q6",
|
|
||||||
"d": "R7OnbfMaD5J2jl7GeE8ESo7CnHSBm_1N2k9IXYFrKJA",
|
|
||||||
"x": "m7zUpx3b-zmVE5cymSs64POG9QcyEpJaYCD82-549_Q",
|
|
||||||
"y": "dU3biz8sZ_8GPB-odm8Wxz3lNDr1xcAQQPQaOcr1fmc"
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
A resulting signature of the above payload using this key is:
|
|
||||||
|
|
||||||
```
|
|
||||||
QhflHPfbd6eVF4lM9bwYpFZIV0PfikbyXuLx959ykRTBpe3CYnzs6YBK8FToVb5R47920PVLrh8zuLzdCr9t3w
|
|
||||||
```
|
|
||||||
|
|
||||||
Concatenating all of these together with a `.` character gives the
|
|
||||||
resulting JWT:
|
|
||||||
|
|
||||||
```
|
|
||||||
eyJ0eXAiOiJKV1QiLCJhbGciOiJFUzI1NiIsImtpZCI6IlBZWU86VEVXVTpWN0pIOjI2SlY6QVFUWjpMSkMzOlNYVko6WEdIQTozNEYyOjJMQVE6WlJNSzpaN1E2In0.eyJpc3MiOiJhdXRoLmRvY2tlci5jb20iLCJzdWIiOiJqbGhhd24iLCJhdWQiOiJyZWdpc3RyeS5kb2NrZXIuY29tIiwiZXhwIjoxNDE1Mzg3MzE1LCJuYmYiOjE0MTUzODcwMTUsImlhdCI6MTQxNTM4NzAxNSwianRpIjoidFlKQ08xYzZjbnl5N2tBbjBjN3JLUGdiVjFIMWJGd3MiLCJhY2Nlc3MiOlt7InR5cGUiOiJyZXBvc2l0b3J5IiwibmFtZSI6InNhbWFsYmEvbXktYXBwIiwiYWN0aW9ucyI6WyJwdXNoIl19XX0.QhflHPfbd6eVF4lM9bwYpFZIV0PfikbyXuLx959ykRTBpe3CYnzs6YBK8FToVb5R47920PVLrh8zuLzdCr9t3w
|
|
||||||
```
|
|
||||||
|
|
||||||
This can now be placed in an HTTP response and returned to the client to use to
|
|
||||||
authenticate to the audience service:
|
authenticate to the audience service:
|
||||||
|
|
||||||
|
|
||||||
```
|
```
|
||||||
HTTP/1.1 200 OK
|
HTTP/1.1 200 OK
|
||||||
Content-Type: application/json
|
Content-Type: application/json
|
||||||
|
@ -414,7 +168,7 @@ Content-Type: application/json
|
||||||
{"token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJFUzI1NiIsImtpZCI6IlBZWU86VEVXVTpWN0pIOjI2SlY6QVFUWjpMSkMzOlNYVko6WEdIQTozNEYyOjJMQVE6WlJNSzpaN1E2In0.eyJpc3MiOiJhdXRoLmRvY2tlci5jb20iLCJzdWIiOiJqbGhhd24iLCJhdWQiOiJyZWdpc3RyeS5kb2NrZXIuY29tIiwiZXhwIjoxNDE1Mzg3MzE1LCJuYmYiOjE0MTUzODcwMTUsImlhdCI6MTQxNTM4NzAxNSwianRpIjoidFlKQ08xYzZjbnl5N2tBbjBjN3JLUGdiVjFIMWJGd3MiLCJhY2Nlc3MiOlt7InR5cGUiOiJyZXBvc2l0b3J5IiwibmFtZSI6InNhbWFsYmEvbXktYXBwIiwiYWN0aW9ucyI6WyJwdXNoIl19XX0.QhflHPfbd6eVF4lM9bwYpFZIV0PfikbyXuLx959ykRTBpe3CYnzs6YBK8FToVb5R47920PVLrh8zuLzdCr9t3w"}
|
{"token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJFUzI1NiIsImtpZCI6IlBZWU86VEVXVTpWN0pIOjI2SlY6QVFUWjpMSkMzOlNYVko6WEdIQTozNEYyOjJMQVE6WlJNSzpaN1E2In0.eyJpc3MiOiJhdXRoLmRvY2tlci5jb20iLCJzdWIiOiJqbGhhd24iLCJhdWQiOiJyZWdpc3RyeS5kb2NrZXIuY29tIiwiZXhwIjoxNDE1Mzg3MzE1LCJuYmYiOjE0MTUzODcwMTUsImlhdCI6MTQxNTM4NzAxNSwianRpIjoidFlKQ08xYzZjbnl5N2tBbjBjN3JLUGdiVjFIMWJGd3MiLCJhY2Nlc3MiOlt7InR5cGUiOiJyZXBvc2l0b3J5IiwibmFtZSI6InNhbWFsYmEvbXktYXBwIiwiYWN0aW9ucyI6WyJwdXNoIl19XX0.QhflHPfbd6eVF4lM9bwYpFZIV0PfikbyXuLx959ykRTBpe3CYnzs6YBK8FToVb5R47920PVLrh8zuLzdCr9t3w"}
|
||||||
```
|
```
|
||||||
|
|
||||||
## Using the signed token
|
## Using the Bearer token
|
||||||
|
|
||||||
Once the client has a token, it will try the registry request again with the
|
Once the client has a token, it will try the registry request again with the
|
||||||
token placed in the HTTP `Authorization` header like so:
|
token placed in the HTTP `Authorization` header like so:
|
||||||
|
@ -424,34 +178,3 @@ Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJFUzI1NiIsImtpZCI6IkJWM0Q6MkFWWj
|
||||||
```
|
```
|
||||||
|
|
||||||
This is also described in [Section 2.1 of RFC 6750: The OAuth 2.0 Authorization Framework: Bearer Token Usage](https://tools.ietf.org/html/rfc6750#section-2.1)
|
This is also described in [Section 2.1 of RFC 6750: The OAuth 2.0 Authorization Framework: Bearer Token Usage](https://tools.ietf.org/html/rfc6750#section-2.1)
|
||||||
|
|
||||||
## Verifying the token
|
|
||||||
|
|
||||||
The registry must now verify the token presented by the user by inspecting the
|
|
||||||
claim set within. The registry will:
|
|
||||||
|
|
||||||
- Ensure that the issuer (`iss` claim) is an authority it trusts.
|
|
||||||
- Ensure that the registry identifies as the audience (`aud` claim).
|
|
||||||
- Check that the current time is between the `nbf` and `exp` claim times.
|
|
||||||
- If enforcing single-use tokens, check that the JWT ID (`jti` claim) value has
|
|
||||||
not been seen before.
|
|
||||||
- To enforce this, the registry may keep a record of `jti`s it has seen for
|
|
||||||
up to the `exp` time of the token to prevent token replays.
|
|
||||||
- Check the `access` claim value and use the identified resources and the list
|
|
||||||
of actions authorized to determine whether the token grants the required
|
|
||||||
level of access for the operation the client is attempting to perform.
|
|
||||||
- Verify that the signature of the token is valid.
|
|
||||||
|
|
||||||
If any of these requirements are not met, the registry will return a
|
|
||||||
`403 Forbidden` response to indicate that the token is invalid.
|
|
||||||
|
|
||||||
**Note**: it is only at this point in the workflow that an authorization error
|
|
||||||
may occur. The token server should *not* return errors when the user does not
|
|
||||||
have the requested authorization. Instead, the returned token should indicate
|
|
||||||
whatever of the requested scope the client does have (the intersection of
|
|
||||||
requested and granted access). If the token does not supply proper
|
|
||||||
authorization then the registry will return the appropriate error.
|
|
||||||
|
|
||||||
At no point in this process should the registry need to call back to the
|
|
||||||
authorization server. The registry only needs to be supplied with the trusted
|
|
||||||
public keys to verify the token signatures.
|
|
||||||
|
|
Loading…
Reference in a new issue