The problem

I want to achieve following things:

  • I want to use AWS ECR as my repository,
  • the repositories must be private,
  • in the docker-compose.yml I want to refer to the docker images using custom domain - not hardcoding to the AWS ECR,
  • as long as I’m using AWS ECR as the repository, I want to use ECR Credentials Helper (no docker login),
  • I want to use AWS Cognito to control what users can fetch what image from the repository and to block access for the particular user if needed.

High Level solution

  1. Install AWS ECR Credentials Helper Login,
  2. Configure Docker to use custom wrapper over AWS ECR Credentials Helper Login script to allow to use custom domains,
  3. Utilize credentials_process property of Credentials Helper to invoke a script that will authorize the user against Cognito.

That’s how it more or less looks like in a visualized way:

d d s o o t c m c o k y k r e . e e r d r o c c m c r o a r e m i e d p n d e o . H n s c e t e o l i m p a u e l p r s d o A c W k S e r C o c g r n e i d t H o e l p e r A c W u S s t m E o y C m . R d c o C r m r e a e d i d e n e n . n t c t i o i a m a l l s = s _ > p H r 1 e o E 2 l c C 3 p e R 4 e s 5 r s C 6 u 7 s 8 t 9 o . m d k W r r . g a e e p c t p r e . f r r o e r g i 1 o A 2 n W 3 . S 4 a 5 m E 6 a C 7 z R 8 o 9 n C . a r d w e k s d r . e . c n e o t c m i r a . l r s e g H i e o l n p . e a r m a z o n a w s . c o m

Detailed steps

Install AWS ECR Credentials Helper Login

That’s the easiest part. We need to have the AWS ECR. On Ubuntu it’s just a matter of installing it from APT:

sudo apt install amazon-ecr-credential-helper

Use custom wrapper over AWS ECR Credentials Helper

AWS ECR Wrapper

The downloaded AWS ECR Credentials Helper will not work with your custom domain (e.g. docker.mydomain.com). It will fail if the URL is not the one from AWS ECR (e.g. 123456789012.dkr.ecr.eu-central-1.amazonaws.com).

That’s due to this part.

Solution is to use this awesome wrapper over ECR credentials helper authored by amencevice you can find here.

It will create a mapping of your custom domain to the ECR URL. That’s not perfect as I wanted to keep all pieces of information related to AWS ECR apart from the device where I run docker (and hide it behind custom domain; only proxy should be aware of it) but that’s much better than referencing AWS ECR directly in docker-compose.yml.

The mapping by default is put into ~/.ecr/custom.json and will be read by the wrapper. Exemplary content of the mapping is:

{
  "docker.mydomain.com": "123456789012.dkr.ecr.eu-central-1.amazonaws.com"
}

AWS ECR Wrapper cred helper configuration

Now we need to inform Docker to use this new wrapper. See the docs in linked amencevice GitHub account, but in short:

  • put the docker-credential-ecr-custom somewhere on $PATH
  • refer it from the ~/.docker/config.json credHelpers:
{
  "credHelpers": {
    "docker.mydomain.com": "ecr-custom"
  }
}

Note: the script named docker-credential-foo-bar should be referenced from config.json as foo-bar.

At this point the Docker will check what credentials helper it should use for docker.mydomain.com and will use the AWS ECR Wrapper script, that will use the mapping from ~/.ecr/custom.json to get the correct ECR URL and pass the rest of the load to the original AWS ECR Credentials Helper.

Use custom credentials_process to login using AWS Cognito

Now we need to bootstrap our AWS Cognito script to authorize the user and get the credentials to access AWS ECR.

We do this by using credential_process property of the ECR (docs).

We do this by putting credentials_process entry in ~/.aws/config:

[default]
credential_process = /var/piotr/cognito-ecr-credentials-provider.sh

The cognito-ecr-credentials-provider.sh must return particular response to stdin after making sure the user is correctly authenticated (and authorized to access the particular ECR image). What you could use is something like this:

#!/bin/bash

# Required env-vars (to be provided by execution environment of this script) are:
# AWS_USERNAME
# AWS_PASSWORD
# AWS_REGION
# AWS_ACCOUNT_ID
# AWS_COGNITO_CLIENT_ID
# AWS_COGNITO_USER_POOL_ID
# AWS_COGNITO_IDENTITY_POOL_ID

INITIATE_AUTH_RESPONSE=$(curl -sX POST --location "https://cognito-idp.$AWS_REGION.amazonaws.com" \
  -H "Content-Type: application/x-amz-json-1.1" \
  -H "X-Amz-Target: AWSCognitoIdentityProviderService.InitiateAuth" \
  -d "{
    \"AuthFlow\" : \"USER_PASSWORD_AUTH\",
    \"ClientId\" : \"$AWS_COGNITO_CLIENT_ID\",
    \"AuthParameters\" : {
        \"USERNAME\" : \"$AWS_USERNAME\",
        \"PASSWORD\" : \"$AWS_PASSWORD\"
    }
}")

ID_TOKEN=$(echo "$INITIATE_AUTH_RESPONSE" | jq -r '.AuthenticationResult.IdToken')

GET_ID_RESPONSE=$(curl -sX POST --location "https://cognito-identity.$AWS_REGION.amazonaws.com" \
  -H "Content-Type: application/x-amz-json-1.1" \
  -H "X-Amz-Target: com.amazonaws.cognito.identity.model.AWSCognitoIdentityService.GetId" \
  -d "{
   \"AccountId\": \"$AWS_ACCOUNT_ID\",
   \"IdentityPoolId\": \"$AWS_COGNITO_IDENTITY_POOL_ID\",
   \"Logins\": {
      \"cognito-idp.$AWS_REGION.amazonaws.com/$AWS_COGNITO_USER_POOL_ID\" : \"$ID_TOKEN\"
   }
}")

IDENTITY_ID=$(echo "$GET_ID_RESPONSE" | jq -r '.IdentityId')

GET_CREDENTIALS_RESPONSE=$(curl -sX POST --location "https://cognito-identity.$AWS_REGION.amazonaws.com" \
  -H "Content-Type: application/x-amz-json-1.1" \
  -H "X-Amz-Target: com.amazonaws.cognito.identity.model.AWSCognitoIdentityService.GetCredentialsForIdentity" \
  -d "{
   \"IdentityId\": \"$IDENTITY_ID\",
   \"Logins\": {
      \"cognito-idp.$AWS_REGION.amazonaws.com/$AWS_COGNITO_USER_POOL_ID\": \"$ID_TOKEN\"
   }
}")

EXPIRATION_IN_UNIX_TIMESTAMP=$(echo "$GET_CREDENTIALS_RESPONSE" | jq -r '.Credentials.Expiration')
EXPIRATION_IN_ISO8601=$(date -d "@$EXPIRATION_IN_UNIX_TIMESTAMP" -u +"%Y-%m-%dT%H:%M:%SZ")

AWS_CREDENTIALS_FORMAT=$(echo "$GET_CREDENTIALS_RESPONSE" | jq "
{
  \"Version\": 1,
  \"AccessKeyId\": .Credentials.AccessKeyId,
  \"SecretAccessKey\": .Credentials.SecretKey,
  \"SessionToken\": .Credentials.SessionToken,
  \"Expiration\": \"$EXPIRATION_IN_ISO8601\"
}")

printf "%s" "$AWS_CREDENTIALS_FORMAT"

This script utilizes a bunch of env-vars and proceeds with the login procedure against AWS Cognito for a user that is in given Cognito User Pool.

In this particular case, the following app client configuration (authentication flows) are enabled:

  • ALLOW_USER_PASSWORD_AUTH
  • ALLOW_REFRESH_TOKEN_AUTH

The end

The returned credentials will be stored in the credentials’ storage.

Now, in my eyes that’s a LOT of work for something that should not be that complicated…