AWS CodeArtifact: Private Package Repository for Maven, npm and pip
Published June 2026 · 18 min read
Every engineering team eventually accumulates internal libraries — shared utilities, design-system components, company-approved data-access layers — that must be distributed across dozens of services without publishing them to the public internet. AWS CodeArtifact is a fully managed artifact repository that stores Maven, Gradle, npm, PyPI, NuGet, Swift, and generic packages, integrates natively with IAM, and can act as a caching proxy in front of Maven Central, npmjs.com, and PyPI simultaneously. This guide covers everything from domain setup and package-manager configuration through cross-account sharing, CI/CD token refresh, and compliance controls.
Table of Contents
- CodeArtifact vs Nexus vs Artifactory vs JFrog
- Domain and Repository Setup
- Maven and Gradle Configuration
- npm Configuration and Scoped Packages
- pip / Python and Poetry Integration
- Upstream Repository Chaining
- Cross-Account Access
- CI/CD Integration — GitHub Actions and CodeBuild
- Package Approval Workflow
- Security, Compliance, and Cost Optimization
1. CodeArtifact vs Nexus vs Artifactory vs JFrog
Before adopting CodeArtifact it is worth understanding how it compares to the market-leading self-managed and SaaS alternatives. The decision often comes down to operational overhead tolerance, AWS integration depth, and total cost of ownership.
| Feature | AWS CodeArtifact | Sonatype Nexus OSS | JFrog Artifactory Cloud | GitHub Packages |
|---|---|---|---|---|
| Hosting model | Fully managed SaaS | Self-hosted (EC2/ECS) | SaaS or self-hosted | SaaS (GitHub) |
| IAM authentication | Native (STS token) | Manual config | SAML / OIDC add-on | GitHub token only |
| Upstream proxy | Maven Central, npm, PyPI, NuGet | Yes (all) | Yes (all) | npm only |
| Cross-account sharing | Resource policy (JSON) | LDAP / manual | Paid feature | GitHub Org only |
| Storage cost | $0.05/GB/month | EC2 + EBS cost | ~$0.08/GB/month | $0.008/GB/month |
| Request cost | $0.05 per 10K requests | 0 (self-hosted) | Metered | Free (GitHub Actions) |
| Package formats | Maven, npm, PyPI, NuGet, Swift, Generic | All major formats | All major formats | npm, Maven, NuGet, RubyGems, Docker |
| Ops overhead | Zero | High (patching, HA) | Low (SaaS tier) | Zero |
| VPC endpoint | Yes | N/A | No | No |
JFrog Artifactory remains the better choice when you need multi-cloud artifact federation, native Docker registry features beyond ECR's capabilities, or XRay vulnerability scanning tightly integrated into the repository itself. Nexus OSS wins purely on zero cost when you are already paying for EC2 and want full format support with no per-request charges.
CodeArtifact Core Concepts
- Domain — A logical grouping of repositories, tied to one AWS account. All repositories in a domain share the same KMS encryption key and cross-account policy. A domain spans regions.
- Repository — Stores packages for one format (e.g., a Maven repo cannot hold npm packages). A repository can have upstream repositories forming a chain.
- Upstream repository — A repository that CodeArtifact queries when a package is not found locally. You can chain: your-team-repo → your-org-repo → maven-central-proxy.
- Package origin control — Per-package policy that either allows or blocks packages whose name matches a public registry package, preventing dependency confusion attacks.
- Authorization token — A 12-hour STS-backed token fetched via
aws codeartifact get-authorization-token. Package managers use this as a Bearer token or Basic Auth password.
2. Domain and Repository Setup
You can create a domain and repositories via the AWS CLI, Terraform, or CloudFormation. The CLI is the fastest for initial exploration; Terraform is recommended for production because it gives you version-controlled, reproducible infrastructure.
CLI Quickstart
# Create a domain (once per AWS account / organisation)
aws codeartifact create-domain \
--domain techoral \
--encryption-key arn:aws:kms:us-east-1:123456789012:key/abcd-1234
# Create a Maven Central upstream proxy repository
aws codeartifact create-repository \
--domain techoral \
--repository maven-central-proxy \
--description "Proxy for Maven Central"
# Associate the public upstream
aws codeartifact associate-external-connection \
--domain techoral \
--repository maven-central-proxy \
--external-connection public:maven-central
# Create your team's internal Maven repository
aws codeartifact create-repository \
--domain techoral \
--repository techoral-maven \
--description "Internal Maven artifacts" \
--upstreams repositoryName=maven-central-proxy
# Similarly for npm
aws codeartifact create-repository \
--domain techoral \
--repository npmjs-proxy \
--description "Proxy for npmjs.com"
aws codeartifact associate-external-connection \
--domain techoral \
--repository npmjs-proxy \
--external-connection public:npmjs
aws codeartifact create-repository \
--domain techoral \
--repository techoral-npm \
--description "Internal npm packages" \
--upstreams repositoryName=npmjs-proxy
# And for PyPI
aws codeartifact create-repository \
--domain techoral \
--repository pypi-proxy \
--description "Proxy for pypi.org"
aws codeartifact associate-external-connection \
--domain techoral \
--repository pypi-proxy \
--external-connection public:pypi
aws codeartifact create-repository \
--domain techoral \
--repository techoral-pypi \
--description "Internal Python packages" \
--upstreams repositoryName=pypi-proxy
Terraform
For repeatable infrastructure, define your CodeArtifact resources in Terraform. The following module creates the full domain-plus-repository hierarchy:
resource "aws_codeartifact_domain" "techoral" {
domain = "techoral"
encryption_key = aws_kms_key.codeartifact.arn
tags = {
Environment = "production"
ManagedBy = "terraform"
}
}
resource "aws_kms_key" "codeartifact" {
description = "CodeArtifact domain encryption key"
deletion_window_in_days = 14
enable_key_rotation = true
}
# Maven Central proxy
resource "aws_codeartifact_repository" "maven_central_proxy" {
repository = "maven-central-proxy"
domain = aws_codeartifact_domain.techoral.domain
external_connections {
external_connection_name = "public:maven-central"
}
}
# Internal Maven repo with upstream
resource "aws_codeartifact_repository" "techoral_maven" {
repository = "techoral-maven"
domain = aws_codeartifact_domain.techoral.domain
description = "Internal Maven artifacts"
upstream {
repository_name = aws_codeartifact_repository.maven_central_proxy.repository
}
}
# npm proxy
resource "aws_codeartifact_repository" "npmjs_proxy" {
repository = "npmjs-proxy"
domain = aws_codeartifact_domain.techoral.domain
external_connections {
external_connection_name = "public:npmjs"
}
}
resource "aws_codeartifact_repository" "techoral_npm" {
repository = "techoral-npm"
domain = aws_codeartifact_domain.techoral.domain
upstream {
repository_name = aws_codeartifact_repository.npmjs_proxy.repository
}
}
# PyPI proxy
resource "aws_codeartifact_repository" "pypi_proxy" {
repository = "pypi-proxy"
domain = aws_codeartifact_domain.techoral.domain
external_connections {
external_connection_name = "public:pypi"
}
}
resource "aws_codeartifact_repository" "techoral_pypi" {
repository = "techoral-pypi"
domain = aws_codeartifact_domain.techoral.domain
upstream {
repository_name = aws_codeartifact_repository.pypi_proxy.repository
}
}
deletion_window_in_days to at least 14 days and enable key rotation.
Fetching an Authorization Token
# Token is valid for 12 hours; store it as an env var
export CODEARTIFACT_TOKEN=$(aws codeartifact get-authorization-token \
--domain techoral \
--domain-owner 123456789012 \
--query authorizationToken \
--output text)
# Get the endpoint URL for a specific repository + format
aws codeartifact get-repository-endpoint \
--domain techoral \
--domain-owner 123456789012 \
--repository techoral-maven \
--format maven
# Output: https://techoral-123456789012.d.codeartifact.us-east-1.amazonaws.com/maven/techoral-maven/
3. Maven and Gradle Configuration
Maven authenticates to CodeArtifact using the authorization token as the Basic Auth password. The token is passed through Maven's settings.xml, keeping credentials out of your pom.xml.
settings.xml
<!-- ~/.m2/settings.xml -->
<settings xmlns="http://maven.apache.org/SETTINGS/1.0.0">
<servers>
<server>
<id>techoral-maven</id>
<username>aws</username>
<!-- Use the env var set by: export CODEARTIFACT_TOKEN=$(aws codeartifact get-authorization-token ...) -->
<password>${env.CODEARTIFACT_TOKEN}</password>
</server>
<server>
<id>techoral-maven-snapshots</id>
<username>aws</username>
<password>${env.CODEARTIFACT_TOKEN}</password>
</server>
</servers>
<profiles>
<profile>
<id>codeartifact</id>
<repositories>
<repository>
<id>techoral-maven</id>
<url>https://techoral-123456789012.d.codeartifact.us-east-1.amazonaws.com/maven/techoral-maven/</url>
<releases><enabled>true</enabled></releases>
<snapshots><enabled>true</enabled></snapshots>
</repository>
</repositories>
<pluginRepositories>
<pluginRepository>
<id>techoral-maven</id>
<url>https://techoral-123456789012.d.codeartifact.us-east-1.amazonaws.com/maven/techoral-maven/</url>
</pluginRepository>
</pluginRepositories>
</profile>
</profiles>
<activeProfiles>
<activeProfile>codeartifact</activeProfile>
</activeProfiles>
</settings>
Publishing to CodeArtifact from pom.xml
<!-- pom.xml distributionManagement -->
<distributionManagement>
<repository>
<id>techoral-maven</id>
<name>Techoral Maven Releases</name>
<url>https://techoral-123456789012.d.codeartifact.us-east-1.amazonaws.com/maven/techoral-maven/</url>
</repository>
<snapshotRepository>
<id>techoral-maven-snapshots</id>
<name>Techoral Maven Snapshots</name>
<url>https://techoral-123456789012.d.codeartifact.us-east-1.amazonaws.com/maven/techoral-maven/</url>
</snapshotRepository>
</distributionManagement>
# Publish the artifact
export CODEARTIFACT_TOKEN=$(aws codeartifact get-authorization-token \
--domain techoral --domain-owner 123456789012 \
--query authorizationToken --output text)
mvn deploy
Gradle (Kotlin DSL)
// build.gradle.kts
val codeArtifactToken: String by lazy {
System.getenv("CODEARTIFACT_TOKEN")
?: error("CODEARTIFACT_TOKEN env var not set")
}
val codeArtifactUrl = "https://techoral-123456789012.d.codeartifact.us-east-1.amazonaws.com/maven/techoral-maven/"
repositories {
maven {
url = uri(codeArtifactUrl)
credentials {
username = "aws"
password = codeArtifactToken
}
}
}
publishing {
repositories {
maven {
url = uri(codeArtifactUrl)
credentials {
username = "aws"
password = codeArtifactToken
}
}
}
publications {
create<MavenPublication>("mavenJava") {
from(components["java"])
}
}
}
4. npm Configuration and Scoped Packages
npm uses a .npmrc file for registry configuration. The CodeArtifact login helper writes the correct .npmrc automatically, or you can manage it manually for fine-grained control over scoped packages.
.npmrc Configuration
; .npmrc — commit this to your repo (without the token line)
; The token line is written at build time by the login command
@techoral:registry=https://techoral-123456789012.d.codeartifact.us-east-1.amazonaws.com/npm/techoral-npm/
//techoral-123456789012.d.codeartifact.us-east-1.amazonaws.com/npm/techoral-npm/:always-auth=true
//techoral-123456789012.d.codeartifact.us-east-1.amazonaws.com/npm/techoral-npm/:_authToken=${CODEARTIFACT_TOKEN}
Login via CLI Helper
# One-liner: fetch token and write .npmrc
aws codeartifact login \
--tool npm \
--domain techoral \
--domain-owner 123456789012 \
--repository techoral-npm \
--region us-east-1
# This writes two lines to ~/.npmrc (or project .npmrc):
# registry=https://...
# //...:_authToken=TOKEN
Publishing Scoped Packages
// package.json — your internal library
{
"name": "@techoral/shared-utils",
"version": "1.4.2",
"description": "Shared utility functions",
"main": "dist/index.js",
"types": "dist/index.d.ts",
"publishConfig": {
"registry": "https://techoral-123456789012.d.codeartifact.us-east-1.amazonaws.com/npm/techoral-npm/"
},
"scripts": {
"build": "tsc",
"prepublishOnly": "npm run build"
}
}
# Publish
aws codeartifact login --tool npm --domain techoral \
--domain-owner 123456789012 --repository techoral-npm
npm publish
Consuming Scoped Packages in Other Projects
# Install scoped package — npm resolves @techoral/* from CodeArtifact
npm install @techoral/shared-utils
# Install a public package — resolved via upstream npmjs-proxy and cached
npm install lodash
@techoral/). This prevents dependency confusion attacks where a malicious actor publishes a package with the same name to the public npmjs registry. You can enforce this with CodeArtifact's package origin control — see Section 10.
5. pip / Python and Poetry Integration
Python tooling uses pip.conf for installation and Twine for publishing. Poetry has its own repository configuration that slots in cleanly with CodeArtifact tokens.
pip.conf
# ~/.pip/pip.conf (Linux/macOS)
# or %APPDATA%\pip\pip.ini (Windows)
[global]
index-url = https://aws:TOKEN@techoral-123456789012.d.codeartifact.us-east-1.amazonaws.com/pypi/techoral-pypi/simple/
# Better: use the login helper to configure pip automatically
aws codeartifact login \
--tool pip \
--domain techoral \
--domain-owner 123456789012 \
--repository techoral-pypi
# Then install normally — public packages served from PyPI cache
pip install requests boto3 pydantic
# Install internal package
pip install techoral-core
requirements.txt from CodeArtifact
# requirements.txt
# Pin public packages normally — pulled from PyPI cache via upstream
requests==2.32.3
boto3==1.34.0
pydantic==2.7.1
# Internal package from CodeArtifact
techoral-core==2.1.0
techoral-auth-middleware==1.0.5
Publishing with Twine
# Build your distribution
python -m build
# Get token and publish with Twine
export CODEARTIFACT_TOKEN=$(aws codeartifact get-authorization-token \
--domain techoral --domain-owner 123456789012 \
--query authorizationToken --output text)
twine upload \
--repository-url https://techoral-123456789012.d.codeartifact.us-east-1.amazonaws.com/pypi/techoral-pypi/ \
--username aws \
--password $CODEARTIFACT_TOKEN \
dist/*
Poetry Configuration
# Add CodeArtifact as a Poetry source
export CODEARTIFACT_TOKEN=$(aws codeartifact get-authorization-token \
--domain techoral --domain-owner 123456789012 \
--query authorizationToken --output text)
poetry config repositories.techoral \
https://techoral-123456789012.d.codeartifact.us-east-1.amazonaws.com/pypi/techoral-pypi/simple/
poetry config http-basic.techoral aws $CODEARTIFACT_TOKEN
# pyproject.toml — add source to pull from CodeArtifact
[[tool.poetry.source]]
name = "techoral"
url = "https://techoral-123456789012.d.codeartifact.us-east-1.amazonaws.com/pypi/techoral-pypi/simple/"
priority = "primary"
[tool.poetry.dependencies]
python = "^3.12"
techoral-core = "^2.1"
requests = "^2.32"
6. Upstream Repository Chaining
CodeArtifact's upstream feature is what makes it a true enterprise artifact proxy, not just a private storage bucket. When a package version is requested, CodeArtifact searches repositories in order: the requested repository first, then each upstream in the configured order, and finally any external connections attached to those upstreams.
Resolution Order
Request: GET com.google.guava:guava:33.2.0
CodeArtifact resolution walk:
1. techoral-maven (team repo) → not found
2. platform-maven (org shared repo) → not found
3. maven-central-proxy (external proxy) → found on Maven Central
→ downloaded, stored in maven-central-proxy cache
→ returned to caller
Next request for same version:
1. techoral-maven → not found
2. platform-maven → not found
3. maven-central-proxy → FOUND in cache (no external network call)
Package Version Pinning
To pin a specific version in CodeArtifact so that it cannot be updated by an upstream, use the DispositionAction policy. More commonly, teams use CodeArtifact's package group policy to block all versions above a certain threshold:
# List all cached versions of a package in the proxy
aws codeartifact list-package-versions \
--domain techoral \
--repository maven-central-proxy \
--format maven \
--namespace com.google.guava \
--package guava
# Mark a version as blocked so it cannot be installed (see Section 9 for full approval flow)
aws codeartifact update-package-versions-status \
--domain techoral \
--repository techoral-maven \
--format maven \
--namespace com.example \
--package vulnerable-lib \
--versions 1.2.3 \
--target-status Archived
Multi-Team Repository Topology
For larger organisations, the recommended topology creates one proxy per public registry per region, a shared organisation-level repository, and team-level repositories as leaves:
┌─────────────────────────┐
│ public: maven-central │ (external connection)
└────────────┬────────────┘
│
┌────────────▼────────────┐
│ maven-central-proxy │ (regional cache)
└────────────┬────────────┘
│ upstream
┌────────────▼────────────┐
│ platform-maven │ (org-wide shared libs)
└─────┬──────────┬────────┘
upstream │ │ upstream
┌──────────────▼─┐ ┌───▼──────────────┐
│ team-a-maven │ │ team-b-maven │
└────────────────┘ └───────────────────┘
7. Cross-Account Access
A common enterprise pattern is to host the CodeArtifact domain in a central DevOps account and grant access to multiple workload accounts — dev, staging, production — and potentially multiple AWS Organizations member accounts. CodeArtifact supports this through resource-based policies on domains and repositories.
Domain Policy — Share with Entire Organisation
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "AllowOrgReadAccess",
"Effect": "Allow",
"Principal": "*",
"Action": [
"codeartifact:GetAuthorizationToken",
"codeartifact:GetRepositoryEndpoint",
"codeartifact:ReadFromRepository"
],
"Resource": "*",
"Condition": {
"StringEquals": {
"aws:PrincipalOrgID": "o-exampleorgid111"
}
}
},
{
"Sid": "AllowStsForOrg",
"Effect": "Allow",
"Principal": "*",
"Action": "sts:GetServiceBearerToken",
"Resource": "*",
"Condition": {
"StringEquals": {
"aws:PrincipalOrgID": "o-exampleorgid111"
},
"Bool": {
"aws:MultiFactorAuthPresent": "false"
}
}
}
]
}
# Apply domain policy
aws codeartifact put-domain-permissions-policy \
--domain techoral \
--domain-owner 123456789012 \
--policy-document file://domain-policy.json
Repository Policy — Specific Accounts Only
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "AllowDevAndStagingAccounts",
"Effect": "Allow",
"Principal": {
"AWS": [
"arn:aws:iam::111111111111:root",
"arn:aws:iam::222222222222:root"
]
},
"Action": [
"codeartifact:ReadFromRepository",
"codeartifact:GetRepositoryEndpoint",
"codeartifact:ListPackages",
"codeartifact:ListPackageVersions",
"codeartifact:DescribePackageVersion"
],
"Resource": "*"
},
{
"Sid": "AllowPublishFromCIAccount",
"Effect": "Allow",
"Principal": {
"AWS": "arn:aws:iam::333333333333:role/ci-publisher-role"
},
"Action": [
"codeartifact:PublishPackageVersion",
"codeartifact:PutPackageMetadata"
],
"Resource": "*"
}
]
}
aws codeartifact put-repository-permissions-policy \
--domain techoral \
--domain-owner 123456789012 \
--repository techoral-maven \
--policy-document file://repo-policy.json
get-authorization-token, they must specify both --domain and --domain-owner 123456789012 (the domain owner's account ID). Without --domain-owner the CLI defaults to the caller's account and will return a "domain not found" error.
8. CI/CD Integration — GitHub Actions and CodeBuild
The CodeArtifact authorization token expires after 12 hours. In CI/CD pipelines, you must refresh the token at the start of each build. With GitHub Actions using OIDC to assume an IAM role, this can be done without storing any long-lived AWS credentials as GitHub secrets.
GitHub Actions with OIDC
# .github/workflows/build-and-publish.yml
name: Build and Publish
on:
push:
branches: [main]
pull_request:
branches: [main]
permissions:
id-token: write # Required for OIDC token exchange
contents: read
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Configure AWS credentials via OIDC
uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: arn:aws:iam::123456789012:role/github-actions-codeartifact-role
aws-region: us-east-1
- name: Get CodeArtifact token and configure Maven
run: |
export CODEARTIFACT_TOKEN=$(aws codeartifact get-authorization-token \
--domain techoral \
--domain-owner 123456789012 \
--query authorizationToken \
--output text)
echo "CODEARTIFACT_TOKEN=${CODEARTIFACT_TOKEN}" >> $GITHUB_ENV
# Configure Maven settings.xml
mkdir -p ~/.m2
cat > ~/.m2/settings.xml <<EOF
<settings>
<servers>
<server>
<id>techoral-maven</id>
<username>aws</username>
<password>${CODEARTIFACT_TOKEN}</password>
</server>
</servers>
</settings>
EOF
- name: Set up JDK 21
uses: actions/setup-java@v4
with:
java-version: '21'
distribution: 'corretto'
- name: Build with Maven
run: mvn --batch-mode clean package
- name: Publish artifact on main
if: github.ref == 'refs/heads/main'
run: mvn --batch-mode deploy -DskipTests
IAM Role for GitHub Actions OIDC
resource "aws_iam_role" "github_actions_codeartifact" {
name = "github-actions-codeartifact-role"
assume_role_policy = jsonencode({
Version = "2012-10-17"
Statement = [{
Effect = "Allow"
Principal = {
Federated = "arn:aws:iam::123456789012:oidc-provider/token.actions.githubusercontent.com"
}
Action = "sts:AssumeRoleWithWebIdentity"
Condition = {
StringEquals = {
"token.actions.githubusercontent.com:aud" = "sts.amazonaws.com"
"token.actions.githubusercontent.com:sub" = "repo:techoral/myapp:ref:refs/heads/main"
}
}
}]
})
}
resource "aws_iam_role_policy" "github_codeartifact" {
role = aws_iam_role.github_actions_codeartifact.id
policy = jsonencode({
Version = "2012-10-17"
Statement = [
{
Effect = "Allow"
Action = "codeartifact:GetAuthorizationToken"
Resource = "arn:aws:codeartifact:us-east-1:123456789012:domain/techoral"
},
{
Effect = "Allow"
Action = "sts:GetServiceBearerToken"
Resource = "*"
Condition = {
StringEquals = {
"sts:AWSServiceName" = "codeartifact.amazonaws.com"
}
}
},
{
Effect = "Allow"
Action = [
"codeartifact:GetRepositoryEndpoint",
"codeartifact:ReadFromRepository",
"codeartifact:PublishPackageVersion",
"codeartifact:PutPackageMetadata"
]
Resource = [
"arn:aws:codeartifact:us-east-1:123456789012:repository/techoral/techoral-maven",
"arn:aws:codeartifact:us-east-1:123456789012:package/techoral/techoral-maven/*/*/*"
]
}
]
})
}
CodeBuild buildspec.yml
# buildspec.yml — CodeBuild with CodeArtifact
version: 0.2
phases:
install:
runtime-versions:
java: corretto21
commands:
# Refresh CodeArtifact token (CodeBuild assumes the project's service role)
- export CODEARTIFACT_TOKEN=$(aws codeartifact get-authorization-token
--domain techoral
--domain-owner 123456789012
--query authorizationToken
--output text)
# Configure Maven
- |
mkdir -p ~/.m2
cat > ~/.m2/settings.xml <<EOF
<settings>
<servers>
<server>
<id>techoral-maven</id>
<username>aws</username>
<password>${CODEARTIFACT_TOKEN}</password>
</server>
</servers>
</settings>
EOF
pre_build:
commands:
- echo "Build started on $(date)"
- mvn dependency:resolve
build:
commands:
- mvn clean package
post_build:
commands:
- mvn deploy -DskipTests
artifacts:
files:
- target/*.jar
discard-paths: yes
cache:
paths:
- /root/.m2/**/*
9. Package Approval Workflow
For regulated environments, you need a gate that prevents developers from installing a newly published internal package until it has been reviewed. CodeArtifact's package version status — Published, Unfinished, Unlisted, Archived, or Disposed — combined with EventBridge and Lambda provides an approval workflow without any third-party tooling.
EventBridge Rule — Catch New Package Versions
{
"source": ["aws.codeartifact"],
"detail-type": ["CodeArtifact Package Version State Change"],
"detail": {
"domainName": ["techoral"],
"repositoryName": ["techoral-maven"],
"packageVersionState": ["Published"]
}
}
# Create the EventBridge rule
aws events put-rule \
--name codeartifact-new-package-version \
--event-pattern file://event-pattern.json \
--state ENABLED
# Attach the Lambda approval function as target
aws events put-targets \
--rule codeartifact-new-package-version \
--targets "Id=ApprovalLambda,Arn=arn:aws:lambda:us-east-1:123456789012:function:package-approval-gate"
Lambda Approval Gate
import boto3
import json
codeartifact = boto3.client('codeartifact')
sns = boto3.client('sns')
DOMAIN = 'techoral'
APPROVED_NAMESPACES = ['com.techoral', 'io.techoral']
APPROVAL_TOPIC_ARN = 'arn:aws:sns:us-east-1:123456789012:package-approval-required'
def lambda_handler(event, context):
detail = event['detail']
namespace = detail.get('packageNamespace', '')
package = detail['packageName']
version = detail['packageVersion']
fmt = detail['packageFormat']
repo = detail['repositoryName']
# Auto-approve known internal namespaces — send for review otherwise
if namespace in APPROVED_NAMESPACES:
print(f"Auto-approving {namespace}:{package}:{version}")
return # stays Published
# Block the package by setting it Unlisted pending review
codeartifact.update_package_versions_status(
domain=DOMAIN,
repository=repo,
format=fmt,
namespace=namespace,
package=package,
versions=[version],
targetStatus='Unlisted'
)
# Notify security team for manual review
sns.publish(
TopicArn=APPROVAL_TOPIC_ARN,
Subject=f'Package review required: {package}:{version}',
Message=json.dumps({
'package': f'{namespace}:{package}',
'version': version,
'repository': repo,
'format': fmt,
'action': 'Set to Unlisted — approve by running update-package-versions-status to Published'
}, indent=2)
)
print(f"Blocked {namespace}:{package}:{version} pending review")
Unlisted can still be installed if the caller knows the exact version. To fully block installation, set the status to Archived. Only set Disposed if you want permanent deletion — disposed package versions cannot be restored.
10. Security, Compliance, and Cost Optimization
CodeArtifact integrates with CloudTrail, KMS, IAM, and VPC endpoints, making it a strong fit for PCI-DSS, SOC 2, and HIPAA compliance frameworks. The following practices lock down your artifact supply chain end-to-end.
Package Origin Control (Dependency Confusion Defence)
A dependency confusion attack occurs when a public package is published with the same name as your internal package — build tools may inadvertently resolve the malicious public version. CodeArtifact's package origin control blocks this at the repository level:
# Block external versions of a specific internal package
# RESTRICT means: only versions published directly to THIS repo are allowed
aws codeartifact put-package-origin-configuration \
--domain techoral \
--repository techoral-npm \
--format npm \
--package "@techoral/shared-utils" \
--restrictions publish=ALLOW,upstream=BLOCK
# For Maven
aws codeartifact put-package-origin-configuration \
--domain techoral \
--repository techoral-maven \
--format maven \
--namespace com.techoral \
--package internal-api-client \
--restrictions publish=ALLOW,upstream=BLOCK
CloudTrail Audit Log
# Query CloudTrail for all package downloads in the last 24h
aws cloudtrail lookup-events \
--lookup-attributes AttributeKey=EventSource,AttributeValue=codeartifact.amazonaws.com \
--start-time $(date -u -d '24 hours ago' +%Y-%m-%dT%H:%M:%SZ) \
--query 'Events[?EventName==`GetPackageVersionAsset`].{Time:EventTime,User:Username,Package:CloudTrailEvent}' \
--output table
VPC Endpoint for Private Access
# Terraform — VPC endpoint for CodeArtifact (no internet egress needed)
resource "aws_vpc_endpoint" "codeartifact_api" {
vpc_id = aws_vpc.main.id
service_name = "com.amazonaws.us-east-1.codeartifact.api"
vpc_endpoint_type = "Interface"
subnet_ids = aws_subnet.private[*].id
security_group_ids = [aws_security_group.codeartifact_endpoint.id]
private_dns_enabled = true
}
resource "aws_vpc_endpoint" "codeartifact_repositories" {
vpc_id = aws_vpc.main.id
service_name = "com.amazonaws.us-east-1.codeartifact.repositories"
vpc_endpoint_type = "Interface"
subnet_ids = aws_subnet.private[*].id
security_group_ids = [aws_security_group.codeartifact_endpoint.id]
private_dns_enabled = true
}
Cost Optimization
- Storage: CodeArtifact charges $0.05/GB/month. Run
aws codeartifact list-packagesperiodically and dispose of packages with zero download activity in the past 90 days. - Request cost: $0.05 per 10,000 requests. The upstream caching feature eliminates repeated calls to Maven Central / PyPI — a cached package has zero additional cost after the first fetch.
- Token calls: Each
get-authorization-tokencall is a billable API request. In CodeBuild, fetch the token once in theinstallphase and pass it as an environment variable to all subsequent phases rather than calling it multiple times per build. - Retained versions: Set a retention policy for SNAPSHOT / pre-release versions in your Maven repository. Keeping thousands of
1.0-SNAPSHOTbuilds is expensive and rarely necessary; keep only the last 10. - External connections: Each external connection (public:maven-central, public:npmjs, etc.) has a cost per request when fetching uncached packages. Consolidate public proxies at the org level so multiple teams share one cache rather than each team having their own proxy repo making separate upstream calls.
PackageVersionStateChange event to trigger a scan Lambda that calls Inspector or Snyk APIs and automatically archives a version if critical CVEs are found.
Summary: CodeArtifact Production Checklist
- Customer-managed KMS key on the domain with key rotation enabled
- VPC endpoints deployed in build subnets — no package traffic over the public internet
- Package origin control configured for all internal package namespaces
- Domain policy scoped to AWS Organisation ID — no hardcoded account IDs in policy
- Token refresh in every CI/CD pipeline — never embed or cache tokens beyond a single build
- EventBridge + Lambda approval gate for repositories used by production workloads
- CloudTrail trail with S3 destination for long-term audit retention
- Monthly cost review: dispose of stale SNAPSHOT versions, consolidate proxy repositories
Frequently Asked Questions
Does CodeArtifact support Docker images?
No. CodeArtifact does not support OCI / Docker image format. Use Amazon ECR for container image storage. ECR and CodeArtifact complement each other in the same CI/CD pipeline: CodeArtifact holds your Maven/npm dependencies and published JARs; ECR holds the built container images. See the AWS ECR Guide for image lifecycle policies and cross-account ECR access.
How do I migrate from Nexus to CodeArtifact?
Use the aws codeartifact publish-package-version CLI command or the REST upload API to push existing artifacts. For Maven, copy the local repository directory (~/.m2/repository) and iterate over JARs/POMs. For npm, use npm publish --registry pointing at CodeArtifact after logging in. Most teams migrate one repository format at a time and run Nexus in parallel (both as upstreams) during the transition period.
Can I use CodeArtifact without the CLI token refresh?
For read-only access inside a CodeBuild project or ECS task using an IAM role, you can use the aws codeartifact login helper which calls get-authorization-token internally. However, if your build runs longer than 12 hours you must re-call login mid-build. There is no persistent credential option — the 12-hour token limit is a security design constraint, not a configuration option.
What happens if Maven Central is down?
If a package version has already been fetched once through the CodeArtifact upstream connection, it is cached permanently in your proxy repository and will be served even when Maven Central is unreachable. First-time requests for uncached versions will fail with a 503 while the upstream is unavailable. This is why it is important to pre-warm your proxy by running a full build early in the migration before decommissioning direct public registry access.