Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

(pipelines): DockerCredential.docker_hub() silently fails to auth against Docker Hub #15737

Closed
james-mathiesen opened this issue Jul 23, 2021 · 7 comments · Fixed by #18313
Closed
Labels
@aws-cdk/pipelines CDK Pipelines library bug This issue is a bug. effort/medium Medium work item – several days of effort p1

Comments

@james-mathiesen
Copy link

Using DockerCredential.docker_hub() from CDKPipeline will silently fail to auth against Docker Hub during all stages of the build. In our case this makes ContainerImage.from_asset() intermittently fail when executed via CDKPipeline and makes the overall CI/CD process unstable.

Because most users rely on public images on Docker Hub the failure mode is subtle. In most cases users will observe only intermittent issues due to Docker Hub enforcement of pull rate limits against the IP addresses in AWS CodeBuild infrastructure.

To reliably reproduce this issue one can create a free Docker Hub account and create a private container image. Construct a pipeline that uses from_asset() to build an image that depends on the private image. Finally, supply a DockerCredential structure to CDKPipeline that is expected to allow access to the private image. This approach will reliably demonstrate that the Docker Hub credentials are not being used.

Private images on Docker Hub are an unusual use case; however using Docker Hub credentials to pull public images from Docker Hub is common practice due to Docker Hub enforcing IP address rate limits for unauthenticated users. The IP address rate limits are especially painful when building on shared infrastructure such as AWS CodeBuild.

Docker Hub offers per-user rate limits for free accounts and it is common practice to setup free accounts to support automated builds. When using shared infrastructure such as AWS CodeBuild, Docker Hub credentials are a practical necessity.

Reproduction Steps

dockerhub_secret = secretsmanager.Secret.from_secret_name_v2(
        self,
        "pull-credentials",
       "dockerhub/image-pull-credentials"
)

pipeline = CdkPipeline(
            self,
            "CICDPipeline",
            docker_credentials=[
                pipelines.DockerCredential.docker_hub(dockerhub_secret),
            ],
...

Because the common failure mode is caused by rate limit enforcement on Docker Hub I cannot provide a copy/paste test that will reliably demonstrate the problem without also sharing valid Docker Hub credentials which I do not wish to do.

What did you expect to happen?

I expected the container asset stages in AWS CodeBuild to authenticate against Docker Hub when pulling image dependencies.

By authenticating I expected to be subject to a per-user pull rate limit on Docker Hub despite the build happening on shared infrastructure in AWS CodeBuild.

What actually happened?

The builds intermittently fail due to enforcement of IP address rate limits at Docker Hub.

Environment

  • CDK CLI Version : 1.115.0
  • Framework Version:
  • Node.js Version: v14.17.2
  • **OS : AWS CodeBuild (Amazon Linux) **
  • Language (Version): Python 3.9.6

Other

Authentication fails because credHelpers in Docker's config.json does not work as the Docker documentation would lead one to believe.

When pulling a public image (e.g. FROM amazon/aws-cli:2.2.18) the lookup in credHelpers is for the key https://index.docker.io/v1/. When the credential helper program is executed the domain passed to the helper is index.docker.io. The CDK incorrectly presumes that both values are index.docker.io so the credential helper is never invoked. Because most images on Docker Hub are public the lack of authentication is not obvious except when pull rate limits are exceeded.

This is a workaround to the problem:

        pipeline = CdkPipeline(
            self,
            "CICDPipeline",
            docker_credentials=[
                pipelines.DockerCredential.docker_hub(dockerhub_secret),
                pipelines.DockerCredential.custom_registry("https://index.docker.io/v1/", dockerhub_secret),
            ],

The above workaround creates two very similar entries, one to support each stage of the auth process.

The entry for https://index.docker.io/v1/ is required to cause docker to invoke the CDK credential helper.

The entry for index.docker.io is required for the CDK credential helper to find a match for the domain passed into the credential helper.


This is 🐛 Bug Report

@james-mathiesen james-mathiesen added bug This issue is a bug. needs-triage This issue or PR still needs to be triaged. labels Jul 23, 2021
@github-actions github-actions bot added the @aws-cdk/pipelines CDK Pipelines library label Jul 23, 2021
@NGL321 NGL321 added needs-reproduction This issue needs reproduction. p2 and removed needs-triage This issue or PR still needs to be triaged. labels Jul 24, 2021
@nohack
Copy link
Contributor

nohack commented Jul 26, 2021

As mentioned by @james-mathiesen looks like docker credential helper is invoked for the dockers official registry when config.json has full url.

When we login to dockerhub, the below entry is added to config.json in auths section.

  "https://index.docker.io/v1/": {
      "auth": "dGhpc2lzd2h5asdasdasda......"
  }

If registry is official they use lookup to get server url and which is this

@nohack
Copy link
Contributor

nohack commented Jul 26, 2021

I think we can pass the full url to fetchDockerLoginCredentials in docker credential cdk asset and check the domain first and fallback to url as well for special cases like this.I doubt if there are any other cases which are similar.
I just pushed the above change here.

I did not reproduce the issue in itself.

@rix0rrr rix0rrr added p1 and removed p2 labels Aug 11, 2021
@njlynch
Copy link
Contributor

njlynch commented Aug 11, 2021

Thanks for the detailed write-up @james-mathiesen , and the patch and further info @nohack! Very useful.

It looks like -- in addition to the changes @nohack has posted -- we should also update the below line to store/point to the full URL instead of the domain. That will remove the need to double-configure the entry as seen in the original post.

return new ExternalDockerCredential('index.docker.io', secret, opts);

Looks like a relatively quick fix, followed by a painstaking testing process. :)

@rix0rrr rix0rrr removed their assignment Aug 12, 2021
@njlynch njlynch added effort/medium Medium work item – several days of effort and removed needs-reproduction This issue needs reproduction. labels Sep 27, 2021
@njlynch njlynch removed their assignment Oct 7, 2021
@mpetito-envative
Copy link

This workaround doesn't appear to be working. I still receive dockerhub rate limit errors in the CDK pipeline synth step after setting up a paid Dockerhub account and corresponding secret configuration in AWS.

var dockercreds = Amazon.CDK.AWS.SecretsManager.Secret.FromSecretNameV2(this, "DockerhubSecret", "dockerhub");

var pipeline = new CodePipeline(this, "Pipeline", new CodePipelineProps
{
    ...
    DockerEnabledForSynth = true,
    Synth = new ShellStep("Synth", new ShellStepProps
    {
        ...
    }),
    DockerCredentials = new[]
    {
        DockerCredential.DockerHub(dockercreds),
        DockerCredential.CustomRegistry("https://index.docker.io/v1/", dockercreds) // https://github.com/aws/aws-cdk/issues/15737
    }
});

I've verified that the IAM Role that CodeBuild is using has access to the secret. (Note: the originally generated IAM policy did not have access because the secret arn was suffixed with dockerhub-????? for some reason.)

I also get the following output in the synth step as expected:

[Container] 2021/10/14 14:15:55 Running command echo '
{
    "version": "1.0",
    "domainCredentials": {
        "index.docker.io": {
            "secretsManagerSecretId": "arn:aws:secretsmanager:us-east-1:<account-id>:secret:dockerhub"
        },
        "https://index.docker.io/v1/": {
            "secretsManagerSecretId": "arn:aws:secretsmanager:us-east-1:<account-id>:secret:dockerhub"
        }
    }
}
' > $HOME/.cdk/cdk-docker-creds.json

The asset that fails during synth is built using the node:lts docker image as part of an S3 bucket deployment.

var source = Source.Asset(..., new Amazon.CDK.AWS.S3.Assets.AssetOptions
{
    Bundling = new BundlingOptions
    {
        Image = DockerImage.FromRegistry("node:lts"),
        Command = new[]
        {
            "bash", "-c", string.Join(" && ", new []
            {
                "npm ci",
                "npm run build",
                "cp -r /asset-input/build/* /asset-output"
            })
        }
    },
    ...
});

@bobmoff
Copy link

bobmoff commented Oct 30, 2021

The workaround works for me.

@kaizencc kaizencc removed their assignment Dec 17, 2021
@mergify mergify bot closed this as completed in #18313 Jan 10, 2022
mergify bot pushed a commit that referenced this issue Jan 10, 2022
…18313)

### Problem:

`DockerCredential.dockerHub()` silently failed to authenticate users, resulting in
unexpected and intermittent throttling due to docker's policy for unauthenticated users.

### Reason: 

`.dockerHub()` added `index.docker.io` to the domain credentials, but the actual docker
command [authenticated](https://github.com/moby/moby/blob/1e71c6cffedb79e3def696652753ea43cdc47b99/registry/config.go#L35) with `https://index.docker.io/v1/` which it was unable to find
as a domain credential, thus failing to trigger `docker-credential-cdk-assets`
during the `docker --config build` call.

Furthermore, the credential `DockerCredential.customRegistry('https://index.docker.io/v1/', secret)`
alone does not work. This would successfully trigger `docker-credential-cdk-assets`
but fail to authenticate because of how `cdk-assets` handles credential lookup.
The command strips the endpoint into just a hostname so in this case we try
`fetchDockerLoginCredentials(awsClient, config, 'index.docker.io')` which fails:

https://github.com/aws/aws-cdk/blob/4fb0309e3b93be276ab3e2d510ffc2ce35823dcd/packages/cdk-assets/bin/docker-credential-cdk-assets.ts#L32-L38

So the workaround for this bug was to specify both domains as credentials, each to
satisfy a separate step of the process:

```ts
dockerCredentials: [
                pipelines.DockerCredential.dockerHub(secret),
                pipelines.DockerCredential.customRegistry('https://index.docker.io/v1/', secret),
            ],
```

### Solution:

This PR introduces two separate changes to address both problems. First, we change
the hardcoded domain in `DockerCredential.dockerHub()` to be `https://index.docker.io/v1/`.
This allows us to successfully trigger `docker-credential-cdk-assets` when the
`docker --config build` command is called.

Next, to make sure the credential lookup succeeds, we check for both the complete
endpoint and the domain name. In this case, we will check for both `https://index.docker.io/v1/`
as well as `index.docker.io`. Since `https://index.docker.io/v1/` exists in the credentials helper,
authentication will succeed.

Why do we still check for the domain `index.docker.io`? I don't know how custom registries or
ecr works in this context and believe it to be beyond the scope of the PR. It's possible that they
require the domain only for lookup.

### Testing:

The change to credential lookups is unit tested in `docker-credentials.test.ts`. I confirmed that
the change to `DockerCredential.dockerHub()` is successful by configuring a mock
`cdk-docker-creds.json` file and successfully `cdk deploy`ing a docker image that depends on
a private repository. This isn't a common use case but ensures that failure to authenticate
results in failure every time. Thanks @james-mathiesen for the suggestion.


### Contributors:

Thanks to @nohack for the code in `cdk-assets`.

Fixes #15737.

----

*By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license*
@github-actions
Copy link

⚠️COMMENT VISIBILITY WARNING⚠️

Comments on closed issues are hard for our team to see.
If you need more assistance, please either tag a team member or open a new issue that references this one.
If you wish to keep having a conversation with other community members under this issue feel free to do so.

TikiTDO pushed a commit to TikiTDO/aws-cdk that referenced this issue Feb 21, 2022
…ws#18313)

### Problem:

`DockerCredential.dockerHub()` silently failed to authenticate users, resulting in
unexpected and intermittent throttling due to docker's policy for unauthenticated users.

### Reason: 

`.dockerHub()` added `index.docker.io` to the domain credentials, but the actual docker
command [authenticated](https://github.com/moby/moby/blob/1e71c6cffedb79e3def696652753ea43cdc47b99/registry/config.go#L35) with `https://index.docker.io/v1/` which it was unable to find
as a domain credential, thus failing to trigger `docker-credential-cdk-assets`
during the `docker --config build` call.

Furthermore, the credential `DockerCredential.customRegistry('https://index.docker.io/v1/', secret)`
alone does not work. This would successfully trigger `docker-credential-cdk-assets`
but fail to authenticate because of how `cdk-assets` handles credential lookup.
The command strips the endpoint into just a hostname so in this case we try
`fetchDockerLoginCredentials(awsClient, config, 'index.docker.io')` which fails:

https://github.com/aws/aws-cdk/blob/4fb0309e3b93be276ab3e2d510ffc2ce35823dcd/packages/cdk-assets/bin/docker-credential-cdk-assets.ts#L32-L38

So the workaround for this bug was to specify both domains as credentials, each to
satisfy a separate step of the process:

```ts
dockerCredentials: [
                pipelines.DockerCredential.dockerHub(secret),
                pipelines.DockerCredential.customRegistry('https://index.docker.io/v1/', secret),
            ],
```

### Solution:

This PR introduces two separate changes to address both problems. First, we change
the hardcoded domain in `DockerCredential.dockerHub()` to be `https://index.docker.io/v1/`.
This allows us to successfully trigger `docker-credential-cdk-assets` when the
`docker --config build` command is called.

Next, to make sure the credential lookup succeeds, we check for both the complete
endpoint and the domain name. In this case, we will check for both `https://index.docker.io/v1/`
as well as `index.docker.io`. Since `https://index.docker.io/v1/` exists in the credentials helper,
authentication will succeed.

Why do we still check for the domain `index.docker.io`? I don't know how custom registries or
ecr works in this context and believe it to be beyond the scope of the PR. It's possible that they
require the domain only for lookup.

### Testing:

The change to credential lookups is unit tested in `docker-credentials.test.ts`. I confirmed that
the change to `DockerCredential.dockerHub()` is successful by configuring a mock
`cdk-docker-creds.json` file and successfully `cdk deploy`ing a docker image that depends on
a private repository. This isn't a common use case but ensures that failure to authenticate
results in failure every time. Thanks @james-mathiesen for the suggestion.


### Contributors:

Thanks to @nohack for the code in `cdk-assets`.

Fixes aws#15737.

----

*By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license*
@bobmoff
Copy link

bobmoff commented Jan 4, 2023

The workaround works for me.

Hmm. I guess I was just lucky, as now it is not working any more.

It works for

ContainerImage.fromAsset

but it fails when using

Source.asset('xxx', { bundling: { image: DockerImage.fromBuild('xxxx'), }, }),

I wonder if it has something to do with that DockerImage.fromBuild is created during synth, and the fromAsset is created during the Asset steps.

Hmm..

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
@aws-cdk/pipelines CDK Pipelines library bug This issue is a bug. effort/medium Medium work item – several days of effort p1
Projects
None yet
Development

Successfully merging a pull request may close this issue.

8 participants