BrandGhost
Private NuGet Feeds in .NET: Azure Artifacts and GitHub Packages

Private NuGet Feeds in .NET: Azure Artifacts and GitHub Packages

07/07/2026

Private NuGet Feeds in .NET: Azure Artifacts and GitHub Packages

NuGet.org is a fantastic resource for open-source packages -- but not every package belongs there. Maybe you have internal utilities shared across a dozen microservices. Maybe your team built a proprietary library that powers a key product feature. Maybe you're working on a package that isn't ready for the public just yet. In all of these situations, a private NuGet feed is the right tool.

This article walks through the two most common hosted options for private NuGet feeds in .NET: Azure Artifacts and GitHub Packages. You'll see how to set each one up as a NuGet source, how to configure authentication both locally and in CI/CD, and how to consume private packages alongside public ones. No assumptions about prior experience with either platform -- just practical configuration you can use today.

If you're building the packages themselves, check out The Complete Guide to Creating NuGet Packages in .NET first. And if you eventually want public distribution on NuGet.org, How to Publish a NuGet Package to NuGet.org covers that path instead.

When Public NuGet.org Isn't the Right Choice

NuGet.org is public by design. Every package you push there is visible and downloadable by anyone on the internet. That's ideal for open-source work -- but it's a problem for the scenarios most teams actually face day-to-day.

Here are the most common reasons to reach for a private NuGet feed:

Proprietary business logic -- If your package contains algorithms, data models, or integrations that are core to your company's product, publishing to NuGet.org would give competitors access to your source.

Not-yet-released libraries -- You want to share a library across teams inside your organization before it's ready for a public release. A private feed lets internal teams depend on it immediately.

Internal tooling and utilities -- Things like shared logging helpers, base test fixtures, or internal SDK wrappers. Useful within your org but irrelevant (or even confusing) to the wider ecosystem.

Compliance and audit requirements -- Some organizations have legal or regulatory requirements about where code can live. A private feed on infrastructure you control (or that your cloud provider operates under your subscription) satisfies those requirements far better than a public package registry.

The private feed solves a simple problem: distributing compiled code reliably inside a boundary you define, without exposing it outside that boundary.

Azure Artifacts as a Private NuGet Feed

Azure Artifacts is part of Azure DevOps -- Microsoft's suite of developer services. If your team already uses Azure DevOps for source control, work tracking, or pipelines, Azure Artifacts is the natural first choice because it lives in the same ecosystem.

Feed Scopes: Organization vs Project

Azure Artifacts supports two scopes for feeds.

An organization-scoped feed is accessible to all projects inside your Azure DevOps organization. This is the right choice for shared internal packages that multiple teams need, like common logging libraries or shared API client code.

A project-scoped feed is scoped to a single Azure DevOps project. It's more isolated and easier to manage access for. Prefer it when the packages are specific to one product or team.

Feed URL Format

Once you create a feed in Azure Artifacts, the NuGet v3 source URL follows this pattern:

https://pkgs.dev.azure.com/{org}/{project}/_packaging/{feedName}/nuget/v3/index.json

For an organization-scoped feed (no project), drop the /{project} segment:

https://pkgs.dev.azure.com/{org}/_packaging/{feedName}/nuget/v3/index.json

You'll use this URL in every NuGet configuration that targets this feed.

Upstream Sources

One Azure Artifacts feature worth knowing about is upstream sources. When you configure an upstream source, your private feed can proxy requests to NuGet.org (or another NuGet feed). This means all your developers restore packages from one source -- your private feed -- even when those packages are public. Azure Artifacts fetches and caches public packages on their behalf.

This simplifies corporate firewall configurations and gives you one auditable source for all packages in use, which can matter for compliance or security reviews.

Setting Up Authentication for Azure Artifacts

Azure Artifacts uses Microsoft Entra ID (formerly Azure Active Directory) for authentication. The simplest way to handle this locally is with the Azure Artifacts Credential Provider -- a plugin that handles interactive login and token refresh automatically.

Installing the Credential Provider

Run this in PowerShell:

iex "& { $(irm https://aka.ms/install-artifacts-credprovider.ps1) }"

This installs the MSAL-based credential provider, which handles modern Microsoft Entra ID authentication. After installation, the first time you restore packages from an Azure Artifacts feed, you'll see an interactive prompt to sign in with your Microsoft account or organizational credentials.

Adding the Feed Source with Credentials

For local development, you can store the feed URL and credentials in your user-level nuget.config:

dotnet nuget add source "https://pkgs.dev.azure.com/myorg/myproject/_packaging/MyFeed/nuget/v3/index.json" `
    --name "MyAzureArtifactsFeed" `
    --username "myuser" `
    --password "MY_PERSONAL_ACCESS_TOKEN" `
    --store-password-in-clear-text

The --store-password-in-clear-text flag is required when storing credentials in nuget.config. The PAT needs at minimum the Packaging (read) scope in Azure DevOps. For local developer machines the credential provider handles token management interactively, but storing a PAT this way is the most reliable approach when scripting setup for a new team member.

GitHub Packages as a Private NuGet Feed

GitHub Packages is the package hosting service built into GitHub. If your team uses GitHub for source control, GitHub Packages is a natural complement -- packages live alongside the repositories that produce them.

Key Differences from NuGet.org

Two things surprise developers new to GitHub Packages as a NuGet source.

First, the scope is per-owner (user or organization), not per-repository. The NuGet source URL is:

https://nuget.pkg.github.com/{OWNER}/index.json

This means all packages under your GitHub organization share one feed URL.

Second -- and this catches a lot of people -- GitHub Packages requires authentication even for reading packages, even packages that are technically public. You need a GitHub Personal Access Token (PAT) with the read:packages scope (or at minimum read:packages) to restore packages from any GitHub Packages feed.

This is different from NuGet.org, where anonymous reads are always allowed. Plan for this in your team's onboarding documentation.

Configuring nuget.config for Multiple Sources

Most real projects use both NuGet.org and one or more private feeds simultaneously. The nuget.config file is where you declare all of these sources in a consistent, version-controllable way.

A project-level nuget.config lives in the same directory as your .sln file (or at the root of the repo). NuGet picks it up automatically when restoring.

nuget.config with Azure Artifacts

<?xml version="1.0" encoding="utf-8"?>
<configuration>
  <packageSources>
    <!-- NuGet.org is always included by default, but being explicit is clearer -->
    <add key="nuget.org" value="https://api.nuget.org/v3/index.json" protocolVersion="3" />
    <!-- Your Azure Artifacts private feed -->
    <add key="MyAzureArtifactsFeed"
         value="https://pkgs.dev.azure.com/myorg/myproject/_packaging/MyFeed/nuget/v3/index.json" />
  </packageSources>

  <packageSourceCredentials>
    <MyAzureArtifactsFeed>
      <!-- In CI, these come from environment variables via the credential provider -->
      <!-- For local dev, the credential provider handles this interactively -->
      <add key="Username" value="optional-username" />
      <add key="ClearTextPassword" value="%AZURE_DEVOPS_TOKEN%" />
    </MyAzureArtifactsFeed>
  </packageSourceCredentials>
</configuration>

A few things to note here. The source name MyAzureArtifactsFeed under <packageSources> must exactly match the XML element name under <packageSourceCredentials>. Spaces in source names will break this -- use camelCase or PascalCase or hyphens.

The %AZURE_DEVOPS_TOKEN% syntax tells NuGet to read from an environment variable at restore time. This keeps the actual secret out of the file. For local development with the credential provider installed, you can often omit the <packageSourceCredentials> block entirely -- the provider handles authentication transparently.

nuget.config with GitHub Packages

<?xml version="1.0" encoding="utf-8"?>
<configuration>
  <packageSources>
    <add key="nuget.org" value="https://api.nuget.org/v3/index.json" protocolVersion="3" />
    <!-- GitHub Packages feed scoped to a GitHub organization or user -->
    <add key="GitHubPackages"
         value="https://nuget.pkg.github.com/MyGitHubOrg/index.json" />
  </packageSources>

  <packageSourceCredentials>
    <GitHubPackages>
      <!-- For GitHub Actions, set NUGET_AUTH_TOKEN to ${{ secrets.GITHUB_TOKEN }} -->
      <!-- For local dev, use a PAT with read:packages scope -->
      <add key="Username" value="x-access-token" />
      <add key="ClearTextPassword" value="%NUGET_AUTH_TOKEN%" />
    </GitHubPackages>
  </packageSourceCredentials>
</configuration>

The Username value x-access-token is the canonical static string that GitHub Packages accepts for both GITHUB_TOKEN and PAT-based authentication -- the token in ClearTextPassword is what is actually validated. GitHub ignores the username when using GITHUB_TOKEN, but PAT authentication requires a valid GitHub username; x-access-token is the safe static value that works in both cases.

The NUGET_AUTH_TOKEN name is a widely adopted convention (popularized by GitHub's own documentation) for this environment variable. You can use any name you like in the %...% placeholder in nuget.config -- as long as you inject the corresponding environment variable at restore time. In GitHub Actions, you set it equal to ${{ secrets.GITHUB_TOKEN }} (the built-in token) and NuGet reads it from the environment. This means you need zero additional secrets configured for consuming packages from your own organization's feed in Actions.

Consuming Private Packages in Visual Studio and CLI

Once your nuget.config is in place and authentication is working, consuming a package from your private NuGet feed works exactly like consuming any public package.

From the CLI

# Restore all packages, including private ones
dotnet restore

# Add a specific package from your private feed
dotnet add package MyCompany.SharedUtils --version 1.2.0

# If NuGet can't find it on NuGet.org first, you can specify the source explicitly
dotnet add package MyCompany.SharedUtils --source MyAzureArtifactsFeed

NuGet searches all configured sources in order when you don't specify --source. If NuGet.org doesn't have the package and your private feed does, it will be found. If you have multiple private feeds and only one has the package, specifying --source speeds things up.

From Visual Studio

The NuGet Package Manager in Visual Studio picks up all sources declared in nuget.config. You'll see your private feed listed in the source dropdown in the Browse tab. Just select it, search for your internal package name, and install as normal.

If you see authentication prompts, it means the credential provider hasn't authenticated yet for that session -- follow the prompt to sign in.

Handling Credentials in CI/CD Pipelines

Local credential providers that handle interactive login won't work in a headless CI environment. You need to pass credentials via environment variables or pipeline secrets instead.

GitHub Actions with GitHub Packages

This is the simplest scenario. GITHUB_TOKEN is automatically provided by GitHub Actions and has read:packages permission by default for packages hosted in the same organization. In repositories where the default token permissions have been restricted by your organization, you may need to explicitly add permissions: packages: read to your workflow job.

jobs:
  build:
    runs-on: ubuntu-latest
    permissions:
      packages: read
    steps:
      - uses: actions/checkout@v4

      - name: Setup .NET
        uses: actions/setup-dotnet@v4
        with:
          dotnet-version: '9.0.x'

      - name: Restore packages
        env:
          # GitHub Packages reads this env var from nuget.config via %NUGET_AUTH_TOKEN%
          NUGET_AUTH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
        run: dotnet restore

      - name: Build
        run: dotnet build --no-restore

The NUGET_AUTH_TOKEN environment variable in the env: block is what the %NUGET_AUTH_TOKEN% placeholder in your nuget.config resolves to. Your nuget.config sets Username to x-access-token and ClearTextPassword to %NUGET_AUTH_TOKEN% -- see the earlier nuget.config section for why x-access-token is the canonical static value for GitHub Packages authentication.

Azure Pipelines with Azure Artifacts

Azure Pipelines has first-class support for Azure Artifacts through the NuGetAuthenticate task, which handles the credential provider setup automatically without manual token management.

steps:
  - task: NuGetAuthenticate@1
    displayName: 'Authenticate with Azure Artifacts'

  - script: dotnet restore
    displayName: 'Restore packages'

The NuGetAuthenticate task injects credentials for all Azure Artifacts sources found in your nuget.config. You don't need to manually set tokens or configure the credential provider.

For other CI systems (Jenkins, GitLab CI, CircleCI), the pattern is consistent: set AZURE_DEVOPS_TOKEN (or whatever name you used in nuget.config) as a pipeline secret and inject it as an environment variable during the restore step. The credential provider reads it automatically.

Azure Artifacts vs GitHub Packages: Key Differences

Both platforms serve the same core need -- hosting private NuGet feeds -- but they make different trade-offs that matter depending on your team's setup.

Azure Artifacts GitHub Packages
Native integration Azure DevOps (Pipelines, Boards, Repos) GitHub (Actions, Issues, PRs)
Authentication Microsoft Entra ID / PAT + credential provider GitHub PAT (read:packages)
Read access Requires org membership or explicit sharing Required even for public packages
Upstream sources NuGet.org proxying supported Not supported
Pricing 2 GB free per org, then pay-as-you-go Included in GitHub plan (varies by tier)
CI credential tooling NuGetAuthenticate task GITHUB_TOKEN built-in secret
Feed URL scope Per-feed (organization or project scoped) Per-owner (org or user)

Choose Azure Artifacts if:

  • Your team already works in Azure DevOps
  • You want upstream source proxying for NuGet.org (single-source restore)
  • You need project-level feed isolation with fine-grained permissions

Choose GitHub Packages if:

  • Your team lives in GitHub
  • Your packages are closely tied to specific GitHub repositories
  • You want minimal setup -- GITHUB_TOKEN just works in GitHub Actions

Both options work well for most teams. The choice usually comes down to where your source control and CI pipelines already live. Mixing and matching is also entirely valid -- you can have both sources configured in nuget.config at the same time.

One practical note on GitHub Packages: because every consumer needs a PAT with read:packages, GitHub Packages adds a small authentication burden for external consumers (people outside your organization). Azure Artifacts has better support for sharing packages with external parties through configurable visibility and upstream source sharing. This is usually not a factor for purely internal packages, but it's worth knowing if your distribution requirements might expand.

Wrapping Up

A private NuGet feed solves a real problem that NuGet.org can't: distributing your proprietary or not-yet-public packages securely within your organization. Azure Artifacts and GitHub Packages are the two dominant hosted options for .NET teams, and both integrate naturally into their respective CI/CD ecosystems.

The key things to remember:

  • Azure Artifacts feed URLs follow the pkgs.dev.azure.com/{org}/{project}/_packaging/{feed}/nuget/v3/index.json pattern; install the credential provider for local dev.
  • GitHub Packages always requires authentication, even for public packages -- use a PAT with read:packages locally and GITHUB_TOKEN in Actions.
  • Your nuget.config can declare multiple sources simultaneously, letting you mix NuGet.org, Azure Artifacts, and GitHub Packages in one project without conflict.
  • In CI, use environment variable placeholders (%TOKEN_NAME%) in nuget.config and inject secrets at pipeline runtime -- never commit real tokens.

For more on building the packages that go into these feeds, see The Complete Guide to Creating NuGet Packages in .NET. When your internal package eventually earns a public release, How to Publish a NuGet Package to NuGet.org covers that workflow end-to-end.


Frequently Asked Questions

Do I need a separate nuget.config per project, or can one file cover the whole repo?

You can place a single nuget.config at the repository root (same level as your .sln file) and it applies to every project in the repo. NuGet searches for configuration files by walking up the directory tree from the project being restored. A repo-root nuget.config is the standard approach for teams -- it keeps all source declarations in one version-controlled location and avoids duplicating configuration across projects.

Can I use both Azure Artifacts and GitHub Packages as NuGet sources at the same time?

Yes. nuget.config supports as many <packageSources> entries as you need. NuGet queries each source in order when searching for a package. You'll need separate <packageSourceCredentials> entries for each authenticated source, each with its own environment variable placeholder. The source names must be unique and must exactly match the credential element names.

Why does GitHub Packages require authentication even for packages marked as public?

This is a deliberate policy decision by GitHub. Unlike NuGet.org -- which allows anonymous reads -- GitHub Packages requires a valid authentication token for all package downloads. The reasoning is that even "public" packages on GitHub Packages are still tied to a GitHub account's storage quota and usage metrics. A PAT with read:packages scope is the minimum requirement. For CI environments, GITHUB_TOKEN covers this automatically with no extra configuration.

What is the Azure Artifacts Credential Provider and do I need it?

The Azure Artifacts Credential Provider is a NuGet plugin that handles Microsoft Entra ID authentication automatically. Without it, you'd need to manually generate and manage PATs and update nuget.config with credentials for every developer. With it installed, the first restore from an Azure Artifacts feed prompts for interactive Microsoft Entra ID login, then caches the token. It's essentially mandatory for any team using Azure Artifacts in a local development context. In CI, NuGetAuthenticate@1 (Azure Pipelines) handles authentication without requiring the provider to be separately installed.

What happens if NuGet can't find a package -- does it check all sources?

By default, NuGet queries all configured sources and returns the package from whichever source has it (choosing the highest version if multiple sources have the package). If you want to restrict which source a specific package comes from, you can use nuget.config's <packageSourceMapping> feature (introduced in NuGet 6.0). This lets you pin packages by ID prefix to a specific source, which prevents accidental resolution from the wrong feed -- a useful security measure called dependency confusion protection.

Is there a way to use a private NuGet feed without storing credentials in nuget.config at all?

Yes -- for Azure Artifacts specifically, the credential provider handles authentication entirely outside of nuget.config. If the credential provider is installed and you're logged into Azure, it intercepts NuGet restore requests to Azure Artifacts feeds and injects credentials from your cached token. This means you can have the feed URL in nuget.config with no credentials block at all, and it just works locally. For CI, environment variable injection remains the standard approach regardless of the credential provider.

How do I share a private NuGet feed with developers outside my organization?

Azure Artifacts supports guest access and external contributor sharing through Azure DevOps organization settings. You can grant a specific Microsoft account read access to a feed without giving them broader organization access. GitHub Packages is more restrictive -- external users need a GitHub account and must be granted access to your packages explicitly, or the package must be public (while still requiring a PAT for authentication). For distributing packages to a wide external audience securely, a self-hosted feed solution or simply publishing to NuGet.org (if the package can be public) are worth evaluating.


What Is a NuGet Package? .nupkg Format and the NuGet Registry Explained

Understand what a NuGet package is, how the .nupkg format works, how the NuGet registry operates, and how package restore works in .NET projects.

How to Publish a NuGet Package to NuGet.org

Learn how to publish NuGet package files to NuGet.org using dotnet nuget push. Covers API keys, nuget.config setup, the validation queue, and unlisting.

The Complete Guide to Creating NuGet Packages in .NET

Learn how to create, version, and publish NuGet packages in .NET. This complete guide covers dotnet pack, metadata, versioning, publishing, CI/CD, and more.

An error has occurred. This application may no longer respond until reloaded. Reload