There is a problem at Contoso that none of the previous lessons has touched. The Reserva class, with its booking reference validation, its date rules and its seat count calculation, is copy-pasted into three repositories: into contoso-reservas, into contoso-api-disponibilidad and into the nightly process that generates boarding passes. They started out identical, but each has been tweaked on its own and today there are three different versions. The consequence shows up in production: the website accepts a seven-character booking reference that the API rejects, and nobody knows which of the three is right.

Copying shared code does not scale. The solution is to treat it as what it is — a library with its own version — and distribute it as a package. Azure Artifacts is the Azure DevOps service that hosts those packages privately, and along the way it solves a bigger problem you may not have been aware of: the security of your dependency supply chain.

Contents

  1. Package managers and feeds
  2. Supported package types and Universal Packages
  3. Creating the contoso-paquetes feed: scope and views
  4. Upstream sources and dependency confusion
  5. Publishing and consuming contoso.reservas.modelos
  6. Semantic versioning automated from the pipeline
  7. Retention, cleanup and cost
  8. Supply chain security
  9. Comparison with GitHub Packages and with a container registry
  10. Common Mistakes and Tips
  11. Exercises
  12. Conclusion

  1. Package managers and feeds

A package manager (NuGet, npm, Maven, pip) does three things: it downloads a library and its transitive dependencies, resolves the versions that are compatible across all of them, and records what was used. A feed is a package repository: the place the manager downloads from and publishes to.

Compared with copy and paste, a package brings three guarantees Contoso needs:

Copied code A package in a feed
Identity None: three copies with no common name Explicit name and version
Updating Manual, repository by repository dotnet add package and a number that goes up
Knowing who uses what Impossible The feed's list of consumers
Breaking changes Discovered in production Announced by bumping the major version

  1. Supported package types and Universal Packages

Type Ecosystem Typical use at Contoso
NuGet .NET contoso.reservas.modelos, the shared library
npm JavaScript / TypeScript Common web components for the portal
Maven Java Integration with the legacy crew system
Python Python Utilities for the analytics process from 03-06
Cargo Rust Not used today
Universal Packages Anything Files that are not code packages

Universal Packages deserve an explanation because they solve a very real case. Not everything that has to be versioned and shared is a library: Contoso has a 300 MB dataset of airports and routes, boarding pass templates in PDF and payment gateway configuration files. None of that fits in NuGet, none of it should go into the Git repository (05-02), and yet it needs a version, history and traceability. A Universal Package is simply a versioned folder, published and downloaded with the CLI:

# Publish the airport data folder as a universal package
az artifacts universal publish \
  --organization https://dev.azure.com/contoso-airlines \
  --feed contoso-paquetes --name datos-aeropuertos --version 1.4.0 \
  --path ./datos --description "Airport and route catalog"

# Consume it from a pipeline or from a machine
az artifacts universal download \
  --organization https://dev.azure.com/contoso-airlines \
  --feed contoso-paquetes --name datos-aeropuertos --version 1.4.0 --path ./datos

  1. Creating the contoso-paquetes feed: scope and views

# The feed is created from the portal (Artifacts) or with the REST API.
# Organization scope: visible from any project in contoso-airlines.
az artifacts universal publish --help   # The CLI covers publishing and downloading

The first decision is the scope:

Project scope Organization scope
Who sees it Only the project that contains it Any project in the organization
Permissions Inherited from the project Managed separately
When to choose it Packages internal to one product Libraries shared between products

Contoso creates contoso-paquetes with organization scope, because contoso.reservas.modelos will also be consumed by the contoso-millas project once it exists. A project-scoped feed would force them to duplicate it.

The second decision is the views, which are the promotion mechanism. A new feed comes with three:

View What it contains Who consumes from it
@local Everything published, plus whatever was downloaded from upstream sources The build pipelines themselves
@prerelease Versions promoted for testing Development and preproduction environments
@release Versions approved for production The pipelines that deploy to production

The flow is the same gating pattern from 05-04, applied to packages: every build publishes to @local, gets promoted to @prerelease when it passes the integration tests, and to @release only once it has been validated. Consumers point at the view that corresponds to them through the feed URL, which includes the view name:

https://pkgs.dev.azure.com/contoso-airlines/_packaging/contoso-paquetes@release/nuget/v3/index.json

A published version is never modified: promotion does not change the package, it only makes it visible in another view. That immutability is what lets you state that version 2.3.1 is the same everywhere.

  1. Upstream sources and dependency confusion

An upstream source makes your feed act as a proxy for the public registry: when somebody asks for Newtonsoft.Json, the feed first looks among its own packages, and if it does not have it, downloads it from nuget.org, saves a copy and serves it. It brings two things:

  • A single URL in every project's configuration, instead of two.
  • Protection against a package disappearing. If an author pulls their library from the public registry — it has happened, and it has broken thousands of builds — the saved copy in your feed is still there and your pipelines carry on building.

But the most important reason is security, and it is worth understanding properly.

Dependency confusion

This is a real supply chain attack, demonstrated in 2021 against dozens of large companies. It works like this:

  1. Contoso has an internal package called contoso.reservas.modelos, which exists only in its private feed.
  2. An attacker discovers that name — it appears in a leaked package.json, in a screenshot from a talk, in a public error message — and publishes to the public registry a package with the same name and a sky-high version, 99.0.0.
  3. If the package manager is configured with two sources — the private feed and the public one — many implementations query both and pick the highest version. The highest version is the attacker's.
  4. Contoso's pipeline downloads and runs the attacker's code during the build, with access to environment variables and to the agent's network.
graph TD
    A["Pipeline requests<br/>contoso.reservas.modelos"] --> B{"How many sources<br/>are configured?"}
    B -->|"Two sources:<br/>private and public"| C["Compares versions:<br/>2.3.1 private vs 99.0.0 public"]
    C --> D["Downloads the attacker's<br/>99.0.0"]
    B -->|"A single source:<br/>feed with upstream"| E["The feed resolves its<br/>internal packages first"]
    E --> F["Downloads the legitimate<br/>2.3.1"]

The defense Azure Artifacts gives you is twofold. First, a single configured source: the feed, with the public registry as an upstream source, so that resolution is decided by the feed and internal packages always win. Second, the feed marks packages it has saved from an upstream source and stops a public package from impersonating an internal one with the same name. To that you add two good practices: use a reserved prefix or namespace for your packages, and never configure several sources in parallel in nuget.config or .npmrc.

  1. Publishing and consuming contoso.reservas.modelos

The contoso-modelos repository stops being a copied folder and gets its own publishing pipeline:

name: 2.3.$(Rev:r)        # MAJOR.MINOR set by hand; PATCH automatic

trigger:
  branches: { include: [ main ] }

pool: { vmImage: ubuntu-latest }

variables:
  - name: feed
    value: 'contoso-airlines/contoso-paquetes'   # organization/feed

steps:
  - task: UseDotNet@2
    inputs: { packageType: sdk, version: '8.0.x' }

  # Authentication against the feed: gets a token from the run, no secrets involved
  - task: NuGetAuthenticate@1

  - script: dotnet build src/Contoso.Reservas.Modelos -c Release
  - script: dotnet test src/Contoso.Reservas.Modelos.Pruebas -c Release

  # The package version number is taken from the build number:
  # one version per build, with nobody editing a file by hand
  - script: |
      dotnet pack src/Contoso.Reservas.Modelos \
        -c Release -o $(Build.ArtifactStagingDirectory) \
        -p:PackageVersion=$(Build.BuildNumber)
    displayName: Pack

  # Publish to the feed's @local view
  - task: NuGetCommand@2
    inputs:
      command: push
      packagesToPush: '$(Build.ArtifactStagingDirectory)/*.nupkg'
      nuGetFeedType: internal
      publishVstsFeed: '$(feed)'
    displayName: Publish to contoso-paquetes

NuGetAuthenticate@1 is the key security piece: it obtains credentials from the run's own context, so there is no personal access token stored anywhere. The same secretless identity logic as module 4.

On the consumer side, in contoso-reservas, the nuget.config declares a single source:

<configuration>
  <packageSources>
    <clear />   <!-- Essential: removes nuget.org inherited from the machine -->
    <add key="contoso-paquetes"
         value="https://pkgs.dev.azure.com/contoso-airlines/_packaging/contoso-paquetes@release/nuget/v3/index.json" />
  </packageSources>
</configuration>

The <clear /> is not decoration: without it, the machine's global public source is still active and the door to dependency confusion is open again.

  1. Semantic versioning automated from the pipeline

The rules from 05-02 apply equally to packages, with one contractual consequence: the version is a promise to whoever consumes it.

Change in contoso.reservas.modelos Version
An optional property is added to Reserva 2.3.1 → 2.4.0 (minor)
The booking reference validation is fixed without changing the signature 2.3.1 → 2.3.2 (patch)
CodigoReserva is renamed to Localizador 2.3.1 → 3.0.0 (major)
A test publication before settling on it 2.4.0-preview.5

The automation consists of deriving the number from the pipeline. With name: 2.3.$(Rev:r) and -p:PackageVersion=$(Build.BuildNumber), every build of main produces a new version with nobody editing a file: the major and minor parts are decided by a person changing the name — because they are a design decision — and the patch is handled by the machine. Test versions are marked with a suffix (-preview.N), and package managers ignore them by default, which makes them ideal for the @prerelease view.

  1. Retention, cleanup and cost

Cost warning: Azure Artifacts bills for total feed storage, with 2 GiB free per organization and a rising cost above that (on the order of $2/GiB a month in the first band). It sounds like little until a pipeline publishes a version on every build: 20 builds a day × 30 MB is 600 MB a month, and within six months you have eaten the quota. On top of that, packages saved from upstream sources also count, just like the Git LFS storage from 05-02.

The defense is a retention policy per feed: keep the N most recent versions of each package and automatically delete the rest after X days. Two precautions when configuring it: packages promoted to a view are never deleted by retention — which is exactly why you promote what matters — and it is worth reviewing consumption periodically in the billing view, because the growth is silent.

  1. Supply chain security

Your dependencies are third-party code that runs with your permissions. Five practices, from the most elementary to the most advanced:

  1. Pin versions and use lock files. packages.lock.json in .NET, package-lock.json in npm: they record the exact version of every dependency, transitive ones included, and make the build reproducible. Without a lock file, the same commit can build differently tomorrow. It must be committed to the repository.
  2. Scan for vulnerabilities. dotnet list package --vulnerable --include-transitive in the pipeline from 05-03, plus software composition analysis tools that cross-reference your dependencies against CVE databases. Transitive matters: almost always the vulnerability is three levels below what you declared.
  3. Avoid dependency confusion with a single source and <clear />, as we have seen.
  4. Generate a software bill of materials (SBOM), the complete inventory of everything that makes up your artifact. When a serious vulnerability turns up in a library — Log4Shell is the canonical example — the question "are we affected?" is answered by consulting the SBOM in minutes instead of auditing repositories for days. It is generated in the pipeline and published alongside the artifact.
  5. Sign your packages, so that whoever consumes them can verify they come from Contoso and have not been tampered with. The certificate lives in kv-contoso-pro (04-03) and the signing happens in the pipeline, never on a laptop.

  1. Comparison with GitHub Packages and with a container registry

Azure Artifacts GitHub Packages Azure Container Registry
What it hosts NuGet, npm, Maven, Python, Cargo, universal The same, plus containers Container images and OCI artifacts
Upstream sources Very good, with impersonation protection Limited Public registry caching
Views and promotion Yes (@local, @prerelease, @release) No, tags are used instead Tags and image locking
Billing By storage, 2 GiB free By storage and transfer By registry tier

The distinction from a container registry is conceptual and worth keeping clear: a package feed distributes pieces for building an application — libraries that get compiled into your binary — whereas a container registry distributes the whole application already built, with its file system and its operating system dependencies. They do not compete: the pipeline consumes packages from Artifacts to produce an image that it publishes to the registry. That registry, Azure Container Registry, is precisely where lesson 06-01 begins, and it will reappear in 05-06 as a store for Bicep modules.

Common Mistakes and Tips

  • Copy-pasting the shared library. It is the problem this lesson started with: three copies, three truths and a failure in production.
  • Configuring the private feed and the public one in parallel. That is the open door to dependency confusion. A single source, with upstream sources, and <clear /> to remove inherited ones.
  • Storing a personal access token to publish. Unnecessary and dangerous: NuGetAuthenticate@1 (or its npm and Maven equivalent) uses the run's identity.
  • Publishing with no retention policy. The feed grows silently until the bill arrives. Configure it the same day you create the feed.
  • Editing the version number by hand. It gets forgotten, gets duplicated and blocks publishing. Derive it from Build.BuildNumber.
  • Bumping the major version without warning. Semantic versioning is a contract: a breaking change published as a patch breaks consumers with no notice.
  • Ignoring transitive dependencies. That is where most vulnerabilities live. --include-transitive, always.
  • Tip: reserve a prefix for your packages (contoso.*) and document that this namespace is internal. It makes impersonation easier to spot.
  • Tip: commit the lock file to the repository and treat it as code: if it changes in a pull request, somebody should look at why.

Exercises

Exercise 1: designing the feed

Contoso Miles is going to share two things with the bookings project: a .NET points-calculation library and a 200 MB dataset of redemption tables, which changes monthly.

  1. A new feed or the same contoso-paquetes? With what scope?
  2. Which package type suits each of the two items, and why?
  3. Estimate the dataset's annual storage growth and propose a retention policy.

Exercise 2: dependency confusion

A Contoso developer posts an error trace on a public forum that includes the line Contoso.Reservas.Modelos, Version=2.3.1. Two weeks later, the API pipeline starts downloading contoso.reservas.modelos 99.0.0, a package nobody on the team has published.

  1. Explain the mechanics of the attack step by step.
  2. Review this nuget.config and state what is wrong: <packageSources><add key="nuget.org" value="https://api.nuget.org/v3/index.json" /><add key="contoso" value="https://pkgs.dev.azure.com/.../contoso-paquetes@release/nuget/v3/index.json" /></packageSources>
  3. Which three measures would you apply, and which is the most important?

Exercise 3: versioning and promotion

The contoso.reservas.modelos library is at version 2.3.1. Diego needs to: (a) fix the booking reference validation without changing any public signature; (b) add an optional AsientoPreferente property; (c) rename CodigoReserva to Localizador.

  1. Assign a version number to each change and justify it.
  2. Describe how change (c) travels through the feed's views until it reaches production.
  3. What has to happen in contoso-reservas and contoso-api-disponibilidad in order to consume (c), and how do you avoid breaking them both at once?

Solutions

Solution 1:

  1. The same contoso-paquetes, with organization scope, which is exactly what that scope was chosen for: it lets two different projects consume the same libraries without duplicating feeds or configuration. A feed per project would only make sense if the permissions had to be strictly separated.
  2. The points-calculation library, a NuGet package: it is .NET code that gets compiled into the consuming applications and needs dependency resolution. The redemption tables, a Universal Package: they are data, not code, they do not fit any package manager, they should not go into the Git repository because of their size and they still need a version and traceability.
  3. 200 MB × 12 publications a year = 2.4 GiB per year, which on its own exceeds the whole organization's 2 GiB free quota. A reasonable policy: keep the 3 most recent versions and delete the rest after 90 days, promoting the version in use in production to @release to protect it from automatic deletion. That way consumption settles at around 600-800 MB.

Solution 2:

  1. The attacker obtains the package's internal name from the public trace. They publish a package with the same name and version 99.0.0 on nuget.org. The API pipeline has two sources configured in parallel, so it queries both and picks the highest version, which is the attacker's. It downloads that package and runs its code during the build, with access to the agent's variables and network. From there the attacker can exfiltrate tokens or tamper with the artifact that will be deployed.
  2. Two things are wrong: it declares two sources in parallel — the public one and the private one — which is the necessary condition for the attack; and it has no <clear />, so it also drags in the global sources configured on the machine or in the agent image. The correct form is <clear /> followed by a single source, Contoso's feed, with nuget.org configured as an upstream source of that feed.
  3. (a) A single source with <clear /> and upstream sources — the most important, because it removes the resolution ambiguity that makes the attack possible. (b) Reserve and document the contoso.* prefix as an internal namespace. (c) A committed lock file and dependency analysis in the pipeline, which would have caught the anomalous version jump. On top of that, rotate any credential the agent had access to during the compromised builds.

Solution 3:

  1. (a) 2.3.2, patch: it fixes behavior without altering the public surface. (b) 2.4.0, minor: it adds functionality compatibly; anyone not using the new property does not notice. (c) 3.0.0, major: renaming a public member is a breaking change and all consuming code stops compiling.
  2. The contoso-modelos pipeline publishes 3.0.0 to @local. It is promoted to @prerelease — possibly earlier as 3.0.0-preview.1 — and the development environments consume and validate it. Once the website and the API have migrated and their integration tests pass, it is promoted to @release, the view the production pipelines consume. The version is never modified at any point: only where it is visible changes.
  3. Each repository has to update its reference to 3.0.0 and adapt the code to the new name, each in its own pull request with its own review. Because consumption is by explicit version, they do not break at once: as long as the API keeps referencing 2.4.0, it carries on building and deploying normally. That is precisely the value of versioning over copied code, where the change would have hit everybody at the same time. To smooth it further, an intermediate 2.5.0 version can introduce Localizador while marking CodigoReserva as obsolete, giving room to migrate before 3.0.0 removes it.

Conclusion

Contoso's models library is no longer copied into three places. You understand what a package manager gives you over copy and paste — identity, versioning, traceable updates and an explicit contract about breaking changes — you know the types Azure Artifacts hosts and you know when a Universal Package is the right answer: when what needs versioning and sharing is not code and does not belong in the Git repository, like Contoso's airport catalog. You have created the contoso-paquetes feed with organization scope so that contoso-millas can consume it too, and you have used the @local, @prerelease and @release views as a promotion mechanism — the same gating pattern from 05-04 applied to packages — knowing that a published version is immutable and that promotion only changes where it is visible.

You have understood upstream sources in depth: the single URL, the saved copy that protects you if a public package disappears and, above all, the defense against dependency confusion, that supply chain attack in which a public package with the same name and a sky-high version impersonates the internal one and manages to run code on your build agent. The defense boils down to one line of configuration: <clear /> and a single source. You have published contoso.reservas.modelos from a pipeline that authenticates with NuGetAuthenticate@1 — with no stored token — and that derives the version number from Build.BuildNumber, leaving people only the design decision of when the major or minor part goes up. And you have closed with what holds all the rest up: retention configured from day one, because the feed grows silently against a 2 GiB free quota, and the five supply chain security practices — pin versions with lock files, scan for vulnerabilities including transitive ones, a single source, generate the SBOM that answers "are we affected?" in minutes, and sign with the certificate from kv-contoso-pro.

With this, Contoso's delivery cycle is almost complete: work is planned in Boards, code is versioned and reviewed in Repos, built and tested in Pipelines, deployed with approvals and slots, and the shared pieces are distributed with versions from Artifacts. Almost. Because the entire platform it is all deployed onto — the networks, the applications, the databases, module 4's governance policies — still exists only because somebody once typed the right commands. There is no way to recreate it, no way to know who changed what, and no way to stand it up in North Europe after a disaster. In the last lesson of the module, Infrastructure as Code with Bicep, that ends: the whole platform becomes reviewable, versioned and repeatable code, deployed by its own pipeline with a preview, an approval and everything else you have learned here.

Azure Course

Module 1: Introduction to Azure

Module 2: Core Azure Services

Module 3: Azure Databases

Module 4: Security in Azure

Module 5: Azure DevOps

Module 6: Advanced Azure Services

Module 7: Monitoring and Management

Module 8: Cost Management and Optimization

Module 9: Case Studies and Best Practices

© Copyright 2026. All rights reserved