Need help with nginx as authentication for private docker repository

Not sure if the category is correct

Basically i want to use my API as authentication for docker login remote

here is my docker compose:

version: '3'

services:
  registry:
    restart: always
    image: registry:2
    ports:
    - "5000:5000"
    environment:
      REGISTRY_STORAGE_FILESYSTEM_ROOTDIRECTORY: /data
      REGISTRY_HTTP_ADDR: 127.0.0.1:5000
#      REGISTRY_HTTP_TLS_CERTIFICATE: /etc/letsencrypt/live/dockerhub.thechemicalworkshop.com/fullchain.pem
#      REGISTRY_HTTP_TLS_KEY: /etc/letsencrypt/live/dockerhub.thechemicalworkshop.com/privkey.pem
    volumes:
      - ./data:/data
    network_mode: host

as you can see im hosting this on localhost port 5000

then im using nginx as reverse proxy, ssl and some other stuff

server {
    client_max_body_size 2000m;
    proxy_pass_request_headers      on;
    server_name dockerhub.thechemicalworkshop.com;
    auth_request /auth;

    location / {
    # 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;
    }

    proxy_pass                          http://127.0.0.1:5000;
    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_read_timeout                  900;
    proxy_pass_request_headers      on;
    }

        location = /auth {
                proxy_pass_request_headers      on;
                proxy_pass http://localhost:8080/api/authorize_docker;
                proxy_pass_header token;
                proxy_set_header Authorization $http_authorization;
                proxy_pass_header  Authorization;
                proxy_set_header X-Real-IP $remote_addr;
                proxy_set_header  X-Forwarded-Proto $scheme;
}

listen [::]:443 ssl; # managed by Certbot
    listen 443 ssl; # managed by Certbot
    ssl_certificate /etc/letsencrypt/live/dockerhub.thechemicalworkshop.com/fullchain.pem; # managed by Certbot
    ssl_certificate_key /etc/letsencrypt/live/dockerhub.thechemicalworkshop.com/privkey.pem; # managed by Certbot
    include /etc/letsencrypt/options-ssl-nginx.conf; # managed by Certbot
    ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem; # managed by Certbot
}

i hope this config is correct, basically im passing bunch of stuff to http://localhost:8080/api/authorize_docker which is running this script:

@app.route("/api/authorize_docker")
async def authorize_docker():
    data = await request.data
    data2 = await request.get_data()
    js = await request.json
    js2 = await request.get_json()
    form = await request.form
    files = await request.files
    values = await request.values
    # stream = await request.stream
    print(request.authorization)
    print(await request.get_data())
    print(request.headers)
    if matti_check(request.headers["X-Real-Ip"]):
        return "Welcome_Matti"
    else:
        print(request.headers)
        return "ERROR", 500

ignore the print statements, they are for debugging
basically this is a very simple IP whitelist (which works)
i’d also like to impement oauth or whatever docker login uses
inside my api so i can use combination of login/password, ip but also generally have better control over my docker repository as every request will go thru the API

here is my problem:

I dont see the login/password anywhere ?

here is what i run:

Powershell Windows 10
docker login https://dockerhub.thechemicalworkshop.com/
Username: user
Password:
Login Succeeded

username and password can be anything, will still pass because it’s comming from my IP (see python code above)

i wiresharked my pc (no user/pass found)
python logs, also no user/pass found

python logs:

4|TCW-API  | None
4|TCW-API  | b''
4|TCW-API  | Remote-Addr: 127.0.0.1
4|TCW-API  | X-Real-Ip: REDACTED
4|TCW-API  | X-Forwarded-Proto: https
4|TCW-API  | Host: localhost:8080
4|TCW-API  | Connection: close
4|TCW-API  | User-Agent: docker/20.10.14 go/go1.16.15 git-commit/87a90dc kernel/5.10.104-linuxkit os/linux arch/amd64 UpstreamClient(Docker-Client/20.10.14 \(windows\))
4|TCW-API  | Accept-Encoding: gzip
4|TCW-API  |
4|TCW-API  | [2022-04-29 22:28:14,748] 127.0.0.1:39722 GET /api/authorize_docker 1.0 200 13 32601
4|TCW-API  | None
4|TCW-API  | b''
4|TCW-API  | Remote-Addr: 127.0.0.1
4|TCW-API  | X-Real-Ip: REDACTED
4|TCW-API  | X-Forwarded-Proto: https
4|TCW-API  | Host: localhost:8080
4|TCW-API  | Connection: close
4|TCW-API  | User-Agent: docker/20.10.14 go/go1.16.15 git-commit/87a90dc kernel/5.10.104-linuxkit os/linux arch/amd64 UpstreamClient(Docker-Client/20.10.14 \(windows\))
4|TCW-API  | Accept-Encoding: gzip
4|TCW-API  |
4|TCW-API  | [2022-04-29 22:28:15,865] 127.0.0.1:39730 GET /api/authorize_docker 1.0 200 13 21546

why is it requesting twice tho?

How do i fetch the login/password inside my script so i can well, authenticate user (or not)?

how does EXACTLY docker login adress work, i’d like a full explanation on authentication

Thanks !

Okay i spent few hours tring different things…
looking at this:
Authorization for Private Docker Registry | by Thilina Manamgoda | Medium (disclaimer, i dont know go)
Token Authentication Specification | Docker Documentation

i wrote this:
same nginx config

python config:

@app.route("/api/authorize_docker")
async def authorize_docker():

    print(request.headers)
    print(request.authorization)
    if matti_check(request.headers["X-Real-Ip"]):
        response = await make_response('{"token": "b20iLCJzdWISwiJ5IzdCr9t3w", "expires_in": 360000,"issued_at": "2022-04-30T00:33:00Z"}')

        return response
    else:
        print(request.headers)
        return "ERROR", 500

compose config:

version: '3'

services:
  registry:
    restart: always
    image: registry:2
    ports:
    - "5000:5000"
    environment:
      REGISTRY_STORAGE_FILESYSTEM_ROOTDIRECTORY: /data
      REGISTRY_HTTP_ADDR: 127.0.0.1:5000
      REGISTRY_AUTH: token
      REGISTRY_AUTH_TOKEN_REALM: http://api.thechemicalworkshop.com/api/authorize_docker
      REGISTRY_AUTH_TOKEN_ISSUER: "Auth Service"
      REGISTRY_AUTH_TOKEN_SERVICE: "Docker registry"
      REGISTRY_AUTH_TOKEN_ROOTCERTBUNDLE: /ssl/fullchain.pem
    volumes:
      - ./data:/data
      - ./ssl:/ssl
    network_mode: host
Windows 10 powershell

docker login https://dockerhub.thechemicalworkshop.com/
Username: sdadsdsa
Password:
Error response from daemon: login attempt to https://dockerhub.thechemicalworkshop.com/v2/ failed with status: 401 Unauthorized

python logs:

4|TCW-API  | None
4|TCW-API  | Remote-Addr: 127.0.0.1
4|TCW-API  | X-Real-Ip: REDACTED
4|TCW-API  | X-Forwarded-Proto: https
4|TCW-API  | Host: localhost:8080
4|TCW-API  | Connection: close
4|TCW-API  | User-Agent: docker/20.10.14 go/go1.16.15 git-commit/87a90dc kernel/5.10.104-linuxkit os/linux arch/amd64 UpstreamClient(Docker-Client/20.10.14 \(windows\))
4|TCW-API  | Accept-Encoding: gzip
4|TCW-API  |
4|TCW-API  | [2022-04-30 02:03:39,950] 127.0.0.1:58032 GET /api/authorize_docker 1.0 200 96 21987
4|TCW-API  | {'username': 'sdajnkdjnksanjk', 'password': 'asdkmasmkdkmlas'}
4|TCW-API  | Remote-Addr: 127.0.0.1
4|TCW-API  | X-Real-Ip: REDACTED
4|TCW-API  | Host: localhost:8080
4|TCW-API  | Connection: close
4|TCW-API  | User-Agent: docker/20.10.14 go/go1.16.15 git-commit/87a90dc kernel/5.10.104-linuxkit os/linux arch/amd64 UpstreamClient(Docker-Client/20.10.14 \(windows\))
4|TCW-API  | Authorization: Basic c2Rham5rZGpua3NhbmprOmFzZGttYXNta2RrbWxhcw==
4|TCW-API  | Accept-Encoding: gzip
4|TCW-API  |
4|TCW-API  | [2022-04-30 02:03:40,045] 127.0.0.1:58038 GET /api/authorize_docker 1.0 200 96 25505
4|TCW-API  | None
4|TCW-API  | Remote-Addr: 127.0.0.1
4|TCW-API  | Authorization: Bearer b20iLCJzdWISwiJ5IzdCr9t3w
4|TCW-API  | X-Real-Ip: REDACTED
4|TCW-API  | X-Forwarded-Proto: https
4|TCW-API  | Host: localhost:8080
4|TCW-API  | Connection: close
4|TCW-API  | User-Agent: docker/20.10.14 go/go1.16.15 git-commit/87a90dc kernel/5.10.104-linuxkit os/linux arch/amd64 UpstreamClient(Docker-Client/20.10.14 \(windows\))
4|TCW-API  | Accept-Encoding: gzip
4|TCW-API  |
4|TCW-API  | [2022-04-30 02:03:40,185] 127.0.0.1:58040 GET /api/authorize_docker 1.0 200 96 8726

im pretty sure im double routing this, but it gives me finally login and password !

i dont like this path, but also don’t super hate it
as i wanted ngnix to handle this all

any chance we can get the path above working without this idk what

      REGISTRY_AUTH: token
      REGISTRY_AUTH_TOKEN_REALM: http://api.thechemicalworkshop.com/api/authorize_docker
      REGISTRY_AUTH_TOKEN_ISSUER: "Auth Service"
      REGISTRY_AUTH_TOKEN_SERVICE: "Docker registry"
      REGISTRY_AUTH_TOKEN_ROOTCERTBUNDLE: /ssl/fullchain.pem

also my question is, what do i have to return to client after i return the token?

it gives me user and pass
i generate token and give it to them
it gives me token and i do what? give 200? that didn’t work looks like it

Okay figured more stuff out by looking at docker compose logs

the token i pass must be valid

going to https://oauth.tools/ and loading eyJ0eXAiOiJKV1QiLCJhbGciOiJFUzI1NiIsImtpZCI6IlBZWU86VEVXVTpWN0pIOjI2SlY6QVFUWjpMSkMzOlNYVko6WEdIQTozNEYyOjJMQVE6WlJNSzpaN1E2In0.eyJpc3MiOiJhdXRoLmRvY2tlci5jb20iLCJzdWIiOiJqbGhhd24iLCJhdWQiOiJyZWdpc3RyeS5kb2NrZXIuY29tIiwiZXhwIjoxNDE1Mzg3MzE1LCJuYmYiOjE0MTUzODcwMTUsImlhdCI6MTQxNTM4NzAxNSwianRpIjoidFlKQ08xYzZjbnl5N2tBbjBjN3JLUGdiVjFIMWJGd3MiLCJhY2Nlc3MiOlt7InR5cGUiOiJyZXBvc2l0b3J5IiwibmFtZSI6InNhbWFsYmEvbXktYXBwIiwiYWN0aW9ucyI6WyJwdXNoIl19XX0.QhflHPfbd6eVF4lM9bwYpFZIV0PfikbyXuLx959ykRTBpe3CYnzs6YBK8FToVb5R47920PVLrh8zuLzdCr9t3w

i can see some fields

however i am not sure how to create a new key from scratch + attach the signature (could figure that out but
on Token Authentication Specification | Docker Documentation i don’t really see the full specification and what fields are needed and what type of signature…)

Any Chance i could get help on that or correct specificaton/format/however you make these keys ?

okay i found some specifications

im using same api/python script

raw_token = 'eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiIsImtpZCI6Ik1ZNFQ6S05SVTpHSlNXOkVOUlU6R0ZSVzpDWVpXOkdOUlc6Q01MQzpHVlRHOktNM0Q6TVJTVzpDWlREIn0.eyJpc3MiOiJUaGUgQ2hlbWljYWwgV29ya3Nob3AiLCJhdWQiOiJkb2NrZXJodWIudGhlY2hlbWljYWx3b3Jrc2hvcC5jb20iLCJleHAiOjE2ODc5MjczMjAwMDB9.wHUtXjyROxSJ4eXlT4VpCaMFczCYnmwqHXpc_G9aAJzTCfhaXTP7ORkWGGWgilDhF4yFOZHSCEhPxWvG7cosCEqeQA1VTX3qVJGEXITvjqkV_QO-oYWlg6VeWib5ex7C8GVCpquOVlnD_bqJ-H_mxFBmoSEtdkBV2_AcXaZYopHVXsGnuWurAPzeNOs0cc2k2Mt9LaveBrQVrrKRSma-5mtoyjfu_HaSqYz6Zv8G1zxN7PZ064nq1XKp1LYt6dF4ShcxQRVMx69H6iefc0-h4ZSRIu_sWSChLr96tYJIestfwHsi_83A-YUcWhYQGQQAfRHuchTJ7BoybmzRzhJhHg'

    if matti_check(request.headers["X-Real-Ip"]):
        payload = {"expires_in": 360000,"issued_at": "2022-04-30T15:33:00Z"}

        payload['token'] = raw_token

        response = await make_response(payload)

        return response
    else:
        print(request.headers)
        return "ERROR", 500

currently using this script to make the token

import jwt

private_key = b'''-----BEGIN PRIVATE KEY-----
MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQDOlBy6Ru+8d5mO
-----END PRIVATE KEY-----'''

public_key = b'''-----BEGIN CERTIFICATE-----
MIIDbTCCAlUCFEAIdgSu+9F+y39e4uqNZLBX02ThMA0GCSqGSIb3DQEBCwUAMHMx
-----END CERTIFICATE-----'''

encoded = jwt.encode(\
    {\
        "iss": "The Chemical Workshop", \
        "aud": "dockerhub.thechemicalworkshop.com",\
        "exp": 1687927320000\
        }, \
        private_key, algorithm="RS256", headers={"kid": "MY4T:KNRU:GJSW:ENRU:GFRW:CYZW:GNRW:CMLC:GVTG:KM3D:MRSW:CZTD"})
print(encoded)

I removed most of the key for space reasons
looking at here Token authentication error: token signed by untrusted key with ID: "xyz" · Issue #1143 · distribution/distribution · GitHub
i used openssl x509 -in domain.crt -noout -outform der -pubkey
and got

-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAzpQcukbvvHeZjpsRmR1T
u27d/Ep1DYvqLwfVsfoxXIU5lUrQ53J3K89Z/hrb8J0hF0ewaaqfzLE5khBY7E/v
10bqiAr9oNTu+3bKM+z9YRaJ4mh1atE3IFsW7uYBAJ8KYykxzUsJ8nJHyqFbJ0nH
pvdlRgoD/+CTn6uIGIrhMx8kgDbXGwX+t/OhsG87jVwfsr5Sa7lVilnMR6HJ/5F8
rAJ/O8sGfBGlbL62qUdbnLxrwxANiyA1p9F3UEoo2GhA9rkD0oileUVeYtVJ9zIu
NARfrProR7X6i8ZSCjF9FyhasyEl/rgNv69CPEFj1HCVYb/CHkCVj1L3Zi1v8K1K
PwIDAQAB
-----END PUBLIC KEY-----

and this is where am i lost looking at these instructions:

    Take the DER encoded public key which the JWT token was signed against.

    Create a SHA256 hash out of it and truncate to 240bits.

    Split the result into 12 base32 encoded groups with : as delimiter.

source Token Authentication Implementation | Docker Documentation

so to make a hash out of it, do i take the whole token (including -----BEGIN PUBLIC KEY-----) or just the content?

well i get either

hash content 
f95642eb641cac63ca1b5fe3cdeafcf9ecb576f7f60c321aa8583e1d86d4cb7f
or
hash everything
75e190d3c15134507fbb352fd4d87301248544a418caf09bd8176c401dc55071

later (what i understood) im supposed to take first 30 characters (please fact check)
and encode it into base 32 then place : every 4 characters
which gives me (i tried both)

MY4T:KNRU:GJSW:ENRU:GFRW:CYZW:GNRW:CMLC:GVTG:KM3D:MRSW:CZTD
or
G42W:KMJZ:GBSD:GYZR:GUYT:GNBV:GA3W:MYTC:GM2T:EZTE:GRSD:QNZT

docker compose gives me this error:
docker-registry-registry-1 | time="2022-04-30T17:10:56.438672709Z" level=info msg="token signed by untrusted key with ID: "MY4T:KNRU:GJSW:ENRU:GFRW:CYZW:GNRW:CMLC:GVTG:KM3D:MRSW:CZTD""

basically im stuck again, i’d prefer to use username and password alongside ip using nginx (first approach)

because this (second approach) looks way more complicated, plus does not wanna work lol but i dont mind it, aslong as it finally works

Any ideas/help?

solved it, I’ll make a writeup in few days and post a link here

Have you ever considered using alternatives that already come with embedded authentification/authorization?

I can highly recommend to use Harbor and JFrog Container Registry as registry. Both are well maintained and bring all kinds of bells and whistles to the table. Nexus3 also has an embedded container registry and Gitlab provides easy integration of the docker registry and extend it’s authentification/authorization to it.

1 Like

i actually have

the problem is simple, i already have an api running 24/7 and i already have functions written for the api that define me (IP/DNS, user/pass) and can also simply write stuff like notifications or logs or whatever i might want tbh

even tho python is very slow, my api can still handle 200 requests+ per second, so im not worried about speed, as for size/usage, well it’s already running anyways so it’s kinda free lol

also, current difficultry i have is that running docker login followed by docker push/pull still requests a token, the token i create is valid for like a long time so not sure why… my REGISTRY_AUTH_TOKEN_REALM never actually gets sent the token, just scopes/login and password
so i basically generate a new token every request (bit strange but i dont mind)

whoever needs KID or full token generated, here is the info

1 Like