AWS CodeArtifact · 2026-10-24 · CodeArtifact arc
AWS CodeArtifact for MCP Server Package Distribution: Domain Setup, npm and PyPI Publishing, IAM Permissions, and Cross-Account Patterns
MCP server teams that move beyond a single repository quickly discover they need a way to share versioned packages across services — a shared retry library, a common protocol parser, a base server SDK that five different MCP servers depend on. AWS CodeArtifact solves this, but four non-obvious constraints cause the majority of first-setup failures: the domain/repository hierarchy is mandatory, not optional — a domain is the top-level namespace inside which repositories live, and the domain name is embedded in every endpoint URL, so you cannot create repositories without first creating a domain; npm .npmrc auth token keys must use the //host/ format without the https: scheme prefix — a team that copies the CodeArtifact endpoint URL verbatim into the _authToken line gets "unable to authenticate, need: Basic realm" errors that look like permission problems but are a config file format issue; Python teams need both aws codeartifact login --tool pip and --tool twine as separate calls because pip writes to pip.conf and twine writes to .pypirc — running only the pip login leaves twine unauthenticated and upload fails with 403; and the IAM permission sts:GetServiceBearerToken is a separate action from codeartifact:GetAuthorizationToken and cannot be granted via the CodeArtifact resource policy — it must appear in the IAM identity policy of the calling principal, and omitting it produces an "unauthorized to perform sts:GetServiceBearerToken" error even when the CodeArtifact actions are all present. This guide synthesises all four patterns. Pattern 1 covers the domain and repository architecture — the two-level hierarchy, endpoint URL format, and upstream proxy chains that let a private CodeArtifact repository transparently serve both private MCP packages and cached public npm/PyPI packages — synthesised from AWS CodeArtifact for MCP Servers — Domain Setup, Upstream Proxying, Token Authentication. Pattern 2 covers npm package publishing — .npmrc scope mapping, the scheme-prefix trap in the auth token key, publishConfig.registry in package.json, version immutability and the CI bump strategy, Docker multi-stage builds with injected credentials — synthesised from MCP Server npm Publishing via CodeArtifact — .npmrc, Scoped Packages, CI Login. Pattern 3 covers Python MCP server packaging — pyproject.toml with hatchling, the pip /simple/ trailing slash requirement, the dual-login pattern, twine upload, and uv publish as an alternative — synthesised from MCP Server PyPI Publishing via CodeArtifact — pip.conf, twine, Python CI Publishing. Pattern 4 covers the three-layer IAM permission model, minimum action sets for consumer versus publisher versus admin roles, domain and repository resource policies, and the cross-account architecture where CI runs in account A and the artifact domain lives in account B — synthesised from CodeArtifact IAM Permissions for MCP Servers — Domain Policies, Cross-Account, CI Roles.
TL;DR
- Domain and repository hierarchy — endpoint URLs, upstream proxying, CDK ordering: Create a CodeArtifact domain first (
aws codeartifact create-domain --domain myorg), then repositories inside it. The domain name is embedded in every endpoint URL:https://myorg-123456789012.d.codeartifact.us-east-1.amazonaws.com/npm/mcp-servers/. To let your private repository serve public packages, create a separate "upstream proxy" repository, connect it to the public registry withassociate-external-connection --external-connection public:npmjs, then link it to your private repository withupdate-repository --upstreams repositoryName=npm-upstream. Without the upstream chain,npm install httpxfails because CodeArtifact will not proxy to npm unless explicitly configured. In CDK, CodeArtifact uses L1 (CloudFormation) constructs only — useaddDependency()to sequence domain → upstream repo → private repo; without explicit dependencies CloudFormation may attempt to create repositories before the domain exists. - Token authentication — 12-hour expiry, two required IAM actions, refresh in pre_build: CodeArtifact issues short-lived bearer tokens, not static keys. A token is valid for a configurable duration up to 12 hours (43,200 seconds, the default). Every IAM principal that authenticates needs both
codeartifact:GetAuthorizationToken(on the domain ARN) andsts:GetServiceBearerToken(on*, as it is an STS action and cannot be scoped) in its identity policy. Omitting either produces an access denied error. Always refresh the token at the start of every CIpre_buildphase — tokens pre-populated in SSM Parameter Store will silently expire for builds queued longer than 12 hours, which happens in large orgs with CodeBuild concurrency limits. Theaws codeartifact login --tool npm|pip|twinehelper automates token fetch + config file write in one command. - npm publishing — .npmrc format, scope mapping, version immutability, Docker builds: In
.npmrc, the auth token key must use the//host/format without thehttps:scheme://myorg-123.d.codeartifact.us-east-1.amazonaws.com/npm/mcp-servers/:_authToken=${CODEARTIFACT_AUTH_TOKEN}. Scoped packages (@myorg/mcp-server-name) require a scope-to-registry mapping:@myorg:registry=https://<endpoint>/— without this linenpm publishsends the package to the public registry regardless of any other configuration. SetpublishConfig.registryinpackage.jsonas a belt-and-suspenders backup. CodeArtifact package versions are immutable — republishing the same version number produces "PackageVersionAlreadyExists". In CI, runnpm version patch --no-git-tag-versionbefore everynpm publish. In Docker multi-stage builds, inject the token viaARG CODEARTIFACT_AUTH_TOKEN, write it to.npmrcin the deps stage, and remove.npmrcbefore the final stage to keep credentials out of the image. - IAM — three-layer model, minimum action sets, cross-account both-sides requirement: CodeArtifact evaluates three policy layers in sequence: the caller's IAM identity policy, the domain resource policy, and the repository resource policy — all three must allow the action. A consumer (ECS task installing packages) needs:
codeartifact:GetAuthorizationTokenon the domain ARN,codeartifact:GetRepositoryEndpointon*,codeartifact:ReadFromRepositoryon the repo ARN, andsts:GetServiceBearerTokenon*. A CI publisher additionally needscodeartifact:PublishPackageVersionandcodeartifact:PutPackageMetadata. For cross-account access (CI in account A, domain in account B), both the resource policy on the domain/repo in account B (granting the foreign principal) and the identity policy on the role in account A (granting the CodeArtifact actions) are required — neither alone is sufficient, matching the standard AWS cross-account permission model. Domain and repository resource policies are full-replace PUT operations — everyput-domain-permissions-policyorput-repository-permissions-policycall replaces the entire existing policy.
Pattern 1 — Domain and Repository Architecture
Why the hierarchy exists
CodeArtifact's domain/repository model exists for a specific reason: domains are the billing, encryption, and access-control boundary shared across an organization, while repositories are the package-format-specific stores (one for npm, one for PyPI, one for Maven). A domain can hold many repositories. The domain name is embedded in every endpoint URL — https://<domain>-<account>.d.codeartifact.<region>.amazonaws.com/npm/<repo>/ — which means the domain name is permanent once set and becomes part of every .npmrc, pip.conf, and buildspec.yml that references it. Choose it once, choose it carefully.
For most MCP server teams, the right setup is one domain per AWS account (or one shared domain in a central artifact account) with three repositories: a private repository for internal packages, an upstream proxy repository with an external connection to the public registry, and no direct install from the public registry at all — the upstream chain means npm install checks the private repo first, falls through to the proxy repo, and falls through to registry.npmjs.org only on a cache miss, after which the package is stored in the proxy.
Creating the infrastructure
# Step 1: create the domain
aws codeartifact create-domain \
--domain myorg \
--region us-east-1
# Step 2: create the upstream proxy repo and connect it to public npm
aws codeartifact create-repository \
--domain myorg \
--domain-owner 123456789012 \
--repository npm-upstream \
--description "Upstream proxy for public npm registry"
aws codeartifact associate-external-connection \
--domain myorg \
--domain-owner 123456789012 \
--repository npm-upstream \
--external-connection public:npmjs
# Step 3: create the private repo and chain it to the upstream proxy
aws codeartifact create-repository \
--domain myorg \
--domain-owner 123456789012 \
--repository mcp-servers \
--description "Internal MCP server npm packages"
aws codeartifact update-repository \
--domain myorg \
--domain-owner 123456789012 \
--repository mcp-servers \
--upstreams repositoryName=npm-upstream
With this setup, npm install @myorg/mcp-server-analytics checks mcp-servers, fails (not found), traverses to npm-upstream, fails (not cached), fetches from registry.npmjs.org, and stores the package in npm-upstream. The second install never touches the public registry. This protects against npm package removals and rate limits.
CDK infrastructure: L1 constructs with explicit dependency ordering
// CDK: CodeArtifact domain + upstream + private repo for MCP server packages
import * as codeartifact from "aws-cdk-lib/aws-codeartifact";
// Domain — created first; all repos depend on it
const domain = new codeartifact.CfnDomain(this, "McpDomain", {
domainName: "myorg",
});
// Upstream proxy: external connection to public npm
const npmUpstream = new codeartifact.CfnRepository(this, "NpmUpstream", {
domainName: domain.domainName,
repositoryName: "npm-upstream",
externalConnections: ["public:npmjs"],
});
npmUpstream.addDependency(domain); // REQUIRED: domain must exist before repo
// Private repo: chain through upstream for public package fallback
const mcpServersRepo = new codeartifact.CfnRepository(this, "McpServersRepo", {
domainName: domain.domainName,
repositoryName: "mcp-servers",
upstreams: [npmUpstream.repositoryName],
});
mcpServersRepo.addDependency(npmUpstream); // REQUIRED: upstream must exist before private repo
CodeArtifact CDK support uses L1 (CloudFormation) constructs as of CDK v2.140 — there are no L2 constructs with higher-level abstractions. The addDependency() calls are mandatory: without them, CloudFormation's parallel resource creation may attempt to create mcp-servers before npm-upstream exists, and the upstreams association will fail with a ResourceNotFoundException. The same applies to PyPI repositories — create the pypi-upstream repository with externalConnections: ["public:pypi"] and chain it to the private Python repository.
Supported package formats and external connection strings
| Format | External connection | Login tool flag | Config file written |
|---|---|---|---|
| npm | public:npmjs | --tool npm | ~/.npmrc |
| PyPI | public:pypi | --tool pip (install) + --tool twine (publish) | ~/.config/pip/pip.conf and ~/.pypirc |
| Maven | public:maven-central | --tool mvn or --tool gradle | ~/.m2/settings.xml |
| NuGet | public:nuget-org | --tool dotnet | NuGet.Config |
| Swift | public:swift-index | manual token | Package.resolved |
| RubyGems | public:ruby-gems-org | --tool gem | ~/.gemrc |
Pattern 2 — Token Authentication
The two-action requirement
CodeArtifact authentication is not a simple API key lookup. When a principal calls codeartifact:GetAuthorizationToken, CodeArtifact internally calls sts:GetServiceBearerToken to generate the short-lived credential. Both IAM actions must therefore be present in the principal's identity policy. This is the single most common IAM misconfiguration in CodeArtifact setups:
// Identity policy that grants both required authentication actions
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": [
"codeartifact:GetAuthorizationToken", // Domain-level — resource: domain ARN
"codeartifact:GetRepositoryEndpoint", // Resolve the npm/PyPI URL
"sts:GetServiceBearerToken" // STS action — resource must be "*"
],
"Resource": "*"
}
]
}
The sts:GetServiceBearerToken action is scoped to "*" because it is an STS action rather than a CodeArtifact action — you cannot scope it to a CodeArtifact domain or repository ARN, and attempting to do so will still produce access denied. It also cannot be granted via the CodeArtifact domain resource policy; it must live on the IAM identity policy of the calling principal. This is documented in CodeArtifact's IAM reference but easy to miss because most CodeArtifact tutorials show only the codeartifact:* actions.
Token expiry and the CI pre_build refresh pattern
Tokens have a maximum lifetime of 12 hours (43,200 seconds, the default). Two CI anti-patterns cause silent expiry failures:
- Pre-populated SSM tokens: a common setup writes a fresh token to SSM Parameter Store via a scheduled Lambda, then reads it in buildspec.yml. If a build queues for more than 12 hours (common in orgs with CodeBuild concurrency limits), the token is stale by the time the build starts, and
npm installfails with 401 Unauthorized. - Token in Dockerfile ARG without CI rebuild: CI fetches a token, injects it as a
--build-arginto a Docker build, and caches the built image. If the image is reused 13 hours later (e.g., in a downstream pipeline stage), the token inside the image's build arg is expired and the nextnpm installinside that image will fail.
The reliable pattern is to always fetch a fresh token at the very start of pre_build, never cache tokens in any persistent store, and use aws codeartifact login --tool npm (which fetches and writes in one step) rather than manually constructing the .npmrc entry:
# buildspec.yml: pre_build phase — always refresh the token here
phases:
pre_build:
commands:
# Fetch a fresh token (valid for up to 12 hours) and write it to ~/.npmrc
- aws codeartifact login \
--tool npm \
--domain myorg \
--domain-owner 123456789012 \
--repository mcp-servers \
--region us-east-1
# Now npm commands work for the lifetime of this build
- npm ci
For container-based builds where the Dockerfile needs the token at image build time, always fetch a new token immediately before running docker build:
# CI script: fetch a token right before docker build (never reuse a cached token)
TOKEN=$(aws codeartifact get-authorization-token \
--domain myorg \
--domain-owner 123456789012 \
--query authorizationToken \
--output text)
docker build \
--build-arg CODEARTIFACT_AUTH_TOKEN="${TOKEN}" \
-t my-mcp-service:latest .
Pattern 3 — npm Package Publishing
The .npmrc auth token key format
The single most common npm publishing failure is the .npmrc auth token key format. The key must use the //host/path/ format — without the https: scheme prefix. Teams that copy the CodeArtifact endpoint URL from the AWS console and use it verbatim as the key get "unable to authenticate, need: Basic realm" errors that look like permission problems:
# .npmrc — WRONG: https: prefix on the auth token key breaks npm auth lookup
https://myorg-123456789012.d.codeartifact.us-east-1.amazonaws.com/npm/mcp-servers/:_authToken=TOKEN
# .npmrc — CORRECT: key starts with // (no scheme prefix)
//myorg-123456789012.d.codeartifact.us-east-1.amazonaws.com/npm/mcp-servers/:_authToken=${CODEARTIFACT_AUTH_TOKEN}
# Scope mapping: all @myorg/* packages resolve from CodeArtifact
@myorg:registry=https://myorg-123456789012.d.codeartifact.us-east-1.amazonaws.com/npm/mcp-servers/
The ${CODEARTIFACT_AUTH_TOKEN} syntax in the token value is npm's environment variable substitution — npm reads the file and resolves ${...} references from the environment. This pattern is safe to commit because the token itself is never in source control, only the variable reference.
Scoped packages and publishConfig
Without the scope-to-registry mapping, npm publish sends the package to the public npm registry regardless of the global registry setting in .npmrc. The scope mapping and publishConfig in package.json are belt-and-suspenders:
// package.json for an MCP server npm package
{
"name": "@myorg/mcp-server-analytics",
"version": "1.0.0",
"scripts": {
"build": "tsc --project tsconfig.json",
"prepublishOnly": "npm run build"
},
"publishConfig": {
// Belt-and-suspenders: this ensures npm publish always goes to CodeArtifact
// even if the .npmrc scope mapping is absent in a given environment
"registry": "https://myorg-123456789012.d.codeartifact.us-east-1.amazonaws.com/npm/mcp-servers/"
}
}
Version immutability and the CI bump strategy
CodeArtifact package versions are immutable — once published, a version cannot be overwritten. Attempting to republish the same version number produces a PackageVersionAlreadyExists error. In CI, the standard mitigation is to bump the patch version before every publish on a merge to main:
# In the post_build phase of buildspec.yml
# Bump patch version without creating a git tag
# (CodeBuild does not have push access to git)
- npm version patch --no-git-tag-version
- npm publish
# Output the published version for downstream pipeline stages
- echo "Published $(node -p "require('./package.json').version")"
For teams that want semantic versioning from commit messages, standard-version or semantic-release provide structured bump logic. For the simple "increment on every main commit" pattern, npm version patch --no-git-tag-version is sufficient and avoids the complexity of a full semantic release setup.
Docker multi-stage builds with CodeArtifact credentials
# Dockerfile: install @myorg/mcp-server-* packages from CodeArtifact
FROM node:22-alpine AS deps
WORKDIR /app
COPY package.json package-lock.json ./
# Token injected as build arg from CI — fetched fresh before docker build
ARG CODEARTIFACT_AUTH_TOKEN
RUN echo "@myorg:registry=https://myorg-123456789012.d.codeartifact.us-east-1.amazonaws.com/npm/mcp-servers/" >> .npmrc \
&& echo "//myorg-123456789012.d.codeartifact.us-east-1.amazonaws.com/npm/mcp-servers/:_authToken=${CODEARTIFACT_AUTH_TOKEN}" >> .npmrc \
&& npm ci \
&& rm -f .npmrc # Remove credentials before copy to final stage
FROM node:22-alpine AS runner
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY . .
CMD ["node", "dist/index.js"]
The rm -f .npmrc in the same RUN layer is important: if it were in a separate layer, the credentials would still be present in the earlier layer's image history. Use BuildKit's --mount=type=secret in sensitive environments to keep the token out of build history entirely: RUN --mount=type=secret,id=npmrc,target=.npmrc npm ci.
Pattern 4 — Python MCP Server Publishing
pyproject.toml and the pip upstream requirement
Python MCP servers built with the mcp SDK are pip-installable packages. The packaging setup with hatchling is standard:
# pyproject.toml
[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"
[project]
name = "mcp-server-analytics"
version = "0.1.0"
requires-python = ">=3.11"
dependencies = [
"mcp>=1.0.0",
"httpx>=0.27.0",
"pydantic>=2.0.0",
]
[project.scripts]
mcp-server-analytics = "mcp_server_analytics:main"
[tool.hatch.build.targets.wheel]
packages = ["src/mcp_server_analytics"]
Before publishing, set up a PyPI upstream connection so that installing dependencies during the build phase works even if those packages are not already in CodeArtifact:
# Create a PyPI upstream proxy and connect to public PyPI
aws codeartifact create-repository \
--domain myorg --domain-owner 123456789012 \
--repository pypi-upstream \
--description "Upstream proxy for public PyPI"
aws codeartifact associate-external-connection \
--domain myorg --domain-owner 123456789012 \
--repository pypi-upstream \
--external-connection public:pypi
# Create the private Python package repository
aws codeartifact create-repository \
--domain myorg --domain-owner 123456789012 \
--repository mcp-servers-python
# Chain the upstream so pip falls through to public PyPI for missing packages
aws codeartifact update-repository \
--domain myorg --domain-owner 123456789012 \
--repository mcp-servers-python \
--upstreams repositoryName=pypi-upstream
Without this upstream chain, pip install httpx fails with "No matching distribution found" even though httpx is on public PyPI — CodeArtifact will not proxy to the public index unless explicitly configured.
The dual-login pattern: pip and twine are separate
This is the most frequent Python CodeArtifact misconfiguration: aws codeartifact login --tool pip and --tool twine write to different config files and must both be run in CI builds that install dependencies AND publish packages:
# pip.conf is written by --tool pip
# Location: ~/.config/pip/pip.conf
[global]
index-url = https://aws:TOKEN@myorg-123456789012.d.codeartifact.us-east-1.amazonaws.com/pypi/mcp-servers-python/simple/
# IMPORTANT: URL must end with /simple/ — omitting trailing slash causes 404 during discovery
# .pypirc is written by --tool twine (entirely separate file)
[distutils]
index-servers = codeartifact
[codeartifact]
repository = https://myorg-123456789012.d.codeartifact.us-east-1.amazonaws.com/pypi/mcp-servers-python/
username = aws
password = TOKEN
In CI, the pre_build phase runs both login commands:
# buildspec.yml pre_build — BOTH logins required
pre_build:
commands:
# Configure pip for installing dependencies (writes pip.conf)
- aws codeartifact login --tool pip \
--domain myorg --domain-owner 123456789012 \
--repository mcp-servers-python
# Configure twine for publishing (writes .pypirc — SEPARATE from pip.conf)
- aws codeartifact login --tool twine \
--domain myorg --domain-owner 123456789012 \
--repository mcp-servers-python
# Now install dev dependencies with pip pointing at CodeArtifact
- pip install -r requirements-dev.txt
Full Python CI buildspec: build, test, version bump, publish
# buildspec.yml — complete Python MCP server publish workflow
version: 0.2
phases:
install:
runtime-versions:
python: 3.12
commands:
- pip install --upgrade build twine bump2version
pre_build:
commands:
- aws codeartifact login --tool pip \
--domain myorg --domain-owner 123456789012 \
--repository mcp-servers-python
- aws codeartifact login --tool twine \
--domain myorg --domain-owner 123456789012 \
--repository mcp-servers-python
- pip install -r requirements-dev.txt
build:
commands:
- python -m pytest tests/ -v --tb=short
- python -m build # Creates dist/*.whl and dist/*.tar.gz
post_build:
commands:
- |
if [ "$CODEBUILD_BUILD_SUCCEEDING" != "1" ]; then
echo "Build failed — skipping publish"
exit 0
fi
# Bump patch version (CodeArtifact rejects duplicate versions)
- bump2version patch --no-commit --no-tag
# Rebuild with bumped version
- python -m build
# Publish to CodeArtifact (twine reads .pypirc written in pre_build)
- twine upload --repository codeartifact dist/*
Using uv with CodeArtifact
uv supports CodeArtifact without config file changes — environment variables are sufficient:
TOKEN=$(aws codeartifact get-authorization-token \
--domain myorg --domain-owner 123456789012 \
--query authorizationToken --output text)
ENDPOINT=$(aws codeartifact get-repository-endpoint \
--domain myorg --domain-owner 123456789012 \
--repository mcp-servers-python --format pypi \
--query repositoryEndpoint --output text)
# uv reads UV_INDEX_URL for install
export UV_INDEX_URL="https://aws:${TOKEN}@${ENDPOINT#https://}simple/"
uv pip install mcp-server-analytics
# uv publish (uv 0.4+)
uv publish --publish-url "${ENDPOINT}" --token "${TOKEN}" dist/*.whl
Pattern 5 — IAM Permissions: Three-Layer Model
How CodeArtifact evaluates permissions
CodeArtifact permission evaluation follows the standard AWS layered model but with three policy layers rather than two: the IAM identity policy on the calling principal, the domain-level resource policy, and the repository-level resource policy. All three must allow the requested action. For same-account access, only the identity policy is required — domain and repo resource policies are optional. For cross-account access, both the resource policy (in the artifact account) and the identity policy (in the calling account) are mandatory.
Minimum action sets by role type
| Role type | Required actions | Resource scoping |
|---|---|---|
| Consumer (ECS task, developer install) | codeartifact:GetAuthorizationToken, codeartifact:GetRepositoryEndpoint, codeartifact:ReadFromRepository, sts:GetServiceBearerToken |
GetAuthorizationToken: domain ARN. ReadFromRepository: repo ARN. Others: * |
| Publisher (CI build) | All Consumer actions + codeartifact:PublishPackageVersion, codeartifact:PutPackageMetadata |
Publish actions: repo ARN or * |
| Admin (domain/repo management) | All Publisher actions + codeartifact:CreateRepository, codeartifact:UpdateRepository, codeartifact:AssociateExternalConnection, codeartifact:PutDomainPermissionsPolicy, codeartifact:PutRepositoryPermissionsPolicy |
Domain ARN and repo ARN |
Consumer role CDK example (ECS task)
// CDK: minimum IAM for an ECS task role that installs packages from CodeArtifact
const taskRole = new iam.Role(this, "McpTaskRole", {
assumedBy: new iam.ServicePrincipal("ecs-tasks.amazonaws.com"),
});
taskRole.addToPolicy(new iam.PolicyStatement({
sid: "CodeArtifactTokenAndEndpoint",
effect: iam.Effect.ALLOW,
actions: [
"codeartifact:GetAuthorizationToken", // Must target domain ARN, not repo ARN
"codeartifact:GetRepositoryEndpoint",
"sts:GetServiceBearerToken", // STS action — cannot be granted by resource policy
],
resources: [
`arn:aws:codeartifact:${region}:${account}:domain/myorg`,
"*",
],
}));
taskRole.addToPolicy(new iam.PolicyStatement({
sid: "CodeArtifactRead",
effect: iam.Effect.ALLOW,
actions: ["codeartifact:ReadFromRepository"],
resources: [
`arn:aws:codeartifact:${region}:${account}:repository/myorg/mcp-servers`,
],
}));
The gotcha with codeartifact:GetAuthorizationToken is that it is a domain-scoped action: the resource must be the domain ARN (arn:aws:codeartifact:region:account:domain/name). Using a repository ARN as the resource for GetAuthorizationToken causes "not authorized to perform codeartifact:GetAuthorizationToken on resource arn:...:repository/..." even if the action is listed in the policy — the resource type mismatch causes the evaluation to fail.
Domain and repository resource policies
Domain and repository resource policies are both full-replace operations — calling put-domain-permissions-policy replaces the entire existing domain policy atomically. To add a new principal, read the current policy first with get-domain-permissions-policy --query policy, append the new statement, then PUT the complete updated document:
# Domain resource policy: allow another account to authenticate
aws codeartifact put-domain-permissions-policy \
--domain myorg \
--domain-owner 111111111111 \
--policy-document '{
"Version": "2012-10-17",
"Statement": [{
"Sid": "AllowCrossAccountTokenFetch",
"Effect": "Allow",
"Principal": {
"AWS": "arn:aws:iam::222222222222:role/CiPublisherRole"
},
"Action": [
"codeartifact:GetAuthorizationToken",
"codeartifact:DescribeDomain",
"sts:GetServiceBearerToken"
],
"Resource": "*"
}]
}'
Tokens carry a permission snapshot from the time they are issued. If you update a repository resource policy and then try to use an existing token to access the updated policy's new grants, the operation fails — the token's embedded permission set reflects the old policy. Fetch a new token after any resource policy update.
Pattern 6 — Cross-Account Architecture
CI in account A, artifact domain in account B
A common enterprise pattern runs CI (CodeBuild) in a dedicated "tools" account and holds the artifact domain in a separate "artifacts" account. Both accounts must cooperate for CodeArtifact access to work:
- Account B (artifacts): the domain resource policy must explicitly allow account A's CI role to call
codeartifact:GetAuthorizationTokenandsts:GetServiceBearerToken. The repository resource policy must allow the CI role tocodeartifact:PublishPackageVersion. - Account A (CI): the CodeBuild service role's IAM identity policy must grant all the CodeArtifact actions. Granting them only in account B's resource policy, without the identity policy in account A, results in access denied — this is the standard AWS cross-account "both sides required" rule.
// CDK Stack B (artifacts account): domain + repo with cross-account resource policies
const domain = new codeartifact.CfnDomain(this, "SharedDomain", {
domainName: "shared-artifacts",
permissionsPolicyDocument: {
Version: "2012-10-17",
Statement: [{
Sid: "AllowCiAccountTokenFetch",
Effect: "Allow",
Principal: { AWS: `arn:aws:iam::${ciAccountId}:root` },
Action: [
"codeartifact:GetAuthorizationToken",
"codeartifact:DescribeDomain",
"sts:GetServiceBearerToken",
],
Resource: "*",
}],
},
});
const repo = new codeartifact.CfnRepository(this, "SharedRepo", {
domainName: domain.domainName,
repositoryName: "mcp-packages",
permissionsPolicyDocument: {
Version: "2012-10-17",
Statement: [{
Sid: "AllowCiAccountPublish",
Effect: "Allow",
Principal: { AWS: `arn:aws:iam::${ciAccountId}:role/CiPublisherRole` },
Action: [
"codeartifact:ReadFromRepository",
"codeartifact:PublishPackageVersion",
"codeartifact:PutPackageMetadata",
"codeartifact:GetRepositoryEndpoint",
],
Resource: "*",
}],
},
});
repo.addDependency(domain);
// CDK Stack A (CI account): CodeBuild role — identity policy is required even with resource policy
const ciRole = new iam.Role(this, "CiRole", {
assumedBy: new iam.ServicePrincipal("codebuild.amazonaws.com"),
});
ciRole.addToPolicy(new iam.PolicyStatement({
actions: [
"codeartifact:GetAuthorizationToken",
"codeartifact:GetRepositoryEndpoint",
"codeartifact:ReadFromRepository",
"codeartifact:PublishPackageVersion",
"codeartifact:PutPackageMetadata",
"sts:GetServiceBearerToken",
],
resources: ["*"],
}));
Verifying effective permissions
# Check the current domain resource policy
aws codeartifact get-domain-permissions-policy \
--domain myorg --query policy | jq .
# Check the current repository resource policy
aws codeartifact get-repository-permissions-policy \
--domain myorg --repository mcp-servers --query policy | jq .
# Simulate a specific action using IAM policy simulator
aws iam simulate-principal-policy \
--policy-source-arn arn:aws:iam::123456789012:role/EcsTaskRole \
--action-names codeartifact:ReadFromRepository \
--resource-arns arn:aws:codeartifact:us-east-1:123456789012:repository/myorg/mcp-servers
# Quick end-to-end auth test: try to get a token and verify it is non-empty
aws codeartifact get-authorization-token \
--domain myorg --domain-owner 123456789012 \
--query authorizationToken --output text | head -c 20
AliveMCP Probe Worker: CodeArtifact in Production
AliveMCP's probe worker is a Node.js process running on ECS Fargate that pings every registered MCP endpoint every 60 seconds. It consumes internal utility packages distributed via CodeArtifact: a shared retry library (@alivemcp/retry-with-backoff), a protocol parser (@alivemcp/mcp-probe-parser), and a metrics emitter (@alivemcp/probe-metrics).
The ECS task role has the minimum consumer permissions: codeartifact:GetAuthorizationToken on the domain ARN, codeartifact:ReadFromRepository on the mcp-servers repo ARN, codeartifact:GetRepositoryEndpoint on *, and sts:GetServiceBearerToken on *. The container entrypoint script refreshes the CodeArtifact token before starting the probe loop:
#!/bin/sh
# entrypoint.sh: refresh token, install latest package versions, start probe
set -e
# Refresh the CodeArtifact token (12-hour lifetime; container restarts handle expiry)
aws codeartifact login \
--tool npm \
--domain myorg \
--domain-owner 123456789012 \
--repository mcp-servers \
--region us-east-1
# Install latest versions with --prefer-offline fallback
# If CodeArtifact is temporarily unavailable, the container starts with cached node_modules
npm install --prefer-offline --no-audit --no-fund
exec node dist/probe-worker.js
The --prefer-offline flag is the resilience mechanism: if the CodeArtifact endpoint is temporarily unavailable (service degradation, network partition), the container starts with whatever is in the existing node_modules rather than failing hard. This accepts a small risk of running a slightly stale package version in exchange for not having probe workers fail to start during AWS CodeArtifact maintenance windows. Since ECS Fargate restarts tasks every 24 hours for Fargate spot, the token also gets refreshed on every task restart, well within the 12-hour expiry window.
Aggregated Failure Modes
| Symptom | Root cause | Fix |
|---|---|---|
| "User is not authorized to perform: sts:GetServiceBearerToken" | sts:GetServiceBearerToken missing from identity policy; cannot be granted via resource policy | Add sts:GetServiceBearerToken with resource: "*" to the role's identity policy |
| "unable to authenticate, need: Basic realm" on npm | .npmrc auth token key uses https://host/ format instead of //host/ (with scheme prefix) | Remove the https: prefix — key must start with // |
| "npm ERR! 401 Unauthorized" after working for days | 12-hour token expired; CI job queued beyond the expiry window | Always refresh the token in pre_build; never pre-populate tokens in SSM |
"npm ERR! 404" for @myorg/mcp-server-* | @myorg:registry= scope mapping missing from .npmrc; npm looks in public registry | Add @myorg:registry=https://<endpoint>/ to .npmrc |
| "PackageVersionAlreadyExists" on npm publish | Version in package.json already published; CodeArtifact is immutable per version | Run npm version patch --no-git-tag-version before every npm publish |
| pip returns 404 for all packages including public (httpx, requests) | No PyPI upstream connection; CodeArtifact will not proxy to public PyPI without configuration | Create a pypi-upstream repo with public:pypi external connection and associate as upstream |
| pip succeeds but twine upload gets 403 | --tool pip login was called but not --tool twine; they write different config files | Run both aws codeartifact login --tool pip and --tool twine in pre_build |
| pip.conf index-url causes 404 during package discovery | URL missing trailing /simple/; CodeArtifact PyPI endpoint requires it | Ensure URL ends with /simple/; the CLI login helper adds it automatically |
| "HTTPError: 400 Bad Request — File already exists" on twine | Python package version already published; versions are immutable | Bump version in pyproject.toml; use bump2version patch before rebuilding |
| "is not authorized to perform codeartifact:GetAuthorizationToken on resource arn:...:repository/..." | GetAuthorizationToken resource scoped to a repo ARN; this action requires a domain ARN | Change the resource for GetAuthorizationToken to the domain ARN |
| Cross-account access denied despite resource policy allowing it | Identity policy in the calling account does not grant the CodeArtifact actions | Add CodeArtifact actions to the calling role's identity policy; both sides required |
| "ResourceNotFoundException: domain not found" on repository create | CLI targeting a different account or region than the domain; or CDK creating repo before domain | Pass --domain-owner <account-id> to every CLI command; in CDK add addDependency(domain) |
| 403 after repository policy update with existing token | Tokens carry a permission snapshot from issuance time; old token reflects old policy | Fetch a new token after any resource policy change |
Docker build "401 Unauthorized" for @myorg/ packages | Dockerfile does not inject the CodeArtifact token before npm ci | Pass token as build arg, write to .npmrc in the deps stage, remove before final stage |