Every CI/CD pipeline on the AWS Certified DevOps Engineer - Professional (DOP-C02) exam has a quiet dependency problem. The build stage runs npm install, pip install, or mvn package, and in that moment the build reaches out to a public registry on the internet, pulls whatever version resolves today, and bakes it into the artifact you are about to deploy. That is a supply-chain risk, an availability risk, and a reproducibility risk, and the exam increasingly asks you to close it.
AWS CodeArtifact is the managed package repository that answers those questions. It is small enough that most study guides give it one bullet (“stores npm, pip, Maven and NuGet packages”) and large enough that the exam can ask about its internals: what a domain actually is, where a package lands when a downstream repository pulls it through two upstreams and an external connection, why a build in an assumed role can keep a token longer than the role session, and how origin controls stop a dependency substitution attack. This guide covers those mechanisms at the depth the exam tests them.
CodeArtifact lives in Domain 1 (SDLC Automation) of the exam guide under artifact management, and it bleeds into Domain 5 (Security and Compliance) whenever a scenario mentions “private packages”, “approved dependencies”, or “dependency confusion”. If you have not yet read our overview of SDLC automation on the DOP-C02, start there for the CodePipeline, CodeBuild and CodeDeploy context. This article assumes that baseline and goes deep on the one service that post only names.
The Object Model: Domain, Repository, Package, Version, Asset
CodeArtifact has a strict hierarchy, and half of the exam questions become easy once you can draw it from memory.
| Object | What it is | Exam-relevant fact |
|---|---|---|
| Domain | The top-level container. Owns the storage and the KMS key. | Every asset is stored once per domain no matter how many repositories reference it. All assets and metadata in a domain are encrypted with one symmetric KMS key. |
| Repository | A named endpoint inside a domain that package managers talk to. | Polyglot: one repository can hold npm, PyPI, Maven and NuGet packages at the same time. Up to 1,000 repositories per domain. A repository cannot be moved to another domain. |
| Package | A named piece of software plus an optional namespace. | Namespace = npm scope (@types), Maven groupId (org.apache.logging.log4j), or a required value for the generic format. PyPI and NuGet have no namespace. |
| Package version | One version of a package (lodash 4.17.21). | Each version has a status (Published, Unfinished, Unlisted, Archived, Disposed). |
| Package version revision | A string that identifies one exact set of assets for a version. | Adding a wheel to a Python version that already has an sdist creates a new revision, not a new version. |
| Asset | A single file (an npm .tgz, a Maven POM or JAR). | Assets are what the domain deduplicates and encrypts. |
Two consequences of that model turn up in scenarios:
- “Copy” is cheap. When a downstream repository pulls a version from an upstream, or when you call
CopyPackageVersionsto promote a build fromstagingtoprod, CodeArtifact only writes metadata. No bytes move, because the asset already exists in the domain. This is why “promote a package between repositories in the same domain” is a sub-second, zero-storage operation. - Repositories are policy boundaries, domains are storage and encryption boundaries. You pay for storage once per domain and manage one KMS key per domain. The domain owner can also set a domain policy that restricts which accounts may create repositories or configure external connections inside it.
AWS recommends one production domain per organization, typically owned by a shared tooling account, with repositories per team or per environment inside it. A second pre-production domain is reasonable for testing changes to the domain configuration itself.
Supported formats and the login command
CodeArtifact supports Cargo, generic, Maven, npm, NuGet, PyPI, Ruby and Swift package formats. Note that container images are not on that list. If the scenario says “store Docker images”, the answer is Amazon ECR, and the exam likes pairing “ECR for images, CodeArtifact for packages” in the same stem.
The aws codeartifact login command fetches a token and rewrites the package manager’s configuration in one step. It supports dotnet, npm, nuget, pip, swift and twine. For Maven, Gradle, Cargo, Ruby and the generic format, you call get-authorization-token yourself and feed the token to the tool through its own configuration or the CODEARTIFACT_AUTH_TOKEN environment variable.
# One-step configuration for npm
aws codeartifact login --tool npm \
--domain acme --domain-owner 111122223333 \
--repository app-npm
# Manual token for tools login doesn't support (Maven, Gradle, Cargo, Ruby, generic)
export CODEARTIFACT_AUTH_TOKEN=$(aws codeartifact get-authorization-token \
--domain acme --domain-owner 111122223333 \
--query authorizationToken --output text)
Authorization Tokens: The 12-Hour Rule and --duration-seconds 0
CodeArtifact never serves packages anonymously. Packages in a CodeArtifact repository cannot be made public, which is itself an exam fact: if a scenario needs a publicly downloadable artifact, the answer is S3 or a public ECR gallery, not CodeArtifact.
Every request from a package manager carries an authorization token obtained from the GetAuthorizationToken API using ordinary AWS credentials. The permission model has a twist that shows up in IAM questions: calling GetAuthorizationToken requires both codeartifact:GetAuthorizationToken and sts:GetServiceBearerToken. The STS permission has to be on the identity policy of the caller. Adding it to the domain resource policy has no effect. Root users cannot call GetAuthorizationToken at all.
Token lifetime is the detail the exam likes most:
| Setting | Value |
|---|---|
| Default lifetime | 12 hours |
Minimum (--duration-seconds) | 900 seconds (15 minutes) |
Maximum (--duration-seconds) | 43200 seconds (12 hours) |
Special value 0 | Token lifetime equals the remaining session duration of the assumed role |
The trap is that by default the token lifetime is independent of the role session. If a CodeBuild project or an engineer assumes a role with a 15-minute session and then calls login, the resulting CodeArtifact token is valid for the full 12 hours, long after the STS credentials have expired. When the scenario says “tokens must not outlive the temporary credentials that created them”, the answer is --duration-seconds 0.
Revocation is coarse. There is no API to revoke a single token. If a token leaks, you deny access in the IAM policy of the principal that created it (or, for an IAM user, change its permissions or delete it), which invalidates the token on the next request.
Upstream Repositories and External Connections
This is where CodeArtifact questions get interesting. A repository can be configured with upstream repositories (other CodeArtifact repositories in the same domain) and with an external connection (a public registry). From a package manager’s point of view, the downstream repository then appears to contain everything in the chain.
The lookup algorithm
When a client requests a version from my_repo:
- If
my_repocontains the version, it is returned. - Otherwise CodeArtifact searches
my_repo’s upstreams in the order they were listed in--upstreams. If the version is found, a reference to it is copied intomy_repoand returned. - If no repository in the chain has it, and a repository in the chain has an external connection, the version is fetched from the public registry.
- Otherwise the client gets an HTTP
404.
Limits worth memorizing: a repository can have up to 10 direct upstreams, but because upstreams can have their own upstreams, CodeArtifact will search at most 25 repositories for a single request. A repository can have only one external connection.
Where the package is retained
The exam loves this one because it is counter-intuitive. Consider the chain repo-A → repo-B → repo-C, where repo-C has an external connection to npmjs.com, and a developer connected to repo-A installs lodash 4.17.21 for the first time:
| Repository | Retains lodash 4.17.21? | Why |
|---|---|---|
repo-A | Yes | It is the most-downstream repository, the one the client asked. |
repo-B | No | Intermediate repositories in a chain never retain pulled versions. |
repo-C | Yes | It owns the external connection, and the repository with the external connection always retains what it ingests. |
If the same version had instead lived in repo-C as a privately published package (no external connection involved), only repo-A would retain a reference. The rule in one sentence: the requesting repository always retains, the external-connection repository always retains, intermediates never do.
A retained reference is durable. Deleting the upstream repository, removing the upstream relationship, deleting the version upstream, or adding assets to it upstream does not change the version that repo-A already holds. This is exactly the behaviour you want for reproducible builds, and it is the reason the exam answer to “make sure a build still works if the public package is unpublished” is “pull it through CodeArtifact once”.
Supported external connections
| Format | Public repository | External connection name |
|---|---|---|
| npm | npm public registry | public:npmjs |
| PyPI | Python Package Index | public:pypi |
| Maven | Maven Central | public:maven-central |
| Maven | Gradle plugins | public:maven-gradleplugins |
| Maven | Google Android | public:maven-googleandroid |
| Maven | Clojars | public:maven-clojars |
| Maven | CommonsWare Android | public:maven-commonsware |
| NuGet | NuGet Gallery | public:nuget-org |
| Ruby | RubyGems.org | public:ruby-gems-org |
| Cargo | crates.io | public:crates-io |
AWS’s intended topology is one repository per domain per public registry that holds the external connection, and every other repository pointing at it as an upstream. The console enforces this for you: adding an external connection through the console creates (or reuses) a -store repository such as npm-store or pypi-store in the domain and adds it as an upstream. That is how a single repository can appear to be connected to several public registries even though the one-external-connection-per-repository limit still holds. In the CLI you do it explicitly:
# The one repository in the domain that talks to npmjs.com
aws codeartifact associate-external-connection \
--domain acme --repository npm-store \
--external-connection public:npmjs
# Team repository that proxies through it (order of --upstreams = search priority)
aws codeartifact create-repository \
--domain acme --repository app-npm \
--upstreams repositoryName=shared-internal repositoryName=npm-store
A worked promotion pattern that appears in architecture questions: dev → staging → prod repositories where prod has no upstream to any public store. Builds that target prod can only resolve versions that were explicitly copied there with copy-package-versions, which gives you an approved-dependency list enforced by topology rather than by review.
Package Origin Controls: Stopping Dependency Substitution
A dependency substitution attack (often called dependency confusion) works like this. Your team publishes an internal package named acme-auth-client to your private repository. An attacker publishes a package with the same name and a higher version to the public registry. The next npm install resolves “the newest version of acme-auth-client”, which now comes from the attacker. Any repository that can accept versions of the same package from both direct publishing and public ingestion is vulnerable.
CodeArtifact’s answer is package origin controls, two settings held per package per repository:
| Setting | ALLOW | BLOCK |
|---|---|---|
| Publish | Versions can be published directly by a package manager. | Direct publishing is rejected. |
| Upstream | Versions can be retained from upstream repositories or ingested through an external connection. | Nothing can arrive from upstreams or public sources. |
The defaults are set automatically from how the first version of the package arrived:
- First version published directly →
Publish: ALLOW,Upstream: BLOCK. Your internal package can never be shadowed by a public one. - First version ingested from a public source →
Publish: BLOCK,Upstream: ALLOW. Nobody can accidentally (or deliberately) overwritelodashwith a local build.
Packages created before roughly May 2022 defaulted to ALLOW/ALLOW and must be fixed by hand. Enforcement started on July 14, 2022.
The patching workflow is a classic scenario. Your team needs to ship a patched build of a public package that is currently Publish: BLOCK / Upstream: ALLOW. The procedure is: flip it to Publish: ALLOW / Upstream: BLOCK, publish the patched version, then flip it back. While it is flipped, new public versions cannot sneak in.
aws codeartifact put-package-origin-configuration \
--domain acme --repository app-npm \
--format npm --package acme-auth-client \
--restrictions publish=ALLOW,upstream=BLOCK
Two more rules round out the mechanism:
- Publishing a version that already exists upstream is a
409 Conflictwhen both settings areALLOW. CodeArtifact will not let you publishcom.acme:lib:1.0intomyrepoif1.0is reachable from an upstream or from Maven Central. You could publish1.1. WithUpstream: BLOCK, upstream versions are not “reachable”, so any version you do not already hold can be published. BLOCKalways wins when a package setting and a package group setting disagree. A package set toALLOW/ALLOWis effectively deferring to its package group.
Package Groups: Origin Controls at Scale
Setting origin controls one package at a time does not scale to a domain with thousands of packages, so CodeArtifact adds package groups, patterns that match packages by format, namespace and name and carry their own origin configuration.
Pattern syntax
A package’s path is /format/namespace/name. A group pattern is that path truncated and terminated with a suffix:
| Pattern | Matches |
|---|---|
/* | Everything. This is the root package group, present in every domain, modifiable but not deletable, and it allows all origins by default. |
/npm/* | All npm packages. |
/npm/space/* | All npm packages in scope @space. |
/maven/com.acme~ | Maven packages whose groupId starts with com.acme on a word boundary (com.acme, com.acme.billing, but not com.acmeco). |
/npm/space/acme-ui~ | npm packages in @space whose name starts with acme-ui. |
/maven/org.apache.logging.log4j/log4j-core$ | Exactly one package. |
Formats without namespaces (PyPI, NuGet) use an empty namespace segment: the path for the Python package requests is /python//requests. A package is associated with exactly one group, the most specific match, and only that group’s configuration applies, although a group can be set to inherit from its parent.
Restriction settings
Package group origin controls have three restriction types rather than the two that individual packages have, because they split “upstream” into internal and external:
| Restriction | Controls | Values |
|---|---|---|
PUBLISH | Direct publishing | ALLOW, BLOCK, ALLOW_SPECIFIC_REPOSITORIES, INHERIT |
EXTERNAL_UPSTREAM | Ingestion from public repositories through external connections | same |
INTERNAL_UPSTREAM | Retention from other CodeArtifact repositories in the domain | same |
ALLOW_SPECIFIC_REPOSITORIES comes with an allowed-repository list per restriction type. The canonical exam configuration for “private names can be published but never ingested, everything else can be ingested but never published” is:
aws codeartifact create-package-group \
--domain acme --package-group '/npm/space/acme~'
aws codeartifact update-package-group-origin-configuration \
--domain acme --package-group '/npm/space/acme~' \
--restrictions PUBLISH=ALLOW,EXTERNAL_UPSTREAM=BLOCK,INTERNAL_UPSTREAM=ALLOW
With the root group left at its default, every package that does not match /npm/space/acme~ keeps the normal first-version-wins behaviour, which blocks publishing for anything that came in from npmjs.com.
Weak matches: the confusable-name defence
Package groups have one more subtlety that is easy to under-appreciate. Patterns are case sensitive, but CodeArtifact also associates variations of the pattern (different case, runs of -, . and _ collapsed, visually confusable characters) with the same group as weak matches. A weak match is associated with the group but is blocked from every origin instead of inheriting the group’s settings. So if your group is /npm//acme-spicy-client$ with Publish: ALLOW, the exact name can be published, but AcMe-Spicy-Client, acme.spicy.client and acme_spicy_client are all blocked from being published or ingested. That closes the typosquatting variant of dependency confusion without you enumerating every spelling.
Cross-Account Domains, Policies and Encryption
Most DOP-C02 scenarios put CodeArtifact in a shared services account and have workload accounts consume it. Three mechanisms make that work:
- Domain policies (
put-domain-permissions-policy) decide which accounts mayCreateRepositoryin the domain,GetAuthorizationTokenagainst it, and configure external connections. Because domain names are only unique within an account, consumers in other accounts must pass--domain-owner 111122223333alongside--domain. - Repository policies (
put-repository-permissions-policy) control read (ReadFromRepository,DescribePackageVersion) and write (PublishPackageVersion) at the repository level. A repository can even be owned by a different account than its domain if the domain policy grants that accountCreateRepository. - Organization-wide sharing is done by conditioning the resource policy on
aws:PrincipalOrgID, the same way you would share an S3 bucket or KMS key.
Encryption is per domain with one symmetric KMS key, either AWS managed or customer managed. Asymmetric keys are not supported. CodeArtifact also supports keys backed by KMS External Key Stores (XKS), with an explicit warning that every package asset and every dependency is decrypted on each fetch, so XKS latency becomes build latency, and loss of the external key material means loss of every asset in the domain. If you want the KMS side of that story, see KMS custom key stores for the security specialty.
CodeArtifact emits events to Amazon EventBridge for package version state changes (created, modified, deleted, status changed). The standard exam pattern is “a new version of an internal library is published → EventBridge rule → start CodePipeline for every downstream service”, which is the artifact-management cousin of the image-push trigger you use with ECR. For the EventBridge mechanics themselves, see our DOP-C02 EventBridge deep dive.
Wiring CodeArtifact into CodeBuild and CodePipeline
The reference buildspec.yml for a Node.js service that pulls from and publishes to CodeArtifact looks like this:
version: 0.2
env:
variables:
CA_DOMAIN: acme
CA_OWNER: "111122223333"
CA_REPO: app-npm
phases:
install:
runtime-versions:
nodejs: 20
pre_build:
commands:
# Token lifetime tied to the build role's session, not the 12h default
- aws codeartifact login --tool npm --domain $CA_DOMAIN
--domain-owner $CA_OWNER --repository $CA_REPO --duration-seconds 0
build:
commands:
- npm ci
- npm test
- npm run build
post_build:
commands:
- npm publish # goes to CodeArtifact because login rewrote the registry
artifacts:
files:
- dist/**/*
The CodeBuild service role needs codeartifact:GetAuthorizationToken, codeartifact:GetRepositoryEndpoint, codeartifact:ReadFromRepository, sts:GetServiceBearerToken, and codeartifact:PublishPackageVersion plus codeartifact:PutPackageMetadata if it publishes. Two CodeBuild-specific points the exam checks:
- No internet, no problem. If the build runs inside a VPC without a NAT gateway, create interface VPC endpoints for
codeartifact.apiandcodeartifact.repositories. Because package assets are served from S3 behind the scenes, you also need an S3 gateway endpoint (and, for the policy, access to the CodeArtifact-owned bucket for your Region). This is the standard fix for “builds fail to download dependencies after we removed the NAT gateway”. - Reproducible builds come from the retention rule. Once the first build pulls a version through the external connection, it is retained in the domain. Removing the external connection afterwards, or pointing production builds at a repository with no public upstream, locks the dependency set without a vendoring step.
For the pipeline side, CodeArtifact is not a native CodePipeline source or deploy provider. You consume it from CodeBuild actions, and you trigger pipelines from its EventBridge events. For a multi-account pipeline the pattern is to keep the domain in the tooling account and grant each account’s CodeBuild role read access through the repository policy, which pairs naturally with the cross-account pipeline patterns covered elsewhere on this blog.
Decision Table: Where the Exam Wants Each Answer
| Scenario phrase | Answer |
|---|---|
| ”Cache public packages so builds survive a registry outage” | Repository with an external connection (or an upstream to a -store repo); retained versions persist |
| ”Store Docker images” | Amazon ECR, not CodeArtifact |
| ”Public download of a package” | Not CodeArtifact (repositories cannot be public) |
| “Internal package shadowed by a public one with the same name” | Package origin controls: Publish: ALLOW, Upstream: BLOCK on the package, or a package group with EXTERNAL_UPSTREAM=BLOCK |
| ”Only approved versions in production builds” | prod repository with no public upstream; promote with copy-package-versions |
| ”Token must expire with the role session” | --duration-seconds 0 |
| ”Builds in a private subnet without NAT cannot fetch packages” | Interface endpoints for codeartifact.api + codeartifact.repositories and an S3 gateway endpoint |
| ”Share one domain with every account in the Organization” | Domain + repository resource policies conditioned on aws:PrincipalOrgID; consumers pass --domain-owner |
| ”Rebuild all services when a shared library ships” | EventBridge rule on CodeArtifact package-version events → CodePipeline |
”Second npm publish of an existing version fails with 409” | Expected: the version is reachable upstream and both origin settings are ALLOW |
Practice the Mechanics Under Exam Conditions
CodeArtifact questions rarely stand alone. The exam embeds them in a 150-word pipeline scenario where the real test is whether you notice the dependency problem at all and then pick the fix that respects origin controls, retention and token lifetimes at the same time. The only reliable way to get fast at that is timed practice on full-length professional-level questions.
Sailor.sh’s AWS Certified DevOps Engineer - Professional (DOP-C02) mock exam bundle includes eight full mock exams that weave artifact management into the same SDLC, security and multi-account scenarios the real exam uses. Each explanation walks through why the distractors fail, which is where you learn the difference between “CodeArtifact caches packages” and “the external-connection repository always retains, intermediates never do”. If you are still mapping the domains, the DOP-C02 exam guide and study plan are the right companions.
Frequently Asked Questions
What is the difference between a CodeArtifact domain and a repository?
A domain is the storage and encryption boundary: all assets in it are stored once and encrypted with one symmetric KMS key, and the domain owner can apply policy across every repository in it. A repository is the endpoint a package manager connects to. A domain can hold up to 1,000 repositories, each repository belongs to exactly one domain, and repositories cannot be moved between domains.
How long is a CodeArtifact authorization token valid?
By default 12 hours. You can set --duration-seconds between 900 (15 minutes) and 43200 (12 hours). Setting it to 0 while using an assumed role ties the token lifetime to the remaining role session. Otherwise the token lifetime is independent of the role session and can outlive it.
Which repository retains a package pulled through an upstream chain?
The most-downstream repository the client asked, plus the repository that holds the external connection if the version came from a public registry. Intermediate repositories in the chain never retain pulled versions. The retained reference survives deletion of the upstream repository or the upstream version.
How many upstream repositories and external connections can a repository have?
Up to 10 direct upstream repositories and only one external connection. Because upstreams can chain, CodeArtifact searches at most 25 repositories for a single request. AWS recommends one repository per domain with an external connection to a given public registry, which is what the console’s -store repositories implement.
What are package origin controls and what are their defaults?
Two per-package settings, Publish and Upstream, each ALLOW or BLOCK. If a package’s first version was published directly, the defaults are Publish: ALLOW, Upstream: BLOCK. If it was ingested from a public source, the defaults are Publish: BLOCK, Upstream: ALLOW. They exist to prevent dependency substitution attacks, and a BLOCK at either the package or package-group level always wins.
How do package groups differ from package origin controls?
Package groups apply origin configuration to every package matching a pattern such as /npm/@acme/* or /maven/com.acme~, with three restriction types (PUBLISH, EXTERNAL_UPSTREAM, INTERNAL_UPSTREAM) that each accept ALLOW, BLOCK, ALLOW_SPECIFIC_REPOSITORIES or INHERIT. Every domain has a root group /* that allows everything by default. Name variations that match a group only weakly (case, punctuation, confusable characters) are blocked from all origins.
Can CodeArtifact store container images or serve public packages?
No on both counts. Container images belong in Amazon ECR. CodeArtifact repositories cannot be made public; every request needs an authorization token obtained with AWS credentials.
How does a CodeBuild project in a private subnet reach CodeArtifact?
Create interface VPC endpoints for codeartifact.api and codeartifact.repositories, plus an S3 gateway endpoint because package assets are served from S3. Grant the build role codeartifact:GetAuthorizationToken, codeartifact:GetRepositoryEndpoint, codeartifact:ReadFromRepository and sts:GetServiceBearerToken.
Conclusion
CodeArtifact is a small service with a precise rule set, and the DOP-C02 exam tests the rules rather than the marketing summary. Remember the hierarchy (domain stores and encrypts, repository serves), the lookup algorithm (local, then upstreams in order, then external connection, then 404), the retention rule (requester and external-connection repositories keep it, intermediates do not), the token model (12 hours by default, independent of the role session unless you pass 0), and the origin-control defaults (first arrival decides, BLOCK always wins). Layer package groups on top for domain-wide policy, and put the whole thing behind VPC endpoints and resource policies for the multi-account pipelines the exam prefers.
Once those mechanics are reflexive, the artifact-management questions turn into free points inside otherwise hard scenarios. Build that reflex with timed mock exams, then go back to the SDLC automation guide and notice how many pipeline questions were quietly asking about dependencies all along.