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
- Package managers and feeds
- Supported package types and Universal Packages
- Creating the
contoso-paquetesfeed: scope and views - Upstream sources and dependency confusion
- Publishing and consuming
contoso.reservas.modelos - Semantic versioning automated from the pipeline
- Retention, cleanup and cost
- Supply chain security
- Comparison with GitHub Packages and with a container registry
- Common Mistakes and Tips
- Exercises
- Conclusion
- 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 |
- 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
- Creating the
contoso-paquetes feed: scope and views
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 downloadingThe 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:
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.
- 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:
- Contoso has an internal package called
contoso.reservas.modelos, which exists only in its private feed. - 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. - 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.
- 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.
- Publishing and consuming
contoso.reservas.modelos
contoso.reservas.modelosThe 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-paquetesNuGetAuthenticate@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.
- 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.
- 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.
- Supply chain security
Your dependencies are third-party code that runs with your permissions. Five practices, from the most elementary to the most advanced:
- Pin versions and use lock files.
packages.lock.jsonin .NET,package-lock.jsonin 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. - Scan for vulnerabilities.
dotnet list package --vulnerable --include-transitivein 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. - Avoid dependency confusion with a single source and
<clear />, as we have seen. - 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.
- 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.
- 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.
- A new feed or the same
contoso-paquetes? With what scope? - Which package type suits each of the two items, and why?
- 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.
- Explain the mechanics of the attack step by step.
- Review this
nuget.configand 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> - 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.
- Assign a version number to each change and justify it.
- Describe how change (c) travels through the feed's views until it reaches production.
- What has to happen in
contoso-reservasandcontoso-api-disponibilidadin order to consume (c), and how do you avoid breaking them both at once?
Solutions
Solution 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. - 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.
- 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
@releaseto protect it from automatic deletion. That way consumption settles at around 600-800 MB.
Solution 2:
- The attacker obtains the package's internal name from the public trace. They publish a package with the same name and version
99.0.0on 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. - 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. - (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 thecontoso.*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:
- (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.
- The
contoso-modelospipeline publishes3.0.0to@local. It is promoted to@prerelease— possibly earlier as3.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. - Each repository has to update its reference to
3.0.0and 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 referencing2.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 intermediate2.5.0version can introduceLocalizadorwhile markingCodigoReservaas 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
- What Is Azure?
- Service Models, Regions and Availability Zones
- Creating and Setting Up Your Azure Account
- A Tour of the Azure Portal
- Azure Resource Manager: Subscriptions, Resource Groups and Tags
- Azure CLI, PowerShell and Cloud Shell
Module 2: Core Azure Services
- Azure Virtual Machines
- Compute Scaling and High Availability
- Azure App Service
- Azure Storage: Blobs, Files, Queues and Tables
- Azure Networking: Virtual Networks, Subnets and NSGs
- Hybrid Connectivity and Global Delivery
Module 3: Azure Databases
- Choosing the Right Data Service
- Azure SQL Database
- Azure Cosmos DB
- Azure Database for MySQL
- Azure Database for PostgreSQL
- Data Analytics: Data Lake, Data Factory and Synapse
Module 4: Security in Azure
- Microsoft Entra ID and Identity Management
- RBAC and Managed Identities
- Azure Key Vault
- DDoS Protection and Web Application Firewall
- Microsoft Defender for Cloud
- Governance and Compliance with Azure Policy
Module 5: Azure DevOps
- Introduction to Azure DevOps
- Azure Repos
- Azure Pipelines: Continuous Integration
- Continuous Deployment with Environments and Approvals
- Azure Artifacts
- Infrastructure as Code with Bicep
Module 6: Advanced Azure Services
- Containers in Azure: Container Registry and Container Apps
- Azure Kubernetes Service (AKS)
- Azure Functions
- Azure Logic Apps
- Messaging and Events: Service Bus, Event Grid and Event Hubs
- Azure AI Services
Module 7: Monitoring and Management
- Azure Monitor: Metrics, Alerts and Dashboards
- Log Analytics and KQL Queries
- Application Insights
- Azure Automation and Runbooks
- Backup and Disaster Recovery
Module 8: Cost Management and Optimization
- Pricing Calculator and Cost Estimation
- Azure Cost Management: Analysis, Budgets and Alerts
- Reservations, Savings Plans and Azure Hybrid Benefit
- Azure Advisor
- Optimization Strategies and FinOps Culture
