Deploying Docker Registry with Granular Access Control, TLS & S3

Deploying Docker Registry with Granular Access Control, TLS & S3
Photo by Richard Sagredo / Unsplash

Deploying a self-hosted Docker registry might seem like a simple task, and there are many guides available on how to do it. However, most of these guides cover only the basics. One of the biggest limitations I encountered was the inability to configure different access rights for each user.

This guide demonstrates a production-ready setup using cesanta/docker_auth for advanced access management, ensuring both security and flexible permission configurations.

Key Advantages of This Setup

  1. Centralized authentication with JWT token validation - All users authenticate via a central service, ensuring consistent access management.
  2. Fine-grained permissions per user/group/image repository - Each user or team can be assigned specific permissions for different repositories.
  3. Automatic HTTPS via Caddy reverse proxy - Secure communications with automated TLS certificates from Let's Encrypt.
  4. Audit logging of all registry operations - Keeps track of all actions performed on the registry.
  5. Role-based access control (RBAC) for teams - Allows defining roles such as "admin", "developer", etc.
  6. Secure TLS communication with auto-renewing certificates - Ensures encrypted communication without manual certificate management.

How to Set It Up?

First, we create a docker-compose.yml file to define all necessary services:

services:
  auth:
    image: cesanta/docker_auth:1
    restart: always
    volumes:
      - ./conf/auth-server.yml:/config/auth_config.yml
      - ./conf/keys:/certs

  registry:
    image: registry:2
    restart: always
    environment:
      # Where to listen
      REGISTRY_HTTP_ADDR: 0.0.0.0:5000
      REGISTRY_HTTP_SECRET: somerandomstringhere
      # Registry auth
      REGISTRY_AUTH: token
      REGISTRY_AUTH_TOKEN_REALM: https://${AUTH_DOMAIN}/auth
      REGISTRY_AUTH_TOKEN_SERVICE: "Docker registry"
      REGISTRY_AUTH_TOKEN_ISSUER: "Acme auth server" # this must match the issuer name in the next config file.
      REGISTRY_AUTH_TOKEN_ROOTCERTBUNDLE: /certs/certificate.pem
      # S3 Storage configuration
      REGISTRY_STORAGE: s3
      REGISTRY_STORAGE_S3_ACCESSKEY: accesskey
      REGISTRY_STORAGE_S3_SECRETKEY: secretkey
      REGISTRY_STORAGE_S3_BUCKET: bucketname
      REGISTRY_STORAGE_S3_REGION: eu-west-1
    
    volumes:
      - ./conf/keys/server.pem:/certs/certificate.pem

  caddy:
    ports:
      - 80:80
      - 443:443
    image: caddy:2
    environment:
      AUTH_DOMAIN: ${AUTH_DOMAIN}
      REGISTRY_DOMAIN: ${REGISTRY_DOMAIN}
    restart: always
    volumes:
      - ./conf/Caddyfile:/etc/caddy/Caddyfile

This setup includes three core services:

  • auth: Handles user authentication and permissions.
  • registry: The actual Docker registry.
  • caddy: A reverse proxy that provides HTTPS automatically.

Authentication Configuration

The auth service requires a configuration file (conf/auth-server.yml):

server:
  addr: ":5001"

token:
  issuer: "Acme auth server" # Must match issuer in the Registry config.
  expiration: 900
  key: "/certs/server.key"
  certificate: "/certs/server.pem"

users:
  # Password is specified as a BCrypt hash. Use `htpasswd -nB USERNAME` to generate.
  "username1":
    password: "$2y$05$YSouiTqTc36VcV80P8lKXOMGGQRnzoqjQLKg8oar7ZJ0EFil2DtxC" # testing
  "username2":
    password: "$2y$05$YSouiTqTc36VcV80P8lKXOMGGQRnzoqjQLKg8oar7ZJ0EFil2DtxC" # testing
  # example with labels  
  "admin":
    password: "$2y$05$YSouiTqTc36VcV80P8lKXOMGGQRnzoqjQLKg8oar7ZJ0EFil2DtxC" # testing
    labels:
      role: ["administrator"]
  "developer":
    password: "$2y$05$YSouiTqTc36VcV80P8lKXOMGGQRnzoqjQLKg8oar7ZJ0EFil2DtxC" # testing
    labels:
      team: ["backend"]
      role: ["developer"]
  "": {} # Allow anonymous (no "docker login") access.

# Access is denied by default.
acl:
  - match: { account: "username1" }
    actions: ["*"]
    comment: "username1 has full access to everything."

  - match: { account: "username2" }
    actions: ["*"]
    comment: "username2 has full access to everything."

  - match: {labels: {"role": "administrator"}}
    actions: ["*"]
    comment: "Full registry access"

  - match:
      labels: {"role": "developer"}
      name: "dev-${labels:team}/*"
    actions: ["push", "pull"]
    comment: "Team-specific write access"
  
  - match: { account: "" }
    actions: ["pull"]
    comment: "Anonymous users can pull everything."

This is just a basic config file to showcase some of the features, with static users, but Github / Google / LDAP and other auth backends are possible.

More about options supported on this config file can be found in reference config file.

Generate SSL Cert for JWT encryption

Before starting the services, generate an SSL certificate which is used for JWT encryption. The server.pem needs to be mounted into both auth-server and registry, and the server.key (private key) needs to be mounted into auth-server

mkdir -p conf/keys
openssl req -newkey rsa:4096 -nodes -sha256 \  
  -keyout conf/keys/server.key \  
  -x509 -days 3650 -out conf/keys/server.pem \  
  -subj "/CN=docker-registry-auth-server-jwt-signer"

Caddy Reverse Proxy Configuration

For HTTPS with a certificates managed by LetsEncrypt - I've picked Caddy, as that is most simple way to get it working. Also, Caddyfile allows to use environment variables, so it's easy to adapt to your needs without ever touching the Caddyfile itself.

In the past, I had TLS setup handled by Traefik, or Nginx Ingress Controller & Cert-manager on K8S, but for this tutorial - Caddy is simply the easiest solution.

# conf/Caddyfile
{$AUTH_DOMAIN} {
    reverse_proxy auth:5001
}

{$REGISTRY_DOMAIN} {
    reverse_proxy registry:5000
}

Setting Up Environment Variables

Create an .env file with domain names. These domain names would be used in the Caddy configuration to handle the TLS.

# .env
AUTH_DOMAIN=auth.yourdomain.com
REGISTRY_DOMAIN=registry.yourdomain.com

Running the Setup

Run the following command:

docker compose up -d

Caddy will automatically manage TLS certificates, and the authentication service will control user access to the registry.

Usage Examples

Push Image with Admin Rights

docker login registry.yourdomain.com -u admin
docker tag nginx registry.yourdomain.com/library/nginx:latest
docker push registry.yourdomain.com/library/nginx:latest

Pull Image as Developer

docker login registry.yourdomain.com -u developer
docker pull registry.yourdomain.com/library/nginx:latest

Comparison with Basic htpasswd Setup

FeatureToken Auth SetupBasic htpasswd
Granular Permissions✅ Per-repo policies❌ All-or-nothing
Audit Logging✅ Detailed operations❌ Limited visibility
Token Expiration✅ Configurable TTL❌ Permanent sessions
External Auth Integration✅ LDAP/OAuth support❌ Local users only

This setup provides robust security and flexibility for teams managing container workloads. The complete code is available on GitHub. Additionally, the repository includes support for Minio as an alternative to S3.

xor22h/docker-registry-setup
Contribute to xor22h/docker-registry-setup development by creating an account on GitHub.