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.
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:- 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. - Host header validation. CodeArtifact validates the
HostHTTP header on every request. If the header doesn't match the expected CodeArtifact domain, the service returnsHTTP 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:
- TLS termination. CloudFront presents your custom AWS Certificate Manager (ACM) certificate to clients, so they see a valid certificate for your domain.
- 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.
- 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
Hostheader
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 theAllViewermanaged policy (ID:216adef6-5c7f-47e4-b989-5492eafa07d3) forwards the client's Host header (your custom domain) to the origin, causing CodeArtifact to return400 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:
| Setting | Value | Rationale |
|---|---|---|
| CachePolicyId | 4135ea2d-... (CachingDisabled) | Responses are auth-dependent; caching could serve wrong packages to wrong users |
| OriginRequestPolicyId | b689b0a8-... (AllViewerExceptHostHeader) | Forwards auth headers but NOT Host. Lets CloudFront send CodeArtifact's domain as Host |
| AllowedMethods | All 7 | Package publishing requires PUT/POST |
| OriginReadTimeout | 120 | Large 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 Type | Name | Value | TTL |
|---|---|---|---|
| CNAME | packages | <DISTRIBUTION_DOMAIN>.cloudfront.net | 300 |
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:
- 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>
- Remove the DNS record for your custom domain (CNAME pointing to CloudFront).
- Delete the ACM certificate (only after detaching from CloudFront):
1
aws acm delete-certificate --certificate-arn <CERT_ARN> --region us-east-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.
Enjoyed reading this content? Let the author know!
Your likes, comments, shares, and saves help creators reach more builders.
Loading recommendations
Loading article