Please take a few minutes to complete this short survey on service testing.

Implementing Authentication and Access Control to Docker Registry

  • 2017-02-12

If you are running Docker as part of your infrastructure you probably are also hosting a private Docker registry for storing private Docker images. Vanilla installation is pretty good, you just put the Docker Distribution in a private VPC and you are good to go. Let's imagine a scenario where you wanted to build a public registry with custom access control to the images, something similar to Docker Hub. How would you do that? Good news is that I built exactly that when I was building Layerstore and in this article I'm going to show you how you can do it yourself.

Before we go into nitty-gritty details let me give you some background on Layerstore. Layerstore was Docker marketplace where anyone could sell Docker images either as individual images or as image bundles. The entire life cycle of a sale might look something similar to this:

  1. Seller reserves image identifier. This identifier will be used to push and pull images from the registry.
  2. Seller receives read and write permissions to the reserved image identifier.
  3. Seller uploads the image with docker push command, configures product page and sets the price.
  4. Purchaser buys the product and receives read access to the image.
  5. Purchaser downloads image onto his servers with docker pull.

We are going to explore these steps in detail in a moment. Of course I am going to skip irrelevant product parts and concentrate mostly on Docker registry and services surrounding it.

Understanding Authentication in Docker

Docker Registry v2 uses token-based authentication for establishing push and pull sessions. It's described in more detail in the official documentation:

In summary, this is what's happening behind the scenes:

  1. Client attempts to push/pull from the registry.
  2. If authorization headers are missing, the registry redirects client to the authorization service.
  3. Authorization service issues authorization token.
  4. Client retries request to the registry with the authorization token.

The registry itself never talks to any other services in order to verify the request. Each registry service instance is capable of verifying request signature and deny access on bad requests on its own. All communication between registry must occur over HTTPS/TLS. Since registry never talks to other services, you must expose two services to the public, the registry and the authorization service, for Docker client to work.

The Layerstore Architecture

Layerstore architecture for storing Docker images

When it comes to Layerstore, the architecture of the system is fairly simple. It is composed of these five core components:

  • Docker Registry
  • Web Service
  • Authentication Service
  • ACL Service
  • Docker Authentication Service

You can see how they interact with each other in the diagram above. In order to implement custom authentication you will be required to write custom logic for managing users/accounts, performing authentication and managing ACL. The Docker does not provide any out of the box tooling for this.

Let's take a look at all of these components separately.

Docker Registry

Docker Registry is simply the Docker distribution service backed by S3. S3 is a fairly good choice if you can allow images being stored by a third-party. It guarantees redundancy and scales indefinitely.

There are only couple key pieces to configuring distribution correctly for token authentication. As you can see, the distribution service does not talk to any other service directly besides storage. This is a really nice design that allows to easily scale this service horizontally.

Web Service

Web Service is the user facing web interface. It's marked as Layerstore in the architecture diagram. This service is responsible for managing user accounts, product listings and Docker image metadata.

Authentication Service

This piece of the system is responsible for maintaining relationships between user accounts and API keys. In Layerstore you never authenticate with your password with Docker client. Each Layerstore user can have as many API keys as they want and they can also reject them at any time. When you run docker login you are using your API keys instead of your password.

ACL Service

Who can access which Docker images is controlled by the ACL service. When a seller decides to put an image for sale, he will be granted read/write access to this image. When someone purchases an image, Layerstore service will create a new record in the ACL database granting read-only access to that image.

Docker Authentication Service

This service implements Docker authentication protocol for token exchange. We are going to go into a bit more detail in the section covering service configuration, but for now keep in mind that this is one of the most important services. You could implement only this service and ignore everything else. This service will receive user credentials from the Docker client for authentication and access request which must be signed with a private key by the service. This signature is later verified by the Docker registry with the public key. This is the reason why the registry can run independently.

Service Configuration and Implementation

Here's an excerpt from the Docker registry configuration file.

auth:
    token:
        realm: https://auth.layerstore.com:5001/auth
        service: "Docker registry"
        issuer: "Auth Service"
        rootcertbundle: /ssl/server.pem

It tells registry to use token authentication protocol, which keys to use for request validation, sets its identity and specifies where authorization service lives. Values for service and issuer are important. You can set them to anything you want but they will have to match values returned by the Docker Authentication Service.

In this example https://auth.layerstore.com:5001/auth points to the Docker Authentication Service. The /auth endpoint is going to receive an authentication request from the Docker client as described in Docker Registry v2 authentication via central service.

For example, if we're running docker pull registry.layerstore.com/image the request to auth service might look like this:

https://auth.layerstore.com:5001/auth?service=Docker%20registry&scope=repository:image:push,pull

Docker client will also pass credentials in HTTP authentication headers. The authorization service must verify these credentials. In the case of Layerstore this is done by checking that account and API keys are valid records in the database. Once the user is authenticated, you can check his access permissions. In our example we're requesting push and pull access to the image. Access records are stored in the ACL service and they are a simple mapping of account to repository and access type. It may look something similar to this table:

Account Repository Action
user1 image pull
user1 image2 push
user1 image2 pull

Let's say we are user1. Even though the user is requesting both push and pull actions, he will only be granted pull action to the image repository. It is up to the ACL service to intersect requested actions with the allowed actions. Once we have the intersection, we can create token that we can pass to the registry.

import (
    "encoding/base64"
    "encoding/json"
    "fmt"
    "github.com/docker/distribution/registry/auth/token"
    "github.com/docker/libtrust"
    "strings"
)

func joseBase64UrlEncode(b []byte) string {
    return strings.TrimRight(base64.URLEncoding.EncodeToString(b), "=")
}

func CreateToken(publicKey libtrust.PublicKey,
    privateKey libtrust.PrivateKey,
    resourceType string,
    resourceName string, actions []string) (string, error) {

    now := time.Now().Unix()

    header := token.Header{
        Type:       "JWT",
        SigningAlg: signingAlgorithm,
        KeyID:      publicKey.KeyID(),
    }
    headerJSON, _ := json.Marshal(header)

    claims := token.ClaimSet{
        Issuer:     "Auth Service",
        Subject:    account,
        Audience:   "Docker registry",
        NotBefore:  now - 1,
        IssuedAt:   now,
        Expiration: now + expiration,
        JWTID:      fmt.Sprintf("%d", rand.Int63()),
        Access:     []*token.ResourceActions{},
    }

    if len(actions) > 0 {
        claims.Access = []*token.ResourceActions{
            &token.ResourceActions{
                Type:    resourceType,
                Name:    resourceName,
                Actions: actions},
        }
    }

    claimsJSON, _ := json.Marshal(claims)

    payload := fmt.Sprintf("%s%s%s",
        joseBase64UrlEncode(headerJSON),
        token.TokenSeparator,
        joseBase64UrlEncode(claimsJSON))

    sig, _, _ := privateKey.Sign(strings.NewReader(payload), 0)
    return fmt.Sprintf("%s%s%s",
        payload,
        token.TokenSeparator,
        joseBase64UrlEncode(sig)), nil
}

The code above, sans error checking, can be used to generate the token for passing to the registry. You provide your public key pair, resource information and permitted actions. Resource type would be set to repository and the name to the image name. Docker registry will decode this token and verify its signature with the public key.

Bluebook - API Testing for Developers

API, end-to-end, and integration testing made simple.

Try Now

Subscribe

Subscribe to stay up to date with the latest content:

Hut for macOS

Design and prototype web APIs and services.

Download Now