AWS Builder Center

Custom Domains for AWS CodeArtifact with CloudFront

A validated approach to custom domain names for CodeArtifact using CloudFront and a single origin request policy change.

AWS Solutions Architect

Introduction

If you use AWS CodeArtifact, your repository endpoints look like this:
1
https://<DOMAIN>-<ACCOUNT_ID>.d.codeartifact.<REGION>.amazonaws.com/pypi/<REPO_NAME>/simple/
For teams that prefer a cleaner developer experience, or need to maintain consistent internal URLs that don't change across environments, a custom domain name like packages.yourcompany.com is a common request.
This post walks through how to configure a custom domain name for your CodeArtifact repositories using Amazon CloudFront, and explains why straightforward approaches (like a CNAME record or Application Load Balancer) don't work.

The Challenge

At first glance, you might think a CNAME record pointing packages.yourcompany.com to the CodeArtifact endpoint would solve the problem. Unfortunately, two issues prevent this:
  1. TLS certificate mismatch. CodeArtifact's TLS certificate only covers *.d.codeartifact.<region>.amazonaws.com. When a client connects to your custom domain and receives this certificate, TLS verification fails because the certificate doesn't cover your domain.
  2. Host header validation. CodeArtifact validates the Host HTTP header on every request. If the header doesn't match the expected CodeArtifact domain, the service returns HTTP 400 Bad Request.
This behavior can be confirmed by sending a request with a modified Host header to the CodeArtifact endpoint:
1
2
3
4
5
6
7
8
9
10
11
12
# Correct Host header → 200 OK
curl -s -o /dev/null -w "%{http_code}" \
-H "Authorization: Bearer $CODEARTIFACT_AUTH_TOKEN" \
"https://<DOMAIN>-<ACCOUNT_ID>.d.codeartifact.<REGION>.amazonaws.com/pypi/<REPO_NAME>/simple/requests/"
# Result: 200

# Incorrect Host header → 400 Bad Request
curl -s -o /dev/null -w "%{http_code}" \
-H "Authorization: Bearer $CODEARTIFACT_AUTH_TOKEN" \
-H "Host: packages.mycompany.com" \
"https://<DOMAIN>-<ACCOUNT_ID>.d.codeartifact.<REGION>.amazonaws.com/pypi/<REPO_NAME>/simple/requests/"
# Result: 400
This confirms that any proxy solution must rewrite the Host header to the CodeArtifact domain while still forwarding the client's authentication token.

Why Not an Application Load Balancer?

A natural next thought is to place an Application Load Balancer (ALB) in front of CodeArtifact. The ALB would terminate TLS with your custom certificate and forward requests to the CodeArtifact VPC endpoint. However:
  • ALB forwards the client's Host header to the target by default (your custom domain name).
  • ALB does not support arbitrary Host header rewriting in standard forward actions.
  • To work around this, you'd need a Lambda target that rewrites the Host header before forwarding. This adds latency, complexity, and an additional cost layer.

The Solution: Amazon CloudFront

Amazon CloudFront solves both challenges natively:
  1. TLS termination. CloudFront presents your custom AWS Certificate Manager (ACM) certificate to clients, so they see a valid certificate for your domain.
  2. Host header rewrite. When configured with the correct Origin Request Policy, CloudFront automatically sends the origin's domain name as the Host header, which CodeArtifact expects.
  3. Authorization passthrough. The Authorization: Bearer <token> header is forwarded transparently.

Architecture

The following diagram shows the request flow from a package manager client through to CodeArtifact:

The Key Insight: Origin Request Policy

The critical configuration is the Origin Request Policy. AWS provides a managed policy called AllViewerExceptHostHeader (ID: b689b0a8-53d0-40ab-baf2-68738e2966ac) that:
  • ✅ Forwards viewer headers to the origin, including Authorization
  • ❌ Does not forward the viewer's Host header
When the client's Host header is excluded, CloudFront defaults to sending the origin domain name as the Host header in the request to CodeArtifact. This is exactly what CodeArtifact expects.
Common mistake: Using the AllViewer managed policy (ID: 216adef6-5c7f-47e4-b989-5492eafa07d3) forwards the client's Host header (your custom domain) to the origin, causing CodeArtifact to return 400 Bad Request.

Implementation

Prerequisites

  • An existing CodeArtifact domain and repository
  • A custom domain name you control
  • Access to your DNS management (Amazon Route 53 (Route 53) or external)
  • AWS CLI configured with appropriate permissions

Step 1: Request an ACM Certificate

CloudFront requires certificates to be in us-east-1, regardless of your CodeArtifact region.
1
2
3
4
aws acm request-certificate \
--domain-name packages.yourcompany.com \
--validation-method DNS \
--region us-east-1
Add the DNS validation CNAME record that ACM provides, then wait for the certificate status to show ISSUED.

Step 2: Get Your CodeArtifact Repository Endpoint

1
2
3
4
5
6
aws codeartifact get-repository-endpoint \
--domain <DOMAIN> \
--repository <REPO_NAME> \
--format pypi \
--region us-east-1 \
--query repositoryEndpoint --output text
This returns a URL like: https://<DOMAIN>-<ACCOUNT_ID>.d.codeartifact.us-east-1.amazonaws.com/pypi/<REPO_NAME>/

Step 3: Create the CloudFront Distribution

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
aws cloudfront create-distribution --distribution-config '{
"CallerReference": "codeartifact-custom-domain-'
$(date +%s)'",
"Comment": "CodeArtifact custom domain proxy",
"Enabled": true,
"Aliases": {
"Quantity": 1,
"Items": ["packages.yourcompany.com"]
},
"Origins": {
"Quantity": 1,
"Items": [{
"Id": "codeartifact-origin",
"DomainName": "<DOMAIN>-<ACCOUNT_ID>.d.codeartifact.<REGION>.amazonaws.com",
"CustomOriginConfig": {
"HTTPPort": 80,
"HTTPSPort": 443,
"OriginProtocolPolicy": "https-only",
"OriginSslProtocols": {"Quantity": 1, "Items": ["TLSv1.2"]},
"OriginReadTimeout": 120
}
}]
},
"DefaultCacheBehavior": {
"TargetOriginId": "codeartifact-origin",
"ViewerProtocolPolicy": "https-only",
"AllowedMethods": {
"Quantity": 7,
"Items": ["GET","HEAD","OPTIONS","PUT","POST","PATCH","DELETE"],
"CachedMethods": {"Quantity": 2, "Items": ["GET","HEAD"]}
},
"CachePolicyId": "4135ea2d-6df8-44a3-9df3-4b5a84be39ad",
"OriginRequestPolicyId": "b689b0a8-53d0-40ab-baf2-68738e2966ac",
"Compress": false
},
"PriceClass": "PriceClass_100",
"ViewerCertificate": {
"ACMCertificateArn": "<YOUR_CERT_ARN>",
"SSLSupportMethod": "sni-only",
"MinimumProtocolVersion": "TLSv1.2_2021"
}
}'

Key settings explained:
SettingValueRationale
CachePolicyId4135ea2d-... (CachingDisabled)Responses are auth-dependent; caching could serve wrong packages to wrong users
OriginRequestPolicyIdb689b0a8-... (AllViewerExceptHostHeader)Forwards auth headers but NOT Host. Lets CloudFront send CodeArtifact's domain as Host
AllowedMethodsAll 7Package publishing requires PUT/POST
OriginReadTimeout120Large package uploads or initial upstream fetches may exceed the 30s default

Step 4: Configure DNS

Once the distribution is deployed (status shows Deployed), point your custom domain to it. The CloudFront distribution domain name (e.g., d1234abcdef.cloudfront.net) is returned in the output of the create-distribution command.
Route 53:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# Find your hosted zone ID
aws route53 list-hosted-zones \
--query "HostedZones[?Name=='yourcompany.com.'].Id" --output text

# Add CNAME record
aws route53 change-resource-record-sets \
--hosted-zone-id <ZONE_ID> \
--change-batch '{
"Changes": [{
"Action": "CREATE",
"ResourceRecordSet": {
"Name": "packages.yourcompany.com",
"Type": "CNAME",
"TTL": 300,
"ResourceRecords": [{"Value": "<DISTRIBUTION_DOMAIN>.cloudfront.net"}]
}
}]
}'

External DNS provider (GoDaddy, Cloudflare, etc.):
Add a CNAME record in your provider's DNS management console:
Record TypeNameValueTTL
CNAMEpackages<DISTRIBUTION_DOMAIN>.cloudfront.net300
DNS propagation typically takes 1–5 minutes. Verify resolution before testing:
1
2
dig packages.yourcompany.com +short
# Should return the CloudFront distribution domain or its edge IPs

Step 5: Test the Setup

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# Get a fresh auth token
export CODEARTIFACT_AUTH_TOKEN=$(aws codeartifact get-authorization-token \
--domain my-domain --query authorizationToken --output text)

# Test with curl
curl -s -w "\n%{http_code}" \
-H "Authorization: Bearer $CODEARTIFACT_AUTH_TOKEN" \
"https://packages.yourcompany.com/pypi/my-repo/simple/requests/"
# Expected: 200

# Test with pip
pip install \
--index-url https://aws:$CODEARTIFACT_AUTH_TOKEN@packages.yourcompany.com/pypi/my-repo/simple/ \
requests

Production Considerations

Data Transfer Cost

When CodeArtifact and CloudFront are in the same region (recommended), the origin fetch is intra-region, which means negligible additional cost. The total DTO is approximately equivalent to direct CodeArtifact access (~$0.085-0.095/GB vs. $0.09/GB direct).

Security: AWS WAF

Since CloudFront exposes the repository to the public internet, consider attaching an AWS WAF Web ACL for defense in depth:
  • IP allowlist: Restrict to corporate VPN/office ranges
  • Rate limiting: Protect against abuse (e.g., 1000 requests per 5 minutes per IP)
  • Geo-blocking: Restrict to expected countries if applicable
This is especially important because CodeArtifact tokens are time-limited (12 hours) but not IP-bound. An inadvertently disclosed token could be used from anywhere without WAF protection.

Token Management

The aws codeartifact login CLI command automatically configures package managers with the native CodeArtifact URL. It does not know about your custom CloudFront domain, so you'll need to configure your package manager manually.
pip (Python):
1
2
pip config set global.index-url \
"https://packages.yourcompany.com/pypi/<REPO_NAME>/simple/"
npm (Node.js):
1
2
3
# .npmrc
registry=https://packages.yourcompany.com/npm/<REPO_NAME>/
//packages.yourcompany.com/npm/<REPO_NAME>/:_authToken=${CODEARTIFACT_AUTH_TOKEN}
Note: CodeArtifact tokens expire after 12 hours by default. This is standard CodeArtifact behavior, independent of the custom domain setup. Refresh the token by re-running get-authorization-token and updating your configuration. In CI/CD pipelines, add the token refresh as a setup step before any install commands.

Cleaning Up

To avoid ongoing charges, remove the resources created in this walkthrough:
  1. Disable and delete the CloudFront distribution:
1
2
3
4
5
6
7
8
9
10
# Get the current config and ETag
aws cloudfront get-distribution-config --id <DISTRIBUTION_ID> > /tmp/cf-config.json

# Set Enabled to false in the config, then update
aws cloudfront update-distribution --id <DISTRIBUTION_ID> \
--if-match <ETAG> \
--distribution-config file:///tmp/cf-disabled.json

# Wait for status to show "Deployed", then delete
aws cloudfront delete-distribution --id <DISTRIBUTION_ID> --if-match <NEW_ETAG>
  1. Remove the DNS record for your custom domain (CNAME pointing to CloudFront).
  2. Delete the ACM certificate (only after detaching from CloudFront):
1
aws acm delete-certificate --certificate-arn <CERT_ARN> --region us-east-1
  1. Optionally delete the CodeArtifact repository and domain if they were created for testing:
1
2
aws codeartifact delete-repository --domain <DOMAIN> --repository <REPO_NAME> --region <REGION>
aws codeartifact delete-domain --domain <DOMAIN> --region <REGION>

Conclusion

While AWS CodeArtifact doesn't natively support custom domain names, Amazon CloudFront provides a solution that requires minimal infrastructure. The key is using the AllViewerExceptHostHeader managed origin request policy, which forwards authentication headers while sending the correct Host header to CodeArtifact.
With this setup, your team can use a clean, branded URL for all package operations. The configuration uses only managed AWS services, requires no custom code or Lambda functions, provides TLS end-to-end with optional WAF protection, and works for CodeArtifact-supported package formats.
Any opinions in this article are those of the individual author and may not reflect the opinions of AWS.
Enjoyed reading this content? Let the author know!

Your likes, comments, shares, and saves help creators reach more builders.

Loading recommendations

Loading article