Skip to content

A simple token authentication service for private docker registries written in python

License

Notifications You must be signed in to change notification settings

SushiTee/docker-registry-token-auth-service

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

8 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Docker registry token authentication service

This is a simple token authentication service for a private Docker registry written in python. It is based on the Distribution Registry v2 Bearer token specification.

It is supposed to be run behind a proxy such as nginx which handles SSL. Later in this document, I will provide an example configuration for nginx.

This little service has the following capabilities:

  • generate JWT tokens for the Docker registry
  • restrict access to the registry to authenticated users
  • select whether a user can push or pull images by a simple json configuration file (config/users.json)
  • whitelist/blacklist repositories for public access (pull access only) by a simple json configuration file (config/repositories.json)

Configuration

The service is supposed to be running in combination with a Docker registry. Therefore the following requirements are necessary:

  • Docker including docker compose
  • A reverse proxy which handles SSL and forwards requests to the registry and the token service

In the next chapters example files will be created which are required to successfully run the service. This includes the needed certificates, the configuration files and the docker-compose.yml file.

Generate certificates

As JWT tokens are used for authentication, the service needs a private key and a certificate. The following commands generate a new ECDSA key and a self-signed certificate:

mkdir certs
openssl ecparam -genkey -name prime256v1 -noout -out certs/RootCA.key
openssl req -x509 -nodes -new -sha256 -days 1024 -key certs/RootCA.key -out certs/RootCA.crt -subj "/CN=localhost/O=company/C=US"

Create configuration files

The subdirectory config contains example configuration files. Copy and rename them with the same name but without the .example suffix:

cp config/config.py.example config/config.py
cp config/users.json.example config/users.json
cp config/repositories.json.example config/repositories.json
cp config/uwsgi.ini.example config/uwsgi.ini

Edit those files accordingly. If you set backlist to true in repositories.json, the repositories listed in the repositories array will not be accessible without authentication. If you set blacklist to false, it will work as a whitelist and only the repositories listed in the repositories array will be accessible without authentication.

You cannot give only push access. If you want to give users push access you always have to put pull as well.

The passwords for the users in users.json are hashed with bcrypt and can be generated with the following command:

python3 -c 'import bcrypt; print(bcrypt.hashpw(b"password", bcrypt.gensalt()).decode("utf-8"))'

Alternatively, you can use the htpasswd command from the httpd docker image if you don't want to use python:

docker run --rm -it httpd htpasswd -nbB username password

This service only supports bcrypt hashed passwords!

In case you wonder: The passwords in the example are foo and bar.

Prepare communication through a socket

In this example the token service and the registry are using unix sockets to communicate:

  • the token service listens on /var/www/upstream-sockets/registry-auth-service.sock
  • the registry listens on /var/www/upstream-sockets/docker-registry.sock

Since the reverse proxy is accessing the registry and the token service through the sockets, the user and group of the sockets must be the same as the user and group of the reverse proxy. In this example, the user and group are www-data. Create the directory and set the permissions:

mkdir -p /var/www/upstream-sockets
chown -R www-data:www-data /var/www/upstream-sockets

Since in my example the reverse proxy is not running inside a container, the directory is created in a place where it actually can be accessed by the reverse proxy.

The services create the socket files when they are started. This means that the services must run using the correct user. For the token service, the user and group are set in the uwsgi.ini file. For the registry, the user and group are set in the docker-compose.yml file which is described in the next chapter.

Both services use certificates to sign and verify the tokens. Those certificates must be accessible by the services as well. So change the permissions of the certificates:

chown -R www-data:www-data certs

Create the docker-compose file

The docker-compose.yml file starts the token service and the registry. Here is an example fitting the previous configuration:

version: '3'

services:
  registry:
    image: registry:2
    user: "33:33" # set the user to the same as used by the reverse proxy
    environment:
      REGISTRY_AUTH: token
      REGISTRY_AUTH_TOKEN_REALM: https://example.org/v2/token
      REGISTRY_AUTH_TOKEN_SERVICE: Authentication
      REGISTRY_AUTH_TOKEN_ISSUER: example issuer # match the issuer in the config file
      REGISTRY_AUTH_TOKEN_ROOTCERTBUNDLE: /mnt/local/certs/RootCA.crt
      REGISTRY_HTTP_SECRET: iru7cBDFI4CgqTmjz4n0Z+YkQHQOAxEX # generate your own (eg. with 'openssl rand -base64 24')
      REGISTRY_HTTP_ADDR: /mnt/local/socket/docker-registry.sock
      REGISTRY_HTTP_NET: unix
      REGISTRY_STORAGE_FILESYSTEM_ROOTDIRECTORY: /registry-data
    volumes:
      - ./certs/RootCA.crt:/mnt/local/certs/RootCA.crt
      - /var/www/upstream-sockets:/mnt/local/socket
      - ./registry-data:/registry-data
    restart: always

  auth-server:
    build:
      context: .
    volumes:
      - /var/www/upstream-sockets:/app/socket  # Mount socket from host to container
      - ./config:/app/config  # Mount config files from host to container
      - ./certs/RootCA.key:/app/certs/RootCA.key  # Mount RootCA from host to container
    restart: always

The token service expects the config files to be in the config directory of the service. The path to the certificate is set within the config.py file.

Reverse proxy configuration

You can of course use any reverse proxy you like. In this example, nginx is used as a reverse proxy to handle SSL and forward requests to the registry and the token service. Here is the relevant part of the configuration:

upstream docker-registry {
    server unix:///var/www/upstream-sockets/docker-registry.sock;
}

upstream docker-auth {
    server unix:///var/www/upstream-sockets/registry-auth-service.sock;
}

map $upstream_http_docker_distribution_api_version $docker_distribution_api_version {
  '' 'registry/2.0';
}

server {
    // general stuff
    listen 443 ssl http2;
    listen [::]:443 ssl http2;

    server_name example.org;

    // SSL stuff
    ssl_certificate /path/to/fullchain.pem;
    ssl_certificate_key /path/to/privkey.pem;
    ssl_dhparam /path/to/ssl-dhparams.pem;

    location /v2/ {
        # disable any limits to avoid HTTP 413 for large image uploads
        client_max_body_size 0;

        # required to avoid HTTP 411: see Issue #1486 (https://github.com/moby/moby/issues/1486)
        chunked_transfer_encoding on;

        # Do not allow connections from docker 1.5 and earlier
        # docker pre-1.6.0 did not properly set the user agent on ping, catch "Go *" user agents
        if ($http_user_agent ~ "^(docker\/1\.(3|4|5(?!\.[0-9]-dev))|Go ).*$" ) {
          return 404;
        }

        ## If $docker_distribution_api_version is empty, the header is not added.
        ## See the map directive above where this variable is defined.
        add_header 'Docker-Distribution-Api-Version' $docker_distribution_api_version always;

        proxy_pass                          http://docker-registry;
        proxy_set_header  Host              $http_host;   # required for docker client's sake
        proxy_set_header  X-Real-IP         $remote_addr; # pass on real client's IP
        proxy_set_header  X-Forwarded-For   $proxy_add_x_forwarded_for;
        proxy_set_header  X-Forwarded-Proto $scheme;
        proxy_set_header  Authorization     $http_authorization;
        proxy_read_timeout                  900;
    }

    location /v2/token {
        uwsgi_pass docker-auth;
        include uwsgi_params;
    }
}

It is important that you have two upstreams, one for the registry and one for the token service. They must match the environment variables of the docker-compose.yml and the configuration of the token service.

Usage

After the configuration is done, the service can be started with the following command:

docker-compose up -d

Docker login, pull and push

The docker registry can be accessed like any other registry. Here are some examples:

Login:

docker login -u bar -p bar https://example.org

Create an example image:

docker pull alpine
docker tag alpine example.org/some-image:latest
docker push example.org/some-image:latest

Pull the image:

docker pull example.org/some-image:latest

Registry API

You can use the registry API to get information about the available repositories as well. Here is an example:

TOKEN=$(curl -s -X GET https://example.org/v2/token\?account\=anon\&service\=Authentication\&scope\=registry:catalog:\* | jq -r '.token')
curl -H "Authorization: Bearer $TOKEN" https://example.org/v2/_catalog

The output should look like this:

{"repositories":["some-image"]}

Now to list the tags of the repository... of course you need another token for another scope:

TOKEN=$(curl -s -X GET https://example.org/v2/token\?account\=anon\&service\=Authentication\&scope\=repository:some-image:pull | jq -r '.token')
curl -H "Authorization: Bearer $TOKEN" https://example.org/v2/some-image/tags/list

This will print:

{"name":"some-image","tags":["latest"]}

About

A simple token authentication service for private docker registries written in python

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published