AWS CodeArtifact: Private Package Repository for Maven, npm and pip

AWS CodeArtifact Private Package Repository

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

  1. CodeArtifact vs Nexus vs Artifactory vs JFrog
  2. Domain and Repository Setup
  3. Maven and Gradle Configuration
  4. npm Configuration and Scoped Packages
  5. pip / Python and Poetry Integration
  6. Upstream Repository Chaining
  7. Cross-Account Access
  8. CI/CD Integration — GitHub Actions and CodeBuild
  9. Package Approval Workflow
  10. 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.

FeatureAWS CodeArtifactSonatype Nexus OSSJFrog Artifactory CloudGitHub Packages
Hosting modelFully managed SaaSSelf-hosted (EC2/ECS)SaaS or self-hostedSaaS (GitHub)
IAM authenticationNative (STS token)Manual configSAML / OIDC add-onGitHub token only
Upstream proxyMaven Central, npm, PyPI, NuGetYes (all)Yes (all)npm only
Cross-account sharingResource policy (JSON)LDAP / manualPaid featureGitHub Org only
Storage cost$0.05/GB/monthEC2 + EBS cost~$0.08/GB/month$0.008/GB/month
Request cost$0.05 per 10K requests0 (self-hosted)MeteredFree (GitHub Actions)
Package formatsMaven, npm, PyPI, NuGet, Swift, GenericAll major formatsAll major formatsnpm, Maven, NuGet, RubyGems, Docker
Ops overheadZeroHigh (patching, HA)Low (SaaS tier)Zero
VPC endpointYesN/ANoNo
When to choose CodeArtifact: If your build infrastructure already runs on AWS — CodeBuild, GitHub Actions with OIDC to IAM, ECS — CodeArtifact eliminates the need to manage credentials separately. IAM role assumption replaces static API keys, and the same CloudTrail audit trail covers both package access and infrastructure changes. For AWS-native shops it is almost always the right call over operating a Nexus EC2 cluster.

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
  }
}
KMS key tip: All repositories in a domain share the same KMS key. If you delete the domain's KMS key, all packages in all repositories in that domain become permanently inaccessible. Always set 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
Scoped package strategy: Always scope your internal packages under a company-owned namespace (e.g., @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)
Caching behaviour: Once a package version is fetched through an external connection, CodeArtifact caches it permanently in the proxy repository. Even if Maven Central, npmjs.com, or PyPI experience an outage, your cached packages remain available. This is a significant reliability improvement over tools that always fetch from the public registry at build time.

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
Cross-account token note: When a principal in account 111111111111 calls 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")
Blocking UNLISTED packages: A package with status 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-packages periodically 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-token call is a billable API request. In CodeBuild, fetch the token once in the install phase 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-SNAPSHOT builds 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.
Vulnerability scanning integration: CodeArtifact does not perform vulnerability scanning natively. Integrate with Amazon Inspector (container images) or a third-party tool like Snyk or OWASP Dependency-Check in your CodeBuild pipeline. Use EventBridge on the 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.